@shipload/item-renderer 1.0.0-next.12 → 1.0.0-next.14
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 +34 -49
- package/src/templates/packed-entity.ts +13 -3
- package/src/templates/resource.ts +31 -54
- 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.14",
|
|
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.14",
|
|
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
|
+
displayNameWithTier,
|
|
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: displayNameWithTier(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,28 @@
|
|
|
1
1
|
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
describeModuleForItem,
|
|
4
|
+
displayNameWithTier,
|
|
5
|
+
formatLocation,
|
|
6
|
+
renderDescription,
|
|
7
|
+
} from '@shipload/sdk'
|
|
3
8
|
import type {CargoItem} from '../payload/codec.ts'
|
|
4
9
|
import {panel} from '../primitives/panel.ts'
|
|
5
10
|
import {iconHex} from '../primitives/icon-hex.ts'
|
|
6
11
|
import {text} from '../primitives/text.ts'
|
|
7
|
-
import {divider} from '../primitives/divider.ts'
|
|
8
12
|
import {compactRow} from '../primitives/compact-row.ts'
|
|
9
13
|
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
10
14
|
import {spanParagraph} from '../primitives/span-paragraph.ts'
|
|
11
15
|
import {tokens} from '../tokens/index.ts'
|
|
12
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
shortCode,
|
|
18
|
+
formatMass,
|
|
19
|
+
tierBorder,
|
|
20
|
+
metaRowBlock,
|
|
21
|
+
BADGE_Y,
|
|
22
|
+
HEADER_H,
|
|
23
|
+
ICON_Y,
|
|
24
|
+
META_BLOCK_GAP,
|
|
25
|
+
} from './_shared.ts'
|
|
13
26
|
|
|
14
27
|
function capabilityColor(name: string): string {
|
|
15
28
|
const key = name.toLowerCase().replace(/\s+/g, '') as keyof typeof tokens.colors.capability
|
|
@@ -18,6 +31,7 @@ function capabilityColor(name: string): string {
|
|
|
18
31
|
|
|
19
32
|
export interface RenderModuleOpts {
|
|
20
33
|
mode?: 'values' | 'ranges'
|
|
34
|
+
location?: {x: number; y: number}
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
export function renderModule(
|
|
@@ -36,9 +50,14 @@ export function renderModule(
|
|
|
36
50
|
|
|
37
51
|
const capabilityName = group?.capability ?? resolved.name.replace(/\s+T\d+$/i, '')
|
|
38
52
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
const metaRows = [
|
|
54
|
+
{label: 'Mass', value: formatMass(resolved.mass)},
|
|
55
|
+
...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const metaYStart = pad + HEADER_H
|
|
59
|
+
const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
|
|
60
|
+
const bodyYStart = metaYStart + metaH + META_BLOCK_GAP
|
|
42
61
|
|
|
43
62
|
let bodyHeight = 0
|
|
44
63
|
if (mode === 'ranges') {
|
|
@@ -65,17 +84,17 @@ export function renderModule(
|
|
|
65
84
|
bodyHeight = capHeaderH + attrsH + 8
|
|
66
85
|
}
|
|
67
86
|
|
|
68
|
-
const height =
|
|
87
|
+
const height = bodyYStart + bodyHeight + pad
|
|
69
88
|
|
|
70
89
|
const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
|
|
71
90
|
|
|
72
91
|
const quantity = Number(BigInt(item.quantity.toString()))
|
|
73
|
-
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
92
|
+
const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity})
|
|
74
93
|
|
|
75
94
|
const iconColor = group ? capabilityColor(group.capability) : capabilityColor(capabilityName)
|
|
76
95
|
const icon = iconHex({
|
|
77
96
|
x: pad,
|
|
78
|
-
y: pad +
|
|
97
|
+
y: pad + ICON_Y,
|
|
79
98
|
color: iconColor,
|
|
80
99
|
code: shortCode(resolved.itemId),
|
|
81
100
|
})
|
|
@@ -83,52 +102,18 @@ export function renderModule(
|
|
|
83
102
|
const name = text({
|
|
84
103
|
x: pad + 34,
|
|
85
104
|
y: pad + 22,
|
|
86
|
-
value: resolved
|
|
105
|
+
value: displayNameWithTier(resolved),
|
|
87
106
|
size: tokens.typography.sizes.title,
|
|
88
107
|
weight: 700,
|
|
89
108
|
family: tokens.typography.display,
|
|
90
109
|
})
|
|
91
110
|
|
|
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
111
|
let capSection = ''
|
|
127
112
|
if (mode === 'ranges') {
|
|
128
113
|
const accentColor = capabilityColor(capabilityName)
|
|
129
114
|
capSection = text({
|
|
130
115
|
x: pad,
|
|
131
|
-
y:
|
|
116
|
+
y: bodyYStart + 16,
|
|
132
117
|
value: capabilityName.toUpperCase(),
|
|
133
118
|
size: tokens.typography.sizes.subtitle,
|
|
134
119
|
weight: 700,
|
|
@@ -140,7 +125,7 @@ export function renderModule(
|
|
|
140
125
|
const accentColor = capabilityColor(group.capability)
|
|
141
126
|
const capHeader = text({
|
|
142
127
|
x: pad,
|
|
143
|
-
y:
|
|
128
|
+
y: bodyYStart + 16,
|
|
144
129
|
value: group.capability.toUpperCase(),
|
|
145
130
|
size: tokens.typography.sizes.subtitle,
|
|
146
131
|
weight: 700,
|
|
@@ -151,14 +136,14 @@ export function renderModule(
|
|
|
151
136
|
const spans = renderDescription(desc)
|
|
152
137
|
const {svg: paraSvg} = spanParagraph({
|
|
153
138
|
x: pad,
|
|
154
|
-
y:
|
|
139
|
+
y: bodyYStart + 36,
|
|
155
140
|
spans,
|
|
156
141
|
charsPerLine: 36,
|
|
157
142
|
lineHeight: 14,
|
|
158
143
|
})
|
|
159
144
|
capSection = capHeader + paraSvg
|
|
160
145
|
} else if (group && attrs.length > 0) {
|
|
161
|
-
const capY =
|
|
146
|
+
const capY = bodyYStart + 22
|
|
162
147
|
const capHeader = text({
|
|
163
148
|
x: pad,
|
|
164
149
|
y: capY,
|
|
@@ -186,7 +171,7 @@ export function renderModule(
|
|
|
186
171
|
capSection = capHeader + attrRows
|
|
187
172
|
}
|
|
188
173
|
|
|
189
|
-
const inner = `${chrome}${icon}${name}${badge}${
|
|
174
|
+
const inner = `${chrome}${icon}${name}${badge}${metaSvg}${capSection}`
|
|
190
175
|
|
|
191
176
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
192
177
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {ResolvedItem, ResolvedModuleSlot} from '@shipload/sdk'
|
|
2
|
-
import {describeModuleForSlot, renderDescription} from '@shipload/sdk'
|
|
2
|
+
import {describeModuleForSlot, displayNameWithTier, 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: `${displayNameWithTier(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,27 @@
|
|
|
1
1
|
import type {ResolvedItem} from '@shipload/sdk'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getStatDefinitions,
|
|
4
|
+
categoryColors,
|
|
5
|
+
displayNameWithTier,
|
|
6
|
+
formatLocation,
|
|
7
|
+
} from '@shipload/sdk'
|
|
3
8
|
import type {CargoItem} from '../payload/codec.ts'
|
|
4
9
|
import {panel} from '../primitives/panel.ts'
|
|
5
10
|
import {iconHex} from '../primitives/icon-hex.ts'
|
|
6
11
|
import {text} from '../primitives/text.ts'
|
|
7
|
-
import {divider} from '../primitives/divider.ts'
|
|
8
12
|
import {statBar} from '../primitives/stat-bar.ts'
|
|
9
13
|
import {quantityBadge} from '../primitives/quantity-badge.ts'
|
|
10
14
|
import {tokens} from '../tokens/index.ts'
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
import {
|
|
16
|
+
shortCode,
|
|
17
|
+
formatMass,
|
|
18
|
+
tierBorder,
|
|
19
|
+
metaRowBlock,
|
|
20
|
+
BADGE_Y,
|
|
21
|
+
HEADER_H,
|
|
22
|
+
ICON_Y,
|
|
23
|
+
META_BLOCK_GAP,
|
|
24
|
+
} from './_shared.ts'
|
|
20
25
|
|
|
21
26
|
function categoryColor(category?: string): string {
|
|
22
27
|
if (!category) return tokens.colors.text.muted
|
|
@@ -26,6 +31,7 @@ function categoryColor(category?: string): string {
|
|
|
26
31
|
|
|
27
32
|
export interface RenderResourceOpts {
|
|
28
33
|
mode?: 'values' | 'ranges'
|
|
34
|
+
location?: {x: number; y: number}
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
type StatRow = {
|
|
@@ -69,19 +75,25 @@ export function renderResource(
|
|
|
69
75
|
}))
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
const
|
|
73
|
-
|
|
78
|
+
const metaRows = [
|
|
79
|
+
{label: 'Mass', value: formatMass(resolved.mass)},
|
|
80
|
+
...(opts?.location ? [{label: 'Location', value: formatLocation(opts.location)}] : []),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
const metaYStart = pad + HEADER_H
|
|
84
|
+
const {svg: metaSvg, height: metaH} = metaRowBlock(pad, metaYStart, innerW, metaRows)
|
|
85
|
+
const statsYStart = metaYStart + metaH + META_BLOCK_GAP
|
|
74
86
|
const statsH = rows.length * 26 + 8
|
|
75
|
-
const height =
|
|
87
|
+
const height = statsYStart + statsH + pad
|
|
76
88
|
|
|
77
89
|
const chrome = panel({width: w, height, borderColor: tierBorder(resolved.tier)})
|
|
78
90
|
|
|
79
91
|
const quantity = Number(BigInt(item.quantity.toString()))
|
|
80
|
-
const badge = quantityBadge({x: w - pad, y: pad, quantity})
|
|
92
|
+
const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity})
|
|
81
93
|
|
|
82
94
|
const icon = iconHex({
|
|
83
95
|
x: pad,
|
|
84
|
-
y: pad +
|
|
96
|
+
y: pad + ICON_Y,
|
|
85
97
|
color: categoryColor(resolved.category),
|
|
86
98
|
code: shortCode(resolved.itemId),
|
|
87
99
|
})
|
|
@@ -89,52 +101,17 @@ export function renderResource(
|
|
|
89
101
|
const name = text({
|
|
90
102
|
x: pad + 34,
|
|
91
103
|
y: pad + 22,
|
|
92
|
-
value:
|
|
104
|
+
value: displayNameWithTier(resolved),
|
|
93
105
|
size: tokens.typography.sizes.title,
|
|
94
106
|
weight: 700,
|
|
95
107
|
family: tokens.typography.display,
|
|
96
108
|
})
|
|
97
109
|
|
|
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
110
|
const statsSvg = rows
|
|
134
111
|
.map((row, i) =>
|
|
135
112
|
statBar({
|
|
136
113
|
x: pad,
|
|
137
|
-
y:
|
|
114
|
+
y: statsYStart + i * 26,
|
|
138
115
|
width: innerW,
|
|
139
116
|
label: row.label,
|
|
140
117
|
abbreviation: row.abbreviation,
|
|
@@ -145,7 +122,7 @@ export function renderResource(
|
|
|
145
122
|
)
|
|
146
123
|
.join('')
|
|
147
124
|
|
|
148
|
-
const inner = `${chrome}${icon}${name}${badge}${
|
|
125
|
+
const inner = `${chrome}${icon}${name}${badge}${metaSvg}${statsSvg}`
|
|
149
126
|
|
|
150
127
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" viewBox="0 0 ${w} ${height}">${inner}</svg>`
|
|
151
128
|
}
|
|
@@ -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
|
}
|