@shipload/sdk 1.0.0-next.74 → 1.0.0-next.75

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipload/sdk",
3
3
  "description": "SDKs for Shipload",
4
- "version": "1.0.0-next.74",
4
+ "version": "1.0.0-next.75",
5
5
  "homepage": "https://github.com/shipload/toolkit/tree/master/packages/sdk",
6
6
  "repository": {
7
7
  "type": "git",
@@ -0,0 +1,119 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {
3
+ ITEM_BEAM,
4
+ ITEM_ENGINE_T1,
5
+ ITEM_ORE_T1,
6
+ ITEM_PLATE,
7
+ ITEM_PLATE_T2,
8
+ ITEM_PROSPECTOR_T1A_PACKED,
9
+ ITEM_PROSPECTOR_T2A_PACKED,
10
+ ITEM_PROSPECTOR_T2B_PACKED,
11
+ ITEM_ROUSTABOUT_T1A_PACKED,
12
+ ITEM_STORAGE_T1,
13
+ } from './item-ids'
14
+ import {getComponents} from './catalog'
15
+ import {getRecipe} from './recipes-runtime'
16
+ import {completeComponentSet, getComponentProcess} from './component-process'
17
+ import {resolveItem} from '../resolution/resolve-item'
18
+
19
+ function inputsOf(itemId: number) {
20
+ const recipe = getRecipe(itemId)
21
+ if (!recipe) throw new Error(`no recipe for ${itemId}`)
22
+ return recipe.inputs
23
+ }
24
+
25
+ describe('component process', () => {
26
+ test('every component at every tier is refined or machined, five of each per tier', () => {
27
+ const tiers = new Set(getComponents().map((item) => item.tier))
28
+ for (const tier of tiers) {
29
+ const kinds = getComponents({tier}).map((item) => getComponentProcess(item.id))
30
+ expect(kinds.filter((k) => k === 'refined').length).toBe(5)
31
+ expect(kinds.filter((k) => k === 'machined').length).toBe(5)
32
+ }
33
+ })
34
+
35
+ test('the refined five are the single-resource components', () => {
36
+ const refined = getComponents({tier: 1})
37
+ .filter((item) => getComponentProcess(item.id) === 'refined')
38
+ .map((item) => item.name)
39
+ .sort()
40
+ expect(refined).toEqual(['Ceramic', 'Plasma Cell', 'Plate', 'Polymer', 'Sensor'])
41
+ })
42
+
43
+ test('the kind holds across tiers', () => {
44
+ expect(getComponentProcess(ITEM_PLATE)).toBe('refined')
45
+ expect(getComponentProcess(ITEM_PLATE_T2)).toBe('refined')
46
+ expect(getComponentProcess(ITEM_BEAM)).toBe('machined')
47
+ })
48
+
49
+ test('non-components and unknown ids have no kind', () => {
50
+ expect(getComponentProcess(ITEM_ORE_T1)).toBeUndefined()
51
+ expect(getComponentProcess(ITEM_ENGINE_T1)).toBeUndefined()
52
+ expect(getComponentProcess(ITEM_ROUSTABOUT_T1A_PACKED)).toBeUndefined()
53
+ expect(getComponentProcess(65535)).toBeUndefined()
54
+ })
55
+
56
+ test('resolveItem carries the kind for components only', () => {
57
+ expect(resolveItem(ITEM_BEAM).process).toBe('machined')
58
+ expect(resolveItem(ITEM_PLATE_T2).process).toBe('refined')
59
+ expect(resolveItem(ITEM_ORE_T1).process).toBeUndefined()
60
+ })
61
+ })
62
+
63
+ describe('completeComponentSet', () => {
64
+ test('recognises the hull First and Second step recipes', () => {
65
+ expect(completeComponentSet(inputsOf(ITEM_ROUSTABOUT_T1A_PACKED))).toEqual({
66
+ process: 'refined',
67
+ tier: 1,
68
+ quantity: 200,
69
+ })
70
+ expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T1A_PACKED))).toEqual({
71
+ process: 'machined',
72
+ tier: 1,
73
+ quantity: 100,
74
+ })
75
+ expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T2A_PACKED))).toEqual({
76
+ process: 'refined',
77
+ tier: 2,
78
+ quantity: 200,
79
+ })
80
+ expect(completeComponentSet(inputsOf(ITEM_PROSPECTOR_T2B_PACKED))).toEqual({
81
+ process: 'machined',
82
+ tier: 2,
83
+ quantity: 150,
84
+ })
85
+ })
86
+
87
+ test('a mixed module recipe is not a complete set', () => {
88
+ expect(completeComponentSet(inputsOf(ITEM_STORAGE_T1))).toBeNull()
89
+ })
90
+
91
+ test('unequal quantities keep the set but drop the shared quantity', () => {
92
+ const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
93
+ index === 0 ? {...input, quantity: 50} : input
94
+ )
95
+ expect(completeComponentSet(inputs)).toEqual({process: 'refined', tier: 1, quantity: null})
96
+ })
97
+
98
+ test('one machined part among four refined is not a set', () => {
99
+ const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
100
+ index === 0 ? {...input, itemId: ITEM_BEAM} : input
101
+ )
102
+ expect(completeComponentSet(inputs)).toBeNull()
103
+ })
104
+
105
+ test('a repeated family is not a set', () => {
106
+ const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input) => ({
107
+ ...input,
108
+ itemId: ITEM_PLATE,
109
+ }))
110
+ expect(completeComponentSet(inputs)).toBeNull()
111
+ })
112
+
113
+ test('mixed tiers are not a set', () => {
114
+ const inputs = inputsOf(ITEM_ROUSTABOUT_T1A_PACKED).map((input, index) =>
115
+ index === 0 ? {...input, itemId: ITEM_PLATE_T2} : input
116
+ )
117
+ expect(completeComponentSet(inputs)).toBeNull()
118
+ })
119
+ })
@@ -0,0 +1,33 @@
1
+ import {getItem} from './catalog'
2
+ import {getItemFamily, type ComponentProcess} from './metadata'
3
+ import type {RecipeInput} from './recipes-runtime'
4
+
5
+ export const COMPONENT_PROCESS_ORDER: readonly ComponentProcess[] = ['refined', 'machined']
6
+
7
+ export function getComponentProcess(itemId: number): ComponentProcess | undefined {
8
+ return getItemFamily(itemId)?.process
9
+ }
10
+
11
+ export interface CompleteComponentSet {
12
+ process: ComponentProcess
13
+ tier: number
14
+ quantity: number | null
15
+ }
16
+
17
+ export function completeComponentSet(inputs: readonly RecipeInput[]): CompleteComponentSet | null {
18
+ const parts = inputs.filter((input) => getComponentProcess(input.itemId) !== undefined)
19
+ if (parts.length !== 5) return null
20
+ const process = getComponentProcess(parts[0].itemId)
21
+ const tier = getItem(parts[0].itemId).tier
22
+ const families = new Set<string>()
23
+ for (const part of parts) {
24
+ const item = getItem(part.itemId)
25
+ if (getComponentProcess(part.itemId) !== process || item.tier !== tier) return null
26
+ families.add(item.family)
27
+ }
28
+ if (!process || families.size !== 5) return null
29
+ const quantity = parts.every((part) => part.quantity === parts[0].quantity)
30
+ ? parts[0].quantity
31
+ : null
32
+ return {process, tier, quantity}
33
+ }
@@ -1,9 +1,12 @@
1
1
  import items from './items.json'
