@shipload/item-renderer 1.0.0-next.13 → 1.0.0-next.15
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 +2 -2
- package/src/index.ts +12 -2
- package/src/links.ts +11 -8
- package/src/meta.ts +1 -1
- package/src/payload/codec.ts +24 -2
- package/src/primitives/quantity-badge.ts +1 -1
- package/src/render.ts +12 -7
- package/src/templates/_shared.ts +66 -2
- package/src/templates/component.ts +34 -45
- package/src/templates/index.ts +2 -1
- package/src/templates/module.ts +29 -49
- package/src/templates/packed-entity.ts +13 -3
- package/src/templates/resource.ts +25 -53
- package/src/templates/ship-panel.ts +25 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipload/item-renderer",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.15",
|
|
4
4
|
"description": "Deterministic SVG rendering for Shipload items",
|
|
5
5
|
"homepage": "https://github.com/shipload/toolkit/tree/master/packages/item-renderer",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"fonts:copy": "bun run scripts/copy-fonts.ts"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@shipload/sdk": "^1.0.0-next.
|
|
48
|
+
"@shipload/sdk": "^1.0.0-next.15",
|
|
49
49
|
"@wharfkit/antelope": "1.2.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -5,8 +5,18 @@ export const VERSION = '0.1.0'
|
|
|
5
5
|
export {InvalidPayloadError, UnknownItemError, RenderError} from './errors.ts'
|
|
6
6
|
|
|
7
7
|
// Payload
|
|
8
|
-
export {
|
|
9
|
-
|
|
8
|
+
export {
|
|
9
|
+
encodeCargoItem,
|
|
10
|
+
decodeCargoItem,
|
|
11
|
+
encodeNftPayload,
|
|
12
|
+
decodeNftPayload,
|
|
13
|
+
} from './payload/codec.ts'
|
|
14
|
+
export type {
|
|
15
|
+
CargoItem,
|
|
16
|
+
CargoItemLike,
|
|
17
|
+
NftItemPayload,
|
|
18
|
+
NftItemPayloadLike,
|
|
19
|
+
} from './payload/codec.ts'
|
|
10
20
|
|
|
11
21
|
// Rendering
|
|
12
22
|
export {renderItem, renderFromPayload, type RenderOptions} from './render.ts'
|
package/src/links.ts
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import type {CargoItem} from './payload/codec.ts'
|
|
2
|
-
import {
|
|
2
|
+
import {encodeCargoItem, encodeNftPayload} from './payload/codec.ts'
|
|
3
3
|
|
|
4
4
|
const DEFAULT_WEBSITE_BASE = 'https://shiploadgame.com'
|
|
5
5
|
const DEFAULT_IMAGE_BASE = 'https://item.shiploadgame.com'
|
|
6
6
|
|
|
7
7
|
export function linkToItemPage(item: CargoItem, baseUrl = DEFAULT_WEBSITE_BASE): string {
|
|
8
|
-
const payload =
|
|
8
|
+
const payload = encodeCargoItem(item)
|
|
9
9
|
return `${baseUrl}/guide/item/${payload}`
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function linkToItemImage(
|
|
13
13
|
item: CargoItem,
|
|
14
14
|
ext: 'png' | 'svg',
|
|
15
|
-
baseUrl
|
|
15
|
+
opts?: {location?: {x: number | bigint; y: number | bigint}; baseUrl?: string}
|
|
16
16
|
): string {
|
|
17
|
-
const payload =
|
|
18
|
-
return `${baseUrl}/item/${payload}.${ext}`
|
|
17
|
+
const payload = encodeNftPayload({item, location: opts?.location ?? null})
|
|
18
|
+
return `${opts?.baseUrl ?? DEFAULT_IMAGE_BASE}/item/${payload}.${ext}`
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function linkToItemSocial(
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
export function linkToItemSocial(
|
|
22
|
+
item: CargoItem,
|
|
23
|
+
opts?: {location?: {x: number | bigint; y: number | bigint}; baseUrl?: string}
|
|
24
|
+
): string {
|
|
25
|
+
const payload = encodeNftPayload({item, location: opts?.location ?? null})
|
|
26
|
+
return `${opts?.baseUrl ?? DEFAULT_IMAGE_BASE}/social/${payload}.png`
|
|
24
27
|
}
|
package/src/meta.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function itemPageMeta(
|
|
|
32
32
|
return {
|
|
33
33
|
title: `${displayName(resolved)} · Shipload Guide`,
|
|
34
34
|
description: describeItem(resolved),
|
|
35
|
-
ogImage: linkToItemSocial(item, opts?.imageBaseUrl),
|
|
35
|
+
ogImage: linkToItemSocial(item, {baseUrl: opts?.imageBaseUrl}),
|
|
36
36
|
ogImageWidth: SOCIAL_CARD_WIDTH,
|
|
37
37
|
ogImageHeight: SOCIAL_CARD_HEIGHT,
|
|
38
38
|
}
|
package/src/payload/codec.ts
CHANGED
|
@@ -6,13 +6,16 @@ import {base64UrlToBytes, bytesToBase64Url} from './base64url.ts'
|
|
|
6
6
|
export type CargoItem = InstanceType<typeof ServerContract.Types.cargo_item>
|
|
7
7
|
export type CargoItemLike = Parameters<typeof ServerContract.Types.cargo_item.from>[0]
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export type NftItemPayload = InstanceType<typeof ServerContract.Types.nft_item_payload>
|
|
10
|
+
export type NftItemPayloadLike = Parameters<typeof ServerContract.Types.nft_item_payload.from>[0]
|
|
11
|
+
|
|
12
|
+
export function encodeCargoItem(input: CargoItemLike): string {
|
|
10
13
|
const item = ServerContract.Types.cargo_item.from(input)
|
|
11
14
|
const bytes = Serializer.encode({object: item}).array
|
|
12
15
|
return bytesToBase64Url(bytes)
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
export function
|
|
18
|
+
export function decodeCargoItem(input: string): CargoItem {
|
|
16
19
|
if (input.length === 0) throw new InvalidPayloadError('empty payload')
|
|
17
20
|
const bytes = base64UrlToBytes(input)
|
|
18
21
|
try {
|
|
@@ -24,3 +27,22 @@ export function decodePayload(input: string): CargoItem {
|
|
|
24
27
|
throw new InvalidPayloadError(`cargo_item decode failed: ${(e as Error).message}`)
|
|
25
28
|
}
|
|
26
29
|
}
|
|
30
|
+
|
|
31
|
+
export function encodeNftPayload(input: NftItemPayloadLike): string {
|
|
32
|
+
const payload = ServerContract.Types.nft_item_payload.from(input)
|
|
33
|
+
const bytes = Serializer.encode({object: payload}).array
|
|
34
|
+
return bytesToBase64Url(bytes)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function decodeNftPayload(input: string): NftItemPayload {
|
|
38
|
+
if (input.length === 0) throw new InvalidPayloadError('empty payload')
|
|
39
|
+
const bytes = base64UrlToBytes(input)
|
|
40
|
+
try {
|
|
41
|
+
return Serializer.decode({
|
|
42
|
+
data: bytes,
|
|
43
|
+
type: ServerContract.Types.nft_item_payload,
|
|
44
|
+
}) as NftItemPayload
|
|
45
|
+
} catch (e) {
|
|
46
|
+
throw new InvalidPayloadError(`nft_item_payload decode failed: ${(e as Error).message}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -9,7 +9,7 @@ export interface QuantityBadgeProps {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function quantityBadge({x, y, quantity}: QuantityBadgeProps): string {
|
|
12
|
-
if (quantity <=
|
|
12
|
+
if (quantity <= 0) return ''
|
|
13
13
|
const label = `×${quantity}`
|
|
14
14
|
const w = label.length * 7 + 12
|
|
15
15
|
const h = tokens.spacing.quantityBadgeHeight
|
package/src/render.ts
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
import {resolveItem, type ResolvedItem} from '@shipload/sdk'
|
|
2
2
|
import type {CargoItem} from './payload/codec.ts'
|
|
3
|
-
import {
|
|
3
|
+
import {decodeNftPayload} from './payload/codec.ts'
|
|
4
4
|
import {renderByType} from './templates/index.ts'
|
|
5
5
|
import {UnknownItemError} from './errors.ts'
|
|
6
6
|
|
|
7
7
|
export interface RenderOptions {
|
|
8
8
|
width?: number
|
|
9
9
|
theme?: 'dark' | 'light'
|
|
10
|
+
location?: {x: number; y: number}
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function renderItem(item: CargoItem, resolved: ResolvedItem,
|
|
13
|
-
return renderByType(item, resolved)
|
|
13
|
+
export function renderItem(item: CargoItem, resolved: ResolvedItem, opts?: RenderOptions): string {
|
|
14
|
+
return renderByType(item, resolved, {location: opts?.location})
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function renderFromPayload(
|
|
17
18
|
payload: string,
|
|
18
19
|
opts?: RenderOptions
|
|
19
20
|
): Promise<{svg: string; item: ResolvedItem}> {
|
|
20
|
-
const
|
|
21
|
+
const decoded = decodeNftPayload(payload)
|
|
22
|
+
const cargo = decoded.item
|
|
21
23
|
let resolved: ResolvedItem
|
|
22
24
|
try {
|
|
23
|
-
resolved = resolveItem(
|
|
25
|
+
resolved = resolveItem(cargo.item_id, cargo.stats, cargo.modules)
|
|
24
26
|
} catch {
|
|
25
|
-
throw new UnknownItemError(Number(BigInt(
|
|
27
|
+
throw new UnknownItemError(Number(BigInt(cargo.item_id.toString())))
|
|
26
28
|
}
|
|
27
|
-
const
|
|
29
|
+
const location = decoded.location
|
|
30
|
+
? {x: Number(decoded.location.x), y: Number(decoded.location.y)}
|
|
31
|
+
: opts?.location
|
|
32
|
+
const svg = renderItem(cargo, resolved, {...opts, location})
|
|
28
33
|
return {svg, item: resolved}
|
|
29
34
|
}
|
package/src/templates/_shared.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {formatMassScaled} from '@shipload/sdk'
|
|
2
|
+
import {text} from '../primitives/text.ts'
|
|
3
|
+
import {divider} from '../primitives/divider.ts'
|
|
1
4
|
import {tokens} from '../tokens/index.ts'
|
|
2
5
|
|
|
3
|
-
export function formatMass(
|
|
4
|
-
return
|
|
6
|
+
export function formatMass(kg: number): string {
|
|
7
|
+
return formatMassScaled(kg)
|
|
5
8
|
}
|
|
6
9
|
|
|
7
10
|
export function tierBorder(tier: number): string {
|
|
@@ -12,3 +15,64 @@ export function shortCode(itemId: number): string {
|
|
|
12
15
|
const str = itemId.toString(10)
|
|
13
16
|
return str.slice(-2).padStart(2, '0')
|
|
14
17
|
}
|
|
18
|
+
|
|
19
|
+
export const META_ROW_H = 22
|
|
20
|
+
export const HEADER_H = 48
|
|
21
|
+
export const ICON_Y = 4
|
|
22
|
+
export const BADGE_Y = 6
|
|
23
|
+
export const META_BLOCK_GAP = 16
|
|
24
|
+
|
|
25
|
+
export interface MetaRowProps {
|
|
26
|
+
x: number
|
|
27
|
+
y: number
|
|
28
|
+
width: number
|
|
29
|
+
label: string
|
|
30
|
+
value: string
|
|
31
|
+
showDivider?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function metaRow({x, y, width, label, value, showDivider = true}: MetaRowProps): string {
|
|
35
|
+
const labelText = text({
|
|
36
|
+
x,
|
|
37
|
+
y: y + 12,
|
|
38
|
+
value: label,
|
|
39
|
+
size: tokens.typography.sizes.body,
|
|
40
|
+
color: tokens.colors.text.secondary,
|
|
41
|
+
})
|
|
42
|
+
const valueText = text({
|
|
43
|
+
x: x + width,
|
|
44
|
+
y: y + 12,
|
|
45
|
+
value,
|
|
46
|
+
size: tokens.typography.sizes.body,
|
|
47
|
+
weight: 700,
|
|
48
|
+
anchor: 'end',
|
|
49
|
+
})
|
|
50
|
+
const sep = showDivider
|
|
51
|
+
? divider({
|
|
52
|
+
x,
|
|
53
|
+
y: y + META_ROW_H - 4,
|
|
54
|
+
width,
|
|
55
|
+
color: tokens.colors.surface.panelBorderBright,
|
|
56
|
+
})
|
|
57
|
+
: ''
|
|
58
|
+
return labelText + valueText + sep
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function metaRowBlock(
|
|
62
|
+
x: number,
|
|
63
|
+
yStart: number,
|
|
64
|
+
width: number,
|
|
65
|
+
rows: {label: string; value: string}[]
|
|
66
|
+
): {svg: string; height: number} {
|
|
67
|
+
let svg = ''
|
|
68
|
+
rows.forEach((row, i) => {
|
|
69
|
+
svg += metaRow({
|
|
70
|
+
x,
|
|
71
|
+
y: yStart + i * META_ROW_H,
|
|
72
|
+
width,
|
|
73
|
+
...row,
|
|
74
|
+
showDivider: i < rows.length - 1,
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
return {svg, height: rows.length * META_ROW_H}
|
|
78
|
+
}
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import type {ResolvedItem, ResourceCategory} from '@shipload/sdk'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
formatTier,
|
|
4
|
+
getRecipe,
|
|
5
|
+
getStatDefinitions,
|
|
6
|
+
categoryColors,
|
|
7
|
+
displayName,
|
|
8
|
+
formatLocation,
|
|
9
|
+
} from '@shipload/sdk'
|
|
3
10
|
import type {CargoItem} from '../payload/codec.ts'
|
|
4
11
|
import {panel} from '../primitives/panel.ts'
|
|
5
12
|
import {iconHex} from '../primitives/icon-hex.ts'
|
|
6
13
|
import {text} from '../primitives/text.ts'
|
|
7
|
-
import {divider} from '../primitives/divider.ts'
|
|
8
14
|
import {statBar} from '../primitives/stat-bar.ts'
|
|
9
15
|
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
10
16
|
import {tokens} from '../tokens/index.ts'
|
|
11
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
shortCode,
|
|
19
|
+
formatMass,
|
|
20
|
+
tierBorder,
|
|
21
|
+
metaRowBlock,
|
|
22
|
+
BADGE_Y,
|
|
23
|
+
HEADER_H,
|
|
24
|
+
ICON_Y,
|
|
25
|
+
META_BLOCK_GAP,
|
|
26
|
+
} from './_shared.ts'
|
|
12
27
|
|
|
13
28
|
export interface RenderComponentOpts {
|
|
14
29
|
mode?: 'values' | 'ranges'
|
|
30
|
+
location?: {x: number; y: number}
|
|
15
31
|
}
|
|
16
32
|
|
|
17
33
|
type StatRow = {
|
|
@@ -63,19 +79,26 @@ export function renderComponent(
|
|
|
63
79
|
})
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
const
|
|
67
|
-
|
|
82
|
+
const metaRows = [
|
|
83
|
+
{label: 'Type', value: `COMPONENT · ${formatTier(resolved.tier)}`},
|
|
84
|
+
{label: 'Mass', value: formatMass(resolved.mass)},
|
|
85
|
+
...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
const metaYStart = pad + HEADER_H
|
|
89
|
+
const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
|
|
90
|
+
const statsYStart = metaYStart + metaH + META_BLOCK_GAP
|
|
68
91
|
const statsH = rows.length * 26 + 8
|
|
69
|
-
const height =
|
|
92
|
+
const height = statsYStart + statsH + pad
|
|
70
93
|
|
|
71
94
|
const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
|
|
72
95
|
|
|
73
96
|
const quantity = Number(BigInt(item.quantity.toString()))
|
|
74
|
-
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
97
|
+
const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity})
|
|
75
98
|
|
|
76
99
|
const icon = iconHex({
|
|
77
100
|
x: pad,
|
|
78
|
-
y: pad +
|
|
101
|
+
y: pad + ICON_Y,
|
|
79
102
|
color: tokens.colors.accent.component,
|
|
80
103
|
code: shortCode(resolved.itemId),
|
|
81
104
|
})
|
|
@@ -83,51 +106,17 @@ export function renderComponent(
|
|
|
83
106
|
const name = text({
|
|
84
107
|
x: pad + 34,
|
|
85
108
|
y: pad + 22,
|
|
86
|
-
value: resolved
|
|
109
|
+
value: displayName(resolved),
|
|
87
110
|
size: tokens.typography.sizes.title,
|
|
88
111
|
weight: 700,
|
|
89
112
|
family: tokens.typography.display,
|
|
90
113
|
})
|
|
91
114
|
|
|
92
|
-
const subtitleText = text({
|
|
93
|
-
x: pad,
|
|
94
|
-
y: pad + headerH + 4,
|
|
95
|
-
value: 'Type',
|
|
96
|
-
size: tokens.typography.sizes.body,
|
|
97
|
-
color: tokens.colors.text.secondary,
|
|
98
|
-
})
|
|
99
|
-
const subtitleValue = text({
|
|
100
|
-
x: w - pad,
|
|
101
|
-
y: pad + headerH + 4,
|
|
102
|
-
value: `COMPONENT · ${formatTier(resolved.tier)}`,
|
|
103
|
-
size: tokens.typography.sizes.body,
|
|
104
|
-
weight: 600,
|
|
105
|
-
anchor: 'end',
|
|
106
|
-
})
|
|
107
|
-
const massLabel = text({
|
|
108
|
-
x: pad,
|
|
109
|
-
y: pad + headerH + metaRowH - 8,
|
|
110
|
-
value: 'Mass',
|
|
111
|
-
size: tokens.typography.sizes.body,
|
|
112
|
-
color: tokens.colors.text.secondary,
|
|
113
|
-
})
|
|
114
|
-
const massValue = text({
|
|
115
|
-
x: w - pad,
|
|
116
|
-
y: pad + headerH + metaRowH - 8,
|
|
117
|
-
value: formatMass(resolved.mass),
|
|
118
|
-
size: tokens.typography.sizes.body,
|
|
119
|
-
weight: 600,
|
|
120
|
-
anchor: 'end',
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
const sepY = pad + headerH + metaRowH + 6
|
|
124
|
-
const sep = divider({x: pad, y: sepY, width: innerW})
|
|
125
|
-
|
|
126
115
|
const statsSvg = rows
|
|
127
116
|
.map((row, i) =>
|
|
128
117
|
statBar({
|
|
129
118
|
x: pad,
|
|
130
|
-
y:
|
|
119
|
+
y: statsYStart + i * 26,
|
|
131
120
|
width: innerW,
|
|
132
121
|
label: row.label,
|
|
133
122
|
abbreviation: row.abbreviation,
|
|
@@ -138,7 +127,7 @@ export function renderComponent(
|
|
|
138
127
|
)
|
|
139
128
|
.join('')
|
|
140
129
|
|
|
141
|
-
const inner = `${chrome}${icon}${name}${badge}${
|
|
130
|
+
const inner = `${chrome}${icon}${name}${badge}${metaSvg}${statsSvg}`
|
|
142
131
|
|
|
143
132
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
144
133
|
}
|
package/src/templates/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {renderModule} from './module.ts'
|
|
|
8
8
|
|
|
9
9
|
export interface RenderByTypeOpts {
|
|
10
10
|
mode?: 'values' | 'ranges'
|
|
11
|
+
location?: {x: number; y: number}
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function renderByType(
|
|
@@ -19,7 +20,7 @@ export function renderByType(
|
|
|
19
20
|
case 'resource':
|
|
20
21
|
return renderResource(item, resolved, opts)
|
|
21
22
|
case 'entity':
|
|
22
|
-
return renderPackedEntity(item, resolved)
|
|
23
|
+
return renderPackedEntity(item, resolved, opts)
|
|
23
24
|
case 'component':
|
|
24
25
|
return renderComponent(item, resolved, opts)
|
|
25
26
|
case 'module':
|
package/src/templates/module.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
-
import {describeModuleForItem,
|
|
2
|
+
import {describeModuleForItem, displayName, formatLocation, renderDescription} from '@shipload/sdk'
|
|
3
3
|
import type {CargoItem} from '../payload/codec.ts'
|
|
4
4
|
import {panel} from '../primitives/panel.ts'
|
|
5
5
|
import {iconHex} from '../primitives/icon-hex.ts'
|
|
6
6
|
import {text} from '../primitives/text.ts'
|
|
7
|
-
import {divider} from '../primitives/divider.ts'
|
|
8
7
|
import {compactRow} from '../primitives/compact-row.ts'
|
|
9
8
|
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
10
9
|
import {spanParagraph} from '../primitives/span-paragraph.ts'
|
|
11
10
|
import {tokens} from '../tokens/index.ts'
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
shortCode,
|
|
13
|
+
formatMass,
|
|
14
|
+
tierBorder,
|
|
15
|
+
metaRowBlock,
|
|
16
|
+
BADGE_Y,
|
|
17
|
+
HEADER_H,
|
|
18
|
+
ICON_Y,
|
|
19
|
+
META_BLOCK_GAP,
|
|
20
|
+
} from './_shared.ts'
|
|
13
21
|
|
|
14
22
|
function capabilityColor(name: string): string {
|
|
15
23
|
const key = name.toLowerCase().replace(/\s+/g, '') as keyof typeof tokens.colors.capability
|
|
@@ -18,6 +26,7 @@ function capabilityColor(name: string): string {
|
|
|
18
26
|
|
|
19
27
|
export interface RenderModuleOpts {
|
|
20
28
|
mode?: 'values' | 'ranges'
|
|
29
|
+
location?: {x: number; y: number}
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
export function renderModule(
|
|
@@ -36,9 +45,14 @@ export function renderModule(
|
|
|
36
45
|
|
|
37
46
|
const capabilityName = group?.capability ?? resolved.name.replace(/\s+T\d+$/i, '')
|
|
38
47
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
const metaRows = [
|
|
49
|
+
{label: 'Mass', value: formatMass(resolved.mass)},
|
|
50
|
+
...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const metaYStart = pad + HEADER_H
|
|
54
|
+
const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
|
|
55
|
+
const bodyYStart = metaYStart + metaH + META_BLOCK_GAP
|
|
42
56
|
|
|
43
57
|
let bodyHeight = 0
|
|
44
58
|
if (mode === 'ranges') {
|
|
@@ -65,17 +79,17 @@ export function renderModule(
|
|
|
65
79
|
bodyHeight = capHeaderH + attrsH + 8
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
const height =
|
|
82
|
+
const height = bodyYStart + bodyHeight + pad
|
|
69
83
|
|
|
70
84
|
const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
|
|
71
85
|
|
|
72
86
|
const quantity = Number(BigInt(item.quantity.toString()))
|
|
73
|
-
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
87
|
+
const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity})
|
|
74
88
|
|
|
75
89
|
const iconColor = group ? capabilityColor(group.capability) : capabilityColor(capabilityName)
|
|
76
90
|
const icon = iconHex({
|
|
77
91
|
x: pad,
|
|
78
|
-
y: pad +
|
|
92
|
+
y: pad + ICON_Y,
|
|
79
93
|
color: iconColor,
|
|
80
94
|
code: shortCode(resolved.itemId),
|
|
81
95
|
})
|
|
@@ -83,52 +97,18 @@ export function renderModule(
|
|
|
83
97
|
const name = text({
|
|
84
98
|
x: pad + 34,
|
|
85
99
|
y: pad + 22,
|
|
86
|
-
value: resolved
|
|
100
|
+
value: displayName(resolved),
|
|
87
101
|
size: tokens.typography.sizes.title,
|
|
88
102
|
weight: 700,
|
|
89
103
|
family: tokens.typography.display,
|
|
90
104
|
})
|
|
91
105
|
|
|
92
|
-
const subtitleLabel = text({
|
|
93
|
-
x: pad,
|
|
94
|
-
y: pad + headerH + 4,
|
|
95
|
-
value: 'Type',
|
|
96
|
-
size: tokens.typography.sizes.body,
|
|
97
|
-
color: tokens.colors.text.secondary,
|
|
98
|
-
})
|
|
99
|
-
const subtitleValue = text({
|
|
100
|
-
x: w - pad,
|
|
101
|
-
y: pad + headerH + 4,
|
|
102
|
-
value: `MODULE · ${formatTier(resolved.tier)}`,
|
|
103
|
-
size: tokens.typography.sizes.body,
|
|
104
|
-
weight: 600,
|
|
105
|
-
anchor: 'end',
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
const massLabel = text({
|
|
109
|
-
x: pad,
|
|
110
|
-
y: pad + headerH + metaRowH - 8,
|
|
111
|
-
value: 'Mass',
|
|
112
|
-
size: tokens.typography.sizes.body,
|
|
113
|
-
color: tokens.colors.text.secondary,
|
|
114
|
-
})
|
|
115
|
-
const massValue = text({
|
|
116
|
-
x: w - pad,
|
|
117
|
-
y: pad + headerH + metaRowH - 8,
|
|
118
|
-
value: formatMass(resolved.mass),
|
|
119
|
-
size: tokens.typography.sizes.body,
|
|
120
|
-
weight: 600,
|
|
121
|
-
anchor: 'end',
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const sep = divider({x: pad, y: sepY, width: innerW})
|
|
125
|
-
|
|
126
106
|
let capSection = ''
|
|
127
107
|
if (mode === 'ranges') {
|
|
128
108
|
const accentColor = capabilityColor(capabilityName)
|
|
129
109
|
capSection = text({
|
|
130
110
|
x: pad,
|
|
131
|
-
y:
|
|
111
|
+
y: bodyYStart + 16,
|
|
132
112
|
value: capabilityName.toUpperCase(),
|
|
133
113
|
size: tokens.typography.sizes.subtitle,
|
|
134
114
|
weight: 700,
|
|
@@ -140,7 +120,7 @@ export function renderModule(
|
|
|
140
120
|
const accentColor = capabilityColor(group.capability)
|
|
141
121
|
const capHeader = text({
|
|
142
122
|
x: pad,
|
|
143
|
-
y:
|
|
123
|
+
y: bodyYStart + 16,
|
|
144
124
|
value: group.capability.toUpperCase(),
|
|
145
125
|
size: tokens.typography.sizes.subtitle,
|
|
146
126
|
weight: 700,
|
|
@@ -151,14 +131,14 @@ export function renderModule(
|
|
|
151
131
|
const spans = renderDescription(desc)
|
|
152
132
|
const {svg: paraSvg} = spanParagraph({
|
|
153
133
|
x: pad,
|
|
154
|
-
y:
|
|
134
|
+
y: bodyYStart + 36,
|
|
155
135
|
spans,
|
|
156
136
|
charsPerLine: 36,
|
|
157
137
|
lineHeight: 14,
|
|
158
138
|
})
|
|
159
139
|
capSection = capHeader + paraSvg
|
|
160
140
|
} else if (group && attrs.length > 0) {
|
|
161
|
-
const capY =
|
|
141
|
+
const capY = bodyYStart + 22
|
|
162
142
|
const capHeader = text({
|
|
163
143
|
x: pad,
|
|
164
144
|
y: capY,
|
|
@@ -186,7 +166,7 @@ export function renderModule(
|
|
|
186
166
|
capSection = capHeader + attrRows
|
|
187
167
|
}
|
|
188
168
|
|
|
189
|
-
const inner = `${chrome}${icon}${name}${badge}${
|
|
169
|
+
const inner = `${chrome}${icon}${name}${badge}${metaSvg}${capSection}`
|
|
190
170
|
|
|
191
171
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
192
172
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {ResolvedItem, ResolvedModuleSlot} from '@shipload/sdk'
|
|
2
|
-
import {describeModuleForSlot, renderDescription} from '@shipload/sdk'
|
|
2
|
+
import {describeModuleForSlot, displayName, renderDescription} from '@shipload/sdk'
|
|
3
3
|
import type {CargoItem} from '../payload/codec.ts'
|
|
4
4
|
import {renderShipPanel, type ShipPanelSlot} from './ship-panel.ts'
|
|
5
5
|
|
|
@@ -15,13 +15,23 @@ function slotToPanelSlot(slot: ResolvedModuleSlot): ShipPanelSlot {
|
|
|
15
15
|
return {name: slot.name, installed: true, description: shorthand}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export
|
|
18
|
+
export interface RenderPackedEntityOpts {
|
|
19
|
+
mode?: 'values' | 'ranges'
|
|
20
|
+
location?: {x: number; y: number}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderPackedEntity(
|
|
24
|
+
item: CargoItem,
|
|
25
|
+
resolved: ResolvedItem,
|
|
26
|
+
opts?: RenderPackedEntityOpts
|
|
27
|
+
): string {
|
|
19
28
|
const quantity = Number(BigInt(item.quantity.toString()))
|
|
20
29
|
const slots = (resolved.moduleSlots ?? []).map(slotToPanelSlot)
|
|
21
30
|
return renderShipPanel({
|
|
22
|
-
name: `${resolved
|
|
31
|
+
name: `${displayName(resolved)} (Packed)`,
|
|
23
32
|
tier: resolved.tier,
|
|
24
33
|
quantity,
|
|
34
|
+
location: opts?.location,
|
|
25
35
|
attributes: resolved.attributes ?? [],
|
|
26
36
|
slots,
|
|
27
37
|
})
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
-
import {getStatDefinitions, categoryColors, displayName} from '@shipload/sdk'
|
|
2
|
+
import {getStatDefinitions, categoryColors, displayName, formatLocation} from '@shipload/sdk'
|
|
3
3
|
import type {CargoItem} from '../payload/codec.ts'
|
|
4
4
|
import {panel} from '../primitives/panel.ts'
|
|
5
5
|
import {iconHex} from '../primitives/icon-hex.ts'
|
|
6
6
|
import {text} from '../primitives/text.ts'
|
|
7
|
-
import {divider} from '../primitives/divider.ts'
|
|
8
7
|
import {statBar} from '../primitives/stat-bar.ts'
|
|
9
8
|
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
10
9
|
import {tokens} from '../tokens/index.ts'
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
import {
|
|
11
|
+
shortCode,
|
|
12
|
+
formatMass,
|
|
13
|
+
tierBorder,
|
|
14
|
+
metaRowBlock,
|
|
15
|
+
BADGE_Y,
|
|
16
|
+
HEADER_H,
|
|
17
|
+
ICON_Y,
|
|
18
|
+
META_BLOCK_GAP,
|
|
19
|
+
} from './_shared.ts'
|
|
20
20
|
|
|
21
21
|
function categoryColor(category?: string): string {
|
|
22
22
|
if (!category) return tokens.colors.text.muted
|
|
@@ -26,6 +26,7 @@ function categoryColor(category?: string): string {
|
|
|
26
26
|
|
|
27
27
|
export interface RenderResourceOpts {
|
|
28
28
|
mode?: 'values' | 'ranges'
|
|
29
|
+
location?: {x: number; y: number}
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
type StatRow = {
|
|
@@ -69,19 +70,25 @@ export function renderResource(
|
|
|
69
70
|
}))
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
const
|
|
73
|
-
|
|
73
|
+
const metaRows = [
|
|
74
|
+
{label: 'Mass', value: formatMass(resolved.mass)},
|
|
75
|
+
...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const metaYStart = pad + HEADER_H
|
|
79
|
+
const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
|
|
80
|
+
const statsYStart = metaYStart + metaH + META_BLOCK_GAP
|
|
74
81
|
const statsH = rows.length * 26 + 8
|
|
75
|
-
const height =
|
|
82
|
+
const height = statsYStart + statsH + pad
|
|
76
83
|
|
|
77
84
|
const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
|
|
78
85
|
|
|
79
86
|
const quantity = Number(BigInt(item.quantity.toString()))
|
|
80
|
-
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
87
|
+
const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity})
|
|
81
88
|
|
|
82
89
|
const icon = iconHex({
|
|
83
90
|
x: pad,
|
|
84
|
-
y: pad +
|
|
91
|
+
y: pad + ICON_Y,
|
|
85
92
|
color: categoryColor(resolved.category),
|
|
86
93
|
code: shortCode(resolved.itemId),
|
|
87
94
|
})
|
|
@@ -95,46 +102,11 @@ export function renderResource(
|
|
|
95
102
|
family: tokens.typography.display,
|
|
96
103
|
})
|
|
97
104
|
|
|
98
|
-
const catLabel = resolved.category ? (CATEGORY_LABELS[resolved.category] ?? 'Item') : 'Item'
|
|
99
|
-
const catText = text({
|
|
100
|
-
x: pad,
|
|
101
|
-
y: pad + headerH + 4,
|
|
102
|
-
value: 'Category',
|
|
103
|
-
size: tokens.typography.sizes.body,
|
|
104
|
-
color: tokens.colors.text.secondary,
|
|
105
|
-
})
|
|
106
|
-
const catValue = text({
|
|
107
|
-
x: w - pad,
|
|
108
|
-
y: pad + headerH + 4,
|
|
109
|
-
value: catLabel,
|
|
110
|
-
size: tokens.typography.sizes.body,
|
|
111
|
-
weight: 600,
|
|
112
|
-
anchor: 'end',
|
|
113
|
-
})
|
|
114
|
-
const massLabel = text({
|
|
115
|
-
x: pad,
|
|
116
|
-
y: pad + headerH + metaRowH - 8,
|
|
117
|
-
value: 'Mass',
|
|
118
|
-
size: tokens.typography.sizes.body,
|
|
119
|
-
color: tokens.colors.text.secondary,
|
|
120
|
-
})
|
|
121
|
-
const massValue = text({
|
|
122
|
-
x: w - pad,
|
|
123
|
-
y: pad + headerH + metaRowH - 8,
|
|
124
|
-
value: formatMass(resolved.mass),
|
|
125
|
-
size: tokens.typography.sizes.body,
|
|
126
|
-
weight: 600,
|
|
127
|
-
anchor: 'end',
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const sepY = pad + headerH + metaRowH + 6
|
|
131
|
-
const sep = divider({x: pad, y: sepY, width: innerW})
|
|
132
|
-
|
|
133
105
|
const statsSvg = rows
|
|
134
106
|
.map((row, i) =>
|
|
135
107
|
statBar({
|
|
136
108
|
x: pad,
|
|
137
|
-
y:
|
|
109
|
+
y: statsYStart + i * 26,
|
|
138
110
|
width: innerW,
|
|
139
111
|
label: row.label,
|
|
140
112
|
abbreviation: row.abbreviation,
|
|
@@ -145,7 +117,7 @@ export function renderResource(
|
|
|
145
117
|
)
|
|
146
118
|
.join('')
|
|
147
119
|
|
|
148
|
-
const inner = `${chrome}${icon}${name}${badge}${
|
|
120
|
+
const inner = `${chrome}${icon}${name}${badge}${metaSvg}${statsSvg}`
|
|
149
121
|
|
|
150
122
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
151
123
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type {TextSpan} from '@shipload/sdk'
|
|
2
|
+
import {formatLocation, formatMassScaled} from '@shipload/sdk'
|
|
2
3
|
import {panel} from '../primitives/panel.ts'
|
|
3
4
|
import {iconHex} from '../primitives/icon-hex.ts'
|
|
4
5
|
import {text} from '../primitives/text.ts'
|
|
5
|
-
import {divider} from '../primitives/divider.ts'
|
|
6
6
|
import {moduleSlot} from '../primitives/module-slot.ts'
|
|
7
7
|
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
8
8
|
import {wrapText} from '../primitives/wrap.ts'
|
|
9
9
|
import {tokens} from '../tokens/index.ts'
|
|
10
|
+
import {tierBorder, metaRowBlock, BADGE_Y, HEADER_H, ICON_Y} from './_shared.ts'
|
|
11
|
+
|
|
12
|
+
const HULL_MASS_LABELS = new Set(['mass', 'capacity'])
|
|
10
13
|
|
|
11
14
|
export interface ShipPanelSlot {
|
|
12
15
|
name?: string
|
|
@@ -18,16 +21,15 @@ export interface ShipPanelProps {
|
|
|
18
21
|
name: string
|
|
19
22
|
tier: number
|
|
20
23
|
quantity?: number
|
|
24
|
+
location?: {x: number; y: number}
|
|
21
25
|
attributes: {capability: string; attributes: {label: string; value: number}[]}[]
|
|
22
26
|
slots: ShipPanelSlot[]
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
function
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
function tierBorder(tier: number): string {
|
|
30
|
-
return tokens.colors.tier[tier] ?? tokens.colors.surface.panelBorder
|
|
29
|
+
function formatHullValue(label: string, value: number): string {
|
|
30
|
+
return HULL_MASS_LABELS.has(label.toLowerCase())
|
|
31
|
+
? formatMassScaled(value)
|
|
32
|
+
: value.toLocaleString('en-US')
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const MODULE_LABEL_PREFIX = (capability: string) => `${capability}: `
|
|
@@ -54,22 +56,28 @@ export function renderShipPanel(props: ShipPanelProps): string {
|
|
|
54
56
|
const quantity = props.quantity ?? 0
|
|
55
57
|
|
|
56
58
|
const hullGroup = props.attributes?.find((g) => g.capability.toLowerCase() === 'hull')
|
|
57
|
-
const
|
|
59
|
+
const hullAttrs = (hullGroup?.attributes ?? []).map((a) => ({
|
|
60
|
+
label: a.label,
|
|
61
|
+
value: formatHullValue(a.label, a.value),
|
|
62
|
+
}))
|
|
63
|
+
const metaRows = props.location
|
|
64
|
+
? [{label: 'Location', value: formatLocation(props.location)}, ...hullAttrs]
|
|
65
|
+
: hullAttrs
|
|
58
66
|
|
|
59
|
-
const headerH = 48
|
|
60
|
-
const hullHeaderH = 20
|
|
61
|
-
const hullRowH = 22
|
|
62
67
|
const sectionGap = 12
|
|
68
|
+
|
|
69
|
+
const metaYStart = pad + HEADER_H
|
|
70
|
+
const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
|
|
71
|
+
|
|
63
72
|
const rowHeights = props.slots.map(rowHeightFor)
|
|
64
73
|
const modulesHeight = rowHeights.reduce((a, b) => a + b, 0)
|
|
65
|
-
const height =
|
|
66
|
-
headerH + hullHeaderH + hullRows.length * hullRowH + sectionGap + modulesHeight + pad
|
|
74
|
+
const height = metaYStart + metaH + sectionGap + modulesHeight + pad
|
|
67
75
|
|
|
68
76
|
const chrome = panel({width: w, height, borderColor: tierBorder(props.tier)})
|
|
69
77
|
|
|
70
78
|
const icon = iconHex({
|
|
71
79
|
x: pad,
|
|
72
|
-
y: pad +
|
|
80
|
+
y: pad + ICON_Y,
|
|
73
81
|
color: tokens.colors.text.accent,
|
|
74
82
|
code: 'SH',
|
|
75
83
|
})
|
|
@@ -83,47 +91,9 @@ export function renderShipPanel(props: ShipPanelProps): string {
|
|
|
83
91
|
family: tokens.typography.display,
|
|
84
92
|
})
|
|
85
93
|
|
|
86
|
-
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
87
|
-
|
|
88
|
-
const hullHeader = text({
|
|
89
|
-
x: pad,
|
|
90
|
-
y: pad + headerH,
|
|
91
|
-
value: 'HULL',
|
|
92
|
-
size: tokens.typography.sizes.subtitle,
|
|
93
|
-
weight: 700,
|
|
94
|
-
color: tokens.colors.text.secondary,
|
|
95
|
-
letterSpacing: 1,
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
let y = pad + headerH + 6
|
|
99
|
-
let hullSvg = ''
|
|
100
|
-
for (const row of hullRows) {
|
|
101
|
-
hullSvg +=
|
|
102
|
-
text({
|
|
103
|
-
x: pad,
|
|
104
|
-
y: y + 12,
|
|
105
|
-
value: row.label,
|
|
106
|
-
size: tokens.typography.sizes.body,
|
|
107
|
-
color: tokens.colors.text.secondary,
|
|
108
|
-
}) +
|
|
109
|
-
text({
|
|
110
|
-
x: w - pad,
|
|
111
|
-
y: y + 12,
|
|
112
|
-
value: formatNumber(row.value),
|
|
113
|
-
size: tokens.typography.sizes.body,
|
|
114
|
-
weight: 700,
|
|
115
|
-
anchor: 'end',
|
|
116
|
-
}) +
|
|
117
|
-
divider({
|
|
118
|
-
x: pad,
|
|
119
|
-
y: y + hullRowH - 4,
|
|
120
|
-
width: innerW,
|
|
121
|
-
color: tokens.colors.surface.panelBorderBright,
|
|
122
|
-
})
|
|
123
|
-
y += hullRowH
|
|
124
|
-
}
|
|
94
|
+
const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity})
|
|
125
95
|
|
|
126
|
-
y
|
|
96
|
+
let y = metaYStart + metaH + sectionGap
|
|
127
97
|
let modulesSvg = ''
|
|
128
98
|
for (let i = 0; i < props.slots.length; i++) {
|
|
129
99
|
const slot = props.slots[i]!
|
|
@@ -139,6 +109,6 @@ export function renderShipPanel(props: ShipPanelProps): string {
|
|
|
139
109
|
y += rowHeights[i]!
|
|
140
110
|
}
|
|
141
111
|
|
|
142
|
-
const inner = `${chrome}${icon}${name}${badge}${
|
|
112
|
+
const inner = `${chrome}${icon}${name}${badge}${metaSvg}${modulesSvg}`
|
|
143
113
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
144
114
|
}
|