@shipload/sdk 1.0.0-next.4 → 1.0.0-next.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/lib/scan.d.ts +34 -0
  2. package/lib/scan.js +136 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +129 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +2473 -973
  7. package/lib/shipload.js +11529 -5211
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +11338 -5162
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +970 -0
  12. package/lib/testing.js +4013 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4007 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.ts +51 -0
  18. package/src/capabilities/crafting.test.ts +7 -0
  19. package/src/capabilities/crafting.ts +5 -6
  20. package/src/capabilities/gathering.test.ts +16 -0
  21. package/src/capabilities/gathering.ts +35 -18
  22. package/src/capabilities/index.ts +0 -1
  23. package/src/capabilities/modules.ts +9 -0
  24. package/src/capabilities/storage.ts +16 -1
  25. package/src/contracts/platform.ts +231 -3
  26. package/src/contracts/server.ts +1021 -481
  27. package/src/coordinates/address.ts +88 -0
  28. package/src/coordinates/constants.test.ts +15 -0
  29. package/src/coordinates/constants.ts +23 -0
  30. package/src/coordinates/index.ts +15 -0
  31. package/src/coordinates/memo.test.ts +47 -0
  32. package/src/coordinates/memo.ts +20 -0
  33. package/src/coordinates/permutation.ts +77 -0
  34. package/src/coordinates/regions.ts +48 -0
  35. package/src/coordinates/sectors.ts +115 -0
  36. package/src/data/capabilities.ts +12 -5
  37. package/src/data/capability-formulas.ts +14 -7
  38. package/src/data/catalog.ts +0 -5
  39. package/src/data/colors.ts +14 -47
  40. package/src/data/entities.json +76 -10
  41. package/src/data/item-ids.ts +18 -12
  42. package/src/data/items.json +321 -38
  43. package/src/data/kind-registry.json +109 -0
  44. package/src/data/kind-registry.ts +165 -0
  45. package/src/data/metadata.ts +119 -33
  46. package/src/data/recipes-runtime.ts +3 -23
  47. package/src/data/recipes.json +238 -117
  48. package/src/derivation/build-methods.ts +45 -0
  49. package/src/derivation/capabilities.test.ts +151 -0
  50. package/src/derivation/capabilities.ts +512 -0
  51. package/src/derivation/capability-mappings.ts +9 -12
  52. package/src/derivation/crafting.ts +23 -24
  53. package/src/derivation/index.ts +25 -2
  54. package/src/derivation/recipe-usage.test.ts +78 -0
  55. package/src/derivation/recipe-usage.ts +141 -0
  56. package/src/derivation/reserve-regen.ts +34 -0
  57. package/src/derivation/resources.ts +125 -38
  58. package/src/derivation/rollups.test.ts +55 -0
  59. package/src/derivation/rollups.ts +56 -0
  60. package/src/derivation/stars.test.ts +51 -0
  61. package/src/derivation/stars.ts +15 -0
  62. package/src/derivation/stats.ts +6 -6
  63. package/src/derivation/stratum.ts +17 -20
  64. package/src/derivation/tiers.ts +40 -7
  65. package/src/derivation/wormhole.ts +136 -0
  66. package/src/entities/entity.ts +98 -0
  67. package/src/entities/gamestate.ts +3 -28
  68. package/src/entities/makers.ts +124 -134
  69. package/src/entities/slot-multiplier.ts +43 -0
  70. package/src/errors.ts +12 -16
  71. package/src/format.ts +26 -4
  72. package/src/index-module.ts +267 -47
  73. package/src/managers/actions.ts +528 -95
  74. package/src/managers/base.ts +6 -2
  75. package/src/managers/construction-types.ts +80 -0
  76. package/src/managers/construction.ts +412 -0
  77. package/src/managers/context.ts +20 -1
  78. package/src/managers/coordinates.ts +14 -0
  79. package/src/managers/entities.ts +18 -66
  80. package/src/managers/epochs.ts +40 -0
  81. package/src/managers/index.ts +17 -1
  82. package/src/managers/locations.ts +25 -29
  83. package/src/managers/nft.test.ts +14 -0
  84. package/src/managers/nft.ts +70 -0
  85. package/src/managers/plot.ts +122 -0
  86. package/src/nft/atomicassets.abi.json +1342 -0
  87. package/src/nft/atomicassets.ts +237 -0
  88. package/src/nft/atomicdata.ts +130 -0
  89. package/src/nft/buildImmutableData.ts +338 -0
  90. package/src/nft/description.ts +98 -24
  91. package/src/nft/index.ts +3 -0
  92. package/src/planner/index.ts +127 -0
  93. package/src/planner/planner.test.ts +319 -0
  94. package/src/resolution/describe-module.ts +18 -13
  95. package/src/resolution/display-name.ts +38 -10
  96. package/src/resolution/resolve-item.test.ts +37 -0
  97. package/src/resolution/resolve-item.ts +55 -24
  98. package/src/scan/index.ts +180 -0
  99. package/src/scan/scan-wasm.base64.ts +2 -0
  100. package/src/scheduling/accessor.ts +68 -22
  101. package/src/scheduling/availability.ts +108 -0
  102. package/src/scheduling/cancel.test.ts +348 -0
  103. package/src/scheduling/cancel.ts +209 -0
  104. package/src/scheduling/energy.ts +47 -0
  105. package/src/scheduling/idle-resolve.ts +45 -0
  106. package/src/scheduling/lane-core.ts +128 -0
  107. package/src/scheduling/lanes.test.ts +249 -0
  108. package/src/scheduling/lanes.ts +198 -0
  109. package/src/scheduling/projection.ts +209 -105
  110. package/src/scheduling/schedule.ts +241 -104
  111. package/src/scheduling/task-cargo.ts +46 -0
  112. package/src/shipload.ts +21 -1
  113. package/src/subscriptions/manager.ts +229 -142
  114. package/src/subscriptions/mappers.ts +5 -8
  115. package/src/subscriptions/types.ts +11 -3
  116. package/src/testing/catalog-hash.ts +19 -0
  117. package/src/testing/index.ts +2 -0
  118. package/src/testing/projection-parity.ts +167 -0
  119. package/src/travel/reach.ts +23 -0
  120. package/src/travel/route-planner.ts +196 -0
  121. package/src/travel/travel.ts +200 -112
  122. package/src/types/capabilities.ts +29 -6
  123. package/src/types/entity.ts +3 -3
  124. package/src/types/index.ts +0 -1
  125. package/src/types.ts +28 -13
  126. package/src/utils/cargo.ts +27 -0
  127. package/src/utils/display-name.ts +70 -0
  128. package/src/utils/system.ts +36 -24
  129. package/src/capabilities/loading.ts +0 -8
  130. package/src/entities/container.ts +0 -108
  131. package/src/entities/ship-deploy.ts +0 -259
  132. package/src/entities/ship.ts +0 -204
  133. package/src/entities/warehouse.ts +0 -119
  134. package/src/types/entity-traits.ts +0 -69