2
2
 
3
+ export type ComponentProcess = 'refined' | 'machined'
4
+
3
5
  export interface ItemFamily {
4
6
  name: string
5
7
  description: string
6
8
  color: string
9
+ process?: ComponentProcess
7
10
  }
8
11
 
9
12
  export interface EntityMetadata {
@@ -14,27 +17,27 @@ export const itemFamilies: Record<string, ItemFamily> = {
14
17
  // === Resources ===
15
18
  ore: {
16
19
  name: 'Ore',
17
- description: 'Refines into Plate and Beam.',
20
+ description: 'Goes into Plate and Beam.',
18
21
  color: '#C26D3F',
19
22
  },
20
23
  crystal: {
21
24
  name: 'Crystal',
22
- description: 'Refines into Sensors and Resonators.',
25
+ description: 'Goes into Sensors and Resonators.',
23
26
  color: '#4ADBFF',
24
27
  },
25
28
  gas: {
26
29
  name: 'Gas',
27
- description: 'Refines into Plasma Cells and Reactors.',
30
+ description: 'Goes into Plasma Cells and Reactors.',
28
31
  color: '#B877FF',
29
32
  },
30
33
  regolith: {
31
34
  name: 'Regolith',
32
- description: 'Refines into Ceramic and Frame.',
35
+ description: 'Goes into Ceramic and Frame.',
33
36
  color: '#C4A57B',
34
37
  },
35
38
  biomass: {
36
39
  name: 'Biomass',
37
- description: 'Refines into Polymer and Resin.',
40
+ description: 'Goes into Polymer and Resin.',
38
41
  color: '#5A8B3E',
39
42
  },
40
43
 
@@ -43,51 +46,61 @@ export const itemFamilies: Record<string, ItemFamily> = {
43
46
  name: 'Plate',
44
47
  description: 'Goes into Cargo Holds, Containers, and hulls.',
45
48
  color: '#7B8D9E',
49
+ process: 'refined',
46
50
  },
47
51
  frame: {
48
52
  name: 'Frame',
49
53
  description: 'Goes into Limpet Bays, Cargo Holds, and hulls.',
50
54
  color: '#C4A57B',
55
+ process: 'machined',
51
56
  },
52
57
  'plasma-cell': {
53
58
  name: 'Plasma Cell',
54
59
  description: 'Goes into Engines, Battery Banks, and hulls.',
55
60
  color: '#E86344',
61
+ process: 'refined',
56
62
  },
57
63
  resonator: {
58
64
  name: 'Resonator',
59
65
  description: 'Goes into Power Cores, Tractor Beams, Warp Drives, and hulls.',
60
66
  color: '#4ADBFF',
67
+ process: 'machined',
61
68
  },
62
69
  beam: {
63
70
  name: 'Beam',
64
71
  description: 'Goes into Limpet Bays and hulls.',
65
72
  color: '#7B8D9E',
73
+ process: 'machined',
66
74
  },
67
75
  sensor: {
68
76
  name: 'Sensor',
69
77
  description: 'Goes into Fabricators, Assembly Arms, and hulls.',
70
78
  color: '#4ADBFF',
79
+ process: 'refined',
71
80
  },
72
81
  polymer: {
73
82
  name: 'Polymer',
74
83
  description: 'Goes into Shuttle Bays, Battery Banks, and hulls.',
75
84
  color: '#5A8B3E',
85
+ process: 'refined',
76
86
  },
77
87
  ceramic: {
78
88
  name: 'Ceramic',
79
89
  description: 'Goes into Assembly Arms, Containers, and hulls.',
80
90
  color: '#C4A57B',
91
+ process: 'refined',
81
92
  },
82
93
  reactor: {
83
94
  name: 'Reactor',
84
95
  description: 'Goes into Fabricators, Warp Drives, and hulls.',
85
96
  color: '#B877FF',
97
+ process: 'machined',
86
98
  },
87
99
  resin: {
88
100
  name: 'Resin',
89
101
  description: 'Goes into Tractor Beams and hulls.',
90
102
  color: '#5A8B3E',
103
+ process: 'machined',
91
104
  },
92
105
 
93
106
  // === Modules ===
@@ -141,7 +141,13 @@ export {
141
141
  categoryLabelFromIndex,
142
142
  } from './data/catalog'
143
143
  export {getItemFamily, itemFamilies, itemFamilyKey} from './data/metadata'
144
- export type {ItemFamily} from './data/metadata'
144
+ export type {ComponentProcess, ItemFamily} from './data/metadata'
145
+ export {
146
+ COMPONENT_PROCESS_ORDER,
147
+ completeComponentSet,
148
+ getComponentProcess,
149
+ } from './data/component-process'
150
+ export type {CompleteComponentSet} from './data/component-process'
145
151
  export {getCurrentEpoch, getEpochInfo} from './scheduling/epoch'
146
152
  export type {EpochInfo} from './scheduling/epoch'
147
153
  export {
@@ -5,6 +5,8 @@ import {getItem, getModules} from '../data/catalog'
5
5
  import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
6
6
  import {getEntityLayout} from '../data/recipes-runtime'
7
7
  import {entityMetadata, getItemFamily} from '../data/metadata'
8
+ import type {ComponentProcess} from '../data/metadata'
9
+ import {getComponentProcess} from '../data/component-process'
8
10
  import {
9
11
  getModuleCapabilityType,
10
12
  isModuleItem,
@@ -75,6 +77,7 @@ export interface ResolvedItem {
75
77
  tier: number
76
78
  mass: number
77
79
  itemType: ResolvedItemType
80
+ process?: ComponentProcess
78
81
  stats?: ResolvedItemStat[]
79
82
  attributes?: ResolvedAttributeGroup[]
80
83
  moduleSlots?: ResolvedModuleSlot[]
@@ -149,6 +152,7 @@ function resolveComponent(id: number, stats?: UInt64Type): ResolvedItem {
149
152
  tier: item.tier,
150
153
  mass: item.mass,
151
154
  itemType: 'component',
155
+ process: getComponentProcess(id),
152
156
  stats: resolvedStats,
153
157
  }
154
158
  }