@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.
- package/README.md +1 -349
- package/lib/shipload.d.ts +1729 -1127
- package/lib/shipload.js +7944 -3165
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +7487 -2840
- package/lib/shipload.m.js.map +1 -1
- package/package.json +6 -4
- package/src/capabilities/crafting.ts +22 -0
- package/src/capabilities/gathering.ts +36 -0
- package/src/capabilities/guards.ts +3 -8
- package/src/capabilities/hauling.ts +22 -0
- package/src/capabilities/index.ts +4 -1
- package/src/capabilities/modules.ts +86 -0
- package/src/capabilities/storage.ts +101 -9
- package/src/contracts/server.ts +785 -293
- package/src/data/capabilities.ts +408 -0
- package/src/data/categories.ts +55 -0
- package/src/data/colors.ts +71 -0
- package/src/data/entities.json +50 -0
- package/src/data/item-ids.ts +75 -0
- package/src/data/items.json +252 -0
- package/src/data/locations.ts +53 -0
- package/src/data/metadata.ts +208 -0
- package/src/data/nebula-adjectives.json +211 -0
- package/src/data/nebula-nouns.json +151 -0
- package/src/data/recipes-runtime.ts +65 -0
- package/src/data/recipes.json +878 -0
- package/src/data/syllables.json +1386 -780
- package/src/data/tiers.ts +45 -0
- package/src/derivation/crafting.ts +348 -0
- package/src/derivation/index.ts +30 -0
- package/src/derivation/location-size.ts +15 -0
- package/src/derivation/resources.ts +112 -0
- package/src/derivation/stats.ts +146 -0
- package/src/derivation/stratum.ts +134 -0
- package/src/derivation/tiers.ts +54 -0
- package/src/entities/cargo-utils.ts +10 -68
- package/src/entities/container.ts +37 -0
- package/src/entities/entity-inventory.ts +13 -13
- package/src/entities/inventory-accessor.ts +2 -6
- package/src/entities/location.ts +5 -200
- package/src/entities/makers.ts +144 -17
- package/src/entities/player.ts +1 -274
- package/src/entities/ship-deploy.ts +258 -0
- package/src/entities/ship.ts +28 -34
- package/src/entities/warehouse.ts +35 -7
- package/src/errors.ts +59 -5
- package/src/format.ts +12 -0
- package/src/index-module.ts +188 -50
- package/src/managers/actions.ts +138 -88
- package/src/managers/context.ts +19 -9
- package/src/managers/index.ts +0 -1
- package/src/managers/locations.ts +2 -85
- package/src/market/items.ts +41 -0
- package/src/nft/description.ts +176 -0
- package/src/nft/deserializers.ts +83 -0
- package/src/nft/index.ts +2 -0
- package/src/resolution/describe-module.ts +165 -0
- package/src/resolution/display-name.ts +43 -0
- package/src/resolution/resolve-item.ts +358 -0
- package/src/scheduling/projection.ts +200 -67
- package/src/scheduling/schedule.ts +2 -2
- package/src/shipload.ts +10 -5
- package/src/subscriptions/connection.ts +154 -0
- package/src/subscriptions/debug.ts +17 -0
- package/src/subscriptions/index.ts +5 -0
- package/src/subscriptions/manager.ts +240 -0
- package/src/subscriptions/mappers.ts +28 -0
- package/src/subscriptions/types.ts +143 -0
- package/src/travel/travel.ts +37 -23
- package/src/types/capabilities.ts +11 -14
- package/src/types/entity-traits.ts +3 -4
- package/src/types/entity.ts +9 -6
- package/src/types.ts +72 -72
- package/src/utils/system.ts +66 -53
- package/src/capabilities/extraction.ts +0 -37
- package/src/data/goods.json +0 -23
- package/src/managers/trades.ts +0 -119
- package/src/market/goods.ts +0 -31
- package/src/market/market.ts +0 -208
- package/src/market/rolls.ts +0 -8
- package/src/trading/collect.ts +0 -938
- package/src/trading/deal.ts +0 -207
- package/src/trading/trade.ts +0 -203
|
@@ -1,55 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {UInt16Type, UInt64, UInt64Type} from '@wharfkit/antelope'
|
|
2
2
|
import {BaseManager} from './base'
|
|
3
|
-
import {CoordinatesType, coordsToLocationId, Distance
|
|
4
|
-
import {marketPrice, marketPrices} from '../market/market'
|
|
3
|
+
import {CoordinatesType, coordsToLocationId, Distance} from '../types'
|
|
5
4
|
import {hasSystem} from '../utils/system'
|
|
6
5
|
import {findNearbyPlanets} from '../travel/travel'
|
|
7
|
-
import {Location, toLocation} from '../entities/location'
|
|
8
6
|
import {ServerContract} from '../contracts'
|
|
9
7
|
|
|
10
8
|
export class LocationsManager extends BaseManager {
|
|
11
|
-
async getMarketPrice(location: CoordinatesType, goodId: number): Promise<GoodPrice> {
|
|
12
|
-
const game = await this.getGame()
|
|
13
|
-
const state = await this.getState()
|
|
14
|
-
return marketPrice(location, goodId, game.config.seed, state)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async getMarketPrices(location: CoordinatesType): Promise<GoodPrice[]> {
|
|
18
|
-
const game = await this.getGame()
|
|
19
|
-
const state = await this.getState()
|
|
20
|
-
return marketPrices(location, game.config.seed, state)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async getMarketPricesWithSupply(location: CoordinatesType): Promise<GoodPrice[]> {
|
|
24
|
-
const [game, state, supplyRows] = await Promise.all([
|
|
25
|
-
this.getGame(),
|
|
26
|
-
this.getState(),
|
|
27
|
-
this.getSupplyRows(location),
|
|
28
|
-
])
|
|
29
|
-
|
|
30
|
-
const prices = marketPrices(location, game.config.seed, state)
|
|
31
|
-
|
|
32
|
-
const supplyMap = new Map<number, number>()
|
|
33
|
-
for (const row of supplyRows) {
|
|
34
|
-
if (UInt64.from(row.epoch).equals(state.epoch)) {
|
|
35
|
-
supplyMap.set(Number(row.good_id), Number(row.supply))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return prices.map((price) => {
|
|
40
|
-
const actualSupply = supplyMap.get(Number(price.id))
|
|
41
|
-
if (actualSupply !== undefined) {
|
|
42
|
-
return GoodPrice.from({
|
|
43
|
-
id: price.id,
|
|
44
|
-
good: price.good,
|
|
45
|
-
price: price.price,
|
|
46
|
-
supply: UInt64.from(actualSupply),
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
return price
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
9
|
async hasSystem(location: CoordinatesType): Promise<boolean> {
|
|
54
10
|
const game = await this.getGame()
|
|
55
11
|
return hasSystem(game.config.seed, location)
|
|
@@ -63,45 +19,6 @@ export class LocationsManager extends BaseManager {
|
|
|
63
19
|
return findNearbyPlanets(game.config.seed, origin, maxDistance)
|
|
64
20
|
}
|
|
65
21
|
|
|
66
|
-
async getSupplyRows(location: CoordinatesType) {
|
|
67
|
-
const hash = Checksum256.hash(Bytes.from(`${location.x}-${location.y}`, 'utf8'))
|
|
68
|
-
return this.server.table('supply').all({
|
|
69
|
-
index_position: 'secondary',
|
|
70
|
-
from: hash,
|
|
71
|
-
to: hash,
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async getLocationWithPrices(coords: CoordinatesType): Promise<Location> {
|
|
76
|
-
const location = toLocation(coords)
|
|
77
|
-
const prices = await this.getMarketPrices(location.coordinates)
|
|
78
|
-
location.setMarketPrices(prices)
|
|
79
|
-
return location
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async getLocationWithSupply(coords: CoordinatesType): Promise<Location> {
|
|
83
|
-
const location = toLocation(coords)
|
|
84
|
-
const [rows, state] = await Promise.all([
|
|
85
|
-
this.getSupplyRows(location.coordinates),
|
|
86
|
-
this.getState(),
|
|
87
|
-
])
|
|
88
|
-
location.setLocationRows(rows, state.epoch)
|
|
89
|
-
return location
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async getLocationComplete(coords: CoordinatesType): Promise<Location> {
|
|
93
|
-
const location = toLocation(coords)
|
|
94
|
-
const [prices, rows, state] = await Promise.all([
|
|
95
|
-
this.getMarketPrices(location.coordinates),
|
|
96
|
-
this.getSupplyRows(location.coordinates),
|
|
97
|
-
this.getState(),
|
|
98
|
-
])
|
|
99
|
-
|
|
100
|
-
location.setMarketPrices(prices)
|
|
101
|
-
location.setLocationRows(rows, state.epoch)
|
|
102
|
-
return location
|
|
103
|
-
}
|
|
104
|
-
|
|
105
22
|
async getLocationEntity(
|
|
106
23
|
id: UInt64Type
|
|
107
24
|
): Promise<ServerContract.Types.location_row | undefined> {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {UInt16, UInt16Type} from '@wharfkit/antelope'
|
|
2
|
+
import items from '../data/items.json'
|
|
3
|
+
import {itemMetadata} from '../data/metadata'
|
|
4
|
+
import {Item} from '../types'
|
|
5
|
+
|
|
6
|
+
const itemsById = new Map<number, Item>()
|
|
7
|
+
|
|
8
|
+
for (const raw of items as any[]) {
|
|
9
|
+
const meta = itemMetadata[raw.id]
|
|
10
|
+
if (!meta) {
|
|
11
|
+
throw new Error(`Missing metadata for item ${raw.id}. Add an entry to metadata.ts.`)
|
|
12
|
+
}
|
|
13
|
+
itemsById.set(raw.id, {
|
|
14
|
+
id: raw.id,
|
|
15
|
+
name: meta.name,
|
|
16
|
+
description: meta.description,
|
|
17
|
+
color: meta.color,
|
|
18
|
+
mass: raw.mass,
|
|
19
|
+
type: raw.type,
|
|
20
|
+
tier: raw.tier,
|
|
21
|
+
category: raw.category,
|
|
22
|
+
moduleType: raw.type === 'module' ? raw.subtype : undefined,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const itemIds = Array.from(itemsById.keys())
|
|
27
|
+
|
|
28
|
+
export function getItem(itemId: UInt16Type): Item {
|
|
29
|
+
const id = UInt16.from(itemId).toNumber()
|
|
30
|
+
const item = itemsById.get(id)
|
|
31
|
+
if (!item) throw new Error(`Unknown item id: ${id}`)
|
|
32
|
+
return item
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getItems(): Item[] {
|
|
36
|
+
return Array.from(itemsById.values())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function __registerItemInternal(item: Item): void {
|
|
40
|
+
itemsById.set(item.id, item)
|
|
41
|
+
}
|
|
@@ -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 * Math.pow(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 * Math.pow(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
|
+
}
|
package/src/nft/index.ts
ADDED
|
@@ -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
|
+
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
|
+
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,43 @@
|
|
|
1
|
+
import type {ResolvedItem} from './resolve-item'
|
|
2
|
+
import type {ResourceCategory} from '../types'
|
|
3
|
+
import {CATEGORY_LABELS, TIER_ADJECTIVES, tierNumber} from '../types'
|
|
4
|
+
import {formatMass as defaultFormatMass} from '../format'
|
|
5
|
+
|
|
6
|
+
export interface DisplayNameInput {
|
|
7
|
+
itemType: 'resource' | 'component' | 'module' | 'entity' | string
|
|
8
|
+
tier: number | string
|
|
9
|
+
category?: ResourceCategory
|
|
10
|
+
name: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function asTierNumber(tier: number | string): number {
|
|
14
|
+
return typeof tier === 'number' ? tier : tierNumber(tier)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function displayName(resolved: DisplayNameInput): string {
|
|
18
|
+
if (resolved.itemType === 'resource') {
|
|
19
|
+
const adj = TIER_ADJECTIVES[asTierNumber(resolved.tier)] ?? 'Unknown'
|
|
20
|
+
const cat = resolved.category ? CATEGORY_LABELS[resolved.category] : 'Resource'
|
|
21
|
+
return `${adj} ${cat}`
|
|
22
|
+
}
|
|
23
|
+
return resolved.name
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DescribeOptions {
|
|
27
|
+
translate?: (key: string) => string
|
|
28
|
+
formatNumber?: (n: number) => string
|
|
29
|
+
formatMass?: (kg: number) => string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function describeItem(resolved: ResolvedItem, opts?: DescribeOptions): string {
|
|
33
|
+
const massFmt = opts?.formatMass ?? defaultFormatMass
|
|
34
|
+
const mass = massFmt(resolved.mass)
|
|
35
|
+
const tier = `T${asTierNumber(resolved.tier)}`
|
|
36
|
+
if (resolved.itemType === 'resource') {
|
|
37
|
+
const cat = resolved.category ? CATEGORY_LABELS[resolved.category] : 'Resource'
|
|
38
|
+
const header = `${tier} ${cat}`
|
|
39
|
+
const stats = resolved.stats?.map((s) => `${s.label} ${s.value}`).join(', ')
|
|
40
|
+
return [header, stats, mass].filter(Boolean).join(' · ')
|
|
41
|
+
}
|
|
42
|
+
return `${tier} ${resolved.name} · ${mass}`
|
|
43
|
+
}
|