@shipload/sdk 1.0.0-next.62 → 1.0.0-next.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/shipload.d.ts +1172 -29
- package/lib/shipload.js +3877 -1294
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +3781 -1298
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +391 -13
- package/lib/testing.js +1554 -114
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +1555 -115
- package/lib/testing.m.js.map +1 -1
- package/package.json +2 -2
- package/src/contracts/fund.ts +201 -0
- package/src/contracts/platform.ts +73 -1
- package/src/contracts/server.ts +946 -26
- package/src/contracts.ts +1 -0
- package/src/data/charters.json +198 -0
- package/src/data/entities.json +36 -0
- package/src/data/item-ids.ts +2 -0
- package/src/data/items.json +12 -0
- package/src/data/kind-registry.json +20 -3
- package/src/data/kind-registry.ts +5 -0
- package/src/data/metadata.ts +14 -0
- package/src/data/recipes.json +0 -48
- package/src/derivation/capabilities.ts +14 -0
- package/src/derivation/recipe-usage.test.ts +0 -2
- package/src/format.ts +9 -0
- package/src/index-module.ts +24 -1
- package/src/influence/categories.ts +32 -0
- package/src/influence/charters.test.ts +126 -0
- package/src/influence/charters.ts +120 -0
- package/src/influence/citizenry.ts +31 -0
- package/src/influence/constants.ts +63 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +36 -0
- package/src/influence/index.ts +63 -0
- package/src/influence/influence.test.ts +291 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +135 -0
- package/src/managers/construction.ts +11 -1
- package/src/managers/context.ts +9 -0
- package/src/managers/entities.ts +11 -0
- package/src/managers/index.ts +12 -0
- package/src/managers/influence.test.ts +58 -0
- package/src/managers/influence.ts +347 -0
- package/src/nft/description.ts +7 -0
- package/src/scheduling/task-cargo.ts +1 -0
- package/src/shipload.ts +5 -0
- package/src/types.ts +23 -4
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {Checksum256} from '@wharfkit/antelope'
|
|
3
|
+
import {ITEM_ORE_T1, ITEM_PLATE, ITEM_CRYSTAL_T1} from '../data/item-ids'
|
|
4
|
+
import {categoryIndex} from './categories'
|
|
5
|
+
import {
|
|
6
|
+
CHARTER_WORKSHOP_COST,
|
|
7
|
+
DEMAND_TRIPLE_SEED,
|
|
8
|
+
INFLUENCE_ATOMIC_PER_POINT,
|
|
9
|
+
INFLUENCE_D1_SEED,
|
|
10
|
+
NEED_FP_SCALE,
|
|
11
|
+
} from './constants'
|
|
12
|
+
import {decayActive, normalizeActive} from './decay'
|
|
13
|
+
import {
|
|
14
|
+
buildDemand,
|
|
15
|
+
deriveDemand,
|
|
16
|
+
lackingMaskFrom,
|
|
17
|
+
needForCategory,
|
|
18
|
+
pickAcuteCategory,
|
|
19
|
+
popcount5,
|
|
20
|
+
} from './demand'
|
|
21
|
+
import {findDecomp, DECOMP_REGISTRY} from './decomp'
|
|
22
|
+
import {contributeDuration} from './duration'
|
|
23
|
+
import {getStatCount, statsSumSq} from './quality'
|
|
24
|
+
import {pricingFromWeights, valueCargoItem, valueContribution} from './valuation'
|
|
25
|
+
import {citizenryName} from './citizenry'
|
|
26
|
+
|
|
27
|
+
const PAR = 213
|
|
28
|
+
const WORLD = {x: 56, y: 0}
|
|
29
|
+
const packStats = (a: number, b = a, c = a): bigint =>
|
|
30
|
+
BigInt(a) | (BigInt(b) << 10n) | (BigInt(c) << 20n)
|
|
31
|
+
|
|
32
|
+
const SEED_WEIGHTS_FP = [10000, 10510, 14289, 21964, 32326, 42658, 52913, 61128, 68570, 75380]
|
|
33
|
+
|
|
34
|
+
const pricing = pricingFromWeights(
|
|
35
|
+
INFLUENCE_D1_SEED,
|
|
36
|
+
Array.from({length: 5}, (_, category) =>
|
|
37
|
+
SEED_WEIGHTS_FP.map((weightFp, i) => ({category, tier: i + 1, weightFp}))
|
|
38
|
+
).flat()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const baseDemand = buildDemand(0, 255, DEMAND_TRIPLE_SEED)
|
|
42
|
+
|
|
43
|
+
describe('decay', () => {
|
|
44
|
+
test('zero epochs is the identity', () => {
|
|
45
|
+
expect(decayActive(1_000_000n, 0)).toBe(1_000_000n)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('one epoch retains 19/20 with a floor', () => {
|
|
49
|
+
expect(decayActive(1_000_000n, 1)).toBe(950_000n)
|
|
50
|
+
expect(decayActive(19n, 1)).toBe(18n)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('is path independent', () => {
|
|
54
|
+
const direct = decayActive(123_456_789n, 7)
|
|
55
|
+
const staged = decayActive(decayActive(123_456_789n, 3), 4)
|
|
56
|
+
expect(staged).toBe(direct)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('early-exits at zero and clamps the iteration count', () => {
|
|
60
|
+
expect(decayActive(0n, 5_000)).toBe(0n)
|
|
61
|
+
expect(decayActive(1_000_000n, 100_000)).toBe(decayActive(1_000_000n, 1_024))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('normalizeActive never decays backwards', () => {
|
|
65
|
+
expect(normalizeActive({active: 1_000n, lastUpdateEpoch: 9}, 4)).toBe(1_000n)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('quality', () => {
|
|
70
|
+
test('par stats give exactly the quality divisor', () => {
|
|
71
|
+
expect(statsSumSq(packStats(PAR), 3)).toBe(3 * INFLUENCE_D1_SEED)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('the retired Workshop falls back to three stats', () => {
|
|
75
|
+
expect(getStatCount(10208)).toBe(3)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('a crafted component reports its recipe slot count', () => {
|
|
79
|
+
expect(getStatCount(ITEM_PLATE)).toBe(3)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('demand', () => {
|
|
84
|
+
test('the lacking mask is the complement over five categories', () => {
|
|
85
|
+
expect(lackingMaskFrom(0b00101)).toBe(0b11010)
|
|
86
|
+
expect(popcount5(0b11010)).toBe(3)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('the acute pick indexes the lacking set only', () => {
|
|
90
|
+
const lacking = 0b10010
|
|
91
|
+
expect(pickAcuteCategory(lacking, 0)).toBe(1)
|
|
92
|
+
expect(pickAcuteCategory(lacking, 1)).toBe(4)
|
|
93
|
+
expect(pickAcuteCategory(lacking, 2)).toBe(1)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('the triple assigns peak, base and floor', () => {
|
|
97
|
+
const view = buildDemand(0b00001, 1, DEMAND_TRIPLE_SEED)
|
|
98
|
+
expect(needForCategory(view, 0)).toBe(DEMAND_TRIPLE_SEED.floor)
|
|
99
|
+
expect(needForCategory(view, 1)).toBe(DEMAND_TRIPLE_SEED.peak)
|
|
100
|
+
expect(needForCategory(view, 2)).toBe(DEMAND_TRIPLE_SEED.base)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('the acute pick rotates with the epoch seed, not the game seed', () => {
|
|
104
|
+
const gameSeed = Checksum256.hash(Buffer.from('game'))
|
|
105
|
+
const coordinates = WORLD
|
|
106
|
+
const picks = new Set<number>()
|
|
107
|
+
for (let i = 0; i < 24; i++) {
|
|
108
|
+
const epochSeed = Checksum256.hash(Buffer.from(`epoch-${i}`))
|
|
109
|
+
picks.add(
|
|
110
|
+
deriveDemand({gameSeed, epochSeed, coordinates, triple: DEMAND_TRIPLE_SEED}).acute
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
expect(picks.size).toBeGreaterThan(1)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('the composition partition is stable across epochs', () => {
|
|
117
|
+
const gameSeed = Checksum256.hash(Buffer.from('game'))
|
|
118
|
+
const coordinates = WORLD
|
|
119
|
+
const masks = new Set<number>()
|
|
120
|
+
for (let i = 0; i < 8; i++) {
|
|
121
|
+
const epochSeed = Checksum256.hash(Buffer.from(`epoch-${i}`))
|
|
122
|
+
masks.add(
|
|
123
|
+
deriveDemand({gameSeed, epochSeed, coordinates, triple: DEMAND_TRIPLE_SEED})
|
|
124
|
+
.abundantMask
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
expect(masks.size).toBe(1)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('decomposition', () => {
|
|
132
|
+
test('the registry is sorted by item id and covers every recipe', () => {
|
|
133
|
+
for (let i = 1; i < DECOMP_REGISTRY.length; i++) {
|
|
134
|
+
expect(DECOMP_REGISTRY[i].itemId).toBeGreaterThan(DECOMP_REGISTRY[i - 1].itemId)
|
|
135
|
+
}
|
|
136
|
+
expect(DECOMP_REGISTRY.length).toBe(57)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('the retired Workshop has no decomposition entry', () => {
|
|
140
|
+
expect(findDecomp(10208)).toBeUndefined()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('buckets are sorted and hold only raw resources', () => {
|
|
144
|
+
const entry = findDecomp(ITEM_PLATE)
|
|
145
|
+
if (!entry) throw new Error('Plate has no decomposition entry')
|
|
146
|
+
expect(entry.processedKg).toBeGreaterThan(0)
|
|
147
|
+
for (let i = 1; i < entry.buckets.length; i++) {
|
|
148
|
+
const prev = entry.buckets[i - 1]
|
|
149
|
+
const cur = entry.buckets[i]
|
|
150
|
+
expect(prev.category < cur.category || prev.tier < cur.tier).toBe(true)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('valuation', () => {
|
|
156
|
+
test('a tonne of par T1 ore at base demand is worth exactly one point', () => {
|
|
157
|
+
const value = valueCargoItem(
|
|
158
|
+
{itemId: ITEM_ORE_T1, quantity: 1, stats: packStats(PAR)},
|
|
159
|
+
baseDemand,
|
|
160
|
+
pricing
|
|
161
|
+
)
|
|
162
|
+
expect(value).toBe(BigInt(INFLUENCE_ATOMIC_PER_POINT))
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('the acute multiplier doubles a resource', () => {
|
|
166
|
+
const acute = buildDemand(0, categoryIndex('crystal'), DEMAND_TRIPLE_SEED)
|
|
167
|
+
const item = {itemId: ITEM_CRYSTAL_T1, quantity: 3, stats: packStats(PAR)}
|
|
168
|
+
expect(valueCargoItem(item, acute, pricing)).toBe(
|
|
169
|
+
valueCargoItem(item, baseDemand, pricing) * 2n
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('resource valuation is split invariant', () => {
|
|
174
|
+
const one = valueCargoItem(
|
|
175
|
+
{itemId: ITEM_ORE_T1, quantity: 1, stats: packStats(97)},
|
|
176
|
+
baseDemand,
|
|
177
|
+
pricing
|
|
178
|
+
)
|
|
179
|
+
const many = valueCargoItem(
|
|
180
|
+
{itemId: ITEM_ORE_T1, quantity: 137, stats: packStats(97)},
|
|
181
|
+
baseDemand,
|
|
182
|
+
pricing
|
|
183
|
+
)
|
|
184
|
+
expect(many).toBe(one * 137n)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('component valuation is split invariant', () => {
|
|
188
|
+
const one = valueCargoItem(
|
|
189
|
+
{itemId: ITEM_PLATE, quantity: 1, stats: packStats(PAR)},
|
|
190
|
+
baseDemand,
|
|
191
|
+
pricing
|
|
192
|
+
)
|
|
193
|
+
const many = valueCargoItem(
|
|
194
|
+
{itemId: ITEM_PLATE, quantity: 40, stats: packStats(PAR)},
|
|
195
|
+
baseDemand,
|
|
196
|
+
pricing
|
|
197
|
+
)
|
|
198
|
+
expect(many).toBe(one * 40n)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('components ignore demand entirely', () => {
|
|
202
|
+
const acute = buildDemand(0, categoryIndex('ore'), DEMAND_TRIPLE_SEED)
|
|
203
|
+
const item = {itemId: ITEM_PLATE, quantity: 5, stats: packStats(PAR)}
|
|
204
|
+
expect(valueCargoItem(item, acute, pricing)).toBe(valueCargoItem(item, baseDemand, pricing))
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('quality scales quadratically within the per-unit floor tolerance', () => {
|
|
208
|
+
const single = valueCargoItem(
|
|
209
|
+
{itemId: ITEM_ORE_T1, quantity: 100, stats: packStats(100)},
|
|
210
|
+
baseDemand,
|
|
211
|
+
pricing
|
|
212
|
+
)
|
|
213
|
+
const doubled = valueCargoItem(
|
|
214
|
+
{itemId: ITEM_ORE_T1, quantity: 100, stats: packStats(200)},
|
|
215
|
+
baseDemand,
|
|
216
|
+
pricing
|
|
217
|
+
)
|
|
218
|
+
const delta = doubled - single * 4n
|
|
219
|
+
expect(delta >= -105n && delta <= 105n).toBe(true)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('a bundle totals its rows', () => {
|
|
223
|
+
const bundle = [
|
|
224
|
+
{itemId: ITEM_ORE_T1, quantity: 10, stats: packStats(PAR)},
|
|
225
|
+
{itemId: ITEM_PLATE, quantity: 2, stats: packStats(PAR)},
|
|
226
|
+
]
|
|
227
|
+
expect(valueContribution(bundle, baseDemand, pricing)).toBe(
|
|
228
|
+
valueCargoItem(bundle[0], baseDemand, pricing) +
|
|
229
|
+
valueCargoItem(bundle[1], baseDemand, pricing)
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('a packed entity cannot be valued', () => {
|
|
234
|
+
expect(() =>
|
|
235
|
+
valueCargoItem({itemId: 10208, quantity: 1, stats: packStats(PAR)}, baseDemand, pricing)
|
|
236
|
+
).toThrow()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('the Workshop charter costs 200,000 points of hauled par T1 material', () => {
|
|
240
|
+
const perTonne = valueCargoItem(
|
|
241
|
+
{itemId: ITEM_ORE_T1, quantity: 200_000, stats: packStats(PAR)},
|
|
242
|
+
baseDemand,
|
|
243
|
+
pricing
|
|
244
|
+
)
|
|
245
|
+
expect(perTonne).toBe(CHARTER_WORKSHOP_COST)
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
describe('contribution duration', () => {
|
|
250
|
+
test('grows with mass and never falls below a single tick', () => {
|
|
251
|
+
expect(contributeDuration(1_000)).toBeGreaterThan(0)
|
|
252
|
+
expect(contributeDuration(1_000_000)).toBeGreaterThan(contributeDuration(1_000))
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('splitting a haul costs more than one trip', () => {
|
|
256
|
+
const single = contributeDuration(100_000_000)
|
|
257
|
+
const hundred = contributeDuration(1_000_000) * 100
|
|
258
|
+
expect(hundred).toBeGreaterThan(single)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('ground level is floored at the base orbital climb', () => {
|
|
262
|
+
expect(contributeDuration(1_000, 0)).toBe(contributeDuration(1_000, 800))
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe('citizenry names', () => {
|
|
267
|
+
const gameSeed = Checksum256.hash(Buffer.from('game'))
|
|
268
|
+
|
|
269
|
+
test('is deterministic for a coordinate', () => {
|
|
270
|
+
const a = citizenryName(gameSeed, WORLD)
|
|
271
|
+
const b = citizenryName(gameSeed, WORLD)
|
|
272
|
+
expect(a).toBeDefined()
|
|
273
|
+
expect(a).toBe(b as string)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('composes the world name', () => {
|
|
277
|
+
const name = citizenryName(gameSeed, WORLD) ?? ''
|
|
278
|
+
expect(name.length).toBeGreaterThan(0)
|
|
279
|
+
expect(/Collective|Compact|Concord of|Union/.test(name)).toBe(true)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('is undefined away from a world', () => {
|
|
283
|
+
expect(citizenryName(gameSeed, {x: 7, y: 11})).toBeUndefined()
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe('scales', () => {
|
|
288
|
+
test('the need scale divides out against the atomic unit', () => {
|
|
289
|
+
expect(NEED_FP_SCALE % INFLUENCE_ATOMIC_PER_POINT).toBe(0)
|
|
290
|
+
})
|
|
291
|
+
})
|
|
@@ -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 {categoryIndex} from './categories'
|
|
3
|
+
import {
|
|
4
|
+
INFLUENCE_ATOMIC_PER_POINT,
|
|
5
|
+
INFLUENCE_MASS_KG_MAX,
|
|
6
|
+
INFLUENCE_NEED_FP_MAX,
|
|
7
|
+
INFLUENCE_WEIGHT_FP_MAX,
|
|
8
|
+
MASS_KG_PER_TONNE,
|
|
9
|
+
NEED_FP_SCALE,
|
|
10
|
+
WEIGHT_FP_SCALE,
|
|
11
|
+
W_EFF_ATOMIC_PER_KG,
|
|
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
|
+
weightedKgFp: 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 = weightedKgFp * BigInt(sumSq) * needFp
|
|
40
|
+
const den =
|
|
41
|
+
(BigInt(MASS_KG_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
|
+
massKg: bigint,
|
|
52
|
+
weightFp: bigint,
|
|
53
|
+
sumSq: number,
|
|
54
|
+
nStats: number,
|
|
55
|
+
d1: number,
|
|
56
|
+
needFp: bigint
|
|
57
|
+
): bigint {
|
|
58
|
+
if (massKg > INFLUENCE_MASS_KG_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(massKg * weightFp, sumSq, nStats, d1, needFp)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function componentBaseAtomic(
|
|
65
|
+
weightedRawKgFp: bigint,
|
|
66
|
+
sumSq: number,
|
|
67
|
+
nStats: number,
|
|
68
|
+
d1: number
|
|
69
|
+
): bigint {
|
|
70
|
+
return weightedQualityAtomic(weightedRawKgFp, sumSq, nStats, d1, BigInt(NEED_FP_SCALE))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function componentEffortAtomic(processedKgPerUnit: number, quantity: number): bigint {
|
|
74
|
+
const processed = BigInt(processedKgPerUnit) * BigInt(quantity)
|
|
75
|
+
if (processed > INFLUENCE_MASS_KG_MAX) {
|
|
76
|
+
throw new Error('influence: processed mass out of range')
|
|
77
|
+
}
|
|
78
|
+
return processed * W_EFF_ATOMIC_PER_KG
|
|
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.rawKg) * 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.processedKg, 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
|
+
}
|
package/src/managers/actions.ts
CHANGED
|
@@ -315,6 +315,30 @@ export class ActionsManager extends BaseManager {
|
|
|
315
315
|
})
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
depotstore(
|
|
319
|
+
shipId: UInt64Type,
|
|
320
|
+
depotId: UInt64Type,
|
|
321
|
+
items: ServerContract.ActionParams.Type.cargo_item[]
|
|
322
|
+
): Action {
|
|
323
|
+
return this.server.action('depotstore', {
|
|
324
|
+
ship_id: UInt64.from(shipId),
|
|
325
|
+
depot_id: UInt64.from(depotId),
|
|
326
|
+
items,
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
depottake(
|
|
331
|
+
shipId: UInt64Type,
|
|
332
|
+
depotId: UInt64Type,
|
|
333
|
+
items: ServerContract.ActionParams.Type.cargo_item[]
|
|
334
|
+
): Action {
|
|
335
|
+
return this.server.action('depottake', {
|
|
336
|
+
ship_id: UInt64.from(shipId),
|
|
337
|
+
depot_id: UInt64.from(depotId),
|
|
338
|
+
items,
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
318
342
|
shuttle(
|
|
319
343
|
carrierId: UInt64Type,
|
|
320
344
|
fromId: UInt64Type,
|
|
@@ -511,6 +535,35 @@ export class ActionsManager extends BaseManager {
|
|
|
511
535
|
return this.server.action('canceljob', params)
|
|
512
536
|
}
|
|
513
537
|
|
|
538
|
+
upgradejob(
|
|
539
|
+
targetId: UInt64Type,
|
|
540
|
+
dockId: UInt64Type,
|
|
541
|
+
targetItemId: UInt16Type,
|
|
542
|
+
inputs: ServerContract.ActionParams.Type.cargo_item[]
|
|
543
|
+
): Action {
|
|
544
|
+
const params: ServerContract.ActionParams.upgradejob = {
|
|
545
|
+
target_id: UInt64.from(targetId),
|
|
546
|
+
dock_id: UInt64.from(dockId),
|
|
547
|
+
target_item_id: UInt16.from(targetItemId),
|
|
548
|
+
inputs,
|
|
549
|
+
}
|
|
550
|
+
return this.server.action('upgradejob', params)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
claimupgrade(jobId: UInt64Type): Action {
|
|
554
|
+
const params: ServerContract.ActionParams.claimupgrade = {
|
|
555
|
+
job_id: UInt64.from(jobId),
|
|
556
|
+
}
|
|
557
|
+
return this.server.action('claimupgrade', params)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
cancelupgrade(jobId: UInt64Type): Action {
|
|
561
|
+
const params: ServerContract.ActionParams.cancelupgrade = {
|
|
562
|
+
job_id: UInt64.from(jobId),
|
|
563
|
+
}
|
|
564
|
+
return this.server.action('cancelupgrade', params)
|
|
565
|
+
}
|
|
566
|
+
|
|
514
567
|
setsocket(workshopId: UInt64Type, slot: UInt8Type, open: boolean): Action {
|
|
515
568
|
const params: ServerContract.ActionParams.setsocket = {
|
|
516
569
|
workshop_id: UInt64.from(workshopId),
|
|
@@ -599,6 +652,12 @@ export class ActionsManager extends BaseManager {
|
|
|
599
652
|
})
|
|
600
653
|
}
|
|
601
654
|
|
|
655
|
+
abandonplot(plotId: UInt64Type): Action {
|
|
656
|
+
return this.server.action('abandonplot', {
|
|
657
|
+
plot_id: UInt64.from(plotId),
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
|
|
602
661
|
addmodule(
|
|
603
662
|
entityId: UInt64Type,
|
|
604
663
|
moduleIndex: number,
|
|
@@ -835,4 +894,80 @@ export class ActionsManager extends BaseManager {
|
|
|
835
894
|
max_rows: UInt64.from(maxRows),
|
|
836
895
|
})
|
|
837
896
|
}
|
|
897
|
+
|
|
898
|
+
found(player: NameType, entityId: UInt64Type, location: CoordinatesType): Action {
|
|
899
|
+
return this.server.action('found', {
|
|
900
|
+
player: Name.from(player),
|
|
901
|
+
entity_id: UInt64.from(entityId),
|
|
902
|
+
x: Int64.from(location.x),
|
|
903
|
+
y: Int64.from(location.y),
|
|
904
|
+
})
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
naturalize(player: NameType, location: CoordinatesType): Action {
|
|
908
|
+
return this.server.action('naturalize', {
|
|
909
|
+
player: Name.from(player),
|
|
910
|
+
x: Int64.from(location.x),
|
|
911
|
+
y: Int64.from(location.y),
|
|
912
|
+
})
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
contribute(
|
|
916
|
+
player: NameType,
|
|
917
|
+
entityId: UInt64Type,
|
|
918
|
+
location: CoordinatesType,
|
|
919
|
+
bundle: ServerContract.ActionParams.Type.cargo_item[]
|
|
920
|
+
): Action {
|
|
921
|
+
return this.platform.action('contribute', {
|
|
922
|
+
game: this.server.account,
|
|
923
|
+
player: Name.from(player),
|
|
924
|
+
entity_id: UInt64.from(entityId),
|
|
925
|
+
x: Int64.from(location.x),
|
|
926
|
+
y: Int64.from(location.y),
|
|
927
|
+
bundle,
|
|
928
|
+
})
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// addcontrib asserts the citizenship rows exist, so naturalize must precede contribute.
|
|
932
|
+
firstContribution(
|
|
933
|
+
player: NameType,
|
|
934
|
+
entityId: UInt64Type,
|
|
935
|
+
location: CoordinatesType,
|
|
936
|
+
bundle: ServerContract.ActionParams.Type.cargo_item[]
|
|
937
|
+
): Action[] {
|
|
938
|
+
return [
|
|
939
|
+
this.naturalize(player, location),
|
|
940
|
+
this.contribute(player, entityId, location, bundle),
|
|
941
|
+
]
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
mintready(maxMints?: UInt32Type): Action {
|
|
945
|
+
const params: {max_mints?: UInt32} = {}
|
|
946
|
+
if (maxMints !== undefined) {
|
|
947
|
+
params.max_mints = UInt32.from(maxMints)
|
|
948
|
+
}
|
|
949
|
+
return this.server.action('mintready', params)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
charterready(location: CoordinatesType): Action {
|
|
953
|
+
return this.server.action('charterready', {
|
|
954
|
+
x: Int64.from(location.x),
|
|
955
|
+
y: Int64.from(location.y),
|
|
956
|
+
})
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
votecharter(player: NameType, location: CoordinatesType, nodeId: UInt32Type): Action {
|
|
960
|
+
return this.server.action('votecharter', {
|
|
961
|
+
player: Name.from(player),
|
|
962
|
+
x: Int64.from(location.x),
|
|
963
|
+
y: Int64.from(location.y),
|
|
964
|
+
node_id: UInt32.from(nodeId),
|
|
965
|
+
})
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
voteready(maxBallots: UInt32Type = UInt32.from(0)): Action {
|
|
969
|
+
return this.server.action('voteready', {
|
|
970
|
+
max_ballots: UInt32.from(maxBallots),
|
|
971
|
+
})
|
|
972
|
+
}
|
|
838
973
|
}
|
|
@@ -4,7 +4,7 @@ import type {ServerContract} from '../contracts'
|
|
|
4
4
|
import {PlotManager} from './plot'
|
|
5
5
|
import {getItem} from '../data/catalog'
|
|
6
6
|
import {calc_build_duration} from '../capabilities/crafting'
|
|
7
|
-
import {getLanes, getTasks} from '../scheduling/schedule'
|
|
7
|
+
import {getLanes, getTasks, isIdle} from '../scheduling/schedule'
|
|
8
8
|
import {HoldKind, TaskType} from '../types'
|
|
9
9
|
import type {
|
|
10
10
|
BuildableTarget,
|
|
@@ -34,6 +34,16 @@ export class ConstructionManager extends BaseManager {
|
|
|
34
34
|
return null
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
canAbandon(
|
|
38
|
+
entity: ServerContract.Types.entity_row,
|
|
39
|
+
cargo: ServerContract.Types.cargo_row[]
|
|
40
|
+
): boolean {
|
|
41
|
+
if (entity.kind.toString() !== 'plot') return false
|
|
42
|
+
if (!isIdle(entity)) return false
|
|
43
|
+
if (Number(entity.cargomass) !== 0) return false
|
|
44
|
+
return !cargo.some((c) => c.entity_id.equals(entity.id))
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
eligibleSources(
|
|
38
48
|
target: BuildableTarget,
|
|
39
49
|
entities: ServerContract.Types.entity_info[],
|
package/src/managers/context.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {ActionsManager} from './actions'
|
|
|
12
12
|
import {ClusterManager} from './cluster'
|
|
13
13
|
import {NftManager} from './nft'
|
|
14
14
|
import {JobsManager} from './jobs'
|
|
15
|
+
import {InfluenceManager} from './influence'
|
|
15
16
|
import {SubscriptionsManager} from '../subscriptions/manager'
|
|
16
17
|
|
|
17
18
|
export class GameContext {
|
|
@@ -24,6 +25,7 @@ export class GameContext {
|
|
|
24
25
|
private _clusters?: ClusterManager
|
|
25
26
|
private _nft?: NftManager
|
|
26
27
|
private _jobs?: JobsManager
|
|
28
|
+
private _influence?: InfluenceManager
|
|
27
29
|
private _subscriptions?: SubscriptionsManager
|
|
28
30
|
private _subscriptionsUrl?: string
|
|
29
31
|
|
|
@@ -100,6 +102,13 @@ export class GameContext {
|
|
|
100
102
|
return this._jobs
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
get influence(): InfluenceManager {
|
|
106
|
+
if (!this._influence) {
|
|
107
|
+
this._influence = new InfluenceManager(this)
|
|
108
|
+
}
|
|
109
|
+
return this._influence
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
setSubscriptionsUrl(url: string) {
|
|
104
113
|
this._subscriptionsUrl = url
|
|
105
114
|
}
|
package/src/managers/entities.ts
CHANGED
|
@@ -55,6 +55,17 @@ export class EntitiesManager extends BaseManager {
|
|
|
55
55
|
return result as ServerContract.Types.entity_summary[]
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
async getDepot(
|
|
59
|
+
depotId: UInt64Type,
|
|
60
|
+
owner: NameType | ServerContract.Types.player_row
|
|
61
|
+
): Promise<ServerContract.Types.depot_view> {
|
|
62
|
+
const result = await this.server.readonly('getdepot', {
|
|
63
|
+
depot_id: depotId,
|
|
64
|
+
owner: this.resolveOwner(owner),
|
|
65
|
+
})
|
|
66
|
+
return result as ServerContract.Types.depot_view
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
private resolveOwner(owner: NameType | ServerContract.Types.player_row): Name {
|
|
59
70
|
if (typeof owner === 'object' && owner !== null && 'owner' in owner) {
|
|
60
71
|
return owner.owner
|