TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import * as React from "react";2import type { SymbolStyle, SymbolTier } from "@spinza/game-core/client";34/* ------------------------------------------------------------------------5 SymbolSvg — the exact same procedural symbol artwork the PixiJS renderer6 draws on the reels (`components/game/symbol-art.ts`), ported to SVG so7 lobby posters show the symbols the player will actually meet in-game.8 Geometry, colours, highlights and glow follow symbol-art.ts 1:1; `size` is9 the tile edge, the glyph is centred on (x, y).10 ------------------------------------------------------------------------ */1112const INK = "#0b0d14";13const FONT = "var(--font-geist-sans), Geist, Inter, ui-sans-serif, system-ui, sans-serif";1415function clamp8(n: number): number {16 return Math.max(0, Math.min(255, n));17}1819function parseHex(c: string): [number, number, number] {20 const h = c.replace("#", "");21 const full = h.length === 3 ? h.split("").map((ch) => ch + ch).join("") : h;22 const n = parseInt(full, 16);23 return [(n >> 16) & 255, (n >> 8) & 255, n & 255];24}2526function toHex(r: number, g: number, b: number): string {27 return `#${((clamp8(r) << 16) | (clamp8(g) << 8) | clamp8(b)).toString(16).padStart(6, "0")}`;28}2930/** Same arithmetic as symbol-art.ts `lighten` (adds 255*amt per channel). */31export function lightenHex(c: string, amt: number): string {32 const [r, g, b] = parseHex(c);33 const d = Math.round(255 * amt);34 return toHex(r + d, g + d, b + d);35}3637/** Same arithmetic as symbol-art.ts `darken`. */38export function darkenHex(c: string, amt: number): string {39 const [r, g, b] = parseHex(c);40 const d = Math.round(255 * amt);41 return toHex(r - d, g - d, b - d);42}4344const f = (n: number) => Math.round(n * 100) / 100;4546/** Flat [x0,y0,x1,y1,…] list → SVG points string. */47function pts(list: number[]): string {48 const out: string[] = [];49 for (let i = 0; i < list.length; i += 2) out.push(`${f(list[i])},${f(list[i + 1])}`);50 return out.join(" ");51}5253function polygonPts(cx: number, cy: number, r: number, sides: number, rot = -Math.PI / 2): string {54 const list: number[] = [];55 for (let i = 0; i < sides; i++) {56 const a = rot + (i * Math.PI * 2) / sides;57 list.push(cx + Math.cos(a) * r, cy + Math.sin(a) * r);58 }59 return pts(list);60}6162function starPts(cx: number, cy: number, outer: number, inner: number, points = 5): string {63 const list: number[] = [];64 for (let i = 0; i < points * 2; i++) {65 const r = i % 2 === 0 ? outer : inner;66 const a = -Math.PI / 2 + (i * Math.PI) / points;67 list.push(cx + Math.cos(a) * r, cy + Math.sin(a) * r);68 }69 return pts(list);70}7172function arcPath(cx: number, cy: number, r: number, a0: number, a1: number): string {73 const x0 = cx + Math.cos(a0) * r;74 const y0 = cy + Math.sin(a0) * r;75 const x1 = cx + Math.cos(a1) * r;76 const y1 = cy + Math.sin(a1) * r;77 const large = Math.abs(a1 - a0) > Math.PI ? 1 : 0;78 const sweep = a1 > a0 ? 1 : 0;79 return `M${f(x0)} ${f(y0)} A${f(r)} ${f(r)} 0 ${large} ${sweep} ${f(x1)} ${f(y1)}`;80}8182export { pts as svgPoints, polygonPts as svgPolygon, starPts as svgStar, arcPath as svgArc };8384function Label({ text, fontSize, color, bold = true }: { text: string; fontSize: number; color: string; bold?: boolean }) {85 return (86 <text x={0} y={0} textAnchor="middle" dominantBaseline="central" fontSize={f(fontSize)} fontWeight={bold ? 800 : 600} fill={color} style={{ fontFamily: FONT, letterSpacing: 1 }}>87 {text}88 </text>89 );90}9192/* ------------------------------------------------------------------ glyph */9394function Glyph({ style, size }: { style: SymbolStyle; size: number }) {95 const main = style.color;96 const accent = style.accent ?? lightenHex(style.color, 0.35);97 const r = size * 0.36;98 const sw = (k: number) => f(size * k);99100 switch (style.shape) {101 case "gem":102 return (103 <>104 <polygon points={pts([0, -r, r * 0.85, -r * 0.25, r * 0.55, r, -r * 0.55, r, -r * 0.85, -r * 0.25])} fill={main} />105 <polygon points={pts([0, -r, r * 0.85, -r * 0.25, 0, -r * 0.1, -r * 0.85, -r * 0.25])} fill={accent} fillOpacity={0.85} />106 <polygon points={pts([0, -r * 0.1, r * 0.85, -r * 0.25, r * 0.55, r])} fill={darkenHex(main, 0.15)} />107 <polygon points={pts([-r * 0.2, -r * 0.7, r * 0.15, -r * 0.55, -r * 0.35, -r * 0.35])} fill="#fff" fillOpacity={0.65} />108 </>109 );110 case "hex":111 return (112 <>113 <polygon points={polygonPts(0, 0, r, 6)} fill={main} />114 <polygon points={polygonPts(0, 0, r * 0.68, 6)} fill={darkenHex(main, 0.25)} />115 <polygon points={polygonPts(0, 0, r * 0.42, 6)} fill={accent} />116 {style.label ? <Label text={style.label} fontSize={size * 0.26} color={INK} /> : null}117 </>118 );119 case "star":120 return (121 <>122 <polygon points={starPts(0, 0, r, r * 0.45)} fill={main} />123 <polygon points={starPts(0, 0, r * 0.55, r * 0.25)} fill="#fff" fillOpacity={0.85} />124 </>125 );126 case "circle":127 return (128 <>129 <circle r={f(r)} fill={main} stroke={accent} strokeWidth={sw(0.02)} />130 <circle cx={f(-r * 0.3)} cy={f(-r * 0.3)} r={f(r * 0.35)} fill="#fff" fillOpacity={0.35} />131 </>132 );133 case "diamond":134 return (135 <>136 <polygon points={pts([0, -r, r, 0, 0, r, -r, 0])} fill={main} />137 <polygon points={pts([0, -r, r, 0, 0, 0])} fill={accent} fillOpacity={0.8} />138 <polygon points={pts([0, 0, 0, r, -r, 0])} fill={darkenHex(main, 0.2)} />139 </>140 );141 case "shield":142 return (143 <>144 <polygon points={pts([0, -r, r * 0.85, -r * 0.65, r * 0.75, r * 0.15, 0, r, -r * 0.75, r * 0.15, -r * 0.85, -r * 0.65])} fill={main} />145 <polygon points={pts([0, -r * 0.7, r * 0.5, -r * 0.5, r * 0.45, r * 0.05, 0, r * 0.6, -r * 0.45, r * 0.05, -r * 0.5, -r * 0.5])} fill={darkenHex(main, 0.28)} />146 <path d={`M${f(-r * 0.08)} ${f(-r * 0.5)}h${f(r * 0.16)}v${f(r * 0.9)}h${f(-r * 0.16)}z M${f(-r * 0.38)} ${f(-r * 0.25)}h${f(r * 0.76)}v${f(r * 0.16)}h${f(-r * 0.76)}z`} fill={accent} />147 </>148 );149 case "bolt":150 return (151 <>152 <polygon points={pts([r * 0.15, -r, -r * 0.55, r * 0.1, -r * 0.05, r * 0.1, -r * 0.25, r, r * 0.6, -r * 0.15, r * 0.05, -r * 0.15])} fill={main} />153 <polygon points={pts([r * 0.1, -r * 0.8, -r * 0.3, 0, 0, 0])} fill="#fff" fillOpacity={0.5} />154 </>155 );156 case "ring":157 return (158 <>159 <circle r={f(r * 0.81)} fill="none" stroke={main} strokeWidth={f(r * 0.38)} />160 <circle r={f(r * 0.62)} fill="none" stroke={accent} strokeWidth={sw(0.02)} strokeOpacity={0.9} />161 <path d={arcPath(0, 0, r * 0.81, -Math.PI * 0.9, -Math.PI * 0.3)} fill="none" stroke="#fff" strokeWidth={sw(0.04)} strokeOpacity={0.55} />162 </>163 );164 case "chip": {165 let pins = "";166 for (let i = -2; i <= 2; i++) {167 const y = f(i * r * 0.28 - r * 0.06);168 pins += `M${f(-r - r * 0.18)} ${y}h${f(r * 0.18)}v${f(r * 0.12)}h${f(-r * 0.18)}z M${f(r)} ${y}h${f(r * 0.18)}v${f(r * 0.12)}h${f(-r * 0.18)}z `;169 }170 return (171 <>172 <rect x={f(-r)} y={f(-r * 0.75)} width={f(r * 2)} height={f(r * 1.5)} rx={f(r * 0.25)} fill={main} />173 <rect x={f(-r * 0.75)} y={f(-r * 0.5)} width={f(r * 1.5)} height={f(r)} rx={f(r * 0.15)} fill={darkenHex(main, 0.35)} />174 <path d={pins} fill={accent} />175 {style.label ? <Label text={style.label} fontSize={size * 0.22} color={accent} /> : null}176 </>177 );178 }179 case "crystal":180 return (181 <>182 <polygon points={pts([0, -r, r * 0.5, -r * 0.3, r * 0.35, r * 0.8, -r * 0.35, r * 0.8, -r * 0.5, -r * 0.3])} fill={main} />183 <polygon points={pts([0, -r, r * 0.5, -r * 0.3, 0, r * 0.1])} fill={accent} fillOpacity={0.75} />184 <polygon points={pts([0, r * 0.1, r * 0.35, r * 0.8, -r * 0.35, r * 0.8])} fill={darkenHex(main, 0.2)} />185 </>186 );187 case "letter":188 return (189 <>190 <rect x={f(-r * 0.95)} y={f(-r * 0.95)} width={f(r * 1.9)} height={f(r * 1.9)} rx={f(r * 0.3)} fill={darkenHex(main, 0.45)} fillOpacity={0.9} stroke={main} strokeWidth={sw(0.025)} />191 <Label text={style.label ?? "?"} fontSize={size * 0.4} color={main} />192 </>193 );194 case "vault": {195 let spokes = "";196 for (let i = 0; i < 6; i++) {197 const a = (i * Math.PI) / 3;198 spokes += `M${f(Math.cos(a) * r * 0.25)} ${f(Math.sin(a) * r * 0.25)}L${f(Math.cos(a) * r * 0.62)} ${f(Math.sin(a) * r * 0.62)} `;199 }200 return (201 <>202 <circle r={f(r)} fill={darkenHex(main, 0.4)} stroke={main} strokeWidth={sw(0.03)} />203 <circle r={f(r * 0.7)} fill="none" stroke={main} strokeWidth={sw(0.02)} strokeOpacity={0.7} />204 <path d={spokes} stroke={accent} strokeWidth={sw(0.035)} fill="none" />205 <circle r={f(r * 0.22)} fill={accent} />206 </>207 );208 }209 case "orb":210 return (211 <>212 <circle r={f(r)} fill={main} stroke={accent} strokeWidth={sw(0.02)} strokeOpacity={0.9} />213 <circle r={f(r * 0.75)} fill={accent} fillOpacity={0.35} />214 <circle cx={f(-r * 0.25)} cy={f(-r * 0.3)} r={f(r * 0.25)} fill="#fff" fillOpacity={0.5} />215 {style.label ? <Label text={style.label} fontSize={size * 0.36} color={accent} /> : null}216 </>217 );218 case "coin":219 return (220 <>221 <circle r={f(r)} fill={main} stroke={accent} strokeWidth={sw(0.02)} />222 <circle r={f(r * 0.8)} fill="none" stroke={darkenHex(main, 0.25)} strokeWidth={sw(0.03)} />223 <Label text={style.label ?? "SC"} fontSize={size * 0.24} color={darkenHex(main, 0.5)} />224 </>225 );226 case "skull":227 return (228 <>229 <circle cy={f(-r * 0.15)} r={f(r * 0.8)} fill={main} />230 <rect x={f(-r * 0.45)} y={f(r * 0.3)} width={f(r * 0.9)} height={f(r * 0.55)} rx={f(r * 0.15)} fill={main} />231 <circle cx={f(-r * 0.3)} cy={f(-r * 0.2)} r={f(r * 0.22)} fill={INK} />232 <circle cx={f(r * 0.3)} cy={f(-r * 0.2)} r={f(r * 0.22)} fill={INK} />233 <polygon points={pts([0, r * 0.05, r * 0.12, r * 0.3, -r * 0.12, r * 0.3])} fill={INK} />234 </>235 );236 case "flame":237 return (238 <>239 <polygon points={pts([0, -r, r * 0.55, -r * 0.2, r * 0.7, r * 0.45, r * 0.3, r, -r * 0.3, r, -r * 0.7, r * 0.45, -r * 0.55, -r * 0.2])} fill={main} />240 <polygon points={pts([0, -r * 0.4, r * 0.3, r * 0.1, r * 0.25, r * 0.6, -r * 0.25, r * 0.6, -r * 0.3, r * 0.1])} fill={accent} />241 </>242 );243 case "snow": {244 let axes = "";245 let branches = "";246 for (let i = 0; i < 3; i++) {247 const a = (i * Math.PI) / 3;248 axes += `M${f(Math.cos(a) * r)} ${f(Math.sin(a) * r)}L${f(-Math.cos(a) * r)} ${f(-Math.sin(a) * r)} `;249 for (const s of [1, -1]) {250 const bx = Math.cos(a) * r * 0.6 * s;251 const by = Math.sin(a) * r * 0.6 * s;252 branches += `M${f(bx)} ${f(by)}L${f(bx + Math.cos(a + 0.6) * r * 0.3 * s)} ${f(by + Math.sin(a + 0.6) * r * 0.3 * s)} `;253 branches += `M${f(bx)} ${f(by)}L${f(bx + Math.cos(a - 0.6) * r * 0.3 * s)} ${f(by + Math.sin(a - 0.6) * r * 0.3 * s)} `;254 }255 }256 return (257 <>258 <path d={axes} stroke={main} strokeWidth={sw(0.035)} fill="none" strokeLinecap="round" />259 <path d={branches} stroke={accent} strokeWidth={sw(0.025)} fill="none" strokeLinecap="round" />260 <circle r={f(r * 0.14)} fill="#fff" />261 </>262 );263 }264 case "eye":265 return (266 <>267 <ellipse rx={f(r)} ry={f(r * 0.6)} fill={main} />268 <circle r={f(r * 0.42)} fill={darkenHex(main, 0.5)} />269 <circle r={f(r * 0.2)} fill={accent} />270 <circle cx={f(r * 0.12)} cy={f(-r * 0.14)} r={f(r * 0.07)} fill="#fff" />271 </>272 );273 case "moon":274 return (275 <>276 <circle r={f(r)} fill={main} />277 <circle cx={f(r * 0.45)} cy={f(-r * 0.2)} r={f(r * 0.78)} fill={INK} fillOpacity={0.85} />278 <circle cx={f(-r * 0.4)} cy={f(r * 0.3)} r={f(r * 0.12)} fill={darkenHex(main, 0.2)} />279 </>280 );281 case "leaf":282 return (283 <>284 <path d={`M0 ${f(r)} C${f(-r * 1.1)} ${f(r * 0.2)} ${f(-r * 0.6)} ${f(-r * 0.9)} 0 ${f(-r)} C${f(r * 0.6)} ${f(-r * 0.9)} ${f(r * 1.1)} ${f(r * 0.2)} 0 ${f(r)}Z`} fill={main} />285 <line x1={0} y1={f(r * 0.9)} x2={0} y2={f(-r * 0.8)} stroke={accent} strokeWidth={sw(0.02)} />286 </>287 );288 case "atom":289 return (290 <>291 <circle r={f(r * 0.2)} fill={accent} />292 <ellipse rx={f(r)} ry={f(r * 0.38)} fill="none" stroke={main} strokeWidth={sw(0.025)} />293 <ellipse rx={f(r)} ry={f(r * 0.38)} fill="none" stroke={main} strokeWidth={sw(0.025)} transform="rotate(60)" />294 <ellipse rx={f(r)} ry={f(r * 0.38)} fill="none" stroke={main} strokeWidth={sw(0.025)} transform="rotate(120)" />295 </>296 );297 case "crown":298 return (299 <>300 <polygon points={pts([-r, r * 0.6, -r, -r * 0.4, -r * 0.5, r * 0.05, 0, -r, r * 0.5, r * 0.05, r, -r * 0.4, r, r * 0.6])} fill={main} />301 <rect x={f(-r)} y={f(r * 0.4)} width={f(r * 2)} height={f(r * 0.25)} fill={darkenHex(main, 0.3)} />302 <circle cy={f(-r * 0.95)} r={f(r * 0.12)} fill={accent} />303 <circle cx={f(-r)} cy={f(-r * 0.4)} r={f(r * 0.1)} fill={accent} />304 <circle cx={f(r)} cy={f(-r * 0.4)} r={f(r * 0.1)} fill={accent} />305 </>306 );307 case "wave": {308 const wave = (y: number) => `M${f(-r)} ${f(y)} C${f(-r * 0.5)} ${f(y - r * 0.4)} ${f(-r * 0.2)} ${f(y + r * 0.4)} 0 ${f(y)} C${f(r * 0.2)} ${f(y - r * 0.4)} ${f(r * 0.5)} ${f(y + r * 0.4)} ${f(r)} ${f(y)}`;309 return (310 <>311 <path d={`${wave(-r * 0.5)} ${wave(r * 0.5)}`} fill="none" stroke={main} strokeWidth={sw(0.04)} strokeOpacity={0.8} strokeLinecap="round" />312 <path d={wave(0)} fill="none" stroke={accent} strokeWidth={sw(0.04)} strokeLinecap="round" />313 </>314 );315 }316 case "cube":317 return (318 <>319 <polygon points={pts([0, -r, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r, -r * 0.87, r * 0.5, -r * 0.87, -r * 0.5])} fill={main} />320 <polygon points={pts([0, -r, r * 0.87, -r * 0.5, 0, 0, -r * 0.87, -r * 0.5])} fill={accent} />321 <polygon points={pts([0, 0, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r])} fill={darkenHex(main, 0.3)} />322 {style.label ? <Label text={style.label} fontSize={size * 0.3} color="#fff" /> : null}323 </>324 );325 /* ----------------------------------------------------------- themed set */326 case "lock":327 return (328 <>329 <rect x={f(-r * 0.75)} y={f(-r * 0.1)} width={f(r * 1.5)} height={f(r * 1.1)} rx={f(r * 0.2)} fill={main} />330 <path d={arcPath(0, -r * 0.15, r * 0.5, Math.PI, 0)} fill="none" stroke={accent} strokeWidth={sw(0.05)} />331 <circle cy={f(r * 0.35)} r={f(r * 0.16)} fill={INK} />332 <rect x={f(-r * 0.06)} y={f(r * 0.35)} width={f(r * 0.12)} height={f(r * 0.35)} fill={INK} />333 </>334 );335 case "key":336 return (337 <>338 <circle cx={f(-r * 0.45)} cy={f(-r * 0.35)} r={f(r * 0.42)} fill="none" stroke={main} strokeWidth={sw(0.06)} />339 <path d={`M${f(-r * 0.15)} ${f(-r * 0.05)}L${f(r * 0.85)} ${f(r * 0.95)}`} stroke={main} strokeWidth={sw(0.06)} />340 <path d={`M${f(r * 0.45)} ${f(r * 0.55)}L${f(r * 0.7)} ${f(r * 0.3)} M${f(r * 0.65)} ${f(r * 0.75)}L${f(r * 0.9)} ${f(r * 0.5)}`} stroke={accent} strokeWidth={sw(0.05)} />341 </>342 );343 case "bag":344 return (345 <>346 <path d={`M${f(-r * 0.75)} ${f(r * 0.9)} C${f(-r * 1.1)} ${f(-r * 0.3)} ${f(-r * 0.3)} ${f(-r * 0.6)} 0 ${f(-r * 0.55)} C${f(r * 0.3)} ${f(-r * 0.6)} ${f(r * 1.1)} ${f(-r * 0.3)} ${f(r * 0.75)} ${f(r * 0.9)}Z`} fill={main} />347 <rect x={f(-r * 0.3)} y={f(-r * 0.95)} width={f(r * 0.6)} height={f(r * 0.35)} rx={f(r * 0.1)} fill={accent} />348 <line x1={f(-r * 0.35)} y1={f(r * 0.3)} x2={f(r * 0.35)} y2={f(r * 0.3)} stroke={darkenHex(main, 0.3)} strokeWidth={sw(0.03)} />349 </>350 );351 case "warning":352 return (353 <>354 <polygon points={pts([0, -r, r * 0.95, r * 0.75, -r * 0.95, r * 0.75])} fill={main} />355 <polygon points={pts([0, -r * 0.6, r * 0.55, r * 0.55, -r * 0.55, r * 0.55])} fill={INK} />356 <rect x={f(-r * 0.08)} y={f(-r * 0.3)} width={f(r * 0.16)} height={f(r * 0.5)} fill={main} />357 <circle cy={f(r * 0.38)} r={f(r * 0.09)} fill={main} />358 </>359 );360 case "gauge":361 return (362 <>363 <path d={arcPath(0, r * 0.2, r * 0.9, Math.PI, 0)} fill="none" stroke={darkenHex(main, 0.3)} strokeWidth={sw(0.09)} />364 <path d={arcPath(0, r * 0.2, r * 0.9, Math.PI, Math.PI * 1.6)} fill="none" stroke={main} strokeWidth={sw(0.09)} />365 <line x1={0} y1={f(r * 0.2)} x2={f(r * 0.45)} y2={f(-r * 0.5)} stroke={accent} strokeWidth={sw(0.04)} />366 <circle cy={f(r * 0.2)} r={f(r * 0.12)} fill={accent} />367 </>368 );369 case "rocket":370 return (371 <>372 <path d={`M0 ${f(-r)} C${f(r * 0.6)} ${f(-r * 0.4)} ${f(r * 0.5)} ${f(r * 0.4)} ${f(r * 0.35)} ${f(r * 0.6)} L${f(-r * 0.35)} ${f(r * 0.6)} C${f(-r * 0.5)} ${f(r * 0.4)} ${f(-r * 0.6)} ${f(-r * 0.4)} 0 ${f(-r)}Z`} fill={main} />373 <polygon points={pts([-r * 0.35, r * 0.2, -r * 0.75, r * 0.8, -r * 0.35, r * 0.6])} fill={accent} />374 <polygon points={pts([r * 0.35, r * 0.2, r * 0.75, r * 0.8, r * 0.35, r * 0.6])} fill={accent} />375 <circle cy={f(-r * 0.25)} r={f(r * 0.18)} fill={INK} />376 <polygon points={pts([-r * 0.2, r * 0.6, r * 0.2, r * 0.6, 0, r])} fill="#ffb347" />377 </>378 );379 case "chest":380 return (381 <>382 <rect x={f(-r * 0.9)} y={f(-r * 0.2)} width={f(r * 1.8)} height={f(r * 1.0)} rx={f(r * 0.1)} fill={main} />383 <rect x={f(-r * 0.9)} y={f(-r * 0.75)} width={f(r * 1.8)} height={f(r * 0.6)} rx={f(r * 0.3)} fill={darkenHex(main, 0.2)} />384 <rect x={f(-r * 0.9)} y={f(-r * 0.2)} width={f(r * 1.8)} height={f(r * 0.12)} fill={accent} />385 <rect x={f(-r * 0.15)} y={f(-r * 0.2)} width={f(r * 0.3)} height={f(r * 0.35)} rx={f(r * 0.05)} fill={accent} />386 </>387 );388 case "planet":389 return (390 <>391 <circle r={f(r * 0.7)} fill={main} />392 <ellipse cy={f(r * 0.05)} rx={f(r * 1.05)} ry={f(r * 0.28)} fill="none" stroke={accent} strokeWidth={sw(0.04)} />393 <circle cx={f(-r * 0.25)} cy={f(-r * 0.25)} r={f(r * 0.18)} fill="#fff" fillOpacity={0.3} />394 <ellipse cx={f(r * 0.15)} cy={f(r * 0.15)} rx={f(r * 0.3)} ry={f(r * 0.12)} fill={darkenHex(main, 0.25)} fillOpacity={0.8} />395 </>396 );397 case "comet":398 return (399 <>400 <polygon points={pts([r * 0.5, -r * 0.5, -r * 1.0, r * 0.15, -r * 0.6, r * 0.6])} fill={accent} fillOpacity={0.5} />401 <line x1={f(r * 0.5)} y1={f(-r * 0.5)} x2={f(-r * 0.7)} y2={f(r * 0.7)} stroke={accent} strokeOpacity={0.4} strokeWidth={sw(0.05)} />402 <circle cx={f(r * 0.5)} cy={f(-r * 0.5)} r={f(r * 0.4)} fill={main} />403 <circle cx={f(r * 0.4)} cy={f(-r * 0.6)} r={f(r * 0.12)} fill="#fff" fillOpacity={0.6} />404 </>405 );406 case "satellite":407 return (408 <>409 <rect x={f(-r * 0.3)} y={f(-r * 0.3)} width={f(r * 0.6)} height={f(r * 0.6)} rx={f(r * 0.1)} fill={main} />410 <rect x={f(-r * 1.0)} y={f(-r * 0.18)} width={f(r * 0.6)} height={f(r * 0.36)} fill={accent} />411 <rect x={f(r * 0.4)} y={f(-r * 0.18)} width={f(r * 0.6)} height={f(r * 0.36)} fill={accent} />412 <path d={[-0.8, -0.6, 0.6, 0.8].map((x) => `M${f(x * r)} ${f(-r * 0.18)}V${f(r * 0.18)}`).join(" ")} stroke={darkenHex(main, 0.4)} strokeWidth={sw(0.012)} />413 <path d={arcPath(0, -r * 0.55, r * 0.3, Math.PI, 0)} fill="none" stroke={main} strokeWidth={sw(0.03)} />414 </>415 );416 default:417 return <circle r={f(r)} fill={main} />;418 }419}420421/* -------------------------------------------------------------- SymbolSvg */422423export interface SymbolSvgProps {424 style: SymbolStyle;425 /** Tile edge in user units; the glyph radius is 0.36 × size (as on the reels). */426 size: number;427 tier?: SymbolTier;428 /** Centre position inside the parent SVG (default 0,0). */429 x?: number;430 y?: number;431 rotate?: number;432 opacity?: number;433 /** Dark reel tile behind the glyph (default true, matches the in-game plate). */434 plate?: boolean;435 /** Coloured halo behind the glyph (default true, driven by `style.glow`). */436 halo?: boolean;437 className?: string;438}439440/**441 * Renders a reel symbol as an SVG `<g>` centred on (x, y). Drop it inside any442 * `<svg>`; use `SymbolIcon` for a standalone image.443 */444export function SymbolSvg({ style, size, tier = "mid", x = 0, y = 0, rotate = 0, opacity, plate = true, halo = true, className }: SymbolSvgProps) {445 const main = style.color;446 const accent = style.accent ?? lightenHex(style.color, 0.35);447 const glow = style.glow ?? 0.3;448 const r = size * 0.36;449 const pad = size * 0.06;450 const rich = tier === "premium" || tier === "special";451 const transform = `translate(${f(x)} ${f(y)})${rotate ? ` rotate(${f(rotate)})` : ""}`;452 return (453 <g transform={transform} opacity={opacity} className={className}>454 {plate ? <rect x={f(-size / 2 + pad)} y={f(-size / 2 + pad)} width={f(size - pad * 2)} height={f(size - pad * 2)} rx={f(size * 0.16)} fill={INK} fillOpacity={0.72} stroke={rich ? accent : "#fff"} strokeOpacity={rich ? 0.45 : 0.08} strokeWidth={f(Math.max(1, size * 0.012))} /> : null}455 {halo && glow > 0 ? (456 <>457 <circle r={f(r * 1.25)} fill={main} fillOpacity={f(0.18 * glow)} />458 <circle r={f(r)} fill={main} fillOpacity={f(0.16 * glow)} />459 </>460 ) : null}461 <Glyph style={style} size={size} />462 </g>463 );464}465466/** Standalone `<svg>` wrapper around `SymbolSvg` (paytable rows, tooltips…). */467export function SymbolIcon({ style, tier, size = 64, className, label }: { style: SymbolStyle; tier?: SymbolTier; size?: number; className?: string; label?: string }) {468 return (469 <svg viewBox={`${-size / 2} ${-size / 2} ${size} ${size}`} width={size} height={size} className={className} role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true}>470 <SymbolSvg style={style} tier={tier} size={size} />471 </svg>472 );473}474