@@ -1,13 +1,10 @@
1
1
  import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
2
2
  import {getStatDefinitions, type StatDefinition} from './stats'
3
- import {
4
- getRecipe,
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,
@@ -15,6 +12,7 @@ import {
15
12
  ITEM_STORAGE_T1,
16
13
  ITEM_HAULER_T1,
17
14
  ITEM_WARP_T1,
15
+ ITEM_BATTERY_T1,
18
16
  ITEM_SHIP_T1_PACKED,
19
17
  ITEM_CONTAINER_T1_PACKED,
20
18
  ITEM_WAREHOUSE_T1_PACKED,
@@ -31,16 +29,14 @@ export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
31
29
  storage: ITEM_STORAGE_T1,
32
30
  hauler: ITEM_HAULER_T1,
33
31
  warp: ITEM_WARP_T1,
32
+ battery: ITEM_BATTERY_T1,
34
33
  'ship-t1': ITEM_SHIP_T1_PACKED,
35
34
  'container-t1': ITEM_CONTAINER_T1_PACKED,
36
35
  'warehouse-t1': ITEM_WAREHOUSE_T1_PACKED,
36
+ 'extractor-t1': ITEM_EXTRACTOR_T1_PACKED,
37
37
  'container-t2': ITEM_CONTAINER_T2_PACKED,
38
38
  }
39
39
 
40
- function isCategoryInput(input: RecipeInput): input is RecipeInputCategory {
41
- return 'category' in input
42
- }
43
-
44
40
  /**
45
41
  * Walk a recipe's slot source down to the raw category stat that ultimately
46
42
  * lands in that slot. Returns the StatDefinition or undefined if the trace
@@ -56,8 +52,9 @@ function traceToRawCategoryStat(
56
52
  ): StatDefinition | undefined {
57
53
  const input = recipe.inputs[source.inputIndex]
58
54
  if (!input) return undefined
59
- if (isCategoryInput(input)) {
60
- const defs = getStatDefinitions(input.category)
55
+ const inputItem = getItem(input.itemId)
56
+ if (inputItem.type === 'resource' && inputItem.category) {
57
+ const defs = getStatDefinitions(inputItem.category)
61
58
  return defs[source.statIndex]
62
59
  }
63
60
  if (visited.has(input.itemId)) return undefined
@@ -1,6 +1,6 @@
1
1
  import {UInt64} from '@wharfkit/antelope'
2
2
  import type {ResourceCategory} from '../types'
3
- import {findItemByCategoryAndTier, getRecipe, type Recipe} from '../data/recipes-runtime'
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
- 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.
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
- if (!input || !('category' in input)) {
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 === input.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
  })
@@ -147,7 +145,7 @@ export function computeEntityStats(
147
145
  const key = keyForStatSlot(recipe, slot)
148
146
  if (!src) return {key, value: 1}
149
147
  const input = recipe.inputs[src.inputIndex]
150
- if (!input || 'category' in input) {
148
+ if (!input) {
151
149
  return {key, value: 1}
152
150
  }
153
151
  const blended = blendedByComponent[input.itemId] ?? {}
@@ -185,12 +183,7 @@ export function computeInputMass(itemId: number): number {
185
183
 
186
184
  let total = 0
187
185
  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
- }
186
+ total += getItem(input.itemId).mass * input.quantity
194
187
  }
195
188
  return total
196
189
  }
@@ -305,10 +298,13 @@ export function computeCraftedOutputStats(
305
298
  const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
306
299
  const input = recipe.inputs[src.inputIndex]
307
300
  let value = 0
308
- if (input && 'category' in input) {
309
- value = blendStacks(decodedByCategory[input.category] ?? [], key)
310
- } else if (input) {
311
- value = blendedByItem[input.itemId]?.[key] ?? 0
301
+ if (input) {
302
+ const inputItem = getItem(input.itemId)
303
+ if (inputItem.type === 'resource' && inputItem.category) {
304
+ value = blendStacks(decodedByCategory[inputItem.category] ?? [], key)
305
+ } else {
306
+ value = blendedByItem[input.itemId]?.[key] ?? 0
307
+ }
312
308
  }
313
309
  out.push(Math.max(1, Math.min(999, value)))
314
310
  } else {
@@ -319,10 +315,13 @@ export function computeCraftedOutputStats(
319
315
  const input = recipe.inputs[src.inputIndex]
320
316
  const weight = recipe.blendWeights[src.inputIndex] ?? 1
321
317
  let value = 0
322
- if (input && 'category' in input) {
323
- value = blendStacks(decodedByCategory[input.category] ?? [], key)
324
- } else if (input) {
325
- value = blendedByItem[input.itemId]?.[key] ?? 0
318
+ if (input) {
319
+ const inputItem = getItem(input.itemId)
320
+ if (inputItem.type === 'resource' && inputItem.category) {
321
+ value = blendStacks(decodedByCategory[inputItem.category] ?? [], key)
322
+ } else {
323
+ value = blendedByItem[input.itemId]?.[key] ?? 0
324
+ }
326
325
  }
327
326
  weightedSum += value * weight
328
327
  totalWeight += weight
@@ -340,7 +339,7 @@ export function computeCraftedOutputStats(
340
339
  * returns a UInt64 whose bit-packed form matches what the contract writes
341
340
  * to cargo_item.stats on gather.
342
341
  *
343
- * Use this whenever off-chain code simulates a gather (testmap, player
342
+ * Use this whenever off-chain code simulates a gather (webapp, player
344
343
  * scanners that project cargo outcomes) and needs a value that matches
345
344
  * what on-chain cargo would carry.
346
345
  */
@@ -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
- YIELD_THRESHOLD,
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,28 @@ export {
25
28
  PLANET_SUBTYPE_INDUSTRIAL,
26
29
  } from './resources'
27
30
 
28
- export {RESERVE_TIERS, TIER_ROLL_MAX, rollTier, rollWithinTier} from './tiers'
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
+
48
+ export {
49
+ STAR_STEP,
50
+ MAX_STARS_PER_STAT,
51
+ MAX_STAR_RATING,
52
+ starsForStat,
53
+ starRating,
54
+ statMagnitude,
55
+ } from './stars'
@@ -0,0 +1,78 @@
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_PLATE,
12
+ ITEM_BEAM,
13
+ ITEM_GATHERER_T1,
14
+ ITEM_CRAFTER_T1,
15
+ ITEM_EXTRACTOR_T1_PACKED,
16
+ ITEM_SHIP_T1_PACKED,
17
+ } from '../data/item-ids'
18
+
19
+ test('getAllRecipes returns the full catalog including the gatherer', () => {
20
+ const all = getAllRecipes()
21
+ expect(all.length).toBeGreaterThan(20)
22
+ expect(all.some((r) => r.outputItemId === ITEM_GATHERER_T1)).toBe(true)
23
+ })
24
+
25
+ test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
26
+ const consumers = getRecipeConsumers(ITEM_SENSOR)
27
+ const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
28
+ expect(ids).toEqual(
29
+ [ITEM_GATHERER_T1, ITEM_CRAFTER_T1, ITEM_SHIP_T1_PACKED, ITEM_EXTRACTOR_T1_PACKED].sort(
30
+ (a, b) => a - b
31
+ )
32
+ )
33
+ })
34
+
35
+ test('Sensor feeds the gatherer drain stat', () => {
36
+ const consumers = getRecipeConsumers(ITEM_SENSOR)
37
+ const gatherer = consumers.find((c) => c.outputItemId === ITEM_GATHERER_T1)
38
+ expect(gatherer).toBeDefined()
39
+ const drain = gatherer?.statFlows.find(
40
+ (f) => f.capability === 'Gathering' && f.attribute === 'drain'
41
+ )
42
+ expect(drain).toBeDefined()
43
+ })
44
+
45
+ test('Sensor is a mass-only sink in the Extractor recipe', () => {
46
+ const consumers = getRecipeConsumers(ITEM_SENSOR)
47
+ const extractor = consumers.find((c) => c.outputItemId === ITEM_EXTRACTOR_T1_PACKED)
48
+ expect(extractor).toBeDefined()
49
+ expect(extractor?.statFlows).toHaveLength(0)
50
+ })
51
+
52
+ test('getResourceDemand returns the resource tonnage for a single-resource component', () => {
53
+ expect(getResourceDemand(ITEM_PLATE)).toEqual({ore: 10})
54
+ })
55
+
56
+ test('getResourceDemand traces a dual-resource component to both resources', () => {
57
+ expect(getResourceDemand(ITEM_BEAM)).toEqual({ore: 5, gas: 5})
58
+ })
59
+
60
+ test('getResourceDemand recurses through a module to raw resources', () => {
61
+ // Gatherer = 300 Beam (5 ore + 5 gas each) + 300 Sensor (10 crystal each)
62
+ expect(getResourceDemand(ITEM_GATHERER_T1)).toEqual({
63
+ ore: 1500,
64
+ gas: 1500,
65
+ crystal: 3000,
66
+ })
67
+ })
68
+
69
+ test('getResourceDemand scales by quantity', () => {
70
+ expect(getResourceDemand(ITEM_PLATE, 3)).toEqual({ore: 30})
71
+ })
72
+
73
+ test('getComponentDemand reports Resin as consumed by exactly one recipe', () => {
74
+ const demand = getComponentDemand()
75
+ const resin = demand.find((d) => d.itemId === ITEM_RESIN)
76
+ expect(resin).toBeDefined()
77
+ expect(resin?.consumerCount).toBe(1)
78
+ })
@@ -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,15 +1,29 @@
1
1
  import {getItem} from '../data/catalog'
