@shipload/sdk 1.0.0-next.5 → 1.0.0-next.50
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 +2881 -1149
- package/lib/shipload.js +13031 -5511
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +12807 -5460
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1028 -0
- package/lib/testing.js +4336 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +4330 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.ts +51 -0
- package/src/capabilities/crafting.test.ts +7 -0
- package/src/capabilities/crafting.ts +6 -7
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +37 -19
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +22 -0
- package/src/capabilities/storage.ts +16 -1
- package/src/contracts/platform.ts +231 -3
- package/src/contracts/server.ts +1160 -520
- 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 +31 -16
- package/src/data/capability-formulas.ts +21 -14
- package/src/data/catalog.ts +0 -5
- package/src/data/colors.ts +13 -47
- package/src/data/entities.json +154 -10
- package/src/data/item-ids.ts +31 -12
- package/src/data/items.json +192 -74
- package/src/data/kind-registry.json +131 -0
- package/src/data/kind-registry.ts +172 -0
- package/src/data/metadata.ts +166 -48
- package/src/data/recipes-runtime.ts +5 -23
- package/src/data/recipes.json +1003 -163
- package/src/derivation/build-methods.test.ts +13 -0
- package/src/derivation/build-methods.ts +48 -0
- package/src/derivation/capabilities.test.ts +151 -0
- package/src/derivation/capabilities.ts +527 -0
- package/src/derivation/capability-mappings.ts +58 -16
- package/src/derivation/crafting.ts +36 -33
- package/src/derivation/index.ts +28 -2
- package/src/derivation/recipe-usage.test.ts +81 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +98 -19
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +56 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -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 +141 -0
- package/src/entities/entity.ts +98 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +131 -134
- 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 +290 -34
- package/src/managers/actions.ts +602 -105
- 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 +412 -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/flatten-gather-plan.test.ts +80 -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 +336 -0
- package/src/nft/description.ts +104 -36
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +242 -0
- package/src/planner/planner.test.ts +325 -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 +74 -47
- 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.ts +108 -0
- package/src/scheduling/cancel.test.ts +348 -0
- package/src/scheduling/cancel.ts +209 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +249 -0
- package/src/scheduling/lanes.ts +198 -0
- package/src/scheduling/projection.ts +210 -107
- package/src/scheduling/schedule.ts +240 -117
- 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 +23 -0
- package/src/travel/route-planner.ts +277 -0
- package/src/travel/travel.ts +148 -117
- package/src/types/capabilities.ts +29 -6
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +36 -14
- 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 +36 -24
- 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,6 +1,6 @@
|
|
|
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'
|
|
@@ -58,11 +58,8 @@ function keyForStatSlot(
|
|
|
58
58
|
function keyForRecipeInputStat(recipe: Recipe, inputIndex: number, statIndex: number): string {
|
|
59
59
|
const input = recipe.inputs[inputIndex]
|
|
60
60
|
if (!input) return ''
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return defs[statIndex]?.key ?? ''
|
|
64
|
-
}
|
|
65
|
-
// itemId-typed input — its stats follow that item's own statSlots layout.
|
|
61
|
+
// Every input names an item by id; its stats follow that item's own layout
|
|
62
|
+
// (resource stat definitions for resources, statSlots for crafted items).
|
|
66
63
|
const innerKeys = getItemStatKeys(input.itemId)
|
|
67
64
|
return innerKeys[statIndex] ?? ''
|
|
68
65
|
}
|
|
@@ -117,10 +114,11 @@ export function computeComponentStats(
|
|
|
117
114
|
const src = slot.sources[0]
|
|
118
115
|
const key = keyForStatSlot(recipe, slot)
|
|
119
116
|
const input = src ? recipe.inputs[src.inputIndex] : undefined
|
|
120
|
-
|
|
117
|
+
const inputItem = input ? getItem(input.itemId) : undefined
|
|
118
|
+
if (!inputItem || inputItem.type !== 'resource' || !inputItem.category) {
|
|
121
119
|
return {key, value: Math.max(1, Math.min(999, 0))}
|
|
122
120
|
}
|
|
123
|
-
const matching = categoryStacks.find((cs) => cs.category ===
|
|
121
|
+
const matching = categoryStacks.find((cs) => cs.category === inputItem.category)
|
|
124
122
|
const value = matching ? blendStacks(matching.stacks, key) : 0
|
|
125
123
|
return {key, value: Math.max(1, Math.min(999, value))}
|
|
126
124
|
})
|
|
@@ -143,16 +141,20 @@ export function computeEntityStats(
|
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
return recipe.statSlots.map((slot) => {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
const slotKey = keyForStatSlot(recipe, slot)
|
|
145
|
+
if (slot.sources.length === 0) return {key: slotKey, value: 1}
|
|
146
|
+
let weightedSum = 0
|
|
147
|
+
let totalWeight = 0
|
|
148
|
+
for (const src of slot.sources) {
|
|
149
|
+
const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
|
|
150
|
+
const input = recipe.inputs[src.inputIndex]
|
|
151
|
+
if (!input) continue
|
|
152
|
+
const weight = recipe.blendWeights?.[src.inputIndex] ?? 1
|
|
153
|
+
weightedSum += (blendedByComponent[input.itemId]?.[key] ?? 0) * weight
|
|
154
|
+
totalWeight += weight
|
|
152
155
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
return {key, value: Math.max(1, Math.min(999, value))}
|
|
156
|
+
const value = totalWeight > 0 ? Math.floor(weightedSum / totalWeight) : 0
|
|
157
|
+
return {key: slotKey, value: Math.max(1, Math.min(999, value))}
|
|
156
158
|
})
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -185,12 +187,7 @@ export function computeInputMass(itemId: number): number {
|
|
|
185
187
|
|
|
186
188
|
let total = 0
|
|
187
189
|
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
|
-
}
|
|
190
|
+
total += getItem(input.itemId).mass * input.quantity
|
|
194
191
|
}
|
|
195
192
|
return total
|
|
196
193
|
}
|
|
@@ -300,15 +297,18 @@ export function computeCraftedOutputStats(
|
|
|
300
297
|
out.push(0)
|
|
301
298
|
continue
|
|
302
299
|
}
|
|
303
|
-
if (slot.sources.length === 1
|
|
300
|
+
if (slot.sources.length === 1) {
|
|
304
301
|
const src = slot.sources[0]
|
|
305
302
|
const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
|
|
306
303
|
const input = recipe.inputs[src.inputIndex]
|
|
307
304
|
let value = 0
|
|
308
|
-
if (input
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
305
|
+
if (input) {
|
|
306
|
+
const inputItem = getItem(input.itemId)
|
|
307
|
+
if (inputItem.type === 'resource' && inputItem.category) {
|
|
308
|
+
value = blendStacks(decodedByCategory[inputItem.category] ?? [], key)
|
|
309
|
+
} else {
|
|
310
|
+
value = blendedByItem[input.itemId]?.[key] ?? 0
|
|
311
|
+
}
|
|
312
312
|
}
|
|
313
313
|
out.push(Math.max(1, Math.min(999, value)))
|
|
314
314
|
} else {
|
|
@@ -319,10 +319,13 @@ export function computeCraftedOutputStats(
|
|
|
319
319
|
const input = recipe.inputs[src.inputIndex]
|
|
320
320
|
const weight = recipe.blendWeights[src.inputIndex] ?? 1
|
|
321
321
|
let value = 0
|
|
322
|
-
if (input
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
322
|
+
if (input) {
|
|
323
|
+
const inputItem = getItem(input.itemId)
|
|
324
|
+
if (inputItem.type === 'resource' && inputItem.category) {
|
|
325
|
+
value = blendStacks(decodedByCategory[inputItem.category] ?? [], key)
|
|
326
|
+
} else {
|
|
327
|
+
value = blendedByItem[input.itemId]?.[key] ?? 0
|
|
328
|
+
}
|
|
326
329
|
}
|
|
327
330
|
weightedSum += value * weight
|
|
328
331
|
totalWeight += weight
|
|
@@ -340,7 +343,7 @@ export function computeCraftedOutputStats(
|
|
|
340
343
|
* returns a UInt64 whose bit-packed form matches what the contract writes
|
|
341
344
|
* to cargo_item.stats on gather.
|
|
342
345
|
*
|
|
343
|
-
* Use this whenever off-chain code simulates a gather (
|
|
346
|
+
* Use this whenever off-chain code simulates a gather (webapp, player
|
|
344
347
|
* scanners that project cargo outcomes) and needs a value that matches
|
|
345
348
|
* what on-chain cargo would carry.
|
|
346
349
|
*/
|
package/src/derivation/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export {
|
|
|
7
7
|
getEligibleResources,
|
|
8
8
|
getResourceWeight,
|
|
9
9
|
getLocationCandidates,
|
|
10
|
+
getLocationProfile,
|
|
10
11
|
getDepthThreshold,
|
|
11
12
|
getResourceTier,
|
|
12
13
|
DEPTH_THRESHOLD_T1,
|
|
@@ -16,7 +17,9 @@ export {
|
|
|
16
17
|
DEPTH_THRESHOLD_T5,
|
|
17
18
|
LOCATION_MIN_DEPTH,
|
|
18
19
|
LOCATION_MAX_DEPTH,
|
|
19
|
-
|
|
20
|
+
yieldThresholdAt,
|
|
21
|
+
YIELD_FRACTION_SHALLOW,
|
|
22
|
+
YIELD_FRACTION_DEEP,
|
|
20
23
|
PLANET_SUBTYPE_GAS_GIANT,
|
|
21
24
|
PLANET_SUBTYPE_ROCKY,
|
|
22
25
|
PLANET_SUBTYPE_TERRESTRIAL,
|
|
@@ -25,8 +28,31 @@ export {
|
|
|
25
28
|
PLANET_SUBTYPE_INDUSTRIAL,
|
|
26
29
|
} from './resources'
|
|
27
30
|
|
|
28
|
-
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'
|
|
29
40
|
export type {ReserveTier, TierRange} from './tiers'
|
|
30
41
|
|
|
42
|
+
export {getEffectiveReserve} from './reserve-regen'
|
|
43
|
+
export type {EffectiveReserveInput} from './reserve-regen'
|
|
44
|
+
|
|
31
45
|
export * from './stats'
|
|
32
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,81 @@
|
|
|
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_SENSOR_T2,
|
|
11
|
+
ITEM_RESIN,
|
|
12
|
+
ITEM_FRAME,
|
|
13
|
+
ITEM_PLATE,
|
|
14
|
+
ITEM_BEAM,
|
|
15
|
+
ITEM_GATHERER_T1,
|
|
16
|
+
ITEM_CRAFTER_T1,
|
|
17
|
+
ITEM_EXTRACTOR_T1_PACKED,
|
|
18
|
+
ITEM_SHIP_T1_PACKED,
|
|
19
|
+
} from '../data/item-ids'
|
|
20
|
+
|
|
21
|
+
test('getAllRecipes returns the full catalog including the gatherer', () => {
|
|
22
|
+
const all = getAllRecipes()
|
|
23
|
+
expect(all.length).toBeGreaterThan(20)
|
|
24
|
+
expect(all.some((r) => r.outputItemId === ITEM_GATHERER_T1)).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
|
|
28
|
+
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
29
|
+
const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
|
|
30
|
+
expect(ids).toEqual(
|
|
31
|
+
[ITEM_CRAFTER_T1, ITEM_SHIP_T1_PACKED, ITEM_EXTRACTOR_T1_PACKED, ITEM_SENSOR_T2].sort(
|
|
32
|
+
(a, b) => a - b
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('Frame feeds the gatherer drain stat', () => {
|
|
38
|
+
const consumers = getRecipeConsumers(ITEM_FRAME)
|
|
39
|
+
const gatherer = consumers.find((c) => c.outputItemId === ITEM_GATHERER_T1)
|
|
40
|
+
expect(gatherer).toBeDefined()
|
|
41
|
+
const drain = gatherer?.statFlows.find(
|
|
42
|
+
(f) => f.capability === 'Gathering' && f.attribute === 'drain'
|
|
43
|
+
)
|
|
44
|
+
expect(drain).toBeDefined()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('Sensor is a mass-only sink in the Extractor recipe', () => {
|
|
48
|
+
const consumers = getRecipeConsumers(ITEM_SENSOR)
|
|
49
|
+
const extractor = consumers.find((c) => c.outputItemId === ITEM_EXTRACTOR_T1_PACKED)
|
|
50
|
+
expect(extractor).toBeDefined()
|
|
51
|
+
expect(extractor?.statFlows).toHaveLength(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('getResourceDemand returns the resource tonnage for a single-resource component', () => {
|
|
55
|
+
expect(getResourceDemand(ITEM_PLATE)).toEqual({ore: 10})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('getResourceDemand traces a dual-resource component to both resources', () => {
|
|
59
|
+
expect(getResourceDemand(ITEM_BEAM)).toEqual({ore: 5, gas: 5})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('getResourceDemand recurses through a module to raw resources', () => {
|
|
63
|
+
// Gatherer = 300 Beam (5 ore + 5 gas each) + 300 Frame (5 regolith + 5 biomass each)
|
|
64
|
+
expect(getResourceDemand(ITEM_GATHERER_T1)).toEqual({
|
|
65
|
+
ore: 1500,
|
|
66
|
+
gas: 1500,
|
|
67
|
+
regolith: 1500,
|
|
68
|
+
biomass: 1500,
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('getResourceDemand scales by quantity', () => {
|
|
73
|
+
expect(getResourceDemand(ITEM_PLATE, 3)).toEqual({ore: 30})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('getComponentDemand reports Resin as consumed by three recipes', () => {
|
|
77
|
+
const demand = getComponentDemand()
|
|
78
|
+
const resin = demand.find((d) => d.itemId === ITEM_RESIN)
|
|
79
|
+
expect(resin).toBeDefined()
|
|
80
|
+
expect(resin?.consumerCount).toBe(3)
|
|
81
|
+
})
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {getItem} from '../data/catalog'
|
|
2
|
+
import {LocationType} from '../types'
|
|
2
3
|
|
|
3
4
|
export const DEPTH_THRESHOLD_T1 = 0
|
|
4
5
|
export const DEPTH_THRESHOLD_T2 = 1500
|
|
@@ -14,7 +15,15 @@ export const DEPTH_THRESHOLD_T10 = 63000
|
|
|
14
15
|
export const LOCATION_MIN_DEPTH = 500
|
|
15
16
|
export const LOCATION_MAX_DEPTH = 65535
|
|
16
17
|
|
|
17
|
-
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
|
+
}
|
|
18
27
|
|
|
19
28
|
export const PLANET_SUBTYPE_GAS_GIANT = 0
|
|
20
29
|
export const PLANET_SUBTYPE_ROCKY = 1
|
|
@@ -75,37 +84,107 @@ export function getResourceWeight(itemId: number, stratum: number): number {
|
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const ICY_RESOURCES = [101, 301, 302, 401, 403, 501, 502]
|
|
84
|
-
const OCEAN_RESOURCES = [201, 203, 301, 303, 501, 502, 503]
|
|
85
|
-
const INDUSTRIAL_RESOURCES = [101, 102, 103, 201, 203, 402, 403]
|
|
87
|
+
const RESOURCE_ORE = 0
|
|
88
|
+
const RESOURCE_GAS = 1
|
|
89
|
+
const RESOURCE_REGOLITH = 2
|
|
90
|
+
const RESOURCE_BIOMASS = 3
|
|
91
|
+
const RESOURCE_CRYSTAL = 4
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
interface LocationProfileEntry {
|
|
94
|
+
category: number
|
|
95
|
+
maxTier: number
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function categoryBaseId(category: number): number {
|
|
99
|
+
switch (category) {
|
|
100
|
+
case RESOURCE_ORE:
|
|
101
|
+
return 100
|
|
102
|
+
case RESOURCE_CRYSTAL:
|
|
103
|
+
return 200
|
|
104
|
+
case RESOURCE_GAS:
|
|
105
|
+
return 300
|
|
106
|
+
case RESOURCE_REGOLITH:
|
|
107
|
+
return 400
|
|
108
|
+
case RESOURCE_BIOMASS:
|
|
109
|
+
return 500
|
|
110
|
+
default:
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resourceId(category: number, tier: number): number {
|
|
116
|
+
return categoryBaseId(category) + tier
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getLocationProfile(locationType: number, subtype: number): LocationProfileEntry[] {
|
|
120
|
+
if (locationType === LocationType.ASTEROID) {
|
|
121
|
+
return [
|
|
122
|
+
{category: RESOURCE_ORE, maxTier: 5},
|
|
123
|
+
{category: RESOURCE_CRYSTAL, maxTier: 5},
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
if (locationType === LocationType.NEBULA) {
|
|
127
|
+
return [
|
|
128
|
+
{category: RESOURCE_GAS, maxTier: 5},
|
|
129
|
+
{category: RESOURCE_REGOLITH, maxTier: 5},
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
if (locationType === LocationType.ICE_FIELD) {
|
|
133
|
+
return [
|
|
134
|
+
{category: RESOURCE_GAS, maxTier: 5},
|
|
135
|
+
{category: RESOURCE_BIOMASS, maxTier: 5},
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
if (locationType === LocationType.PLANET) {
|
|
91
139
|
switch (subtype) {
|
|
92
140
|
case PLANET_SUBTYPE_GAS_GIANT:
|
|
93
|
-
return
|
|
141
|
+
return [
|
|
142
|
+
{category: RESOURCE_GAS, maxTier: 10},
|
|
143
|
+
{category: RESOURCE_CRYSTAL, maxTier: 3},
|
|
144
|
+
]
|
|
94
145
|
case PLANET_SUBTYPE_ROCKY:
|
|
95
|
-
return
|
|
146
|
+
return [
|
|
147
|
+
{category: RESOURCE_REGOLITH, maxTier: 10},
|
|
148
|
+
{category: RESOURCE_ORE, maxTier: 3},
|
|
149
|
+
]
|
|
96
150
|
case PLANET_SUBTYPE_TERRESTRIAL:
|
|
97
|
-
return
|
|
151
|
+
return [
|
|
152
|
+
{category: RESOURCE_ORE, maxTier: 10},
|
|
153
|
+
{category: RESOURCE_BIOMASS, maxTier: 3},
|
|
154
|
+
]
|
|
98
155
|
case PLANET_SUBTYPE_ICY:
|
|
99
|
-
return
|
|
156
|
+
return [
|
|
157
|
+
{category: RESOURCE_CRYSTAL, maxTier: 10},
|
|
158
|
+
{category: RESOURCE_REGOLITH, maxTier: 3},
|
|
159
|
+
]
|
|
100
160
|
case PLANET_SUBTYPE_OCEAN:
|
|
101
|
-
return
|
|
161
|
+
return [
|
|
162
|
+
{category: RESOURCE_BIOMASS, maxTier: 10},
|
|
163
|
+
{category: RESOURCE_GAS, maxTier: 3},
|
|
164
|
+
]
|
|
102
165
|
case PLANET_SUBTYPE_INDUSTRIAL:
|
|
103
|
-
return
|
|
166
|
+
return [
|
|
167
|
+
{category: RESOURCE_ORE, maxTier: 3},
|
|
168
|
+
{category: RESOURCE_CRYSTAL, maxTier: 3},
|
|
169
|
+
{category: RESOURCE_REGOLITH, maxTier: 3},
|
|
170
|
+
{category: RESOURCE_BIOMASS, maxTier: 3},
|
|
171
|
+
]
|
|
104
172
|
}
|
|
105
173
|
}
|
|
106
174
|
return []
|
|
107
175
|
}
|
|
108
176
|
|
|
177
|
+
export function getLocationCandidates(locationType: number, subtype: number): number[] {
|
|
178
|
+
const profile = getLocationProfile(locationType, subtype)
|
|
179
|
+
const ids: number[] = []
|
|
180
|
+
for (const {category, maxTier} of profile) {
|
|
181
|
+
for (let tier = 1; tier <= maxTier; tier++) {
|
|
182
|
+
ids.push(resourceId(category, tier))
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return ids
|
|
186
|
+
}
|
|
187
|
+
|
|
109
188
|
export function getEligibleResources(
|
|
110
189
|
locationType: number,
|
|
111
190
|
subtype: number,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {describe, expect, test} from 'bun:test'
|
|
2
|
+
import {ServerContract} from '../contracts'
|
|
3
|
+
import {rollupGatherer, rollupCrafter, rollupLoaders} from './rollups'
|
|
4
|
+
|
|
5
|
+
const gLane = (slot: number, y: number, d: number, depth: number) =>
|
|
6
|
+
ServerContract.Types.gatherer_lane.from({
|
|
7
|
+
slot_index: slot,
|
|
8
|
+
yield: y,
|
|
9
|
+
drain: d,
|
|
10
|
+
depth,
|
|
11
|
+
output_pct: 100,
|
|
12
|
+
})
|
|
13
|
+
const cLane = (slot: number, s: number, d: number) =>
|
|
14
|
+
ServerContract.Types.crafter_lane.from({slot_index: slot, speed: s, drain: d, output_pct: 100})
|
|
15
|
+
const lLane = (slot: number, m: number, t: number) =>
|
|
16
|
+
ServerContract.Types.loader_lane.from({slot_index: slot, mass: m, thrust: t, output_pct: 100})
|
|
17
|
+
|
|
18
|
+
describe('rollupGatherer', () => {
|
|
19
|
+
test('empty → undefined', () => {
|
|
20
|
+
expect(rollupGatherer([])).toBeUndefined()
|
|
21
|
+
})
|
|
22
|
+
test('sums yield/drain, takes MAX depth', () => {
|
|
23
|
+
const r = rollupGatherer([gLane(2, 300, 1250, 500), gLane(3, 250, 1000, 5495)])!
|
|
24
|
+
expect(r.yield.toNumber()).toBe(550)
|
|
25
|
+
expect(r.drain.toNumber()).toBe(2250)
|
|
26
|
+
expect(r.depth.toNumber()).toBe(5495)
|
|
27
|
+
})
|
|
28
|
+
test('clamps summed yield to uint16', () => {
|
|
29
|
+
const r = rollupGatherer([gLane(2, 60000, 0, 1), gLane(3, 60000, 0, 1)])!
|
|
30
|
+
expect(r.yield.toNumber()).toBe(65535)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('rollupCrafter', () => {
|
|
35
|
+
test('empty → undefined', () => {
|
|
36
|
+
expect(rollupCrafter([])).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
test('sums speed/drain', () => {
|
|
39
|
+
const r = rollupCrafter([cLane(2, 100, 30), cLane(3, 140, 25)])!
|
|
40
|
+
expect(r.speed.toNumber()).toBe(240)
|
|
41
|
+
expect(r.drain.toNumber()).toBe(55)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('rollupLoaders', () => {
|
|
46
|
+
test('empty → undefined', () => {
|
|
47
|
+
expect(rollupLoaders([])).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
test('integer-averages mass, sums thrust, counts quantity', () => {
|
|
50
|
+
const r = rollupLoaders([lLane(2, 200, 5), lLane(3, 201, 7)])!
|
|
51
|
+
expect(r.mass.toNumber()).toBe(200) // floor(401/2)
|
|
52
|
+
expect(r.thrust.toNumber()).toBe(12)
|
|
53
|
+
expect(r.quantity.toNumber()).toBe(2)
|
|
54
|
+
})
|
|
55
|
+
})
|