@shipload/sdk 0.7.1 → 1.0.0-next.0

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 (94) hide show
  1. package/lib/shipload.d.ts +2730 -287
  2. package/lib/shipload.js +10862 -2229
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +10434 -2171
  5. package/lib/shipload.m.js.map +1 -1
  6. package/package.json +11 -20
  7. package/src/capabilities/crafting.ts +22 -0
  8. package/src/capabilities/gathering.ts +36 -0
  9. package/src/capabilities/guards.ts +38 -0
  10. package/src/capabilities/hauling.ts +22 -0
  11. package/src/capabilities/index.ts +8 -0
  12. package/src/capabilities/loading.ts +8 -0
  13. package/src/capabilities/modules.ts +86 -0
  14. package/src/capabilities/movement.ts +29 -0
  15. package/src/capabilities/storage.ts +159 -0
  16. package/src/contracts/server.ts +1389 -285
  17. package/src/data/capabilities.ts +408 -0
  18. package/src/data/catalog.ts +135 -0
  19. package/src/data/categories.ts +55 -0
  20. package/src/data/colors.ts +84 -0
  21. package/src/data/entities.json +50 -0
  22. package/src/data/item-ids.ts +75 -0
  23. package/src/data/items.json +252 -0
  24. package/src/data/locations.ts +53 -0
  25. package/src/data/metadata.ts +208 -0
  26. package/src/data/nebula-adjectives.json +211 -0
  27. package/src/data/nebula-nouns.json +151 -0
  28. package/src/data/recipes-runtime.ts +65 -0
  29. package/src/data/recipes.json +878 -0
  30. package/src/data/syllables.json +1790 -0
  31. package/src/data/tiers.ts +45 -0
  32. package/src/derivation/crafting.ts +350 -0
  33. package/src/derivation/index.ts +32 -0
  34. package/src/derivation/location-size.ts +15 -0
  35. package/src/derivation/resources.ts +112 -0
  36. package/src/derivation/stats.ts +146 -0
  37. package/src/derivation/strata.ts +43 -0
  38. package/src/derivation/stratum.ts +134 -0
  39. package/src/derivation/tiers.ts +54 -0
  40. package/src/entities/cargo-utils.ts +84 -0
  41. package/src/entities/container.ts +108 -0
  42. package/src/entities/entity-inventory.ts +39 -0
  43. package/src/entities/gamestate.ts +152 -0
  44. package/src/entities/inventory-accessor.ts +42 -0
  45. package/src/entities/location.ts +60 -0
  46. package/src/entities/makers.ts +196 -0
  47. package/src/entities/player.ts +15 -0
  48. package/src/entities/ship-deploy.ts +258 -0
  49. package/src/entities/ship.ts +204 -0
  50. package/src/entities/warehouse.ts +119 -0
  51. package/src/errors.ts +100 -9
  52. package/src/format.ts +12 -0
  53. package/src/index-module.ts +317 -7
  54. package/src/managers/actions.ts +250 -0
  55. package/src/managers/base.ts +25 -0
  56. package/src/managers/context.ts +114 -0
  57. package/src/managers/entities.ts +103 -0
  58. package/src/managers/epochs.ts +47 -0
  59. package/src/managers/index.ts +9 -0
  60. package/src/managers/locations.ts +68 -0
  61. package/src/managers/players.ts +13 -0
  62. package/src/nft/description.ts +176 -0
  63. package/src/nft/deserializers.ts +83 -0
  64. package/src/nft/index.ts +2 -0
  65. package/src/resolution/describe-module.ts +166 -0
  66. package/src/resolution/display-name.ts +39 -0
  67. package/src/resolution/resolve-item.ts +358 -0
  68. package/src/scheduling/accessor.ts +82 -0
  69. package/src/{epoch.ts → scheduling/epoch.ts} +1 -1
  70. package/src/scheduling/projection.ts +463 -0
  71. package/src/scheduling/schedule.ts +179 -0
  72. package/src/shipload.ts +47 -160
  73. package/src/subscriptions/connection.ts +154 -0
  74. package/src/subscriptions/debug.ts +17 -0
  75. package/src/subscriptions/index.ts +5 -0
  76. package/src/subscriptions/manager.ts +240 -0
  77. package/src/subscriptions/mappers.ts +28 -0
  78. package/src/subscriptions/types.ts +143 -0
  79. package/src/travel/travel.ts +500 -0
  80. package/src/types/capabilities.ts +76 -0
  81. package/src/types/entity-traits.ts +69 -0
  82. package/src/types/entity.ts +39 -0
  83. package/src/types/index.ts +3 -0
  84. package/src/types.ts +140 -35
  85. package/src/{hash.ts → utils/hash.ts} +2 -2
  86. package/src/utils/system.ts +168 -0
  87. package/src/goods.ts +0 -124
  88. package/src/market.ts +0 -214
  89. package/src/rolls.ts +0 -8
  90. package/src/ship.ts +0 -36
  91. package/src/state.ts +0 -0
  92. package/src/syllables.ts +0 -1184
  93. package/src/system.ts +0 -37
  94. package/src/travel.ts +0 -259
