@shipload/sdk 1.0.0-next.61 → 1.0.0-next.63
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 +1243 -59
- package/lib/shipload.js +3911 -1261
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +3812 -1265
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +391 -13
- package/lib/testing.js +1554 -114
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +1555 -115
- package/lib/testing.m.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/fund.ts +201 -0
- package/src/contracts/platform.ts +73 -1
- package/src/contracts/server.ts +946 -26
- package/src/contracts.ts +1 -0
- package/src/data/charters.json +198 -0
- package/src/data/entities.json +36 -0
- package/src/data/item-ids.ts +2 -0
- package/src/data/items.json +12 -0
- package/src/data/kind-registry.json +20 -3
- package/src/data/kind-registry.ts +5 -0
- package/src/data/metadata.ts +14 -0
- package/src/data/recipes.json +0 -48
- package/src/derivation/capabilities.ts +14 -0
- package/src/derivation/recipe-usage.test.ts +0 -2
- package/src/format.ts +9 -0
- package/src/index-module.ts +34 -3
- package/src/influence/categories.ts +32 -0
- package/src/influence/charters.test.ts +126 -0
- package/src/influence/charters.ts +120 -0
- package/src/influence/citizenry.ts +31 -0
- package/src/influence/constants.ts +63 -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 +36 -0
- package/src/influence/index.ts +63 -0
- package/src/influence/influence.test.ts +291 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +135 -0
- package/src/managers/construction.ts +11 -1
- package/src/managers/context.ts +18 -0
- package/src/managers/entities.ts +11 -0
- package/src/managers/index.ts +13 -0
- package/src/managers/influence.test.ts +58 -0
- package/src/managers/influence.ts +347 -0
- package/src/managers/jobs.test.ts +62 -0
- package/src/managers/jobs.ts +52 -0
- package/src/nft/description.ts +7 -0
- package/src/scheduling/jobs.test.ts +32 -0
- package/src/scheduling/jobs.ts +37 -0
- package/src/scheduling/task-cargo.ts +1 -0
- package/src/shipload.ts +10 -0
- package/src/types.ts +23 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {CHARTER_NONE} from './constants'
|
|
3
|
+
import {
|
|
4
|
+
charterEligible,
|
|
5
|
+
charterIneligible,
|
|
6
|
+
charterNode,
|
|
7
|
+
charterSingletonMandate,
|
|
8
|
+
charterSpawnNodeFor,
|
|
9
|
+
effectiveMandate,
|
|
10
|
+
eligibleCharters,
|
|
11
|
+
type BuiltCharter,
|
|
12
|
+
type CharterWorld,
|
|
13
|
+
} from './charters'
|
|
14
|
+
|
|
15
|
+
const WORKSHOP = 1
|
|
16
|
+
const NEXUS = 2
|
|
17
|
+
const DOCK = 3
|
|
18
|
+
const WORKSHOP_TUNEUP = 4
|
|
19
|
+
const DOCK_TUNEUP = 5
|
|
20
|
+
const DEPOT = 6
|
|
21
|
+
|
|
22
|
+
const WORKSHOP_ENTITY = 100n
|
|
23
|
+
const DOCK_ENTITY = 300n
|
|
24
|
+
const DEPOT_ENTITY = 600n
|
|
25
|
+
|
|
26
|
+
function world(built: BuiltCharter[], present?: bigint[]): CharterWorld {
|
|
27
|
+
if (!present) return {built}
|
|
28
|
+
return {built, entityExists: (id) => present.includes(id)}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function node(nodeId: number) {
|
|
32
|
+
const found = charterNode(nodeId)
|
|
33
|
+
if (!found) throw new Error(`registry is missing node ${nodeId}`)
|
|
34
|
+
return found
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function eligibleIds(w: CharterWorld): number[] {
|
|
38
|
+
return eligibleCharters(w).map((n) => n.nodeId)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('charter eligibility mirror', () => {
|
|
42
|
+
test('an unbuilt world offers only the root', () => {
|
|
43
|
+
expect(eligibleIds(world([]))).toEqual([WORKSHOP])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('the root is the mandate by the singleton rule with no ballot', () => {
|
|
47
|
+
expect(charterSingletonMandate(world([]))).toBe(WORKSHOP)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('completing the root opens the fork and closes the singleton path', () => {
|
|
51
|
+
const built = world([{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}])
|
|
52
|
+
expect(eligibleIds(built)).toEqual([NEXUS, DOCK, WORKSHOP_TUNEUP, DEPOT])
|
|
53
|
+
expect(charterSingletonMandate(built)).toBe(CHARTER_NONE)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('a completed charter reports already-taken', () => {
|
|
57
|
+
const built = world([{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}])
|
|
58
|
+
expect(charterIneligible(built, node(WORKSHOP))).toBe('already-taken')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('an unmet prereq reports prereq-missing', () => {
|
|
62
|
+
expect(charterIneligible(world([]), node(NEXUS))).toBe('prereq-missing')
|
|
63
|
+
expect(charterIneligible(world([]), node(DOCK_TUNEUP))).toBe('prereq-missing')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('a refit whose spawned target is gone reports refit-target-missing', () => {
|
|
67
|
+
const built = [{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}]
|
|
68
|
+
expect(charterEligible(world(built, [WORKSHOP_ENTITY]), node(WORKSHOP_TUNEUP))).toBe(true)
|
|
69
|
+
expect(charterIneligible(world(built, []), node(WORKSHOP_TUNEUP))).toBe(
|
|
70
|
+
'refit-target-missing'
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('a demolished target can empty an otherwise singleton eligible set', () => {
|
|
75
|
+
const built = [
|
|
76
|
+
{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY},
|
|
77
|
+
{nodeId: NEXUS, entityId: 200n},
|
|
78
|
+
{nodeId: DOCK, entityId: DOCK_ENTITY},
|
|
79
|
+
{nodeId: DOCK_TUNEUP, entityId: 0n},
|
|
80
|
+
{nodeId: DEPOT, entityId: DEPOT_ENTITY},
|
|
81
|
+
]
|
|
82
|
+
expect(charterSingletonMandate(world(built, [WORKSHOP_ENTITY, DOCK_ENTITY]))).toBe(
|
|
83
|
+
WORKSHOP_TUNEUP
|
|
84
|
+
)
|
|
85
|
+
expect(charterSingletonMandate(world(built, [DOCK_ENTITY]))).toBe(CHARTER_NONE)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('an absent entity predicate trusts the charter record', () => {
|
|
89
|
+
const built = world([{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}])
|
|
90
|
+
expect(charterEligible(built, node(WORKSHOP_TUNEUP))).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('refit targets resolve through the node that spawns the item', () => {
|
|
94
|
+
expect(charterSpawnNodeFor(node(WORKSHOP_TUNEUP).effect.targetItemId)?.nodeId).toBe(
|
|
95
|
+
WORKSHOP
|
|
96
|
+
)
|
|
97
|
+
expect(charterSpawnNodeFor(node(DOCK_TUNEUP).effect.targetItemId)?.nodeId).toBe(DOCK)
|
|
98
|
+
expect(charterSpawnNodeFor(0)).toBeUndefined()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('effective mandate', () => {
|
|
103
|
+
const forked = world([{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}])
|
|
104
|
+
|
|
105
|
+
test('a stored choice holds inside its own epoch', () => {
|
|
106
|
+
expect(effectiveMandate({chosen: DOCK, chosenEpoch: 7}, forked, 7)).toBe(DOCK)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('the same stored choice expires one epoch later', () => {
|
|
110
|
+
expect(effectiveMandate({chosen: DOCK, chosenEpoch: 7}, forked, 8)).toBe(CHARTER_NONE)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('an expired choice falls through to the singleton rule', () => {
|
|
114
|
+
const unbuilt = world([])
|
|
115
|
+
expect(effectiveMandate({chosen: NEXUS, chosenEpoch: 7}, unbuilt, 8)).toBe(WORKSHOP)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('a cleared pair reads the singleton rule', () => {
|
|
119
|
+
expect(effectiveMandate({chosen: CHARTER_NONE, chosenEpoch: 0}, world([]), 3)).toBe(
|
|
120
|
+
WORKSHOP
|
|
121
|
+
)
|
|
122
|
+
expect(effectiveMandate({chosen: CHARTER_NONE, chosenEpoch: 0}, forked, 3)).toBe(
|
|
123
|
+
CHARTER_NONE
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import chartersJson from '../data/charters.json'
|
|
2
|
+
import {CHARTER_EFFECT_REFIT_MODULES, CHARTER_EFFECT_SPAWN_ENTITY, CHARTER_NONE} from './constants'
|
|
3
|
+
|
|
4
|
+
export interface CharterEffect {
|
|
5
|
+
kind: number
|
|
6
|
+
itemId: number
|
|
7
|
+
targetItemId: number
|
|
8
|
+
slotMask: number
|
|
9
|
+
stat: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CharterNode {
|
|
13
|
+
nodeId: number
|
|
14
|
+
cost: bigint
|
|
15
|
+
prereqs: number[]
|
|
16
|
+
effect: CharterEffect
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const CHARTER_REGISTRY: CharterNode[] = chartersJson.nodes.map((node) => ({
|
|
20
|
+
nodeId: node.nodeId,
|
|
21
|
+
cost: BigInt(node.cost),
|
|
22
|
+
prereqs: [...node.prereqs],
|
|
23
|
+
effect: {...node.effect},
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
export function charterNode(nodeId: number): CharterNode | undefined {
|
|
27
|
+
if (nodeId === CHARTER_NONE) return undefined
|
|
28
|
+
return CHARTER_REGISTRY.find((n) => n.nodeId === nodeId)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CharterIneligibility = 'already-taken' | 'prereq-missing' | 'refit-target-missing'
|
|
32
|
+
|
|
33
|
+
export const CHARTER_INELIGIBILITY_MESSAGES: Record<CharterIneligibility, string> = {
|
|
34
|
+
'already-taken': 'charter is already completed at this world',
|
|
35
|
+
'prereq-missing': 'charter prerequisites are not completed',
|
|
36
|
+
'refit-target-missing': 'charter refit target not found at this world',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BuiltCharter {
|
|
40
|
+
nodeId: number
|
|
41
|
+
entityId: bigint
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CharterWorld {
|
|
45
|
+
built: BuiltCharter[]
|
|
46
|
+
entityExists?: (entityId: bigint) => boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ChosenCharter {
|
|
50
|
+
chosen: number
|
|
51
|
+
chosenEpoch: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function builtCharter(world: CharterWorld, nodeId: number): BuiltCharter | undefined {
|
|
55
|
+
return world.built.find((row) => row.nodeId === nodeId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function charterSpawnNodeFor(targetItemId: number): CharterNode | undefined {
|
|
59
|
+
return CHARTER_REGISTRY.find(
|
|
60
|
+
(node) =>
|
|
61
|
+
node.effect.kind === CHARTER_EFFECT_SPAWN_ENTITY && node.effect.itemId === targetItemId
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function charterPrereqsMet(world: CharterWorld, node: CharterNode): boolean {
|
|
66
|
+
return node.prereqs.every(
|
|
67
|
+
(prereq) => prereq === CHARTER_NONE || builtCharter(world, prereq) !== undefined
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function charterEffectTargetEntity(world: CharterWorld, node: CharterNode): bigint {
|
|
72
|
+
const spawn = charterSpawnNodeFor(node.effect.targetItemId)
|
|
73
|
+
if (!spawn) return 0n
|
|
74
|
+
const record = builtCharter(world, spawn.nodeId)
|
|
75
|
+
if (!record) return 0n
|
|
76
|
+
if (world.entityExists && !world.entityExists(record.entityId)) return 0n
|
|
77
|
+
return record.entityId
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function charterEffectTargetPresent(world: CharterWorld, node: CharterNode): boolean {
|
|
81
|
+
if (node.effect.kind !== CHARTER_EFFECT_REFIT_MODULES) return true
|
|
82
|
+
return charterEffectTargetEntity(world, node) !== 0n
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function charterIneligible(
|
|
86
|
+
world: CharterWorld,
|
|
87
|
+
node: CharterNode
|
|
88
|
+
): CharterIneligibility | undefined {
|
|
89
|
+
if (builtCharter(world, node.nodeId) !== undefined) return 'already-taken'
|
|
90
|
+
if (!charterPrereqsMet(world, node)) return 'prereq-missing'
|
|
91
|
+
if (!charterEffectTargetPresent(world, node)) return 'refit-target-missing'
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function charterEligible(world: CharterWorld, node: CharterNode): boolean {
|
|
96
|
+
return charterIneligible(world, node) === undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function eligibleCharters(world: CharterWorld): CharterNode[] {
|
|
100
|
+
return CHARTER_REGISTRY.filter((node) => charterEligible(world, node))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function charterSingletonMandate(world: CharterWorld): number {
|
|
104
|
+
let only = CHARTER_NONE
|
|
105
|
+
for (const node of CHARTER_REGISTRY) {
|
|
106
|
+
if (!charterEligible(world, node)) continue
|
|
107
|
+
if (only !== CHARTER_NONE) return CHARTER_NONE
|
|
108
|
+
only = node.nodeId
|
|
109
|
+
}
|
|
110
|
+
return only
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function effectiveMandate(
|
|
114
|
+
stored: ChosenCharter,
|
|
115
|
+
world: CharterWorld,
|
|
116
|
+
epoch: number
|
|
117
|
+
): number {
|
|
118
|
+
if (stored.chosen !== CHARTER_NONE && stored.chosenEpoch === epoch) return stored.chosen
|
|
119
|
+
return charterSingletonMandate(world)
|
|
120
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {Checksum256, type Checksum256Type} from '@wharfkit/antelope'
|
|
2
|
+
import {Coordinates, type CoordinatesType, LocationType} from '../types'
|
|
3
|
+
import {hash512} from '../utils/hash'
|
|
4
|
+
import {deriveLocationStatic, getSystemName} from '../utils/system'
|
|
5
|
+
|
|
6
|
+
const CITIZENRY_PATTERNS: ((world: string) => string)[] = [
|
|
7
|
+
(w) => `${w} Collective`,
|
|
8
|
+
(w) => `${w} Compact`,
|
|
9
|
+
(w) => `Concord of ${w}`,
|
|
10
|
+
(w) => `${w} Union`,
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export function citizenryName(
|
|
14
|
+
gameSeed: Checksum256Type,
|
|
15
|
+
coordinates: CoordinatesType
|
|
16
|
+
): string | undefined {
|
|
17
|
+
const coords = Coordinates.from(coordinates)
|
|
18
|
+
const location = deriveLocationStatic(gameSeed, coords)
|
|
19
|
+
if (Number(location.type) !== LocationType.PLANET) return undefined
|
|
20
|
+
|
|
21
|
+
const world = getSystemName(gameSeed, coords)
|
|
22
|
+
const roll = hash512(
|
|
23
|
+
Checksum256.from(gameSeed),
|
|
24
|
+
`citizenry-${coords.x.toString()}-${coords.y.toString()}`
|
|
25
|
+
).array[0]
|
|
26
|
+
return CITIZENRY_PATTERNS[roll % CITIZENRY_PATTERNS.length](world)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function citizenryPatternCount(): number {
|
|
30
|
+
return CITIZENRY_PATTERNS.length
|
|
31
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const INFLUENCE_ATOMIC_PER_POINT = 10_000
|
|
2
|
+
export const NEED_FP_SCALE = 10_000
|
|
3
|
+
export const WEIGHT_FP_SCALE = 10_000
|
|
4
|
+
export const MASS_KG_PER_TONNE = 1_000
|
|
5
|
+
|
|
6
|
+
export const INFLUENCE_WEIGHT_FP_MAX = 1_000_000n
|
|
7
|
+
export const INFLUENCE_NEED_FP_MAX = 1_000_000n
|
|
8
|
+
export const INFLUENCE_MASS_KG_MAX = 1n << 48n
|
|
9
|
+
|
|
10
|
+
export const DECAY_RETAIN_NUM = 19n
|
|
11
|
+
export const DECAY_RETAIN_DEN = 20n
|
|
12
|
+
export const DECAY_MAX_ITERATIONS = 1_024
|
|
13
|
+
|
|
14
|
+
export const W_EFF_ATOMIC_PER_KG = BigInt(INFLUENCE_ATOMIC_PER_POINT / MASS_KG_PER_TONNE)
|
|
15
|
+
|
|
16
|
+
export const DECOMP_MAX_BUCKETS = 12
|
|
17
|
+
export const DECOMP_MAX_DEPTH = 8
|
|
18
|
+
export const DECOMP_RAW_KG_MAX = 100_000_000
|
|
19
|
+
export const DECOMP_PROCESSED_KG_MAX = 100_000_000
|
|
20
|
+
|
|
21
|
+
export const RESOURCE_ORE = 0
|
|
22
|
+
export const RESOURCE_GAS = 1
|
|
23
|
+
export const RESOURCE_REGOLITH = 2
|
|
24
|
+
export const RESOURCE_BIOMASS = 3
|
|
25
|
+
export const RESOURCE_CRYSTAL = 4
|
|
26
|
+
export const RESOURCE_CATEGORY_COUNT = 5
|
|
27
|
+
|
|
28
|
+
export const CIVIC_LOADER_STAT = 213
|
|
29
|
+
export const CIVIC_LOADER_TIER = 1
|
|
30
|
+
export const DEFAULT_ORBITAL_Z = 800
|
|
31
|
+
|
|
32
|
+
export const GRADE_STANDARD = 0
|
|
33
|
+
export const GRADE_PREMIUM = 1
|
|
34
|
+
export const MINTREADY_DEFAULT_CAP = 10
|
|
35
|
+
|
|
36
|
+
export const BAR_SEED = 102_363
|
|
37
|
+
export const BAR_STEP_UNIT = 20
|
|
38
|
+
export const BAR_STEP_UP = BAR_STEP_UNIT * 9
|
|
39
|
+
export const BAR_STEP_DOWN = BAR_STEP_UNIT
|
|
40
|
+
|
|
41
|
+
export const CHARTER_NONE = 0
|
|
42
|
+
export const CHARTER_WORKSHOP = 1
|
|
43
|
+
export const CHARTER_EFFECT_SPAWN_ENTITY = 1
|
|
44
|
+
export const CHARTER_EFFECT_REFIT_MODULES = 2
|
|
45
|
+
export const VOTE_READY_DEFAULT_MAX = 20
|
|
46
|
+
export const CHARTER_WORKSHOP_COST = 2_000_000_000n
|
|
47
|
+
export const CHARTER_BASELINE_STAT = 213
|
|
48
|
+
export const CHARTER_MAX_PREREQS = 4
|
|
49
|
+
|
|
50
|
+
export const INFLUENCE_D1_SEED = 45_369
|
|
51
|
+
export const DEMAND_TRIPLE_SEED = {peak: 20_000n, base: 10_000n, floor: 1_000n}
|
|
52
|
+
|
|
53
|
+
export function poolKey(category: number, tier: number, grade: number): bigint {
|
|
54
|
+
return (BigInt(category) << 16n) | (BigInt(tier) << 8n) | BigInt(grade)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function influenceWeightKey(category: number, tier: number): bigint {
|
|
58
|
+
return (BigInt(category) << 8n) | BigInt(tier)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function atomicToPoints(atomic: bigint | number | string): number {
|
|
62
|
+
return Math.floor(Number(BigInt(atomic.toString())) / INFLUENCE_ATOMIC_PER_POINT)
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {DECAY_MAX_ITERATIONS, DECAY_RETAIN_DEN, DECAY_RETAIN_NUM} from './constants'
|
|
2
|
+
|
|
3
|
+
export function decayActive(active: bigint, epochsElapsed: number): bigint {
|
|
4
|
+
const iterations = epochsElapsed > DECAY_MAX_ITERATIONS ? DECAY_MAX_ITERATIONS : epochsElapsed
|
|
5
|
+
let value = active
|
|
6
|
+
for (let i = 0; i < iterations && value > 0n; i++) {
|
|
7
|
+
value = (value * DECAY_RETAIN_NUM) / DECAY_RETAIN_DEN
|
|
8
|
+
}
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DecayableStanding {
|
|
13
|
+
active: bigint
|
|
14
|
+
lastUpdateEpoch: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeActive(standing: DecayableStanding, epoch: number): bigint {
|
|
18
|
+
const elapsed = epoch > standing.lastUpdateEpoch ? epoch - standing.lastUpdateEpoch : 0
|
|
19
|
+
return decayActive(standing.active, elapsed)
|
|
20
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {tryGetItem} from '../data/catalog'
|
|
2
|
+
import {getRecipe} from '../data/recipes-runtime'
|
|
3
|
+
import recipes from '../data/recipes.json'
|
|
4
|
+
import {
|
|
5
|
+
DECOMP_MAX_BUCKETS,
|
|
6
|
+
DECOMP_MAX_DEPTH,
|
|
7
|
+
DECOMP_PROCESSED_KG_MAX,
|
|
8
|
+
DECOMP_RAW_KG_MAX,
|
|
9
|
+
} from './constants'
|
|
10
|
+
import {categoryIndex} from './categories'
|
|
11
|
+
|
|
12
|
+
export interface DecompBucket {
|
|
13
|
+
category: number
|
|
14
|
+
tier: number
|
|
15
|
+
rawKg: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DecompEntry {
|
|
19
|
+
itemId: number
|
|
20
|
+
processedKg: number
|
|
21
|
+
buckets: DecompBucket[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Accumulator {
|
|
25
|
+
buckets: DecompBucket[]
|
|
26
|
+
processedKg: number
|
|
27
|
+
overflow: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function add(acc: Accumulator, category: number, tier: number, kg: number): void {
|
|
31
|
+
const existing = acc.buckets.find((b) => b.category === category && b.tier === tier)
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.rawKg += kg
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (acc.buckets.length >= DECOMP_MAX_BUCKETS) {
|
|
37
|
+
acc.overflow = true
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
acc.buckets.push({category, tier, rawKg: kg})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function walk(itemId: number, multiplier: number, acc: Accumulator, depth: number): void {
|
|
44
|
+
if (depth > DECOMP_MAX_DEPTH) {
|
|
45
|
+
acc.overflow = true
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
const recipe = getRecipe(itemId)
|
|
49
|
+
if (!recipe) return
|
|
50
|
+
|
|
51
|
+
for (const input of recipe.inputs) {
|
|
52
|
+
const def = tryGetItem(input.itemId)
|
|
53
|
+
const units = multiplier * input.quantity
|
|
54
|
+
const kg = units * (def ? def.mass : 0)
|
|
55
|
+
acc.processedKg += kg
|
|
56
|
+
if (getRecipe(input.itemId)) {
|
|
57
|
+
walk(input.itemId, units, acc, depth + 1)
|
|
58
|
+
} else if (def?.type !== 'resource' || def.category === undefined) {
|
|
59
|
+
acc.overflow = true
|
|
60
|
+
} else {
|
|
61
|
+
add(acc, categoryIndex(def.category), def.tier, kg)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildRegistry(): DecompEntry[] {
|
|
67
|
+
const out: DecompEntry[] = []
|
|
68
|
+
for (const recipe of recipes as {outputItemId: number}[]) {
|
|
69
|
+
const acc: Accumulator = {buckets: [], processedKg: 0, overflow: false}
|
|
70
|
+
walk(recipe.outputItemId, 1, acc, 0)
|
|
71
|
+
acc.buckets.sort((a, b) => a.category - b.category || a.tier - b.tier)
|
|
72
|
+
|
|
73
|
+
const rawTotal = acc.buckets.reduce((sum, b) => sum + b.rawKg, 0)
|
|
74
|
+
if (rawTotal > DECOMP_RAW_KG_MAX || acc.processedKg > DECOMP_PROCESSED_KG_MAX) {
|
|
75
|
+
acc.overflow = true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
out.push({
|
|
79
|
+
itemId: recipe.outputItemId,
|
|
80
|
+
processedKg: acc.overflow ? 0 : acc.processedKg,
|
|
81
|
+
buckets: acc.buckets,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
out.sort((a, b) => a.itemId - b.itemId)
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const DECOMP_REGISTRY: DecompEntry[] = buildRegistry()
|
|
89
|
+
|
|
90
|
+
const byItemId = new Map<number, DecompEntry>()
|
|
91
|
+
for (const entry of DECOMP_REGISTRY) byItemId.set(entry.itemId, entry)
|
|
92
|
+
|
|
93
|
+
export function findDecomp(itemId: number): DecompEntry | undefined {
|
|
94
|
+
return byItemId.get(itemId)
|
|
95
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {Checksum256, type Checksum256Type, type Checksum512} from '@wharfkit/antelope'
|
|
2
|
+
import {getLocationProfile} from '../derivation/resources'
|
|
3
|
+
import {Coordinates, type CoordinatesType} from '../types'
|
|
4
|
+
import {hash512} from '../utils/hash'
|
|
5
|
+
import {deriveLocationStatic} from '../utils/system'
|
|
6
|
+
import {RESOURCE_CATEGORY_COUNT} from './constants'
|
|
7
|
+
|
|
8
|
+
export interface DemandTriple {
|
|
9
|
+
peak: bigint
|
|
10
|
+
base: bigint
|
|
11
|
+
floor: bigint
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DemandView {
|
|
15
|
+
abundantMask: number
|
|
16
|
+
lackingMask: number
|
|
17
|
+
acute: number
|
|
18
|
+
needFp: bigint[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ALL_CATEGORIES_MASK = 0x1f
|
|
22
|
+
|
|
23
|
+
export function abundantMaskFor(locationType: number, subtype: number): number {
|
|
24
|
+
let mask = 0
|
|
25
|
+
for (const entry of getLocationProfile(locationType, subtype)) {
|
|
26
|
+
mask |= 1 << entry.category
|
|
27
|
+
}
|
|
28
|
+
return mask
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function lackingMaskFrom(abundantMask: number): number {
|
|
32
|
+
return ~abundantMask & ALL_CATEGORIES_MASK
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function popcount5(mask: number): number {
|
|
36
|
+
let n = 0
|
|
37
|
+
for (let i = 0; i < RESOURCE_CATEGORY_COUNT; i++) if (mask & (1 << i)) n++
|
|
38
|
+
return n
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function pickAcuteCategory(lackingMask: number, roll: number): number {
|
|
42
|
+
const count = popcount5(lackingMask)
|
|
43
|
+
if (count === 0) throw new Error('location has no lacking category')
|
|
44
|
+
let index = roll % count
|
|
45
|
+
for (let c = 0; c < RESOURCE_CATEGORY_COUNT; c++) {
|
|
46
|
+
if ((lackingMask & (1 << c)) === 0) continue
|
|
47
|
+
if (index === 0) return c
|
|
48
|
+
index--
|
|
49
|
+
}
|
|
50
|
+
throw new Error('acute pick fell through')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function demandRoll(hash: Checksum512): number {
|
|
54
|
+
const bytes = hash.array
|
|
55
|
+
return ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildDemand(abundantMask: number, acute: number, triple: DemandTriple): DemandView {
|
|
59
|
+
const lackingMask = lackingMaskFrom(abundantMask)
|
|
60
|
+
const needFp: bigint[] = []
|
|
61
|
+
for (let c = 0; c < RESOURCE_CATEGORY_COUNT; c++) {
|
|
62
|
+
if (c === acute) needFp.push(triple.peak)
|
|
63
|
+
else if (lackingMask & (1 << c)) needFp.push(triple.base)
|
|
64
|
+
else needFp.push(triple.floor)
|
|
65
|
+
}
|
|
66
|
+
return {abundantMask, lackingMask, acute, needFp}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function needForCategory(demand: DemandView, category: number): bigint {
|
|
70
|
+
if (category < 0 || category >= RESOURCE_CATEGORY_COUNT) {
|
|
71
|
+
throw new Error('invalid category')
|
|
72
|
+
}
|
|
73
|
+
return demand.needFp[category]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Two distinct seeds: composition from the genesis game seed, acute rotation from the epoch seed.
|
|
77
|
+
export function deriveDemand(args: {
|
|
78
|
+
gameSeed: Checksum256Type
|
|
79
|
+
epochSeed: Checksum256Type
|
|
80
|
+
coordinates: CoordinatesType
|
|
81
|
+
triple: DemandTriple
|
|
82
|
+
}): DemandView {
|
|
83
|
+
const coords = Coordinates.from(args.coordinates)
|
|
84
|
+
const location = deriveLocationStatic(args.gameSeed, coords)
|
|
85
|
+
const abundant = abundantMaskFor(Number(location.type), Number(location.subtype))
|
|
86
|
+
if (abundant === 0) throw new Error('location has no composition')
|
|
87
|
+
|
|
88
|
+
const hash = hash512(
|
|
89
|
+
Checksum256.from(args.epochSeed),
|
|
90
|
+
`demand-${coords.x.toString()}-${coords.y.toString()}`
|
|
91
|
+
)
|
|
92
|
+
const acute = pickAcuteCategory(lackingMaskFrom(abundant), demandRoll(hash))
|
|
93
|
+
return buildDemand(abundant, acute, args.triple)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isAbundant(demand: DemandView, category: number): boolean {
|
|
97
|
+
return (demand.abundantMask & (1 << category)) !== 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function isAcute(demand: DemandView, category: number): boolean {
|
|
101
|
+
return demand.acute === category
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function needMultiplier(demand: DemandView, category: number): number {
|
|
105
|
+
return Number(needForCategory(demand, category)) / 10_000
|
|
106
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {computeLoaderMass, computeLoaderThrust} from '../nft/description'
|
|
2
|
+
import {PRECISION} from '../types'
|
|
3
|
+
import {CIVIC_LOADER_STAT, CIVIC_LOADER_TIER, DEFAULT_ORBITAL_Z} from './constants'
|
|
4
|
+
|
|
5
|
+
export interface CivicLoaderStats {
|
|
6
|
+
thrust: number
|
|
7
|
+
mass: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function civicLoader(): CivicLoaderStats {
|
|
11
|
+
return {
|
|
12
|
+
thrust: Math.min(computeLoaderThrust(CIVIC_LOADER_STAT, CIVIC_LOADER_TIER), 65_535),
|
|
13
|
+
mass: computeLoaderMass(CIVIC_LOADER_STAT),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function acceleration(thrust: number, mass: number): number {
|
|
18
|
+
if (mass <= 0) return 0
|
|
19
|
+
return (thrust / mass) * PRECISION
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function flightTime(distance: number, accel: number): number {
|
|
23
|
+
if (accel <= 0 || distance <= 0) return 0
|
|
24
|
+
return Math.floor(2 * Math.sqrt(distance / accel))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function contributeDuration(totalMassKg: number, altitudeZ = 0): number {
|
|
28
|
+
const loader = civicLoader()
|
|
29
|
+
const totalMass = totalMassKg + loader.mass
|
|
30
|
+
const z = Math.max(altitudeZ, DEFAULT_ORBITAL_Z)
|
|
31
|
+
return flightTime(z, acceleration(loader.thrust, totalMass))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function contributeDurationForTonnes(tonnes: number, altitudeZ = 0): number {
|
|
35
|
+
return contributeDuration(Math.floor(tonnes * 1000), altitudeZ)
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export * from './constants'
|
|
2
|
+
export * from './categories'
|
|
3
|
+
export {decayActive, normalizeActive, type DecayableStanding} from './decay'
|
|
4
|
+
export {getStatCount, statsSumSq, qualityFactor} from './quality'
|
|
5
|
+
export {
|
|
6
|
+
DECOMP_REGISTRY,
|
|
7
|
+
findDecomp,
|
|
8
|
+
type DecompBucket,
|
|
9
|
+
type DecompEntry,
|
|
10
|
+
} from './decomp'
|
|
11
|
+
export {
|
|
12
|
+
abundantMaskFor,
|
|
13
|
+
buildDemand,
|
|
14
|
+
demandRoll,
|
|
15
|
+
deriveDemand,
|
|
16
|
+
isAbundant,
|
|
17
|
+
isAcute,
|
|
18
|
+
lackingMaskFrom,
|
|
19
|
+
needForCategory,
|
|
20
|
+
needMultiplier,
|
|
21
|
+
pickAcuteCategory,
|
|
22
|
+
popcount5,
|
|
23
|
+
type DemandTriple,
|
|
24
|
+
type DemandView,
|
|
25
|
+
} from './demand'
|
|
26
|
+
export {
|
|
27
|
+
componentBaseAtomic,
|
|
28
|
+
componentEffortAtomic,
|
|
29
|
+
pricingFromWeights,
|
|
30
|
+
resourceValueAtomic,
|
|
31
|
+
valueCargoItem,
|
|
32
|
+
valueContribution,
|
|
33
|
+
weightedQualityAtomic,
|
|
34
|
+
type InfluencePricing,
|
|
35
|
+
type ValuedItem,
|
|
36
|
+
} from './valuation'
|
|
37
|
+
export {
|
|
38
|
+
civicLoader,
|
|
39
|
+
contributeDuration,
|
|
40
|
+
contributeDurationForTonnes,
|
|
41
|
+
type CivicLoaderStats,
|
|
42
|
+
} from './duration'
|
|
43
|
+
export {citizenryName, citizenryPatternCount} from './citizenry'
|
|
44
|
+
export {
|
|
45
|
+
charterEffectTargetEntity,
|
|
46
|
+
charterEffectTargetPresent,
|
|
47
|
+
charterEligible,
|
|
48
|
+
charterIneligible,
|
|
49
|
+
charterNode,
|
|
50
|
+
charterPrereqsMet,
|
|
51
|
+
charterSingletonMandate,
|
|
52
|
+
charterSpawnNodeFor,
|
|
53
|
+
CHARTER_INELIGIBILITY_MESSAGES,
|
|
54
|
+
CHARTER_REGISTRY,
|
|
55
|
+
effectiveMandate,
|
|
56
|
+
eligibleCharters,
|
|
57
|
+
type BuiltCharter,
|
|
58
|
+
type CharterEffect,
|
|
59
|
+
type CharterIneligibility,
|
|
60
|
+
type CharterNode,
|
|
61
|
+
type CharterWorld,
|
|
62
|
+
type ChosenCharter,
|
|
63
|
+
} from './charters'
|