@shipload/sdk 1.0.0-next.65 → 1.0.0-next.66

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 (79) hide show
  1. package/lib/shipload.d.ts +698 -316
  2. package/lib/shipload.js +2473 -1235
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +2392 -1216
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +205 -146
  7. package/lib/testing.js +846 -556
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +846 -556
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/crafting.test.ts +51 -6
  13. package/src/capabilities/crafting.ts +19 -4
  14. package/src/capabilities/gathering.ts +2 -2
  15. package/src/capabilities/storage.test.ts +26 -0
  16. package/src/capabilities/storage.ts +6 -6
  17. package/src/contracts/fund.ts +79 -2
  18. package/src/contracts/platform.ts +10 -1
  19. package/src/contracts/server.ts +467 -331
  20. package/src/data/charters.json +325 -127
  21. package/src/data/entities.json +24 -24
  22. package/src/data/items.json +114 -114
  23. package/src/data/kind-registry.json +3 -3
  24. package/src/data/kind-registry.ts +5 -0
  25. package/src/data/recipes.json +56 -95
  26. package/src/derivation/build-methods.test.ts +59 -1
  27. package/src/derivation/build-methods.ts +2 -0
  28. package/src/derivation/capabilities.ts +7 -7
  29. package/src/derivation/strata.ts +5 -3
  30. package/src/derivation/tiers.ts +5 -5
  31. package/src/entity/hydrate.test.ts +156 -0
  32. package/src/entity/hydrate.ts +86 -0
  33. package/src/errors.ts +9 -0
  34. package/src/format.ts +15 -13
  35. package/src/index-module.ts +32 -5
  36. package/src/influence/ballot.test.ts +103 -0
  37. package/src/influence/ballot.ts +98 -0
  38. package/src/influence/charter-metadata.test.ts +136 -0
  39. package/src/influence/charter-metadata.ts +236 -0
  40. package/src/influence/charters.test.ts +51 -65
  41. package/src/influence/charters.ts +70 -40
  42. package/src/influence/civic.test.ts +278 -0
  43. package/src/influence/civic.ts +269 -0
  44. package/src/influence/constants.ts +30 -9
  45. package/src/influence/decomp.ts +15 -15
  46. package/src/influence/duration.ts +42 -6
  47. package/src/influence/index.ts +49 -6
  48. package/src/influence/influence.test.ts +76 -9
  49. package/src/influence/valuation.ts +17 -17
  50. package/src/managers/actions.ts +65 -37
  51. package/src/managers/base.ts +4 -0
  52. package/src/managers/context.ts +3 -1
  53. package/src/managers/index.ts +4 -2
  54. package/src/managers/influence.test.ts +18 -20
  55. package/src/managers/influence.ts +137 -84
  56. package/src/managers/jobs.test.ts +2 -2
  57. package/src/managers/jobs.ts +4 -4
  58. package/src/nft/description.ts +10 -11
  59. package/src/planner/index.ts +54 -0
  60. package/src/planner/planner.test.ts +127 -2
  61. package/src/resolution/display-name.ts +1 -1
  62. package/src/scheduling/availability.test.ts +113 -0
  63. package/src/scheduling/availability.ts +48 -8
  64. package/src/scheduling/jobs.ts +1 -1
  65. package/src/scheduling/projection.ts +2 -16
  66. package/src/scheduling/task-cargo.test.ts +62 -1
  67. package/src/scheduling/task-cargo.ts +25 -10
  68. package/src/scheduling/task-effects.test.ts +35 -0
  69. package/src/scheduling/task-effects.ts +34 -0
  70. package/src/scheduling/unwrap.test.ts +3 -3
  71. package/src/scheduling/unwrap.ts +3 -3
  72. package/src/shipload.ts +22 -3
  73. package/src/travel/reach.test.ts +86 -0
  74. package/src/travel/reach.ts +23 -10
  75. package/src/travel/route-build.test.ts +63 -0
  76. package/src/travel/route-build.ts +60 -0
  77. package/src/travel/travel.ts +2 -1
  78. package/src/types.ts +6 -2
  79. package/src/utils/hash.ts +18 -2
