@shipload/sdk 2.0.0-rc2 → 2.0.0-rc21

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 (84) hide show
  1. package/README.md +1 -349
  2. package/lib/shipload.d.ts +1729 -1127
  3. package/lib/shipload.js +7944 -3165
  4. package/lib/shipload.js.map +1 -1
  5. package/lib/shipload.m.js +7487 -2840
  6. package/lib/shipload.m.js.map +1 -1
  7. package/package.json +6 -4
  8. package/src/capabilities/crafting.ts +22 -0
  9. package/src/capabilities/gathering.ts +36 -0
  10. package/src/capabilities/guards.ts +3 -8
  11. package/src/capabilities/hauling.ts +22 -0
  12. package/src/capabilities/index.ts +4 -1
  13. package/src/capabilities/modules.ts +86 -0
  14. package/src/capabilities/storage.ts +101 -9
  15. package/src/contracts/server.ts +785 -293
  16. package/src/data/capabilities.ts +408 -0
  17. package/src/data/categories.ts +55 -0
  18. package/src/data/colors.ts +71 -0
  19. package/src/data/entities.json +50 -0
  20. package/src/data/item-ids.ts +75 -0
  21. package/src/data/items.json +252 -0
  22. package/src/data/locations.ts +53 -0
  23. package/src/data/metadata.ts +208 -0
  24. package/src/data/nebula-adjectives.json +211 -0
  25. package/src/data/nebula-nouns.json +151 -0
  26. package/src/data/recipes-runtime.ts +65 -0
  27. package/src/data/recipes.json +878 -0
  28. package/src/data/syllables.json +1386 -780
  29. package/src/data/tiers.ts +45 -0
  30. package/src/derivation/crafting.ts +348 -0
  31. package/src/derivation/index.ts +30 -0
  32. package/src/derivation/location-size.ts +15 -0
  33. package/src/derivation/resources.ts +112 -0
  34. package/src/derivation/stats.ts +146 -0
  35. package/src/derivation/stratum.ts +134 -0
  36. package/src/derivation/tiers.ts +54 -0
  37. package/src/entities/cargo-utils.ts +10 -68
  38. package/src/entities/container.ts +37 -0
  39. package/src/entities/entity-inventory.ts +13 -13
  40. package/src/entities/inventory-accessor.ts +2 -6
  41. package/src/entities/location.ts +5 -200
  42. package/src/entities/makers.ts +144 -17
  43. package/src/entities/player.ts +1 -274
  44. package/src/entities/ship-deploy.ts +258 -0
  45. package/src/entities/ship.ts +28 -34
  46. package/src/entities/warehouse.ts +35 -7
  47. package/src/errors.ts +59 -5
  48. package/src/format.ts +12 -0
  49. package/src/index-module.ts +188 -50
  50. package/src/managers/actions.ts +138 -88
  51. package/src/managers/context.ts +19 -9
  52. package/src/managers/index.ts +0 -1
  53. package/src/managers/locations.ts +2 -85
  54. package/src/market/items.ts +41 -0
  55. package/src/nft/description.ts +176 -0
  56. package/src/nft/deserializers.ts +83 -0
  57. package/src/nft/index.ts +2 -0
  58. package/src/resolution/describe-module.ts +165 -0
  59. package/src/resolution/display-name.ts +43 -0
  60. package/src/resolution/resolve-item.ts +358 -0
  61. package/src/scheduling/projection.ts +200 -67
  62. package/src/scheduling/schedule.ts +2 -2
  63. package/src/shipload.ts +10 -5
  64. package/src/subscriptions/connection.ts +154 -0
  65. package/src/subscriptions/debug.ts +17 -0
  66. package/src/subscriptions/index.ts +5 -0
  67. package/src/subscriptions/manager.ts +240 -0
  68. package/src/subscriptions/mappers.ts +28 -0
  69. package/src/subscriptions/types.ts +143 -0
  70. package/src/travel/travel.ts +37 -23
  71. package/src/types/capabilities.ts +11 -14
  72. package/src/types/entity-traits.ts +3 -4
  73. package/src/types/entity.ts +9 -6
  74. package/src/types.ts +72 -72
  75. package/src/utils/system.ts +66 -53
  76. package/src/capabilities/extraction.ts +0 -37
  77. package/src/data/goods.json +0 -23
  78. package/src/managers/trades.ts +0 -119
  79. package/src/market/goods.ts +0 -31
  80. package/src/market/market.ts +0 -208
  81. package/src/market/rolls.ts +0 -8
  82. package/src/trading/collect.ts +0 -938
  83. package/src/trading/deal.ts +0 -207
  84. package/src/trading/trade.ts +0 -203
