@shipload/sdk 1.0.0-next.7 → 1.0.0-next.71
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/scan.d.ts +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +5822 -1484
- package/lib/shipload.js +20838 -5419
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +20674 -5669
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1691 -0
- package/lib/testing.js +7109 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +7103 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.test.ts +82 -0
- package/src/capabilities/craftable.ts +82 -0
- package/src/capabilities/crafting.test.ts +80 -0
- package/src/capabilities/crafting.ts +50 -8
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +38 -20
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +27 -30
- package/src/capabilities/movement.ts +1 -1
- package/src/capabilities/storage.test.ts +26 -0
- package/src/capabilities/storage.ts +22 -7
- package/src/contracts/fund.ts +306 -0
- package/src/contracts/platform.ts +312 -3
- package/src/contracts/server.ts +2793 -478
- package/src/contracts/token.ts +170 -0
- package/src/contracts.ts +2 -0
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +49 -18
- package/src/data/capability-formulas.ts +26 -14
- package/src/data/catalog.test.ts +19 -0
- package/src/data/catalog.ts +13 -3
- package/src/data/charters.json +396 -0
- package/src/data/citizenry-adjectives.json +42 -0
- package/src/data/citizenry-nouns.json +50 -0
- package/src/data/colors.ts +14 -32
- package/src/data/entities.json +421 -15
- package/src/data/item-ids.ts +53 -13
- package/src/data/items.json +330 -77
- package/src/data/kind-registry.json +212 -0
- package/src/data/kind-registry.ts +192 -0
- package/src/data/metadata.ts +312 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2173 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +71 -0
- package/src/derivation/build-methods.ts +50 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +707 -0
- package/src/derivation/capability-mappings.ts +60 -16
- package/src/derivation/crafting.test.ts +17 -0
- package/src/derivation/crafting.ts +57 -34
- package/src/derivation/index.ts +27 -2
- package/src/derivation/recipe-usage.test.ts +88 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +9 -1
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +72 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/strata.ts +5 -3
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +154 -0
- package/src/entities/entity.ts +102 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +144 -136
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/entity/hydrate.test.ts +156 -0
- package/src/entity/hydrate.ts +86 -0
- package/src/errors.ts +21 -16
- package/src/format.ts +42 -9
- package/src/index-module.ts +425 -32
- package/src/influence/ballot.test.ts +103 -0
- package/src/influence/ballot.ts +98 -0
- package/src/influence/categories.ts +32 -0
- package/src/influence/charter-metadata.test.ts +136 -0
- package/src/influence/charter-metadata.ts +236 -0
- package/src/influence/charters.test.ts +112 -0
- package/src/influence/charters.ts +150 -0
- package/src/influence/citizenry.ts +38 -0
- package/src/influence/civic-shuttle.test.ts +202 -0
- package/src/influence/civic-shuttle.ts +142 -0
- package/src/influence/civic.test.ts +287 -0
- package/src/influence/civic.ts +269 -0
- package/src/influence/constants.ts +84 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +101 -0
- package/src/influence/index.ts +118 -0
- package/src/influence/influence.test.ts +450 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +950 -93
- package/src/managers/balances.test.ts +161 -0
- package/src/managers/balances.ts +93 -0
- package/src/managers/base.ts +10 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +453 -0
- package/src/managers/context.ts +58 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +35 -63
- package/src/managers/epochs.ts +72 -1
- package/src/managers/index.ts +38 -1
- package/src/managers/influence.test.ts +56 -0
- package/src/managers/influence.ts +425 -0
- package/src/managers/jobs.test.ts +96 -0
- package/src/managers/jobs.ts +58 -0
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +21 -0
- package/src/managers/nft.ts +110 -0
- package/src/managers/players.ts +25 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +388 -0
- package/src/nft/description.ts +222 -100
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +296 -0
- package/src/planner/planner.test.ts +459 -0
- package/src/receipts.ts +22 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +39 -11
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +112 -58
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.test.ts +320 -0
- package/src/scheduling/availability.ts +251 -0
- package/src/scheduling/cancel.test.ts +550 -0
- package/src/scheduling/cancel.ts +254 -0
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/epoch.ts +1 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.test.ts +448 -0
- package/src/scheduling/jobs.ts +343 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +279 -0
- package/src/scheduling/projection.ts +265 -149
- package/src/scheduling/schedule.test.ts +163 -0
- package/src/scheduling/schedule.ts +282 -116
- package/src/scheduling/task-cargo.test.ts +110 -0
- package/src/scheduling/task-cargo.ts +62 -0
- package/src/scheduling/task-effects.test.ts +35 -0
- package/src/scheduling/task-effects.ts +34 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +63 -4
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.test.ts +86 -0
- package/src/travel/reach.ts +34 -0
- package/src/travel/route-build.test.ts +63 -0
- package/src/travel/route-build.ts +60 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +190 -123
- package/src/types/capabilities.ts +37 -8
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +81 -23
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +2 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/hash.ts +22 -2
- package/src/utils/system.ts +24 -5
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- package/src/types/entity-traits.ts +0 -69
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {categoryFromIndex} from '../data/catalog'
|
|
2
|
+
import type {ResourceCategory} from '../types'
|
|
3
|
+
import {
|
|
4
|
+
RESOURCE_BIOMASS,
|
|
5
|
+
RESOURCE_CATEGORY_COUNT,
|
|
6
|
+
RESOURCE_CRYSTAL,
|
|
7
|
+
RESOURCE_GAS,
|
|
8
|
+
RESOURCE_ORE,
|
|
9
|
+
RESOURCE_REGOLITH,
|
|
10
|
+
} from './constants'
|
|
11
|
+
|
|
12
|
+
const INDEX_BY_CATEGORY: Record<ResourceCategory, number> = {
|
|
13
|
+
ore: RESOURCE_ORE,
|
|
14
|
+
gas: RESOURCE_GAS,
|
|
15
|
+
regolith: RESOURCE_REGOLITH,
|
|
16
|
+
biomass: RESOURCE_BIOMASS,
|
|
17
|
+
crystal: RESOURCE_CRYSTAL,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function categoryIndex(category: ResourceCategory): number {
|
|
21
|
+
return INDEX_BY_CATEGORY[category]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function categoryAtIndex(index: number): ResourceCategory {
|
|
25
|
+
const category = categoryFromIndex(index)
|
|
26
|
+
if (!category) throw new Error(`influence: invalid resource category index ${index}`)
|
|
27
|
+
return category
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function allCategoryIndexes(): number[] {
|
|
31
|
+
return Array.from({length: RESOURCE_CATEGORY_COUNT}, (_, i) => i)
|
|
32
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {CHARTER_REGISTRY, type CharterNode} from './charters'
|
|
4
|
+
import {
|
|
5
|
+
charterMetadata,
|
|
6
|
+
charterName,
|
|
7
|
+
charterSignature,
|
|
8
|
+
charterSummary,
|
|
9
|
+
romanNumeral,
|
|
10
|
+
type CharterSignature,
|
|
11
|
+
} from './charter-metadata'
|
|
12
|
+
|
|
13
|
+
function nodeById(nodeId: number): CharterNode {
|
|
14
|
+
const node = CHARTER_REGISTRY.find((candidate) => candidate.nodeId === nodeId)
|
|
15
|
+
if (!node) throw new Error(`no charter node ${nodeId} in the registry`)
|
|
16
|
+
return node
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function signatureFor(nodeId: number): CharterSignature {
|
|
20
|
+
const signature = charterSignature(nodeById(nodeId))
|
|
21
|
+
if (!signature) throw new Error(`no signature for charter node ${nodeId}`)
|
|
22
|
+
return signature
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sharesWord(name: string, label: string): boolean {
|
|
26
|
+
const haystack = name.toLowerCase()
|
|
27
|
+
return label
|
|
28
|
+
.split(/\s+/)
|
|
29
|
+
.filter((word) => word.length > 2)
|
|
30
|
+
.some((word) => haystack.includes(word.toLowerCase()))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('charter metadata', () => {
|
|
34
|
+
test('covers every node in the chain-synced registry', () => {
|
|
35
|
+
const missing = CHARTER_REGISTRY.filter((node) => !charterMetadata[node.nodeId])
|
|
36
|
+
expect(missing).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('carries no entry for a node the registry does not define', () => {
|
|
40
|
+
const known = new Set(CHARTER_REGISTRY.map((node) => node.nodeId))
|
|
41
|
+
const orphans = Object.keys(charterMetadata)
|
|
42
|
+
.map(Number)
|
|
43
|
+
.filter((nodeId) => !known.has(nodeId))
|
|
44
|
+
expect(orphans).toEqual([])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('every entry has a name and a summary', () => {
|
|
48
|
+
for (const node of CHARTER_REGISTRY) {
|
|
49
|
+
expect(charterName(node.nodeId).length).toBeGreaterThan(0)
|
|
50
|
+
expect(charterSummary(node.nodeId).length).toBeGreaterThan(0)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('falls back to a node-numbered name for an unknown id', () => {
|
|
55
|
+
expect(charterName(9999)).toBe('Charter 9999')
|
|
56
|
+
expect(charterSummary(9999)).toBe('')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('charter names agree with the grant they are derived from', () => {
|
|
61
|
+
for (const node of CHARTER_REGISTRY) {
|
|
62
|
+
const signature = signatureFor(node.nodeId)
|
|
63
|
+
const name = charterName(node.nodeId)
|
|
64
|
+
|
|
65
|
+
test(`node ${node.nodeId} names the building it grants (${signature.buildingLabel})`, () => {
|
|
66
|
+
expect(sharesWord(name, signature.buildingLabel)).toBeTrue()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (signature.kind !== 'gate' && signature.rankCount > 1) {
|
|
70
|
+
test(`node ${node.nodeId} carries rank ${signature.rank} of ${signature.rankCount}`, () => {
|
|
71
|
+
expect(name.endsWith(` ${romanNumeral(signature.rank)}`)).toBeTrue()
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('charter signatures', () => {
|
|
78
|
+
test('reads the depot level gate from its grant list', () => {
|
|
79
|
+
expect(signatureFor(40100001)).toEqual({kind: 'gate', buildingLabel: 'Depot', level: 1})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('ranks the depot bays by their prereq chain', () => {
|
|
83
|
+
expect([40100101, 40100102, 40100103, 40100104].map(signatureFor)).toEqual(
|
|
84
|
+
[1, 2, 3, 4].map((rank) => ({
|
|
85
|
+
kind: 'module',
|
|
86
|
+
buildingLabel: 'Depot',
|
|
87
|
+
count: 1,
|
|
88
|
+
rank,
|
|
89
|
+
rankCount: 4,
|
|
90
|
+
}))
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('ranks the depot transfer rungs by their prereq chain', () => {
|
|
95
|
+
expect([40100401, 40100402, 40100403, 40100404].map(signatureFor)).toEqual([
|
|
96
|
+
{
|
|
97
|
+
kind: 'rung',
|
|
98
|
+
buildingLabel: 'Depot',
|
|
99
|
+
statLabel: 'transfer speed',
|
|
100
|
+
rank: 1,
|
|
101
|
+
rankCount: 4,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
kind: 'rung',
|
|
105
|
+
buildingLabel: 'Depot',
|
|
106
|
+
statLabel: 'transfer speed',
|
|
107
|
+
rank: 2,
|
|
108
|
+
rankCount: 4,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
kind: 'rung',
|
|
112
|
+
buildingLabel: 'Depot',
|
|
113
|
+
statLabel: 'transfer speed',
|
|
114
|
+
rank: 3,
|
|
115
|
+
rankCount: 4,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
kind: 'rung',
|
|
119
|
+
buildingLabel: 'Depot',
|
|
120
|
+
statLabel: 'transfer speed',
|
|
121
|
+
rank: 4,
|
|
122
|
+
rankCount: 4,
|
|
123
|
+
},
|
|
124
|
+
])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('ranks the Workshop tune-ups as a four-rung ladder', () => {
|
|
128
|
+
expect(signatureFor(10100203)).toEqual({
|
|
129
|
+
kind: 'rung',
|
|
130
|
+
buildingLabel: 'Workshop',
|
|
131
|
+
statLabel: 'crafting speed',
|
|
132
|
+
rank: 3,
|
|
133
|
+
rankCount: 4,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {CHARTER_REGISTRY, type CharterGrant, type CharterNode} from './charters'
|
|
2
|
+
import {
|
|
3
|
+
CHARTER_NONE,
|
|
4
|
+
CIVIC_DEPOT,
|
|
5
|
+
CIVIC_DOCK,
|
|
6
|
+
CIVIC_GRANT_LEVEL_GATE,
|
|
7
|
+
CIVIC_GRANT_MODULE,
|
|
8
|
+
CIVIC_GRANT_RUNG,
|
|
9
|
+
CIVIC_NEXUS,
|
|
10
|
+
CIVIC_STAT_BUILD_SPEED,
|
|
11
|
+
CIVIC_STAT_CRAFT_SPEED,
|
|
12
|
+
CIVIC_STAT_STORAGE_CAPACITY,
|
|
13
|
+
CIVIC_STAT_TRANSFER_SPEED,
|
|
14
|
+
CIVIC_WORKSHOP,
|
|
15
|
+
} from './constants'
|
|
16
|
+
|
|
17
|
+
const BUILDING_LABELS: Record<number, string> = {
|
|
18
|
+
[CIVIC_WORKSHOP]: 'Workshop',
|
|
19
|
+
[CIVIC_NEXUS]: 'Nexus',
|
|
20
|
+
[CIVIC_DOCK]: 'Construction Dock',
|
|
21
|
+
[CIVIC_DEPOT]: 'Depot',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const STAT_LABELS: Record<number, string> = {
|
|
25
|
+
[CIVIC_STAT_CRAFT_SPEED]: 'crafting speed',
|
|
26
|
+
[CIVIC_STAT_BUILD_SPEED]: 'build speed',
|
|
27
|
+
[CIVIC_STAT_TRANSFER_SPEED]: 'transfer speed',
|
|
28
|
+
[CIVIC_STAT_STORAGE_CAPACITY]: 'storage capacity',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function civicBuildingLabel(building: number): string {
|
|
32
|
+
return BUILDING_LABELS[building] ?? `Building ${building}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function civicStatLabel(stat: number): string {
|
|
36
|
+
return STAT_LABELS[stat] ?? `Stat ${stat}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CharterMeta {
|
|
40
|
+
name: string
|
|
41
|
+
summary: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const charterMetadata: Record<number, CharterMeta> = {
|
|
45
|
+
10001: {
|
|
46
|
+
name: 'Communal Workshop',
|
|
47
|
+
summary: 'Shared crafting for anyone at this world',
|
|
48
|
+
},
|
|
49
|
+
10100201: {
|
|
50
|
+
name: 'Workshop tune-up I',
|
|
51
|
+
summary: "Raises the Workshop's Fabricator stats",
|
|
52
|
+
},
|
|
53
|
+
10100202: {
|
|
54
|
+
name: 'Workshop tune-up II',
|
|
55
|
+
summary: "Raises the Workshop's Fabricator stats",
|
|
56
|
+
},
|
|
57
|
+
10100203: {
|
|
58
|
+
name: 'Workshop tune-up III',
|
|
59
|
+
summary: "Raises the Workshop's Fabricator stats",
|
|
60
|
+
},
|
|
61
|
+
10100204: {
|
|
62
|
+
name: 'Workshop tune-up IV',
|
|
63
|
+
summary: "Raises the Workshop's Fabricator stats",
|
|
64
|
+
},
|
|
65
|
+
20001: {
|
|
66
|
+
name: 'Civic Nexus',
|
|
67
|
+
summary: 'Advances the world to Tier II; wrap items for trade and unwrap them here',
|
|
68
|
+
},
|
|
69
|
+
30100001: {
|
|
70
|
+
name: 'Construction Dock',
|
|
71
|
+
summary: 'Ships queue here for upgrades',
|
|
72
|
+
},
|
|
73
|
+
30100301: {
|
|
74
|
+
name: 'Dock tune-up I',
|
|
75
|
+
summary: "Raises the Dock's Assembly Arm stats",
|
|
76
|
+
},
|
|
77
|
+
30100302: {
|
|
78
|
+
name: 'Dock tune-up II',
|
|
79
|
+
summary: "Raises the Dock's Assembly Arm stats",
|
|
80
|
+
},
|
|
81
|
+
30100303: {
|
|
82
|
+
name: 'Dock tune-up III',
|
|
83
|
+
summary: "Raises the Dock's Assembly Arm stats",
|
|
84
|
+
},
|
|
85
|
+
30100304: {
|
|
86
|
+
name: 'Dock tune-up IV',
|
|
87
|
+
summary: "Raises the Dock's Assembly Arm stats",
|
|
88
|
+
},
|
|
89
|
+
40100001: {
|
|
90
|
+
name: 'Public Depot',
|
|
91
|
+
summary: 'Storage at this world for every player',
|
|
92
|
+
},
|
|
93
|
+
40100101: {
|
|
94
|
+
name: 'Depot Cargo Hold I',
|
|
95
|
+
summary: 'Raises what each player can keep at the depot',
|
|
96
|
+
},
|
|
97
|
+
40100102: {
|
|
98
|
+
name: 'Depot Cargo Hold II',
|
|
99
|
+
summary: 'Raises what each player can keep at the depot',
|
|
100
|
+
},
|
|
101
|
+
40100103: {
|
|
102
|
+
name: 'Depot Cargo Hold III',
|
|
103
|
+
summary: 'Raises what each player can keep at the depot',
|
|
104
|
+
},
|
|
105
|
+
40100104: {
|
|
106
|
+
name: 'Depot Cargo Hold IV',
|
|
107
|
+
summary: 'Raises what each player can keep at the depot',
|
|
108
|
+
},
|
|
109
|
+
40100401: {
|
|
110
|
+
name: 'Depot Shuttle Bay I',
|
|
111
|
+
summary: 'Moves cargo in and out of the depot faster',
|
|
112
|
+
},
|
|
113
|
+
40100402: {
|
|
114
|
+
name: 'Depot Shuttle Bay II',
|
|
115
|
+
summary: 'Moves cargo in and out of the depot faster',
|
|
116
|
+
},
|
|
117
|
+
40100403: {
|
|
118
|
+
name: 'Depot Shuttle Bay III',
|
|
119
|
+
summary: 'Moves cargo in and out of the depot faster',
|
|
120
|
+
},
|
|
121
|
+
40100404: {
|
|
122
|
+
name: 'Depot Shuttle Bay IV',
|
|
123
|
+
summary: 'Moves cargo in and out of the depot faster',
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function charterName(nodeId: number): string {
|
|
128
|
+
return charterMetadata[nodeId]?.name ?? `Charter ${nodeId}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function charterSummary(nodeId: number): string {
|
|
132
|
+
return charterMetadata[nodeId]?.summary ?? ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface CharterGateSignature {
|
|
136
|
+
kind: 'gate'
|
|
137
|
+
buildingLabel: string
|
|
138
|
+
level: number
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface CharterModuleSignature {
|
|
142
|
+
kind: 'module'
|
|
143
|
+
buildingLabel: string
|
|
144
|
+
count: number
|
|
145
|
+
rank: number
|
|
146
|
+
rankCount: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface CharterRungSignature {
|
|
150
|
+
kind: 'rung'
|
|
151
|
+
buildingLabel: string
|
|
152
|
+
statLabel: string
|
|
153
|
+
rank: number
|
|
154
|
+
rankCount: number
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type CharterSignature = CharterGateSignature | CharterModuleSignature | CharterRungSignature
|
|
158
|
+
|
|
159
|
+
const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
|
|
160
|
+
|
|
161
|
+
export function romanNumeral(rank: number): string {
|
|
162
|
+
return ROMAN[rank - 1] ?? String(rank)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function headlineGrant(node: CharterNode): CharterGrant | undefined {
|
|
166
|
+
return (
|
|
167
|
+
node.grants.find((grant) => grant.kind === CIVIC_GRANT_LEVEL_GATE) ??
|
|
168
|
+
node.grants.find((grant) => grant.kind === CIVIC_GRANT_MODULE) ??
|
|
169
|
+
node.grants.find((grant) => grant.kind === CIVIC_GRANT_RUNG)
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function headlineSiblings(headline: CharterGrant): CharterNode[] {
|
|
174
|
+
return CHARTER_REGISTRY.filter((other) => {
|
|
175
|
+
const grant = headlineGrant(other)
|
|
176
|
+
return (
|
|
177
|
+
grant !== undefined &&
|
|
178
|
+
grant.kind === headline.kind &&
|
|
179
|
+
grant.building === headline.building &&
|
|
180
|
+
grant.stat === headline.stat
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function rungRank(node: CharterNode, siblings: CharterNode[]): number {
|
|
186
|
+
const byId = new Map(siblings.map((sibling) => [sibling.nodeId, sibling]))
|
|
187
|
+
const seen = new Set<number>([node.nodeId])
|
|
188
|
+
let current: CharterNode | undefined = node
|
|
189
|
+
let rank = 1
|
|
190
|
+
while (current) {
|
|
191
|
+
const previousId = current.prereqs.find((id) => byId.has(id) && !seen.has(id))
|
|
192
|
+
if (previousId === undefined) return rank
|
|
193
|
+
seen.add(previousId)
|
|
194
|
+
current = byId.get(previousId)
|
|
195
|
+
rank += 1
|
|
196
|
+
}
|
|
197
|
+
return rank
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function charterSignature(node: CharterNode): CharterSignature | undefined {
|
|
201
|
+
const grant = headlineGrant(node)
|
|
202
|
+
if (!grant) return undefined
|
|
203
|
+
if (grant.kind === CIVIC_GRANT_LEVEL_GATE) {
|
|
204
|
+
return {
|
|
205
|
+
kind: 'gate',
|
|
206
|
+
buildingLabel: civicBuildingLabel(grant.building),
|
|
207
|
+
level: grant.value,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const siblings = headlineSiblings(grant)
|
|
211
|
+
if (grant.kind === CIVIC_GRANT_MODULE) {
|
|
212
|
+
return {
|
|
213
|
+
kind: 'module',
|
|
214
|
+
buildingLabel: civicBuildingLabel(grant.building),
|
|
215
|
+
count: grant.value,
|
|
216
|
+
rank: rungRank(node, siblings),
|
|
217
|
+
rankCount: siblings.length,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
kind: 'rung',
|
|
222
|
+
buildingLabel: civicBuildingLabel(grant.building),
|
|
223
|
+
statLabel: civicStatLabel(grant.stat),
|
|
224
|
+
rank: rungRank(node, siblings),
|
|
225
|
+
rankCount: siblings.length,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const node of CHARTER_REGISTRY) {
|
|
230
|
+
if (node.nodeId === CHARTER_NONE) continue
|
|
231
|
+
if (!charterMetadata[node.nodeId]) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Missing charter metadata for node ${node.nodeId}. Add an entry to charter-metadata.ts.`
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|