@shipload/item-renderer 1.0.0-next.47 → 1.0.0-next.49

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipload/item-renderer",
3
- "version": "1.0.0-next.47",
3
+ "version": "1.0.0-next.49",
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.47",
48
+ "@shipload/sdk": "^1.0.0-next.49",
49
49
  "@wharfkit/antelope": "1.2.0"
50
50
  },
51
51
  "devDependencies": {
package/src/index.ts CHANGED
@@ -69,6 +69,37 @@ export type {
69
69
  EntityIconSvgOpts,
70
70
  } from './primitives/entity-icon.ts'
71
71
 
72
+ // Module icon primitive
73
+ export {
74
+ moduleIcon,
75
+ moduleIconBody,
76
+ moduleIconSlugs,
77
+ moduleIconSlugForName,
78
+ moduleIconSlugForType,
79
+ moduleIconSvg,
80
+ } from './primitives/module-icon.ts'
81
+ export type {
82
+ ModuleIconInlineOpts,
83
+ ModuleIconSlug,
84
+ ModuleIconSvgOpts,
85
+ } from './primitives/module-icon.ts'
86
+
87
+ // Station entity icon primitive
88
+ export {
89
+ stationEntityIcon,
90
+ stationEntityIconBody,
91
+ stationEntityIconKinds,
92
+ stationEntityIconKindForName,
93
+ stationEntityIconKindForPackedItemId,
94
+ stationEntityIconLabelForKind,
95
+ stationEntityIconSvg,
96
+ } from './primitives/station-entity-icon.ts'
97
+ export type {
98
+ StationEntityIconInlineOpts,
99
+ StationEntityIconKind,
100
+ StationEntityIconSvgOpts,
101
+ } from './primitives/station-entity-icon.ts'
102
+
72
103
  // Item cell templates
73
104
  export {renderItemCell, itemCellGroup, abbreviateQuantity} from './templates/item-cell.ts'
74
105
  export type {ItemCellProps, ItemCellGroupProps} from './templates/item-cell.ts'
@@ -23,6 +23,7 @@ const entityIconLabels: Record<EntityIconSlug, string> = {
23
23
 
24
24
  const entityIconNames: Record<string, EntityIconSlug> = {
25
25
  ship: 'ship',
26
+ roustabout: 'ship',
26
27
  prospector: 'ship',
27
28
  hauler: 'ship',
28
29
  container: 'container',
@@ -0,0 +1,297 @@
1
+ import {el, escapeXml} from './svg.ts'
2
+
3
+ export const moduleIconSlugs = [
4
+ 'engine',
5
+ 'generator',
6
+ 'gatherer',
7
+ 'loader',
8
+ 'warp',
9
+ 'crafter',
10
+ 'launcher',
11
+ 'storage',
12
+ 'hauler',
13
+ 'battery',
14
+ 'any',
15
+ ] as const
16
+
17
+ export type ModuleIconSlug = (typeof moduleIconSlugs)[number]
18
+
19
+ export interface ModuleIconSvgOpts {
20
+ size?: number
21
+ title?: string
22
+ className?: string
23
+ }
24
+
25
+ export interface ModuleIconInlineOpts {
26
+ x: number
27
+ y: number
28
+ size: number
29
+ }
30
+
31
+ const moduleIconLabels: Record<ModuleIconSlug, string> = {
32
+ engine: 'Engine',
33
+ generator: 'Power Core',
34
+ gatherer: 'Limpet Bay',
35
+ loader: 'Shuttle Bay',
36
+ warp: 'Warp Drive',
37
+ crafter: 'Fabricator',
38
+ launcher: 'Drive Coil',
39
+ storage: 'Cargo Hold',
40
+ hauler: 'Tractor Beam',
41
+ battery: 'Battery Bank',
42
+ any: 'Any',
43
+ }
44
+
45
+ const moduleIconNames: Record<string, ModuleIconSlug> = {
46
+ engine: 'engine',
47
+ generator: 'generator',
48
+ 'power core': 'generator',
49
+ 'power-core': 'generator',
50
+ gatherer: 'gatherer',
51
+ 'limpet bay': 'gatherer',
52
+ 'limpet-bay': 'gatherer',
53
+ loader: 'loader',
54
+ 'shuttle bay': 'loader',
55
+ 'shuttle-bay': 'loader',
56
+ warp: 'warp',
57
+ 'warp drive': 'warp',
58
+ 'warp-drive': 'warp',
59
+ crafter: 'crafter',
60
+ fabricator: 'crafter',
61
+ launcher: 'launcher',
62
+ 'drive coil': 'launcher',
63
+ 'drive-coil': 'launcher',
64
+ storage: 'storage',
65
+ 'cargo hold': 'storage',
66
+ 'cargo-hold': 'storage',
67
+ hauler: 'hauler',
68
+ 'tractor beam': 'hauler',
69
+ 'tractor-beam': 'hauler',
70
+ battery: 'battery',
71
+ 'battery bank': 'battery',
72
+ 'battery-bank': 'battery',
73
+ any: 'any',
74
+ }
75
+
76
+ function normalizeModuleName(name: string): string {
77
+ return name
78
+ .trim()
79
+ .toLowerCase()
80
+ .replace(/\s*\(?t\d+\)?$/, '')
81
+ }
82
+
83
+ export function moduleIconSlugForName(name: string): ModuleIconSlug | null {
84
+ return moduleIconNames[normalizeModuleName(name)] ?? null
85
+ }
86
+
87
+ export function moduleIconSlugForType(
88
+ moduleType: string | null | undefined
89
+ ): ModuleIconSlug | null {
90
+ return moduleType ? (moduleIconNames[normalizeModuleName(moduleType)] ?? null) : null
91
+ }
92
+
93
+ function engineIcon(): string {
94
+ return [
95
+ '<path d="M88 57 C88 38 106 28 128 28 C150 28 168 38 168 57 L168 139 C168 164 152 181 128 181 C104 181 88 164 88 139 Z" fill="#553686" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
96
+ '<path d="M98 54 C103 44 114 39 128 39 C142 39 153 44 158 54 L158 69 C148 76 108 76 98 69 Z" fill="#f1daa4" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
97
+ '<path d="M88 123 C100 136 108 141 128 141 C148 141 156 136 168 123 L168 143 C159 163 144 173 128 173 C112 173 97 163 88 143 Z" fill="#2b2342" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
98
+ '<path d="M104 139 V163 M128 142 V172 M152 139 V163" stroke="#120d1b" stroke-width="7" stroke-linecap="round"/>',
99
+ '<path d="M97 68 C112 76 144 76 159 68" fill="none" stroke="#7450a7" stroke-width="7" stroke-linecap="round"/>',
100
+ '<path d="M96 161 C94 188 108 213 121 232 L128 207 L135 232 C148 213 162 188 160 161 C149 174 138 185 128 193 C118 185 107 174 96 161 Z" fill="#39e4ef" stroke="#120d1b" stroke-width="9" stroke-linejoin="round"/>',
101
+ '<path d="M105 183 L98 198 M151 183 L158 198" fill="none" stroke="#eefcff" stroke-width="7" stroke-linecap="round"/>',
102
+ ].join('')
103
+ }
104
+
105
+ function generatorIcon(): string {
106
+ return [
107
+ '<path d="M68 63 C68 46 93 37 128 37 C163 37 188 46 188 63 L188 193 C188 210 163 219 128 219 C93 219 68 210 68 193 Z" fill="#493172" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
108
+ '<path d="M80 58 C88 49 105 44 128 44 C151 44 168 49 176 58 L176 76 C164 86 92 86 80 76 Z" fill="#2b2342" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
109
+ '<rect x="89" y="80" width="78" height="108" rx="12" fill="#35dfe9" stroke="#120d1b" stroke-width="9"/>',
110
+ '<path d="M136 98 L110 143 H130 L119 175 L148 124 H130 Z" fill="#eefcff" stroke="#35dfe9" stroke-width="4" stroke-linejoin="round"/>',
111
+ '<rect x="47" y="91" width="21" height="78" rx="8" fill="#f1daa4" stroke="#120d1b" stroke-width="8"/>',
112
+ '<rect x="188" y="91" width="21" height="78" rx="8" fill="#f1daa4" stroke="#120d1b" stroke-width="8"/>',
113
+ '<rect x="106" y="27" width="44" height="27" rx="8" fill="#f1daa4" stroke="#120d1b" stroke-width="8"/>',
114
+ '<path d="M80 174 C92 184 164 184 176 174 L176 196 C164 207 92 207 80 196 Z" fill="#2b2342" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
115
+ ].join('')
116
+ }
117
+
118
+ function gathererIcon(): string {
119
+ return [
120
+ '<path d="M48 102 L76 70 C86 59 101 61 108 72 L93 91 C86 88 77 93 70 103 L70 151 C86 171 106 181 128 181 C150 181 170 171 186 151 L186 103 C179 93 170 88 163 91 L148 72 C155 61 170 59 180 70 L208 102 L208 176 C187 201 159 214 128 214 C97 214 69 201 48 176 Z" fill="#1e61b5" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
121
+ '<path d="M68 94 L87 71 C94 62 105 65 108 74 L90 103 L90 150 L106 170 C97 177 84 175 76 165 L62 148 L62 106 Z" fill="#f1daa4" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
122
+ '<path d="M188 94 L169 71 C162 62 151 65 148 74 L166 103 L166 150 L150 170 C159 177 172 175 180 165 L194 148 L194 106 Z" fill="#f1daa4" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
123
+ '<path d="M73 160 C90 176 108 184 128 184 C148 184 166 176 183 160 L183 187 C166 202 148 210 128 210 C108 210 90 202 73 187 Z" fill="#1f6fc7" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
124
+ '<path d="M94 103 C96 82 112 72 128 72 C144 72 160 82 162 103 L162 147 C155 162 142 170 128 170 C114 170 101 162 94 147 Z" fill="#332954" stroke="#120d1b" stroke-width="9" stroke-linejoin="round"/>',
125
+ '<ellipse cx="128" cy="93" rx="30" ry="20" fill="#42e2ef" stroke="#120d1b" stroke-width="8"/>',
126
+ '<rect x="116" y="127" width="24" height="34" rx="9" fill="#3ee4ef" stroke="#120d1b" stroke-width="7"/>',
127
+ '<rect x="98" y="182" width="60" height="24" rx="8" fill="#3ee4ef" stroke="#f1daa4" stroke-width="7"/>',
128
+ ].join('')
129
+ }
130
+
131
+ function loaderIcon(): string {
132
+ return [
133
+ '<path d="M51 84 H86 L86 103 H77 V177 L106 208 H75 L48 181 V97 Z" fill="#3f3268" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
134
+ '<path d="M205 84 H170 L170 103 H179 V177 L150 208 H181 L208 181 V97 Z" fill="#3f3268" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
135
+ '<path d="M48 84 L72 55 H100 L86 84 Z" fill="#f59416" stroke="#120d1b" stroke-width="9" stroke-linejoin="round"/>',
136
+ '<path d="M208 84 L184 55 H156 L170 84 Z" fill="#f59416" stroke="#120d1b" stroke-width="9" stroke-linejoin="round"/>',
137
+ '<path d="M78 102 H98 V164 H78 Z M158 102 H178 V164 H158 Z" fill="#f1daa4" stroke="#120d1b" stroke-width="7" stroke-linejoin="round"/>',
138
+ '<path d="M93 120 L109 96 H147 L163 120 V184 H93 Z" fill="#6f696d" stroke="#120d1b" stroke-width="9" stroke-linejoin="round"/>',
139
+ '<rect x="105" y="124" width="46" height="48" fill="#302b33" stroke="#120d1b" stroke-width="7"/>',
140
+ '<path d="M111 86 H145 L153 99 V120 H103 V99 Z" fill="#9b9184" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
141
+ '<rect x="121" y="75" width="14" height="31" rx="4" fill="#f59416" stroke="#120d1b" stroke-width="6"/>',
142
+ '<rect x="75" y="151" width="32" height="35" rx="7" fill="#3f3268" stroke="#120d1b" stroke-width="8"/>',
143
+ '<rect x="149" y="151" width="32" height="35" rx="7" fill="#3f3268" stroke="#120d1b" stroke-width="8"/>',
144
+ '<rect x="84" y="161" width="14" height="20" rx="4" fill="#f59416" stroke="#120d1b" stroke-width="5"/>',
145
+ '<rect x="158" y="161" width="14" height="20" rx="4" fill="#f59416" stroke="#120d1b" stroke-width="5"/>',
146
+ '<rect x="73" y="193" width="52" height="41" rx="9" fill="#f59416" stroke="#120d1b" stroke-width="9"/>',
147
+ '<rect x="131" y="193" width="52" height="41" rx="9" fill="#f59416" stroke="#120d1b" stroke-width="9"/>',
148
+ ].join('')
149
+ }
150
+
151
+ function warpIcon(): string {
152
+ return [
153
+ '<circle cx="128" cy="128" r="94" fill="#3f3268" stroke="#120d1b" stroke-width="10"/>',
154
+ '<circle cx="128" cy="128" r="73" fill="#2b2245" stroke="#120d1b" stroke-width="7"/>',
155
+ '<rect x="108" y="17" width="40" height="22" rx="5" fill="#f59416" stroke="#120d1b" stroke-width="7"/>',
156
+ '<rect x="108" y="217" width="40" height="22" rx="5" fill="#f59416" stroke="#120d1b" stroke-width="7"/>',
157
+ '<rect x="17" y="108" width="22" height="40" rx="5" fill="#f59416" stroke="#120d1b" stroke-width="7"/>',
158
+ '<rect x="217" y="108" width="22" height="40" rx="5" fill="#f59416" stroke="#120d1b" stroke-width="7"/>',
159
+ '<path d="M79 159 C64 119 88 78 128 70 C160 64 183 79 183 104 C183 129 153 144 130 128 C113 116 119 96 143 99" fill="none" stroke="#3ee4ef" stroke-width="17" stroke-linecap="round" stroke-linejoin="round"/>',
160
+ '<path d="M177 97 C192 137 168 178 128 186 C96 192 73 177 73 152 C73 127 103 112 126 128 C143 140 137 160 113 157" fill="none" stroke="#3ee4ef" stroke-width="17" stroke-linecap="round" stroke-linejoin="round"/>',
161
+ '<circle cx="128" cy="128" r="17" fill="#2b2245" stroke="#120d1b" stroke-width="8"/>',
162
+ '<path d="M67 101 C79 74 101 58 128 56 M189 155 C177 182 155 198 128 200" fill="none" stroke="#60448e" stroke-width="7" stroke-linecap="round"/>',
163
+ ].join('')
164
+ }
165
+
166
+ function crafterIcon(): string {
167
+ return [
168
+ '<path d="M60 28 H196 L228 64 V194 L197 228 H59 L28 194 V64 Z" fill="#3f2b63" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
169
+ '<rect x="101" y="43" width="54" height="18" rx="4" fill="#39e4ef" stroke="#120d1b" stroke-width="5"/>',
170
+ '<circle cx="83" cy="77" r="25" fill="#e72a9a" stroke="#120d1b" stroke-width="8"/>',
171
+ '<circle cx="173" cy="77" r="25" fill="#e72a9a" stroke="#120d1b" stroke-width="8"/>',
172
+ '<circle cx="83" cy="77" r="10" fill="#2c2343" stroke="#120d1b" stroke-width="6"/>',
173
+ '<circle cx="173" cy="77" r="10" fill="#2c2343" stroke="#120d1b" stroke-width="6"/>',
174
+ '<path d="M65 94 H93 L83 147 L54 142 Z" fill="#e72a9a" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
175
+ '<path d="M191 94 H163 L173 147 L202 142 Z" fill="#e72a9a" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
176
+ '<path d="M59 142 L83 151 L95 176 L80 185 L65 166 L49 160 Z" fill="#e72a9a" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
177
+ '<path d="M197 142 L173 151 L161 176 L176 185 L191 166 L207 160 Z" fill="#e72a9a" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
178
+ '<path d="M128 78 L143 93 H164 V116 L148 128 L164 140 V163 H143 L128 178 L113 163 H92 V140 L108 128 L92 116 V93 H113 Z" fill="#ffcb1f" stroke="#120d1b" stroke-width="9" stroke-linejoin="round"/>',
179
+ '<circle cx="128" cy="128" r="22" fill="#2b2631" stroke="#120d1b" stroke-width="8"/>',
180
+ '<path d="M79 186 L64 203 M177 186 L192 203" stroke="#e72a9a" stroke-width="12" stroke-linecap="round"/>',
181
+ ].join('')
182
+ }
183
+
184
+ function launcherIcon(): string {
185
+ return [
186
+ '<rect x="35" y="78" width="186" height="100" rx="28" fill="#3f3268" stroke="#120d1b" stroke-width="10"/>',
187
+ '<rect x="21" y="93" width="30" height="70" rx="12" fill="#f1daa4" stroke="#120d1b" stroke-width="8"/>',
188
+ '<rect x="205" y="93" width="30" height="70" rx="12" fill="#f1daa4" stroke="#120d1b" stroke-width="8"/>',
189
+ '<path d="M61 78 C51 98 51 158 61 178 H86 C75 155 75 101 86 78 Z" fill="#2d2248" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
190
+ '<path d="M195 78 C205 98 205 158 195 178 H170 C181 155 181 101 170 78 Z" fill="#2d2248" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
191
+ '<rect x="86" y="94" width="22" height="68" rx="7" fill="#39e4ef" stroke="#120d1b" stroke-width="6"/>',
192
+ '<rect x="118" y="94" width="22" height="68" rx="7" fill="#39e4ef" stroke="#120d1b" stroke-width="6"/>',
193
+ '<rect x="150" y="94" width="22" height="68" rx="7" fill="#39e4ef" stroke="#120d1b" stroke-width="6"/>',
194
+ '<path d="M89 178 H167" stroke="#6d4c9b" stroke-width="7" stroke-linecap="round"/>',
195
+ ].join('')
196
+ }
197
+
198
+ function storageIcon(): string {
199
+ return [
200
+ '<path d="M35 53 L58 30 H198 L221 53 V198 L199 224 H57 L35 198 Z" fill="#3f3268" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
201
+ '<path d="M60 58 H196 V189 H60 Z" fill="#161321" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
202
+ '<rect x="75" y="72" width="50" height="50" rx="9" fill="#f59416" stroke="#120d1b" stroke-width="8"/>',
203
+ '<rect x="132" y="72" width="50" height="50" rx="9" fill="#f59416" stroke="#120d1b" stroke-width="8"/>',
204
+ '<rect x="75" y="132" width="50" height="50" rx="9" fill="#f59416" stroke="#120d1b" stroke-width="8"/>',
205
+ '<rect x="132" y="132" width="50" height="50" rx="9" fill="#f59416" stroke="#120d1b" stroke-width="8"/>',
206
+ '<rect x="87" y="84" width="26" height="26" fill="#ffac2e" stroke="#9a4a13" stroke-width="5"/>',
207
+ '<rect x="144" y="84" width="26" height="26" fill="#ffac2e" stroke="#9a4a13" stroke-width="5"/>',
208
+ '<rect x="87" y="144" width="26" height="26" fill="#ffac2e" stroke="#9a4a13" stroke-width="5"/>',
209
+ '<rect x="144" y="144" width="26" height="26" fill="#ffac2e" stroke="#9a4a13" stroke-width="5"/>',
210
+ '<rect x="86" y="203" width="84" height="25" rx="8" fill="#f1daa4" stroke="#120d1b" stroke-width="8"/>',
211
+ ].join('')
212
+ }
213
+
214
+ function haulerIcon(): string {
215
+ return [
216
+ '<path d="M30 83 L56 57 H105 L128 80 V176 L103 201 H43 L30 188 Z" fill="#3f3268" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
217
+ '<path d="M99 76 L124 96 V160 L99 181 L80 169 V88 Z" fill="#f1daa4" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
218
+ '<rect x="49" y="119" width="22" height="54" rx="5" fill="#39e4ef" stroke="#120d1b" stroke-width="7"/>',
219
+ '<path d="M126 105 L210 55 C218 50 227 55 227 64 V192 C227 201 218 206 210 201 L126 151 Z" fill="#35dfe9" fill-opacity=".86" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
220
+ '<path d="M141 112 L210 72 V184 L141 144 Z" fill="#54edf4" stroke="#9effff" stroke-width="5" stroke-linejoin="round"/>',
221
+ '<path d="M178 96 H216 V160 H178 Z" fill="#f59416" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
222
+ '<path d="M216 96 L237 110 V174 L216 160 Z" fill="#d76d10" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
223
+ '<path d="M178 96 L199 82 H237 L216 96 Z" fill="#ffac2e" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
224
+ ].join('')
225
+ }
226
+
227
+ function batteryIcon(): string {
228
+ return [
229
+ '<path d="M35 54 H221 V198 L199 224 H57 L35 198 Z" fill="#3f3268" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
230
+ '<path d="M48 77 H208 V188 H48 Z" fill="#15141d" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
231
+ '<rect x="37" y="48" width="182" height="25" rx="5" fill="#f1daa4" stroke="#120d1b" stroke-width="8"/>',
232
+ '<rect x="68" y="92" width="40" height="82" rx="13" fill="#35dfe9" stroke="#120d1b" stroke-width="8"/>',
233
+ '<rect x="118" y="92" width="40" height="82" rx="13" fill="#35dfe9" stroke="#120d1b" stroke-width="8"/>',
234
+ '<rect x="168" y="92" width="40" height="82" rx="13" fill="#35dfe9" stroke="#120d1b" stroke-width="8"/>',
235
+ '<path d="M78 106 V158 M128 106 V158 M178 106 V158" stroke="#9effff" stroke-width="7" stroke-linecap="round"/>',
236
+ '<path d="M54 188 L72 206 H184 L202 188" fill="none" stroke="#120d1b" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>',
237
+ ].join('')
238
+ }
239
+
240
+ function anyIcon(): string {
241
+ return [
242
+ '<path d="M49 74 L83 40 H108 L108 69 L91 86 L91 170 L108 187 L108 216 H82 L49 183 Z" fill="#3f3268" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
243
+ '<path d="M207 74 L173 40 H148 L148 69 L165 86 L165 170 L148 187 L148 216 H174 L207 183 Z" fill="#3f3268" stroke="#120d1b" stroke-width="10" stroke-linejoin="round"/>',
244
+ '<path d="M84 91 L105 70 L105 186 L84 165 Z" fill="#f1daa4" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
245
+ '<path d="M172 91 L151 70 L151 186 L172 165 Z" fill="#f1daa4" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
246
+ '<path d="M128 72 L164 128 L128 184 L92 128 Z" fill="#35dfe9" stroke="#120d1b" stroke-width="9" stroke-linejoin="round"/>',
247
+ '<path d="M128 73 L145 128 L128 183 L111 128 Z" fill="#7af4f8" stroke="#35dfe9" stroke-width="4" stroke-linejoin="round"/>',
248
+ '<path d="M95 128 H161" stroke="#9effff" stroke-width="7" stroke-linecap="round"/>',
249
+ '<path d="M101 217 L113 199 H143 L155 217 V236 H101 Z" fill="#f59416" stroke="#120d1b" stroke-width="8" stroke-linejoin="round"/>',
250
+ ].join('')
251
+ }
252
+
253
+ const iconBodies: Record<ModuleIconSlug, string> = {
254
+ engine: engineIcon(),
255
+ generator: generatorIcon(),
256
+ gatherer: gathererIcon(),
257
+ loader: loaderIcon(),
258
+ warp: warpIcon(),
259
+ crafter: crafterIcon(),
260
+ launcher: launcherIcon(),
261
+ storage: storageIcon(),
262
+ hauler: haulerIcon(),
263
+ battery: batteryIcon(),
264
+ any: anyIcon(),
265
+ }
266
+
267
+ export function moduleIconBody(slug: ModuleIconSlug): string {
268
+ return iconBodies[slug]
269
+ }
270
+
271
+ export function moduleIcon(slug: ModuleIconSlug, opts: ModuleIconInlineOpts): string {
272
+ const scale = opts.size / 256
273
+ return el(
274
+ 'g',
275
+ {transform: `translate(${opts.x} ${opts.y}) scale(${scale})`, 'data-module': slug},
276
+ moduleIconBody(slug)
277
+ )
278
+ }
279
+
280
+ export function moduleIconSvg(slug: ModuleIconSlug, opts: ModuleIconSvgOpts = {}): string {
281
+ const size = opts.size ?? 64
282
+ const title = opts.title ?? `${moduleIconLabels[slug]} module icon`
283
+ const children = `<title>${escapeXml(title)}</title>${moduleIconBody(slug)}`
284
+ return el(
285
+ 'svg',
286
+ {
287
+ xmlns: 'http://www.w3.org/2000/svg',
288
+ width: size,
289
+ height: size,
290
+ viewBox: '0 0 256 256',
291
+ role: 'img',
292
+ class: opts.className,
293
+ 'aria-label': title,
294
+ },
295
+ children
296
+ )
297
+ }
@@ -0,0 +1,261 @@
1
+ import {getPackedEntityType} from '@shipload/sdk'
2
+ import {el, escapeXml} from './svg.ts'
3
+
4
+ export const stationEntityIconKinds = [
5
+ 'hub',
6
+ 'warehouse',
7
+ 'extractor',
8
+ 'factory',
9
+ 'mdriver',
10
+ 'mcatcher',
11
+ ] as const
12
+
13
+ export type StationEntityIconKind = (typeof stationEntityIconKinds)[number]
14
+
15
+ export interface StationEntityIconSvgOpts {
16
+ size?: number
17
+ title?: string
18
+ className?: string
19
+ }
20
+
21
+ export interface StationEntityIconInlineOpts {
22
+ x: number
23
+ y: number
24
+ size: number
25
+ }
26
+
27
+ const stationEntityIconLabels: Record<StationEntityIconKind, string> = {
28
+ hub: 'Station Hub',
29
+ warehouse: 'Warehouse',
30
+ extractor: 'Extractor',
31
+ factory: 'Factory',
32
+ mdriver: 'Mass Driver',
33
+ mcatcher: 'Mass Catcher',
34
+ }
35
+
36
+ const stationEntityIconNames: Record<string, StationEntityIconKind> = {
37
+ hub: 'hub',
38
+ 'station hub': 'hub',
39
+ warehouse: 'warehouse',
40
+ extractor: 'extractor',
41
+ 'mining rig': 'extractor',
42
+ factory: 'factory',
43
+ mdriver: 'mdriver',
44
+ 'meteor driver': 'mdriver',
45
+ 'mass driver': 'mdriver',
46
+ mcatcher: 'mcatcher',
47
+ 'meteor catcher': 'mcatcher',
48
+ 'mass catcher': 'mcatcher',
49
+ }
50
+
51
+ const kindSet = new Set<string>(stationEntityIconKinds)
52
+
53
+ function normalizeName(name: string): string {
54
+ return name
55
+ .trim()
56
+ .toLowerCase()
57
+ .replace(/\s*\(?t\d+\)?$/, '')
58
+ }
59
+
60
+ export function stationEntityIconLabelForKind(kind: StationEntityIconKind): string {
61
+ return stationEntityIconLabels[kind]
62
+ }
63
+
64
+ export function stationEntityIconKindForName(name: string): StationEntityIconKind | null {
65
+ return stationEntityIconNames[normalizeName(name)] ?? null
66
+ }
67
+
68
+ export function stationEntityIconKindForPackedItemId(
69
+ packedItemId: number
70
+ ): StationEntityIconKind | null {
71
+ const kind = getPackedEntityType(packedItemId)?.toString()
72
+ return kind && kindSet.has(kind) ? (kind as StationEntityIconKind) : null
73
+ }
74
+
75
+ const OUTLINE = '#070712'
76
+
77
+ function wrap(children: string): string {
78
+ return el(
79
+ 'g',
80
+ {
81
+ fill: 'none',
82
+ stroke: OUTLINE,
83
+ 'stroke-width': 8,
84
+ 'stroke-linejoin': 'round',
85
+ 'stroke-linecap': 'round',
86
+ },
87
+ children
88
+ )
89
+ }
90
+
91
+ function hubIcon(): string {
92
+ return wrap(
93
+ [
94
+ '<path d="M128 18l36 22h42l10 10v42l22 36-22 36v42l-10 10h-42l-36 22-36-22H50l-10-10v-42l-22-36 22-36V50l10-10h42z" fill="#f7e8a8"/>',
95
+ '<path d="M128 31l31 20h36l8 8v36l20 33-20 33v36l-8 8h-36l-31 20-31-20H61l-8-8v-36l-20-33 20-33V59l8-8h36z" fill="#382f55"/>',
96
+ '<path d="M83 63h90l20 20v90l-20 20H83l-20-20V83z" fill="#4d406d"/>',
97
+ '<path d="M128 51l29 22 35 6-7 36 16 33-33 17-17 33-23-26-23 26-17-33-33-17 16-33-7-36 35-6z" fill="#312949"/>',
98
+ '<circle cx="128" cy="128" r="57" fill="#f8e7a8"/>',
99
+ '<circle cx="128" cy="128" r="43" fill="#dba92e"/>',
100
+ '<circle cx="128" cy="128" r="31" fill="#f3c53a" stroke-width="5"/>',
101
+ '<path d="M128 82l11 32 32-8-24 24 23 25-31-9-11 32-11-32-31 9 23-25-24-24 32 8z" fill="#fff7b8"/>',
102
+ '<path d="M104 42h48v19h-48zM104 195h48v19h-48zM42 104h19v48H42zM195 104h19v48h-19z" fill="#f2d784"/>',
103
+ '<path d="M113 50h30M113 204h30M50 113v30M204 113v30" stroke="#ffca22" stroke-width="7"/>',
104
+ '<path d="M80 80l11 11M176 80l-11 11M80 176l11-11M176 176l-11-11" stroke="#ffca22" stroke-width="7"/>',
105
+ ].join('')
106
+ )
107
+ }
108
+
109
+ function warehouseIcon(): string {
110
+ return wrap(
111
+ [
112
+ '<path d="M48 30h160l18 18v160l-18 18H48l-18-18V48z" fill="#f7e8a8"/>',
113
+ '<path d="M58 40h140l16 16v144l-16 16H58l-16-16V56z" fill="#362d53"/>',
114
+ '<path d="M77 59h42l10 10v54l-10 10H77l-10-10V69z" fill="#f28a15"/>',
115
+ '<path d="M137 59h42l10 10v54l-10 10h-42l-10-10V69z" fill="#f28a15"/>',
116
+ '<path d="M60 140h136l11 11v47l-11 11H60l-11-11v-47z" fill="#f28a15"/>',
117
+ '<path d="M67 151h122M67 165h122M67 179h122M67 193h122" stroke="#9b4d0f" stroke-width="5"/>',
118
+ '<path d="M128 44v95" stroke="#211c35" stroke-width="15"/>',
119
+ '<path d="M113 51h30M113 119h30M50 83v45M206 83v45M81 216h35M140 216h35" stroke="#ffb02d" stroke-width="7"/>',
120
+ '<path d="M91 79v35M108 79v35M151 79v35M168 79v35" stroke="#8c4510" stroke-width="6"/>',
121
+ '<path d="M75 126h106M75 139h106" stroke="#f7e8a8" stroke-width="7"/>',
122
+ '<path d="M87 151l-16 16M179 151l-16 16" stroke="#fff2bc" stroke-width="5"/>',
123
+ ].join('')
124
+ )
125
+ }
126
+
127
+ function extractorIcon(): string {
128
+ return wrap(
129
+ [
130
+ '<path d="M51 29h154l21 21v154l-21 21H51l-21-21V50z" fill="#f7e8a8"/>',
131
+ '<path d="M62 39h132l20 20v136l-20 20H62l-20-20V59z" fill="#332b50"/>',
132
+ '<path d="M72 63h51l10 10v82l-10 10H72l-10-10V73z" fill="#237e75"/>',
133
+ '<path d="M133 63h51l10 10v82l-10 10h-51l-10-10V73z" fill="#2b9389"/>',
134
+ '<path d="M70 92h55M70 122h55M133 92h55M133 122h55" stroke="#185b55" stroke-width="7"/>',
135
+ '<path d="M128 50v119" stroke="#211b35" stroke-width="14"/>',
136
+ '<path d="M74 54h108M65 164h126" stroke="#f7e8a8" stroke-width="7"/>',
137
+ '<path d="M60 36h136M60 220h136" stroke="#4b3f6c" stroke-width="12"/>',
138
+ '<path d="M31 88h37v71H31zM188 88h37v71h-37z" fill="#342d50"/>',
139
+ '<path d="M45 101h14v45H45zM197 101h14v45h-14z" fill="#23a89b"/>',
140
+ '<circle cx="128" cy="183" r="40" fill="#332b50"/>',
141
+ '<circle cx="128" cy="183" r="27" fill="#18a99d"/>',
142
+ '<circle cx="128" cy="183" r="16" fill="#14746d" stroke-width="4"/>',
143
+ '<path d="M101 161l12 12M155 161l-12 12M101 205l12-12M155 205l-12-12" stroke="#f7e8a8" stroke-width="7"/>',
144
+ '<path d="M114 38h28M119 127v31" stroke="#5ce9dc" stroke-width="6"/>',
145
+ ].join('')
146
+ )
147
+ }
148
+
149
+ function factoryIcon(): string {
150
+ return wrap(
151
+ [
152
+ '<circle cx="128" cy="128" r="104" fill="#f7e8a8"/>',
153
+ '<circle cx="128" cy="128" r="93" fill="#332b50"/>',
154
+ '<path d="M77 37h102l25 25v132l-25 25H77l-25-25V62z" fill="#433861"/>',
155
+ '<circle cx="128" cy="128" r="72" fill="#2b2541"/>',
156
+ '<path d="M128 72l13 19 23-2 3 24 21 13-13 21 8 22-24 8-10 22-21-13-21 13-10-22-24-8 8-22-13-21 21-13 3-24 23 2z" fill="#f6c51f"/>',
157
+ '<circle cx="128" cy="128" r="24" fill="#49405f"/>',
158
+ '<path d="M72 80h112l14 16v21H58V96zM58 158h140v21l-14 16H72z" fill="#e72c9a"/>',
159
+ '<path d="M58 91l-21 28v52l21 28M198 91l21 28v52l-21 28" stroke="#332b50" stroke-width="17"/>',
160
+ '<path d="M78 84l-30 65M178 84l30 65" stroke="#e72c9a" stroke-width="18"/>',
161
+ '<circle cx="55" cy="80" r="21" fill="#e72c9a"/>',
162
+ '<circle cx="201" cy="80" r="21" fill="#e72c9a"/>',
163
+ '<circle cx="55" cy="80" r="11" fill="#ffc51d"/>',
164
+ '<circle cx="201" cy="80" r="11" fill="#ffc51d"/>',
165
+ '<path d="M50 126l-9 22M206 126l9 22" stroke="#332b50" stroke-width="7"/>',
166
+ '<path d="M83 93l-15 15M95 93l-15 15M161 170l-15 15M173 170l-15 15" stroke="#ffe5a4" stroke-width="5"/>',
167
+ ].join('')
168
+ )
169
+ }
170
+
171
+ function mdriverIcon(): string {
172
+ return wrap(
173
+ [
174
+ '<path d="M48 28h160l20 20v160l-20 20H48l-20-20V48z" fill="#f7e8a8"/>',
175
+ '<path d="M59 39h138l18 18v142l-18 18H59l-18-18V57z" fill="#332b50"/>',
176
+ '<circle cx="128" cy="128" r="54" fill="#2a2342"/>',
177
+ '<circle cx="128" cy="128" r="27" fill="#18cfe5"/>',
178
+ '<path d="M52 63h42M162 193h42M52 193h42M162 63h42" stroke="#f7e8a8" stroke-width="7"/>',
179
+ '<path d="M31 128h194" stroke="#19162c" stroke-width="48"/>',
180
+ '<path d="M40 128h176" stroke="#20d8ec" stroke-width="30"/>',
181
+ '<path d="M56 128h144" stroke="#75eff8" stroke-width="13"/>',
182
+ '<path d="M38 128l42-42M176 170l42-42" stroke="#19162c" stroke-width="34"/>',
183
+ '<path d="M50 116l58-58M148 198l58-58" stroke="#332b50" stroke-width="26"/>',
184
+ '<path d="M84 94l89 89M82 162l89-89" stroke="#f7e8a8" stroke-width="9"/>',
185
+ '<path d="M96 102l64 64M96 154l64-64" stroke="#1f1a31" stroke-width="9"/>',
186
+ '<path d="M97 43h62M97 213h62M43 97v62M213 97v62" stroke="#20d8ec" stroke-width="7"/>',
187
+ '<path d="M70 68l-17 17M186 188l17-17" stroke="#ffffff" stroke-opacity=".65" stroke-width="5"/>',
188
+ ].join('')
189
+ )
190
+ }
191
+
192
+ function mcatcherIcon(): string {
193
+ return wrap(
194
+ [
195
+ '<path d="M38 61h180l-17 129-45 39H100l-45-39z" fill="#f7e8a8"/>',
196
+ '<path d="M49 72h158l-17 108-39 35h-46l-39-35z" fill="#332b50"/>',
197
+ '<path d="M61 73c7 58 36 88 67 88s60-30 67-88z" fill="#36ddeb"/>',
198
+ '<path d="M72 83c8 43 29 63 56 63s48-20 56-63" stroke="#90f6ff" stroke-width="10"/>',
199
+ '<path d="M65 64c24-29 102-29 126 0l-12 22c-26-18-76-18-102 0z" fill="#f7e8a8"/>',
200
+ '<path d="M74 76c21-19 87-19 108 0" stroke="#fff4bd" stroke-width="9"/>',
201
+ '<path d="M43 78l-18 55 24 67 40 29M213 78l18 55-24 67-40 29" stroke="#332b50" stroke-width="19"/>',
202
+ '<path d="M58 82v59M198 82v59" stroke="#8b45de" stroke-width="21"/>',
203
+ '<path d="M75 162l31 53M181 162l-31 53" stroke="#8b45de" stroke-width="25"/>',
204
+ '<path d="M109 174h38l12 23-12 32h-38l-12-32z" fill="#352a55"/>',
205
+ '<path d="M116 183h24v18h-24z" fill="#35ddeb"/>',
206
+ '<path d="M128 160v54" stroke="#35ddeb" stroke-width="8"/>',
207
+ '<path d="M79 169l12 11M177 169l-12 11" stroke="#58edf6" stroke-width="6"/>',
208
+ '<path d="M87 104c22 17 60 17 82 0" stroke="#bcfbff" stroke-opacity=".75" stroke-width="5"/>',
209
+ ].join('')
210
+ )
211
+ }
212
+
213
+ const iconBodies: Record<StationEntityIconKind, string> = {
214
+ hub: hubIcon(),
215
+ warehouse: warehouseIcon(),
216
+ extractor: extractorIcon(),
217
+ factory: factoryIcon(),
218
+ mdriver: mdriverIcon(),
219
+ mcatcher: mcatcherIcon(),
220
+ }
221
+
222
+ export function stationEntityIconBody(kind: StationEntityIconKind): string {
223
+ return iconBodies[kind]
224
+ }
225
+
226
+ export function stationEntityIcon(
227
+ kind: StationEntityIconKind,
228
+ opts: StationEntityIconInlineOpts
229
+ ): string {
230
+ const scale = opts.size / 256
231
+ return el(
232
+ 'g',
233
+ {
234
+ transform: `translate(${opts.x} ${opts.y}) scale(${scale})`,
235
+ 'data-station-entity': kind,
236
+ },
237
+ stationEntityIconBody(kind)
238
+ )
239
+ }
240
+
241
+ export function stationEntityIconSvg(
242
+ kind: StationEntityIconKind,
243
+ opts: StationEntityIconSvgOpts = {}
244
+ ): string {
245
+ const size = opts.size ?? 64
246
+ const title = opts.title ?? `${stationEntityIconLabels[kind]} entity icon`
247
+ const children = `<title>${escapeXml(title)}</title>${stationEntityIconBody(kind)}`
248
+ return el(
249
+ 'svg',
250
+ {
251
+ xmlns: 'http://www.w3.org/2000/svg',
252
+ width: size,
253
+ height: size,
254
+ viewBox: '0 0 256 256',
255
+ role: 'img',
256
+ class: opts.className,
257
+ 'aria-label': title,
258
+ },
259
+ children
260
+ )
261
+ }
@@ -4,7 +4,9 @@ import {el} from '../primitives/svg.ts'
4
4
  import {text} from '../primitives/text.ts'
5
5
  import {componentIcon, componentIconSlugForName} from '../primitives/component-icon.ts'
6
6
  import {entityIcon, entityIconSlugForName} from '../primitives/entity-icon.ts'
7
+ import {moduleIcon, moduleIconSlugForName} from '../primitives/module-icon.ts'
7
8
  import {resourceIcon} from '../primitives/resource-icon.ts'
9
+ import {stationEntityIcon, stationEntityIconKindForName} from '../primitives/station-entity-icon.ts'
8
10
  import {tokens} from '../tokens/index.ts'
9
11
 
10
12
  export interface ItemCellProps {
@@ -63,8 +65,14 @@ function cellInner(props: ItemCellProps): string {
63
65
  props.resolved.itemType === 'component'
64
66
  ? componentIconSlugForName(props.resolved.name)
65
67
  : null
68
+ const stationEntityKind =
69
+ props.resolved.itemType === 'entity'
70
+ ? stationEntityIconKindForName(props.resolved.name)
71
+ : null
66
72
  const entitySlug =
67
73
  props.resolved.itemType === 'entity' ? entityIconSlugForName(props.resolved.name) : null
74
+ const moduleSlug =
75
+ props.resolved.itemType === 'module' ? moduleIconSlugForName(props.resolved.name) : null
68
76
 
69
77
  if (props.iconImageHref) {
70
78
  const iconSize = Math.round(size * (showQuantity ? 0.72 : 0.9))
@@ -85,6 +93,14 @@ function cellInner(props: ItemCellProps): string {
85
93
  y: iconY,
86
94
  size: iconSize,
87
95
  })
96
+ } else if (stationEntityKind) {
97
+ const iconSize = Math.round(size * (showQuantity ? 0.66 : 0.84))
98
+ const iconY = showQuantity ? Math.round(size * 0.12) : Math.round(height / 2 - iconSize / 2)
99
+ content = stationEntityIcon(stationEntityKind, {
100
+ x: (size - iconSize) / 2,
101
+ y: iconY,
102
+ size: iconSize,
103
+ })
88
104
  } else if (entitySlug) {
89
105
  const iconSize = Math.round(size * (showQuantity ? 0.66 : 0.84))
90
106
  const iconY = showQuantity ? Math.round(size * 0.12) : Math.round(height / 2 - iconSize / 2)
@@ -93,6 +109,14 @@ function cellInner(props: ItemCellProps): string {
93
109
  y: iconY,
94
110
  size: iconSize,
95
111
  })
112
+ } else if (moduleSlug) {
113
+ const iconSize = Math.round(size * (showQuantity ? 0.66 : 0.84))
114
+ const iconY = showQuantity ? Math.round(size * 0.12) : Math.round(height / 2 - iconSize / 2)
115
+ content = moduleIcon(moduleSlug, {
116
+ x: (size - iconSize) / 2,
117
+ y: iconY,
118
+ size: iconSize,
119
+ })
96
120
  } else if (props.resolved.abbreviation) {
97
121
  content = showQuantity
98
122
  ? text({
@@ -3,6 +3,7 @@ import {describeModuleForItem, formatLocation, renderDescription} from '@shiploa
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
+ import {moduleIcon, moduleIconSlugForName} from '../primitives/module-icon.ts'
6
7
  import {text} from '../primitives/text.ts'
7
8
  import {quantityBadge} from '../primitives/quantity-badge.ts'
8
9
  import {spanParagraph} from '../primitives/span-paragraph.ts'
@@ -91,12 +92,15 @@ export function renderModule(
91
92
 
92
93
  const badge = quantityBadge({x: w - pad, y: pad + BADGE_Y, quantity, tone: iconColor})
93
94
 
94
- const icon = iconHex({
95
- x: pad,
96
- y: pad + ICON_Y,
97
- color: iconColor,
98
- code: shortCode(resolved.itemId),
99
- })
95
+ const moduleSlug = moduleIconSlugForName(resolved.name)
96
+ const icon = moduleSlug
97
+ ? moduleIcon(moduleSlug, {x: pad, y: pad + ICON_Y - 2, size: 28})
98
+ : iconHex({
99
+ x: pad,
100
+ y: pad + ICON_Y,
101
+ color: iconColor,
102
+ code: shortCode(resolved.itemId),
103
+ })
100
104
 
101
105
  const name = titleText(pad + 34, pad + 22, resolved)
102
106