SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
22.1 KB · 474 lines tsx
Raw Blame History
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