@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.
- package/lib/shipload.d.ts +754 -316
- package/lib/shipload.js +2650 -1336
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +2564 -1308
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +219 -145
- package/lib/testing.js +910 -557
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +910 -557
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/capabilities/crafting.test.ts +51 -6
- package/src/capabilities/crafting.ts +19 -4
- package/src/capabilities/gathering.ts +2 -2
- package/src/capabilities/storage.test.ts +26 -0
- package/src/capabilities/storage.ts +6 -6
- package/src/contracts/fund.ts +110 -5
- package/src/contracts/platform.ts +10 -1
- package/src/contracts/server.ts +498 -331
- package/src/data/charters.json +325 -127
- package/src/data/entities.json +24 -24
- package/src/data/items.json +114 -114
- package/src/data/kind-registry.json +3 -3
- package/src/data/kind-registry.ts +5 -0
- package/src/data/recipes.json +56 -95
- package/src/derivation/build-methods.test.ts +59 -1
- package/src/derivation/build-methods.ts +2 -0
- package/src/derivation/capabilities.ts +7 -7
- package/src/derivation/strata.ts +5 -3
- package/src/derivation/tiers.ts +5 -5
- package/src/entity/hydrate.test.ts +156 -0
- package/src/entity/hydrate.ts +86 -0
- package/src/errors.ts +9 -0
- package/src/format.ts +15 -13
- package/src/index-module.ts +33 -5
- package/src/influence/ballot.test.ts +103 -0
- package/src/influence/ballot.ts +98 -0
- package/src/influence/charter-metadata.test.ts +136 -0
- package/src/influence/charter-metadata.ts +236 -0
- package/src/influence/charters.test.ts +51 -65
- package/src/influence/charters.ts +70 -40
- package/src/influence/civic.test.ts +278 -0
- package/src/influence/civic.ts +269 -0
- package/src/influence/constants.ts +30 -9
- package/src/influence/decomp.ts +15 -15
- package/src/influence/duration.ts +42 -6
- package/src/influence/index.ts +49 -6
- package/src/influence/influence.test.ts +76 -9
- package/src/influence/valuation.ts +17 -17
- package/src/managers/actions.ts +65 -37
- package/src/managers/base.ts +4 -0
- package/src/managers/context.ts +3 -1
- package/src/managers/index.ts +5 -2
- package/src/managers/influence.test.ts +18 -20
- package/src/managers/influence.ts +162 -84
- package/src/managers/jobs.test.ts +2 -2
- package/src/managers/jobs.ts +4 -4
- package/src/nft/description.ts +10 -11
- package/src/planner/index.ts +54 -0
- package/src/planner/planner.test.ts +127 -2
- package/src/resolution/display-name.ts +1 -1
- package/src/scheduling/availability.test.ts +113 -0
- package/src/scheduling/availability.ts +48 -8
- package/src/scheduling/jobs.ts +1 -1
- package/src/scheduling/projection.ts +2 -16
- package/src/scheduling/task-cargo.test.ts +62 -1
- package/src/scheduling/task-cargo.ts +25 -10
- package/src/scheduling/task-effects.test.ts +35 -0
- package/src/scheduling/task-effects.ts +34 -0
- package/src/scheduling/unwrap.test.ts +3 -3
- package/src/scheduling/unwrap.ts +3 -3
- package/src/shipload.ts +23 -4
- package/src/travel/reach.test.ts +86 -0
- package/src/travel/reach.ts +23 -10
- package/src/travel/route-build.test.ts +63 -0
- package/src/travel/route-build.ts +60 -0
- package/src/travel/travel.ts +2 -1
- package/src/types.ts +6 -2
- package/src/utils/hash.ts +18 -2
package/src/derivation/strata.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
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(
|
|
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,
|
package/src/derivation/tiers.ts
CHANGED
|
@@ -8,11 +8,11 @@ export interface TierRange {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export const RESERVE_TIERS: Record<ReserveTier, TierRange> = {
|
|
11
|
-
small: {min:
|
|
12
|
-
medium: {min:
|
|
13
|
-
large: {min:
|
|
14
|
-
massive: {min:
|
|
15
|
-
motherlode: {min:
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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(
|
|
13
|
-
if (
|
|
14
|
-
const sign =
|
|
15
|
-
return `${sign}${formatMass(Math.abs(
|
|
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(
|
|
27
|
-
if (
|
|
28
|
-
const sign =
|
|
29
|
-
const tonnes = Math.abs(
|
|
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(
|
|
35
|
+
return formatMass(mass)
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function formatInfluence(atomic: bigint | number | string): string {
|
package/src/index-module.ts
CHANGED
|
@@ -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'
|
|
@@ -61,9 +62,12 @@ export type {
|
|
|
61
62
|
ContributePreview,
|
|
62
63
|
ContributePreviewRow,
|
|
63
64
|
FoundedWorld,
|
|
65
|
+
FoundedWorldRef,
|
|
64
66
|
InfluenceStanding,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
Ballot,
|
|
68
|
+
BallotVote,
|
|
69
|
+
NodeProgress,
|
|
70
|
+
QueuedSeat,
|
|
67
71
|
VoteOption,
|
|
68
72
|
VoteStandings,
|
|
69
73
|
PlayerRosterEntry,
|
|
@@ -98,6 +102,7 @@ export {resolveLockedAmount, wrapCostKey} from './managers/nft'
|
|
|
98
102
|
export {
|
|
99
103
|
getItem,
|
|
100
104
|
getItems,
|
|
105
|
+
tryGetItem,
|
|
101
106
|
itemIds,
|
|
102
107
|
getResources,
|
|
103
108
|
getComponents,
|
|
@@ -257,6 +262,12 @@ export type {ReachStats} from './travel/reach'
|
|
|
257
262
|
export {simulateRoute} from './travel/route-simulator'
|
|
258
263
|
export type {RouteMoverInput, RouteLegSim, RouteSim} from './travel/route-simulator'
|
|
259
264
|
|
|
265
|
+
export {
|
|
266
|
+
planRouteWithRetry,
|
|
267
|
+
contractRouteLegCost,
|
|
268
|
+
contractRouteHeuristic,
|
|
269
|
+
} from './travel/route-build'
|
|
270
|
+
|
|
260
271
|
export * as schedule from './scheduling/schedule'
|
|
261
272
|
export {LANE_MOBILITY, LANE_BARRIER} from './scheduling/schedule'
|
|
262
273
|
export type {
|
|
@@ -303,8 +314,8 @@ export type {
|
|
|
303
314
|
ProjectionOptions,
|
|
304
315
|
} from './scheduling/projection'
|
|
305
316
|
|
|
306
|
-
export {taskCargoChanges} from './scheduling/task-cargo'
|
|
307
|
-
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'
|
|
308
319
|
|
|
309
320
|
export {
|
|
310
321
|
jobsToLanes,
|
|
@@ -345,12 +356,13 @@ export {
|
|
|
345
356
|
cargoReadyAt,
|
|
346
357
|
taskCargoEffect,
|
|
347
358
|
calcCounterpartDelivery,
|
|
359
|
+
incomingSources,
|
|
348
360
|
cargoKey,
|
|
349
361
|
cargoInputKey,
|
|
350
362
|
hasSourceCoupling,
|
|
351
363
|
hasIncomingCoupling,
|
|
352
364
|
} from './scheduling/availability'
|
|
353
|
-
export type {CargoInput, IncomingSource} from './scheduling/availability'
|
|
365
|
+
export type {CargoInput, IncomingHoldEntity, IncomingSource} from './scheduling/availability'
|
|
354
366
|
|
|
355
367
|
export {clusterStockAvailable} from './scheduling/cluster-stock'
|
|
356
368
|
|
|
@@ -384,6 +396,10 @@ export {
|
|
|
384
396
|
CAP_UNDEPLOY,
|
|
385
397
|
CAP_DEMOLISH,
|
|
386
398
|
CAP_MODULES,
|
|
399
|
+
CAP_CATCH,
|
|
400
|
+
CAP_STORE_CARGO,
|
|
401
|
+
canCatch,
|
|
402
|
+
canStoreCargo,
|
|
387
403
|
isShip,
|
|
388
404
|
isWarehouse,
|
|
389
405
|
isExtractor,
|
|
@@ -655,17 +671,29 @@ export {
|
|
|
655
671
|
gatherEnergyCost,
|
|
656
672
|
maxQtyForCharge,
|
|
657
673
|
planParallelTransfer,
|
|
674
|
+
planSingleGather,
|
|
658
675
|
splitCost,
|
|
659
676
|
} from './planner'
|
|
660
677
|
export type {
|
|
661
678
|
BuildGatherPlanOpts,
|
|
662
679
|
FillCap,
|
|
663
680
|
GatherCycle,
|
|
681
|
+
GatherLane,
|
|
664
682
|
GatherLimpet,
|
|
665
683
|
GatherPlan,
|
|
666
684
|
GatherPlanEntity,
|
|
667
685
|
LanePlanEntry,
|
|
668
686
|
PlanTarget,
|
|
687
|
+
SingleGatherInput,
|
|
688
|
+
SingleGatherPlan,
|
|
669
689
|
} from './planner'
|
|
670
690
|
|
|
691
|
+
export {entityToRouteMover, hydrateEntityLanes} from './entity/hydrate'
|
|
692
|
+
export type {
|
|
693
|
+
GathererLaneSource,
|
|
694
|
+
RouteMoverCaps,
|
|
695
|
+
RouteMoverOpts,
|
|
696
|
+
RouteMoverRow,
|
|
697
|
+
} from './entity/hydrate'
|
|
698
|
+
|
|
671
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
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {CHARTER_REGISTRY, charterEligibleChained, type CharterWorld} from './charters'
|
|
2
|
+
import {CHARTER_NONE} from './constants'
|
|
3
|
+
|
|
4
|
+
export interface BallotVoter {
|
|
5
|
+
account: string
|
|
6
|
+
weight: bigint
|
|
7
|
+
picks: number[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Projects a full settlement from scratch; it does not resume an in-flight partial round. */
|
|
11
|
+
export interface BallotInput {
|
|
12
|
+
world: CharterWorld
|
|
13
|
+
seats: number
|
|
14
|
+
voters: BallotVoter[]
|
|
15
|
+
picks?: number[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProjectedSeat {
|
|
19
|
+
nodeId: number
|
|
20
|
+
weight: bigint
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProjectedOption {
|
|
24
|
+
nodeId: number
|
|
25
|
+
cost: bigint
|
|
26
|
+
seat: number
|
|
27
|
+
weight: bigint
|
|
28
|
+
rank: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BallotProjection {
|
|
32
|
+
seats: ProjectedSeat[]
|
|
33
|
+
options: ProjectedOption[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tallyRound(input: BallotInput, seated: number[]): Map<number, bigint> {
|
|
37
|
+
const tallies = new Map<number, bigint>()
|
|
38
|
+
for (const voter of input.voters) {
|
|
39
|
+
for (const pick of voter.picks) {
|
|
40
|
+
const node = CHARTER_REGISTRY.find((n) => n.nodeId === pick)
|
|
41
|
+
if (!node || !charterEligibleChained(input.world, node, seated)) continue
|
|
42
|
+
if (voter.weight > 0n) tallies.set(pick, (tallies.get(pick) ?? 0n) + voter.weight)
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return tallies
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function leader(tallies: Map<number, bigint>): ProjectedSeat | undefined {
|
|
50
|
+
let winner = CHARTER_NONE
|
|
51
|
+
let best = 0n
|
|
52
|
+
for (const [option, weight] of tallies) {
|
|
53
|
+
if (weight === 0n) continue
|
|
54
|
+
if (winner === CHARTER_NONE || weight > best || (weight === best && option < winner)) {
|
|
55
|
+
best = weight
|
|
56
|
+
winner = option
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return winner === CHARTER_NONE ? undefined : {nodeId: winner, weight: best}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function projectBallot(input: BallotInput): BallotProjection {
|
|
63
|
+
const seats: ProjectedSeat[] = []
|
|
64
|
+
while (seats.length < input.seats) {
|
|
65
|
+
const seat = leader(
|
|
66
|
+
tallyRound(
|
|
67
|
+
input,
|
|
68
|
+
seats.map((s) => s.nodeId)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
if (!seat) break
|
|
72
|
+
seats.push(seat)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const projected = seats.map((s) => s.nodeId)
|
|
76
|
+
const next = tallyRound(input, projected)
|
|
77
|
+
const picks = input.picks ?? []
|
|
78
|
+
|
|
79
|
+
const options: ProjectedOption[] = []
|
|
80
|
+
for (const node of CHARTER_REGISTRY) {
|
|
81
|
+
const seatIndex = projected.indexOf(node.nodeId)
|
|
82
|
+
const option: ProjectedOption = {
|
|
83
|
+
nodeId: node.nodeId,
|
|
84
|
+
cost: node.cost,
|
|
85
|
+
seat: seatIndex + 1,
|
|
86
|
+
weight: 0n,
|
|
87
|
+
rank: picks.indexOf(node.nodeId) + 1,
|
|
88
|
+
}
|
|
89
|
+
if (seatIndex >= 0) {
|
|
90
|
+
option.weight = seats[seatIndex].weight
|
|
91
|
+
} else {
|
|
92
|
+
if (!charterEligibleChained(input.world, node, projected)) continue
|
|
93
|
+
option.weight = next.get(node.nodeId) ?? 0n
|
|
94
|
+
}
|
|
95
|
+
options.push(option)
|
|
96
|
+
}
|
|
97
|
+
return {seats, options}
|
|
98
|
+
}
|