@shipload/sdk 1.0.0-next.2 → 1.0.0-next.21

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 (85) hide show
  1. package/lib/shipload.d.ts +1731 -1044
  2. package/lib/shipload.js +6758 -4523
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +6649 -4479
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +833 -0
  7. package/lib/testing.js +3647 -0
  8. package/lib/testing.js.map +1 -0
  9. package/lib/testing.m.js +3641 -0
  10. package/lib/testing.m.js.map +1 -0
  11. package/package.json +15 -2
  12. package/src/capabilities/gathering.ts +17 -7
  13. package/src/capabilities/modules.ts +9 -0
  14. package/src/capabilities/storage.ts +1 -1
  15. package/src/contracts/platform.ts +211 -3
  16. package/src/contracts/server.ts +723 -438
  17. package/src/data/capabilities.ts +9 -329
  18. package/src/data/capability-formulas.ts +76 -0
  19. package/src/data/catalog.ts +0 -5
  20. package/src/data/colors.ts +14 -28
  21. package/src/data/entities.json +46 -10
  22. package/src/data/item-ids.ts +17 -13
  23. package/src/data/items.json +308 -37
  24. package/src/data/kind-registry.json +85 -0
  25. package/src/data/kind-registry.ts +150 -0
  26. package/src/data/metadata.ts +99 -24
  27. package/src/data/recipes-runtime.ts +3 -23
  28. package/src/data/recipes.json +265 -96
  29. package/src/derivation/build-methods.ts +45 -0
  30. package/src/derivation/capabilities.ts +414 -0
  31. package/src/derivation/capability-mappings.ts +117 -0
  32. package/src/derivation/crafting.ts +23 -24
  33. package/src/derivation/index.ts +8 -2
  34. package/src/derivation/reserve-regen.ts +34 -0
  35. package/src/derivation/resources.ts +125 -38
  36. package/src/derivation/stats.ts +1 -2
  37. package/src/derivation/stratum.ts +15 -19
  38. package/src/derivation/tiers.ts +28 -7
  39. package/src/entities/entity.ts +98 -0
  40. package/src/entities/gamestate.ts +3 -28
  41. package/src/entities/makers.ts +75 -129
  42. package/src/entities/slot-multiplier.ts +37 -0
  43. package/src/errors.ts +10 -15
  44. package/src/format.ts +26 -4
  45. package/src/index-module.ts +151 -40
  46. package/src/managers/actions.ts +184 -82
  47. package/src/managers/base.ts +2 -2
  48. package/src/managers/construction-types.ts +68 -0
  49. package/src/managers/construction.ts +292 -0
  50. package/src/managers/context.ts +9 -0
  51. package/src/managers/entities.ts +18 -66
  52. package/src/managers/epochs.ts +40 -0
  53. package/src/managers/index.ts +16 -1
  54. package/src/managers/locations.ts +2 -20
  55. package/src/managers/nft.ts +28 -0
  56. package/src/managers/plot.ts +123 -0
  57. package/src/nft/atomicassets.ts +231 -0
  58. package/src/nft/atomicdata.ts +130 -0
  59. package/src/nft/buildImmutableData.ts +319 -0
  60. package/src/nft/description.ts +45 -13
  61. package/src/nft/index.ts +3 -0
  62. package/src/resolution/describe-module.ts +5 -8
  63. package/src/resolution/display-name.ts +38 -10
  64. package/src/resolution/resolve-item.ts +20 -12
  65. package/src/scheduling/accessor.ts +4 -0
  66. package/src/scheduling/projection.ts +79 -27
  67. package/src/scheduling/schedule.ts +15 -1
  68. package/src/scheduling/task-cargo.ts +46 -0
  69. package/src/shipload.ts +5 -0
  70. package/src/subscriptions/manager.ts +40 -6
  71. package/src/subscriptions/mappers.ts +3 -8
  72. package/src/subscriptions/types.ts +3 -2
  73. package/src/testing/catalog-hash.ts +19 -0
  74. package/src/testing/index.ts +2 -0
  75. package/src/testing/projection-parity.ts +143 -0
  76. package/src/travel/travel.ts +61 -2
  77. package/src/types/index.ts +0 -1
  78. package/src/types.ts +17 -12
  79. package/src/utils/cargo.ts +27 -0
  80. package/src/utils/system.ts +25 -24
  81. package/src/entities/container.ts +0 -108
  82. package/src/entities/ship-deploy.ts +0 -258
  83. package/src/entities/ship.ts +0 -204
  84. package/src/entities/warehouse.ts +0 -119
  85. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,414 @@