@@ -1,26 +1,34 @@
1
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 {
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 {
5
10
  kind: number
6
- itemId: number
7
- targetItemId: number
8
- slotMask: number
11
+ building: number
9
12
  stat: number
13
+ value: number
10
14
  }
11
15
 
12
16
  export interface CharterNode {
13
17
  nodeId: number
14
18
  cost: bigint
15
19
  prereqs: number[]
16
- effect: CharterEffect
20
+ grants: CharterGrant[]
21
+ exclusionGroup: number
22
+ repeatCap: number
17
23
  }
18
24
 
19
25
  export const CHARTER_REGISTRY: CharterNode[] = chartersJson.nodes.map((node) => ({
20
26
  nodeId: node.nodeId,
21
27
  cost: BigInt(node.cost),
22
28
  prereqs: [...node.prereqs],
23
- effect: {...node.effect},
29
+ grants: node.grants.map((grant) => ({...grant})),
30
+ exclusionGroup: node.exclusionGroup,
31
+ repeatCap: node.repeatCap,
24
32
  }))
25
33
 
26
34
  export function charterNode(nodeId: number): CharterNode | undefined {
@@ -28,58 +36,77 @@ export function charterNode(nodeId: number): CharterNode | undefined {
28
36
  return CHARTER_REGISTRY.find((n) => n.nodeId === nodeId)
29
37
  }
30
38
 
31
- export type CharterIneligibility = 'already-taken' | 'prereq-missing' | 'refit-target-missing'
39
+ export type CharterIneligibility = 'already-taken' | 'prereq-missing'
32
40
 
33
41
  export const CHARTER_INELIGIBILITY_MESSAGES: Record<CharterIneligibility, string> = {
34
42
  'already-taken': 'charter is already completed at this world',
35
43
  'prereq-missing': 'charter prerequisites are not completed',
36
- 'refit-target-missing': 'charter refit target not found at this world',
37
44
  }
38
45
 
39
46
  export interface BuiltCharter {
40
47
  nodeId: number
41
- entityId: bigint
48
+ repeats: number
42
49
  }
43
50
 
44
51
  export interface CharterWorld {
45
52
  built: BuiltCharter[]
46
- entityExists?: (entityId: bigint) => boolean
47
53
  }
48
54
 
49
- export interface ChosenCharter {
50
- chosen: number
51
- chosenEpoch: number
55
+ export interface WorldBuilding {
56
+ entityId: bigint
57
+ building: number
52
58
  }
53
59
 
54
60
  function builtCharter(world: CharterWorld, nodeId: number): BuiltCharter | undefined {
55
61
  return world.built.find((row) => row.nodeId === nodeId)
56
62
  }
57
63
 
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
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
+ )
62
72
  )
63
73
  }
64
74
 
65
- export function charterPrereqsMet(world: CharterWorld, node: CharterNode): boolean {
66
- return node.prereqs.every(
67
- (prereq) => prereq === CHARTER_NONE || builtCharter(world, prereq) !== undefined
68
- )
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
69
91
  }
70
92
 
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
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
78
104
  }
79
105
 
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
106
+ export function charterPrereqsMet(world: CharterWorld, node: CharterNode): boolean {
107
+ return node.prereqs.every(
108
+ (prereq) => prereq === CHARTER_NONE || builtCharter(world, prereq) !== undefined
109
+ )
83
110
  }
84
111
 
