@shipload/sdk 2.0.0-rc2 → 2.0.0-rc20

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 (80) hide show
  1. package/README.md +1 -349
  2. package/lib/shipload.d.ts +1658 -1126
  3. package/lib/shipload.js +6847 -3082
  4. package/lib/shipload.js.map +1 -1
  5. package/lib/shipload.m.js +6468 -2793
  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 +57 -0
  14. package/src/capabilities/storage.ts +101 -9
  15. package/src/contracts/server.ts +717 -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/items.json +17 -0
  20. package/src/data/locations.ts +53 -0
  21. package/src/data/nebula-adjectives.json +211 -0
  22. package/src/data/nebula-nouns.json +151 -0
  23. package/src/data/recipes.ts +587 -0
  24. package/src/data/syllables.json +1386 -780
  25. package/src/data/tiers.ts +45 -0
  26. package/src/derivation/crafting.ts +287 -0
  27. package/src/derivation/index.ts +30 -0
  28. package/src/derivation/location-size.ts +15 -0
  29. package/src/derivation/resources.ts +136 -0
  30. package/src/derivation/stats.ts +146 -0
  31. package/src/derivation/stratum.ts +134 -0
  32. package/src/derivation/tiers.ts +54 -0
  33. package/src/entities/cargo-utils.ts +10 -68
  34. package/src/entities/container.ts +37 -0
  35. package/src/entities/entity-inventory.ts +13 -13
  36. package/src/entities/inventory-accessor.ts +2 -6
  37. package/src/entities/location.ts +5 -200
  38. package/src/entities/makers.ts +136 -17
  39. package/src/entities/player.ts +1 -274
  40. package/src/entities/ship-deploy.ts +258 -0
  41. package/src/entities/ship.ts +28 -34
  42. package/src/entities/warehouse.ts +35 -7
  43. package/src/errors.ts +59 -5
  44. package/src/format.ts +12 -0
  45. package/src/index-module.ts +233 -50
  46. package/src/managers/actions.ts +138 -88
  47. package/src/managers/context.ts +19 -9
  48. package/src/managers/index.ts +0 -1
  49. package/src/managers/locations.ts +2 -85
  50. package/src/market/items.ts +93 -0
  51. package/src/nft/description.ts +176 -0
  52. package/src/nft/deserializers.ts +81 -0
  53. package/src/nft/index.ts +2 -0
  54. package/src/resolution/describe-module.ts +165 -0
  55. package/src/resolution/display-name.ts +39 -0
  56. package/src/resolution/resolve-item.ts +343 -0
  57. package/src/scheduling/projection.ts +220 -67
  58. package/src/scheduling/schedule.ts +2 -2
  59. package/src/shipload.ts +10 -5
  60. package/src/subscriptions/connection.ts +154 -0
  61. package/src/subscriptions/debug.ts +17 -0
  62. package/src/subscriptions/index.ts +5 -0
  63. package/src/subscriptions/manager.ts +240 -0
  64. package/src/subscriptions/mappers.ts +28 -0
  65. package/src/subscriptions/types.ts +143 -0
  66. package/src/travel/travel.ts +30 -17
  67. package/src/types/capabilities.ts +11 -14
  68. package/src/types/entity-traits.ts +3 -4
  69. package/src/types/entity.ts +9 -6
  70. package/src/types.ts +61 -55
  71. package/src/utils/system.ts +66 -53
  72. package/src/capabilities/extraction.ts +0 -37
  73. package/src/data/goods.json +0 -23
  74. package/src/managers/trades.ts +0 -119
  75. package/src/market/goods.ts +0 -31
  76. package/src/market/market.ts +0 -208
  77. package/src/market/rolls.ts +0 -8
  78. package/src/trading/collect.ts +0 -938
  79. package/src/trading/deal.ts +0 -207
  80. 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,287 @@
