@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.
Files changed (156) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +3601 -1364
  7. package/lib/shipload.js +15671 -5261
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +15392 -5209
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1173 -0
  12. package/lib/testing.js +4971 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4965 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +35 -0
  20. package/src/capabilities/crafting.ts +34 -7
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +37 -19
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.ts +16 -1
  28. package/src/contracts/platform.ts +231 -3
  29. package/src/contracts/server.ts +1518 -501
  30. package/src/coordinates/address.ts +88 -0
  31. package/src/coordinates/constants.test.ts +15 -0
  32. package/src/coordinates/constants.ts +23 -0
  33. package/src/coordinates/index.ts +15 -0
  34. package/src/coordinates/memo.test.ts +47 -0
  35. package/src/coordinates/memo.ts +20 -0
  36. package/src/coordinates/permutation.ts +77 -0
  37. package/src/coordinates/regions.ts +48 -0
  38. package/src/coordinates/sectors.ts +115 -0
  39. package/src/data/capabilities.ts +49 -18
  40. package/src/data/capability-formulas.ts +26 -14
  41. package/src/data/catalog.ts +7 -8
  42. package/src/data/colors.ts +14 -48
  43. package/src/data/entities.json +380 -15
  44. package/src/data/item-ids.ts +51 -13
  45. package/src/data/items.json +318 -77
  46. package/src/data/kind-registry.json +195 -0
  47. package/src/data/kind-registry.ts +182 -0
  48. package/src/data/metadata.ts +291 -51
  49. package/src/data/recipes-runtime.ts +6 -23
  50. package/src/data/recipes.json +2260 -173
  51. package/src/data/tiers.ts +9 -6
  52. package/src/derivation/build-methods.test.ts +13 -0
  53. package/src/derivation/build-methods.ts +48 -0
  54. package/src/derivation/capabilities.test.ts +348 -0
  55. package/src/derivation/capabilities.ts +693 -0
  56. package/src/derivation/capability-mappings.ts +60 -16
  57. package/src/derivation/crafting.test.ts +17 -0
  58. package/src/derivation/crafting.ts +57 -34
  59. package/src/derivation/index.ts +27 -2
  60. package/src/derivation/recipe-usage.test.ts +90 -0
  61. package/src/derivation/recipe-usage.ts +141 -0
  62. package/src/derivation/reserve-regen.ts +34 -0
  63. package/src/derivation/resources.ts +9 -1
  64. package/src/derivation/rollups.test.ts +55 -0
  65. package/src/derivation/rollups.ts +72 -0
  66. package/src/derivation/stars.test.ts +67 -0
  67. package/src/derivation/stars.ts +25 -0
  68. package/src/derivation/stat-scaling.ts +12 -0
  69. package/src/derivation/stats.ts +6 -6
  70. package/src/derivation/stratum.ts +26 -22
  71. package/src/derivation/tiers.ts +40 -7
  72. package/src/derivation/upgrades.ts +14 -0
  73. package/src/derivation/wormhole.ts +154 -0
  74. package/src/entities/entity.ts +102 -0
  75. package/src/entities/gamestate.ts +3 -28
  76. package/src/entities/makers.ts +141 -135
  77. package/src/entities/slot-multiplier.ts +43 -0
  78. package/src/errors.ts +12 -16
  79. package/src/format.ts +26 -4
  80. package/src/index-module.ts +333 -35
  81. package/src/managers/actions.ts +668 -103
  82. package/src/managers/base.ts +6 -2
  83. package/src/managers/cluster.test.ts +39 -0
  84. package/src/managers/cluster.ts +53 -0
  85. package/src/managers/construction-types.ts +80 -0
  86. package/src/managers/construction.ts +443 -0
  87. package/src/managers/context.ts +29 -1
  88. package/src/managers/coordinates.ts +14 -0
  89. package/src/managers/entities.ts +27 -66
  90. package/src/managers/epochs.ts +40 -0
  91. package/src/managers/index.ts +20 -1
  92. package/src/managers/locations.ts +25 -29
  93. package/src/managers/nft.test.ts +14 -0
  94. package/src/managers/nft.ts +70 -0
  95. package/src/managers/players.ts +25 -0
  96. package/src/managers/plot.ts +122 -0
  97. package/src/nft/atomicassets.abi.json +1342 -0
  98. package/src/nft/atomicassets.ts +237 -0
  99. package/src/nft/atomicdata.ts +130 -0
  100. package/src/nft/buildImmutableData.ts +388 -0
  101. package/src/nft/description.ts +215 -99
  102. package/src/nft/index.ts +3 -0
  103. package/src/planner/index.ts +242 -0
  104. package/src/planner/planner.test.ts +334 -0
  105. package/src/resolution/describe-module.ts +43 -25
  106. package/src/resolution/display-name.ts +38 -10
  107. package/src/resolution/resolve-item.test.ts +48 -0
  108. package/src/resolution/resolve-item.ts +112 -58
  109. package/src/scan/index.ts +244 -0
  110. package/src/scan/scan-wasm.base64.ts +2 -0
  111. package/src/scheduling/accessor.ts +65 -23
  112. package/src/scheduling/availability.test.ts +204 -0
  113. package/src/scheduling/availability.ts +211 -0
  114. package/src/scheduling/cancel.test.ts +542 -0
  115. package/src/scheduling/cancel.ts +254 -0
  116. package/src/scheduling/cluster-stock.test.ts +31 -0
  117. package/src/scheduling/cluster-stock.ts +15 -0
  118. package/src/scheduling/energy.ts +47 -0
  119. package/src/scheduling/idle-resolve.ts +45 -0
  120. package/src/scheduling/jobs.ts +71 -0
  121. package/src/scheduling/lane-core.ts +128 -0
  122. package/src/scheduling/lanes.test.ts +250 -0
  123. package/src/scheduling/lanes.ts +227 -0
  124. package/src/scheduling/projection.ts +257 -133
  125. package/src/scheduling/schedule.test.ts +119 -0
  126. package/src/scheduling/schedule.ts +257 -117
  127. package/src/scheduling/task-cargo.test.ts +44 -0
  128. package/src/scheduling/task-cargo.ts +46 -0
  129. package/src/scheduling/unwrap.test.ts +86 -0
  130. package/src/scheduling/unwrap.ts +190 -0
  131. package/src/shipload.ts +26 -1
  132. package/src/subscriptions/manager.cluster.test.ts +51 -0
  133. package/src/subscriptions/manager.ts +245 -172
  134. package/src/subscriptions/mappers.ts +5 -8
  135. package/src/subscriptions/types.ts +28 -3
  136. package/src/testing/catalog-hash.ts +19 -0
  137. package/src/testing/index.ts +2 -0
  138. package/src/testing/projection-parity.ts +167 -0
  139. package/src/travel/reach.ts +21 -0
  140. package/src/travel/route-planner.ts +288 -0
  141. package/src/travel/route-simulator.ts +170 -0
  142. package/src/travel/travel.ts +188 -122
  143. package/src/types/capabilities.ts +37 -8
  144. package/src/types/entity.ts +3 -3
  145. package/src/types/index.ts +0 -1
  146. package/src/types.ts +51 -16
  147. package/src/utils/cargo.test.ts +14 -0
  148. package/src/utils/cargo.ts +29 -0
  149. package/src/utils/display-name.ts +70 -0
  150. package/src/utils/system.ts +15 -16
  151. package/src/capabilities/loading.ts +0 -8
  152. package/src/entities/container.ts +0 -123
  153. package/src/entities/ship-deploy.ts +0 -295
  154. package/src/entities/ship.ts +0 -221
  155. package/src/entities/warehouse.ts +0 -127
  156. 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
