@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,450 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {Checksum256, UInt8, UInt16, UInt64} from '@wharfkit/antelope'
3
+ import type {ServerContract} from '../contracts'
4
+ import {ITEM_ORE_T1, ITEM_PLATE, ITEM_CRYSTAL_T1} from '../data/item-ids'
5
+ import {categoryIndex} from './categories'
6
+ import {
7
+ CHARTER_WORKSHOP_COST,
8
+ DEMAND_TRIPLE_SEED,
9
+ INFLUENCE_ATOMIC_PER_POINT,
10
+ INFLUENCE_D1_SEED,
11
+ NEED_FP_SCALE,
12
+ } from './constants'
13
+ import {decayActive, normalizeActive} from './decay'
14
+ import {
15
+ buildDemand,
16
+ deriveDemand,
17
+ lackingMaskFrom,
18
+ needForCategory,
19
+ pickAcuteCategory,
20
+ popcount5,
21
+ } from './demand'
22
+ import {findDecomp, DECOMP_REGISTRY} from './decomp'
23
+ import {civicDropoffDuration, depotTransferDuration, contributeDuration} from './duration'
24
+ import {getStatCount, statsSumSq} from './quality'
25
+ import {pricingFromWeights, valueCargoItem, valueContribution} from './valuation'
26
+ import {citizenryName, citizenryPatternCount} from './citizenry'
27
+ import citizenryAdjectives from '../data/citizenry-adjectives.json'
28
+ import citizenryNouns from '../data/citizenry-nouns.json'
29
+
30
+ const PAR = 213
31
+ const WORLD = {x: 56, y: 0}
32
+ const packStats = (a: number, b = a, c = a): bigint =>
33
+ BigInt(a) | (BigInt(b) << 10n) | (BigInt(c) << 20n)
34
+
35
+ const SEED_WEIGHTS_FP = [10000, 10510, 14289, 21964, 32326, 42658, 52913, 61128, 68570, 75380]
36
+
37
+ const pricing = pricingFromWeights(
38
+ INFLUENCE_D1_SEED,
39
+ Array.from({length: 5}, (_, category) =>
40
+ SEED_WEIGHTS_FP.map((weightFp, i) => ({category, tier: i + 1, weightFp}))
41
+ ).flat()
42
+ )
43
+
44
+ const baseDemand = buildDemand(0, 255, DEMAND_TRIPLE_SEED)
45
+
46
+ describe('decay', () => {
47
+ test('zero epochs is the identity', () => {
48
+ expect(decayActive(1_000_000n, 0)).toBe(1_000_000n)
49
+ })
50
+
51
+ test('one epoch retains 19/20 with a floor', () => {
52
+ expect(decayActive(1_000_000n, 1)).toBe(950_000n)
53
+ expect(decayActive(19n, 1)).toBe(18n)
54
+ })
55
+
56
+ test('is path independent', () => {
57
+ const direct = decayActive(123_456_789n, 7)
58
+ const staged = decayActive(decayActive(123_456_789n, 3), 4)
59
+ expect(staged).toBe(direct)
60
+ })
61
+
62
+ test('early-exits at zero and clamps the iteration count', () => {
63
+ expect(decayActive(0n, 5_000)).toBe(0n)
64
+ expect(decayActive(1_000_000n, 100_000)).toBe(decayActive(1_000_000n, 1_024))
65
+ })
66
+
67
+ test('normalizeActive never decays backwards', () => {
68
+ expect(normalizeActive({active: 1_000n, lastUpdateEpoch: 9}, 4)).toBe(1_000n)
69
+ })
70
+ })
71
+
72
+ describe('quality', () => {
73
+ test('par stats give exactly the quality divisor', () => {
74
+ expect(statsSumSq(packStats(PAR), 3)).toBe(3 * INFLUENCE_D1_SEED)
75
+ })
76
+
77
+ test('the retired Workshop falls back to three stats', () => {
78
+ expect(getStatCount(10208)).toBe(3)
79
+ })
80
+
81
+ test('a crafted component reports its recipe slot count', () => {
82
+ expect(getStatCount(ITEM_PLATE)).toBe(3)
83
+ })
84
+ })
85
+
86
+ describe('demand', () => {
87
+ test('the lacking mask is the complement over five categories', () => {
88
+ expect(lackingMaskFrom(0b00101)).toBe(0b11010)
89
+ expect(popcount5(0b11010)).toBe(3)
90
+ })
91
+
92
+ test('the acute pick indexes the lacking set only', () => {
93
+ const lacking = 0b10010
94
+ expect(pickAcuteCategory(lacking, 0)).toBe(1)
95
+ expect(pickAcuteCategory(lacking, 1)).toBe(4)
96
+ expect(pickAcuteCategory(lacking, 2)).toBe(1)
97
+ })
98
+
99
+ test('the triple assigns peak, base and floor', () => {
100
+ const view = buildDemand(0b00001, 1, DEMAND_TRIPLE_SEED)
101
+ expect(needForCategory(view, 0)).toBe(DEMAND_TRIPLE_SEED.floor)
102
+ expect(needForCategory(view, 1)).toBe(DEMAND_TRIPLE_SEED.peak)
103
+ expect(needForCategory(view, 2)).toBe(DEMAND_TRIPLE_SEED.base)
104
+ })
105
+
106
+ test('the acute pick rotates with the epoch seed, not the game seed', () => {
107
+ const gameSeed = Checksum256.hash(Buffer.from('game'))
108
+ const coordinates = WORLD
109
+ const picks = new Set<number>()
110
+ for (let i = 0; i < 24; i++) {
111
+ const epochSeed = Checksum256.hash(Buffer.from(`epoch-${i}`))
112
+ picks.add(
113
+ deriveDemand({gameSeed, epochSeed, coordinates, triple: DEMAND_TRIPLE_SEED}).acute
114
+ )
115
+ }
116
+ expect(picks.size).toBeGreaterThan(1)
117
+ })
118
+
119
+ test('the composition partition is stable across epochs', () => {
120
+ const gameSeed = Checksum256.hash(Buffer.from('game'))
121
+ const coordinates = WORLD
122
+ const masks = new Set<number>()
123
+ for (let i = 0; i < 8; i++) {
124
+ const epochSeed = Checksum256.hash(Buffer.from(`epoch-${i}`))
125
+ masks.add(
126
+ deriveDemand({gameSeed, epochSeed, coordinates, triple: DEMAND_TRIPLE_SEED})
127
+ .abundantMask
128
+ )
129
+ }
130
+ expect(masks.size).toBe(1)
131
+ })
132
+ })
133
+
134
+ describe('decomposition', () => {
135
+ test('the registry is sorted by item id and covers every recipe', () => {
136
+ for (let i = 1; i < DECOMP_REGISTRY.length; i++) {
137
+ expect(DECOMP_REGISTRY[i].itemId).toBeGreaterThan(DECOMP_REGISTRY[i - 1].itemId)
138
+ }
139
+ expect(DECOMP_REGISTRY.length).toBe(56)
140
+ })
141
+
142
+ test('the retired Workshop has no decomposition entry', () => {
143
+ expect(findDecomp(10208)).toBeUndefined()
144
+ })
145
+
146
+ test('buckets are sorted and hold only raw resources', () => {
147
+ const entry = findDecomp(ITEM_PLATE)
148
+ if (!entry) throw new Error('Plate has no decomposition entry')
149
+ expect(entry.processedMass).toBeGreaterThan(0)
150
+ for (let i = 1; i < entry.buckets.length; i++) {
151
+ const prev = entry.buckets[i - 1]
152
+ const cur = entry.buckets[i]
153
+ expect(prev.category < cur.category || prev.tier < cur.tier).toBe(true)
154
+ }
155
+ })
156
+ })
157
+
158
+ describe('valuation', () => {
159
+ test('a tonne of par T1 ore at base demand is worth exactly one point', () => {
160
+ const value = valueCargoItem(
161
+ {itemId: ITEM_ORE_T1, quantity: 1, stats: packStats(PAR)},
162
+ baseDemand,
163
+ pricing
164
+ )
165
+ expect(value).toBe(BigInt(INFLUENCE_ATOMIC_PER_POINT))
166
+ })
167
+
168
+ test('the acute multiplier doubles a resource', () => {
169
+ const acute = buildDemand(0, categoryIndex('crystal'), DEMAND_TRIPLE_SEED)
170
+ const item = {itemId: ITEM_CRYSTAL_T1, quantity: 3, stats: packStats(PAR)}
171
+ expect(valueCargoItem(item, acute, pricing)).toBe(
172
+ valueCargoItem(item, baseDemand, pricing) * 2n
173
+ )
174
+ })
175
+
176
+ test('resource valuation is split invariant', () => {
177
+ const one = valueCargoItem(
178
+ {itemId: ITEM_ORE_T1, quantity: 1, stats: packStats(97)},
179
+ baseDemand,
180
+ pricing
181
+ )
182
+ const many = valueCargoItem(
183
+ {itemId: ITEM_ORE_T1, quantity: 137, stats: packStats(97)},
184
+ baseDemand,
185
+ pricing
186
+ )
187
+ expect(many).toBe(one * 137n)
188
+ })
189
+
190
+ test('component valuation is split invariant', () => {
191
+ const one = valueCargoItem(
192
+ {itemId: ITEM_PLATE, quantity: 1, stats: packStats(PAR)},
193
+ baseDemand,
194
+ pricing
195
+ )
196
+ const many = valueCargoItem(
197
+ {itemId: ITEM_PLATE, quantity: 40, stats: packStats(PAR)},
198
+ baseDemand,
199
+ pricing
200
+ )
201
+ expect(many).toBe(one * 40n)
202
+ })
203
+
204
+ test('components ignore demand entirely', () => {
205
+ const acute = buildDemand(0, categoryIndex('ore'), DEMAND_TRIPLE_SEED)
206
+ const item = {itemId: ITEM_PLATE, quantity: 5, stats: packStats(PAR)}
207
+ expect(valueCargoItem(item, acute, pricing)).toBe(valueCargoItem(item, baseDemand, pricing))
208
+ })
209
+
210
+ test('quality scales quadratically within the per-unit floor tolerance', () => {
211
+ const single = valueCargoItem(
212
+ {itemId: ITEM_ORE_T1, quantity: 100, stats: packStats(100)},
213
+ baseDemand,
214
+ pricing
215
+ )
216
+ const doubled = valueCargoItem(
217
+ {itemId: ITEM_ORE_T1, quantity: 100, stats: packStats(200)},
218
+ baseDemand,
219
+ pricing
220
+ )
221
+ const delta = doubled - single * 4n
222
+ expect(delta >= -105n && delta <= 105n).toBe(true)
223
+ })
224
+
225
+ test('a bundle totals its rows', () => {
226
+ const bundle = [
227
+ {itemId: ITEM_ORE_T1, quantity: 10, stats: packStats(PAR)},
228
+ {itemId: ITEM_PLATE, quantity: 2, stats: packStats(PAR)},
229
+ ]
230
+ expect(valueContribution(bundle, baseDemand, pricing)).toBe(
231
+ valueCargoItem(bundle[0], baseDemand, pricing) +
232
+ valueCargoItem(bundle[1], baseDemand, pricing)
233
+ )
234
+ })
235
+
236
+ test('a packed entity cannot be valued', () => {
237
+ expect(() =>
238
+ valueCargoItem({itemId: 10208, quantity: 1, stats: packStats(PAR)}, baseDemand, pricing)
239
+ ).toThrow()
240
+ })
241
+
242
+ test('the Workshop charter costs 200,000 points of hauled par T1 material', () => {
243
+ const perTonne = valueCargoItem(
244
+ {itemId: ITEM_ORE_T1, quantity: 200_000, stats: packStats(PAR)},
245
+ baseDemand,
246
+ pricing
247
+ )
248
+ expect(perTonne).toBe(CHARTER_WORKSHOP_COST)
249
+ })
250
+ })
251
+
252
+ describe('contribution duration', () => {
253
+ test('grows with mass and never falls below a single tick', () => {
254
+ expect(contributeDuration(10)).toBeGreaterThan(0)
255
+ expect(contributeDuration(10_000)).toBeGreaterThan(contributeDuration(10))
256
+ })
257
+
258
+ test('splitting a haul costs more than one trip', () => {
259
+ const single = contributeDuration(1_000_000)
260
+ const hundred = contributeDuration(10_000) * 100
261
+ expect(hundred).toBeGreaterThan(single)
262
+ })
263
+
264
+ test('ground level is floored at the base orbital climb', () => {
265
+ expect(contributeDuration(10, 0)).toBe(contributeDuration(10, 800))
266
+ })
267
+ })
268
+
269
+ describe('citizenry names', () => {
270
+ const gameSeed = Checksum256.hash(Buffer.from('game'))
271
+
272
+ test('is deterministic for a coordinate', () => {
273
+ const a = citizenryName(gameSeed, WORLD)
274
+ const b = citizenryName(gameSeed, WORLD)
275
+ expect(a).toBeDefined()
276
+ expect(a).toBe(b as string)
277
+ })
278
+
279
+ test('composes the world name with a known noun', () => {
280
+ const name = citizenryName(gameSeed, WORLD) ?? ''
281
+ expect(name.length).toBeGreaterThan(0)
282
+ expect(citizenryNouns.some((noun) => name.includes(noun))).toBe(true)
283
+ })
284
+
285
+ test('spreads across nouns and forms', () => {
286
+ const names: string[] = []
287
+ for (let x = 0; x < 160 && names.length < 200; x++) {
288
+ for (let y = 0; y < 160 && names.length < 200; y++) {
289
+ const name = citizenryName(gameSeed, {x, y})
290
+ if (name) names.push(name)
291
+ }
292
+ }
293
+ expect(names.length).toBeGreaterThan(100)
294
+
295
+ const nouns = new Set(names.map((n) => citizenryNouns.find((noun) => n.includes(noun))))
296
+ expect(nouns.size).toBeGreaterThan(20)
297
+
298
+ const ofForm = names.filter((n) => n.includes(' of ')).length
299
+ expect(ofForm).toBeGreaterThan(0)
300
+ expect(ofForm).toBeLessThan(names.length * 0.6)
301
+ })
302
+
303
+ test('counts every noun and adjective combination', () => {
304
+ expect(citizenryPatternCount()).toBe(
305
+ citizenryNouns.length * (1 + 2 * citizenryAdjectives.length)
306
+ )
307
+ })
308
+
309
+ test('is undefined away from a world', () => {
310
+ expect(citizenryName(gameSeed, {x: 7, y: 11})).toBeUndefined()
311
+ })
312
+ })
313
+
314
+ describe('scales', () => {
315
+ test('the need scale divides out against the atomic unit', () => {
316
+ expect(NEED_FP_SCALE % INFLUENCE_ATOMIC_PER_POINT).toBe(0)
317
+ })
318
+ })
319
+
320
+ describe('depot transfer duration', () => {
321
+ type ModuleEntry = ServerContract.Types.module_entry
322
+ const DEPOT_ITEM = 10219
323
+ const LOADER_T1 = 10103
324
+
325
+ function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
326
+ return {
327
+ type: UInt8.from(0),
328
+ installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
329
+ } as unknown as ModuleEntry
330
+ }
331
+ function emptySlot(): ModuleEntry {
332
+ return {type: UInt8.from(0)} as unknown as ModuleEntry
333
+ }
334
+
335
+ // A spawned depot: loader in slot 0, four empty bays after it.
336
+ const depotWithLoader = (): ModuleEntry[] => [
337
+ makeModuleEntry(LOADER_T1, packStats(500)),
338
+ emptySlot(),
339
+ emptySlot(),
340
+ emptySlot(),
341
+ emptySlot(),
342
+ ]
343
+ const params = (overrides: Record<string, unknown> = {}) => ({
344
+ depotModules: depotWithLoader(),
345
+ depotItemId: DEPOT_ITEM,
346
+ depotKind: 'depot',
347
+ depotZ: 0,
348
+ shipKind: 'ship',
349
+ shipZ: 0,
350
+ cargoMass: 100,
351
+ ...overrides,
352
+ })
353
+
354
+ test('drives off the depot loader, not the ship', () => {
355
+ expect(depotTransferDuration(params())).toBeGreaterThan(0)
356
+ })
357
+
358
+ test('grows with cargo mass', () => {
359
+ expect(depotTransferDuration(params({cargoMass: 10_000}))).toBeGreaterThan(
360
+ depotTransferDuration(params({cargoMass: 100}))
361
+ )
362
+ })
363
+
364
+ test('empty cargo costs nothing', () => {
365
+ expect(depotTransferDuration(params({cargoMass: 0}))).toBe(0)
366
+ })
367
+
368
+ test('a depot with no loader installed cannot transfer', () => {
369
+ expect(depotTransferDuration(params({depotModules: [emptySlot()]}))).toBe(0)
370
+ })
371
+
372
+ test('co-located transfers are floored at the orbital minimum distance', () => {
373
+ // max(planetary 100, orbital 200) = 200, so anything under 200 reads as 200
374
+ const touching = depotTransferDuration(params({shipZ: 0, depotZ: 0}))
375
+ const under = depotTransferDuration(params({shipZ: 150, depotZ: 0}))
376
+ const over = depotTransferDuration(params({shipZ: 400, depotZ: 0}))
377
+ expect(under).toBe(touching)
378
+ expect(over).toBeGreaterThan(touching)
379
+ })
380
+
381
+ test('never reports zero for real cargo', () => {
382
+ expect(depotTransferDuration(params({cargoMass: 1}))).toBeGreaterThanOrEqual(1)
383
+ })
384
+ })
385
+
386
+ describe('civic drop-off duration', () => {
387
+ type ModuleEntry = ServerContract.Types.module_entry
388
+ const DEPOT_ITEM = 10219
389
+ const WORKSHOP_ITEM = 10208
390
+ const LOADER_T1 = 10103
391
+
392
+ function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
393
+ return {
394
+ type: UInt8.from(0),
395
+ installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
396
+ } as unknown as ModuleEntry
397
+ }
398
+ function emptySlot(): ModuleEntry {
399
+ return {type: UInt8.from(0)} as unknown as ModuleEntry
400
+ }
401
+
402
+ const withLoader = () => ({
403
+ buildingModules: [makeModuleEntry(LOADER_T1, packStats(500)), emptySlot()],
404
+ buildingItemId: DEPOT_ITEM,
405
+ buildingKind: 'depot',
406
+ buildingZ: 0,
407
+ shipKind: 'ship',
408
+ shipZ: 400,
409
+ cargoMass: 100,
410
+ })
411
+ const withoutLoader = () => ({
412
+ buildingModules: [] as ModuleEntry[],
413
+ buildingItemId: WORKSHOP_ITEM,
414
+ buildingKind: 'workshop',
415
+ buildingZ: 0,
416
+ shipKind: 'ship',
417
+ shipZ: 400,
418
+ cargoMass: 100,
419
+ })
420
+
421
+ test('a building with a slot-0 loader prices off that loader', () => {
422
+ const p = withLoader()
423
+ expect(civicDropoffDuration(p)).toBe(
424
+ depotTransferDuration({
425
+ depotModules: p.buildingModules,
426
+ depotItemId: p.buildingItemId,
427
+ depotKind: p.buildingKind,
428
+ depotZ: p.buildingZ,
429
+ shipKind: p.shipKind,
430
+ shipZ: p.shipZ,
431
+ cargoMass: p.cargoMass,
432
+ })
433
+ )
434
+ })
435
+
436
+ test('a building with no loader falls back to the civic loader', () => {
437
+ const p = withoutLoader()
438
+ expect(civicDropoffDuration(p)).toBe(contributeDuration(p.cargoMass, p.shipZ))
439
+ expect(civicDropoffDuration(p)).toBeGreaterThan(0)
440
+ })
441
+
442
+ test('grows with cargo mass on both paths', () => {
443
+ expect(civicDropoffDuration({...withLoader(), cargoMass: 10_000})).toBeGreaterThan(
444
+ civicDropoffDuration(withLoader())
445
+ )
446
+ expect(civicDropoffDuration({...withoutLoader(), cargoMass: 10_000})).toBeGreaterThan(
447
+ civicDropoffDuration(withoutLoader())
448
+ )
449
+ })
450
+ })
@@ -0,0 +1,25 @@
1
+ import {getRecipe} from '../data/recipes-runtime'
2
+ import {decodeStat} from '../derivation/crafting'
3
+
4
+ const CRAFTED_ITEM_ID_FLOOR = 10_000
5
+ const UNCRAFTED_STAT_COUNT = 3
6
+
7
+ export function getStatCount(itemId: number): number {
8
+ if (itemId < CRAFTED_ITEM_ID_FLOOR) return UNCRAFTED_STAT_COUNT
9
+ const recipe = getRecipe(itemId)
10
+ return recipe ? recipe.statSlots.length : UNCRAFTED_STAT_COUNT
11
+ }
12
+
13
+ export function statsSumSq(packedStats: bigint, nStats: number): number {
14
+ let sum = 0
15
+ for (let i = 0; i < nStats; i++) {
16
+ const s = decodeStat(packedStats, i)
17
+ sum += s * s
18
+ }
19
+ return sum
20
+ }
21
+
22
+ export function qualityFactor(packedStats: bigint, nStats: number, d1: number): number {
23
+ if (nStats <= 0 || d1 <= 0) throw new Error('influence: invalid quality divisor')
24
+ return statsSumSq(packedStats, nStats) / (nStats * d1)
25
+ }
@@ -0,0 +1,154 @@
1
+ import {getItem} from '../data/catalog'
2
+ import {MASS_UNITS_PER_TONNE} from '../types'
3
+ import {categoryIndex} from './categories'
4
+ import {
5
+ INFLUENCE_ATOMIC_PER_POINT,
6
+ INFLUENCE_MASS_MAX,
7
+ INFLUENCE_NEED_FP_MAX,
8
+ INFLUENCE_WEIGHT_FP_MAX,
9
+ NEED_FP_SCALE,
10
+ WEIGHT_FP_SCALE,
11
+ W_EFF_ATOMIC_PER_UNIT,
12
+ } from './constants'
13
+ import {findDecomp} from './decomp'
14
+ import type {DemandView} from './demand'
15
+ import {needForCategory} from './demand'
16
+ import {getStatCount, statsSumSq} from './quality'
17
+
18
+ const UINT64_MAX = (1n << 64n) - 1n
19
+
20
+ export interface InfluencePricing {
21
+ d1: number
22
+ weight(category: number, tier: number): bigint
23
+ }
24
+
25
+ export interface ValuedItem {
26
+ itemId: number
27
+ quantity: number
28
+ stats: bigint
29
+ }
30
+
31
+ export function weightedQualityAtomic(
32
+ weightedMassFp: bigint,
33
+ sumSq: number,
34
+ nStats: number,
35
+ d1: number,
36
+ needFp: bigint
37
+ ): bigint {
38
+ if (nStats <= 0 || d1 <= 0) throw new Error('influence: invalid quality divisor')
39
+ const num = weightedMassFp * BigInt(sumSq) * needFp
40
+ const den =
41
+ (BigInt(MASS_UNITS_PER_TONNE) *
42
+ BigInt(WEIGHT_FP_SCALE) *
43
+ BigInt(nStats) *
44
+ BigInt(d1) *
45
+ BigInt(NEED_FP_SCALE)) /
46
+ BigInt(INFLUENCE_ATOMIC_PER_POINT)
47
+ return num / den
48
+ }
49
+
50
+ export function resourceValueAtomic(
51
+ mass: bigint,
52
+ weightFp: bigint,
53
+ sumSq: number,
54
+ nStats: number,
55
+ d1: number,
56
+ needFp: bigint
57
+ ): bigint {
58
+ if (mass > INFLUENCE_MASS_MAX) throw new Error('influence: mass out of range')
59
+ if (weightFp > INFLUENCE_WEIGHT_FP_MAX) throw new Error('influence: weight out of range')
60
+ if (needFp > INFLUENCE_NEED_FP_MAX) throw new Error('influence: need out of range')
61
+ return weightedQualityAtomic(mass * weightFp, sumSq, nStats, d1, needFp)
62
+ }
63
+
64
+ export function componentBaseAtomic(
65
+ weightedRawMassFp: bigint,
66
+ sumSq: number,
67
+ nStats: number,
68
+ d1: number
69
+ ): bigint {
70
+ return weightedQualityAtomic(weightedRawMassFp, sumSq, nStats, d1, BigInt(NEED_FP_SCALE))
71
+ }
72
+
73
+ export function componentEffortAtomic(processedMassPerUnit: number, quantity: number): bigint {
74
+ const processed = BigInt(processedMassPerUnit) * BigInt(quantity)
75
+ if (processed > INFLUENCE_MASS_MAX) {
76
+ throw new Error('influence: processed mass out of range')
77
+ }
78
+ return processed * W_EFF_ATOMIC_PER_UNIT
79
+ }
80
+
81
+ export function valueCargoItem(
82
+ item: ValuedItem,
83
+ demand: DemandView,
84
+ pricing: InfluencePricing
85
+ ): bigint {
86
+ const def = getItem(item.itemId)
87
+ if (def.type !== 'resource' && def.type !== 'component') {
88
+ throw new Error('item class cannot be valued')
89
+ }
90
+
91
+ const nStats = getStatCount(item.itemId)
92
+ const sumSq = statsSumSq(item.stats, nStats)
93
+
94
+ if (def.type === 'resource') {
95
+ if (def.category === undefined) throw new Error('resource has no category')
96
+ const category = categoryIndex(def.category)
97
+ const weightFp = pricing.weight(category, def.tier)
98
+ const needFp = needForCategory(demand, category)
99
+ const perUnit = resourceValueAtomic(
100
+ BigInt(def.mass),
101
+ weightFp,
102
+ sumSq,
103
+ nStats,
104
+ pricing.d1,
105
+ needFp
106
+ )
107
+ const total = perUnit * BigInt(item.quantity)
108
+ if (total > UINT64_MAX) throw new Error('influence: resource value overflow')
109
+ return total
110
+ }
111
+
112
+ const entry = findDecomp(item.itemId)
113
+ if (!entry) throw new Error('item has no decomposition')
114
+
115
+ let weighted = 0n
116
+ for (const bucket of entry.buckets) {
117
+ weighted += BigInt(bucket.rawMass) * pricing.weight(bucket.category, bucket.tier)
118
+ }
119
+
120
+ const perUnit = componentBaseAtomic(weighted, sumSq, nStats, pricing.d1)
121
+ const total = perUnit * BigInt(item.quantity)
122
+ if (total > UINT64_MAX) throw new Error('influence: component value overflow')
123
+
124
+ const withEffort = total + componentEffortAtomic(entry.processedMass, item.quantity)
125
+ if (withEffort > UINT64_MAX) throw new Error('influence: component value overflow')
126
+ return withEffort
127
+ }
128
+
129
+ export function valueContribution(
130
+ bundle: ValuedItem[],
131
+ demand: DemandView,
132
+ pricing: InfluencePricing
133
+ ): bigint {
134
+ let total = 0n
135
+ for (const item of bundle) total += valueCargoItem(item, demand, pricing)
136
+ if (total > UINT64_MAX) throw new Error('influence: contribution value overflow')
137
+ return total
138
+ }
139
+
140
+ export function pricingFromWeights(
141
+ d1: number,
142
+ weights: Iterable<{category: number; tier: number; weightFp: bigint | number | string}>
143
+ ): InfluencePricing {
144
+ const table = new Map<number, bigint>()
145
+ for (const row of weights) {
146
+ table.set(row.category * 16 + row.tier, BigInt(row.weightFp.toString()))
147
+ }
148
+ return {
149
+ d1,
150
+ weight(category: number, tier: number): bigint {
151
+ return table.get(category * 16 + tier) ?? 0n
152
+ },
153
+ }
154
+ }