85
112
  export function charterIneligible(
@@ -88,7 +115,6 @@ export function charterIneligible(
88
115
  ): CharterIneligibility | undefined {
89
116
  if (builtCharter(world, node.nodeId) !== undefined) return 'already-taken'
90
117
  if (!charterPrereqsMet(world, node)) return 'prereq-missing'
91
- if (!charterEffectTargetPresent(world, node)) return 'refit-target-missing'
92
118
  return undefined
93
119
  }
94
120
 
@@ -110,11 +136,15 @@ export function charterSingletonMandate(world: CharterWorld): number {
110
136
  return only
111
137
  }
112
138
 
113
- export function effectiveMandate(
114
- stored: ChosenCharter,
139
+ export function charterEligibleChained(
115
140
  world: CharterWorld,
116
- epoch: number
117
- ): number {
118
- if (stored.chosen !== CHARTER_NONE && stored.chosenEpoch === epoch) return stored.chosen
119
- return charterSingletonMandate(world)
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
120
150
  }
@@ -0,0 +1,278 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {Name} from '@wharfkit/antelope'
3
+
4
+ import {
5
+ MODULE_BUILDER,
6
+ MODULE_CRAFTER,
7
+ MODULE_LOADER,
8
+ MODULE_STORAGE,
9
+ } from '../capabilities/modules'
10
+ import {
11
+ ITEM_BUILDER_T1,
12
+ ITEM_CONSTRUCTION_DOCK_T1_PACKED,
13
+ ITEM_CRAFTER_T1,
14
+ ITEM_DEPOT_T1_PACKED,
15
+ ITEM_LOADER_T1,
16
+ ITEM_STORAGE_T1,
17
+ ITEM_WORKSHOP_T1_PACKED,
18
+ } from '../data/item-ids'
19
+ import {ENTITY_DEPOT, ENTITY_SHIP, ENTITY_WORKSHOP} from '../data/kind-registry'
20
+ import {decodeStats, encodeStats} from '../derivation/crafting'
21
+ import {CHARTER_REGISTRY, charterNode, type CharterNode} from './charters'
22
+ import {
23
+ civicBuildingFor,
24
+ civicHullItem,
25
+ civicModuleItemFor,
26
+ civicStatModuleType,
27
+ emptyCivicModules,
28
+ fitCivicModules,
29
+ isCivicEntity,
30
+ isCivicKind,
31
+ previewCharterGrants,
32
+ raiseCivicStat,
33
+ type CivicStanding,
34
+ } from './civic'
35
+ import {
36
+ CHARTER_BASELINE_STAT,
37
+ CHARTER_RUNG_STEP,
38
+ CIVIC_DEPOT,
39
+ CIVIC_DOCK,
40
+ CIVIC_GRANT_RUNG,
41
+ CIVIC_SPEED_CAP,
42
+ CIVIC_STAT_CRAFT_SPEED,
43
+ CIVIC_STAT_STORAGE_CAPACITY,
44
+ CIVIC_STAT_TRANSFER_SPEED,
45
+ CIVIC_WORKSHOP,
46
+ } from './constants'
47
+ import {getStatCount} from './quality'
48
+
49
+ const NODE_WORKSHOP = 10001
50
+ const NODE_WORKSHOP_TUNEUPS = [10100201, 10100202, 10100203, 10100204]
51
+ const NODE_WORKSHOP_TUNEUP = NODE_WORKSHOP_TUNEUPS[0]
52
+ const NODE_DOCK = 30100001
53
+ const NODE_DOCK_TUNEUPS = [30100301, 30100302, 30100303, 30100304]
54
+ const NODE_DEPOT = 40100001
55
+ const NODE_DEPOT_BAYS = [40100101, 40100102, 40100103, 40100104]
56
+ const NODE_DEPOT_LOADERS = [40100401, 40100402, 40100403, 40100404]
57
+
58
+ function node(nodeId: number): CharterNode {
59
+ const found = charterNode(nodeId)
60
+ if (!found) throw new Error(`no charter node ${nodeId}`)
61
+ return found
62
+ }
63
+
64
+ function stats(
65
+ preview: {modules: {installed?: {item_id: unknown; stats: unknown}}[]},
66
+ slot: number
67
+ ) {
68
+ const installed = preview.modules[slot].installed
69
+ if (!installed) throw new Error(`slot ${slot} is empty`)
70
+ const itemId = Number(installed.item_id)
71
+ return {itemId, stats: decodeStats(BigInt(String(installed.stats)), getStatCount(itemId))}
72
+ }
73
+
74
+ function complete(standing: CivicStanding, nodeId: number): CivicStanding {
75
+ const next = {...standing}
76
+ for (const preview of previewCharterGrants(node(nodeId), standing)) {
77
+ next[preview.building] = preview.modules
78
+ }
79
+ return next
80
+ }
81
+
82
+ describe('civic building tables', () => {
83
+ test('each building maps to its restored hull item', () => {
84
+ expect(civicHullItem(CIVIC_WORKSHOP)).toBe(ITEM_WORKSHOP_T1_PACKED)
85
+ expect(civicHullItem(CIVIC_DOCK)).toBe(ITEM_CONSTRUCTION_DOCK_T1_PACKED)
86
+ expect(civicHullItem(CIVIC_DEPOT)).toBe(ITEM_DEPOT_T1_PACKED)
87
+ expect(civicHullItem(0)).toBe(0)
88
+ })
89
+
90
+ test('each civic stat is carried by one player T1 module type', () => {
91
+ expect(civicStatModuleType(CIVIC_STAT_CRAFT_SPEED)).toBe(MODULE_CRAFTER)
92
+ expect(civicStatModuleType(CIVIC_STAT_STORAGE_CAPACITY)).toBe(MODULE_STORAGE)
93
+ expect(civicStatModuleType(CIVIC_STAT_TRANSFER_SPEED)).toBe(MODULE_LOADER)
94
+ expect(civicModuleItemFor(MODULE_CRAFTER)).toBe(ITEM_CRAFTER_T1)
95
+ expect(civicModuleItemFor(MODULE_BUILDER)).toBe(ITEM_BUILDER_T1)
96
+ expect(civicModuleItemFor(MODULE_LOADER)).toBe(ITEM_LOADER_T1)
97
+ expect(civicModuleItemFor(MODULE_STORAGE)).toBe(ITEM_STORAGE_T1)
98
+ })
99
+
100
+ test('civic kinds are the four buildings and nothing else', () => {
101
+ expect(isCivicKind(ENTITY_WORKSHOP)).toBe(true)
102
+ expect(isCivicKind('depot')).toBe(true)
103
+ expect(isCivicKind(ENTITY_SHIP)).toBe(false)
104
+ expect(civicBuildingFor(ENTITY_DEPOT)).toBe(CIVIC_DEPOT)
105
+ })
106
+
107
+ test('civic-ness is the owner, not the kind', () => {
108
+ const civic = Name.from('eon.shipload')
109
+ expect(isCivicEntity({owner: 'eon.shipload'}, civic)).toBe(true)
110
+ expect(isCivicEntity({owner: Name.from('alice')}, civic)).toBe(false)
111
+ })
112
+
113
+ test('every rung in the registry lands on a stat its building defines', () => {
114
+ for (const entry of CHARTER_REGISTRY) {
115
+ for (const grant of entry.grants) {
116
+ if (grant.kind !== CIVIC_GRANT_RUNG) continue
117
+ expect(civicStatModuleType(grant.stat)).not.toBe(0)
118
+ const layout = emptyCivicModules(grant.building).map((m) => Number(m.type))
119
+ expect(layout).toContain(civicStatModuleType(grant.stat))
120
+ }
121
+ }
122
+ })
123
+ })
124
+
125
+ describe('the fourteen nodes reproduce the chain-fitted buildings', () => {
126
+ test('the Workshop node spawns five crafters at the baseline stat', () => {
127
+ const [workshop] = previewCharterGrants(node(NODE_WORKSHOP))
128
+ expect(workshop.building).toBe(CIVIC_WORKSHOP)
129
+ expect(workshop.spawned).toBe(true)
130
+ expect(workshop.itemId).toBe(ITEM_WORKSHOP_T1_PACKED)
131
+ expect(workshop.modules.length).toBe(5)
132
+ for (let slot = 0; slot < 5; slot++) {
133
+ expect(stats(workshop, slot)).toEqual({
134
+ itemId: ITEM_CRAFTER_T1,
135
+ stats: [CHARTER_BASELINE_STAT, CHARTER_BASELINE_STAT],
136
+ })
137
+ }
138
+ })
139
+
140
+ test('each Workshop tune-up raises every crafter by one rung step, reaching 400', () => {
141
+ let standing = complete({}, NODE_WORKSHOP)
142
+ let raised = CHARTER_BASELINE_STAT
143
+ for (const tuneup of NODE_WORKSHOP_TUNEUPS) {
144
+ const [workshop] = previewCharterGrants(node(tuneup), standing)
145
+ expect(workshop.spawned).toBe(false)
146
+ raised += CHARTER_RUNG_STEP
147
+ for (let slot = 0; slot < 5; slot++) {
148
+ expect(stats(workshop, slot).stats).toEqual([raised, raised])
149
+ }
150
+ standing = complete(standing, tuneup)
151
+ }
152
+ expect(raised).toBe(400)
153
+ })
154
+
155
+ test('the Dock node fits one builder and its four tune-ups reach 400', () => {
156
+ let standing = complete(complete({}, NODE_WORKSHOP), NODE_DOCK)
157
+ expect(stats({modules: standing[CIVIC_DOCK]!}, 0)).toEqual({
158
+ itemId: ITEM_BUILDER_T1,
159
+ stats: [CHARTER_BASELINE_STAT, CHARTER_BASELINE_STAT],
160
+ })
161
+ let raised = CHARTER_BASELINE_STAT
162
+ for (const tuneup of NODE_DOCK_TUNEUPS) {
163
+ const [dock] = previewCharterGrants(node(tuneup), standing)
164
+ raised += CHARTER_RUNG_STEP
165
+ expect(stats(dock, 0).stats).toEqual([raised, raised])
166
+ standing = complete(standing, tuneup)
167
+ }
168
+ expect(raised).toBe(400)
169
+ })
170
+
171
+ test('the Depot node fits a baseline loader and leaves the bays empty', () => {
172
+ const standing = complete(complete({}, NODE_WORKSHOP), NODE_DEPOT)
173
+ const depot = {modules: standing[CIVIC_DEPOT]!}
174
+ expect(stats(depot, 0)).toEqual({
175
+ itemId: ITEM_LOADER_T1,
176
+ stats: [CHARTER_BASELINE_STAT, CHARTER_BASELINE_STAT],
177
+ })
178
+ for (let slot = 1; slot <= 4; slot++) expect(depot.modules[slot].installed).toBeUndefined()
179
+ })
180
+
181
+ test('the four bays fill the storage slots in order at the baseline stat', () => {
182
+ let standing = complete(complete({}, NODE_WORKSHOP), NODE_DEPOT)
183
+ for (let i = 0; i < NODE_DEPOT_BAYS.length; i++) {
184
+ standing = complete(standing, NODE_DEPOT_BAYS[i])
185
+ const depot = {modules: standing[CIVIC_DEPOT]!}
186
+ for (let slot = 1; slot <= 4; slot++) {
187
+ if (slot <= i + 1) {
188
+ expect(stats(depot, slot)).toEqual({
189
+ itemId: ITEM_STORAGE_T1,
190
+ stats: [
191
+ CHARTER_BASELINE_STAT,
192
+ CHARTER_BASELINE_STAT,
193
+ CHARTER_BASELINE_STAT,
194
+ CHARTER_BASELINE_STAT,
195
+ ],
196
+ })
197
+ } else {
198
+ expect(depot.modules[slot].installed).toBeUndefined()
199
+ }
200
+ }
201
+ }
202
+ })
203
+
204
+ test('the four loader rungs step the slot-0 loader by one rung each, reaching 400', () => {
205
+ let standing = complete(complete({}, NODE_WORKSHOP), NODE_DEPOT)
206
+ for (let i = 0; i < NODE_DEPOT_LOADERS.length; i++) {
207
+ standing = complete(standing, NODE_DEPOT_LOADERS[i])
208
+ const depot = {modules: standing[CIVIC_DEPOT]!}
209
+ const stat = CHARTER_BASELINE_STAT + CHARTER_RUNG_STEP * (i + 1)
210
+ expect(stats(depot, 0)).toEqual({itemId: ITEM_LOADER_T1, stats: [stat, stat]})
211
+ for (let slot = 1; slot <= 4; slot++)
212
+ expect(depot.modules[slot].installed).toBeUndefined()
213
+ }
214
+ })
215
+ })
216
+
217
+ describe('grant mechanics', () => {
218
+ test('a module grant joins at a sibling stat', () => {
219
+ const base = fitCivicModules(emptyCivicModules(CIVIC_DEPOT), 1)
220
+ const raised = raiseCivicStat(base, {
221
+ kind: CIVIC_GRANT_RUNG,
222
+ building: CIVIC_DEPOT,
223
+ stat: CIVIC_STAT_TRANSFER_SPEED,
224
+ value: 50,
225
+ })
226
+ const withBay = fitCivicModules(raised, 1)
227
+ expect(stats({modules: withBay}, 1).stats).toEqual([0, 0, 0, 0])
228
+ const withSecondBay = fitCivicModules(
229
+ raiseCivicStat(withBay, {
230
+ kind: CIVIC_GRANT_RUNG,
231
+ building: CIVIC_DEPOT,
232
+ stat: CIVIC_STAT_STORAGE_CAPACITY,
233
+ value: 7,
234
+ }),
235
+ 1
236
+ )
237
+ expect(stats({modules: withSecondBay}, 2).stats).toEqual([7, 7, 7, 7])
238
+ })
239
+
240
+ test('a rung caps at the civic speed cap', () => {
241
+ const fitted = fitCivicModules(emptyCivicModules(CIVIC_WORKSHOP), 5)
242
+ const raised = raiseCivicStat(fitted, {
243
+ kind: CIVIC_GRANT_RUNG,
244
+ building: CIVIC_WORKSHOP,
245
+ stat: CIVIC_STAT_CRAFT_SPEED,
246
+ value: 5000,
247
+ })
248
+ expect(stats({modules: raised}, 0).stats).toEqual([CIVIC_SPEED_CAP, CIVIC_SPEED_CAP])
249
+ })
250
+
251
+ test('a module grant past the layout is refused', () => {
252
+ const full = fitCivicModules(emptyCivicModules(CIVIC_DOCK), 2)
253
+ expect(() => fitCivicModules(full, 1)).toThrow('no open slot')
254
+ })
255
+
256
+ test('a rung with no carrier is refused', () => {
257
+ expect(() =>
258
+ raiseCivicStat(emptyCivicModules(CIVIC_DEPOT), {
259
+ kind: CIVIC_GRANT_RUNG,
260
+ building: CIVIC_DEPOT,
261
+ stat: CIVIC_STAT_TRANSFER_SPEED,
262
+ value: 1,
263
+ })
264
+ ).toThrow('no module carrying')
265
+ })
266
+
267
+ test('a grant on a building that is not standing is refused', () => {
268
+ expect(() => previewCharterGrants(node(NODE_WORKSHOP_TUNEUP))).toThrow('not standing')
269
+ })
270
+
271
+ test('inputs are never mutated', () => {
272
+ const before = emptyCivicModules(CIVIC_WORKSHOP)
273
+ const snapshot = before.map((m) => String(m.installed?.stats ?? 'empty'))
274
+ fitCivicModules(before, 5)
275
+ expect(before.map((m) => String(m.installed?.stats ?? 'empty'))).toEqual(snapshot)
276
+ expect(encodeStats([1, 2])).not.toBe(0n)
277
+ })
278
+ })