@shipload/sdk 1.0.0-next.6 → 1.0.0-next.61
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/scan.d.ts +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +3601 -1364
- package/lib/shipload.js +15671 -5261
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +15392 -5209
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1173 -0
- package/lib/testing.js +4971 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +4965 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.test.ts +82 -0
- package/src/capabilities/craftable.ts +82 -0
- package/src/capabilities/crafting.test.ts +35 -0
- package/src/capabilities/crafting.ts +34 -7
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +37 -19
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +27 -30
- package/src/capabilities/movement.ts +1 -1
- package/src/capabilities/storage.ts +16 -1
- package/src/contracts/platform.ts +231 -3
- package/src/contracts/server.ts +1518 -501
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +49 -18
- package/src/data/capability-formulas.ts +26 -14
- package/src/data/catalog.ts +7 -8
- package/src/data/colors.ts +14 -48
- package/src/data/entities.json +380 -15
- package/src/data/item-ids.ts +51 -13
- package/src/data/items.json +318 -77
- package/src/data/kind-registry.json +195 -0
- package/src/data/kind-registry.ts +182 -0
- package/src/data/metadata.ts +291 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2260 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +13 -0
- package/src/derivation/build-methods.ts +48 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +693 -0
- package/src/derivation/capability-mappings.ts +60 -16
- package/src/derivation/crafting.test.ts +17 -0
- package/src/derivation/crafting.ts +57 -34
- package/src/derivation/index.ts +27 -2
- package/src/derivation/recipe-usage.test.ts +90 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +9 -1
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +72 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +154 -0
- package/src/entities/entity.ts +102 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +141 -135
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/errors.ts +12 -16
- package/src/format.ts +26 -4
- package/src/index-module.ts +333 -35
- package/src/managers/actions.ts +668 -103
- package/src/managers/base.ts +6 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +443 -0
- package/src/managers/context.ts +29 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +27 -66
- package/src/managers/epochs.ts +40 -0
- package/src/managers/index.ts +20 -1
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +14 -0
- package/src/managers/nft.ts +70 -0
- package/src/managers/players.ts +25 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +388 -0
- package/src/nft/description.ts +215 -99
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +242 -0
- package/src/planner/planner.test.ts +334 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +38 -10
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +112 -58
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.test.ts +204 -0
- package/src/scheduling/availability.ts +211 -0
- package/src/scheduling/cancel.test.ts +542 -0
- package/src/scheduling/cancel.ts +254 -0
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.ts +71 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +227 -0
- package/src/scheduling/projection.ts +257 -133
- package/src/scheduling/schedule.test.ts +119 -0
- package/src/scheduling/schedule.ts +257 -117
- package/src/scheduling/task-cargo.test.ts +44 -0
- package/src/scheduling/task-cargo.ts +46 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +26 -1
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.ts +21 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +188 -122
- package/src/types/capabilities.ts +37 -8
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +51 -16
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +29 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/system.ts +15 -16
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- package/src/types/entity-traits.ts +0 -69
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
|
|
2
2
|
import {getStatDefinitions, type StatDefinition} from './stats'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
type Recipe,
|
|
6
|
-
type RecipeInput,
|
|
7
|
-
type RecipeInputCategory,
|
|
8
|
-
} from '../data/recipes-runtime'
|
|
3
|
+
import {getRecipe, type Recipe} from '../data/recipes-runtime'
|
|
4
|
+
import {getItem} from '../data/catalog'
|
|
9
5
|
import {
|
|
10
6
|
ITEM_ENGINE_T1,
|
|
7
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
11
8
|
ITEM_GENERATOR_T1,
|
|
12
9
|
ITEM_GATHERER_T1,
|
|
13
10
|
ITEM_LOADER_T1,
|
|
14
11
|
ITEM_CRAFTER_T1,
|
|
12
|
+
ITEM_BUILDER_T1,
|
|
15
13
|
ITEM_STORAGE_T1,
|
|
16
14
|
ITEM_HAULER_T1,
|
|
17
15
|
ITEM_WARP_T1,
|
|
16
|
+
ITEM_BATTERY_T1,
|
|
18
17
|
ITEM_SHIP_T1_PACKED,
|
|
19
18
|
ITEM_CONTAINER_T1_PACKED,
|
|
20
19
|
ITEM_WAREHOUSE_T1_PACKED,
|
|
21
20
|
ITEM_CONTAINER_T2_PACKED,
|
|
22
21
|
} from '../data/item-ids'
|
|
23
|
-
import type {StatMapping} from '../data/capabilities'
|
|
22
|
+
import type {StatMapping, CapabilityAttributeRow} from '../data/capabilities'
|
|
23
|
+
import {capabilityAttributes} from '../data/capabilities'
|
|
24
24
|
|
|
25
25
|
export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
|
|
26
26
|
engine: ITEM_ENGINE_T1,
|
|
@@ -28,19 +28,18 @@ export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
|
|
|
28
28
|
gatherer: ITEM_GATHERER_T1,
|
|
29
29
|
loader: ITEM_LOADER_T1,
|
|
30
30
|
crafter: ITEM_CRAFTER_T1,
|
|
31
|
+
builder: ITEM_BUILDER_T1,
|
|
31
32
|
storage: ITEM_STORAGE_T1,
|
|
32
33
|
hauler: ITEM_HAULER_T1,
|
|
33
34
|
warp: ITEM_WARP_T1,
|
|
35
|
+
battery: ITEM_BATTERY_T1,
|
|
34
36
|
'ship-t1': ITEM_SHIP_T1_PACKED,
|
|
35
37
|
'container-t1': ITEM_CONTAINER_T1_PACKED,
|
|
36
38
|
'warehouse-t1': ITEM_WAREHOUSE_T1_PACKED,
|
|
39
|
+
'extractor-t1': ITEM_EXTRACTOR_T1_PACKED,
|
|
37
40
|
'container-t2': ITEM_CONTAINER_T2_PACKED,
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
function isCategoryInput(input: RecipeInput): input is RecipeInputCategory {
|
|
41
|
-
return 'category' in input
|
|
42
|
-
}
|
|
43
|
-
|
|
44
43
|
/**
|
|
45
44
|
* Walk a recipe's slot source down to the raw category stat that ultimately
|
|
46
45
|
* lands in that slot. Returns the StatDefinition or undefined if the trace
|
|
@@ -56,8 +55,9 @@ function traceToRawCategoryStat(
|
|
|
56
55
|
): StatDefinition | undefined {
|
|
57
56
|
const input = recipe.inputs[source.inputIndex]
|
|
58
57
|
if (!input) return undefined
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const inputItem = getItem(input.itemId)
|
|
59
|
+
if (inputItem.type === 'resource' && inputItem.category) {
|
|
60
|
+
const defs = getStatDefinitions(inputItem.category)
|
|
61
61
|
return defs[source.statIndex]
|
|
62
62
|
}
|
|
63
63
|
if (visited.has(input.itemId)) return undefined
|
|
@@ -72,6 +72,12 @@ function traceToRawCategoryStat(
|
|
|
72
72
|
return traceToRawCategoryStat(subRecipe, subSource, nextVisited)
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Producing role for a capability·attribute: entity hull slots all roll up to "Hull"; modules use their own name.
|
|
76
|
+
export function sourceLabelForOutput(itemId: number): string {
|
|
77
|
+
const item = getItem(itemId)
|
|
78
|
+
return item.type === 'entity' ? 'Hull' : item.name
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
let cached: StatMapping[] | undefined
|
|
76
82
|
|
|
77
83
|
export function deriveStatMappings(): StatMapping[] {
|
|
@@ -85,20 +91,22 @@ export function deriveStatMappings(): StatMapping[] {
|
|
|
85
91
|
const itemId = KIND_TO_ITEM_ID[kind]
|
|
86
92
|
const recipe = getRecipe(itemId)
|
|
87
93
|
if (!recipe) continue
|
|
94
|
+
const source = sourceLabelForOutput(itemId)
|
|
88
95
|
for (const [slotIdxStr, consumer] of Object.entries(slots)) {
|
|
89
96
|
const slotIdx = Number(slotIdxStr)
|
|
90
97
|
const slot = recipe.statSlots[slotIdx]
|
|
91
98
|
if (!slot) continue
|
|
92
|
-
for (const
|
|
93
|
-
const stat = traceToRawCategoryStat(recipe,
|
|
99
|
+
for (const src of slot.sources) {
|
|
100
|
+
const stat = traceToRawCategoryStat(recipe, src)
|
|
94
101
|
if (!stat) continue
|
|
95
|
-
const key = `${stat.label}|${consumer.capability}|${consumer.attribute}`
|
|
102
|
+
const key = `${stat.label}|${consumer.capability}|${consumer.attribute}|${source}`
|
|
96
103
|
if (seen.has(key)) continue
|
|
97
104
|
seen.add(key)
|
|
98
105
|
out.push({
|
|
99
106
|
stat: stat.label,
|
|
100
107
|
capability: consumer.capability,
|
|
101
108
|
attribute: consumer.attribute,
|
|
109
|
+
source,
|
|
102
110
|
})
|
|
103
111
|
}
|
|
104
112
|
}
|
|
@@ -118,3 +126,39 @@ export function getStatMappingsForStat(stat: string): StatMapping[] {
|
|
|
118
126
|
export function getStatMappingsForCapability(capability: string): StatMapping[] {
|
|
119
127
|
return deriveStatMappings().filter((m) => m.capability === capability)
|
|
120
128
|
}
|
|
129
|
+
|
|
130
|
+
export function getProducersForAttribute(capability: string, attribute: string): string[] {
|
|
131
|
+
const seen = new Set<string>()
|
|
132
|
+
const out: string[] = []
|
|
133
|
+
for (const m of deriveStatMappings()) {
|
|
134
|
+
if (m.capability !== capability || m.attribute !== attribute) continue
|
|
135
|
+
if (seen.has(m.source)) continue
|
|
136
|
+
seen.add(m.source)
|
|
137
|
+
out.push(m.source)
|
|
138
|
+
}
|
|
139
|
+
return out
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getCapabilityAttributeRows(): CapabilityAttributeRow[] {
|
|
143
|
+
const rows: CapabilityAttributeRow[] = []
|
|
144
|
+
for (const ca of capabilityAttributes) {
|
|
145
|
+
const producers = getProducersForAttribute(ca.capability, ca.attribute)
|
|
146
|
+
if (producers.length === 0) {
|
|
147
|
+
rows.push({
|
|
148
|
+
capability: ca.capability,
|
|
149
|
+
attribute: ca.attribute,
|
|
150
|
+
description: ca.description,
|
|
151
|
+
})
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
for (const source of producers) {
|
|
155
|
+
rows.push({
|
|
156
|
+
capability: ca.capability,
|
|
157
|
+
attribute: ca.attribute,
|
|
158
|
+
description: ca.description,
|
|
159
|
+
source,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return rows
|
|
164
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {usedInputStatKeys} from './crafting'
|
|
3
|
+
import {ITEM_BUILDER_T1} from '../data/item-ids'
|
|
4
|
+
|
|
5
|
+
describe('usedInputStatKeys', () => {
|
|
6
|
+
test('maps each input to the stat keys the blend actually consumes', () => {
|
|
7
|
+
// Ceramic.hardness feeds the output's resonance slot; a name match would miss it.
|
|
8
|
+
expect(usedInputStatKeys(ITEM_BUILDER_T1)).toEqual([
|
|
9
|
+
['resonance'],
|
|
10
|
+
['hardness', 'fineness'],
|
|
11
|
+
])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('returns an empty array for an unknown recipe', () => {
|
|
15
|
+
expect(usedInputStatKeys(999999)).toEqual([])
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {UInt64} from '@wharfkit/antelope'
|
|
2
2
|
import type {ResourceCategory} from '../types'
|
|
3
|
-
import {
|
|
3
|
+
import {getRecipe, type Recipe} from '../data/recipes-runtime'
|
|
4
4
|
import {getItem} from '../data/catalog'
|
|
5
5
|
import {getStatDefinitions} from './stats'
|
|
6
6
|
import {deriveResourceStats} from './stratum'
|
|
7
|
+
import {ITEM_SHIP_T1_PACKED} from '../data/item-ids'
|
|
7
8
|
|
|
8
9
|
export interface StackInput {
|
|
9
10
|
quantity: number
|
|
@@ -41,6 +42,7 @@ function getItemStatKeys(itemId: number): string[] {
|
|
|
41
42
|
if (!item.category) return []
|
|
42
43
|
return getStatDefinitions(item.category).map((d) => d.key)
|
|
43
44
|
}
|
|
45
|
+
if (itemId === ITEM_SHIP_T1_PACKED) return ['strength', 'density', '', '']
|
|
44
46
|
const recipe = getRecipe(itemId)
|
|
45
47
|
if (!recipe) return []
|
|
46
48
|
return recipe.statSlots.map((slot) => keyForStatSlot(recipe, slot))
|
|
@@ -58,15 +60,30 @@ function keyForStatSlot(
|
|
|
58
60
|
function keyForRecipeInputStat(recipe: Recipe, inputIndex: number, statIndex: number): string {
|
|
59
61
|
const input = recipe.inputs[inputIndex]
|
|
60
62
|
if (!input) return ''
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return defs[statIndex]?.key ?? ''
|
|
64
|
-
}
|
|
65
|
-
// itemId-typed input — its stats follow that item's own statSlots layout.
|
|
63
|
+
// Every input names an item by id; its stats follow that item's own layout
|
|
64
|
+
// (resource stat definitions for resources, statSlots for crafted items).
|
|
66
65
|
const innerKeys = getItemStatKeys(input.itemId)
|
|
67
66
|
return innerKeys[statIndex] ?? ''
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
export function usedInputStatKeys(outputItemId: number): string[][] {
|
|
70
|
+
const recipe = getRecipe(outputItemId)
|
|
71
|
+
if (!recipe) return []
|
|
72
|
+
const usedIdx = recipe.inputs.map(() => new Set<number>())
|
|
73
|
+
for (const slot of recipe.statSlots) {
|
|
74
|
+
for (const src of slot.sources) {
|
|
75
|
+
usedIdx[src.inputIndex]?.add(src.statIndex)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return recipe.inputs.map((input, i) => {
|
|
79
|
+
const keys = getItemStatKeys(input.itemId)
|
|
80
|
+
return [...usedIdx[i]]
|
|
81
|
+
.sort((a, b) => a - b)
|
|
82
|
+
.map((idx) => keys[idx] ?? '')
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
export function decodeCraftedItemStats(itemId: number, stats: bigint): Record<string, number> {
|
|
71
88
|
const keys = getItemStatKeys(itemId)
|
|
72
89
|
const result: Record<string, number> = {}
|
|
@@ -117,10 +134,11 @@ export function computeComponentStats(
|
|
|
117
134
|
const src = slot.sources[0]
|
|
118
135
|
const key = keyForStatSlot(recipe, slot)
|
|
119
136
|
const input = src ? recipe.inputs[src.inputIndex] : undefined
|
|
120
|
-
|
|
137
|
+
const inputItem = input ? getItem(input.itemId) : undefined
|
|
138
|
+
if (!inputItem || inputItem.type !== 'resource' || !inputItem.category) {
|
|
121
139
|
return {key, value: Math.max(1, Math.min(999, 0))}
|
|
122
140
|
}
|
|
123
|
-
const matching = categoryStacks.find((cs) => cs.category ===
|
|
141
|
+
const matching = categoryStacks.find((cs) => cs.category === inputItem.category)
|
|
124
142
|
const value = matching ? blendStacks(matching.stacks, key) : 0
|
|
125
143
|
return {key, value: Math.max(1, Math.min(999, value))}
|
|
126
144
|
})
|
|
@@ -143,16 +161,20 @@ export function computeEntityStats(
|
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
return recipe.statSlots.map((slot) => {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
const slotKey = keyForStatSlot(recipe, slot)
|
|
165
|
+
if (slot.sources.length === 0) return {key: slotKey, value: 1}
|
|
166
|
+
let weightedSum = 0
|
|
167
|
+
let totalWeight = 0
|
|
168
|
+
for (const src of slot.sources) {
|
|
169
|
+
const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
|
|
170
|
+
const input = recipe.inputs[src.inputIndex]
|
|
171
|
+
if (!input) continue
|
|
172
|
+
const weight = recipe.blendWeights?.[src.inputIndex] ?? 1
|
|
173
|
+
weightedSum += (blendedByComponent[input.itemId]?.[key] ?? 0) * weight
|
|
174
|
+
totalWeight += weight
|
|
152
175
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
return {key, value: Math.max(1, Math.min(999, value))}
|
|
176
|
+
const value = totalWeight > 0 ? Math.floor(weightedSum / totalWeight) : 0
|
|
177
|
+
return {key: slotKey, value: Math.max(1, Math.min(999, value))}
|
|
156
178
|
})
|
|
157
179
|
}
|
|
158
180
|
|
|
@@ -165,7 +187,7 @@ function legacyEntityIdToItemId(id: string): number {
|
|
|
165
187
|
case 'warehouse-t1':
|
|
166
188
|
return 10202
|
|
167
189
|
case 'container-t2':
|
|
168
|
-
return
|
|
190
|
+
return 11200
|
|
169
191
|
default:
|
|
170
192
|
return 0
|
|
171
193
|
}
|
|
@@ -185,12 +207,7 @@ export function computeInputMass(itemId: number): number {
|
|
|
185
207
|
|
|
186
208
|
let total = 0
|
|
187
209
|
for (const input of recipe.inputs) {
|
|
188
|
-
|
|
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
|
-
}
|
|
210
|
+
total += getItem(input.itemId).mass * input.quantity
|
|
194
211
|
}
|
|
195
212
|
return total
|
|
196
213
|
}
|
|
@@ -300,15 +317,18 @@ export function computeCraftedOutputStats(
|
|
|
300
317
|
out.push(0)
|
|
301
318
|
continue
|
|
302
319
|
}
|
|
303
|
-
if (slot.sources.length === 1
|
|
320
|
+
if (slot.sources.length === 1) {
|
|
304
321
|
const src = slot.sources[0]
|
|
305
322
|
const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
|
|
306
323
|
const input = recipe.inputs[src.inputIndex]
|
|
307
324
|
let value = 0
|
|
308
|
-
if (input
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
325
|
+
if (input) {
|
|
326
|
+
const inputItem = getItem(input.itemId)
|
|
327
|
+
if (inputItem.type === 'resource' && inputItem.category) {
|
|
328
|
+
value = blendStacks(decodedByCategory[inputItem.category] ?? [], key)
|
|
329
|
+
} else {
|
|
330
|
+
value = blendedByItem[input.itemId]?.[key] ?? 0
|
|
331
|
+
}
|
|
312
332
|
}
|
|
313
333
|
out.push(Math.max(1, Math.min(999, value)))
|
|
314
334
|
} else {
|
|
@@ -319,10 +339,13 @@ export function computeCraftedOutputStats(
|
|
|
319
339
|
const input = recipe.inputs[src.inputIndex]
|
|
320
340
|
const weight = recipe.blendWeights[src.inputIndex] ?? 1
|
|
321
341
|
let value = 0
|
|
322
|
-
if (input
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
342
|
+
if (input) {
|
|
343
|
+
const inputItem = getItem(input.itemId)
|
|
344
|
+
if (inputItem.type === 'resource' && inputItem.category) {
|
|
345
|
+
value = blendStacks(decodedByCategory[inputItem.category] ?? [], key)
|
|
346
|
+
} else {
|
|
347
|
+
value = blendedByItem[input.itemId]?.[key] ?? 0
|
|
348
|
+
}
|
|
326
349
|
}
|
|
327
350
|
weightedSum += value * weight
|
|
328
351
|
totalWeight += weight
|
|
@@ -340,7 +363,7 @@ export function computeCraftedOutputStats(
|
|
|
340
363
|
* returns a UInt64 whose bit-packed form matches what the contract writes
|
|
341
364
|
* to cargo_item.stats on gather.
|
|
342
365
|
*
|
|
343
|
-
* Use this whenever off-chain code simulates a gather (
|
|
366
|
+
* Use this whenever off-chain code simulates a gather (webapp, player
|
|
344
367
|
* scanners that project cargo outcomes) and needs a value that matches
|
|
345
368
|
* what on-chain cargo would carry.
|
|
346
369
|
*/
|
package/src/derivation/index.ts
CHANGED
|
@@ -17,7 +17,9 @@ export {
|
|
|
17
17
|
DEPTH_THRESHOLD_T5,
|
|
18
18
|
LOCATION_MIN_DEPTH,
|
|
19
19
|
LOCATION_MAX_DEPTH,
|
|
20
|
-
|
|
20
|
+
yieldThresholdAt,
|
|
21
|
+
YIELD_FRACTION_SHALLOW,
|
|
22
|
+
YIELD_FRACTION_DEEP,
|
|
21
23
|
PLANET_SUBTYPE_GAS_GIANT,
|
|
22
24
|
PLANET_SUBTYPE_ROCKY,
|
|
23
25
|
PLANET_SUBTYPE_TERRESTRIAL,
|
|
@@ -26,8 +28,31 @@ export {
|
|
|
26
28
|
PLANET_SUBTYPE_INDUSTRIAL,
|
|
27
29
|
} from './resources'
|
|
28
30
|
|
|
29
|
-
export {
|
|
31
|
+
export {
|
|
32
|
+
RESERVE_TIERS,
|
|
33
|
+
TIER_ROLL_MAX,
|
|
34
|
+
tierOfReserve,
|
|
35
|
+
rollTier,
|
|
36
|
+
rollWithinTier,
|
|
37
|
+
RESOURCE_TIER_MULT_TENTHS,
|
|
38
|
+
applyResourceTierMultiplier,
|
|
39
|
+
} from './tiers'
|
|
30
40
|
export type {ReserveTier, TierRange} from './tiers'
|
|
31
41
|
|
|
42
|
+
export {getEffectiveReserve} from './reserve-regen'
|
|
43
|
+
export type {EffectiveReserveInput} from './reserve-regen'
|
|
44
|
+
|
|
32
45
|
export * from './stats'
|
|
33
46
|
export * from './crafting'
|
|
47
|
+
export * from './upgrades'
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
STAR_STEP,
|
|
51
|
+
MAX_STARS_PER_STAT,
|
|
52
|
+
MAX_STAR_RATING,
|
|
53
|
+
starsForStat,
|
|
54
|
+
starRating,
|
|
55
|
+
statMagnitude,
|
|
56
|
+
compareByStars,
|
|
57
|
+
} from './stars'
|
|
58
|
+
export type {StarSortable} from './stars'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {expect, test} from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
getAllRecipes,
|
|
4
|
+
getRecipeConsumers,
|
|
5
|
+
getComponentDemand,
|
|
6
|
+
getResourceDemand,
|
|
7
|
+
} from './recipe-usage'
|
|
8
|
+
import {
|
|
9
|
+
ITEM_SENSOR,
|
|
10
|
+
ITEM_RESIN,
|
|
11
|
+
ITEM_FRAME,
|
|
12
|
+
ITEM_PLATE,
|
|
13
|
+
ITEM_BEAM,
|
|
14
|
+
ITEM_GATHERER_T1,
|
|
15
|
+
ITEM_CRAFTER_T1,
|
|
16
|
+
ITEM_BUILDER_T1,
|
|
17
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
18
|
+
ITEM_WORKSHOP_T1_PACKED,
|
|
19
|
+
ITEM_ROUSTABOUT_T1A_PACKED,
|
|
20
|
+
ITEM_PROSPECTOR_T1A_PACKED,
|
|
21
|
+
ITEM_TENDER_T1A_PACKED,
|
|
22
|
+
ITEM_TUG_T1A_PACKED,
|
|
23
|
+
ITEM_PORTER_T1A_PACKED,
|
|
24
|
+
} from '../data/item-ids'
|
|
25
|
+
|
|
26
|
+
test('getAllRecipes returns the full catalog including the gatherer', () => {
|
|
27
|
+
const all = getAllRecipes()
|
|
28
|
+
expect(all.length).toBeGreaterThan(20)
|
|
29
|
+
expect(all.some((r) => r.outputItemId === ITEM_GATHERER_T1)).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
|
|
33
|
+
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
34
|
+
const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
|
|
35
|
+
expect(ids).toEqual(
|
|
36
|
+
[
|
|
37
|
+
ITEM_CRAFTER_T1,
|
|
38
|
+
ITEM_BUILDER_T1,
|
|
39
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
40
|
+
ITEM_WORKSHOP_T1_PACKED,
|
|
41
|
+
ITEM_ROUSTABOUT_T1A_PACKED,
|
|
42
|
+
].sort((a, b) => a - b)
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('Frame feeds the gatherer drain stat', () => {
|
|
47
|
+
const consumers = getRecipeConsumers(ITEM_FRAME)
|
|
48
|
+
const gatherer = consumers.find((c) => c.outputItemId === ITEM_GATHERER_T1)
|
|
49
|
+
expect(gatherer).toBeDefined()
|
|
50
|
+
const drain = gatherer?.statFlows.find(
|
|
51
|
+
(f) => f.capability === 'Gathering' && f.attribute === 'drain'
|
|
52
|
+
)
|
|
53
|
+
expect(drain).toBeDefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('Sensor is a mass-only sink in the Extractor recipe', () => {
|
|
57
|
+
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
58
|
+
const extractor = consumers.find((c) => c.outputItemId === ITEM_EXTRACTOR_T1_PACKED)
|
|
59
|
+
expect(extractor).toBeDefined()
|
|
60
|
+
expect(extractor?.statFlows).toHaveLength(0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('getResourceDemand returns the resource tonnage for a single-resource component', () => {
|
|
64
|
+
expect(getResourceDemand(ITEM_PLATE)).toEqual({ore: 10})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('getResourceDemand traces a dual-resource component to both resources', () => {
|
|
68
|
+
expect(getResourceDemand(ITEM_BEAM)).toEqual({ore: 5, gas: 5})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('getResourceDemand recurses through a module to raw resources', () => {
|
|
72
|
+
// Gatherer = 300 Beam (5 ore + 5 gas each) + 300 Frame (5 regolith + 5 biomass each)
|
|
73
|
+
expect(getResourceDemand(ITEM_GATHERER_T1)).toEqual({
|
|
74
|
+
ore: 1500,
|
|
75
|
+
gas: 1500,
|
|
76
|
+
regolith: 1500,
|
|
77
|
+
biomass: 1500,
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('getResourceDemand scales by quantity', () => {
|
|
82
|
+
expect(getResourceDemand(ITEM_PLATE, 3)).toEqual({ore: 30})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('getComponentDemand reports Resin as consumed by eight recipes', () => {
|
|
86
|
+
const demand = getComponentDemand()
|
|
87
|
+
const resin = demand.find((d) => d.itemId === ITEM_RESIN)
|
|
88
|
+
expect(resin).toBeDefined()
|
|
89
|
+
expect(resin?.consumerCount).toBe(8)
|
|
90
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import recipes from '../data/recipes.json'
|
|
2
|
+
import {getRecipe, type Recipe} from '../data/recipes-runtime'
|
|
3
|
+
import {getItem} from '../data/catalog'
|
|
4
|
+
import {getStatDefinitions} from './stats'
|
|
5
|
+
import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
|
|
6
|
+
import {KIND_TO_ITEM_ID} from './capability-mappings'
|
|
7
|
+
import type {ResourceCategory} from '../types'
|
|
8
|
+
|
|
9
|
+
export function getAllRecipes(): Recipe[] {
|
|
10
|
+
return recipes as unknown as Recipe[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ResourceDemand = Partial<Record<ResourceCategory, number>>
|
|
14
|
+
|
|
15
|
+
function accumulateResourceDemand(itemId: number, quantity: number, out: ResourceDemand): void {
|
|
16
|
+
const item = getItem(itemId)
|
|
17
|
+
if (item.type === 'resource' && item.category) {
|
|
18
|
+
out[item.category] = (out[item.category] ?? 0) + quantity
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
const recipe = getRecipe(itemId)
|
|
22
|
+
if (!recipe) return
|
|
23
|
+
for (const input of recipe.inputs) {
|
|
24
|
+
accumulateResourceDemand(input.itemId, input.quantity * quantity, out)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Raw-resource tonnage to craft `quantity` of an item, tracing recipes to the resource leaves.
|
|
29
|
+
export function getResourceDemand(itemId: number, quantity = 1): ResourceDemand {
|
|
30
|
+
const out: ResourceDemand = {}
|
|
31
|
+
accumulateResourceDemand(itemId, quantity, out)
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ITEM_ID_TO_KIND = new Map<number, SlotConsumerKind>()
|
|
36
|
+
for (const [kind, itemId] of Object.entries(KIND_TO_ITEM_ID) as [SlotConsumerKind, number][]) {
|
|
37
|
+
ITEM_ID_TO_KIND.set(itemId, kind)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Traces a stat index down to the raw category stat label it carries (Sensor stat 0 → "Conductivity").
|
|
41
|
+
function resolveComponentStatLabel(itemId: number, statIndex: number): string | undefined {
|
|
42
|
+
let item: ReturnType<typeof getItem>
|
|
43
|
+
try {
|
|
44
|
+
item = getItem(itemId)
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
if (item.type === 'resource' && item.category) {
|
|
49
|
+
return getStatDefinitions(item.category)[statIndex]?.label
|
|
50
|
+
}
|
|
51
|
+
const recipe = getRecipe(itemId)
|
|
52
|
+
const slot = recipe?.statSlots[statIndex]
|
|
53
|
+
const source = slot?.sources[0]
|
|
54
|
+
if (!recipe || !source) return undefined
|
|
55
|
+
const input = recipe.inputs[source.inputIndex]
|
|
56
|
+
if (!input) return undefined
|
|
57
|
+
return resolveComponentStatLabel(input.itemId, source.statIndex)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface StatFlow {
|
|
61
|
+
slotIndex: number
|
|
62
|
+
capability?: string
|
|
63
|
+
attribute?: string
|
|
64
|
+
sourceStatIndex: number
|
|
65
|
+
sourceStatLabel?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RecipeConsumer {
|
|
69
|
+
outputItemId: number
|
|
70
|
+
quantity: number
|
|
71
|
+
statFlows: StatFlow[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Every recipe that consumes `componentItemId`, with how its stats flow through. */
|
|
75
|
+
export function getRecipeConsumers(componentItemId: number): RecipeConsumer[] {
|
|
76
|
+
const out: RecipeConsumer[] = []
|
|
77
|
+
for (const recipe of getAllRecipes()) {
|
|
78
|
+
for (let inputIndex = 0; inputIndex < recipe.inputs.length; inputIndex++) {
|
|
79
|
+
if (recipe.inputs[inputIndex].itemId !== componentItemId) continue
|
|
80
|
+
const kind = ITEM_ID_TO_KIND.get(recipe.outputItemId)
|
|
81
|
+
const formulas = kind ? SLOT_FORMULAS[kind] : undefined
|
|
82
|
+
const statFlows: StatFlow[] = []
|
|
83
|
+
for (let slotIndex = 0; slotIndex < recipe.statSlots.length; slotIndex++) {
|
|
84
|
+
for (const source of recipe.statSlots[slotIndex].sources) {
|
|
85
|
+
if (source.inputIndex !== inputIndex) continue
|
|
86
|
+
const consumer = formulas?.[slotIndex]
|
|
87
|
+
statFlows.push({
|
|
88
|
+
slotIndex,
|
|
89
|
+
capability: consumer?.capability,
|
|
90
|
+
attribute: consumer?.attribute,
|
|
91
|
+
sourceStatIndex: source.statIndex,
|
|
92
|
+
sourceStatLabel: resolveComponentStatLabel(
|
|
93
|
+
componentItemId,
|
|
94
|
+
source.statIndex
|
|
95
|
+
),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out.push({
|
|
100
|
+
outputItemId: recipe.outputItemId,
|
|
101
|
+
quantity: recipe.inputs[inputIndex].quantity,
|
|
102
|
+
statFlows,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DemandRow {
|
|
110
|
+
itemId: number
|
|
111
|
+
consumerCount: number
|
|
112
|
+
statSourceCount: number
|
|
113
|
+
sinkOnlyCount: number
|
|
114
|
+
consumers: number[]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Demand tally for every item consumed as a recipe input, ascending by usage. */
|
|
118
|
+
export function getComponentDemand(): DemandRow[] {
|
|
119
|
+
const inputIds = new Set<number>()
|
|
120
|
+
for (const recipe of getAllRecipes()) {
|
|
121
|
+
for (const input of recipe.inputs) inputIds.add(input.itemId)
|
|
122
|
+
}
|
|
123
|
+
const rows: DemandRow[] = []
|
|
124
|
+
for (const itemId of inputIds) {
|
|
125
|
+
const consumers = getRecipeConsumers(itemId)
|
|
126
|
+
let statSourceCount = 0
|
|
127
|
+
let sinkOnlyCount = 0
|
|
128
|
+
for (const c of consumers) {
|
|
129
|
+
if (c.statFlows.length > 0) statSourceCount++
|
|
130
|
+
else sinkOnlyCount++
|
|
131
|
+
}
|
|
132
|
+
rows.push({
|
|
133
|
+
itemId,
|
|
134
|
+
consumerCount: consumers.length,
|
|
135
|
+
statSourceCount,
|
|
136
|
+
sinkOnlyCount,
|
|
137
|
+
consumers: consumers.map((c) => c.outputItemId),
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
return rows.sort((a, b) => a.consumerCount - b.consumerCount || a.itemId - b.itemId)
|
|
141
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {BlockTimestamp, UInt32} from '@wharfkit/antelope'
|
|
2
|
+
|
|
3
|
+
export interface EffectiveReserveInput {
|
|
4
|
+
remaining: UInt32 | number
|
|
5
|
+
max_reserve: UInt32 | number
|
|
6
|
+
last_block: BlockTimestamp
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toNumber(value: UInt32 | number): number {
|
|
10
|
+
return typeof value === 'number' ? value : Number(value)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function slotsBetween(now: BlockTimestamp, last: BlockTimestamp): number {
|
|
14
|
+
const nowMs = now.toMilliseconds()
|
|
15
|
+
const lastMs = last.toMilliseconds()
|
|
16
|
+
if (nowMs <= lastMs) return 0
|
|
17
|
+
return Math.floor((nowMs - lastMs) / 500)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getEffectiveReserve(
|
|
21
|
+
row: EffectiveReserveInput,
|
|
22
|
+
now: BlockTimestamp,
|
|
23
|
+
epochSeconds: number
|
|
24
|
+
): number {
|
|
25
|
+
const remaining = toNumber(row.remaining)
|
|
26
|
+
const max = toNumber(row.max_reserve)
|
|
27
|
+
if (remaining >= max) return max
|
|
28
|
+
const epochSlots = epochSeconds * 2
|
|
29
|
+
if (epochSlots === 0) return remaining
|
|
30
|
+
const elapsed = slotsBetween(now, row.last_block)
|
|
31
|
+
const regen = Math.floor((max * elapsed) / epochSlots)
|
|
32
|
+
const effective = remaining + regen
|
|
33
|
+
return effective >= max ? max : effective
|
|
34
|
+
}
|
|
@@ -15,7 +15,15 @@ export const DEPTH_THRESHOLD_T10 = 63000
|
|
|
15
15
|
export const LOCATION_MIN_DEPTH = 500
|
|
16
16
|
export const LOCATION_MAX_DEPTH = 65535
|
|
17
17
|
|
|
18
|
-
export const
|
|
18
|
+
export const YIELD_FRACTION_SHALLOW = 0.002
|
|
19
|
+
export const YIELD_FRACTION_DEEP = 0.0004
|
|
20
|
+
|
|
21
|
+
export function yieldThresholdAt(stratum: number): number {
|
|
22
|
+
const clamped = stratum > 65535 ? 65535 : stratum
|
|
23
|
+
const t = clamped / 65535
|
|
24
|
+
const fraction = YIELD_FRACTION_SHALLOW + (YIELD_FRACTION_DEEP - YIELD_FRACTION_SHALLOW) * t
|
|
25
|
+
return Math.floor(fraction * 0xffffffff)
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
export const PLANET_SUBTYPE_GAS_GIANT = 0
|
|
21
29
|
export const PLANET_SUBTYPE_ROCKY = 1
|