2
+ import {LocationType} from '../types'
2
3
 
3
4
  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
5
+ export const DEPTH_THRESHOLD_T2 = 1500
6
+ export const DEPTH_THRESHOLD_T3 = 5000
7
+ export const DEPTH_THRESHOLD_T4 = 12000
8
+ export const DEPTH_THRESHOLD_T5 = 22000
9
+ export const DEPTH_THRESHOLD_T6 = 32000
10
+ export const DEPTH_THRESHOLD_T7 = 42000
11
+ export const DEPTH_THRESHOLD_T8 = 50000
12
+ export const DEPTH_THRESHOLD_T9 = 57000
13
+ export const DEPTH_THRESHOLD_T10 = 63000
8
14
 
9
15
  export const LOCATION_MIN_DEPTH = 500
10
16
  export const LOCATION_MAX_DEPTH = 65535
11
17
 
12
- export const YIELD_THRESHOLD = Math.floor(0.001 * 0xffffffff)
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
+ }
13
27
 
14
28
  export const PLANET_SUBTYPE_GAS_GIANT = 0
15
29
  export const PLANET_SUBTYPE_ROCKY = 1
@@ -18,19 +32,22 @@ export const PLANET_SUBTYPE_ICY = 3
18
32
  export const PLANET_SUBTYPE_OCEAN = 4