@@ -0,0 +1,103 @@
1
+ import {Name, type NameType, type UInt64Type} from '@wharfkit/antelope'
2
+ import {BaseManager} from './base'
3
+ import {Ship} from '../entities/ship'
4
+ import {Warehouse} from '../entities/warehouse'
5
+ import {Container} from '../entities/container'
6
+ import type {ServerContract} from '../contracts'
7
+
8
+ export type EntityType = 'ship' | 'warehouse' | 'container' | 'location'
9
+
10
+ export class EntitiesManager extends BaseManager {
11
+ async getEntity(type: EntityType, id: UInt64Type): Promise<Ship | Warehouse | Container> {
12
+ const result = await this.server.readonly('getentity', {
13
+ entity_type: Name.from(type),
14
+ entity_id: id,
15
+ })
16
+ const entityInfo = result as ServerContract.Types.entity_info
17
+ return this.wrapEntity(entityInfo)
18
+ }
19
+
20
+ async getEntities(
21
+ owner: NameType | ServerContract.Types.player_row,
22
+ type?: EntityType
23
+ ): Promise<(Ship | Warehouse | Container)[]> {
24
+ const ownerName = this.resolveOwner(owner)
25
+ const result = await this.server.readonly('getentities', {
26
+ owner: ownerName,
27
+ entity_type: type ? Name.from(type) : null,
28
+ })
29
+ const entities = result as ServerContract.Types.entity_info[]
30
+ return entities.map((entity) => this.wrapEntity(entity))
31
+ }
32
+
33
+ async getSummaries(
34
+ owner: NameType | ServerContract.Types.player_row,
35
+ type?: EntityType
36
+ ): Promise<ServerContract.Types.entity_summary[]> {
37
+ const ownerName = this.resolveOwner(owner)
38
+ const result = await this.server.readonly('getsummaries', {
39
+ owner: ownerName,
40
+ entity_type: type ? Name.from(type) : null,
41
+ })
42
+ return result as ServerContract.Types.entity_summary[]
43
+ }
44
+
45
+ async getShip(id: UInt64Type): Promise<Ship> {
46
+ return (await this.getEntity('ship', id)) as Ship
47
+ }
48
+
49
+ async getWarehouse(id: UInt64Type): Promise<Warehouse> {
50
+ return (await this.getEntity('warehouse', id)) as Warehouse
51
+ }
52
+
53
+ async getContainer(id: UInt64Type): Promise<Container> {
54
+ return (await this.getEntity('container', id)) as Container
55
+ }
56
+
57
+ async getShips(owner: NameType | ServerContract.Types.player_row): Promise<Ship[]> {
58
+ return (await this.getEntities(owner, 'ship')) as Ship[]
59
+ }
60
+
61
+ async getWarehouses(owner: NameType | ServerContract.Types.player_row): Promise<Warehouse[]> {
62
+ return (await this.getEntities(owner, 'warehouse')) as Warehouse[]
63
+ }
64
+
65
+ async getContainers(owner: NameType | ServerContract.Types.player_row): Promise<Container[]> {
66
+ return (await this.getEntities(owner, 'container')) as Container[]
67
+ }
68
+
69
+ async getShipSummaries(
70
+ owner: NameType | ServerContract.Types.player_row
71
+ ): Promise<ServerContract.Types.entity_summary[]> {
72
+ return this.getSummaries(owner, 'ship')
73
+ }
74
+
75
+ async getWarehouseSummaries(
76
+ owner: NameType | ServerContract.Types.player_row
77
+ ): Promise<ServerContract.Types.entity_summary[]> {
78
+ return this.getSummaries(owner, 'warehouse')
79
+ }
80
+
81
+ async getContainerSummaries(
82
+ owner: NameType | ServerContract.Types.player_row
83
+ ): Promise<ServerContract.Types.entity_summary[]> {
84
+ return this.getSummaries(owner, 'container')
85
+ }
86
+
87
+ private wrapEntity(entity: ServerContract.Types.entity_info): Ship | Warehouse | Container {
88
+ if (entity.type.equals('ship')) {
89
+ return new Ship(entity)
90
+ } else if (entity.type.equals('warehouse')) {
91
+ return new Warehouse(entity)
92
+ } else {
93
+ return new Container(entity)
94
+ }
95
+ }
96
+
97
+ private resolveOwner(owner: NameType | ServerContract.Types.player_row): Name {
98
+ if (typeof owner === 'object' && owner !== null && 'owner' in owner) {
99
+ return owner.owner
100
+ }
101
+ return Name.from(owner)
102
+ }
103
+ }
@@ -0,0 +1,47 @@
1
+ import {UInt64, type UInt64Type} from '@wharfkit/antelope'
2
+ import {BaseManager} from './base'
3
+ import {type EpochInfo, getCurrentEpoch, getEpochInfo} from '../scheduling/epoch'
4
+
5
+ export class EpochsManager extends BaseManager {
6
+ async getCurrentHeight(): Promise<UInt64> {
7
+ const game = await this.getGame()
8
+ return getCurrentEpoch(game)
9
+ }
10
+
11
+ async getCurrent(): Promise<EpochInfo> {
12
+ const game = await this.getGame()
13
+ const epoch = await this.getCurrentHeight()
14
+ return getEpochInfo(game, epoch)
15
+ }
16
+
17
+ async getByHeight(height: UInt64Type): Promise<EpochInfo> {
18
+ const game = await this.getGame()
19
+ return getEpochInfo(game, UInt64.from(height))
20
+ }
21
+
22
+ async getTimeRemaining(): Promise<number> {
23
+ const epochInfo = await this.getCurrent()
24
+ const now = Date.now()
25
+ const endTime = epochInfo.end.getTime()
26
+ return Math.max(0, endTime - now)
27
+ }
28
+
29
+ async getProgress(): Promise<number> {
30
+ const epochInfo = await this.getCurrent()
31
+ const now = Date.now()
32
+ const startTime = epochInfo.start.getTime()
33
+ const endTime = epochInfo.end.getTime()
34
+ const duration = endTime - startTime
35
+ const elapsed = now - startTime
36
+
37
+ if (elapsed <= 0) return 0
38
+ if (elapsed >= duration) return 1
39
+
40
+ return elapsed / duration
41
+ }
42
+
43
+ async fitsInCurrentEpoch(durationMs: number): Promise<boolean> {
44
+ const remaining = await this.getTimeRemaining()
45
+ return durationMs <= remaining
46
+ }
47
+ }
@@ -0,0 +1,9 @@
1
+ export {GameContext} from './context'
2
+ export {BaseManager} from './base'
3
+ export {EntitiesManager} from './entities'
4
+ export type {EntityType} from './entities'
5
+ export {PlayersManager} from './players'
6
+ export {LocationsManager} from './locations'
7
+ export type {LocationStratum} from './locations'
8
+ export {EpochsManager} from './epochs'
9
+ export {ActionsManager} from './actions'
@@ -0,0 +1,68 @@
1
+ import {type UInt16Type, UInt64, type UInt64Type} from '@wharfkit/antelope'
2
+ import {BaseManager} from './base'
3
+ import {type CoordinatesType, coordsToLocationId, type Distance} from '../types'
4
+ import {hasSystem} from '../utils/system'
5
+ import {findNearbyPlanets} from '../travel/travel'
6
+ import type {ServerContract} from '../contracts'
7
+ import {type DerivedStratum, deriveStrata} from '../derivation'
8
+
9
+ export interface LocationStratum extends DerivedStratum {
10
+ reserveMax: number
11
+ }
12
+
13
+ export class LocationsManager extends BaseManager {
14
+ async hasSystem(location: CoordinatesType): Promise<boolean> {
15
+ const game = await this.getGame()
16
+ return hasSystem(game.config.seed, location)
17
+ }
18
+
19
+ async findNearbyPlanets(
20
+ origin: CoordinatesType,
21
+ maxDistance: UInt16Type = 20
22
+ ): Promise<Distance[]> {
23
+ const game = await this.getGame()
24
+ return findNearbyPlanets(game.config.seed, origin, maxDistance)
25
+ }
26
+
27
+ async getStrata(coords: CoordinatesType): Promise<LocationStratum[]> {
28
+ const game = await this.getGame()
29
+ const state = await this.getState()
30
+
31
+ const derived = deriveStrata(coords, game.config.seed, state.epochSeed)
32
+ if (derived.length === 0) return []
33
+
34
+ const overrides = (await this.server.readonly('getreserves', {
35
+ x: coords.x,
36
+ y: coords.y,
37
+ })) as ServerContract.Types.stratum_remaining[]
38
+
39
+ const overrideMap = new Map<number, number>()
40
+ for (const o of overrides) {
41
+ overrideMap.set(Number(o.stratum), Number(o.remaining))
42
+ }
43
+
44
+ return derived.map((s) => ({
45
+ ...s,
46
+ reserveMax: s.reserve,
47
+ reserve: overrideMap.get(s.index) ?? s.reserve,
48
+ }))
49
+ }
50
+
51
+ async getLocationEntity(
52
+ id: UInt64Type
53
+ ): Promise<ServerContract.Types.location_row | undefined> {
54
+ const row = await this.server.table('location').get(UInt64.from(id))
55
+ return row ?? undefined
56
+ }
57
+
58
+ async getLocationEntityAt(
59
+ coords: CoordinatesType
60
+ ): Promise<ServerContract.Types.location_row | undefined> {
61
+ const id = coordsToLocationId(coords)
62
+ return this.getLocationEntity(id)
63
+ }
64
+
65
+ async getAllLocationEntities(): Promise<ServerContract.Types.location_row[]> {
66
+ return this.server.table('location').all()
67
+ }
68
+ }
@@ -0,0 +1,13 @@
1
+ import {Name, type NameType} from '@wharfkit/antelope'
2
+ import {BaseManager} from './base'
3
+ import {Player} from '../entities/player'
4
+
5
+ export class PlayersManager extends BaseManager {
6
+ async getPlayer(account: NameType): Promise<Player | undefined> {
7
+ const playerRow = await this.server.table('player').get(Name.from(account))
8
+ if (!playerRow) {
9
+ return undefined
10
+ }
11
+ return new Player(playerRow)
12
+ }
13
+ }
@@ -0,0 +1,176 @@
1
+ import {
2
+ getModuleCapabilityType,
3
+ MODULE_CRAFTER,
4
+ MODULE_ENGINE,
5
+ MODULE_GATHERER,
6
+ MODULE_GENERATOR,
7
+ MODULE_LOADER,
8
+ MODULE_STORAGE,
9
+ } from '../capabilities/modules'
10
+ import {
11
+ ITEM_CONTAINER_T1_PACKED,
12
+ ITEM_CONTAINER_T2_PACKED,
13
+ ITEM_CRAFTER_T1,
14
+ ITEM_ENGINE_T1,
15
+ ITEM_GATHERER_T1,
16
+ ITEM_GENERATOR_T1,
17
+ ITEM_LOADER_T1,
18
+ ITEM_SHIP_T1_PACKED,
19
+ ITEM_STORAGE_T1,
20
+ ITEM_WAREHOUSE_T1_PACKED,
21
+ } from '../data/item-ids'
22
+ import {decodeStat} from '../derivation/crafting'
23
+
24
+ function idiv(a: number, b: number): number {
25
+ return Math.floor(a / b)
26
+ }
27
+
28
+ export function computeBaseHullmass(stats: bigint): number {
29
+ const density = decodeStat(stats, 1)
30
+ return 25000 + 75 * density
31
+ }
32
+
33
+ export function computeBaseCapacityShip(stats: bigint): number {
34
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
35
+ return Math.floor(1_000_000 * 10 ** (s / 2997))
36
+ }
37
+
38
+ export function computeBaseCapacityWarehouse(stats: bigint): number {
39
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
40
+ return Math.floor(20_000_000 * 10 ** (s / 2997))
41
+ }
42
+
43
+ export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
44
+ export const computeEngineDrain = (thm: number): number => Math.max(30, 50 - idiv(thm, 70))
45
+ export const computeGeneratorCap = (res: number): number => 300 + idiv(res, 6)
46
+ export const computeGeneratorRech = (ref: number): number => 1 + idiv(ref * 3, 1000)
47
+ export const computeGathererYield = (str: number): number => 200 + str
48
+ export const computeGathererDrain = (con: number): number =>
49
+ Math.max(250, 1250 - idiv(con * 25, 20))
50
+ export const computeGathererDepth = (tol: number): number => 200 + idiv(tol * 3, 2)
51
+ export const computeGathererSpeed = (ref: number): number => 100 + idiv(ref * 4, 5)
52
+ export const computeLoaderMass = (fin: number): number => Math.max(200, 2000 - fin * 2)
53
+ export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla, 500)
54
+ export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
55
+ export const computeCrafterDrain = (com: number): number => Math.max(5, 30 - idiv(com, 33))
56
+
57
+ export function entityDisplayName(itemId: number): string {
58
+ switch (itemId) {
59
+ case ITEM_SHIP_T1_PACKED:
60
+ return 'Ship'
61
+ case ITEM_WAREHOUSE_T1_PACKED:
62
+ return 'Warehouse'
63
+ case ITEM_CONTAINER_T1_PACKED:
64
+ return 'Container'
65
+ case ITEM_CONTAINER_T2_PACKED:
66
+ return 'Container'
67
+ default:
68
+ return 'Entity'
69
+ }
70
+ }
71
+
72
+ export function moduleDisplayName(itemId: number): string {
73
+ switch (itemId) {
74
+ case ITEM_ENGINE_T1:
75
+ return 'Engine'
76
+ case ITEM_GENERATOR_T1:
77
+ return 'Generator'
78
+ case ITEM_GATHERER_T1:
79
+ return 'Gatherer'
80
+ case ITEM_LOADER_T1:
81
+ return 'Loader'
82
+ case ITEM_CRAFTER_T1:
83
+ return 'Crafter'
84
+ case ITEM_STORAGE_T1:
85
+ return 'Storage'
86
+ default:
87
+ return 'Module'
88
+ }
89
+ }
90
+
91
+ export function formatModuleLine(slot: number, itemId: number, stats: bigint): string {
92
+ let out = `Slot ${slot} - `
93
+ if (itemId === 0) {
94
+ out += '(empty)'
95
+ return out
96
+ }
97
+
98
+ out += moduleDisplayName(itemId)
99
+ const subtype = getModuleCapabilityType(itemId)
100
+
101
+ switch (subtype) {
102
+ case MODULE_ENGINE: {
103
+ const vol = decodeStat(stats, 0)
104
+ const thm = decodeStat(stats, 1)
105
+ out += ` Thrust ${computeEngineThrust(vol)} Drain ${computeEngineDrain(thm)}`
106
+ break
107
+ }
108
+ case MODULE_GENERATOR: {
109
+ const res = decodeStat(stats, 0)
110
+ const ref = decodeStat(stats, 1)
111
+ out += ` Capacity ${computeGeneratorCap(res)} Recharge ${computeGeneratorRech(ref)}`
112
+ break
113
+ }
114
+ case MODULE_GATHERER: {
115
+ const str = decodeStat(stats, 0)
116
+ const tol = decodeStat(stats, 1)
117
+ const con = decodeStat(stats, 3)
118
+ const ref = decodeStat(stats, 4)
119
+ out += ` Yield ${computeGathererYield(str)} Depth ${computeGathererDepth(
120
+ tol
121
+ )} Speed ${computeGathererSpeed(ref)} Drain ${computeGathererDrain(con)}`
122
+ break
123
+ }
124
+ case MODULE_LOADER: {
125
+ const fin = decodeStat(stats, 0)
126
+ const pla = decodeStat(stats, 1)
127
+ out += ` Mass ${computeLoaderMass(fin)} Thrust ${computeLoaderThrust(pla)}`
128
+ break
129
+ }
130
+ case MODULE_CRAFTER: {
131
+ const rea = decodeStat(stats, 0)
132
+ const com = decodeStat(stats, 1)
133
+ out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(com)}`
134
+ break
135
+ }
136
+ case MODULE_STORAGE: {
137
+ const str = decodeStat(stats, 0)
138
+ const fin = decodeStat(stats, 2)
139
+ const sat = decodeStat(stats, 3)
140
+ const sum = str + fin + sat
141
+ const pct = 10 + idiv(sum * 10, 2997)
142
+ out += ` +${pct}% capacity`
143
+ break
144
+ }
145
+ }
146
+ return out
147
+ }
148
+
149
+ export function buildEntityDescription(
150
+ itemId: number,
151
+ hullStats: bigint,
152
+ moduleItems: number[],
153
+ moduleStats: bigint[]
154
+ ): string {
155
+ const hullMass = computeBaseHullmass(hullStats)
156
+ let baseCapacity = 0
157
+ if (itemId === ITEM_SHIP_T1_PACKED) {
158
+ baseCapacity = computeBaseCapacityShip(hullStats)
159
+ } else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
160
+ baseCapacity = computeBaseCapacityWarehouse(hullStats)
161
+ }
162
+
163
+ let out = entityDisplayName(itemId)
164
+ out += ` - Hull ${hullMass} mass`
165
+ if (baseCapacity > 0) {
166
+ out += ` * ${baseCapacity} capacity`
167
+ }
168
+ out += '\n\n'
169
+
170
+ for (let i = 0; i < moduleItems.length; i++) {
171
+ out += formatModuleLine(i, moduleItems[i], moduleStats[i] ?? 0n)
172
+ out += '\n'
173
+ }
174
+
175
+ return out
176
+ }
@@ -0,0 +1,83 @@
1
+ import {getEntityLayout} from '../data/recipes-runtime'
2
+ import {moduleSlotTypeToCode} from '../capabilities/modules'
3
+ import {
4
+ ITEM_TYPE_COMPONENT,
5
+ ITEM_TYPE_ENTITY,
6
+ ITEM_TYPE_MODULE,
7
+ ITEM_TYPE_RESOURCE,
8
+ itemTypeCode,
9
+ } from '../data/tiers'
10
+
11
+ export interface NFTInstalledModule {
12
+ item_id: number
13
+ stats: string
14
+ }
15
+
16
+ export interface NFTModuleSlot {
17
+ type: number
18
+ installed?: NFTInstalledModule
19
+ }
20
+
21
+ export interface NFTCargoItem {
22
+ item_id: number
23
+ quantity: number
24
+ stats: string
25
+ modules?: NFTModuleSlot[]
26
+ }
27
+
28
+ export interface NFTCommonBase {
29
+ quantity: number
30
+ stats: string
31
+ origin_x: string
32
+ origin_y: string
33
+ }
34
+
35
+ export function readCommonBase(data: Record<string, any>): NFTCommonBase {
36
+ return {
37
+ quantity: Number(data.quantity),
38
+ stats: String(data.stats),
39
+ origin_x: String(data.origin_x),
40
+ origin_y: String(data.origin_y),
41
+ }
42
+ }
43
+
44
+ export function deserializeScalar(data: Record<string, any>, itemId: number): NFTCargoItem {
45
+ const base = readCommonBase(data)
46
+ return {item_id: itemId, quantity: base.quantity, stats: base.stats}
47
+ }
48
+
49
+ export const deserializeResource = deserializeScalar
50
+ export const deserializeComponent = deserializeScalar
51
+ export const deserializeModule = deserializeScalar
52
+
53
+ export function deserializeEntity(data: Record<string, any>, itemId: number): NFTCargoItem {
54
+ const base = readCommonBase(data)
55
+ const moduleItems: number[] = (data.module_items ?? []).map((v: any) => Number(v))
56
+ const moduleStats: string[] = (data.module_stats ?? []).map((v: any) => String(v))
57
+ const layout = getEntityLayout(itemId)
58
+ const slots = layout?.slots ?? []
59
+
60
+ const modules: NFTModuleSlot[] = slots.map((slot, i) => ({
61
+ type: moduleSlotTypeToCode(slot.type),
62
+ installed:
63
+ moduleItems[i] && moduleItems[i] !== 0
64
+ ? {item_id: moduleItems[i], stats: moduleStats[i]}
65
+ : undefined,
66
+ }))
67
+
68
+ return {item_id: itemId, quantity: base.quantity, stats: base.stats, modules}
69
+ }
70
+
71
+ export function deserializeAsset(data: Record<string, any>, itemId: number): NFTCargoItem {
72
+ const type = itemTypeCode(itemId)
73
+ switch (type) {
74
+ case ITEM_TYPE_RESOURCE:
75
+ case ITEM_TYPE_COMPONENT:
76
+ case ITEM_TYPE_MODULE:
77
+ return deserializeScalar(data, itemId)
78
+ case ITEM_TYPE_ENTITY:
79
+ return deserializeEntity(data, itemId)
80
+ default:
81
+ throw new Error(`unknown item type ${type} for item ${itemId}`)
82
+ }
83
+ }
@@ -0,0 +1,2 @@
1
+ export * from './deserializers'
2
+ export * from './description'
@@ -0,0 +1,166 @@
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
+ crafter: {
77
+ id: 'module.crafter.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
+ while (true) {
146
+ const m = regex.exec(tpl)
147
+ if (m === null) break
148
+ if (m.index > lastIndex) {
149
+ spans.push({text: tpl.slice(lastIndex, m.index)})
150
+ }
151
+ const paramName = m[1] ?? ''
152
+ const raw = desc.params[paramName]
153
+ if (raw === undefined) {
154
+ spans.push({text: `{${paramName}}`})
155
+ } else {
156
+ const formatted = typeof raw === 'number' ? formatNumber(raw) : raw
157
+ const highlight = (desc.highlightKeys as readonly string[]).includes(paramName)
158
+ spans.push(highlight ? {text: formatted, highlight: true} : {text: formatted})
159
+ }
160
+ lastIndex = m.index + m[0].length
161
+ }
162
+ if (lastIndex < tpl.length) {
163
+ spans.push({text: tpl.slice(lastIndex)})
164
+ }
165
+ return spans
166
+ }