@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.
- package/lib/shipload.d.ts +786 -321
- package/lib/shipload.js +2731 -1305
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +2644 -1286
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +223 -146
- package/lib/testing.js +907 -563
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +907 -563
- 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 +79 -2
- package/src/contracts/platform.ts +10 -1
- package/src/contracts/server.ts +521 -330
- package/src/data/charters.json +325 -127
- package/src/data/entities.json +29 -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/metadata.ts +9 -2
- 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 +32 -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-shuttle.test.ts +159 -0
- package/src/influence/civic-shuttle.ts +115 -0
- package/src/influence/civic.test.ts +287 -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 +59 -6
- package/src/influence/influence.test.ts +76 -9
- package/src/influence/valuation.ts +17 -17
- package/src/managers/actions.ts +99 -37
- package/src/managers/base.ts +4 -0
- package/src/managers/context.ts +3 -1
- package/src/managers/epochs.ts +32 -1
- package/src/managers/index.ts +4 -2
- package/src/managers/influence.test.ts +18 -20
- package/src/managers/influence.ts +137 -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/epoch.ts +1 -0
- 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 +22 -3
- 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/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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
705
|
+
capacity: Math.floor(220000 * 6 ** exponent),
|
|
706
706
|
}
|
|
707
707
|
}
|
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'
|
|
@@ -63,8 +64,10 @@ export type {
|
|
|
63
64
|
FoundedWorld,
|
|
64
65
|
FoundedWorldRef,
|
|
65
66
|
InfluenceStanding,
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
})
|