@shipload/sdk 1.0.0-next.7 → 1.0.0-next.70

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 (203) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +5696 -1503
  7. package/lib/shipload.js +20294 -5200
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +20371 -5683
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1648 -0
  12. package/lib/testing.js +6980 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +6974 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +80 -0
  20. package/src/capabilities/crafting.ts +50 -8
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +38 -20
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.test.ts +26 -0
  28. package/src/capabilities/storage.ts +22 -7
  29. package/src/contracts/fund.ts +306 -0
  30. package/src/contracts/platform.ts +312 -3
  31. package/src/contracts/server.ts +2661 -466
  32. package/src/contracts/token.ts +170 -0
  33. package/src/contracts.ts +2 -0
  34. package/src/coordinates/address.ts +88 -0
  35. package/src/coordinates/constants.test.ts +15 -0
  36. package/src/coordinates/constants.ts +23 -0
  37. package/src/coordinates/index.ts +15 -0
  38. package/src/coordinates/memo.test.ts +47 -0
  39. package/src/coordinates/memo.ts +20 -0
  40. package/src/coordinates/permutation.ts +77 -0
  41. package/src/coordinates/regions.ts +48 -0
  42. package/src/coordinates/sectors.ts +115 -0
  43. package/src/data/capabilities.ts +49 -18
  44. package/src/data/capability-formulas.ts +26 -14
  45. package/src/data/catalog.test.ts +19 -0
  46. package/src/data/catalog.ts +13 -3
  47. package/src/data/charters.json +396 -0
  48. package/src/data/citizenry-adjectives.json +42 -0
  49. package/src/data/citizenry-nouns.json +50 -0
  50. package/src/data/colors.ts +14 -32
  51. package/src/data/entities.json +421 -15
  52. package/src/data/item-ids.ts +53 -13
  53. package/src/data/items.json +330 -77
  54. package/src/data/kind-registry.json +212 -0
  55. package/src/data/kind-registry.ts +192 -0
  56. package/src/data/metadata.ts +312 -51
  57. package/src/data/recipes-runtime.ts +6 -23
  58. package/src/data/recipes.json +2173 -173
  59. package/src/data/tiers.ts +9 -6
  60. package/src/derivation/build-methods.test.ts +71 -0
  61. package/src/derivation/build-methods.ts +50 -0
  62. package/src/derivation/capabilities.test.ts +348 -0
  63. package/src/derivation/capabilities.ts +707 -0
  64. package/src/derivation/capability-mappings.ts +60 -16
  65. package/src/derivation/crafting.test.ts +17 -0
  66. package/src/derivation/crafting.ts +57 -34
  67. package/src/derivation/index.ts +27 -2
  68. package/src/derivation/recipe-usage.test.ts +88 -0
  69. package/src/derivation/recipe-usage.ts +141 -0
  70. package/src/derivation/reserve-regen.ts +34 -0
  71. package/src/derivation/resources.ts +9 -1
  72. package/src/derivation/rollups.test.ts +55 -0
  73. package/src/derivation/rollups.ts +72 -0
  74. package/src/derivation/stars.test.ts +67 -0
  75. package/src/derivation/stars.ts +25 -0
  76. package/src/derivation/stat-scaling.ts +12 -0
  77. package/src/derivation/stats.ts +6 -6
  78. package/src/derivation/strata.ts +5 -3
  79. package/src/derivation/stratum.ts +26 -22
  80. package/src/derivation/tiers.ts +40 -7
  81. package/src/derivation/upgrades.ts +14 -0
  82. package/src/derivation/wormhole.ts +154 -0
  83. package/src/entities/entity.ts +102 -0
  84. package/src/entities/gamestate.ts +3 -28
  85. package/src/entities/makers.ts +144 -136
  86. package/src/entities/slot-multiplier.ts +43 -0
  87. package/src/entity/hydrate.test.ts +156 -0
  88. package/src/entity/hydrate.ts +86 -0
  89. package/src/errors.ts +21 -16
  90. package/src/format.ts +42 -9
  91. package/src/index-module.ts +411 -32
  92. package/src/influence/ballot.test.ts +103 -0
  93. package/src/influence/ballot.ts +98 -0
  94. package/src/influence/categories.ts +32 -0
  95. package/src/influence/charter-metadata.test.ts +136 -0
  96. package/src/influence/charter-metadata.ts +236 -0
  97. package/src/influence/charters.test.ts +112 -0
  98. package/src/influence/charters.ts +150 -0
  99. package/src/influence/citizenry.ts +38 -0
  100. package/src/influence/civic-shuttle.test.ts +202 -0
  101. package/src/influence/civic-shuttle.ts +142 -0
  102. package/src/influence/civic.test.ts +287 -0
  103. package/src/influence/civic.ts +269 -0
  104. package/src/influence/constants.ts +84 -0
  105. package/src/influence/decay.ts +20 -0
  106. package/src/influence/decomp.ts +95 -0
  107. package/src/influence/demand.ts +106 -0
  108. package/src/influence/duration.ts +101 -0
  109. package/src/influence/index.ts +118 -0
  110. package/src/influence/influence.test.ts +450 -0
  111. package/src/influence/quality.ts +25 -0
  112. package/src/influence/valuation.ts +154 -0
  113. package/src/managers/actions.ts +940 -93
  114. package/src/managers/balances.test.ts +161 -0
  115. package/src/managers/balances.ts +93 -0
  116. package/src/managers/base.ts +10 -2
  117. package/src/managers/cluster.test.ts +39 -0
  118. package/src/managers/cluster.ts +53 -0
  119. package/src/managers/construction-types.ts +80 -0
  120. package/src/managers/construction.ts +453 -0
  121. package/src/managers/context.ts +58 -1
  122. package/src/managers/coordinates.ts +14 -0
  123. package/src/managers/entities.ts +35 -63
  124. package/src/managers/epochs.ts +72 -1
  125. package/src/managers/index.ts +38 -1
  126. package/src/managers/influence.test.ts +56 -0
  127. package/src/managers/influence.ts +425 -0
  128. package/src/managers/jobs.test.ts +80 -0
  129. package/src/managers/jobs.ts +54 -0
  130. package/src/managers/locations.ts +25 -29
  131. package/src/managers/nft.test.ts +21 -0
  132. package/src/managers/nft.ts +110 -0
  133. package/src/managers/players.ts +25 -0
  134. package/src/managers/plot.ts +122 -0
  135. package/src/nft/atomicassets.abi.json +1342 -0
  136. package/src/nft/atomicassets.ts +237 -0
  137. package/src/nft/atomicdata.ts +130 -0
  138. package/src/nft/buildImmutableData.ts +388 -0
  139. package/src/nft/description.ts +222 -100
  140. package/src/nft/index.ts +3 -0
  141. package/src/planner/index.ts +296 -0
  142. package/src/planner/planner.test.ts +459 -0
  143. package/src/receipts.ts +22 -0
  144. package/src/resolution/describe-module.ts +43 -25
  145. package/src/resolution/display-name.ts +39 -11
  146. package/src/resolution/resolve-item.test.ts +48 -0
  147. package/src/resolution/resolve-item.ts +112 -58
  148. package/src/scan/index.ts +244 -0
  149. package/src/scan/scan-wasm.base64.ts +2 -0
  150. package/src/scheduling/accessor.ts +65 -23
  151. package/src/scheduling/availability.test.ts +320 -0
  152. package/src/scheduling/availability.ts +251 -0
  153. package/src/scheduling/cancel.test.ts +550 -0
  154. package/src/scheduling/cancel.ts +254 -0
  155. package/src/scheduling/cluster-stock.test.ts +31 -0
  156. package/src/scheduling/cluster-stock.ts +15 -0
  157. package/src/scheduling/energy.ts +47 -0
  158. package/src/scheduling/epoch.ts +1 -0
  159. package/src/scheduling/idle-resolve.ts +45 -0
  160. package/src/scheduling/jobs.test.ts +49 -0
  161. package/src/scheduling/jobs.ts +137 -0
  162. package/src/scheduling/lane-core.ts +128 -0
  163. package/src/scheduling/lanes.test.ts +250 -0
  164. package/src/scheduling/lanes.ts +227 -0
  165. package/src/scheduling/projection.ts +265 -149
  166. package/src/scheduling/schedule.test.ts +163 -0
  167. package/src/scheduling/schedule.ts +282 -116
  168. package/src/scheduling/task-cargo.test.ts +110 -0
  169. package/src/scheduling/task-cargo.ts +62 -0
  170. package/src/scheduling/task-effects.test.ts +35 -0
  171. package/src/scheduling/task-effects.ts +34 -0
  172. package/src/scheduling/unwrap.test.ts +86 -0
  173. package/src/scheduling/unwrap.ts +190 -0
  174. package/src/shipload.ts +63 -4
  175. package/src/subscriptions/manager.cluster.test.ts +51 -0
  176. package/src/subscriptions/manager.ts +245 -172
  177. package/src/subscriptions/mappers.ts +5 -8
  178. package/src/subscriptions/types.ts +28 -3
  179. package/src/testing/catalog-hash.ts +19 -0
  180. package/src/testing/index.ts +2 -0
  181. package/src/testing/projection-parity.ts +167 -0
  182. package/src/travel/reach.test.ts +86 -0
  183. package/src/travel/reach.ts +34 -0
  184. package/src/travel/route-build.test.ts +63 -0
  185. package/src/travel/route-build.ts +60 -0
  186. package/src/travel/route-planner.ts +288 -0
  187. package/src/travel/route-simulator.ts +170 -0
  188. package/src/travel/travel.ts +190 -123
  189. package/src/types/capabilities.ts +37 -8
  190. package/src/types/entity.ts +3 -3
  191. package/src/types/index.ts +0 -1
  192. package/src/types.ts +81 -23
  193. package/src/utils/cargo.test.ts +14 -0
  194. package/src/utils/cargo.ts +2 -0
  195. package/src/utils/display-name.ts +70 -0
  196. package/src/utils/hash.ts +22 -2
  197. package/src/utils/system.ts +24 -5
  198. package/src/capabilities/loading.ts +0 -8
  199. package/src/entities/container.ts +0 -123
  200. package/src/entities/ship-deploy.ts +0 -295
  201. package/src/entities/ship.ts +0 -221
  202. package/src/entities/warehouse.ts +0 -127
  203. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,112 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {
3
+ CHARTER_NONE,
4
+ CIVIC_DEPOT,
5
+ CIVIC_DOCK,
6
+ CIVIC_STAT_CRAFT_SPEED,
7
+ CIVIC_STAT_TRANSFER_SPEED,
8
+ CIVIC_WORKSHOP,
9
+ } from './constants'
10
+ import {
11
+ charterEligible,
12
+ charterIneligible,
13
+ charterNode,
14
+ charterBuildingEntity,
15
+ charterGateNodeFor,
16
+ charterRungValue,
17
+ charterSingletonMandate,
18
+ charterModuleCount,
19
+ eligibleCharters,
20
+ type BuiltCharter,
21
+ type CharterWorld,
22
+ type WorldBuilding,
23
+ } from './charters'
24
+
25
+ const WORKSHOP = 10001
26
+ const NEXUS = 20001
27
+ const DOCK = 30100001
28
+ const WORKSHOP_TUNEUP = 10100201
29
+ const DOCK_TUNEUP = 30100301
30
+ const DEPOT = 40100001
31
+
32
+ const WORKSHOP_ENTITY = 100n
33
+ const DOCK_ENTITY = 300n
34
+
35
+ function world(built: BuiltCharter[]): CharterWorld {
36
+ return {built}
37
+ }
38
+
39
+ function node(nodeId: number) {
40
+ const found = charterNode(nodeId)
41
+ if (!found) throw new Error(`registry is missing node ${nodeId}`)
42
+ return found
43
+ }
44
+
45
+ function eligibleIds(w: CharterWorld): number[] {
46
+ return eligibleCharters(w).map((n) => n.nodeId)
47
+ }
48
+
49
+ describe('charter eligibility mirror', () => {
50
+ test('an unbuilt world offers only the root', () => {
51
+ expect(eligibleIds(world([]))).toEqual([WORKSHOP])
52
+ })
53
+
54
+ test('the root is the mandate by the singleton rule with no ballot', () => {
55
+ expect(charterSingletonMandate(world([]))).toBe(WORKSHOP)
56
+ })
57
+
58
+ test('completing the root opens the fork and closes the singleton path', () => {
59
+ const built = world([{nodeId: WORKSHOP, repeats: 0}])
60
+ expect(eligibleIds(built)).toEqual([WORKSHOP_TUNEUP, NEXUS, DOCK, DEPOT])
61
+ expect(charterSingletonMandate(built)).toBe(CHARTER_NONE)
62
+ })
63
+
64
+ test('a completed charter reports already-taken', () => {
65
+ const built = world([{nodeId: WORKSHOP, repeats: 0}])
66
+ expect(charterIneligible(built, node(WORKSHOP))).toBe('already-taken')
67
+ })
68
+
69
+ test('an unmet prereq reports prereq-missing', () => {
70
+ expect(charterIneligible(world([]), node(NEXUS))).toBe('prereq-missing')
71
+ expect(charterIneligible(world([]), node(DOCK_TUNEUP))).toBe('prereq-missing')
72
+ })
73
+
74
+ test('a rung is eligible as soon as its prereq is complete', () => {
75
+ const built = world([{nodeId: WORKSHOP, repeats: 0}])
76
+ expect(charterEligible(built, node(WORKSHOP_TUNEUP))).toBe(true)
77
+ })
78
+
79
+ test('a level gate is the node that creates its building', () => {
80
+ expect(charterGateNodeFor(CIVIC_WORKSHOP)?.nodeId).toBe(WORKSHOP)
81
+ expect(charterGateNodeFor(CIVIC_DOCK)?.nodeId).toBe(DOCK)
82
+ expect(charterGateNodeFor(CIVIC_DEPOT)?.nodeId).toBe(DEPOT)
83
+ expect(charterGateNodeFor(99)).toBeUndefined()
84
+ })
85
+
86
+ test('a building resolves to the entity recorded for its kind', () => {
87
+ const buildings: WorldBuilding[] = [
88
+ {entityId: WORKSHOP_ENTITY, building: CIVIC_WORKSHOP},
89
+ {entityId: DOCK_ENTITY, building: CIVIC_DOCK},
90
+ ]
91
+ expect(charterBuildingEntity(buildings, CIVIC_WORKSHOP)).toBe(WORKSHOP_ENTITY)
92
+ expect(charterBuildingEntity(buildings, CIVIC_DEPOT)).toBe(0n)
93
+ expect(charterBuildingEntity([], CIVIC_DEPOT)).toBe(0n)
94
+ })
95
+
96
+ test('rung values sum over the completed nodes only', () => {
97
+ const gateOnly = world([{nodeId: WORKSHOP, repeats: 0}])
98
+ expect(charterRungValue(gateOnly, CIVIC_WORKSHOP, CIVIC_STAT_CRAFT_SPEED)).toBe(200)
99
+
100
+ const tuned = world([
101
+ {nodeId: WORKSHOP, repeats: 0},
102
+ {nodeId: WORKSHOP_TUNEUP, repeats: 0},
103
+ ])
104
+ expect(charterRungValue(tuned, CIVIC_WORKSHOP, CIVIC_STAT_CRAFT_SPEED)).toBe(250)
105
+ expect(charterRungValue(tuned, CIVIC_DEPOT, CIVIC_STAT_TRANSFER_SPEED)).toBe(0)
106
+ })
107
+
108
+ test('modules sum over the completed nodes only', () => {
109
+ expect(charterModuleCount(world([]), CIVIC_WORKSHOP)).toBe(0)
110
+ expect(charterModuleCount(world([{nodeId: WORKSHOP, repeats: 0}]), CIVIC_WORKSHOP)).toBe(5)
111
+ })
112
+ })
@@ -0,0 +1,150 @@
1
+ import chartersJson from '../data/charters.json'
2
+ import {
3
+ CHARTER_NONE,
4
+ CIVIC_GRANT_LEVEL_GATE,
5
+ CIVIC_GRANT_MODULE,
6
+ CIVIC_GRANT_RUNG,
7
+ } from './constants'
8
+
9
+ export interface CharterGrant {
10
+ kind: number
11
+ building: number
12
+ stat: number
13
+ value: number
14
+ }
15
+
16
+ export interface CharterNode {
17
+ nodeId: number
18
+ cost: bigint
19
+ prereqs: number[]
20
+ grants: CharterGrant[]
21
+ exclusionGroup: number
22
+ repeatCap: number
23
+ }
24
+
25
+ export const CHARTER_REGISTRY: CharterNode[] = chartersJson.nodes.map((node) => ({
26
+ nodeId: node.nodeId,
27
+ cost: BigInt(node.cost),
28
+ prereqs: [...node.prereqs],
29
+ grants: node.grants.map((grant) => ({...grant})),
30
+ exclusionGroup: node.exclusionGroup,
31
+ repeatCap: node.repeatCap,
32
+ }))
33
+
34
+ export function charterNode(nodeId: number): CharterNode | undefined {
35
+ if (nodeId === CHARTER_NONE) return undefined
36
+ return CHARTER_REGISTRY.find((n) => n.nodeId === nodeId)
37
+ }
38
+
39
+ export type CharterIneligibility = 'already-taken' | 'prereq-missing'
40
+
41
+ export const CHARTER_INELIGIBILITY_MESSAGES: Record<CharterIneligibility, string> = {
42
+ 'already-taken': 'charter is already completed at this world',
43
+ 'prereq-missing': 'charter prerequisites are not completed',
44
+ }
45
+
46
+ export interface BuiltCharter {
47
+ nodeId: number
48
+ repeats: number
49
+ }
50
+
51
+ export interface CharterWorld {
52
+ built: BuiltCharter[]
53
+ }
54
+
55
+ export interface WorldBuilding {
56
+ entityId: bigint
57
+ building: number
58
+ }
59
+
60
+ function builtCharter(world: CharterWorld, nodeId: number): BuiltCharter | undefined {
61
+ return world.built.find((row) => row.nodeId === nodeId)
62
+ }
63
+
64
+ export function charterGateNodeFor(building: number, level: number = 1): CharterNode | undefined {
65
+ return CHARTER_REGISTRY.find((node) =>
66
+ node.grants.some(
67
+ (grant) =>
68
+ grant.kind === CIVIC_GRANT_LEVEL_GATE &&
69
+ grant.building === building &&
70
+ grant.value === level
71
+ )
72
+ )
73
+ }
74
+
75
+ export function charterBuildingEntity(buildings: WorldBuilding[], building: number): bigint {
76
+ const record = buildings.find((row) => row.building === building)
77
+ return record ? record.entityId : 0n
78
+ }
79
+
80
+ export function charterRungValue(world: CharterWorld, building: number, stat: number): number {
81
+ let total = 0
82
+ for (const node of CHARTER_REGISTRY) {
83
+ if (builtCharter(world, node.nodeId) === undefined) continue
84
+ for (const grant of node.grants) {
85
+ if (grant.kind !== CIVIC_GRANT_RUNG) continue
86
+ if (grant.building !== building || grant.stat !== stat) continue
87
+ total += grant.value
88
+ }
89
+ }
90
+ return total
91
+ }
92
+
93
+ export function charterModuleCount(world: CharterWorld, building: number): number {
94
+ let total = 0
95
+ for (const node of CHARTER_REGISTRY) {
96
+ if (builtCharter(world, node.nodeId) === undefined) continue
97
+ for (const grant of node.grants) {
98
+ if (grant.kind !== CIVIC_GRANT_MODULE) continue
99
+ if (grant.building !== building) continue
100
+ total += grant.value
101
+ }
102
+ }
103
+ return total
104
+ }
105
+
106
+ export function charterPrereqsMet(world: CharterWorld, node: CharterNode): boolean {
107
+ return node.prereqs.every(
108
+ (prereq) => prereq === CHARTER_NONE || builtCharter(world, prereq) !== undefined
109
+ )
110
+ }
111
+
112
+ export function charterIneligible(
113
+ world: CharterWorld,
114
+ node: CharterNode
115
+ ): CharterIneligibility | undefined {
116
+ if (builtCharter(world, node.nodeId) !== undefined) return 'already-taken'
117
+ if (!charterPrereqsMet(world, node)) return 'prereq-missing'
118
+ return undefined
119
+ }
120
+
121
+ export function charterEligible(world: CharterWorld, node: CharterNode): boolean {
122
+ return charterIneligible(world, node) === undefined
123
+ }
124
+
125
+ export function eligibleCharters(world: CharterWorld): CharterNode[] {
126
+ return CHARTER_REGISTRY.filter((node) => charterEligible(world, node))
127
+ }
128
+
129
+ export function charterSingletonMandate(world: CharterWorld): number {
130
+ let only = CHARTER_NONE
131
+ for (const node of CHARTER_REGISTRY) {
132
+ if (!charterEligible(world, node)) continue
133
+ if (only !== CHARTER_NONE) return CHARTER_NONE
134
+ only = node.nodeId
135
+ }
136
+ return only
137
+ }
138
+
139
+ export function charterEligibleChained(
140
+ world: CharterWorld,
141
+ node: CharterNode,
142
+ seated: number[]
143
+ ): boolean {
144
+ if (builtCharter(world, node.nodeId) !== undefined || seated.includes(node.nodeId)) return false
145
+ for (const prereq of node.prereqs) {
146
+ if (prereq === CHARTER_NONE) continue
147
+ if (builtCharter(world, prereq) === undefined && !seated.includes(prereq)) return false
148
+ }
149
+ return true
150
+ }
@@ -0,0 +1,38 @@
1
+ import {Checksum256, type Checksum256Type} from '@wharfkit/antelope'
2
+ import {Coordinates, type CoordinatesType, LocationType} from '../types'
3
+ import {hash512, uint16} from '../utils/hash'
4
+ import {deriveLocationStatic, getSystemName} from '../utils/system'
5
+ import citizenryAdjectives from '../data/citizenry-adjectives.json'
6
+ import citizenryNouns from '../data/citizenry-nouns.json'
7
+
8
+ const CITIZENRY_FORMS: ((world: string, noun: string, adjective: string) => string)[] = [
9
+ (w, n) => `${w} ${n}`,
10
+ (w, n, a) => `${a} ${w} ${n}`,
11
+ (w, n, a) => `${a} ${n} of ${w}`,
12
+ ]
13
+
14
+ const FORMS_WITH_ADJECTIVE = 2
15
+
16
+ export function citizenryName(
17
+ gameSeed: Checksum256Type,
18
+ coordinates: CoordinatesType
19
+ ): string | undefined {
20
+ const coords = Coordinates.from(coordinates)
21
+ const location = deriveLocationStatic(gameSeed, coords)
22
+ if (Number(location.type) !== LocationType.PLANET) return undefined
23
+
24
+ const world = getSystemName(gameSeed, coords)
25
+ const roll = hash512(
26
+ Checksum256.from(gameSeed),
27
+ `citizenry-${coords.x.toString()}-${coords.y.toString()}`
28
+ )
29
+ const form = CITIZENRY_FORMS[roll.array[0] % CITIZENRY_FORMS.length]
30
+ const noun = citizenryNouns[uint16(roll, 1) % citizenryNouns.length]
31
+ const adjective = citizenryAdjectives[uint16(roll, 3) % citizenryAdjectives.length]
32
+ return form(world, noun, adjective)
33
+ }
34
+
35
+ export function citizenryPatternCount(): number {
36
+ const plainForms = CITIZENRY_FORMS.length - FORMS_WITH_ADJECTIVE
37
+ return citizenryNouns.length * (plainForms + FORMS_WITH_ADJECTIVE * citizenryAdjectives.length)
38
+ }
@@ -0,0 +1,202 @@
1
+ import {UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
2
+ import {expect, test} from 'bun:test'
3
+ import type {ServerContract} from '../contracts'
4
+ import {encodeStats} from '../derivation/crafting'
5
+ import {ITEM_DEPOT_T1_PACKED, ITEM_LOADER_T1, ITEM_STORAGE_T1} from '../data/item-ids'
6
+ import {JOB_QUEUE_CAP} from '../scheduling/jobs'
7
+ import {HoldKind, TaskType} from '../types'
8
+ import {
9
+ civicShuttleBays,
10
+ civicTransfersAtCap,
11
+ civicTransfersFrom,
12
+ pendingCivicTransfers,
13
+ selectCivicShuttleBay,
14
+ CIVIC_TRANSFER_PER_PLAYER_CAP,
15
+ } from './civic-shuttle'
16
+
17
+ type Lane = ServerContract.Types.lane
18
+ type ModuleEntry = ServerContract.Types.module_entry
19
+ type Task = ServerContract.Types.task
20
+
21
+ const T0 = new Date('2026-01-01T00:00:00.000Z')
22
+ const loaderStats = encodeStats([300, 500])
23
+ const storageStats = encodeStats([300, 300])
24
+
25
+ function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
26
+ return {
27
+ type: UInt8.from(0),
28
+ installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
29
+ } as unknown as ModuleEntry
30
+ }
31
+
32
+ // The live Depot layout: two loader bays then four storage.
33
+ function depotModules(): ModuleEntry[] {
34
+ return [
35
+ makeModuleEntry(ITEM_LOADER_T1, loaderStats),
36
+ makeModuleEntry(ITEM_LOADER_T1, loaderStats),
37
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
38
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
39
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
40
+ makeModuleEntry(ITEM_STORAGE_T1, storageStats),
41
+ ]
42
+ }
43
+
44
+ function shuttleTask(senderId: number, receiverId: number, group?: number): Task {
45
+ return {
46
+ type: UInt8.from(TaskType.SHUTTLE),
47
+ duration: UInt32.from(60),
48
+ cancelable: UInt8.from(2),
49
+ cargo: [],
50
+ couplings: [
51
+ {
52
+ counterpart: {entity_id: UInt64.from(senderId), entity_type: UInt8.from(1)},
53
+ hold: UInt64.from(1),
54
+ kind: UInt8.from(HoldKind.PULL),
55
+ },
56
+ {
57
+ counterpart: {entity_id: UInt64.from(receiverId), entity_type: UInt8.from(1)},
58
+ hold: UInt64.from(1),
59
+ kind: UInt8.from(HoldKind.PUSH),
60
+ },
61
+ ],
62
+ entitygroup: group === undefined ? undefined : UInt64.from(group),
63
+ } as unknown as Task
64
+ }
65
+
66
+ function makeLane(laneKey: number, tasks: Task[]): Lane {
67
+ return {
68
+ lane_key: UInt8.from(laneKey),
69
+ schedule: {started: {toDate: () => T0}, tasks},
70
+ } as unknown as Lane
71
+ }
72
+
73
+ test('civicShuttleBays lists both Depot bays with their queue depth', () => {
74
+ const lanes = [makeLane(1, [shuttleTask(20, 21)])]
75
+ const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, T0)
76
+ expect(bays.map((b) => b.laneKey)).toEqual([1, 2])
77
+ expect(bays.map((b) => b.slotIndex)).toEqual([0, 1])
78
+ expect(bays[0].queued).toBe(1)
79
+ expect(bays[1].queued).toBe(0)
80
+ expect(bays.every((b) => !b.full)).toBe(true)
81
+ expect(bays.every((b) => b.thrust > 0 && b.mass > 0)).toBe(true)
82
+ })
83
+
84
+ test('a bay is full at the job queue cap', () => {
85
+ const filled = Array.from({length: JOB_QUEUE_CAP}, () => shuttleTask(20, 21))
86
+ const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, [makeLane(1, filled)], T0)
87
+ expect(bays[0].full).toBe(true)
88
+ expect(bays[1].full).toBe(false)
89
+ })
90
+
91
+ const AFTER = new Date(T0.getTime() + 3_600_000)
92
+
93
+ test('a landed booking leaves the bay free, since booking clears it', () => {
94
+ const filled = Array.from({length: JOB_QUEUE_CAP}, () => shuttleTask(20, 21))
95
+ const lanes = [makeLane(1, filled)]
96
+ const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, AFTER)
97
+ expect(bays[0].queued).toBe(0)
98
+ expect(bays[0].full).toBe(false)
99
+ expect(selectCivicShuttleBay(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, AFTER)?.laneKey).toBe(
100
+ 1
101
+ )
102
+ })
103
+
104
+ test('a bay part-way through its queue counts only what is still unfinished', () => {
105
+ const lanes = [makeLane(1, [shuttleTask(20, 21), shuttleTask(22, 23), shuttleTask(24, 25)])]
106
+ const bays = civicShuttleBays(
107
+ depotModules(),
108
+ ITEM_DEPOT_T1_PACKED,
109
+ lanes,
110
+ new Date(T0.getTime() + 90_000)
111
+ )
112
+ expect(bays[0].queued).toBe(2)
113
+ })
114
+
115
+ test('a landed transfer is listed for settling but is not callable', () => {
116
+ const lanes = [makeLane(1, [shuttleTask(20, 21)])]
117
+ const [transfer] = pendingCivicTransfers({lanes} as never, AFTER)
118
+ expect(transfer.isTail).toBe(true)
119
+ expect(transfer.complete).toBe(true)
120
+ expect(transfer.callable).toBe(false)
121
+ })
122
+
123
+ test('landed transfers do not count against the per-player cap', () => {
124
+ const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
125
+ shuttleTask(20 + i, 50)
126
+ )
127
+ const senderIds = mine.map((_, i) => 20 + i)
128
+ const at = {lanes: [makeLane(1, mine)]} as never
129
+ expect(civicTransfersAtCap(at, senderIds, T0)).toBe(true)
130
+ expect(civicTransfersAtCap(at, senderIds, AFTER)).toBe(false)
131
+ expect(civicTransfersFrom(at, senderIds, AFTER).length).toBe(CIVIC_TRANSFER_PER_PLAYER_CAP)
132
+ })
133
+
134
+ test('selectCivicShuttleBay takes the first free bay, then the lowest occupied one', () => {
135
+ const modules = depotModules()
136
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [], T0)?.laneKey).toBe(1)
137
+
138
+ const firstBusy = [makeLane(1, [shuttleTask(20, 21)])]
139
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, firstBusy, T0)?.laneKey).toBe(2)
140
+
141
+ const bothBusy = [makeLane(1, [shuttleTask(20, 21)]), makeLane(2, [shuttleTask(22, 23)])]
142
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, bothBusy, T0)?.laneKey).toBe(1)
143
+ })
144
+
145
+ test('selectCivicShuttleBay returns nothing when the building has no loaders', () => {
146
+ const modules = [makeModuleEntry(ITEM_STORAGE_T1, storageStats)]
147
+ expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [], T0)).toBeUndefined()
148
+ })
149
+
150
+ test('pendingCivicTransfers reports the bay, both counterparties, and the tail', () => {
151
+ const lanes = [
152
+ makeLane(1, [shuttleTask(20, 21), shuttleTask(22, 23)]),
153
+ makeLane(2, [shuttleTask(24, 25)]),
154
+ ]
155
+ const transfers = pendingCivicTransfers({lanes} as never, T0)
156
+ expect(transfers.length).toBe(3)
157
+
158
+ const first = transfers.find((t) => String(t.senderId) === '20')!
159
+ expect(first.laneKey).toBe(1)
160
+ expect(String(first.receiverId)).toBe('21')
161
+ expect(first.isTail).toBe(false)
162
+ expect(first.callable).toBe(false)
163
+ expect(first.startsAt).toEqual(T0)
164
+ expect(first.completesAt).toEqual(new Date(T0.getTime() + 60_000))
165
+
166
+ const second = transfers.find((t) => String(t.senderId) === '22')!
167
+ expect(second.isTail).toBe(true)
168
+ expect(second.callable).toBe(true)
169
+
170
+ const onBayTwo = transfers.find((t) => String(t.senderId) === '24')!
171
+ expect(onBayTwo.laneKey).toBe(2)
172
+ expect(onBayTwo.callable).toBe(true)
173
+ })
174
+
175
+ test('a grouped tail transfer is not callable', () => {
176
+ const lanes = [makeLane(1, [shuttleTask(20, 21, 7)])]
177
+ const [transfer] = pendingCivicTransfers({lanes} as never, T0)
178
+ expect(transfer.isTail).toBe(true)
179
+ expect(transfer.callable).toBe(false)
180
+ })
181
+
182
+ test('civicTransfersFrom counts only the ships a player sends from', () => {
183
+ const lanes = [
184
+ makeLane(1, [shuttleTask(20, 21), shuttleTask(30, 31)]),
185
+ makeLane(2, [shuttleTask(22, 23)]),
186
+ ]
187
+ const entity = {lanes} as never
188
+ expect(civicTransfersFrom(entity, [20, 22], T0).length).toBe(2)
189
+ expect(civicTransfersFrom(entity, [30], T0).length).toBe(1)
190
+ expect(civicTransfersFrom(entity, [99], T0).length).toBe(0)
191
+ })
192
+
193
+ test('civicTransfersAtCap trips at the per-player cap', () => {
194
+ const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
195
+ shuttleTask(20 + i, 50)
196
+ )
197
+ const senderIds = mine.map((_, i) => 20 + i)
198
+ const under = {lanes: [makeLane(1, mine.slice(0, -1))]} as never
199
+ const at = {lanes: [makeLane(1, mine)]} as never
200
+ expect(civicTransfersAtCap(under, senderIds, T0)).toBe(false)
201
+ expect(civicTransfersAtCap(at, senderIds, T0)).toBe(true)
202
+ })
@@ -0,0 +1,142 @@
1
+ import type {UInt64} from '@wharfkit/antelope'
2
+ import type {ServerContract} from '../contracts'
3
+ import {getItem} from '../data/catalog'
4
+ import {JOB_QUEUE_CAP} from '../scheduling/jobs'
5
+ import {laneKeyForModule, resolveLaneLoader} from '../scheduling/lanes'
6
+ import {orderedTasks, type ScheduleData} from '../scheduling/schedule'
7
+ import {HoldKind, TaskType} from '../types'
8
+
9
+ type CargoItem = ServerContract.Types.cargo_item
10
+ type Lane = ServerContract.Types.lane
11
+ type ModuleEntry = ServerContract.Types.module_entry
12
+
13
+ export const CIVIC_TRANSFER_PER_PLAYER_CAP = 4
14
+
15
+ export interface CivicShuttleBay {
16
+ laneKey: number
17
+ slotIndex: number
18
+ thrust: number
19
+ mass: number
20
+ outputPct: number
21
+ queued: number
22
+ full: boolean
23
+ }
24
+
25
+ // a landed booking clears during the next civicshuttle, so only unfinished tasks occupy a bay
26
+ function unfinishedTasks(lane: Lane | undefined, now: Date): number {
27
+ if (!lane) return 0
28
+ const startedMs = lane.schedule.started.toDate().getTime()
29
+ const nowMs = now.getTime()
30
+ let endSec = 0
31
+ let count = 0
32
+ for (const task of lane.schedule.tasks) {
33
+ endSec += task.duration.toNumber()
34
+ if (startedMs + endSec * 1000 > nowMs) count++
35
+ }
36
+ return count
37
+ }
38
+
39
+ export function civicShuttleBays(
40
+ modules: ModuleEntry[],
41
+ entityItemId: number,
42
+ lanes: Lane[],
43
+ now: Date = new Date()
44
+ ): CivicShuttleBay[] {
45
+ const bays: CivicShuttleBay[] = []
46
+ for (let slotIndex = 0; slotIndex < modules.length; slotIndex++) {
47
+ const installed = modules[slotIndex].installed
48
+ if (!installed) continue
49
+ if (getItem(installed.item_id).moduleType !== 'loader') continue
50
+ const laneKey = laneKeyForModule(slotIndex)
51
+ const loader = resolveLaneLoader(modules, entityItemId, laneKey)
52
+ const lane = lanes.find((l) => l.lane_key.toNumber() === laneKey)
53
+ const queued = unfinishedTasks(lane, now)
54
+ bays.push({
55
+ laneKey,
56
+ slotIndex,
57
+ thrust: loader.thrust,
58
+ mass: loader.mass,
59
+ outputPct: loader.outputPct,
60
+ queued,
61
+ full: queued >= JOB_QUEUE_CAP,
62
+ })
63
+ }
64
+ return bays
65
+ }
66
+
67
+ // Mirrors worker_lane_key_or_mobility for loaders: first free bay, else the lowest bay.
68
+ export function selectCivicShuttleBay(
69
+ modules: ModuleEntry[],
70
+ entityItemId: number,
71
+ lanes: Lane[],
72
+ now: Date = new Date()
73
+ ): CivicShuttleBay | undefined {
74
+ const bays = civicShuttleBays(modules, entityItemId, lanes, now)
75
+ return bays.find((bay) => bay.queued === 0) ?? bays[0]
76
+ }
77
+
78
+ export interface PendingCivicTransfer {
79
+ laneKey: number
80
+ taskIndex: number
81
+ senderId: UInt64
82
+ receiverId: UInt64
83
+ cargo: CargoItem[]
84
+ startsAt: Date
85
+ completesAt: Date
86
+ isTail: boolean
87
+ complete: boolean
88
+ callable: boolean
89
+ }
90
+
91
+ export function pendingCivicTransfers(
92
+ entity: ScheduleData,
93
+ now: Date = new Date()
94
+ ): PendingCivicTransfer[] {
95
+ const tailIndex = new Map<number, number>()
96
+ for (const l of entity.lanes ?? []) {
97
+ tailIndex.set(l.lane_key.toNumber(), l.schedule.tasks.length - 1)
98
+ }
99
+ const out: PendingCivicTransfer[] = []
100
+ for (const entry of orderedTasks(entity)) {
101
+ if (entry.task.type.toNumber() !== TaskType.SHUTTLE) continue
102
+ const pull = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PULL)
103
+ const push = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PUSH)
104
+ if (!pull || !push) continue
105
+ const isTail = tailIndex.get(entry.laneKey) === entry.taskIndex
106
+ const complete = entry.completesAt.getTime() <= now.getTime()
107
+ out.push({
108
+ laneKey: entry.laneKey,
109
+ taskIndex: entry.taskIndex,
110
+ senderId: pull.counterpart.entity_id,
111
+ receiverId: push.counterpart.entity_id,
112
+ cargo: entry.task.cargo,
113
+ startsAt: entry.startsAt,
114
+ completesAt: entry.completesAt,
115
+ isTail,
116
+ complete,
117
+ callable: isTail && !complete && !entry.task.entitygroup,
118
+ })
119
+ }
120
+ return out
121
+ }
122
+
123
+ // Mirrors civic_transfers_for: the sender side of every shuttle the building holds, by owner.
124
+ export function civicTransfersFrom(
125
+ entity: ScheduleData,
126
+ senderIds: Array<UInt64 | string | number>,
127
+ now: Date = new Date()
128
+ ): PendingCivicTransfer[] {
129
+ const owned = new Set(senderIds.map((id) => String(id)))
130
+ return pendingCivicTransfers(entity, now).filter((t) => owned.has(String(t.senderId)))
131
+ }
132
+
133
+ export function civicTransfersAtCap(
134
+ entity: ScheduleData,
135
+ senderIds: Array<UInt64 | string | number>,
136
+ now: Date = new Date()
137
+ ): boolean {
138
+ return (
139
+ civicTransfersFrom(entity, senderIds, now).filter((t) => !t.complete).length >=
140
+ CIVIC_TRANSFER_PER_PLAYER_CAP
141
+ )
142
+ }