@@ -0,0 +1,45 @@
1
+ export type CraftedItemCategory = 'component' | 'module' | 'entity' | 'resource'
2
+
3
+ export const ITEM_TYPE_RESOURCE = 0
4
+ export const ITEM_TYPE_COMPONENT = 1
5
+ export const ITEM_TYPE_MODULE = 2
6
+ export const ITEM_TYPE_ENTITY = 3
7
+
8
+ export function itemTypeCode(id: number): number {
9
+ switch (itemCategory(id)) {
10
+ case 'resource':
11
+ return ITEM_TYPE_RESOURCE
12
+ case 'component':
13
+ return ITEM_TYPE_COMPONENT
14
+ case 'module':
15
+ return ITEM_TYPE_MODULE
16
+ case 'entity':
17
+ return ITEM_TYPE_ENTITY
18
+ }
19
+ }
20
+
21
+ export function itemTier(id: number): number {
22
+ if (id < 10000) return 0
23
+ return Math.floor(id / 10000)
24
+ }
25
+
26
+ export function itemOffset(id: number): number {
27
+ return id % 10000
28
+ }
29
+
30
+ export function itemCategory(id: number): CraftedItemCategory {
31
+ if (id < 10000) return 'resource'
32
+ const offset = itemOffset(id)
33
+ if (offset >= 200) return 'entity'
34
+ if (offset >= 100) return 'module'
35
+ return 'component'
36
+ }
37
+
38
+ export function isRelatedItem(a: number, b: number): boolean {
39
+ if (a < 10000 || b < 10000) return false
40
+ return itemOffset(a) === itemOffset(b)
41
+ }
42
+
43
+ export function isCraftedItem(id: number): boolean {
44
+ return id >= 10000
45
+ }
@@ -0,0 +1,348 @@
1
+ import {UInt64} from '@wharfkit/antelope'
2
+ import type {ResourceCategory} from '../types'
3
+ import {findItemByCategoryAndTier, getRecipe, Recipe} from '../data/recipes-runtime'
4
+ import {getItem} from '../market/items'
5
+ import {getStatDefinitions} from './stats'
6
+ import {deriveResourceStats} from './stratum'
7
+
8
+ export interface StackInput {
9
+ quantity: number
10
+ stats: Record<string, number>
11
+ }
12
+
13
+ export interface CategoryStacks {
14
+ category: ResourceCategory
15
+ stacks: StackInput[]
16
+ }
17
+
18
+ export function encodeStats(values: number[]): bigint {
19
+ let stats = 0n
20
+ for (let i = 0; i < values.length && i < 6; i++) {
21
+ stats |= BigInt(values[i] & 0x3ff) << BigInt(i * 10)
22
+ }
23
+ return stats
24
+ }
25
+
26
+ export function decodeStat(stats: bigint, index: number): number {
27
+ return Number((stats >> BigInt(index * 10)) & 0x3ffn)
28
+ }
29
+
30
+ export function decodeStats(stats: bigint, count: number): number[] {
31
+ const result: number[] = []
32
+ for (let i = 0; i < count; i++) {
33
+ result.push(decodeStat(stats, i))
34
+ }
35
+ return result
36
+ }
37
+
38
+ function getItemStatKeys(itemId: number): string[] {
39
+ const item = getItem(itemId)
40
+ if (item.type === 'resource') {
41
+ if (!item.category) return []
42
+ return getStatDefinitions(item.category).map((d) => d.key)
43
+ }
44
+ const recipe = getRecipe(itemId)
45
+ if (!recipe) return []
46
+ return recipe.statSlots.map((slot) => keyForStatSlot(recipe, slot))
47
+ }
48
+
49
+ function keyForStatSlot(
50
+ recipe: Recipe,
51
+ slot: {sources: {inputIndex: number; statIndex: number}[]}
52
+ ): string {
53
+ const src = slot.sources[0]
54
+ if (!src) return ''
55
+ return keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
56
+ }
57
+
58
+ function keyForRecipeInputStat(recipe: Recipe, inputIndex: number, statIndex: number): string {
59
+ const input = recipe.inputs[inputIndex]
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.
66
+ const innerKeys = getItemStatKeys(input.itemId)
67
+ return innerKeys[statIndex] ?? ''
68
+ }
69
+
70
+ export function decodeCraftedItemStats(itemId: number, stats: bigint): Record<string, number> {
71
+ const keys = getItemStatKeys(itemId)
72
+ const result: Record<string, number> = {}
73
+ for (let i = 0; i < keys.length; i++) {
74
+ if (keys[i]) result[keys[i]] = decodeStat(stats, i)
75
+ }
76
+ return result
77
+ }
78
+
79
+ export function blendStacks(stacks: StackInput[], statKey: string): number {
80
+ let totalQty = 0
81
+ let weightedSum = 0
82
+ for (const stack of stacks) {
83
+ const val = stack.stats[statKey] ?? 0
84
+ weightedSum += val * stack.quantity
85
+ totalQty += stack.quantity
86
+ }
87
+ if (totalQty === 0) return 0
88
+ return Math.floor(weightedSum / totalQty)
89
+ }
90
+
91
+ export function blendComponentStacks(
92
+ stacks: {quantity: number; stats: Record<string, number>}[]
93
+ ): Record<string, number> {
94
+ if (stacks.length === 0) return {}
95
+ const allKeys = new Set<string>()
96
+ for (const s of stacks) {
97
+ for (const k of Object.keys(s.stats)) allKeys.add(k)
98
+ }
99
+ const result: Record<string, number> = {}
100
+ for (const key of allKeys) {
101
+ result[key] = blendStacks(
102
+ stacks.map((s) => ({quantity: s.quantity, stats: s.stats})),
103
+ key
104
+ )
105
+ }
106
+ return result
107
+ }
108
+
109
+ export function computeComponentStats(
110
+ componentId: number,
111
+ categoryStacks: CategoryStacks[]
112
+ ): {key: string; value: number}[] {
113
+ const recipe = getRecipe(componentId)
114
+ if (!recipe) return []
115
+
116
+ return recipe.statSlots.map((slot) => {
117
+ const src = slot.sources[0]
118
+ const key = keyForStatSlot(recipe, slot)
119
+ const input = src ? recipe.inputs[src.inputIndex] : undefined
120
+ if (!input || !('category' in input)) {
121
+ return {key, value: Math.max(1, Math.min(999, 0))}
122
+ }
123
+ const matching = categoryStacks.find((cs) => cs.category === input.category)
124
+ const value = matching ? blendStacks(matching.stacks, key) : 0
125
+ return {key, value: Math.max(1, Math.min(999, value))}
126
+ })
127
+ }
128
+
129
+ export function computeEntityStats(
130
+ entityItemIdOrLegacyId: number | string,
131
+ componentStacks: Record<number, {quantity: number; stats: Record<string, number>}[]>
132
+ ): {key: string; value: number}[] {
133
+ const itemId =
134
+ typeof entityItemIdOrLegacyId === 'number'
135
+ ? entityItemIdOrLegacyId
136
+ : legacyEntityIdToItemId(entityItemIdOrLegacyId)
137
+ const recipe = getRecipe(itemId)
138
+ if (!recipe) return []
139
+
140
+ const blendedByComponent: Record<number, Record<string, number>> = {}
141
+ for (const [compId, stacks] of Object.entries(componentStacks)) {
142
+ blendedByComponent[Number(compId)] = blendComponentStacks(stacks)
143
+ }
144
+
145
+ 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}
152
+ }
153
+ const blended = blendedByComponent[input.itemId] ?? {}
154
+ const value = blended[key] ?? 0
155
+ return {key, value: Math.max(1, Math.min(999, value))}
156
+ })
157
+ }
158
+
159
+ function legacyEntityIdToItemId(id: string): number {
160
+ switch (id) {
161
+ case 'container':
162
+ return 10200
163
+ case 'ship-t1':
164
+ return 10201
165
+ case 'warehouse-t1':
166
+ return 10202
167
+ case 'container-t2':
168
+ return 20200
169
+ default:
170
+ return 0
171
+ }
172
+ }
173
+
174
+ function decodeStackStats(itemId: number, stats: UInt64): Record<string, number> {
175
+ if (itemId >= 10000) {
176
+ return decodeCraftedItemStats(itemId, BigInt(stats.toString()))
177
+ }
178
+ const s = BigInt(stats.toString())
179
+ return {stat1: decodeStat(s, 0), stat2: decodeStat(s, 1), stat3: decodeStat(s, 2)}
180
+ }
181
+
182
+ export function computeInputMass(itemId: number): number {
183
+ const recipe = getRecipe(itemId)
184
+ if (!recipe) throw new Error(`computeInputMass: no recipe found for itemId=${itemId}`)
185
+
186
+ let total = 0
187
+ 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
+ }
194
+ }
195
+ return total
196
+ }
197
+
198
+ export function blendCrossGroup(sources: {value: number; weight: number}[]): number {
199
+ let weightedSum = 0
200
+ let totalWeight = 0
201
+ for (const src of sources) {
202
+ weightedSum += src.value * src.weight
203
+ totalWeight += src.weight
204
+ }
205
+ if (totalWeight === 0) return 1
206
+ const result = Math.floor(weightedSum / totalWeight)
207
+ return Math.max(1, Math.min(999, result))
208
+ }
209
+
210
+ export function blendCargoStacks(
211
+ itemId: number,
212
+ stacks: {quantity: number; stats: UInt64}[]
213
+ ): UInt64 {
214
+ const decoded = stacks.map((s) => ({
215
+ quantity: s.quantity,
216
+ stats: decodeStackStats(itemId, s.stats),
217
+ }))
218
+ const allKeys = Object.keys(decoded[0]?.stats ?? {})
219
+ const blended = allKeys.map((key) => Math.max(1, Math.min(999, blendStacks(decoded, key))))
220
+ return UInt64.from(encodeStats(blended))
221
+ }
222
+
223
+ export interface RecipeSlotInput {
224
+ itemId: number
225
+ category: ResourceCategory | undefined
226
+ stacks: {quantity: number; stats: bigint}[]
227
+ }
228
+
229
+ function decodeRawStackToCategoryStats(
230
+ stats: bigint,
231
+ category: ResourceCategory
232
+ ): Record<string, number> {
233
+ const defs = getStatDefinitions(category)
234
+ const result: Record<string, number> = {}
235
+ if (defs[0]) result[defs[0].key] = decodeStat(stats, 0)
236
+ if (defs[1]) result[defs[1].key] = decodeStat(stats, 1)
237
+ if (defs[2]) result[defs[2].key] = decodeStat(stats, 2)
238
+ return result
239
+ }
240
+
241
+ export function computeCraftedOutputStats(
242
+ outputItemId: number,
243
+ slotInputs: RecipeSlotInput[]
244
+ ): UInt64 {
245
+ const recipe = getRecipe(outputItemId)
246
+ if (!recipe) {
247
+ throw new Error(
248
+ `computeCraftedOutputStats: no recipe found for outputItemId=${outputItemId}`
249
+ )
250
+ }
251
+
252
+ const outputItem = getItem(outputItemId)
253
+
254
+ if (outputItem.type === 'entity') {
255
+ for (const slot of slotInputs) {
256
+ if (slot.category !== undefined) {
257
+ throw new Error(
258
+ `entity recipe ${outputItemId} expects component inputs but slot itemId=${slot.itemId} has category=${slot.category}`
259
+ )
260
+ }
261
+ }
262
+ }
263
+
264
+ // Decode each slot's stacks into key→value maps using the slot item's
265
+ // own stat layout, so blending works regardless of recipe shape.
266
+ const decodedByItem: Record<number, {quantity: number; stats: Record<string, number>}[]> = {}
267
+ const decodedByCategory: Partial<Record<ResourceCategory, StackInput[]>> = {}
268
+
269
+ for (const slot of slotInputs) {
270
+ if (slot.category !== undefined) {
271
+ const list = (decodedByCategory[slot.category] ??= [])
272
+ for (const s of slot.stacks) {
273
+ list.push({
274
+ quantity: s.quantity,
275
+ stats: decodeRawStackToCategoryStats(s.stats, slot.category),
276
+ })
277
+ }
278
+ } else {
279
+ const list = (decodedByItem[slot.itemId] ??= [])
280
+ for (const s of slot.stacks) {
281
+ list.push({
282
+ quantity: s.quantity,
283
+ stats: decodeCraftedItemStats(slot.itemId, s.stats),
284
+ })
285
+ }
286
+ }
287
+ }
288
+
289
+ // Pre-blend itemId inputs once per item id.
290
+ const blendedByItem: Record<number, Record<string, number>> = {}
291
+ for (const [id, stacks] of Object.entries(decodedByItem)) {
292
+ blendedByItem[Number(id)] = blendComponentStacks(stacks)
293
+ }
294
+
295
+ const out: number[] = []
296
+ for (const slot of recipe.statSlots) {
297
+ if (slot.sources.length === 0) {
298
+ out.push(0)
299
+ continue
300
+ }
301
+ if (slot.sources.length === 1 || recipe.blendWeights.length === 0) {
302
+ const src = slot.sources[0]
303
+ const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
304
+ const input = recipe.inputs[src.inputIndex]
305
+ let value = 0
306
+ if (input && 'category' in input) {
307
+ value = blendStacks(decodedByCategory[input.category] ?? [], key)
308
+ } else if (input) {
309
+ value = blendedByItem[input.itemId]?.[key] ?? 0
310
+ }
311
+ out.push(Math.max(1, Math.min(999, value)))
312
+ } else {
313
+ let weightedSum = 0
314
+ let totalWeight = 0
315
+ for (const src of slot.sources) {
316
+ const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
317
+ const input = recipe.inputs[src.inputIndex]
318
+ const weight = recipe.blendWeights[src.inputIndex] ?? 1
319
+ let value = 0
320
+ if (input && 'category' in input) {
321
+ value = blendStacks(decodedByCategory[input.category] ?? [], key)
322
+ } else if (input) {
323
+ value = blendedByItem[input.itemId]?.[key] ?? 0
324
+ }
325
+ weightedSum += value * weight
326
+ totalWeight += weight
327
+ }
328
+ const blended = totalWeight > 0 ? Math.floor(weightedSum / totalWeight) : 0
329
+ out.push(Math.max(1, Math.min(999, blended)))
330
+ }
331
+ }
332
+ return UInt64.from(encodeStats(out))
333
+ }
334
+
335
+ /**
336
+ * Mirrors the contract's gather-time transform. Takes a deposit's entropy
337
+ * seed (bigint from deriveStratum), derives stats via weibull hashing, and
338
+ * returns a UInt64 whose bit-packed form matches what the contract writes
339
+ * to cargo_item.stats on gather.
340
+ *
341
+ * Use this whenever off-chain code simulates a gather (testmap, player
342
+ * scanners that project cargo outcomes) and needs a value that matches
343
+ * what on-chain cargo would carry.
344
+ */
345
+ export function encodeGatheredCargoStats(depositSeed: bigint): UInt64 {
346
+ const raw = deriveResourceStats(depositSeed)
347
+ return UInt64.from(encodeStats([raw.stat1, raw.stat2, raw.stat3]))
348
+ }
@@ -0,0 +1,30 @@
1
+ export {deriveStratum, deriveResourceStats} from './stratum'
2
+ export type {StratumInfo, ResourceStats} from './stratum'
3
+ export {deriveLocationSize} from './location-size'
4
+ export {
5
+ getEligibleResources,
6
+ getResourceWeight,
7
+ getLocationCandidates,
8
+ getDepthThreshold,
9
+ getResourceTier,
10
+ DEPTH_THRESHOLD_T1,
11
+ DEPTH_THRESHOLD_T2,
12
+ DEPTH_THRESHOLD_T3,
13
+ DEPTH_THRESHOLD_T4,
14
+ DEPTH_THRESHOLD_T5,
15
+ LOCATION_MIN_DEPTH,
16
+ LOCATION_MAX_DEPTH,
17
+ YIELD_THRESHOLD,
18
+ PLANET_SUBTYPE_GAS_GIANT,
19
+ PLANET_SUBTYPE_ROCKY,
20
+ PLANET_SUBTYPE_TERRESTRIAL,
21
+ PLANET_SUBTYPE_ICY,
22
+ PLANET_SUBTYPE_OCEAN,
23
+ PLANET_SUBTYPE_INDUSTRIAL,
24
+ } from './resources'
25
+
26
+ export {RESERVE_TIERS, TIER_ROLL_MAX, rollTier, rollWithinTier} from './tiers'
27
+ export type {ReserveTier, TierRange} from './tiers'
28
+
29
+ export * from './stats'
30
+ export * from './crafting'
@@ -0,0 +1,15 @@
1
+ import {ServerContract} from '../contracts'
2
+ import {LocationType} from '../types'
3
+ import {LOCATION_MAX_DEPTH, LOCATION_MIN_DEPTH} from './resources'
4
+
5
+ export function deriveLocationSize(loc: ServerContract.Types.location_static): number {
6
+ if (loc.type.toNumber() === LocationType.EMPTY) return 0
7
+
8
+ const raw = (loc.seed0.toNumber() << 8) | loc.seed1.toNumber()
9
+ const normalized = raw / 65535
10
+
11
+ const curved = Math.pow(normalized, 3.0)
12
+
13
+ const range = LOCATION_MAX_DEPTH - LOCATION_MIN_DEPTH
14
+ return Math.floor(LOCATION_MIN_DEPTH + curved * range)
15
+ }
@@ -0,0 +1,112 @@
1
+ import {getItem} from '../market/items'
2
+
3
+ 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
8
+
9
+ export const LOCATION_MIN_DEPTH = 500
10
+ export const LOCATION_MAX_DEPTH = 65535
11
+
12
+ export const YIELD_THRESHOLD = Math.floor(0.001 * 0xffffffff)
13
+
14
+ export const PLANET_SUBTYPE_GAS_GIANT = 0
15
+ export const PLANET_SUBTYPE_ROCKY = 1
16
+ export const PLANET_SUBTYPE_TERRESTRIAL = 2
17
+ export const PLANET_SUBTYPE_ICY = 3
18
+ export const PLANET_SUBTYPE_OCEAN = 4
19
+ export const PLANET_SUBTYPE_INDUSTRIAL = 5
20
+
21
+ 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
+ }
34
+ }
35
+
36
+ export function getResourceTier(itemId: number): number {
37
+ return getItem(itemId).tier
38
+ }
39
+
40
+ export function getResourceWeight(itemId: number, stratum: number): number {
41
+ const tier = getResourceTier(itemId)
42
+ const threshold = getDepthThreshold(tier)
43
+ if (stratum < threshold) return 0
44
+
45
+ const depthAbove = stratum - threshold
46
+
47
+ switch (tier) {
48
+ case 1:
49
+ if (stratum < 2000) return 100
50
+ if (stratum < 10000) return 80
51
+ if (stratum < 30000) return 50
52
+ return 30
53
+ case 2:
54
+ if (depthAbove < 3000) return 40
55
+ if (depthAbove < 8000) return 60
56
+ return 50
57
+ case 3:
58
+ if (depthAbove < 5000) return 20
59
+ if (depthAbove < 15000) return 35
60
+ return 40
61
+ case 4:
62
+ if (depthAbove < 10000) return 10
63
+ if (depthAbove < 25000) return 20
64
+ return 30
65
+ default:
66
+ return 10
67
+ }
68
+ }
69
+
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]
78
+
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) {
83
+ switch (subtype) {
84
+ case PLANET_SUBTYPE_GAS_GIANT:
85
+ return GAS_GIANT_RESOURCES
86
+ case PLANET_SUBTYPE_ROCKY:
87
+ return ROCKY_RESOURCES
88
+ case PLANET_SUBTYPE_TERRESTRIAL:
89
+ return TERRESTRIAL_RESOURCES
90
+ case PLANET_SUBTYPE_ICY:
91
+ return ICY_RESOURCES
92
+ case PLANET_SUBTYPE_OCEAN:
93
+ return OCEAN_RESOURCES
94
+ case PLANET_SUBTYPE_INDUSTRIAL:
95
+ return INDUSTRIAL_RESOURCES
96
+ }
97
+ }
98
+ return []
99
+ }
100
+
101
+ export function getEligibleResources(
102
+ locationType: number,
103
+ subtype: number,
104
+ stratum: number
105
+ ): number[] {
106
+ const candidates = getLocationCandidates(locationType, subtype)
107
+ return candidates.filter((itemId) => {
108
+ const tier = getResourceTier(itemId)
109
+ const threshold = getDepthThreshold(tier)
110
+ return stratum >= threshold
111
+ })
112
+ }
@@ -0,0 +1,146 @@
1
+ import type {ResourceCategory} from '../types'
2
+
3
+ export interface StatDefinition {
4
+ key: string
5
+ label: string
6
+ abbreviation: string
7
+ purpose: string
8
+ inverted?: boolean
9
+ }
10
+
11
+ const ORE_STATS: StatDefinition[] = [
12
+ {
13
+ key: 'strength',
14
+ label: 'Strength',
15
+ abbreviation: 'STR',
16
+ purpose: 'Raw structural/mechanical force',
17
+ },
18
+ {
19
+ key: 'tolerance',
20
+ label: 'Tolerance',
21
+ abbreviation: 'TOL',
22
+ purpose: 'Ability to withstand heat, pressure, and stress extremes',
23
+ },
24
+ {
25
+ key: 'density',
26
+ label: 'Density',
27
+ abbreviation: 'DEN',
28
+ purpose: 'Mass per unit',
29
+ inverted: true,
30
+ },
31
+ ]
32
+
33
+ const CRYSTAL_STATS: StatDefinition[] = [
34
+ {
35
+ key: 'conductivity',
36
+ label: 'Conductivity',
37
+ abbreviation: 'CON',
38
+ purpose: 'Efficiency of energy/signal transfer through crystalline lattice',
39
+ },
40
+ {
41
+ key: 'resonance',
42
+ label: 'Resonance',
43
+ abbreviation: 'RES',
44
+ purpose: 'Frequency tuning and piezoelectric response — storage and amplification',
45
+ },
46
+ {
47
+ key: 'reflectivity',
48
+ label: 'Reflectivity',
49
+ abbreviation: 'REF',
50
+ purpose: 'Optical refraction and reflection — lenses, mirrors, focus',
51
+ },
52
+ ]
53
+
54
+ const GAS_STATS: StatDefinition[] = [
55
+ {
56
+ key: 'volatility',
57
+ label: 'Volatility',
58
+ abbreviation: 'VOL',
59
+ purpose: 'Energy release potential for propulsion and force',
60
+ },
61
+ {
62
+ key: 'reactivity',
63
+ label: 'Reactivity',
64
+ abbreviation: 'REA',
65
+ purpose: 'Chemical interaction speed for processing and penetration',
66
+ },
67
+ {
68
+ key: 'thermal',
69
+ label: 'Thermal',
70
+ abbreviation: 'THM',
71
+ purpose: 'Heat capacity for thermal management',
72
+ },
73
+ ]
74
+
75
+ const REGOLITH_STATS: StatDefinition[] = [
76
+ {
77
+ key: 'composition',
78
+ label: 'Composition',
79
+ abbreviation: 'COM',
80
+ purpose: 'Mineral/metal mix — drives sensor, chip, and optic crafting quality',
81
+ },
82
+ {
83
+ key: 'hardness',
84
+ label: 'Hardness',
85
+ abbreviation: 'HRD',
86
+ purpose: 'Particle hardness — cutting surfaces, abrasives, wear resistance',
87
+ },
88
+ {
89
+ key: 'fineness',
90
+ label: 'Fineness',
91
+ abbreviation: 'FIN',
92
+ purpose: 'Grain size — fine powder for smooth composites and sintering',
93
+ },
94
+ ]
95
+
96
+ const BIOMASS_STATS: StatDefinition[] = [
97
+ {
98
+ key: 'plasticity',
99
+ label: 'Plasticity',
100
+ abbreviation: 'PLA',
101
+ purpose: 'Flexibility and deformation under load',
102
+ },
103
+ {
104
+ key: 'insulation',
105
+ label: 'Insulation',
106
+ abbreviation: 'INS',
107
+ purpose: 'Thermal and electrical blocking capacity',
108
+ },
109
+ {
110
+ key: 'saturation',
111
+ label: 'Saturation',
112
+ abbreviation: 'SAT',
113
+ purpose: 'Concentration of useful organic compounds per unit',
114
+ },
115
+ ]
116
+
117
+ const STAT_MAP: Record<ResourceCategory, StatDefinition[]> = {
118
+ ore: ORE_STATS,
119
+ crystal: CRYSTAL_STATS,
120
+ gas: GAS_STATS,
121
+ regolith: REGOLITH_STATS,
122
+ biomass: BIOMASS_STATS,
123
+ }
124
+
125
+ export function getStatDefinitions(category: ResourceCategory): StatDefinition[] {
126
+ return STAT_MAP[category]
127
+ }
128
+
129
+ export function getStatName(category: ResourceCategory, index: 0 | 1 | 2): StatDefinition {
130
+ return STAT_MAP[category][index]
131
+ }
132
+
133
+ export interface NamedStats {
134
+ definitions: StatDefinition[]
135
+ values: [number, number, number]
136
+ }
137
+
138
+ export function resolveStats(
139
+ category: ResourceCategory,
140
+ stats: {stat1: number; stat2: number; stat3: number}
141
+ ): NamedStats {
142
+ return {
143
+ definitions: STAT_MAP[category],
144
+ values: [stats.stat1, stats.stat2, stats.stat3],
145
+ }
146
+ }