1
+ export function computeBaseHullmass(stats: Record<string, number>): number {
2
+ return 100000 - 75 * stats.density
3
+ }
4
+
5
+ export function computeShipHullCapabilities(stats: Record<string, number>): {
6
+ hullmass: number
7
+ capacity: number
8
+ } {
9
+ const statSum = stats.strength + stats.hardness + stats.saturation
10
+ const exponent = statSum / 2997.0
11
+ return {
12
+ hullmass: computeBaseHullmass(stats),
13
+ capacity: Math.floor(5000000 * 6 ** exponent),
14
+ }
15
+ }
16
+
17
+ export function computeEngineCapabilities(stats: Record<string, number>): {
18
+ thrust: number
19
+ drain: number
20
+ } {
21
+ const vol = stats.volatility
22
+ const thm = stats.thermal
23
+
24
+ return {
25
+ thrust: 400 + Math.floor((vol * 3) / 4),
26
+ drain: Math.max(30, 50 - Math.floor(thm / 70)),
27
+ }
28
+ }
29
+
30
+ export function computeGeneratorCapabilities(stats: Record<string, number>): {
31
+ capacity: number
32
+ recharge: number
33
+ } {
34
+ const res = stats.resonance
35
+ const ref = stats.reflectivity
36
+
37
+ return {
38
+ capacity: 300 + Math.floor(res / 6),
39
+ recharge: 1 + Math.floor((ref * 3) / 1000),
40
+ }
41
+ }
42
+
43
+ export interface GathererDepthParams {
44
+ readonly floor: number
45
+ readonly slope: number
46
+ }
47
+
48
+ export const GATHERER_DEPTH_TABLE: readonly GathererDepthParams[] = [
49
+ {floor: 500, slope: 5},
50
+ {floor: 2000, slope: 11},
51
+ {floor: 7000, slope: 16},
52
+ {floor: 15000, slope: 18},
53
+ {floor: 25000, slope: 19},
54
+ {floor: 35000, slope: 16},
55
+ {floor: 46000, slope: 12},
56
+ {floor: 53500, slope: 10},
57
+ {floor: 60000, slope: 5},
58
+ {floor: 63500, slope: 2},
59
+ ]
60
+
61
+ export const GATHERER_DEPTH_MAX_TIER = 10
62
+
63
+ export function gathererDepthForTier(tol: number, tier: number): number {
64
+ if (tier < 1 || tier > GATHERER_DEPTH_MAX_TIER) {
65
+ throw new Error(`gatherer tier out of range: ${tier}`)
66
+ }
67
+ const p = GATHERER_DEPTH_TABLE[tier - 1]
68
+ return p.floor + tol * p.slope
69
+ }
70
+
71
+ export function computeGathererCapabilities(
72
+ stats: Record<string, number>,
73
+ tier: number
74
+ ): {
75
+ yield: number
76
+ drain: number
77
+ depth: number
78
+ } {
79
+ const str = stats.strength
80
+ const con = stats.conductivity
81
+ const tol = stats.tolerance
82
+
83
+ return {
84
+ yield: 200 + str,
85
+ drain: Math.max(250, 1250 - Math.floor((con * 25) / 20)),
86
+ depth: gathererDepthForTier(tol, tier),
87
+ }
88
+ }
89
+
90
+ export function computeLoaderCapabilities(stats: Record<string, number>): {
91
+ mass: number
92
+ thrust: number
93
+ quantity: number
94
+ } {
95
+ const insulation = stats.insulation
96
+ const plasticity = stats.plasticity
97
+
98
+ return {
99
+ mass: Math.max(200, 2000 - Math.floor(insulation * 2)),
100
+ thrust: 1 + Math.floor(plasticity / 500),
101
+ quantity: 1,
102
+ }
103
+ }
104
+
105
+ export function computeCrafterCapabilities(stats: Record<string, number>): {
106
+ speed: number
107
+ drain: number
108
+ } {
109
+ const rea = stats.reactivity
110
+ const fin = stats.fineness
111
+
112
+ return {
113
+ speed: 100 + Math.floor((rea * 4) / 5),
114
+ drain: Math.max(5, 30 - Math.floor(fin / 33)),
115
+ }
116
+ }
117
+
118
+ export function computeHaulerCapabilities(stats: Record<string, number>): {
119
+ capacity: number
120
+ efficiency: number
121
+ drain: number
122
+ } {
123
+ const resonance = stats.resonance
124
+ const conductivity = stats.conductivity
125
+ const reflectivity = stats.reflectivity
126
+
127
+ return {
128
+ capacity: Math.max(1, 1 + Math.floor(resonance / 400)),
129
+ efficiency: 2000 + conductivity * 6,
130
+ drain: Math.max(3, 15 - Math.floor(reflectivity / 80)),
131
+ }
132
+ }
133
+
134
+ export function computeStorageCapabilities(
135
+ stats: Record<string, number>,
136
+ baseCapacity: number
137
+ ): {
138
+ capacityBonus: number
139
+ } {
140
+ const strength = stats.strength
141
+ const density = stats.density
142
+ const hardness = stats.hardness
143
+ const saturation = stats.saturation
144
+
145
+ const statSum = strength + density + hardness + saturation
146
+ const capacityBonus = Math.floor(
147
+ (baseCapacity * (10 + Math.floor((statSum * 10) / 2997))) / 100
148
+ )
149
+
150
+ return {capacityBonus}
151
+ }
152
+
153
+ import {
154
+ ITEM_CONTAINER_T1_PACKED,
155
+ ITEM_CONTAINER_T2_PACKED,
156
+ ITEM_EXTRACTOR_T1_PACKED,
157
+ ITEM_FACTORY_T1_PACKED,
158
+ ITEM_SHIP_T1_PACKED,
159
+ ITEM_WAREHOUSE_T1_PACKED,
160
+ } from '../data/item-ids'
161
+ import {
162
+ getModuleCapabilityType,
163
+ MODULE_BATTERY,
164
+ MODULE_ENGINE,
165
+ MODULE_GENERATOR,
166
+ MODULE_GATHERER,
167
+ MODULE_LOADER,
168
+ MODULE_STORAGE,
169
+ MODULE_CRAFTER,
170
+ MODULE_HAULER,
171
+ MODULE_WARP,
172
+ } from '../capabilities/modules'
173
+ import {getItem} from '../data/catalog'
174
+ import {decodeCraftedItemStats} from './crafting'
175
+ import {
176
+ applySlotMultiplier,
177
+ clampUint16,
178
+ getSlotAmp,
179
+ type InstalledModule,
180
+ } from '../entities/slot-multiplier'
181
+ import type {EntitySlot} from '../data/recipes-runtime'
182
+
183
+ export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
184
+ switch (itemId) {
185
+ case ITEM_SHIP_T1_PACKED:
186
+ case ITEM_EXTRACTOR_T1_PACKED:
187
+ case ITEM_FACTORY_T1_PACKED:
188
+ return computeShipHullCapabilities(stats).capacity
189
+ case ITEM_CONTAINER_T1_PACKED:
190
+ return computeContainerCapabilities(stats).capacity
191
+ case ITEM_WAREHOUSE_T1_PACKED:
192
+ return computeWarehouseHullCapabilities(stats).capacity
193
+ case ITEM_CONTAINER_T2_PACKED:
194
+ return computeContainerT2Capabilities(stats).capacity
195
+ default:
196
+ return 0
197
+ }
198
+ }
199
+
200
+ export function computeWarpCapabilities(stats: Record<string, number>): {
201
+ range: number
202
+ } {
203
+ const res = stats.resonance
204
+ return {range: 100 + res * 3}
205
+ }
206
+
207
+ export function computeWarehouseHullCapabilities(stats: Record<string, number>): {
208
+ hullmass: number
209
+ capacity: number
210
+ } {
211
+ const statSum = stats.strength + stats.hardness + stats.saturation
212
+ const exponent = statSum / 2997.0
213
+ return {
214
+ hullmass: computeBaseHullmass(stats),
215
+ capacity: Math.floor(100000000 * 6 ** exponent),
216
+ }
217
+ }
218
+
219
+ export interface ComputedCapabilities {
220
+ hullmass: number
221
+ capacity: number
222
+ engines?: {thrust: number; drain: number}
223
+ generator?: {capacity: number; recharge: number}
224
+ gatherer?: {yield: number; drain: number; depth: number}
225
+ loaders?: {mass: number; thrust: number; quantity: number}
226
+ crafter?: {speed: number; drain: number}
227
+ hauler?: {capacity: number; efficiency: number; drain: number}
228
+ warp?: {range: number}
229
+ }
230
+
231
+ export function computeEntityCapabilities(
232
+ stats: Record<string, number>,
233
+ itemId: number,
234
+ modules: InstalledModule[],
235
+ layout: EntitySlot[]
236
+ ): ComputedCapabilities {
237
+ let totalThrust = 0
238
+ let totalEngineDrain = 0
239
+ let hasEngine = false
240
+
241
+ let totalGenCapacity = 0
242
+ let totalGenRecharge = 0
243
+ let hasGenerator = false
244
+
245
+ let totalLoaderMass = 0
246
+ let totalLoaderThrust = 0
247
+ let totalLoaderQuantity = 0
248
+ let hasLoader = false
249
+
250
+ let totalGathYield = 0
251
+ let totalGathDrain = 0
252
+ let maxGathDepth = 0
253
+ let hasGatherer = false
254
+
255
+ let totalStorageBonus = 0
256
+ const baseCapacity = computeBaseCapacity(itemId, stats)
257
+ let installedModuleMass = 0
258
+
259
+ let totalCrafterSpeed = 0
260
+ let totalCrafterDrain = 0
261
+ let hasCrafter = false
262
+
263
+ let totalHaulerCapacity = 0
264
+ let weightedHaulerEffNum = 0n
265
+ let totalHaulerDrain = 0
266
+ let hasHauler = false
267
+
268
+ let totalWarpRange = 0
269
+ let hasWarp = false
270
+
271
+ let totalBatteryStatSum = 0
272
+ let batteryCount = 0
273
+
274
+ for (const mod of modules) {
275
+ const item = getItem(mod.itemId)
276
+ const modType = getModuleCapabilityType(mod.itemId)
277
+ const amp = getSlotAmp(layout, mod.slotIndex)
278
+ const decodedStats = decodeCraftedItemStats(mod.itemId, mod.stats)
279
+ installedModuleMass += item.mass
280
+
281
+ if (modType === MODULE_ENGINE) {
282
+ hasEngine = true
283
+ const caps = computeEngineCapabilities(decodedStats)
284
+ totalThrust += applySlotMultiplier(caps.thrust, amp)
285
+ totalEngineDrain += caps.drain
286
+ } else if (modType === MODULE_GENERATOR) {
287
+ hasGenerator = true
288
+ const caps = computeGeneratorCapabilities(decodedStats)
289
+ totalGenCapacity += applySlotMultiplier(caps.capacity, amp)
290
+ totalGenRecharge += applySlotMultiplier(caps.recharge, amp)
291
+ } else if (modType === MODULE_GATHERER) {
292
+ hasGatherer = true
293
+ const tier = item.tier
294
+ const caps = computeGathererCapabilities(decodedStats, tier)
295
+ totalGathYield += applySlotMultiplier(caps.yield, amp)
296
+ totalGathDrain += caps.drain
297
+ if (caps.depth > maxGathDepth) maxGathDepth = caps.depth
298
+ } else if (modType === MODULE_LOADER) {
299
+ hasLoader = true
300
+ const caps = computeLoaderCapabilities(decodedStats)
301
+ totalLoaderMass += caps.mass
302
+ totalLoaderThrust += applySlotMultiplier(caps.thrust, amp)
303
+ totalLoaderQuantity += caps.quantity
304
+ } else if (modType === MODULE_STORAGE) {
305
+ const caps = computeStorageCapabilities(decodedStats, baseCapacity)
306
+ totalStorageBonus += caps.capacityBonus
307
+ } else if (modType === MODULE_CRAFTER) {
308
+ hasCrafter = true
309
+ const caps = computeCrafterCapabilities(decodedStats)
310
+ totalCrafterSpeed += applySlotMultiplier(caps.speed, amp)
311
+ totalCrafterDrain += caps.drain
312
+ } else if (modType === MODULE_HAULER) {
313
+ hasHauler = true
314
+ const caps = computeHaulerCapabilities(decodedStats)
315
+ const eff = applySlotMultiplier(caps.efficiency, amp)
316
+ totalHaulerCapacity += caps.capacity
317
+ weightedHaulerEffNum += BigInt(eff) * BigInt(caps.capacity)
318
+ totalHaulerDrain += caps.drain
319
+ } else if (modType === MODULE_WARP) {
320
+ hasWarp = true
321
+ const caps = computeWarpCapabilities(decodedStats)
322
+ totalWarpRange += applySlotMultiplier(caps.range, amp)
323
+ } else if (modType === MODULE_BATTERY) {
324
+ batteryCount++
325
+ const vol = decodedStats.volatility ?? 0
326
+ const thm = decodedStats.thermal ?? 0
327
+ const pla = decodedStats.plasticity ?? 0
328
+ const ins = decodedStats.insulation ?? 0
329
+ totalBatteryStatSum += vol + thm + pla + ins
330
+ }
331
+ }
332
+
333
+ if (hasGenerator && batteryCount > 0) {
334
+ const genCapBase = totalGenCapacity
335
+ const bonusPctNum = 10 * batteryCount + Math.floor((totalBatteryStatSum * 10) / 2997)
336
+ totalGenCapacity += Math.floor((genCapBase * bonusPctNum) / 100)
337
+ }
338
+
339
+ const result: ComputedCapabilities = {
340
+ hullmass: computeBaseHullmass(stats) + installedModuleMass,
341
+ capacity: baseCapacity + totalStorageBonus,
342
+ }
343
+
344
+ if (hasEngine) {
345
+ result.engines = {thrust: totalThrust, drain: totalEngineDrain}
346
+ }
347
+ if (hasGenerator) {
348
+ result.generator = {
349
+ capacity: clampUint16(totalGenCapacity),
350
+ recharge: clampUint16(totalGenRecharge),
351
+ }
352
+ }
353
+ if (hasGatherer) {
354
+ result.gatherer = {
355
+ yield: clampUint16(totalGathYield),
356
+ drain: totalGathDrain,
357
+ depth: maxGathDepth,
358
+ }
359
+ }
360
+ if (hasLoader) {
361
+ result.loaders = {
362
+ mass: totalLoaderMass,
363
+ thrust: clampUint16(totalLoaderThrust),
364
+ quantity: totalLoaderQuantity,
365
+ }
366
+ }
367
+ if (hasCrafter) {
368
+ result.crafter = {speed: clampUint16(totalCrafterSpeed), drain: totalCrafterDrain}
369
+ }
370
+ if (hasHauler) {
371
+ const efficiency =
372
+ totalHaulerCapacity > 0 ? Number(weightedHaulerEffNum / BigInt(totalHaulerCapacity)) : 0
373
+ result.hauler = {
374
+ capacity: totalHaulerCapacity,
375
+ efficiency: clampUint16(efficiency),
376
+ drain: totalHaulerDrain,
377
+ }
378
+ }
379
+ if (hasWarp) {
380
+ result.warp = {range: totalWarpRange}
381
+ }
382
+
383
+ return result
384
+ }
385
+
386
+ export function computeContainerCapabilities(stats: Record<string, number>): {
387
+ hullmass: number
388
+ capacity: number
389
+ } {
390
+ const statSum = stats.strength + stats.hardness + stats.saturation
391
+ const exponent = statSum / 2997.0
392
+ return {
393
+ hullmass: computeBaseHullmass(stats),
394
+ capacity: Math.floor(22000000 * 6 ** exponent),
395
+ }
396
+ }
397
+
398
+ export function computeContainerT2Capabilities(stats: Record<string, number>): {
399
+ hullmass: number
400
+ capacity: number
401
+ } {
402
+ const strength = stats.strength
403
+ const density = stats.density
404
+ const hardness = stats.hardness
405
+ const saturation = stats.saturation
406
+
407
+ const hullmass = 70000 - 50 * density
408
+
409
+ const statSum = strength + hardness + saturation
410
+ const exponent = statSum / 2947
411
+ const capacity = Math.floor(24000000 * 6 ** exponent)
412
+
413
+ return {hullmass, capacity}
414
+ }
@@ -0,0 +1,117 @@
1
+ import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
2
+ import {getStatDefinitions, type StatDefinition} from './stats'
3
+ import {getRecipe, type Recipe} from '../data/recipes-runtime'
4
+ import {getItem} from '../data/catalog'
5
+ import {
6
+ ITEM_ENGINE_T1,
7
+ ITEM_EXTRACTOR_T1_PACKED,
8
+ ITEM_GENERATOR_T1,
9
+ ITEM_GATHERER_T1,
10
+ ITEM_LOADER_T1,
11
+ ITEM_CRAFTER_T1,
12
+ ITEM_STORAGE_T1,
13
+ ITEM_HAULER_T1,
14
+ ITEM_WARP_T1,
15
+ ITEM_BATTERY_T1,
16
+ ITEM_SHIP_T1_PACKED,
17
+ ITEM_CONTAINER_T1_PACKED,
18
+ ITEM_WAREHOUSE_T1_PACKED,
19
+ ITEM_CONTAINER_T2_PACKED,
20
+ } from '../data/item-ids'
21
+ import type {StatMapping} from '../data/capabilities'
22
+
23
+ export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
24
+ engine: ITEM_ENGINE_T1,
25
+ generator: ITEM_GENERATOR_T1,
26
+ gatherer: ITEM_GATHERER_T1,
27
+ loader: ITEM_LOADER_T1,
28
+ crafter: ITEM_CRAFTER_T1,
29
+ storage: ITEM_STORAGE_T1,
30
+ hauler: ITEM_HAULER_T1,
31
+ warp: ITEM_WARP_T1,
32
+ battery: ITEM_BATTERY_T1,
33
+ 'ship-t1': ITEM_SHIP_T1_PACKED,
34
+ 'container-t1': ITEM_CONTAINER_T1_PACKED,
35
+ 'warehouse-t1': ITEM_WAREHOUSE_T1_PACKED,
36
+ 'extractor-t1': ITEM_EXTRACTOR_T1_PACKED,
37
+ 'container-t2': ITEM_CONTAINER_T2_PACKED,
38
+ }
39
+
40
+ /**
41
+ * Walk a recipe's slot source down to the raw category stat that ultimately
42
+ * lands in that slot. Returns the StatDefinition or undefined if the trace
43
+ * dead-ends (unknown sub-component, missing slot, etc.).
44
+ *
45
+ * Multi-source sub-slots collapse to `sources[0]`; top-level multi-source slots
46
+ * are expanded by the caller (`deriveStatMappings`).
47
+ */
48
+ function traceToRawCategoryStat(
49
+ recipe: Recipe,
50
+ source: {inputIndex: number; statIndex: number},
51
+ visited: Set<number> = new Set()
52
+ ): StatDefinition | undefined {
53
+ const input = recipe.inputs[source.inputIndex]
54
+ if (!input) return undefined
55
+ const inputItem = getItem(input.itemId)
56
+ if (inputItem.type === 'resource' && inputItem.category) {
57
+ const defs = getStatDefinitions(inputItem.category)
58
+ return defs[source.statIndex]
59
+ }
60
+ if (visited.has(input.itemId)) return undefined
61
+ const subRecipe = getRecipe(input.itemId)
62
+ if (!subRecipe) return undefined
63
+ const subSlot = subRecipe.statSlots[source.statIndex]
64
+ if (!subSlot) return undefined
65
+ const subSource = subSlot.sources[0]
66
+ if (!subSource) return undefined
67
+ const nextVisited = new Set(visited)
68
+ nextVisited.add(input.itemId)
69
+ return traceToRawCategoryStat(subRecipe, subSource, nextVisited)
70
+ }
71
+
72
+ let cached: StatMapping[] | undefined
73
+
74
+ export function deriveStatMappings(): StatMapping[] {
75
+ if (cached) return cached
76
+ const out: StatMapping[] = []
77
+ const seen = new Set<string>()
78
+ for (const [kind, slots] of Object.entries(SLOT_FORMULAS) as [
79
+ SlotConsumerKind,
80
+ Record<number, {capability: string; attribute: string}>,
81
+ ][]) {
82
+ const itemId = KIND_TO_ITEM_ID[kind]
83
+ const recipe = getRecipe(itemId)
84
+ if (!recipe) continue
85
+ for (const [slotIdxStr, consumer] of Object.entries(slots)) {
86
+ const slotIdx = Number(slotIdxStr)
87
+ const slot = recipe.statSlots[slotIdx]
88
+ if (!slot) continue
89
+ for (const source of slot.sources) {
90
+ const stat = traceToRawCategoryStat(recipe, source)
91
+ if (!stat) continue
92
+ const key = `${stat.label}|${consumer.capability}|${consumer.attribute}`
93
+ if (seen.has(key)) continue
94
+ seen.add(key)
95
+ out.push({
96
+ stat: stat.label,
97
+ capability: consumer.capability,
98
+ attribute: consumer.attribute,
99
+ })
100
+ }
101
+ }
102
+ }
103
+ cached = out
104
+ return out
105
+ }
106
+
107
+ export function getStatMappings(): StatMapping[] {
108
+ return deriveStatMappings()
109
+ }
110
+
111
+ export function getStatMappingsForStat(stat: string): StatMapping[] {
112
+ return deriveStatMappings().filter((m) => m.stat === stat)
113
+ }
114
+
115
+ export function getStatMappingsForCapability(capability: string): StatMapping[] {
116
+ return deriveStatMappings().filter((m) => m.capability === capability)
117
+ }
@@ -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,11 @@ 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 {RESERVE_TIERS, TIER_ROLL_MAX, tierOfReserve, rollTier, rollWithinTier} from './tiers'
29
32
  export type {ReserveTier, TierRange} from './tiers'
30
33
 
34
+ export {getEffectiveReserve} from './reserve-regen'
35
+ export type {EffectiveReserveInput} from './reserve-regen'
36
+
31
37
  export * from './stats'
32
38
  export * from './crafting'
@@ -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
+ }