@shipload/sdk 2.0.0-rc13 → 2.0.0-rc15

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.
@@ -6,8 +6,8 @@ import {ServerContract} from './contracts'
6
6
 
7
7
  export {Shipload} from './shipload'
8
8
  export {Ship} from './entities/ship'
9
- export type {ShipStateInput} from './entities/ship'
10
- export {Warehouse} from './entities/warehouse'
9
+ export type {ShipStateInput, PackedModuleInput} from './entities/ship'
10
+ export {Warehouse, computeWarehouseCapabilities} from './entities/warehouse'
11
11
  export type {WarehouseStateInput} from './entities/warehouse'
12
12
  export {Container} from './entities/container'
13
13
  export type {ContainerStateInput} from './entities/container'
@@ -152,11 +152,14 @@ export * from './capabilities'
152
152
  export {
153
153
  categoryColors,
154
154
  tierColors,
155
+ tierLabels,
155
156
  categoryIcons,
157
+ categoryIconShapes,
156
158
  componentIcon,
157
159
  moduleIcon,
158
- itemIcons,
160
+ itemAbbreviations,
159
161
  } from './data/colors'
162
+ export type {CategoryIconShape} from './data/colors'
160
163
 
161
164
  export {itemTier, itemOffset, itemCategory, isRelatedItem, isCraftedItem} from './data/tiers'
162
165
  export type {CraftedItemCategory} from './data/tiers'