1
+ import {UInt64} from '@wharfkit/antelope'
2
+ import type {ResourceCategory} from '../types'
3
+ import {
4
+ entityRecipes,
5
+ getComponentById,
6
+ getEntityRecipe,
7
+ getEntityRecipeByItemId,
8
+ getModuleRecipe,
9
+ moduleRecipes,
10
+ } from '../data/recipes'
11
+ import {getStatDefinitions} from './stats'
12
+ import {deriveResourceStats} from './stratum'
13
+
14
+ export interface StackInput {
15
+ quantity: number
16
+ stats: Record<string, number>
17
+ }
18
+
19
+ export interface CategoryStacks {
20
+ category: ResourceCategory
21
+ stacks: StackInput[]
22
+ }
23
+
24
+ export function encodeStats(values: number[]): bigint {
25
+ let stats = 0n
26
+ for (let i = 0; i < values.length && i < 6; i++) {
27
+ stats |= BigInt(values[i] & 0x3ff) << BigInt(i * 10)
28
+ }
29
+ return stats
30
+ }
31
+
32
+ export function decodeStat(stats: bigint, index: number): number {
33
+ return Number((stats >> BigInt(index * 10)) & 0x3ffn)
34
+ }
35
+
36
+ export function decodeStats(stats: bigint, count: number): number[] {
37
+ const result: number[] = []
38
+ for (let i = 0; i < count; i++) {
39
+ result.push(decodeStat(stats, i))
40
+ }
41
+ return result
42
+ }
43
+
44
+ function mapStatsToKeys(stats: bigint, statDefs: {key: string}[]): Record<string, number> {
45
+ const values = decodeStats(stats, statDefs.length)
46
+ const result: Record<string, number> = {}
47
+ for (let i = 0; i < statDefs.length; i++) {
48
+ result[statDefs[i].key] = values[i]
49
+ }
50
+ return result
51
+ }
52
+
53
+ export function decodeCraftedItemStats(itemId: number, stats: bigint): Record<string, number> {
54
+ const comp = getComponentById(itemId)
55
+ if (comp) return mapStatsToKeys(stats, comp.stats)
56
+
57
+ const entityRecipe = entityRecipes.find((r) => r.packedItemId === itemId)
58
+ if (entityRecipe) return mapStatsToKeys(stats, entityRecipe.stats)
59
+
60
+ const moduleRecipe = moduleRecipes.find((r) => r.itemId === itemId)
61
+ if (moduleRecipe) return mapStatsToKeys(stats, moduleRecipe.stats)
62
+
63
+ return {}
64
+ }
65
+
66
+ export function blendStacks(stacks: StackInput[], statKey: string): number {
67
+ let totalQty = 0
68
+ let weightedSum = 0
69
+ for (const stack of stacks) {
70
+ const val = stack.stats[statKey] ?? 0
71
+ weightedSum += val * stack.quantity
72
+ totalQty += stack.quantity
73
+ }
74
+ if (totalQty === 0) return 0
75
+ return Math.floor(weightedSum / totalQty)
76
+ }
77
+
78
+ export function computeComponentStats(
79
+ componentId: number,
80
+ categoryStacks: CategoryStacks[]
81
+ ): {key: string; value: number}[] {
82
+ const comp = getComponentById(componentId)
83
+ if (!comp) return []
84
+
85
+ return comp.stats.map((statDef) => {
86
+ const matching = categoryStacks.find((cs) => cs.category === statDef.source)
87
+ const value = matching ? blendStacks(matching.stacks, statDef.key) : 0
88
+ return {key: statDef.key, value: Math.max(1, Math.min(999, value))}
89
+ })
90
+ }
91
+
92
+ export function blendComponentStacks(
93
+ stacks: {quantity: number; stats: Record<string, number>}[]
94
+ ): Record<string, number> {
95
+ if (stacks.length === 0) return {}
96
+ const allKeys = new Set<string>()
97
+ for (const s of stacks) {
98
+ for (const k of Object.keys(s.stats)) allKeys.add(k)
99
+ }
100
+ const result: Record<string, number> = {}
101
+ for (const key of allKeys) {
102
+ result[key] = blendStacks(
103
+ stacks.map((s) => ({quantity: s.quantity, stats: s.stats})),
104
+ key
105
+ )
106
+ }
107
+ return result
108
+ }
109
+
110
+ export function computeEntityStats(
111
+ entityRecipeId: string,
112
+ componentStacks: Record<number, {quantity: number; stats: Record<string, number>}[]>
113
+ ): {key: string; value: number}[] {
114
+ const recipe = getEntityRecipe(entityRecipeId) ?? getModuleRecipe(entityRecipeId)
115
+ if (!recipe) return []
116
+
117
+ const blendedByComponent: Record<number, Record<string, number>> = {}
118
+ for (const [compId, stacks] of Object.entries(componentStacks)) {
119
+ blendedByComponent[Number(compId)] = blendComponentStacks(stacks)
120
+ }
121
+
122
+ return recipe.stats.map((stat) => {
123
+ const blended = blendedByComponent[stat.sourceComponentId] ?? {}
124
+ const value = blended[stat.sourceStatKey] ?? 0
125
+ return {key: stat.key, value: Math.max(1, Math.min(999, value))}
126
+ })
127
+ }
128
+
129
+ function decodeStackStats(itemId: number, stats: UInt64): Record<string, number> {
130
+ if (itemId >= 10000) {
131
+ return decodeCraftedItemStats(itemId, BigInt(stats.toString()))
132
+ }
133
+ const s = BigInt(stats.toString())
134
+ return {stat1: decodeStat(s, 0), stat2: decodeStat(s, 1), stat3: decodeStat(s, 2)}
135
+ }
136
+
137
+ export const categoryItemMass: Record<string, number> = {
138
+ ore: 30000,
139
+ crystal: 40000,
140
+ gas: 15000,
141
+ regolith: 22000,
142
+ biomass: 15000,
143
+ }
144
+
145
+ export function computeInputMass(
146
+ itemId: string | number,
147
+ itemType: 'component' | 'module' | 'entity'
148
+ ): number {
149
+ if (itemType === 'component') {
150
+ const comp = getComponentById(itemId as number)
151
+ if (!comp) return 0
152
+ return comp.recipe.reduce((sum, input) => {
153
+ const mass = input.category ? categoryItemMass[input.category] ?? 0 : 0
154
+ return sum + mass * input.quantity
155
+ }, 0)
156
+ }
157
+ if (itemType === 'module') {
158
+ const mod = getModuleRecipe(itemId as string)
159
+ if (!mod) return 0
160
+ return mod.recipe.reduce((sum, input) => {
161
+ const comp = input.itemId ? getComponentById(input.itemId) : undefined
162
+ return sum + (comp?.mass ?? 0) * input.quantity
163
+ }, 0)
164
+ }
165
+ if (itemType === 'entity') {
166
+ const ent = getEntityRecipe(itemId as string)
167
+ if (!ent) return 0
168
+ return ent.recipe.reduce((sum, input) => {
169
+ const comp = input.itemId ? getComponentById(input.itemId) : undefined
170
+ return sum + (comp?.mass ?? 0) * input.quantity
171
+ }, 0)
172
+ }
173
+ return 0
174
+ }
175
+
176
+ export function blendCrossGroup(sources: {value: number; weight: number}[]): number {
177
+ let weightedSum = 0
178
+ let totalWeight = 0
179
+ for (const src of sources) {
180
+ weightedSum += src.value * src.weight
181
+ totalWeight += src.weight
182
+ }
183
+ if (totalWeight === 0) return 1
184
+ const result = Math.floor(weightedSum / totalWeight)
185
+ return Math.max(1, Math.min(999, result))
186
+ }
187
+
188
+ export function blendCargoStacks(
189
+ itemId: number,
190
+ stacks: {quantity: number; stats: UInt64}[]
191
+ ): UInt64 {
192
+ const decoded = stacks.map((s) => ({
193
+ quantity: s.quantity,
194
+ stats: decodeStackStats(itemId, s.stats),
195
+ }))
196
+ const allKeys = Object.keys(decoded[0]?.stats ?? {})
197
+ const blended = allKeys.map((key) => Math.max(1, Math.min(999, blendStacks(decoded, key))))
198
+ return UInt64.from(encodeStats(blended))
199
+ }
200
+
201
+ export interface RecipeSlotInput {
202
+ itemId: number
203
+ category: ResourceCategory | undefined
204
+ stacks: {quantity: number; stats: bigint}[]
205
+ }
206
+
207
+ function decodeRawStackToCategoryStats(
208
+ stats: bigint,
209
+ category: ResourceCategory
210
+ ): Record<string, number> {
211
+ const defs = getStatDefinitions(category)
212
+ const result: Record<string, number> = {}
213
+ if (defs[0]) result[defs[0].key] = decodeStat(stats, 0)
214
+ if (defs[1]) result[defs[1].key] = decodeStat(stats, 1)
215
+ if (defs[2]) result[defs[2].key] = decodeStat(stats, 2)
216
+ return result
217
+ }
218
+
219
+ export function computeCraftedOutputStats(
220
+ outputItemId: number,
221
+ slotInputs: RecipeSlotInput[]
222
+ ): UInt64 {
223
+ const component = getComponentById(outputItemId)
224
+ if (component) {
225
+ const categoryStacks: CategoryStacks[] = []
226
+ for (const slot of slotInputs) {
227
+ if (!slot.category) continue
228
+ const slotIsComponent = getComponentById(slot.itemId) !== undefined
229
+ const stacks: StackInput[] = slot.stacks.map((s) => ({
230
+ quantity: s.quantity,
231
+ stats: slotIsComponent
232
+ ? decodeCraftedItemStats(slot.itemId, s.stats)
233
+ : decodeRawStackToCategoryStats(s.stats, slot.category!),
234
+ }))
235
+ categoryStacks.push({category: slot.category, stacks})
236
+ }
237
+ const stats = computeComponentStats(outputItemId, categoryStacks)
238
+ const ordered = component.stats.map((statDef) => {
239
+ const found = stats.find((s) => s.key === statDef.key)
240
+ return found ? found.value : 0
241
+ })
242
+ return UInt64.from(encodeStats(ordered))
243
+ }
244
+
245
+ const entityRecipe = getEntityRecipeByItemId(outputItemId)
246
+ if (entityRecipe) {
247
+ const componentStacks: Record<number, {quantity: number; stats: Record<string, number>}[]> =
248
+ {}
249
+ for (const slot of slotInputs) {
250
+ if (slot.category !== undefined) {
251
+ throw new Error(
252
+ `entity recipe ${entityRecipe.id} expects component inputs but slot itemId=${slot.itemId} has category=${slot.category}`
253
+ )
254
+ }
255
+ const list = (componentStacks[slot.itemId] ??= [])
256
+ for (const s of slot.stacks) {
257
+ list.push({
258
+ quantity: s.quantity,
259
+ stats: decodeCraftedItemStats(slot.itemId, s.stats),
260
+ })
261
+ }
262
+ }
263
+ const stats = computeEntityStats(entityRecipe.id, componentStacks)
264
+ const ordered = entityRecipe.stats.map((statDef) => {
265
+ const found = stats.find((s) => s.key === statDef.key)
266
+ return found ? found.value : 0
267
+ })
268
+ return UInt64.from(encodeStats(ordered))
269
+ }
270
+
271
+ throw new Error(`computeCraftedOutputStats: no recipe found for outputItemId=${outputItemId}`)
272
+ }
273
+
274
+ /**
275
+ * Mirrors the contract's gather-time transform. Takes a deposit's entropy
276
+ * seed (bigint from deriveStratum), derives stats via weibull hashing, and
277
+ * returns a UInt64 whose bit-packed form matches what the contract writes
278
+ * to cargo_item.stats on gather.
279
+ *
280
+ * Use this whenever off-chain code simulates a gather (testmap, player
281
+ * scanners that project cargo outcomes) and needs a value that matches
282
+ * what on-chain cargo would carry.
283
+ */
284
+ export function encodeGatheredCargoStats(depositSeed: bigint): UInt64 {
285
+ const raw = deriveResourceStats(depositSeed)
286
+ return UInt64.from(encodeStats([raw.stat1, raw.stat2, raw.stat3]))
287
+ }
@@ -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,136 @@
1
+ import {ResourceTier} from '../types'
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
+ interface ResourceEntry {
22
+ id: number
23
+ tier: ResourceTier
24
+ }
25
+
26
+ const RESOURCE_CATALOG: ResourceEntry[] = [
27
+ {id: 101, tier: 't1'},
28
+ {id: 102, tier: 't2'},
29
+ {id: 103, tier: 't3'},
30
+ {id: 201, tier: 't1'},
31
+ {id: 202, tier: 't2'},
32
+ {id: 203, tier: 't3'},
33
+ {id: 301, tier: 't1'},
34
+ {id: 302, tier: 't2'},
35
+ {id: 303, tier: 't3'},
36
+ {id: 401, tier: 't1'},
37
+ {id: 402, tier: 't2'},
38
+ {id: 403, tier: 't3'},
39
+ {id: 501, tier: 't1'},
40
+ {id: 502, tier: 't2'},
41
+ {id: 503, tier: 't3'},
42
+ ]
43
+
44
+ export function getDepthThreshold(tier: ResourceTier): number {
45
+ switch (tier) {
46
+ case 't1':
47
+ return DEPTH_THRESHOLD_T1
48
+ case 't2':
49
+ return DEPTH_THRESHOLD_T2
50
+ case 't3':
51
+ return DEPTH_THRESHOLD_T3
52
+ case 't4':
53
+ return DEPTH_THRESHOLD_T4
54
+ case 't5':
55
+ return DEPTH_THRESHOLD_T5
56
+ }
57
+ }
58
+
59
+ export function getResourceTier(itemId: number): ResourceTier {
60
+ const entry = RESOURCE_CATALOG.find((r) => r.id === itemId)
61
+ return entry ? entry.tier : 't5'
62
+ }
63
+
64
+ export function getResourceWeight(itemId: number, stratum: number): number {
65
+ const tier = getResourceTier(itemId)
66
+ const threshold = getDepthThreshold(tier)
67
+ if (stratum < threshold) return 0
68
+
69
+ const depthAbove = stratum - threshold
70
+
71
+ switch (tier) {
72
+ case 't1':
73
+ if (stratum < 2000) return 100
74
+ if (stratum < 10000) return 80
75
+ if (stratum < 30000) return 50
76
+ return 30
77
+ case 't2':
78
+ if (depthAbove < 3000) return 40
79
+ if (depthAbove < 8000) return 60
80
+ return 50
81
+ case 't3':
82
+ if (depthAbove < 5000) return 20
83
+ if (depthAbove < 15000) return 35
84
+ return 40
85
+ case 't4':
86
+ if (depthAbove < 10000) return 10
87
+ if (depthAbove < 25000) return 20
88
+ return 30
89
+ case 't5':
90
+ return 10
91
+ }
92
+ }
93
+
94
+ const ASTEROID_RESOURCES = [101, 102, 103, 201, 202]
95
+ const NEBULA_RESOURCES = [202, 203, 301, 302, 303]
96
+ const GAS_GIANT_RESOURCES = [301, 302, 303, 401, 501]
97
+ const ROCKY_RESOURCES = [101, 102, 103, 401, 402, 403, 503]
98
+ const TERRESTRIAL_RESOURCES = [201, 202, 401, 402, 501, 502, 503]
99
+ const ICY_RESOURCES = [101, 301, 302, 401, 403, 501, 502]
100
+ const OCEAN_RESOURCES = [201, 203, 301, 303, 501, 502, 503]
101
+ const INDUSTRIAL_RESOURCES = [101, 102, 103, 201, 203, 402, 403]
102
+
103
+ export function getLocationCandidates(locationType: number, subtype: number): number[] {
104
+ if (locationType === 2) return ASTEROID_RESOURCES
105
+ if (locationType === 3) return NEBULA_RESOURCES
106
+ if (locationType === 1) {
107
+ switch (subtype) {
108
+ case PLANET_SUBTYPE_GAS_GIANT:
109
+ return GAS_GIANT_RESOURCES
110
+ case PLANET_SUBTYPE_ROCKY:
111
+ return ROCKY_RESOURCES
112
+ case PLANET_SUBTYPE_TERRESTRIAL:
113
+ return TERRESTRIAL_RESOURCES
114
+ case PLANET_SUBTYPE_ICY:
115
+ return ICY_RESOURCES
116
+ case PLANET_SUBTYPE_OCEAN:
117
+ return OCEAN_RESOURCES
118
+ case PLANET_SUBTYPE_INDUSTRIAL:
119
+ return INDUSTRIAL_RESOURCES
120
+ }
121
+ }
122
+ return []
123
+ }
124
+
125
+ export function getEligibleResources(
126
+ locationType: number,
127
+ subtype: number,
128
+ stratum: number
129
+ ): number[] {
130
+ const candidates = getLocationCandidates(locationType, subtype)
131
+ return candidates.filter((itemId) => {
132
+ const tier = getResourceTier(itemId)
133
+ const threshold = getDepthThreshold(tier)
134
+ return stratum >= threshold
135
+ })
136
+ }
@@ -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
+ }