19
33
  export const PLANET_SUBTYPE_INDUSTRIAL = 5
20
34
 
35
+ const DEPTH_THRESHOLD_TABLE = [
36
+ DEPTH_THRESHOLD_T1,
37
+ DEPTH_THRESHOLD_T2,
38
+ DEPTH_THRESHOLD_T3,
39
+ DEPTH_THRESHOLD_T4,
40
+ DEPTH_THRESHOLD_T5,
41
+ DEPTH_THRESHOLD_T6,
42
+ DEPTH_THRESHOLD_T7,
43
+ DEPTH_THRESHOLD_T8,
44
+ DEPTH_THRESHOLD_T9,
45
+ DEPTH_THRESHOLD_T10,
46
+ ]
47
+
21
48
  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
- }
49
+ if (tier < 1 || tier > 10) return 65535
50
+ return DEPTH_THRESHOLD_TABLE[tier - 1]
34
51
  }
35
52
 
36
53
  export function getResourceTier(itemId: number): number {
@@ -46,9 +63,9 @@ export function getResourceWeight(itemId: number, stratum: number): number {
46
63
 
47
64
  switch (tier) {
48
65
  case 1:
49
- if (stratum < 2000) return 100
50
- if (stratum < 10000) return 80
51
- if (stratum < 30000) return 50
66
+ if (stratum < DEPTH_THRESHOLD_T2) return 100
67
+ if (stratum < DEPTH_THRESHOLD_T3) return 80
68
+ if (stratum < DEPTH_THRESHOLD_T4) return 50
52
69
  return 30
53
70
  case 2:
54
71
  if (depthAbove < 3000) return 40
@@ -67,37 +84,107 @@ export function getResourceWeight(itemId: number, stratum: number): number {
67
84
  }
68
85
  }
69
86
 
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]
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
78
92
 
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) {
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) {
83
139
  switch (subtype) {
84
140
  case PLANET_SUBTYPE_GAS_GIANT:
85
- return GAS_GIANT_RESOURCES
141
+ return [
142
+ {category: RESOURCE_GAS, maxTier: 10},
143
+ {category: RESOURCE_CRYSTAL, maxTier: 3},
144
+ ]
86
145
  case PLANET_SUBTYPE_ROCKY:
87
- return ROCKY_RESOURCES
146
+ return [
147
+ {category: RESOURCE_REGOLITH, maxTier: 10},
148
+ {category: RESOURCE_ORE, maxTier: 3},
149
+ ]
88
150
  case PLANET_SUBTYPE_TERRESTRIAL:
89
- return TERRESTRIAL_RESOURCES
151
+ return [
152
+ {category: RESOURCE_ORE, maxTier: 10},
153
+ {category: RESOURCE_BIOMASS, maxTier: 3},
154
+ ]
90
155
  case PLANET_SUBTYPE_ICY:
91
- return ICY_RESOURCES
156
+ return [
157
+ {category: RESOURCE_CRYSTAL, maxTier: 10},
158
+ {category: RESOURCE_REGOLITH, maxTier: 3},
159
+ ]
92
160
  case PLANET_SUBTYPE_OCEAN:
93
- return OCEAN_RESOURCES
161
+ return [
162
+ {category: RESOURCE_BIOMASS, maxTier: 10},
163
+ {category: RESOURCE_GAS, maxTier: 3},
164
+ ]
94
165
  case PLANET_SUBTYPE_INDUSTRIAL:
95
- return INDUSTRIAL_RESOURCES
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
+ ]
96
172
  }
97
173
  }
98
174
  return []
99
175
  }
100
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
+
101
188
  export function getEligibleResources(
102
189
  locationType: number,
103
190
  subtype: number,