- 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,
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
- if (isCategoryInput(input)) {
60
- const defs = getStatDefinitions(input.category)
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 source of slot.sources) {
93
- const stat = traceToRawCategoryStat(recipe, source)
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 {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'
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
- 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.
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
- if (!input || !('category' in input)) {
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 === input.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 src = slot.sources[0]
147
- const key = keyForStatSlot(recipe, slot)
148
- if (!src) return {key, value: 1}
149
- const input = recipe.inputs[src.inputIndex]
150
- if (!input || 'category' in input) {
151
- return {key, value: 1}
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 blended = blendedByComponent[input.itemId] ?? {}
154
- const value = blended[key] ?? 0
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 20200
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
- 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
- }
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 || recipe.blendWeights.length === 0) {
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 && 'category' in input) {
309
- value = blendStacks(decodedByCategory[input.category] ?? [], key)
310
- } else if (input) {
311
- value = blendedByItem[input.itemId]?.[key] ?? 0
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 && 'category' in input) {
323
- value = blendStacks(decodedByCategory[input.category] ?? [], key)
324
- } else if (input) {
325
- value = blendedByItem[input.itemId]?.[key] ?? 0
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 (testmap, player
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
  */
@@ -17,7 +17,9 @@ export {
17
17
  DEPTH_THRESHOLD_T5,
18
18
  LOCATION_MIN_DEPTH,
19
19
  LOCATION_MAX_DEPTH,
20
- YIELD_THRESHOLD,
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 {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'
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 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
+ }
19
27
 
20
28
  export const PLANET_SUBTYPE_GAS_GIANT = 0
21
29
  export const PLANET_SUBTYPE_ROCKY = 1