@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.
Files changed (55) hide show
  1. package/lib/shipload.d.ts +1243 -59
  2. package/lib/shipload.js +3911 -1261
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +3812 -1265
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +391 -13
  7. package/lib/testing.js +1554 -114
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +1555 -115
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +2 -2
  12. package/src/contracts/fund.ts +201 -0
  13. package/src/contracts/platform.ts +73 -1
  14. package/src/contracts/server.ts +946 -26
  15. package/src/contracts.ts +1 -0
  16. package/src/data/charters.json +198 -0
  17. package/src/data/entities.json +36 -0
  18. package/src/data/item-ids.ts +2 -0
  19. package/src/data/items.json +12 -0
  20. package/src/data/kind-registry.json +20 -3
  21. package/src/data/kind-registry.ts +5 -0
  22. package/src/data/metadata.ts +14 -0
  23. package/src/data/recipes.json +0 -48
  24. package/src/derivation/capabilities.ts +14 -0
  25. package/src/derivation/recipe-usage.test.ts +0 -2
  26. package/src/format.ts +9 -0
  27. package/src/index-module.ts +34 -3
  28. package/src/influence/categories.ts +32 -0
  29. package/src/influence/charters.test.ts +126 -0
  30. package/src/influence/charters.ts +120 -0
  31. package/src/influence/citizenry.ts +31 -0
  32. package/src/influence/constants.ts +63 -0
  33. package/src/influence/decay.ts +20 -0
  34. package/src/influence/decomp.ts +95 -0
  35. package/src/influence/demand.ts +106 -0
  36. package/src/influence/duration.ts +36 -0
  37. package/src/influence/index.ts +63 -0
  38. package/src/influence/influence.test.ts +291 -0
  39. package/src/influence/quality.ts +25 -0
  40. package/src/influence/valuation.ts +154 -0
  41. package/src/managers/actions.ts +135 -0
  42. package/src/managers/construction.ts +11 -1
  43. package/src/managers/context.ts +18 -0
  44. package/src/managers/entities.ts +11 -0
  45. package/src/managers/index.ts +13 -0
  46. package/src/managers/influence.test.ts +58 -0
  47. package/src/managers/influence.ts +347 -0
  48. package/src/managers/jobs.test.ts +62 -0
  49. package/src/managers/jobs.ts +52 -0
  50. package/src/nft/description.ts +7 -0
  51. package/src/scheduling/jobs.test.ts +32 -0
  52. package/src/scheduling/jobs.ts +37 -0
  53. package/src/scheduling/task-cargo.ts +1 -0
  54. package/src/shipload.ts +10 -0
  55. 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'