@shipload/sdk 2.0.0-rc2 → 2.0.0-rc21
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/README.md +1 -349
- package/lib/shipload.d.ts +1729 -1127
- package/lib/shipload.js +7944 -3165
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +7487 -2840
- package/lib/shipload.m.js.map +1 -1
- package/package.json +6 -4
- package/src/capabilities/crafting.ts +22 -0
- package/src/capabilities/gathering.ts +36 -0
- package/src/capabilities/guards.ts +3 -8
- package/src/capabilities/hauling.ts +22 -0
- package/src/capabilities/index.ts +4 -1
- package/src/capabilities/modules.ts +86 -0
- package/src/capabilities/storage.ts +101 -9
- package/src/contracts/server.ts +785 -293
- package/src/data/capabilities.ts +408 -0
- package/src/data/categories.ts +55 -0
- package/src/data/colors.ts +71 -0
- package/src/data/entities.json +50 -0
- package/src/data/item-ids.ts +75 -0
- package/src/data/items.json +252 -0
- package/src/data/locations.ts +53 -0
- package/src/data/metadata.ts +208 -0
- package/src/data/nebula-adjectives.json +211 -0
- package/src/data/nebula-nouns.json +151 -0
- package/src/data/recipes-runtime.ts +65 -0
- package/src/data/recipes.json +878 -0
- package/src/data/syllables.json +1386 -780
- package/src/data/tiers.ts +45 -0
- package/src/derivation/crafting.ts +348 -0
- package/src/derivation/index.ts +30 -0
- package/src/derivation/location-size.ts +15 -0
- package/src/derivation/resources.ts +112 -0
- package/src/derivation/stats.ts +146 -0
- package/src/derivation/stratum.ts +134 -0
- package/src/derivation/tiers.ts +54 -0
- package/src/entities/cargo-utils.ts +10 -68
- package/src/entities/container.ts +37 -0
- package/src/entities/entity-inventory.ts +13 -13
- package/src/entities/inventory-accessor.ts +2 -6
- package/src/entities/location.ts +5 -200
- package/src/entities/makers.ts +144 -17
- package/src/entities/player.ts +1 -274
- package/src/entities/ship-deploy.ts +258 -0
- package/src/entities/ship.ts +28 -34
- package/src/entities/warehouse.ts +35 -7
- package/src/errors.ts +59 -5
- package/src/format.ts +12 -0
- package/src/index-module.ts +188 -50
- package/src/managers/actions.ts +138 -88
- package/src/managers/context.ts +19 -9
- package/src/managers/index.ts +0 -1
- package/src/managers/locations.ts +2 -85
- package/src/market/items.ts +41 -0
- package/src/nft/description.ts +176 -0
- package/src/nft/deserializers.ts +83 -0
- package/src/nft/index.ts +2 -0
- package/src/resolution/describe-module.ts +165 -0
- package/src/resolution/display-name.ts +43 -0
- package/src/resolution/resolve-item.ts +358 -0
- package/src/scheduling/projection.ts +200 -67
- package/src/scheduling/schedule.ts +2 -2
- package/src/shipload.ts +10 -5
- package/src/subscriptions/connection.ts +154 -0
- package/src/subscriptions/debug.ts +17 -0
- package/src/subscriptions/index.ts +5 -0
- package/src/subscriptions/manager.ts +240 -0
- package/src/subscriptions/mappers.ts +28 -0
- package/src/subscriptions/types.ts +143 -0
- package/src/travel/travel.ts +37 -23
- package/src/types/capabilities.ts +11 -14
- package/src/types/entity-traits.ts +3 -4
- package/src/types/entity.ts +9 -6
- package/src/types.ts +72 -72
- package/src/utils/system.ts +66 -53
- package/src/capabilities/extraction.ts +0 -37
- package/src/data/goods.json +0 -23
- package/src/managers/trades.ts +0 -119
- package/src/market/goods.ts +0 -31
- package/src/market/market.ts +0 -208
- package/src/market/rolls.ts +0 -8
- package/src/trading/collect.ts +0 -938
- package/src/trading/deal.ts +0 -207
- package/src/trading/trade.ts +0 -203
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type CraftedItemCategory = 'component' | 'module' | 'entity' | 'resource'
|
|
2
|
+
|
|
3
|
+
export const ITEM_TYPE_RESOURCE = 0
|
|
4
|
+
export const ITEM_TYPE_COMPONENT = 1
|
|
5
|
+
export const ITEM_TYPE_MODULE = 2
|
|
6
|
+
export const ITEM_TYPE_ENTITY = 3
|
|
7
|
+
|
|
8
|
+
export function itemTypeCode(id: number): number {
|
|
9
|
+
switch (itemCategory(id)) {
|
|
10
|
+
case 'resource':
|
|
11
|
+
return ITEM_TYPE_RESOURCE
|
|
12
|
+
case 'component':
|
|
13
|
+
return ITEM_TYPE_COMPONENT
|
|
14
|
+
case 'module':
|
|
15
|
+
return ITEM_TYPE_MODULE
|
|
16
|
+
case 'entity':
|
|
17
|
+
return ITEM_TYPE_ENTITY
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function itemTier(id: number): number {
|
|
22
|
+
if (id < 10000) return 0
|
|
23
|
+
return Math.floor(id / 10000)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function itemOffset(id: number): number {
|
|
27
|
+
return id % 10000
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function itemCategory(id: number): CraftedItemCategory {
|
|
31
|
+
if (id < 10000) return 'resource'
|
|
32
|
+
const offset = itemOffset(id)
|
|
33
|
+
if (offset >= 200) return 'entity'
|
|
34
|
+
if (offset >= 100) return 'module'
|
|
35
|
+
return 'component'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isRelatedItem(a: number, b: number): boolean {
|
|
39
|
+
if (a < 10000 || b < 10000) return false
|
|
40
|
+
return itemOffset(a) === itemOffset(b)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isCraftedItem(id: number): boolean {
|
|
44
|
+
return id >= 10000
|
|
45
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import {UInt64} from '@wharfkit/antelope'
|
|
2
|
+
import type {ResourceCategory} from '../types'
|
|
3
|
+
import {findItemByCategoryAndTier, getRecipe, Recipe} from '../data/recipes-runtime'
|
|
4
|
+
import {getItem} from '../market/items'
|
|
5
|
+
import {getStatDefinitions} from './stats'
|
|
6
|
+
import {deriveResourceStats} from './stratum'
|
|
7
|
+
|
|
8
|
+
export interface StackInput {
|
|
9
|
+
quantity: number
|
|
10
|
+
stats: Record<string, number>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CategoryStacks {
|
|
14
|
+
category: ResourceCategory
|
|
15
|
+
stacks: StackInput[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function encodeStats(values: number[]): bigint {
|
|
19
|
+
let stats = 0n
|
|
20
|
+
for (let i = 0; i < values.length && i < 6; i++) {
|
|
21
|
+
stats |= BigInt(values[i] & 0x3ff) << BigInt(i * 10)
|
|
22
|
+
}
|
|
23
|
+
return stats
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function decodeStat(stats: bigint, index: number): number {
|
|
27
|
+
return Number((stats >> BigInt(index * 10)) & 0x3ffn)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function decodeStats(stats: bigint, count: number): number[] {
|
|
31
|
+
const result: number[] = []
|
|
32
|
+
for (let i = 0; i < count; i++) {
|
|
33
|
+
result.push(decodeStat(stats, i))
|
|
34
|
+
}
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getItemStatKeys(itemId: number): string[] {
|
|
39
|
+
const item = getItem(itemId)
|
|
40
|
+
if (item.type === 'resource') {
|
|
41
|
+
if (!item.category) return []
|
|
42
|
+
return getStatDefinitions(item.category).map((d) => d.key)
|
|
43
|
+
}
|
|
44
|
+
const recipe = getRecipe(itemId)
|
|
45
|
+
if (!recipe) return []
|
|
46
|
+
return recipe.statSlots.map((slot) => keyForStatSlot(recipe, slot))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function keyForStatSlot(
|
|
50
|
+
recipe: Recipe,
|
|
51
|
+
slot: {sources: {inputIndex: number; statIndex: number}[]}
|
|
52
|
+
): string {
|
|
53
|
+
const src = slot.sources[0]
|
|
54
|
+
if (!src) return ''
|
|
55
|
+
return keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function keyForRecipeInputStat(recipe: Recipe, inputIndex: number, statIndex: number): string {
|
|
59
|
+
const input = recipe.inputs[inputIndex]
|
|
60
|
+
if (!input) return ''
|
|
61
|
+
if ('category' in input) {
|
|
62
|
+
const defs = getStatDefinitions(input.category)
|
|
63
|
+
return defs[statIndex]?.key ?? ''
|
|
64
|
+
}
|
|
65
|
+
// itemId-typed input — its stats follow that item's own statSlots layout.
|
|
66
|
+
const innerKeys = getItemStatKeys(input.itemId)
|
|
67
|
+
return innerKeys[statIndex] ?? ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function decodeCraftedItemStats(itemId: number, stats: bigint): Record<string, number> {
|
|
71
|
+
const keys = getItemStatKeys(itemId)
|
|
72
|
+
const result: Record<string, number> = {}
|
|
73
|
+
for (let i = 0; i < keys.length; i++) {
|
|
74
|
+
if (keys[i]) result[keys[i]] = decodeStat(stats, i)
|
|
75
|
+
}
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function blendStacks(stacks: StackInput[], statKey: string): number {
|
|
80
|
+
let totalQty = 0
|
|
81
|
+
let weightedSum = 0
|
|
82
|
+
for (const stack of stacks) {
|
|
83
|
+
const val = stack.stats[statKey] ?? 0
|
|
84
|
+
weightedSum += val * stack.quantity
|
|
85
|
+
totalQty += stack.quantity
|
|
86
|
+
}
|
|
87
|
+
if (totalQty === 0) return 0
|
|
88
|
+
return Math.floor(weightedSum / totalQty)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function blendComponentStacks(
|
|
92
|
+
stacks: {quantity: number; stats: Record<string, number>}[]
|
|
93
|
+
): Record<string, number> {
|
|
94
|
+
if (stacks.length === 0) return {}
|
|
95
|
+
const allKeys = new Set<string>()
|
|
96
|
+
for (const s of stacks) {
|
|
97
|
+
for (const k of Object.keys(s.stats)) allKeys.add(k)
|
|
98
|
+
}
|
|
99
|
+
const result: Record<string, number> = {}
|
|
100
|
+
for (const key of allKeys) {
|
|
101
|
+
result[key] = blendStacks(
|
|
102
|
+
stacks.map((s) => ({quantity: s.quantity, stats: s.stats})),
|
|
103
|
+
key
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return result
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function computeComponentStats(
|
|
110
|
+
componentId: number,
|
|
111
|
+
categoryStacks: CategoryStacks[]
|
|
112
|
+
): {key: string; value: number}[] {
|
|
113
|
+
const recipe = getRecipe(componentId)
|
|
114
|
+
if (!recipe) return []
|
|
115
|
+
|
|
116
|
+
return recipe.statSlots.map((slot) => {
|
|
117
|
+
const src = slot.sources[0]
|
|
118
|
+
const key = keyForStatSlot(recipe, slot)
|
|
119
|
+
const input = src ? recipe.inputs[src.inputIndex] : undefined
|
|
120
|
+
if (!input || !('category' in input)) {
|
|
121
|
+
return {key, value: Math.max(1, Math.min(999, 0))}
|
|
122
|
+
}
|
|
123
|
+
const matching = categoryStacks.find((cs) => cs.category === input.category)
|
|
124
|
+
const value = matching ? blendStacks(matching.stacks, key) : 0
|
|
125
|
+
return {key, value: Math.max(1, Math.min(999, value))}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function computeEntityStats(
|
|
130
|
+
entityItemIdOrLegacyId: number | string,
|
|
131
|
+
componentStacks: Record<number, {quantity: number; stats: Record<string, number>}[]>
|
|
132
|
+
): {key: string; value: number}[] {
|
|
133
|
+
const itemId =
|
|
134
|
+
typeof entityItemIdOrLegacyId === 'number'
|
|
135
|
+
? entityItemIdOrLegacyId
|
|
136
|
+
: legacyEntityIdToItemId(entityItemIdOrLegacyId)
|
|
137
|
+
const recipe = getRecipe(itemId)
|
|
138
|
+
if (!recipe) return []
|
|
139
|
+
|
|
140
|
+
const blendedByComponent: Record<number, Record<string, number>> = {}
|
|
141
|
+
for (const [compId, stacks] of Object.entries(componentStacks)) {
|
|
142
|
+
blendedByComponent[Number(compId)] = blendComponentStacks(stacks)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return recipe.statSlots.map((slot) => {
|
|
146
|
+
const src = slot.sources[0]
|
|
147
|
+
const key = keyForStatSlot(recipe, slot)
|
|
148
|
+
if (!src) return {key, value: 1}
|
|
149
|
+
const input = recipe.inputs[src.inputIndex]
|
|
150
|
+
if (!input || 'category' in input) {
|
|
151
|
+
return {key, value: 1}
|
|
152
|
+
}
|
|
153
|
+
const blended = blendedByComponent[input.itemId] ?? {}
|
|
154
|
+
const value = blended[key] ?? 0
|
|
155
|
+
return {key, value: Math.max(1, Math.min(999, value))}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function legacyEntityIdToItemId(id: string): number {
|
|
160
|
+
switch (id) {
|
|
161
|
+
case 'container':
|
|
162
|
+
return 10200
|
|
163
|
+
case 'ship-t1':
|
|
164
|
+
return 10201
|
|
165
|
+
case 'warehouse-t1':
|
|
166
|
+
return 10202
|
|
167
|
+
case 'container-t2':
|
|
168
|
+
return 20200
|
|
169
|
+
default:
|
|
170
|
+
return 0
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function decodeStackStats(itemId: number, stats: UInt64): Record<string, number> {
|
|
175
|
+
if (itemId >= 10000) {
|
|
176
|
+
return decodeCraftedItemStats(itemId, BigInt(stats.toString()))
|
|
177
|
+
}
|
|
178
|
+
const s = BigInt(stats.toString())
|
|
179
|
+
return {stat1: decodeStat(s, 0), stat2: decodeStat(s, 1), stat3: decodeStat(s, 2)}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function computeInputMass(itemId: number): number {
|
|
183
|
+
const recipe = getRecipe(itemId)
|
|
184
|
+
if (!recipe) throw new Error(`computeInputMass: no recipe found for itemId=${itemId}`)
|
|
185
|
+
|
|
186
|
+
let total = 0
|
|
187
|
+
for (const input of recipe.inputs) {
|
|
188
|
+
if ('itemId' in input) {
|
|
189
|
+
total += getItem(input.itemId).mass * input.quantity
|
|
190
|
+
} else {
|
|
191
|
+
const item = findItemByCategoryAndTier(input.category, input.tier)
|
|
192
|
+
total += item.mass * input.quantity
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return total
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function blendCrossGroup(sources: {value: number; weight: number}[]): number {
|
|
199
|
+
let weightedSum = 0
|
|
200
|
+
let totalWeight = 0
|
|
201
|
+
for (const src of sources) {
|
|
202
|
+
weightedSum += src.value * src.weight
|
|
203
|
+
totalWeight += src.weight
|
|
204
|
+
}
|
|
205
|
+
if (totalWeight === 0) return 1
|
|
206
|
+
const result = Math.floor(weightedSum / totalWeight)
|
|
207
|
+
return Math.max(1, Math.min(999, result))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function blendCargoStacks(
|
|
211
|
+
itemId: number,
|
|
212
|
+
stacks: {quantity: number; stats: UInt64}[]
|
|
213
|
+
): UInt64 {
|
|
214
|
+
const decoded = stacks.map((s) => ({
|
|
215
|
+
quantity: s.quantity,
|
|
216
|
+
stats: decodeStackStats(itemId, s.stats),
|
|
217
|
+
}))
|
|
218
|
+
const allKeys = Object.keys(decoded[0]?.stats ?? {})
|
|
219
|
+
const blended = allKeys.map((key) => Math.max(1, Math.min(999, blendStacks(decoded, key))))
|
|
220
|
+
return UInt64.from(encodeStats(blended))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface RecipeSlotInput {
|
|
224
|
+
itemId: number
|
|
225
|
+
category: ResourceCategory | undefined
|
|
226
|
+
stacks: {quantity: number; stats: bigint}[]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function decodeRawStackToCategoryStats(
|
|
230
|
+
stats: bigint,
|
|
231
|
+
category: ResourceCategory
|
|
232
|
+
): Record<string, number> {
|
|
233
|
+
const defs = getStatDefinitions(category)
|
|
234
|
+
const result: Record<string, number> = {}
|
|
235
|
+
if (defs[0]) result[defs[0].key] = decodeStat(stats, 0)
|
|
236
|
+
if (defs[1]) result[defs[1].key] = decodeStat(stats, 1)
|
|
237
|
+
if (defs[2]) result[defs[2].key] = decodeStat(stats, 2)
|
|
238
|
+
return result
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function computeCraftedOutputStats(
|
|
242
|
+
outputItemId: number,
|
|
243
|
+
slotInputs: RecipeSlotInput[]
|
|
244
|
+
): UInt64 {
|
|
245
|
+
const recipe = getRecipe(outputItemId)
|
|
246
|
+
if (!recipe) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`computeCraftedOutputStats: no recipe found for outputItemId=${outputItemId}`
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const outputItem = getItem(outputItemId)
|
|
253
|
+
|
|
254
|
+
if (outputItem.type === 'entity') {
|
|
255
|
+
for (const slot of slotInputs) {
|
|
256
|
+
if (slot.category !== undefined) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`entity recipe ${outputItemId} expects component inputs but slot itemId=${slot.itemId} has category=${slot.category}`
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Decode each slot's stacks into key→value maps using the slot item's
|
|
265
|
+
// own stat layout, so blending works regardless of recipe shape.
|
|
266
|
+
const decodedByItem: Record<number, {quantity: number; stats: Record<string, number>}[]> = {}
|
|
267
|
+
const decodedByCategory: Partial<Record<ResourceCategory, StackInput[]>> = {}
|
|
268
|
+
|
|
269
|
+
for (const slot of slotInputs) {
|
|
270
|
+
if (slot.category !== undefined) {
|
|
271
|
+
const list = (decodedByCategory[slot.category] ??= [])
|
|
272
|
+
for (const s of slot.stacks) {
|
|
273
|
+
list.push({
|
|
274
|
+
quantity: s.quantity,
|
|
275
|
+
stats: decodeRawStackToCategoryStats(s.stats, slot.category),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
const list = (decodedByItem[slot.itemId] ??= [])
|
|
280
|
+
for (const s of slot.stacks) {
|
|
281
|
+
list.push({
|
|
282
|
+
quantity: s.quantity,
|
|
283
|
+
stats: decodeCraftedItemStats(slot.itemId, s.stats),
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Pre-blend itemId inputs once per item id.
|
|
290
|
+
const blendedByItem: Record<number, Record<string, number>> = {}
|
|
291
|
+
for (const [id, stacks] of Object.entries(decodedByItem)) {
|
|
292
|
+
blendedByItem[Number(id)] = blendComponentStacks(stacks)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const out: number[] = []
|
|
296
|
+
for (const slot of recipe.statSlots) {
|
|
297
|
+
if (slot.sources.length === 0) {
|
|
298
|
+
out.push(0)
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
if (slot.sources.length === 1 || recipe.blendWeights.length === 0) {
|
|
302
|
+
const src = slot.sources[0]
|
|
303
|
+
const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
|
|
304
|
+
const input = recipe.inputs[src.inputIndex]
|
|
305
|
+
let value = 0
|
|
306
|
+
if (input && 'category' in input) {
|
|
307
|
+
value = blendStacks(decodedByCategory[input.category] ?? [], key)
|
|
308
|
+
} else if (input) {
|
|
309
|
+
value = blendedByItem[input.itemId]?.[key] ?? 0
|
|
310
|
+
}
|
|
311
|
+
out.push(Math.max(1, Math.min(999, value)))
|
|
312
|
+
} else {
|
|
313
|
+
let weightedSum = 0
|
|
314
|
+
let totalWeight = 0
|
|
315
|
+
for (const src of slot.sources) {
|
|
316
|
+
const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
|
|
317
|
+
const input = recipe.inputs[src.inputIndex]
|
|
318
|
+
const weight = recipe.blendWeights[src.inputIndex] ?? 1
|
|
319
|
+
let value = 0
|
|
320
|
+
if (input && 'category' in input) {
|
|
321
|
+
value = blendStacks(decodedByCategory[input.category] ?? [], key)
|
|
322
|
+
} else if (input) {
|
|
323
|
+
value = blendedByItem[input.itemId]?.[key] ?? 0
|
|
324
|
+
}
|
|
325
|
+
weightedSum += value * weight
|
|
326
|
+
totalWeight += weight
|
|
327
|
+
}
|
|
328
|
+
const blended = totalWeight > 0 ? Math.floor(weightedSum / totalWeight) : 0
|
|
329
|
+
out.push(Math.max(1, Math.min(999, blended)))
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return UInt64.from(encodeStats(out))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Mirrors the contract's gather-time transform. Takes a deposit's entropy
|
|
337
|
+
* seed (bigint from deriveStratum), derives stats via weibull hashing, and
|
|
338
|
+
* returns a UInt64 whose bit-packed form matches what the contract writes
|
|
339
|
+
* to cargo_item.stats on gather.
|
|
340
|
+
*
|
|
341
|
+
* Use this whenever off-chain code simulates a gather (testmap, player
|
|
342
|
+
* scanners that project cargo outcomes) and needs a value that matches
|
|
343
|
+
* what on-chain cargo would carry.
|
|
344
|
+
*/
|
|
345
|
+
export function encodeGatheredCargoStats(depositSeed: bigint): UInt64 {
|
|
346
|
+
const raw = deriveResourceStats(depositSeed)
|
|
347
|
+
return UInt64.from(encodeStats([raw.stat1, raw.stat2, raw.stat3]))
|
|
348
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export {deriveStratum, deriveResourceStats} from './stratum'
|
|
2
|
+
export type {StratumInfo, ResourceStats} from './stratum'
|
|
3
|
+
export {deriveLocationSize} from './location-size'
|
|
4
|
+
export {
|
|
5
|
+
getEligibleResources,
|
|
6
|
+
getResourceWeight,
|
|
7
|
+
getLocationCandidates,
|
|
8
|
+
getDepthThreshold,
|
|
9
|
+
getResourceTier,
|
|
10
|
+
DEPTH_THRESHOLD_T1,
|
|
11
|
+
DEPTH_THRESHOLD_T2,
|
|
12
|
+
DEPTH_THRESHOLD_T3,
|
|
13
|
+
DEPTH_THRESHOLD_T4,
|
|
14
|
+
DEPTH_THRESHOLD_T5,
|
|
15
|
+
LOCATION_MIN_DEPTH,
|
|
16
|
+
LOCATION_MAX_DEPTH,
|
|
17
|
+
YIELD_THRESHOLD,
|
|
18
|
+
PLANET_SUBTYPE_GAS_GIANT,
|
|
19
|
+
PLANET_SUBTYPE_ROCKY,
|
|
20
|
+
PLANET_SUBTYPE_TERRESTRIAL,
|
|
21
|
+
PLANET_SUBTYPE_ICY,
|
|
22
|
+
PLANET_SUBTYPE_OCEAN,
|
|
23
|
+
PLANET_SUBTYPE_INDUSTRIAL,
|
|
24
|
+
} from './resources'
|
|
25
|
+
|
|
26
|
+
export {RESERVE_TIERS, TIER_ROLL_MAX, rollTier, rollWithinTier} from './tiers'
|
|
27
|
+
export type {ReserveTier, TierRange} from './tiers'
|
|
28
|
+
|
|
29
|
+
export * from './stats'
|
|
30
|
+
export * from './crafting'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {ServerContract} from '../contracts'
|
|
2
|
+
import {LocationType} from '../types'
|
|
3
|
+
import {LOCATION_MAX_DEPTH, LOCATION_MIN_DEPTH} from './resources'
|
|
4
|
+
|
|
5
|
+
export function deriveLocationSize(loc: ServerContract.Types.location_static): number {
|
|
6
|
+
if (loc.type.toNumber() === LocationType.EMPTY) return 0
|
|
7
|
+
|
|
8
|
+
const raw = (loc.seed0.toNumber() << 8) | loc.seed1.toNumber()
|
|
9
|
+
const normalized = raw / 65535
|
|
10
|
+
|
|
11
|
+
const curved = Math.pow(normalized, 3.0)
|
|
12
|
+
|
|
13
|
+
const range = LOCATION_MAX_DEPTH - LOCATION_MIN_DEPTH
|
|
14
|
+
return Math.floor(LOCATION_MIN_DEPTH + curved * range)
|
|
15
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {getItem} from '../market/items'
|
|
2
|
+
|
|
3
|
+
export const DEPTH_THRESHOLD_T1 = 0
|
|
4
|
+
export const DEPTH_THRESHOLD_T2 = 2000
|
|
5
|
+
export const DEPTH_THRESHOLD_T3 = 10000
|
|
6
|
+
export const DEPTH_THRESHOLD_T4 = 30000
|
|
7
|
+
export const DEPTH_THRESHOLD_T5 = 55000
|
|
8
|
+
|
|
9
|
+
export const LOCATION_MIN_DEPTH = 500
|
|
10
|
+
export const LOCATION_MAX_DEPTH = 65535
|
|
11
|
+
|
|
12
|
+
export const YIELD_THRESHOLD = Math.floor(0.001 * 0xffffffff)
|
|
13
|
+
|
|
14
|
+
export const PLANET_SUBTYPE_GAS_GIANT = 0
|
|
15
|
+
export const PLANET_SUBTYPE_ROCKY = 1
|
|
16
|
+
export const PLANET_SUBTYPE_TERRESTRIAL = 2
|
|
17
|
+
export const PLANET_SUBTYPE_ICY = 3
|
|
18
|
+
export const PLANET_SUBTYPE_OCEAN = 4
|
|
19
|
+
export const PLANET_SUBTYPE_INDUSTRIAL = 5
|
|
20
|
+
|
|
21
|
+
export function getDepthThreshold(tier: number): number {
|
|
22
|
+
switch (tier) {
|
|
23
|
+
case 1:
|
|
24
|
+
return DEPTH_THRESHOLD_T1
|
|
25
|
+
case 2:
|
|
26
|
+
return DEPTH_THRESHOLD_T2
|
|
27
|
+
case 3:
|
|
28
|
+
return DEPTH_THRESHOLD_T3
|
|
29
|
+
case 4:
|
|
30
|
+
return DEPTH_THRESHOLD_T4
|
|
31
|
+
default:
|
|
32
|
+
return DEPTH_THRESHOLD_T5
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getResourceTier(itemId: number): number {
|
|
37
|
+
return getItem(itemId).tier
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getResourceWeight(itemId: number, stratum: number): number {
|
|
41
|
+
const tier = getResourceTier(itemId)
|
|
42
|
+
const threshold = getDepthThreshold(tier)
|
|
43
|
+
if (stratum < threshold) return 0
|
|
44
|
+
|
|
45
|
+
const depthAbove = stratum - threshold
|
|
46
|
+
|
|
47
|
+
switch (tier) {
|
|
48
|
+
case 1:
|
|
49
|
+
if (stratum < 2000) return 100
|
|
50
|
+
if (stratum < 10000) return 80
|
|
51
|
+
if (stratum < 30000) return 50
|
|
52
|
+
return 30
|
|
53
|
+
case 2:
|
|
54
|
+
if (depthAbove < 3000) return 40
|
|
55
|
+
if (depthAbove < 8000) return 60
|
|
56
|
+
return 50
|
|
57
|
+
case 3:
|
|
58
|
+
if (depthAbove < 5000) return 20
|
|
59
|
+
if (depthAbove < 15000) return 35
|
|
60
|
+
return 40
|
|
61
|
+
case 4:
|
|
62
|
+
if (depthAbove < 10000) return 10
|
|
63
|
+
if (depthAbove < 25000) return 20
|
|
64
|
+
return 30
|
|
65
|
+
default:
|
|
66
|
+
return 10
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ASTEROID_RESOURCES = [101, 102, 103, 201, 202]
|
|
71
|
+
const NEBULA_RESOURCES = [202, 203, 301, 302, 303]
|
|
72
|
+
const GAS_GIANT_RESOURCES = [301, 302, 303, 401, 501]
|
|
73
|
+
const ROCKY_RESOURCES = [101, 102, 103, 401, 402, 403, 503]
|
|
74
|
+
const TERRESTRIAL_RESOURCES = [201, 202, 401, 402, 501, 502, 503]
|
|
75
|
+
const ICY_RESOURCES = [101, 301, 302, 401, 403, 501, 502]
|
|
76
|
+
const OCEAN_RESOURCES = [201, 203, 301, 303, 501, 502, 503]
|
|
77
|
+
const INDUSTRIAL_RESOURCES = [101, 102, 103, 201, 203, 402, 403]
|
|
78
|
+
|
|
79
|
+
export function getLocationCandidates(locationType: number, subtype: number): number[] {
|
|
80
|
+
if (locationType === 2) return ASTEROID_RESOURCES
|
|
81
|
+
if (locationType === 3) return NEBULA_RESOURCES
|
|
82
|
+
if (locationType === 1) {
|
|
83
|
+
switch (subtype) {
|
|
84
|
+
case PLANET_SUBTYPE_GAS_GIANT:
|
|
85
|
+
return GAS_GIANT_RESOURCES
|
|
86
|
+
case PLANET_SUBTYPE_ROCKY:
|
|
87
|
+
return ROCKY_RESOURCES
|
|
88
|
+
case PLANET_SUBTYPE_TERRESTRIAL:
|
|
89
|
+
return TERRESTRIAL_RESOURCES
|
|
90
|
+
case PLANET_SUBTYPE_ICY:
|
|
91
|
+
return ICY_RESOURCES
|
|
92
|
+
case PLANET_SUBTYPE_OCEAN:
|
|
93
|
+
return OCEAN_RESOURCES
|
|
94
|
+
case PLANET_SUBTYPE_INDUSTRIAL:
|
|
95
|
+
return INDUSTRIAL_RESOURCES
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getEligibleResources(
|
|
102
|
+
locationType: number,
|
|
103
|
+
subtype: number,
|
|
104
|
+
stratum: number
|
|
105
|
+
): number[] {
|
|
106
|
+
const candidates = getLocationCandidates(locationType, subtype)
|
|
107
|
+
return candidates.filter((itemId) => {
|
|
108
|
+
const tier = getResourceTier(itemId)
|
|
109
|
+
const threshold = getDepthThreshold(tier)
|
|
110
|
+
return stratum >= threshold
|
|
111
|
+
})
|
|
112
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type {ResourceCategory} from '../types'
|
|
2
|
+
|
|
3
|
+
export interface StatDefinition {
|
|
4
|
+
key: string
|
|
5
|
+
label: string
|
|
6
|
+
abbreviation: string
|
|
7
|
+
purpose: string
|
|
8
|
+
inverted?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ORE_STATS: StatDefinition[] = [
|
|
12
|
+
{
|
|
13
|
+
key: 'strength',
|
|
14
|
+
label: 'Strength',
|
|
15
|
+
abbreviation: 'STR',
|
|
16
|
+
purpose: 'Raw structural/mechanical force',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: 'tolerance',
|
|
20
|
+
label: 'Tolerance',
|
|
21
|
+
abbreviation: 'TOL',
|
|
22
|
+
purpose: 'Ability to withstand heat, pressure, and stress extremes',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: 'density',
|
|
26
|
+
label: 'Density',
|
|
27
|
+
abbreviation: 'DEN',
|
|
28
|
+
purpose: 'Mass per unit',
|
|
29
|
+
inverted: true,
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const CRYSTAL_STATS: StatDefinition[] = [
|
|
34
|
+
{
|
|
35
|
+
key: 'conductivity',
|
|
36
|
+
label: 'Conductivity',
|
|
37
|
+
abbreviation: 'CON',
|
|
38
|
+
purpose: 'Efficiency of energy/signal transfer through crystalline lattice',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'resonance',
|
|
42
|
+
label: 'Resonance',
|
|
43
|
+
abbreviation: 'RES',
|
|
44
|
+
purpose: 'Frequency tuning and piezoelectric response — storage and amplification',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'reflectivity',
|
|
48
|
+
label: 'Reflectivity',
|
|
49
|
+
abbreviation: 'REF',
|
|
50
|
+
purpose: 'Optical refraction and reflection — lenses, mirrors, focus',
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const GAS_STATS: StatDefinition[] = [
|
|
55
|
+
{
|
|
56
|
+
key: 'volatility',
|
|
57
|
+
label: 'Volatility',
|
|
58
|
+
abbreviation: 'VOL',
|
|
59
|
+
purpose: 'Energy release potential for propulsion and force',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: 'reactivity',
|
|
63
|
+
label: 'Reactivity',
|
|
64
|
+
abbreviation: 'REA',
|
|
65
|
+
purpose: 'Chemical interaction speed for processing and penetration',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: 'thermal',
|
|
69
|
+
label: 'Thermal',
|
|
70
|
+
abbreviation: 'THM',
|
|
71
|
+
purpose: 'Heat capacity for thermal management',
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const REGOLITH_STATS: StatDefinition[] = [
|
|
76
|
+
{
|
|
77
|
+
key: 'composition',
|
|
78
|
+
label: 'Composition',
|
|
79
|
+
abbreviation: 'COM',
|
|
80
|
+
purpose: 'Mineral/metal mix — drives sensor, chip, and optic crafting quality',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
key: 'hardness',
|
|
84
|
+
label: 'Hardness',
|
|
85
|
+
abbreviation: 'HRD',
|
|
86
|
+
purpose: 'Particle hardness — cutting surfaces, abrasives, wear resistance',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: 'fineness',
|
|
90
|
+
label: 'Fineness',
|
|
91
|
+
abbreviation: 'FIN',
|
|
92
|
+
purpose: 'Grain size — fine powder for smooth composites and sintering',
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const BIOMASS_STATS: StatDefinition[] = [
|
|
97
|
+
{
|
|
98
|
+
key: 'plasticity',
|
|
99
|
+
label: 'Plasticity',
|
|
100
|
+
abbreviation: 'PLA',
|
|
101
|
+
purpose: 'Flexibility and deformation under load',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: 'insulation',
|
|
105
|
+
label: 'Insulation',
|
|
106
|
+
abbreviation: 'INS',
|
|
107
|
+
purpose: 'Thermal and electrical blocking capacity',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: 'saturation',
|
|
111
|
+
label: 'Saturation',
|
|
112
|
+
abbreviation: 'SAT',
|
|
113
|
+
purpose: 'Concentration of useful organic compounds per unit',
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
const STAT_MAP: Record<ResourceCategory, StatDefinition[]> = {
|
|
118
|
+
ore: ORE_STATS,
|
|
119
|
+
crystal: CRYSTAL_STATS,
|
|
120
|
+
gas: GAS_STATS,
|
|
121
|
+
regolith: REGOLITH_STATS,
|
|
122
|
+
biomass: BIOMASS_STATS,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getStatDefinitions(category: ResourceCategory): StatDefinition[] {
|
|
126
|
+
return STAT_MAP[category]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getStatName(category: ResourceCategory, index: 0 | 1 | 2): StatDefinition {
|
|
130
|
+
return STAT_MAP[category][index]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface NamedStats {
|
|
134
|
+
definitions: StatDefinition[]
|
|
135
|
+
values: [number, number, number]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function resolveStats(
|
|
139
|
+
category: ResourceCategory,
|
|
140
|
+
stats: {stat1: number; stat2: number; stat3: number}
|
|
141
|
+
): NamedStats {
|
|
142
|
+
return {
|
|
143
|
+
definitions: STAT_MAP[category],
|
|
144
|
+
values: [stats.stat1, stats.stat2, stats.stat3],
|
|
145
|
+
}
|
|
146
|
+
}
|