@@ -226,6 +229,7 @@ export type {
226
229
 
227
230
  export {
228
231
  encodeStats,
232
+ encodeGatheredCargoStats,
229
233
  decodeStat,
230
234
  decodeStats,
231
235
  decodeCraftedItemStats,
@@ -237,7 +241,7 @@ export {
237
241
  blendCrossGroup,
238
242
  categoryItemMass,
239
243
  computeInputMass,
240
- computeCraftedOutputSeed,
244
+ computeCraftedOutputStats,
241
245
  } from './derivation/crafting'
242
246
  export type {StackInput, CategoryStacks, RecipeSlotInput} from './derivation/crafting'
243
247
 
@@ -266,6 +270,19 @@ export type {
266
270
  ResolvedItemType,
267
271
  } from './resolution/resolve-item'
268
272
 
273
+ export {
274
+ describeModule,
275
+ describeModuleForItem,
276
+ describeModuleForSlot,
277
+ renderDescription,
278
+ } from './resolution/describe-module'
279
+ export type {
280
+ TextSpan,
281
+ CapabilityInput,
282
+ ModuleDescription,
283
+ RenderDescriptionOptions,
284
+ } from './resolution/describe-module'
285
+
269
286
  export * as NFT from './nft'
270
287
  export {
271
288
  deserializeAsset,
@@ -312,3 +329,8 @@ export {
312
329
  ITEM_TYPE_ENTITY,
313
330
  itemTypeCode,
314
331
  } from './data/tiers'
332
+
333
+ export {formatMass, formatMassDelta} from './format'
334
+
335
+ export {displayName, describeItem} from './resolution/display-name'
336
+ export type {DescribeOptions} from './resolution/display-name'
@@ -1,4 +1,15 @@
1
- import {Action, Int64, Name, NameType, UInt16, UInt32, UInt64, UInt64Type} from '@wharfkit/antelope'
1
+ import {
2
+ Action,
3
+ Int64,
4
+ Name,
5
+ NameType,
6
+ UInt16,
7
+ UInt16Type,
8
+ UInt32,
9
+ UInt32Type,
10
+ UInt64,
11
+ UInt64Type,
12
+ } from '@wharfkit/antelope'
2
13
  import {BaseManager} from './base'
3
14
  import {CoordinatesType, EntityType, EntityTypeName} from '../types'
4
15
  import {ServerContract} from '../contracts'
@@ -71,7 +82,8 @@ export class ActionsManager extends BaseManager {
71
82
  sourceId: UInt64Type,
72
83
  destType: EntityTypeName,
73
84
  destId: UInt64Type,
74
- goodId: UInt64Type,
85
+ itemId: UInt64Type,
86
+ stats: UInt64Type,
75
87
  quantity: UInt64Type
76
88
  ): Action {
77
89
  return this.server.action('transfer', {
@@ -79,7 +91,8 @@ export class ActionsManager extends BaseManager {
79
91
  source_id: UInt64.from(sourceId),
80
92
  dest_type: destType,
81
93
  dest_id: UInt64.from(destId),
82
- item_id: UInt16.from(goodId),
94
+ item_id: UInt16.from(itemId),
95
+ stats: UInt64.from(stats),
83
96
  quantity: UInt32.from(quantity),
84
97
  })
85
98
  }
@@ -97,10 +110,21 @@ export class ActionsManager extends BaseManager {
97
110
  })
98
111
  }
99
112
 
100
- gather(shipId: UInt64Type, stratum: number, quantity: number): Action {
113
+ gather(
114
+ source: EntityRefInput,
115
+ destination: EntityRefInput,
116
+ stratum: UInt16Type,
117
+ quantity: UInt32Type
118
+ ): Action {
101
119
  return this.server.action('gather', {
102
- entity_type: EntityType.SHIP,
103
- id: UInt64.from(shipId),
120
+ source: ServerContract.Types.entity_ref.from({
121
+ entity_type: source.entityType,
122
+ entity_id: UInt64.from(source.entityId),
123
+ }),
124
+ destination: ServerContract.Types.entity_ref.from({
125
+ entity_type: destination.entityType,
126
+ entity_id: UInt64.from(destination.entityId),
127
+ }),
104
128
  stratum: UInt16.from(stratum),
105
129
  quantity: UInt32.from(quantity),
106
130
  })
@@ -152,14 +176,14 @@ export class ActionsManager extends BaseManager {
152
176
  entityType: EntityTypeName,
153
177
  entityId: UInt64Type,
154
178
  packedItemId: number,
155
- seed: bigint,
179
+ stats: bigint,
156
180
  entityName: string
157
181
  ): Action {
158
182
  return this.server.action('deploy', {
159
183
  entity_type: entityType,
160
184
  id: UInt64.from(entityId),
161
185
  packed_item_id: UInt16.from(packedItemId),
162
- seed: UInt64.from(seed),
186
+ stats: UInt64.from(stats),
163
187
  entity_name: entityName,
164
188
  })
165
189
  }
@@ -194,6 +218,22 @@ export class ActionsManager extends BaseManager {
194
218
  })
195
219
  }
196
220
 
221
+ wrap(
222
+ owner: NameType,
223
+ entityType: EntityTypeName,
224
+ entityId: UInt64Type,
225
+ cargoId: UInt64Type,
226
+ quantity: UInt64Type
227
+ ): Action {
228
+ return this.server.action('wrap', {
229
+ owner: Name.from(owner),
230
+ entity_type: entityType,
231
+ entity_id: UInt64.from(entityId),
232
+ cargo_id: UInt64.from(cargoId),
233
+ quantity: UInt64.from(quantity),
234
+ })
235
+ }
236
+
197
237
  joinGame(account: NameType, companyName: string): Action[] {
198
238
  return [this.foundCompany(account, companyName), this.join(account)]
199
239
  }
@@ -35,7 +35,7 @@ function synthesizeItem(id: number, source: RecipeSource): Item {
35
35
  name: source.name,
36
36
  description: source.description,
37
37
  mass: source.mass,
38
- category: 'metal',
38
+ category: 'ore',
39
39
  tier: 't1',
40
40
  color: source.color,
41
41
  })
@@ -25,44 +25,45 @@ function idiv(a: number, b: number): number {
25
25
  return Math.floor(a / b)
26
26
  }
27
27
 
28
- export function computeBaseHullmass(seed: bigint): number {
29
- const density = decodeStat(seed, 1)
28
+ export function computeBaseHullmass(stats: bigint): number {
29
+ const density = decodeStat(stats, 1)
30
30
  return 25000 + 75 * density
31
31
  }
32
32
 
33
- export function computeBaseCapacityShip(seed: bigint): number {
34
- const s = decodeStat(seed, 0) + decodeStat(seed, 2) + decodeStat(seed, 3)
33
+ export function computeBaseCapacityShip(stats: bigint): number {
34
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
35
35
  return Math.floor(1_000_000 * Math.pow(10, s / 2997))
36
36
  }
37
37
 
38
- export function computeBaseCapacityWarehouse(seed: bigint): number {
39
- const s = decodeStat(seed, 0) + decodeStat(seed, 2) + decodeStat(seed, 3)
38
+ export function computeBaseCapacityWarehouse(stats: bigint): number {
39
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
40
40
  return Math.floor(20_000_000 * Math.pow(10, s / 2997))
41
41
  }
42
42
 
43
43
  export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
44
44
  export const computeEngineDrain = (thm: number): number => Math.max(30, 50 - idiv(thm, 70))
45
45
  export const computeGeneratorCap = (res: number): number => 300 + idiv(res, 6)
46
- export const computeGeneratorRech = (clr: number): number => 5 + idiv(clr * 15, 1000)
46
+ export const computeGeneratorRech = (ref: number): number => 1 + idiv(ref * 3, 1000)
47
47
  export const computeGathererYield = (str: number): number => 200 + str
48
- export const computeGathererDrain = (con: number): number => Math.max(10, 50 - idiv(con, 20))
48
+ export const computeGathererDrain = (con: number): number =>
49
+ Math.max(250, 1250 - idiv(con * 25, 20))
49
50
  export const computeGathererDepth = (tol: number): number => 200 + idiv(tol * 3, 2)
50
51
  export const computeGathererSpeed = (ref: number): number => 100 + idiv(ref * 4, 5)
51
- export const computeLoaderMass = (duc: number): number => Math.max(200, 2000 - duc * 2)
52
+ export const computeLoaderMass = (fin: number): number => Math.max(200, 2000 - fin * 2)
52
53
  export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla, 500)
53
54
  export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
54
- export const computeCrafterDrain = (clr: number): number => Math.max(5, 30 - idiv(clr, 33))
55
+ export const computeCrafterDrain = (com: number): number => Math.max(5, 30 - idiv(com, 33))
55
56
 
56
57
  export function entityDisplayName(itemId: number): string {
57
58
  switch (itemId) {
58
59
  case ITEM_SHIP_T1_PACKED:
59
- return 'Ship T1'
60
+ return 'Ship'
60
61
  case ITEM_WAREHOUSE_T1_PACKED:
61
- return 'Warehouse T1'
62
+ return 'Warehouse'
62
63
  case ITEM_CONTAINER_T1_PACKED:
63
- return 'Container T1'
64
+ return 'Container'
64
65
  case ITEM_CONTAINER_T2_PACKED:
65
- return 'Container T2'
66
+ return 'Container'
66
67
  default:
67
68
  return 'Entity'
68
69
  }
@@ -71,23 +72,23 @@ export function entityDisplayName(itemId: number): string {
71
72
  export function moduleDisplayName(itemId: number): string {
72
73
  switch (itemId) {
73
74
  case ITEM_ENGINE_T1:
74
- return 'Engine T1'
75
+ return 'Engine'
75
76
  case ITEM_GENERATOR_T1:
76
- return 'Generator T1'
77
+ return 'Generator'
77
78
  case ITEM_GATHERER_T1:
78
- return 'Gatherer T1'
79
+ return 'Gatherer'
79
80
  case ITEM_LOADER_T1:
80
- return 'Loader T1'
81
+ return 'Loader'
81
82
  case ITEM_MANUFACTURING_T1:
82
- return 'Manufacturing T1'
83
+ return 'Manufacturing'
83
84
  case ITEM_STORAGE_T1:
84
- return 'Storage T1'
85
+ return 'Storage'
85
86
  default:
86
87
  return 'Module'
87
88
  }
88
89
  }
89
90
 
90
- export function formatModuleLine(slot: number, itemId: number, seed: bigint): string {
91
+ export function formatModuleLine(slot: number, itemId: number, stats: bigint): string {
91
92
  let out = `Slot ${slot} - `
92
93
  if (itemId === 0) {
93
94
  out += '(empty)'
@@ -99,44 +100,44 @@ export function formatModuleLine(slot: number, itemId: number, seed: bigint): st
99
100
 
100
101
  switch (subtype) {
101
102
  case MODULE_ENGINE: {
102
- const vol = decodeStat(seed, 0)
103
- const thm = decodeStat(seed, 1)
103
+ const vol = decodeStat(stats, 0)
104
+ const thm = decodeStat(stats, 1)
104
105
  out += ` Thrust ${computeEngineThrust(vol)} Drain ${computeEngineDrain(thm)}`
105
106
  break
106
107
  }
107
108
  case MODULE_GENERATOR: {
108
- const res = decodeStat(seed, 0)
109
- const clr = decodeStat(seed, 1)
110
- out += ` Capacity ${computeGeneratorCap(res)} Recharge ${computeGeneratorRech(clr)}`
109
+ const res = decodeStat(stats, 0)
110
+ const ref = decodeStat(stats, 1)
111
+ out += ` Capacity ${computeGeneratorCap(res)} Recharge ${computeGeneratorRech(ref)}`
111
112
  break
112
113
  }
113
114
  case MODULE_GATHERER: {
114
- const str = decodeStat(seed, 0)
115
- const tol = decodeStat(seed, 1)
116
- const con = decodeStat(seed, 3)
117
- const ref = decodeStat(seed, 4)
115
+ const str = decodeStat(stats, 0)
116
+ const tol = decodeStat(stats, 1)
117
+ const con = decodeStat(stats, 3)
118
+ const ref = decodeStat(stats, 4)
118
119
  out += ` Yield ${computeGathererYield(str)} Depth ${computeGathererDepth(
119
120
  tol
120
121
  )} Speed ${computeGathererSpeed(ref)} Drain ${computeGathererDrain(con)}`
121
122
  break
122
123
  }
123
124
  case MODULE_LOADER: {
124
- const duc = decodeStat(seed, 0)
125
- const pla = decodeStat(seed, 1)
126
- out += ` Mass ${computeLoaderMass(duc)} Thrust ${computeLoaderThrust(pla)}`
125
+ const fin = decodeStat(stats, 0)
126
+ const pla = decodeStat(stats, 1)
127
+ out += ` Mass ${computeLoaderMass(fin)} Thrust ${computeLoaderThrust(pla)}`
127
128
  break
128
129
  }
129
130
  case MODULE_CRAFTER: {
130
- const rea = decodeStat(seed, 0)
131
- const clr = decodeStat(seed, 1)
132
- out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(clr)}`
131
+ const rea = decodeStat(stats, 0)
132
+ const com = decodeStat(stats, 1)
133
+ out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(com)}`
133
134
  break
134
135
  }
135
136
  case MODULE_STORAGE: {
136
- const str = decodeStat(seed, 0)
137
- const duc = decodeStat(seed, 1)
138
- const pur = decodeStat(seed, 2)
139
- const sum = str + duc + pur
137
+ const str = decodeStat(stats, 0)
138
+ const fin = decodeStat(stats, 2)
139
+ const sat = decodeStat(stats, 3)
140
+ const sum = str + fin + sat
140
141
  const pct = 10 + idiv(sum * 10, 2997)
141
142
  out += ` +${pct}% capacity`
142
143
  break
@@ -147,16 +148,16 @@ export function formatModuleLine(slot: number, itemId: number, seed: bigint): st
147
148
 
148
149
  export function buildEntityDescription(
149
150
  itemId: number,
150
- hullSeed: bigint,
151
+ hullStats: bigint,
151
152
  moduleItems: number[],
152
- moduleSeeds: bigint[]
153
+ moduleStats: bigint[]
153
154
  ): string {
154
- const hullMass = computeBaseHullmass(hullSeed)
155
+ const hullMass = computeBaseHullmass(hullStats)
155
156
  let baseCapacity = 0
156
157
  if (itemId === ITEM_SHIP_T1_PACKED) {
157
- baseCapacity = computeBaseCapacityShip(hullSeed)
158
+ baseCapacity = computeBaseCapacityShip(hullStats)
158
159
  } else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
159
- baseCapacity = computeBaseCapacityWarehouse(hullSeed)
160
+ baseCapacity = computeBaseCapacityWarehouse(hullStats)
160
161
  }
161
162
 
162
163
  let out = entityDisplayName(itemId)
@@ -167,7 +168,7 @@ export function buildEntityDescription(
167
168
  out += '\n\n'
168
169
 
169
170
  for (let i = 0; i < moduleItems.length; i++) {
170
- out += formatModuleLine(i, moduleItems[i], moduleSeeds[i] ?? 0n)
171
+ out += formatModuleLine(i, moduleItems[i], moduleStats[i] ?? 0n)
171
172
  out += '\n'
172
173
  }
173
174
 
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  export interface NFTInstalledModule {
11
11
  item_id: number
12
- seed: string
12
+ stats: string
13
13
  }
14
14
 
15
15
  export interface NFTModuleSlot {
@@ -20,13 +20,13 @@ export interface NFTModuleSlot {
20
20
  export interface NFTCargoItem {
21
21
  item_id: number
22
22
  quantity: number
23
- seed: string
23
+ stats: string
24
24
  modules?: NFTModuleSlot[]
25
25
  }
26
26
 
27
27
  export interface NFTCommonBase {
28
28
  quantity: number
29
- seed: string
29
+ stats: string
30
30
  origin_x: string
31
31
  origin_y: string
32
32
  }
@@ -34,7 +34,7 @@ export interface NFTCommonBase {
34
34
  export function readCommonBase(data: Record<string, any>): NFTCommonBase {
35
35
  return {
36
36
  quantity: Number(data.quantity),
37
- seed: String(data.seed),
37
+ stats: String(data.stats),
38
38
  origin_x: String(data.origin_x),
39
39
  origin_y: String(data.origin_y),
40
40
  }
@@ -42,7 +42,7 @@ export function readCommonBase(data: Record<string, any>): NFTCommonBase {
42
42
 
43
43
  export function deserializeScalar(data: Record<string, any>, itemId: number): NFTCargoItem {
44
44
  const base = readCommonBase(data)
45
- return {item_id: itemId, quantity: base.quantity, seed: base.seed}
45
+ return {item_id: itemId, quantity: base.quantity, stats: base.stats}
46
46
  }
47
47
 
48
48
  export const deserializeResource = deserializeScalar
@@ -52,18 +52,18 @@ export const deserializeModule = deserializeScalar
52
52
  export function deserializeEntity(data: Record<string, any>, itemId: number): NFTCargoItem {
53
53
  const base = readCommonBase(data)
54
54
  const moduleItems: number[] = (data.module_items ?? []).map((v: any) => Number(v))
55
- const moduleSeeds: string[] = (data.module_seeds ?? []).map((v: any) => String(v))
55
+ const moduleStats: string[] = (data.module_stats ?? []).map((v: any) => String(v))
56
56
  const layout = getEntitySlotLayout(itemId)
57
57
 
58
58
  const modules: NFTModuleSlot[] = layout.map((slot, i) => ({
59
59
  type: slot.type,
60
60
  installed:
61
61
  moduleItems[i] && moduleItems[i] !== 0
62
- ? {item_id: moduleItems[i], seed: moduleSeeds[i]}
62
+ ? {item_id: moduleItems[i], stats: moduleStats[i]}
63
63
  : undefined,
64
64
  }))
65
65
 
66
- return {item_id: itemId, quantity: base.quantity, seed: base.seed, modules}
66
+ return {item_id: itemId, quantity: base.quantity, stats: base.stats, modules}
67
67
  }
68
68
 
69
69
  export function deserializeAsset(data: Record<string, any>, itemId: number): NFTCargoItem {
@@ -0,0 +1,165 @@
1
+ import type {ResolvedItem, ResolvedModuleSlot} from './resolve-item'
2
+
3
+ export interface TextSpan {
4
+ text: string
5
+ highlight?: boolean
6
+ }
7
+
8
+ export interface CapabilityInput {
9
+ capability: string
10
+ attributes: {label: string; value: number}[]
11
+ }
12
+
13
+ export interface ModuleDescription {
14
+ id: string
15
+ template: string
16
+ params: Readonly<Record<string, number | string>>
17
+ highlightKeys: readonly string[]
18
+ }
19
+
20
+ export interface RenderDescriptionOptions {
21
+ translate?: (id: string, fallback: string) => string
22
+ formatNumber?: (n: number) => string
23
+ }
24
+
25
+ interface TemplateSpec {
26
+ id: string
27
+ template: string
28
+ params: readonly [string, string][]
29
+ highlightKeys: readonly string[]
30
+ }
31
+
32
+ const TEMPLATES: Record<string, TemplateSpec> = {
33
+ engine: {
34
+ id: 'module.engine.description',
35
+ template:
36
+ 'generates {thrust} thrust for travel while draining {drain} energy per distance travelled',
37
+ params: [
38
+ ['thrust', 'Thrust'],
39
+ ['drain', 'Drain'],
40
+ ],
41
+ highlightKeys: ['thrust', 'drain'],
42
+ },
43
+ generator: {
44
+ id: 'module.generator.description',
45
+ template:
46
+ 'holds {capacity} maximum energy and restores {recharge} per second while recharging',
47
+ params: [
48
+ ['capacity', 'Capacity'],
49
+ ['recharge', 'Recharge'],
50
+ ],
51
+ highlightKeys: ['capacity', 'recharge'],
52
+ },
53
+ gatherer: {
54
+ id: 'module.gatherer.description',
55
+ template:
56
+ 'mines resources at {yield} speed to a max depth of {depth} with {speed} gather speed while draining {drain} energy per second',
57
+ params: [
58
+ ['yield', 'Yield'],
59
+ ['drain', 'Drain'],
60
+ ['depth', 'Depth'],
61
+ ['speed', 'Speed'],
62
+ ],
63
+ highlightKeys: ['yield', 'depth', 'speed', 'drain'],
64
+ },
65
+ loader: {
66
+ id: 'module.loader.description',
67
+ template:
68
+ '{quantity} loader that generates {thrust} thrust with a weight of {mass} per unit',
69
+ params: [
70
+ ['quantity', 'Quantity'],
71
+ ['thrust', 'Thrust'],
72
+ ['mass', 'Mass'],
73
+ ],
74
+ highlightKeys: ['quantity', 'thrust', 'mass'],
75
+ },
76
+ manufacturing: {
77
+ id: 'module.manufacturing.description',
78
+ template: 'manufactures items at {speed} speed while draining {drain} energy per second',
79
+ params: [
80
+ ['speed', 'Speed'],
81
+ ['drain', 'Drain'],
82
+ ],
83
+ highlightKeys: ['speed', 'drain'],
84
+ },
85
+ storage: {
86
+ id: 'module.storage.description',
87
+ template: 'boosts cargo capacity by {bonus}%',
88
+ params: [['bonus', 'Capacity Bonus']],
89
+ highlightKeys: ['bonus'],
90
+ },
91
+ hauler: {
92
+ id: 'module.hauler.description',
93
+ template:
94
+ 'locks onto up to {capacity} targets at {efficiency} efficiency while draining {drain} energy per distance travelled per target',
95
+ params: [
96
+ ['capacity', 'Capacity'],
97
+ ['efficiency', 'Efficiency'],
98
+ ['drain', 'Drain'],
99
+ ],
100
+ highlightKeys: ['capacity', 'efficiency', 'drain'],
101
+ },
102
+ }
103
+
104
+ export function describeModule(input: CapabilityInput): ModuleDescription | null {
105
+ if (!input.attributes || input.attributes.length === 0) return null
106
+ const key = input.capability.toLowerCase()
107
+ const spec = TEMPLATES[key]
108
+ if (!spec) return null
109
+ const params: Record<string, number | string> = {}
110
+ for (const [paramName, attrLabel] of spec.params) {
111
+ const attr = input.attributes.find((a) => a.label === attrLabel)
112
+ if (attr) params[paramName] = attr.value
113
+ }
114
+ return {
115
+ id: spec.id,
116
+ template: spec.template,
117
+ params,
118
+ highlightKeys: spec.highlightKeys,
119
+ }
120
+ }
121
+
122
+ export function describeModuleForItem(resolved: ResolvedItem): ModuleDescription | null {
123
+ if (resolved.itemType !== 'module') return null
124
+ const group = resolved.attributes?.[0]
125
+ if (!group) return null
126
+ return describeModule({capability: group.capability, attributes: group.attributes})
127
+ }
128
+
129
+ export function describeModuleForSlot(slot: ResolvedModuleSlot): ModuleDescription | null {
130
+ if (!slot.installed || !slot.name || !slot.attributes) return null
131
+ return describeModule({capability: slot.name, attributes: slot.attributes})
132
+ }
133
+
134
+ export function renderDescription(
135
+ desc: ModuleDescription,
136
+ options?: RenderDescriptionOptions
137
+ ): TextSpan[] {
138
+ const translate = options?.translate ?? ((_id: string, fallback: string) => fallback)
139
+ const formatNumber = options?.formatNumber ?? ((n: number) => n.toLocaleString('en-US'))
140
+ const tpl = translate(desc.id, desc.template)
141
+
142
+ const spans: TextSpan[] = []
143
+ const regex = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g
144
+ let lastIndex = 0
145
+ let m: RegExpExecArray | null
146
+ while ((m = regex.exec(tpl)) !== null) {
147
+ if (m.index > lastIndex) {
148
+ spans.push({text: tpl.slice(lastIndex, m.index)})
149
+ }
150
+ const paramName = m[1] ?? ''
151
+ const raw = desc.params[paramName]
152
+ if (raw === undefined) {
153
+ spans.push({text: `{${paramName}}`})
154
+ } else {
155
+ const formatted = typeof raw === 'number' ? formatNumber(raw) : raw
156
+ const highlight = (desc.highlightKeys as readonly string[]).includes(paramName)
157
+ spans.push(highlight ? {text: formatted, highlight: true} : {text: formatted})
158
+ }
159
+ lastIndex = m.index + m[0].length
160
+ }
161
+ if (lastIndex < tpl.length) {
162
+ spans.push({text: tpl.slice(lastIndex)})
163
+ }
164
+ return spans
165
+ }
@@ -0,0 +1,57 @@
1
+ import type {ResolvedItem} from './resolve-item'
2
+ import type {ResourceCategory} from '../types'
3
+ import {formatMass as defaultFormatMass} from '../format'
4
+
5
+ const TIER_ADJECTIVES: Record<number, string> = {
6
+ 1: 'Crude',
7
+ 2: 'Dense',
8
+ 3: 'Pure',
9
+ 4: 'Prime',
10
+ 5: 'Pristine',
11
+ 6: 'Radiant',
12
+ 7: 'Exotic',
13
+ 8: 'Mythic',
14
+ 9: 'Cosmic',
15
+ 10: 'Ascendant',
16
+ }
17
+
18
+ const CATEGORY_LABELS: Record<ResourceCategory, string> = {
19
+ ore: 'Ore',
20
+ crystal: 'Crystal',
21
+ gas: 'Gas',
22
+ regolith: 'Regolith',
23
+ biomass: 'Biomass',
24
+ }
25
+
26
+ function tierNumber(tier: string): number {
27
+ return Number(String(tier).replace(/^t/i, ''))
28
+ }
29
+
30
+ export function displayName(resolved: ResolvedItem): string {
31
+ if (resolved.itemType === 'resource') {
32
+ const tierNum = tierNumber(resolved.tier)
33
+ const adj = TIER_ADJECTIVES[tierNum] ?? 'Unknown'
34
+ const cat = resolved.category ? CATEGORY_LABELS[resolved.category] : 'Resource'
35
+ return `${adj} ${cat}`
36
+ }
37
+ return resolved.name
38
+ }
39
+
40
+ export interface DescribeOptions {
41
+ translate?: (key: string) => string
42
+ formatNumber?: (n: number) => string
43
+ formatMass?: (kg: number) => string
44
+ }
45
+
46
+ export function describeItem(resolved: ResolvedItem, opts?: DescribeOptions): string {
47
+ const massFmt = opts?.formatMass ?? defaultFormatMass
48
+ const mass = massFmt(resolved.mass)
49
+ const tier = `T${tierNumber(resolved.tier)}`
50
+ if (resolved.itemType === 'resource') {
51
+ const cat = resolved.category ? CATEGORY_LABELS[resolved.category] : 'Resource'
52
+ const header = `${tier} ${cat}`
53
+ const stats = resolved.stats?.map((s) => `${s.label} ${s.value}`).join(', ')
54
+ return [header, stats, mass].filter(Boolean).join(' · ')
55
+ }
56
+ return `${tier} ${resolved.name} · ${mass}`
57
+ }