import * as React from "react"; import { ARCADE_GAMES, CRASH_GAMES, GAMES_BY_SLUG } from "@spinza/games/client"; import type { GameDefinition, SymbolShape, SymbolStyle, SymbolTier } from "@spinza/game-core/client"; import { cn } from "@/lib/utils"; import { SymbolSvg, svgArc, svgPolygon, svgStar } from "./symbol-svg"; /* ------------------------------------------------------------------------ Spinza key art — 35 hand-composed posters, one scene per game. Slot posters are built from the game's own reel symbols (SymbolSvg mirrors the PixiJS renderer); the 10 Risk Games and 5 Beyond Slots originals have no reels, so their scenes are composed from primitives around each game's mechanic (rising multiplier ladder + the big-button verb as a solid pill). Pure inline SVG, deterministic (seeded PRNG only for star fields / particles), no images, filters used sparingly. ------------------------------------------------------------------------ */ export type ArtVariant = "card" | "hero" | "banner" | "tile"; export interface ArtPalette { primary: string; secondary: string; glow: string; bg: string; } export interface GameArtProps { slug: string; name: string; palette: ArtPalette; backdrop?: string; particles?: string; variant?: ArtVariant; className?: string; /** Render the game name inside the artwork (default true). */ showName?: boolean; /** Decorative by default; pass a label to expose the art to assistive tech. */ label?: string; } /** Ambience → particle system for the non-slot games (their definitions carry no `particles`). */ const AMBIENCE_PARTICLES: Record = { lunar: "stars", ocean: "bubbles", space: "stars", heist: "coins", fire: "fire", void: "stars", reactor: "energy", obsidian: "dust", arcade: "confetti", quantum: "energy", jungle: "dust" }; /** Presentation hints per game, derived from the game library (slots, risk games and originals). */ export const GAME_PRESENTATION: Record = Object.fromEntries([ ...[...GAMES_BY_SLUG.values()].map((g) => [g.slug, { backdrop: g.presentation.backdrop, particles: g.presentation.particles }] as const), ...[...CRASH_GAMES, ...ARCADE_GAMES].map((g) => [g.slug, { backdrop: g.presentation.scene, particles: AMBIENCE_PARTICLES[g.presentation.ambience] ?? "stars" }] as const), ]); /* ------------------------------------------------------------ seeded PRNG */ function hashString(s: string): number { let h = 0x811c9dc5; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } return h >>> 0; } function mulberry32(seed: number): () => number { let a = seed >>> 0; return () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /* ------------------------------------------------------------------ stage */ const FONT = "var(--font-geist-sans), Geist, Inter, ui-sans-serif, system-ui, sans-serif"; const MONO = "var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace"; const f = (n: number) => Math.round(n * 100) / 100; interface Stage { W: number; H: number; v: ArtVariant; slug: string; /** Focal centre and unit radius of the composition. */ cx: number; cy: number; R: number; p: ArtPalette; id: string; rnd: () => number; game?: GameDefinition; /** Reel symbol style by id (falls back to a palette gem). */ sym: (id: string) => SymbolStyle; tier: (id: string) => SymbolTier; /** Tile variant: drop fine detail. */ compact: boolean; } const DIMS: Record = { card: { w: 360, h: 480 }, tile: { w: 240, h: 240 }, hero: { w: 1680, h: 720 }, banner: { w: 1600, h: 600 }, }; function focal(v: ArtVariant, W: number, H: number, showName: boolean): { cx: number; cy: number; R: number } { switch (v) { case "card": return { cx: W / 2, cy: showName ? H * 0.385 : H * 0.5, R: W * 0.405 }; case "tile": return { cx: W / 2, cy: H / 2, R: W * 0.4 }; case "hero": return { cx: W * 0.7, cy: H * 0.5, R: H * 0.42 }; case "banner": return { cx: W * 0.72, cy: H * 0.5, R: H * 0.4 }; } } /* ---------------------------------------------------------------- helpers */ /** Many small discs as a single path (star fields, snow, embers, bubbles). */ function dots(list: Array<[number, number, number]>): string { let d = ""; for (const [x, y, r] of list) d += `M${f(x - r)} ${f(y)}a${f(r)} ${f(r)} 0 1 0 ${f(r * 2)} 0a${f(r)} ${f(r)} 0 1 0 ${f(-r * 2)} 0 `; return d; } function scatterDots(s: Stage, n: number, box: [number, number, number, number], rMin: number, rMax: number): string { const list: Array<[number, number, number]> = []; for (let i = 0; i < n; i++) list.push([box[0] + s.rnd() * box[2], box[1] + s.rnd() * box[3], rMin + s.rnd() * (rMax - rMin)]); return dots(list); } function Sym({ s, id, x, y, size, rotate, opacity, plate, halo }: { s: Stage; id: string; x: number; y: number; size: number; rotate?: number; opacity?: number; plate?: boolean; halo?: boolean }) { return ; } /** Small rounded label pill (feature tags, multiplier badges). */ function Pill({ x, y, text, color, bg = "#000", size = 12, w, ink = "#fff", opacity = 1 }: { x: number; y: number; text: string; color: string; bg?: string; size?: number; w?: number; ink?: string; opacity?: number }) { const width = w ?? text.length * size * 0.66 + size * 1.2; const h = size * 1.7; return ( {text} ); } function Small({ x, y, text, color, size = 12, anchor = "middle", weight = 600, tracking = "0.12em", opacity = 1, rotate }: { x: number; y: number; text: string; color: string; size?: number; anchor?: "start" | "middle" | "end"; weight?: number; tracking?: string; opacity?: number; rotate?: number }) { return ( {text} ); } function Radial({ id, color, o0 = 0.6, o1 = 0, mid }: { id: string; color: string; o0?: number; o1?: number; mid?: [number, string, number] }) { return ( {mid ? : null} ); } function Linear({ id, stops, dir = "v" }: { id: string; stops: Array<[number, string, number?]>; dir?: "v" | "h" | "d" }) { const c = dir === "v" ? { x1: 0, y1: 0, x2: 0, y2: 1 } : dir === "h" ? { x1: 0, y1: 0, x2: 1, y2: 0 } : { x1: 0, y1: 0, x2: 1, y2: 1 }; return ( {stops.map(([o, col, op], i) => ( ))} ); } const PI = Math.PI; /* ======================================================================= */ /* SCENES */ /* ======================================================================= */ /* 01 — NEON VAULT: cyan vault ring, Override Wild core, keycards scanning a grid */ function NeonVault(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const horizon = cy + R * 0.7; let grid = ""; for (let i = -6; i <= 6; i++) grid += `M${f(cx)} ${f(horizon)}L${f(cx + i * W * 0.22)} ${f(H)} `; for (let k = 1; k <= 6; k++) { const t = k / 6; const y = horizon + (H - horizon) * t * t; grid += `M0 ${f(y)}H${f(W)} `; } let ticks = ""; for (let i = 0; i < 12; i++) { const a = (i / 12) * PI * 2; ticks += `M${f(cx + Math.cos(a) * R * 0.86)} ${f(cy + Math.sin(a) * R * 0.86)}L${f(cx + Math.cos(a) * R * 1.02)} ${f(cy + Math.sin(a) * R * 1.02)} `; } const scanY = cy - R * 0.22; return ( {!compact ? ( <> ) : null} ); } /* 02 — COSMIC COLLAPSE: symbols spiral into a black hole, gravity ladder ×1→×13 */ function CosmicCollapse(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 90, [0, 0, W, H], 0.5, 1.5); const bright = scatterDots(s, 10, [0, 0, W, H], 1.6, 2.6); const orbit = [ { id: "P1", a: -0.55, d: 1.05, k: 0.5, rot: 28 }, { id: "H1", a: 0.95, d: 0.98, k: 0.42, rot: -20 }, { id: "H2", a: 2.35, d: 0.9, k: 0.36, rot: 35 }, { id: "M1", a: 3.7, d: 0.78, k: 0.3, rot: -40 }, { id: "M2", a: 4.9, d: 0.66, k: 0.24, rot: 15 }, ]; return ( {!compact ? orbit.map((o) => ) : null} {!compact ? ( ×1 ×2 ×3 ×5 ×8 ×13 ) : null} ); } /* 03 — GOLDEN EMPEROR: symmetrical imperial court, gold pillars, dragon pearls ×2 ×3 ×5 */ function GoldenEmperor(s: Stage) { const { H, cx, cy, R, id, p, compact } = s; const pw = R * 0.22; const px1 = cx - R * 1.02; const px2 = cx + R * 1.02 - pw; const top = cy - R * 1.15; const flecks = scatterDots(s, 30, [cx - R * 1.2, cy - R * 1.2, R * 2.4, R * 2.4], 0.8, 2.2); const pearls = [ { x: cx - R * 0.66, m: "×2" }, { x: cx, m: "×3" }, { x: cx + R * 0.66, m: "×5" }, ]; const py = cy + R * 0.55; return ( {!compact ? ( <> {pearls.map((o) => ( ))} ) : null} ); } /* 04 — DIAMOND HEIST: laser-cut vault door, diamond coins with SC values, jackpot plaques */ function DiamondHeist(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; let spokes = ""; for (let i = 0; i < 24; i++) { const a = (i / 24) * PI * 2; spokes += `M${f(cx + Math.cos(a) * R * 0.58)} ${f(cy + Math.sin(a) * R * 0.58)}L${f(cx + Math.cos(a) * R * 0.98)} ${f(cy + Math.sin(a) * R * 0.98)} `; } const rivets: Array<[number, number, number]> = []; for (let i = 0; i < 12; i++) { const a = (i / 12) * PI * 2 + PI / 12; rivets.push([cx + Math.cos(a) * R * 1.06, cy + Math.sin(a) * R * 1.06, R * 0.025]); } const sparkle = (x: number, y: number, r: number) => `M${f(x)} ${f(y - r)}Q${f(x)} ${f(y)} ${f(x + r)} ${f(y)}Q${f(x)} ${f(y)} ${f(x)} ${f(y + r)}Q${f(x)} ${f(y)} ${f(x - r)} ${f(y)}Q${f(x)} ${f(y)} ${f(x)} ${f(y - r)}Z`; let sparkles = ""; for (let i = 0; i < 14; i++) sparkles += sparkle(s.rnd() * W, s.rnd() * H, R * (0.03 + s.rnd() * 0.05)); const lasers = `M${f(cx - R * 1.4)} ${f(cy - R * 1.3)}L${f(cx + R * 1.4)} ${f(cy + R * 0.2)} M${f(cx - R * 1.4)} ${f(cy + R * 0.9)}L${f(cx + R * 1.4)} ${f(cy - R * 1.1)}`; const tiers = ["MINI", "MINOR", "MAJOR", "GRAND"]; const coins = [ { x: cx - R * 0.72, y: cy + R * 0.5, v: "5×" }, { x: cx, y: cy + R * 0.66, v: "20×" }, { x: cx + R * 0.72, y: cy + R * 0.5, v: "10×" }, ]; return ( {!compact ? ( <> {coins.map((c) => ( ))} {tiers.map((t, i) => { const y = cy - R * 0.62 + i * R * 0.3; const grand = i === 3; return ; })} ) : null} ); } /* 05 — ARCTIC FORTUNE: aurora over a ridge, frozen sticky wilds in ice blocks, snowfall */ function ArcticFortune(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 40, [0, 0, W, H * 0.5], 0.5, 1.3); const snow = scatterDots(s, 45, [0, 0, W, H], 1, 3.2); const band = (y: number, amp: number) => `M${f(-W * 0.1)} ${f(y)} C${f(W * 0.2)} ${f(y - amp)} ${f(W * 0.4)} ${f(y + amp)} ${f(W * 0.6)} ${f(y)} S${f(W * 0.9)} ${f(y - amp)} ${f(W * 1.1)} ${f(y + amp * 0.4)}`; const ridgeY = cy + R * 0.95; const ridge = `M0 ${f(H)} L0 ${f(ridgeY + R * 0.1)} L${f(W * 0.18)} ${f(ridgeY - R * 0.25)} L${f(W * 0.32)} ${f(ridgeY)} L${f(W * 0.5)} ${f(ridgeY - R * 0.45)} L${f(W * 0.66)} ${f(ridgeY - R * 0.05)} L${f(W * 0.82)} ${f(ridgeY - R * 0.3)} L${f(W)} ${f(ridgeY + R * 0.05)} L${f(W)} ${f(H)}Z`; const blocks = [ { x: cx - R * 0.72, y: cy + R * 0.35 }, { x: cx - R * 0.1, y: cy + R * 0.05 }, { x: cx + R * 0.55, y: cy - R * 0.25 }, ]; const bs = R * 0.56; return ( {!compact ? ( <> {blocks.map((b, i) => ( ))} ) : ( )} ); } /* 06 — INFERNO REELS: cracked lava floor, Magma Core shockwave, embers */ function InfernoReels(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const floorY = cy + R * 0.75; let cracks = ""; for (let i = 0; i < 6; i++) { let x = W * (0.05 + i * 0.18) + s.rnd() * W * 0.08; let y = H; cracks += `M${f(x)} ${f(y)}`; while (y > floorY + R * 0.05) { x += (s.rnd() - 0.5) * R * 0.3; y -= R * (0.1 + s.rnd() * 0.15); cracks += `L${f(x)} ${f(y)}`; } cracks += " "; } const embers = scatterDots(s, 40, [cx - R * 1.3, cy - R * 1.3, R * 2.6, R * 2.4], 0.8, 2.6); const embers2 = scatterDots(s, 16, [cx - R * 1.2, cy - R * 1.2, R * 2.4, R * 2], 1.5, 3.5); const burst = svgStar(cx, cy, R * 0.98, R * 0.62, 14); return ( {!compact ? ( <> ) : null} ); } /* 07 — QUANTUM JACKPOT: a processor splitting into ghost copies ×2 ×3 ×4 over circuit traces */ function QuantumJackpot(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const step = R * 0.22; let t1 = ""; let t2 = ""; const pads: Array<[number, number, number]> = []; for (let i = 0; i < 14; i++) { let x = Math.round((s.rnd() * W) / step) * step; let y = Math.round((s.rnd() * H) / step) * step; let d = `M${f(x)} ${f(y)}`; for (let k = 0; k < 3; k++) { if (s.rnd() > 0.5) x += (s.rnd() > 0.5 ? 1 : -1) * step * (1 + Math.floor(s.rnd() * 3)); else y += (s.rnd() > 0.5 ? 1 : -1) * step * (1 + Math.floor(s.rnd() * 3)); d += `L${f(x)} ${f(y)}`; } if (i % 3 === 0) t2 += d + " "; else t1 += d + " "; pads.push([x, y, 3]); } const wave = `M0 ${f(cy + R * 1.05)} ${Array.from({ length: 12 }, (_, i) => `Q${f((W / 12) * (i + 0.5))} ${f(cy + R * (1.05 + (i % 2 ? 0.14 : -0.14)))} ${f((W / 12) * (i + 1))} ${f(cy + R * 1.05)}`).join(" ")}`; const gx = cx - R * 0.42; return ( {!compact ? ( <> ) : null} {!compact ? ( <> ) : null} ); } /* 08 — MIDNIGHT TOKYO: rain-wet skyline, neon signage, stacked wild column */ function MidnightTokyo(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const horizon = cy + R * 0.85; let sky = `M0 ${f(horizon)}`; const wins: Array<[number, number, number]> = []; const wins2: Array<[number, number, number]> = []; let x = 0; while (x < W) { const bw = W * (0.04 + s.rnd() * 0.09); const bh = R * (0.4 + s.rnd() * 1.4); sky += `L${f(x)} ${f(horizon - bh)}L${f(x + bw)} ${f(horizon - bh)}`; for (let k = 0; k < bh / (R * 0.16) - 1; k++) { if (s.rnd() > 0.55) (s.rnd() > 0.6 ? wins2 : wins).push([x + bw * (0.25 + s.rnd() * 0.5), horizon - bh + R * 0.12 + k * R * 0.16, 1.6]); } x += bw + W * 0.008; } sky += `L${f(W)} ${f(horizon)}Z`; let rain = ""; for (let i = 0; i < 45; i++) { const rx = s.rnd() * W; const ry = s.rnd() * H; rain += `M${f(rx)} ${f(ry)}l${f(-R * 0.05)} ${f(R * 0.22)} `; } const colX = cx + R * 0.72; const colH = R * 1.75; return ( {!compact ? ( <> ) : ( )} ); } /* 09 — PHARAOH PROTOCOL: pyramid with a scanning laser, artifact meter */ function PharaohProtocol(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const base = cy + R * 0.7; const apex = cy - R * 0.98; const half = R * 1.05; const tri = `${f(cx - half)},${f(base)} ${f(cx)},${f(apex)} ${f(cx + half)},${f(base)}`; let bricks = ""; for (let k = 1; k < 9; k++) bricks += `M${f(cx - half)} ${f(apex + (base - apex) * (k / 9))}H${f(cx + half)} `; const scanY = cy - R * 0.18; const dust = scatterDots(s, 35, [0, 0, W, H], 0.6, 1.8); const segs = 12; const mw = R * 1.4; const mx = cx - mw / 2; const my = cy + R * 0.86; let segOff = ""; let segOn = ""; for (let i = 0; i < segs; i++) { const d = `M${f(mx + (i * mw) / segs + 1.5)} ${f(my)}h${f(mw / segs - 3)}v${f(R * 0.06)}h${f(-(mw / segs - 3))}z `; if (i < 7) segOn += d; else segOff += d; } return ( {!compact ? ( <> ) : null} ); } /* 10 — LUCKY CIRCUIT: retro arcade bezel, mystery "?" cartridge, confetti */ function LuckyCircuit(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const bw = R * 2.2; const bh = R * 1.9; const bx = cx - bw / 2; const by = cy - bh / 2 - R * 0.05; let scan = ""; for (let y = by + 8; y < by + bh; y += 6) scan += `M${f(bx)} ${f(y)}H${f(bx + bw)} `; const confetti = (color: string, n: number) => { let d = ""; for (let i = 0; i < n; i++) { const x = s.rnd() * W; const y = s.rnd() * H; const k = 3 + s.rnd() * 4; d += `M${f(x)} ${f(y)}h${f(k * 1.6)}v${f(k)}h${f(-k * 1.6)}z `; } return ; }; return ( {!compact ? confetti(p.primary, 14) : null} {!compact ? confetti(p.glow, 12) : null} {!compact ? confetti(p.secondary, 12) : null} {!compact ? ( <> ) : null} ); } /* 11 — VOID MINER: asteroid field, dark-matter core with level meter, drill beam */ function VoidMiner(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 60, [0, 0, W, H], 0.5, 1.3); const rock = (x: number, y: number, r: number, n: number) => { const list: string[] = []; for (let k = 0; k < n; k++) { const a = (k / n) * PI * 2; const rr = r * (0.72 + s.rnd() * 0.36); list.push(`${f(x + Math.cos(a) * rr)},${f(y + Math.sin(a) * rr)}`); } return list.join(" "); }; const rocks = [ [cx - R * 1.0, cy - R * 0.95, R * 0.28, 8], [cx + R * 1.0, cy - R * 0.2, R * 0.22, 7], [cx - R * 0.95, cy + R * 0.35, R * 0.2, 9], [cx + R * 0.7, cy + R * 0.95, R * 0.3, 8], [cx - R * 0.15, cy - R * 1.12, R * 0.16, 7], ] as const; const drillX = cx + R * 0.6; const drillY = cy - R * 0.58; const circ = 2 * PI * R * 0.66; const seg = circ / 5; return ( {rocks.map((r, i) => ( ))} {!compact ? ( <> ) : null} ); } /* 12 — ROYAL CIRCUIT: race-track sweep, lap counter, turbo boost ×2/×3 */ function RoyalCircuit(s: Stage) { const { W, cx, cy, R, id, p, compact } = s; const track = `M${f(cx - R * 1.6)} ${f(cy + R * 1.3)} C${f(cx - R * 0.6)} ${f(cy + R * 1.4)} ${f(cx - R * 0.8)} ${f(cy - R * 0.2)} ${f(cx + R * 0.2)} ${f(cy - R * 0.05)} S${f(cx + R * 1.8)} ${f(cy - R * 1.0)} ${f(cx + R * 1.9)} ${f(cy - R * 1.6)}`; let streaks = ""; for (let i = 0; i < 9; i++) { const y = cy - R * 1.1 + s.rnd() * R * 2.2; const x0 = s.rnd() * W * 0.5; streaks += `M${f(x0)} ${f(y)}h${f(R * (0.4 + s.rnd() * 0.9))} `; } const lapX = cx - R * 0.75; const lapY = cy - R * 0.62; const lapR = R * 0.36; const lapC = 2 * PI * lapR; const lseg = lapC / 8; return ( {!compact ? ( <> ) : null} ); } /* 13 — DRAGON CORE: reactor containment with a dragon charge gauge 0/20 */ function DragonCore(s: Stage) { const { cx, cy, R, id, p, compact } = s; let ticks = ""; let lit = ""; for (let i = 0; i < 20; i++) { const a = -PI * 1.25 + (i / 19) * PI * 1.5; const d = `M${f(cx + Math.cos(a) * R * 0.98)} ${f(cy + Math.sin(a) * R * 0.98)}L${f(cx + Math.cos(a) * R * 1.12)} ${f(cy + Math.sin(a) * R * 1.12)} `; if (i < 13) lit += d; else ticks += d; } const embers = scatterDots(s, 30, [cx - R * 1.3, cy - R * 1.3, R * 2.6, R * 2.6], 0.8, 2.4); const scales = [ { x: cx + R * 0.95, y: cy + R * 0.45, r: 15 }, { x: cx + R * 0.72, y: cy + R * 0.8, r: 30 }, { x: cx + R * 0.4, y: cy + R * 0.8, r: 45 }, ]; return ( {!compact ? ( <> {scales.map((sc, i) => ( ))} ) : null} ); } /* 14 — DEEP TREASURE: descending depth levels with treasure chests, bubbles */ function DeepTreasure(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const bubbles: Array<[number, number, number]> = []; for (let i = 0; i < 26; i++) bubbles.push([s.rnd() * W, cy - R * 1.3 + s.rnd() * R * 2.6, 2 + s.rnd() * R * 0.04]); const levels = ["×1", "×2", "×3", "×4", "×6", "×8", "×10"]; const top = cy - R * 1.05; const lh = R * 0.3; const chest = (x: number, y: number, w: number, key: string) => ( ); return ( {!compact ? levels.map((l, i) => ( )) : null} {!compact ? chest(cx - R * 0.78, cy - R * 0.5, R * 0.42, "c1") : null} {!compact ? chest(cx - R * 0.3, cy + R * 0.15, R * 0.5, "c2") : null} {chest(cx + R * 0.3, cy + R * 0.72, R * 0.6, "c3")} {!compact ? ( <> ) : null} ); } /* 15 — MOONBASE 77: retro lunar base, moon horizon, drifting wilds */ function Moonbase77(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 70, [0, 0, W, H * 0.8], 0.5, 1.4); const mR = R * 3.2; const mY = cy + R * 0.95 + mR; const craters: Array<[number, number, number]> = []; for (let i = 0; i < 10; i++) craters.push([cx - R * 1.3 + s.rnd() * R * 2.6, cy + R * 1.05 + s.rnd() * R * 0.5, R * (0.03 + s.rnd() * 0.08)]); const domeX = cx - R * 0.55; const domeY = cy + R * 0.95; const domeR = R * 0.55; let trail = ""; for (const [x, y] of [ [cx - R * 0.92, cy - R * 0.72], [cx - R * 0.15, cy - R * 0.9], ]) trail += `M${f(x + R * 0.2)} ${f(y + R * 0.1)}q${f(R * 0.25)} ${f(R * 0.1)} ${f(R * 0.55)} ${f(-R * 0.05)} `; return ( {!compact ? ( <> ) : null} ); } /* 16 — WILD TEMPLE: stepped stone chambers, jaguar wild with moving arrows, torches */ function WildTemple(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; let steps = ""; const n = 5; const baseY = cy + R * 1.15; const sh = R * 0.3; for (let i = 0; i < n; i++) { const w = R * (2.7 - i * 0.48); steps += `M${f(cx - w / 2)} ${f(baseY - sh * (i + 1))}h${f(w)}v${f(sh)}h${f(-w)}z `; } let joints = ""; for (let i = 0; i < n; i++) { const w = R * (2.7 - i * 0.48); const y = baseY - sh * (i + 1); for (let k = 1; k < 5; k++) joints += `M${f(cx - w / 2 + (w * k) / 5 + (i % 2 ? w / 10 : 0))} ${f(y)}v${f(sh)} `; } const motes = scatterDots(s, 30, [0, 0, W, H], 0.6, 1.8); const vines = `M${f(cx - R * 1.3)} ${f(H)}q${f(R * 0.2)} ${f(-R * 0.6)} ${f(R * 0.05)} ${f(-R * 1.2)} M${f(cx + R * 1.25)} ${f(H)}q${f(-R * 0.25)} ${f(-R * 0.5)} ${f(-R * 0.1)} ${f(-R * 1.1)} M${f(cx - R * 1.2)} ${f(cy - R * 1.3)}q${f(R * 0.3)} ${f(R * 0.4)} ${f(R * 0.1)} ${f(R * 0.9)}`; const chevron = (x: number, y: number, dir: 1 | -1, k: number) => `M${f(x - dir * R * 0.06)} ${f(y - R * 0.12 * k)}l${f(dir * R * 0.12 * k)} ${f(R * 0.12 * k)}l${f(-dir * R * 0.12 * k)} ${f(R * 0.12 * k)}`; const jx = cx; const jy = cy - R * 0.05; return ( {!compact ? ( <> ) : null} ); } /* 17 — ZERO GRAVITY: three grids 5×3 → 6×4 → 7×5 floating in orbit */ function ZeroGravity(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 70, [0, 0, W, H], 0.5, 1.4); const grid = (x: number, y: number, cols: number, rows: number, cell: number, op: number, key: string, label: string, skew: number) => { const w = cols * cell; const h = rows * cell; let lines = ""; for (let c = 1; c < cols; c++) lines += `M${f(x + c * cell)} ${f(y)}v${f(h)} `; for (let r = 1; r < rows; r++) lines += `M${f(x)} ${f(y + r * cell)}h${f(w)} `; return ( ); }; const c1 = R * 0.13; const c2 = R * 0.155; const c3 = R * 0.19; return ( {!compact ? grid(cx - R * 1.12, cy - R * 1.02, 5, 3, c1, 0.5, "g1", "5×3", -8) : null} {!compact ? grid(cx - R * 0.35, cy - R * 0.62, 6, 4, c2, 0.72, "g2", "6×4", -5) : null} {grid(cx - R * 0.66, cy + R * 0.05, 7, 5, c3, 1, "g3", "7×5", -3)} {!compact ? ( ) : null} {!compact ? ( <> ) : null} ); } /* 18 — REEL REACTOR: heat gauge 0–100 with meltdown zone, hazard stripes */ function ReelReactor(s: Stage) { const { W, cx, cy, R, id, p, compact } = s; const gR = R * 1.0; const a0 = PI * 0.78; const a1 = PI * 2.22; const at = (t: number) => a0 + (a1 - a0) * t; const needle = at(0.72); let hazard = ""; const hy = cy + R * 0.95; for (let x = -R * 0.3; x < W + R; x += R * 0.22) hazard += `M${f(x)} ${f(hy)}h${f(R * 0.11)}l${f(-R * 0.1)} ${f(R * 0.12)}h${f(-R * 0.11)}z `; const tick = (t: number, label: string, color: string) => { const a = at(t); return ; }; return ( {!compact ? [tick(0, "0", "#c9d1c4"), tick(0.4, "40", p.secondary), tick(0.8, "80", "#ef4444"), tick(1, "100", "#ef4444")] : null} {!compact ? ( <> ) : null} ); } /* 19 — OBSIDIAN: pure black, one thin gold line, one white-gold wild, a whisper of 50,000× */ function Obsidian(s: Stage) { const { W, H, cx, cy, R, p, compact } = s; const dust = scatterDots(s, 14, [0, 0, W, H], 0.4, 1.0); const lineY = cy + R * 0.62; return ( {!compact ? : null} ); } /* 20 — SPINZA ORIGINAL: the Spinza ring mark, Mystery Orb, Mega Meter, cascading symbols */ function SpinzaOriginal(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 60, [0, 0, W, H], 0.5, 1.4); let rays = ""; for (let i = 0; i < 28; i++) { const a = (i / 28) * PI * 2; const len = R * (1.15 + s.rnd() * 0.7); rays += `M${f(cx + Math.cos(a) * R * 0.75)} ${f(cy + Math.sin(a) * R * 0.75)}L${f(cx + Math.cos(a) * len)} ${f(cy + Math.sin(a) * len)} `; } const mR = R * 0.78; const circ = 2 * PI * mR; const seg = circ / 30; const colX = cx + R * 0.98; const fall = ["P1", "H1", "H2", "M1"]; return ( {!compact ? ( <> {fall.map((idd, i) => ( ))} ) : null} ); } /* ======================================================================= */ /* RISK GAMES + BEYOND SLOTS — no reel symbols. Shared grammar: a rising */ /* multiplier ladder (`Rungs`) and the big-button verb as a solid pill */ /* (`Verb`) so the cash-out / original category reads instantly. */ /* ======================================================================= */ /** Ad-hoc glyph from the shared symbol vocabulary (no reel plate, no halo). */ function Shape({ shape, color, accent, x, y, size, rotate, opacity, label }: { shape: SymbolShape; color: string; accent?: string; x: number; y: number; size: number; rotate?: number; opacity?: number; label?: string }) { return ; } /** The game's big-button verb as a solid pill with a "button" dot. */ function Verb({ x, y, text, color, ink = "#0b0d14", size = 12, opacity = 1 }: { x: number; y: number; text: string; color: string; ink?: string; size?: number; opacity?: number }) { const width = text.length * size * 0.7 + size * 2.6; const h = size * 1.9; const left = x - width / 2; return ( {text} ); } /** Multiplier ladder labels: opacity and size grow along the list, the last rung is hot. */ function Rungs({ items, color, hot, size, anchor = "middle", weight = 700 }: { items: Array<[number, number, string]>; color: string; hot: string; size: number; anchor?: "start" | "middle" | "end"; weight?: 600 | 700 | 800 }) { const n = Math.max(1, items.length - 1); return ( <> {items.map(([x, y, t], i) => ( ))} ); } /* 21 — SKYFALL: a jet climbs from the cloud deck through the storm into space; altitude rungs on the left */ function Skyfall(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 55, [0, 0, W, Math.max(1, cy - R * 0.45)], 0.5, 1.4); const cloudY = cy + R * 1.02; const clouds: Array<[number, number, number]> = []; for (let i = 0; i < 12; i++) clouds.push([cx - R * 1.4 + i * R * 0.26 + s.rnd() * R * 0.1, cloudY + s.rnd() * R * 0.2, R * (0.12 + s.rnd() * 0.15)]); const jx = cx + R * 0.52; const jy = cy - R * 0.48; const trail = `M${f(cx - R * 1.15)} ${f(cy + R * 0.98)} C${f(cx - R * 0.1)} ${f(cy + R * 0.98)} ${f(cx + R * 0.05)} ${f(cy + R * 0.25)} ${f(jx - R * 0.1)} ${f(jy + R * 0.1)}`; const jet = `M${f(R * 0.72)} 0 L${f(R * 0.12)} ${f(-R * 0.075)} L${f(-R * 0.56)} ${f(-R * 0.06)} L${f(-R * 0.62)} 0 L${f(-R * 0.56)} ${f(R * 0.06)} L${f(R * 0.12)} ${f(R * 0.075)} Z`; const wings = `M${f(R * 0.06)} ${f(-R * 0.05)} L${f(-R * 0.48)} ${f(-R * 0.44)} L${f(-R * 0.62)} ${f(-R * 0.42)} L${f(-R * 0.44)} ${f(-R * 0.05)} Z M${f(R * 0.06)} ${f(R * 0.05)} L${f(-R * 0.48)} ${f(R * 0.44)} L${f(-R * 0.62)} ${f(R * 0.42)} L${f(-R * 0.44)} ${f(R * 0.05)} Z`; const bolt = `M${f(cx - R * 0.62)} ${f(cy + R * 0.15)} l${f(R * 0.1)} ${f(R * 0.2)} l${f(-R * 0.09)} ${f(R * 0.02)} l${f(R * 0.14)} ${f(R * 0.26)}`; const lx = cx - R * 1.14; const rungs: Array<[number, number, string]> = [[lx, cy + R * 0.78, "1.5× CLOUDS"], [lx, cy + R * 0.38, "3× STORM FRONT"], [lx, cy - R * 0.02, "8× STRATOSPHERE"], [lx, cy - R * 0.42, "25× EDGE OF SPACE"], [lx, cy - R * 0.82, "100× ORBIT"]]; let ticks = ""; for (const [x, y] of rungs) ticks += `M${f(x - R * 0.06)} ${f(y)}h${f(R * 0.04)} M${f(x - R * 0.04)} ${f(y - R * 0.2)}v${f(R * 0.4)} `; return ( [x + r * 0.2, y + r * 0.35, r * 0.8]))} fill="#cbd5e1" fillOpacity={0.6} /> {!compact ? : null} {!compact ? : null} {!compact ? : null} ); } /* 22 — DEEP DIVE: research sub sinking past depth markers, a leviathan waiting in the abyss */ function DeepDive(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const surface = cy - R * 1.05; const bubbles: Array<[number, number, number]> = []; for (let i = 0; i < 22; i++) bubbles.push([cx - R * 1.1 + s.rnd() * R * 2.2, surface + R * 0.2 + s.rnd() * R * 2.0, 1.5 + s.rnd() * R * 0.03]); let waves = `M0 ${f(surface)}`; for (let i = 0; i < 10; i++) waves += ` q${f(W / 20)} ${f(-R * 0.06)} ${f(W / 10)} 0`; const sx = cx - R * 0.3; const sy = cy - R * 0.28; const rx = cx + R * 1.12; const rungs: Array<[number, number, string]> = [[rx, cy + R * 0.02, "1,000 m ×2"], [rx, cy + R * 0.38, "3,000 m ×5"], [rx, cy + R * 0.74, "ABYSS ×20"]]; let markers = ""; for (const [, y] of rungs) markers += `M${f(cx - R * 1.2)} ${f(y + R * 0.11)}H${f(cx + R * 1.2)} `; const body = `M${f(cx + R * 1.45)} ${f(cy + R * 1.45)} C${f(cx + R * 1.0)} ${f(cy + R * 0.75)} ${f(cx + R * 0.35)} ${f(cy + R * 1.7)} ${f(cx - R * 0.35)} ${f(cy + R * 1.12)}`; const spines = `M${f(cx + R * 0.95)} ${f(cy + R * 1.0)}l${f(R * 0.05)} ${f(-R * 0.24)}l${f(R * 0.1)} ${f(R * 0.2)} M${f(cx + R * 0.72)} ${f(cy + R * 1.0)}l${f(R * 0.03)} ${f(-R * 0.22)}l${f(R * 0.1)} ${f(R * 0.18)} M${f(cx + R * 1.2)} ${f(cy + R * 1.12)}l${f(R * 0.07)} ${f(-R * 0.22)}l${f(R * 0.09)} ${f(R * 0.22)}`; return ( {!compact ? : null} {!compact ? : null} {!compact ? : null} ); } /* 23 — ROCKET RUN: launch pad, spent stage tumbling away, trajectory milestones toward a big Mars */ function RocketRun(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 70, [0, 0, W, H], 0.5, 1.4); const mx = cx + R * 0.86; const my = cy - R * 0.84; const mr = R * 0.5; const ground = cy + R * 1.08; const px = cx - R * 0.92; const bez = (t: number): [number, number] => { const x0 = px + R * 0.12; const y0 = ground - R * 0.1; const x1 = cx - R * 0.6; const y1 = cy - R * 1.4; const x2 = mx - mr * 0.55; const y2 = my + mr * 0.75; const u = 1 - t; return [u * u * x0 + 2 * u * t * x1 + t * t * x2, u * u * y0 + 2 * u * t * y1 + t * t * y2]; }; const traj = `M${f(bez(0)[0])} ${f(bez(0)[1])} Q${f(cx - R * 0.6)} ${f(cy - R * 1.4)} ${f(bez(1)[0])} ${f(bez(1)[1])}`; const ms: Array<{ t: number; l: string; side: 1 | 0 }> = [ { t: 0.14, l: "2× MAX-Q", side: 1 }, { t: 0.28, l: "5× SEPARATION", side: 1 }, { t: 0.74, l: "15× ESCAPE", side: 0 }, { t: 0.9, l: "60× TMI", side: 0 }, ]; const [rkx, rky] = bez(0.44); const smoke = scatterDots(s, 14, [px - R * 0.3, ground - R * 0.35, R * 0.9, R * 0.35], R * 0.03, R * 0.1); return ( {!compact ? [bez(m.t)[0], bez(m.t)[1], R * 0.02]))} fill={p.glow} /> : null} {!compact ? ms.map((m, i) => { const [x, y] = bez(m.t); return ; }) : null} {!compact ? : null} {!compact ? : null} ); } /* 24 — BANK HEIST: inside the vault, safe-deposit wall, growing SC bags, red/blue siren sweep */ function BankHeist(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const floor = cy + R * 0.98; const lampX = cx + R * 0.1; const lampY = cy - R * 1.22; const bags = [ { x: cx - R * 0.82, y: floor - R * 0.02, k: 0.5, m: "×1.4" }, { x: cx - R * 0.28, y: floor - R * 0.06, k: 0.68, m: "×2" }, { x: cx + R * 0.45, y: floor - R * 0.12, k: 1.0, m: "×4" }, ]; let bars = ""; for (let i = 0; i < 6; i++) bars += `M${f(cx + R * 0.95 + (i % 3) * R * 0.02)} ${f(floor - R * 0.09 * (Math.floor(i / 3) + 1))}h${f(R * 0.26)}v${f(R * 0.08)}h${f(-R * 0.26)}z `; return ( {(compact ? bags.slice(2) : bags).map((b) => ( {!compact ? : null} ))} {!compact ? : null} {!compact ? : null} ); } /* 25 — VOLCANO: explorer on a rope in a crystal-lined shaft, lava rising from below */ function Volcano(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const lavaY = cy + R * 0.82; const wall = (side: 1 | -1) => { let d = `M${f(cx + side * W)} 0 L${f(cx + side * R * 0.62)} 0`; let y = 0; while (y < H) { y += R * 0.22; d += ` L${f(cx + side * R * (0.7 + s.rnd() * 0.22))} ${f(y)}`; } return d + ` L${f(cx + side * W)} ${f(H)} Z`; }; const left = wall(-1); const right = wall(1); const embers = scatterDots(s, 30, [cx - R * 0.8, lavaY - R * 1.4, R * 1.6, R * 1.4], 0.8, 2.4); let lava = `M${f(cx - R * 1.3)} ${f(lavaY)}`; for (let i = 0; i < 8; i++) lava += ` q${f(R * 0.16)} ${f(i % 2 ? R * 0.08 : -R * 0.08)} ${f(R * 0.33)} 0`; lava += ` V${f(H)} H${f(cx - R * 1.3)} Z`; const ex = cx - R * 0.12; const ey = cy - R * 0.02; const rx = cx + R * 0.5; const rungs: Array<[number, number, string]> = [[rx, cy - R * 0.98, "1.5× ASH CAVES"], [rx, cy - R * 0.48, "3× OBSIDIAN"], [rx, cy + R * 0.02, "8× CRYSTALS"], [rx, cy + R * 0.52, "30× MAGMA"]]; return ( {!compact ? ( <> ) : null} {!compact ? : null} {!compact ? : null} ); } /* 26 — BLACK HOLE: lensed disc, time-dilation streaks, a ship spiralling to the horizon */ function BlackHole(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const bx = cx + R * 0.12; const by = cy + R * 0.12; const rb = R * 0.4; const stars = scatterDots(s, 60, [0, 0, W, H], 0.5, 1.3); let streaks = ""; for (let i = 0; i < 44; i++) { const a = s.rnd() * PI * 2; const r0 = rb * 1.7 + s.rnd() * R * 0.5; const r1 = r0 + R * (0.25 + s.rnd() * 0.9); streaks += `M${f(bx + Math.cos(a) * r0)} ${f(by + Math.sin(a) * r0)}L${f(bx + Math.cos(a) * r1)} ${f(by + Math.sin(a) * r1)} `; } const sp = (t: number): [number, number] => { const r = R * 1.3 * Math.exp(-0.42 * t); return [bx + Math.cos(t - 2.6) * r, by + Math.sin(t - 2.6) * r * 0.92]; }; let spiral = ""; for (let i = 0; i <= 60; i++) { const [x, y] = sp((i / 60) * PI * 2.4); spiral += `${i ? "L" : "M"}${f(x)} ${f(y)} `; } const [shx, shy] = sp(PI * 1.35); const [nx, ny] = sp(PI * 1.42); const ang = (Math.atan2(ny - shy, nx - shx) * 180) / PI; const rungs: Array<[number, number, string]> = ([0.12, 0.5, 0.86, 1.15] as const).map((k, i) => { const [x, y] = sp(PI * k); return [x, y - R * 0.08, ["1.5×", "4×", "15×", "60×"][i]]; }); const disc = `rotate(-14 ${f(bx)} ${f(by)})`; return ( {!compact ? : null} {!compact ? : null} ); } /* 27 — FREEFALL: skydiver above a patchwork of fields rushing up, ghost of the unopened chute */ function Freefall(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const y0 = cy + R * 0.3; const cols = 8; const tints = ["#365314", "#65a30d", "#a16207", "#4d7c0f", "#ca8a04", "#1a2e05"]; const paths = tints.map(() => ""); for (let k = 0; k < 6; k++) { const t0 = Math.pow(k / 6, 1.55); const t1 = Math.pow((k + 1) / 6, 1.55); const ya = y0 + (H - y0) * t0; const yb = y0 + (H - y0) * t1; const spreadA = 1 + k * 0.16; const spreadB = 1 + (k + 1) * 0.16; const cw = R * 0.36; for (let j = 0; j < cols; j++) { const c = Math.floor(s.rnd() * tints.length); const xa0 = cx + (j - cols / 2) * cw * spreadA; const xa1 = cx + (j + 1 - cols / 2) * cw * spreadA; const xb0 = cx + (j - cols / 2) * cw * spreadB; const xb1 = cx + (j + 1 - cols / 2) * cw * spreadB; paths[c] += `M${f(xa0)} ${f(ya)}L${f(xa1)} ${f(ya)}L${f(xb1)} ${f(yb)}L${f(xb0)} ${f(yb)}Z `; } } let speed = ""; for (let i = 0; i < 14; i++) { const x = cx - R * 1.2 + s.rnd() * R * 2.4; const y = cy - R * 1.2 + s.rnd() * R * 1.4; speed += `M${f(x)} ${f(y)}v${f(R * (0.15 + s.rnd() * 0.35))} `; } const dx = cx; const dy = cy - R * 0.32; const rx = cx + R * 1.12; const rungs: Array<[number, number, string]> = [[rx, cy - R * 0.95, "1.3× TERMINAL"], [rx, cy - R * 0.45, "3× CLOUD DECK"], [rx, cy + R * 0.05, "10× TREETOPS"], [rx, cy + R * 0.55, "40× GROUND RUSH"]]; return ( {paths.map((d, i) => (d ? : null))} {!compact ? ( <> ) : null} {!compact ? : null} {!compact ? : null} ); } /* 28 — CORE MELTDOWN: reactor vessel with glowing fuel rods, vertical temperature gauge with a red limit */ function CoreMeltdown(s: Stage) { const { cx, cy, R, id, p, compact } = s; const vx = cx - R * 0.32; const vw = R * 1.1; const top = cy - R * 1.02; const bottom = cy + R * 0.98; let rods = ""; for (let i = 0; i < 5; i++) rods += `M${f(vx - vw * 0.36 + i * vw * 0.18)} ${f(top + R * 0.25)}h${f(vw * 0.08)}v${f(bottom - top - R * 0.5)}h${f(-vw * 0.08)}z `; let bands = ""; for (let i = 1; i < 4; i++) bands += `M${f(vx - vw / 2)} ${f(top + ((bottom - top) * i) / 4)}h${f(vw)} `; const gx = cx + R * 0.7; const gw = R * 0.22; const gTop = cy - R * 1.02; const gBot = cy + R * 0.9; const level = gBot - (gBot - gTop) * 0.74; const limit = gBot - (gBot - gTop) * 0.82; let grad = ""; for (let i = 0; i <= 20; i++) grad += `M${f(gx + gw / 2 + 2)} ${f(gBot - ((gBot - gTop) * i) / 20)}h${f(i % 5 === 0 ? R * 0.07 : R * 0.035)} `; const rungs: Array<[number, number, string]> = [[gx + gw / 2 + R * 0.12, gBot - (gBot - gTop) * 0.2, "1.5×"], [gx + gw / 2 + R * 0.12, gBot - (gBot - gTop) * 0.45, "3×"], [gx + gw / 2 + R * 0.12, gBot - (gBot - gTop) * 0.65, "8×"], [gx + gw / 2 + R * 0.12, gBot - (gBot - gTop) * 0.8, "25×"]]; let readouts = ""; for (let i = 0; i < 9; i++) readouts += `M${f(cx - R * 1.1 + i * R * 0.25)} ${f(bottom + R * 0.12)}h${f(R * 0.16)}v${f(R * 0.05)}h${f(-R * 0.16)}z `; return ( {!compact ? : null} {!compact ? : null} {!compact ? : null} {!compact ? : null} {!compact ? : null} ); } /* 29 — STORM CHASE: truck racing down a highway into a tornado, debris and lightning */ function StormChase(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const ground = cy + R * 0.72; const fx = cx + R * 0.42; const funnel = `M${f(fx - R * 0.95)} ${f(cy - R * 1.1)} C${f(fx - R * 0.6)} ${f(cy - R * 0.4)} ${f(fx - R * 0.35)} ${f(cy + R * 0.2)} ${f(fx - R * 0.12)} ${f(ground)} L${f(fx + R * 0.12)} ${f(ground)} C${f(fx + R * 0.3)} ${f(cy + R * 0.2)} ${f(fx + R * 0.55)} ${f(cy - R * 0.4)} ${f(fx + R * 0.95)} ${f(cy - R * 1.1)} Z`; let ribs = ""; for (let i = 1; i < 6; i++) { const t = i / 6; const y = cy - R * 1.1 + (ground - (cy - R * 1.1)) * t; const hw = R * (0.95 - 0.83 * t); ribs += `M${f(fx - hw)} ${f(y)} q${f(hw)} ${f(R * 0.1)} ${f(hw * 2)} 0 `; } let debris = ""; for (let i = 0; i < 22; i++) { const a = s.rnd() * PI * 2; const d = R * (0.35 + s.rnd() * 0.85); const x = fx + Math.cos(a) * d; const y = cy - R * 0.2 + Math.sin(a) * d * 0.7; const k = R * (0.02 + s.rnd() * 0.04); debris += `M${f(x)} ${f(y)}l${f(k * 1.5)} ${f(k * 0.6)}l${f(-k * 0.6)} ${f(k * 1.2)}l${f(-k * 1.5)} ${f(-k * 0.6)}z `; } const bolt = `M${f(cx - R * 0.72)} ${f(cy - R * 1.15)} l${f(R * 0.12)} ${f(R * 0.4)} l${f(-R * 0.14)} ${f(R * 0.04)} l${f(R * 0.2)} ${f(R * 0.5)} l${f(-R * 0.12)} ${f(0)} l${f(R * 0.14)} ${f(R * 0.45)}`; const roadL = `M${f(cx - R * 1.6)} ${f(H)} L${f(fx - R * 0.1)} ${f(ground)} L${f(fx + R * 0.1)} ${f(ground)} L${f(cx + R * 0.7)} ${f(H)} Z`; let dashes = ""; for (let i = 0; i < 7; i++) { const t0 = Math.pow(i / 7, 1.4); const t1 = Math.pow((i + 0.45) / 7, 1.4); const px0 = cx - R * 0.45 + (fx - (cx - R * 0.45)) * t0; const py0 = H + (ground - H) * t0; const px1 = cx - R * 0.45 + (fx - (cx - R * 0.45)) * t1; const py1 = H + (ground - H) * t1; dashes += `M${f(px0)} ${f(py0)}L${f(px1)} ${f(py1)} `; } const tx = cx - R * 0.42; const ty = cy + R * 1.05; const rx = cx + R * 1.14; const rungs: Array<[number, number, string]> = [[rx, cy + R * 1.06, "1.5× OUTFLOW"], [rx, cy + R * 0.88, "3× DEBRIS FIELD"], [rx, cy + R * 0.7, "8× WALL CLOUD"], [rx, cy + R * 0.5, "30× VORTEX"]]; return ( {!compact ? : null} {!compact ? : null} ); } /* 30 — ELEVATOR 999: a lit cabin in an endless shaft, tall condensed floor numbers, express floors */ function Elevator999(s: Stage) { const { H, cx, cy, R, id, p, compact } = s; const railL = cx - R * 0.66; const railR = cx + R * 0.66; let beams = ""; for (let y = -R * 0.1; y < H + R; y += R * 0.32) beams += `M${f(railL)} ${f(y)}H${f(railR)} `; const cabW = R * 0.98; const cabH = R * 1.22; const cabY = cy + R * 0.02; const floors = [ { n: "25", m: "×3.4", y: cy + R * 1.0 }, { n: "50", m: "×11", y: cy + R * 0.52 }, { n: "100", m: "×131", y: cy - R * 0.32 }, { n: "250", m: "×198k", y: cy - R * 0.76 }, ]; let chevrons = ""; for (let i = 0; i < 3; i++) chevrons += `M${f(railR + R * 0.2)} ${f(cy - R * 0.05 - i * R * 0.16)}l${f(R * 0.08)} ${f(-R * 0.09)}l${f(R * 0.08)} ${f(R * 0.09)} `; return ( {!compact ? ( <> {floors.map((fl) => ( {fl.n} ))} ) : null} ); } /* ---------------------------------------------------- Beyond Slots originals */ /* 31 — DROPZONE: capsule ricocheting through a peg tower, energy gates, value buckets, Deep Drop below */ function Dropzone(s: Stage) { const { cx, cy, R, id, p, compact } = s; const rows = compact ? 6 : 9; const gap = R * 0.2; const top = cy - R * 1.02; const pegs: Array<[number, number, number]> = []; for (let r = 0; r < rows; r++) for (let k = 0; k <= r + 2; k++) pegs.push([cx + (k - (r + 2) / 2) * gap, top + r * gap, R * 0.022]); let lane = 0; let path = `M${f(cx)} ${f(top - gap * 0.8)}`; const lit: Array<[number, number, number]> = []; for (let r = 0; r < rows; r++) { lane += s.rnd() > 0.5 ? 0.5 : -0.5; const x = cx + lane * gap; path += ` L${f(x)} ${f(top + r * gap + gap * 0.5)}`; lit.push([x + (s.rnd() > 0.5 ? gap / 2 : -gap / 2), top + r * gap, R * 0.03]); } const capR = Math.min(rows - 1, 5); const capX = cx + lane * gap * (capR / rows); const capY = top + capR * gap + gap * 0.5; const bucketY = top + rows * gap + gap * 0.3; const values = ["25", "6", "2.5", "1.4", ".4", "1.4", "2.5", "6", "25"]; const bw = gap * 1.15; const bx0 = cx - (values.length * bw) / 2; const deepY = bucketY + gap * 1.35; const deepPegs = scatterDots(s, 12, [cx - R * 0.9, deepY + gap * 0.3, R * 1.8, gap * 1.4], R * 0.02, R * 0.02); return ( {!compact ? ( <> ) : null} {values.map((v, i) => { const edge = Math.abs(i - 4) / 4; return ( 0.6 ? p.secondary : p.primary} strokeOpacity={0.4 + edge * 0.6} strokeWidth={1} /> {!compact ? 0.6 ? p.secondary : "#e0fbff"} size={R * (edge > 0.6 ? 0.07 : 0.058)} weight={800} tracking="0" /> : null} ); })} {!compact ? ( <> ) : null} ); } /* 32 — GRID//BREAK: 8×8 neon block grid, a wave down one column, detonating cluster, ×2 ×4 ×8 chain */ function GridBreak(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const n = 8; const c = R * 0.225; const gx = cx - (n * c) / 2 - R * 0.2; const gy = cy - (n * c) / 2 - R * 0.05; const col = 5; const tints = [p.primary, p.secondary, "#a78bfa", "#f472b6", "#facc15", "#22d3ee"]; const paths = tints.map(() => ""); const blown = new Set(["5,3", "5,4", "4,4", "5,5", "6,4", "6,5"]); for (let r = 0; r < n; r++) { for (let k = 0; k < n; k++) { if (blown.has(`${k},${r}`)) continue; const t = Math.floor(s.rnd() * tints.length); paths[t] += `M${f(gx + k * c + 2)} ${f(gy + r * c + 2)}h${f(c - 4)}v${f(c - 4)}h${f(-(c - 4))}z `; } } const ex = gx + col * c + c / 2; const ey = gy + 4 * c + c / 2; let shards = ""; for (let i = 0; i < 12; i++) { const a = s.rnd() * PI * 2; const d = c * (0.9 + s.rnd() * 1.6); const k = c * (0.12 + s.rnd() * 0.16); shards += `M${f(ex + Math.cos(a) * d)} ${f(ey + Math.sin(a) * d)}h${f(k)}v${f(k)}h${f(-k)}z `; } let scan = ""; for (let y = 0; y < H; y += 5) scan += `M0 ${f(y)}H${f(W)} `; return ( {paths.map((d, i) => (d ? : null))} {!compact ? ( <> ) : null} ); } /* 33 — THE VAULT: colossal door, five concentric locked layers, digits being revealed at the core */ function TheVault(s: Stage) { const { cx, cy, R, id, p, compact } = s; const vx = cx; const vy = cy + R * 0.08; let bolts = ""; const bl: Array<[number, number, number]> = []; for (let i = 0; i < 18; i++) { const a = (i / 18) * PI * 2; bl.push([vx + Math.cos(a) * R * 1.3, vy + Math.sin(a) * R * 1.3, R * 0.028]); } bolts = dots(bl); let ticks = ""; for (let i = 0; i < 60; i++) { const a = (i / 60) * PI * 2; const len = i % 5 === 0 ? R * 0.1 : R * 0.05; ticks += `M${f(vx + Math.cos(a) * R * 1.05)} ${f(vy + Math.sin(a) * R * 1.05)}L${f(vx + Math.cos(a) * (R * 1.05 - len))} ${f(vy + Math.sin(a) * (R * 1.05 - len))} `; } const gaugeA0 = PI * 0.75; const gaugeA1 = PI * 2.05; const layers = ["1.20×", "1.71×", "3.12×", "7.79×", "31.2×"]; const lx = cx - R * 1.12; const rungs: Array<[number, number, string]> = layers.map((l, i) => [lx, cy + R * 0.9 - i * R * 0.36, l]); const digits = ["4", "7", "?"]; return ( {digits.map((d, i) => ( ))} {!compact ? ( <> ) : null} ); } /* 34 — ORBIT: burning core, concentric orbits with planets/satellites/comets, an impulse beam, Supernova */ function Orbit(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 60, [0, 0, W, H], 0.5, 1.3); const ox = cx; const oy = cy + R * 0.05; const orbits = [0.42, 0.68, 0.94, 1.2]; const debris: Array<[number, number, number]> = []; for (let i = 0; i < 16; i++) { const r = R * orbits[Math.floor(s.rnd() * orbits.length)]; const a = s.rnd() * PI * 2; debris.push([ox + Math.cos(a) * r, oy + Math.sin(a) * r, R * 0.018]); } const at = (k: number, a: number): [number, number] => [ox + Math.cos(a) * R * orbits[k], oy + Math.sin(a) * R * orbits[k]]; const [sx, sy] = at(0, -2.2); const [px, py] = at(1, 0.6); const [cmx, cmy] = at(2, -0.9); const [ggx, ggy] = at(3, 2.62); const [qx, qy] = at(3, -0.62); const beamA = -1.05; const beamLen = R * 1.45; const bx1 = ox + Math.cos(beamA - 0.09) * beamLen; const by1 = oy + Math.sin(beamA - 0.09) * beamLen; const bx2 = ox + Math.cos(beamA + 0.09) * beamLen; const by2 = oy + Math.sin(beamA + 0.09) * beamLen; return ( {orbits.map((k, i) => ( ))} {!compact ? ( <> ) : null} ); } /* 35 — ESCAPE 99: tower cross-section with rooms and a stairwell, checkpoint rail 10 · 25 · 50 · 75 · 99 */ function Escape99(s: Stage) { const { H, cx, cy, R, id, p, compact } = s; const tw = R * 1.16; const tx = cx - R * 0.3; const top = cy - R * 1.12; const fh = R * 0.29; const nFloors = Math.floor((H - top) / fh) + 1; let floors = ""; for (let i = 1; i < nFloors; i++) floors += `M${f(tx - tw / 2)} ${f(top + i * fh)}h${f(tw)} `; let stairs = ""; for (let i = 0; i < nFloors; i++) { const y = top + i * fh; stairs += `M${f(tx - R * 0.1)} ${f(y + fh)} l${f(R * 0.05)} ${f(-fh * 0.25)} h${f(R * 0.05)} l${f(R * 0.05)} ${f(-fh * 0.25)} h${f(R * 0.05)} l${f(R * 0.05)} ${f(-fh * 0.25)} `; } const roomX = (side: -1 | 1) => tx + side * (R * 0.1 + (tw / 2 - R * 0.1) / 2); const fy = (i: number) => top + i * fh + fh / 2; let crenel = ""; for (let i = 0; i < 6; i++) crenel += `M${f(tx - tw / 2 + i * (tw / 6) + 2)} ${f(top)}v${f(-R * 0.08)}h${f(tw / 12)}v${f(R * 0.08)}z `; let spikes = ""; for (let i = 0; i < 5; i++) spikes += `M${f(roomX(-1) - R * 0.14 + i * R * 0.07)} ${f(fy(4) + fh * 0.4)}l${f(R * 0.035)} ${f(-fh * 0.55)}l${f(R * 0.035)} ${f(fh * 0.55)}z `; const hx = tx; const hy = fy(3) + fh * 0.05; const rx = cx + R * 0.9; const cps = [ { n: "10", m: "×1.4", t: 0 }, { n: "25", m: "×2.9", t: 0.25 }, { n: "50", m: "×18", t: 0.5 }, { n: "75", m: "×190", t: 0.75 }, { n: "99", m: "×3k", t: 1 }, ]; const railTop = cy - R * 0.72; const railBot = cy + R * 1.05; const leaves = scatterDots(s, 16, [cx - R * 1.25, cy + R * 0.4, R * 0.7, R * 0.9], 1.2, 2.6); return ( {!compact ? ( <> {cps.map((cp) => { const y = railBot - (railBot - railTop) * cp.t; const summit = cp.n === "99"; return ( ); })} ) : null} ); } /* Fallback for unknown slugs: palette glow + the game's top symbols if known. */ function Generic(s: Stage) { const { W, H, cx, cy, R, id, p, compact } = s; const stars = scatterDots(s, 40, [0, 0, W, H], 0.5, 1.4); return ( {!compact ? ( <> ) : null} ); } const SCENES: Record React.ReactNode> = { "neon-vault": NeonVault, "cosmic-collapse": CosmicCollapse, "golden-emperor": GoldenEmperor, "diamond-heist": DiamondHeist, "arctic-fortune": ArcticFortune, "inferno-reels": InfernoReels, "quantum-jackpot": QuantumJackpot, "midnight-tokyo": MidnightTokyo, "pharaoh-protocol": PharaohProtocol, "lucky-circuit": LuckyCircuit, "void-miner": VoidMiner, "royal-circuit": RoyalCircuit, "dragon-core": DragonCore, "deep-treasure": DeepTreasure, "moonbase-77": Moonbase77, "wild-temple": WildTemple, "zero-gravity": ZeroGravity, "reel-reactor": ReelReactor, obsidian: Obsidian, "spinza-original": SpinzaOriginal, /* Risk Games */ skyfall: Skyfall, "deep-dive": DeepDive, "rocket-run": RocketRun, "bank-heist": BankHeist, volcano: Volcano, "black-hole": BlackHole, freefall: Freefall, "core-meltdown": CoreMeltdown, "storm-chase": StormChase, "elevator-999": Elevator999, /* Beyond Slots */ dropzone: Dropzone, "grid-break": GridBreak, "the-vault": TheVault, orbit: Orbit, "escape-99": Escape99, }; /* ------------------------------------------------------------------- title */ interface TitleSpec { caps?: boolean; /** Letter spacing in em. */ tracking?: number; weight?: 700 | 800; /** Solid fill colour or a [from, to] gradient. */ fill?: string | [string, string]; glow?: string; eyebrow?: string; eyebrowColor?: string; align?: "left" | "center"; /** Force two lines on the card (long names). */ split?: boolean; /** Second line rendered small with wide tracking (e.g. PHARAOH / protocol). */ subline?: boolean; skew?: number; /** Hard offset shadow colour (retro). */ shadow?: string; /** Carved outline. */ outline?: string; /** Scale factor applied to the fitted size (Obsidian whispers). */ scale?: number; /** Monospace face (control-room / puzzle-grid voices). */ mono?: boolean; /** Horizontal glyph stretch: < 1 tall condensed, > 1 extended. */ stretch?: number; /** A substring of the name rendered in another colour (GRID//BREAK). */ accent?: { text: string; fill: string }; } const TITLES: Record = { "neon-vault": { caps: true, tracking: -0.03, weight: 800, glow: "#00f0ff", eyebrow: "CRACK THE GRID", eyebrowColor: "#ff2bd6" }, "cosmic-collapse": { caps: false, tracking: -0.05, weight: 800, fill: ["#ffffff", "#c084fc"], glow: "#a78bfa", eyebrow: "EVENT HORIZON", eyebrowColor: "#67e8f9" }, "golden-emperor": { caps: true, tracking: 0.14, weight: 700, fill: ["#fff1c2", "#d4a72c"], glow: "#ffd166", align: "center", eyebrow: "IMPERIAL DYNASTY", eyebrowColor: "#e8b4b8", split: true }, "diamond-heist": { caps: true, tracking: -0.02, weight: 800, fill: ["#ffffff", "#bfefff"], glow: "#7dd3fc", eyebrow: "MINI · MINOR · MAJOR · GRAND", eyebrowColor: "#c0c0c0" }, "arctic-fortune": { caps: true, tracking: 0.02, weight: 700, fill: ["#ffffff", "#bfe9ff"], glow: "#7dd3fc", eyebrow: "AVALANCHE MULTIPLIER", eyebrowColor: "#a78bfa" }, "inferno-reels": { caps: true, tracking: -0.04, weight: 800, fill: ["#ffd166", "#ff4500"], glow: "#ff7a1a", eyebrow: "EXPLODING WILDS", eyebrowColor: "#ffb347" }, "quantum-jackpot": { caps: false, tracking: -0.04, weight: 800, fill: ["#ffffff", "#67e8f9"], glow: "#22d3ee", eyebrow: "ONE SYMBOL · SEVERAL STATES", eyebrowColor: "#c084fc" }, "midnight-tokyo": { caps: true, tracking: 0.06, weight: 800, fill: ["#ffffff", "#ff2bd6"], glow: "#ff2bd6", eyebrow: "SHIBUYA · 02:00", eyebrowColor: "#22d3ee" }, "pharaoh-protocol": { caps: true, tracking: 0.04, weight: 800, fill: ["#fff7d6", "#ffd166"], glow: "#fbbf24", subline: true, eyebrowColor: "#22d3ee" }, "lucky-circuit": { caps: true, tracking: 0.0, weight: 800, fill: "#facc15", shadow: "#f472b6", eyebrow: "INSERT COIN · WIN OFTEN", eyebrowColor: "#22d3ee" }, "void-miner": { caps: true, tracking: 0.1, weight: 700, fill: ["#e9d5ff", "#fbbf24"], glow: "#a78bfa", eyebrow: "DARK MATTER CORE", eyebrowColor: "#fbbf24" }, "royal-circuit": { caps: true, tracking: -0.02, weight: 800, fill: ["#fff7d6", "#ffd166"], glow: "#ffd166", skew: -10, eyebrow: "8 LAPS TO THE PIT LANE", eyebrowColor: "#ef4444" }, "dragon-core": { caps: true, tracking: -0.03, weight: 800, fill: ["#ffd166", "#ef4444"], glow: "#f97316", eyebrow: "TWENTY SCALES", eyebrowColor: "#22d3ee" }, "deep-treasure": { caps: false, tracking: -0.04, weight: 800, fill: ["#dffaff", "#22d3ee"], glow: "#22d3ee", eyebrow: "DESCENT ×1 → ×10", eyebrowColor: "#fbbf24" }, "moonbase-77": { caps: true, tracking: 0.12, weight: 700, fill: "#fb923c", glow: "#fb923c", eyebrow: "LOW GRAVITY · WILDS DRIFT", eyebrowColor: "#22d3ee" }, "wild-temple": { caps: true, tracking: 0.05, weight: 800, fill: ["#fde68a", "#fbbf24"], outline: "#0a1a10", glow: "#34d399", eyebrow: "THE JAGUAR MOVES", eyebrowColor: "#34d399" }, "zero-gravity": { caps: true, tracking: 0.22, weight: 700, fill: ["#ffffff", "#a78bfa"], glow: "#22d3ee", eyebrow: "5×3 → 6×4 → 7×5", eyebrowColor: "#67e8f9" }, "reel-reactor": { caps: true, tracking: -0.03, weight: 800, fill: "#a3e635", glow: "#22c55e", eyebrow: "EVERY DEAD SPIN HEATS THE CORE", eyebrowColor: "#facc15" }, obsidian: { caps: true, tracking: 0.42, weight: 700, fill: "#f5e6c8", align: "center", scale: 0.62 }, "spinza-original": { caps: true, tracking: -0.02, weight: 800, fill: ["#ffffff", "#c4b5fd"], glow: "#8b5cf6", subline: true, eyebrowColor: "#22d3ee" }, /* Risk Games — each in its own voice */ skyfall: { caps: true, tracking: 0.2, weight: 700, fill: ["#ffffff", "#7dd3fc"], glow: "#7dd3fc", eyebrow: "ALTITUDE PAYS · RISK GAME", eyebrowColor: "#fbbf24" }, "deep-dive": { caps: true, tracking: 0.3, weight: 700, fill: ["#dffaff", "#22d3ee"], glow: "#0e7490", eyebrow: "1,000 M = ×2 · 3,000 M = ×5", eyebrowColor: "#34d399" }, "rocket-run": { caps: true, tracking: -0.05, weight: 800, skew: -9, fill: ["#fff7ed", "#fb923c"], glow: "#fb923c", eyebrow: "MARS OR BUST", eyebrowColor: "#f472b6" }, "bank-heist": { caps: true, tracking: 0.08, weight: 800, fill: ["#f5e6c8", "#c9a961"], glow: "#ef4444", eyebrow: "THE BAG KEEPS FILLING", eyebrowColor: "#ef4444" }, volcano: { caps: true, tracking: -0.01, weight: 800, fill: ["#ffd166", "#ff5c3d"], outline: "#2a0c07", glow: "#ff8a5c", eyebrow: "DEEPER CRYSTALS · RISING LAVA", eyebrowColor: "#fbbf24" }, "black-hole": { caps: true, tracking: 0.34, weight: 700, stretch: 1.14, fill: ["#ffffff", "#a78bfa"], glow: "#f0abfc", eyebrow: "TIME SLOWS · THE MULTIPLIER DOESN'T", eyebrowColor: "#f0abfc" }, freefall: { caps: true, tracking: -0.06, weight: 800, skew: 14, fill: ["#ffffff", "#60a5fa"], glow: "#60a5fa", eyebrow: "PULL LATE · NOT TOO LATE", eyebrowColor: "#f97316" }, "core-meltdown": { caps: true, tracking: 0.1, weight: 700, mono: true, fill: "#bef264", glow: "#a3e635", eyebrow: "TEMP RISING · LIMIT 100 %", eyebrowColor: "#facc15" }, "storm-chase": { caps: true, tracking: -0.03, weight: 800, skew: -6, fill: ["#f8fafc", "#94a3b8"], glow: "#a3e635", eyebrow: "DRIVE INTO THE WALL CLOUD", eyebrowColor: "#a3e635" }, "elevator-999": { caps: true, tracking: 0.04, weight: 800, stretch: 0.74, fill: ["#f3e2ad", "#c9a961"], glow: "#38bdf8", eyebrow: "EVERY FLOOR IS A DECISION", eyebrowColor: "#38bdf8", split: true }, /* Beyond Slots */ dropzone: { caps: true, tracking: 0.22, weight: 800, fill: ["#ffffff", "#22d3ee"], glow: "#f472b6", eyebrow: "RELEASE THE CAPSULE", eyebrowColor: "#f472b6" }, "grid-break": { caps: true, tracking: 0.02, weight: 700, mono: true, fill: "#bef264", accent: { text: "//", fill: "#38bdf8" }, glow: "#a3e635", eyebrow: "ONE WAVE · INFINITE CHAINS", eyebrowColor: "#38bdf8" }, "the-vault": { caps: true, tracking: 0.3, weight: 700, fill: ["#fff1c2", "#c9a961"], glow: "#c9a961", eyebrow: "FIVE LAYERS · ONE DECISION AT EACH", eyebrowColor: "#38bdf8" }, orbit: { caps: true, tracking: 0.5, weight: 700, fill: ["#ffffff", "#f0abfc"], glow: "#facc15", eyebrow: "FIRE THE IMPULSE · RIDE THE CHAOS", eyebrowColor: "#facc15" }, "escape-99": { caps: true, tracking: -0.02, weight: 800, fill: ["#d1fae5", "#34d399"], outline: "#06090a", glow: "#f97316", eyebrow: "99 FLOORS · CASH OUT ON ANY", eyebrowColor: "#f97316", split: true }, }; function splitLines(name: string, spec: TitleSpec, v: ArtVariant): string[] { const words = name.trim().split(/\s+/); if (words.length < 2) return [name]; if (spec.subline) return [words[0], words.slice(1).join(" ")]; const twoLines = v === "card" ? spec.split || name.length > 9 : v === "hero" || v === "banner" ? name.length > 14 : name.length > 11; if (!twoLines) return [name]; const mid = Math.ceil(words.length / 2); return [words.slice(0, mid).join(" "), words.slice(mid).join(" ")]; } function estimateWidth(text: string, caps: boolean, tracking: number, mono = false, stretch = 1): number { const cw = mono ? 0.64 : caps ? 0.7 : 0.6; return (text.length * (cw + tracking) - tracking) * stretch; } function Title({ s, name, showName }: { s: Stage; name: string; showName: boolean }) { if (!showName) return null; const { W, H, v, id, p } = s; const spec = TITLES[s.slug] ?? { caps: true, tracking: -0.03, weight: 800, glow: p.glow }; const caps = spec.caps !== false; const tracking = spec.tracking ?? -0.03; const weight = spec.weight ?? 800; const stretch = spec.stretch ?? 1; const face = spec.mono ? MONO : FONT; const rawLines = splitLines(name, spec, v); const lines = caps ? rawLines.map((l) => l.toUpperCase()) : rawLines; const cfg = v === "card" ? { pad: 22, maxFont: 54, avail: W - 44, eye: 11 } : v === "hero" ? { pad: 84, maxFont: 168, avail: W * 0.44, eye: 22 } : v === "banner" ? { pad: 56, maxFont: 112, avail: W * 0.42, eye: 18 } : { pad: 14, maxFont: 26, avail: W - 28, eye: 0 }; const fitLines = spec.subline ? [lines[0]] : lines; const est = Math.max(...fitLines.map((l) => estimateWidth(l, caps, tracking, spec.mono, stretch)), 1); let size = Math.min(cfg.maxFont, cfg.avail / est); if (spec.scale) size *= spec.scale; const subSize = size * 0.34; const lineH = size * 0.94; const totalH = spec.subline ? size + subSize * 1.9 : lineH * lines.length; const center = spec.align === "center"; const x = center ? W / 2 : cfg.pad; const anchor = center ? "middle" : "start"; const baseY = v === "card" || v === "tile" ? H - cfg.pad - (totalH - size) : H / 2 + size / 2 - (totalH - size) / 2; const fill = Array.isArray(spec.fill) ? `url(#${id}-title)` : (spec.fill ?? "#fff"); const eyebrow = spec.eyebrow && cfg.eye > 0 && v !== "tile"; const eyeSize = v === "hero" ? size * 0.14 : v === "banner" ? size * 0.15 : Math.max(9.5, size * 0.2); const fontStyle: React.CSSProperties = { fontFamily: face, letterSpacing: `${tracking}em` }; const render = (props: React.SVGProps, key: string, main = false) => lines.map((l, i) => { const isSub = spec.subline && i === 1; const y = isSub ? baseY + subSize * 1.7 : baseY + i * lineH; const base: React.CSSProperties = isSub ? { fontFamily: face, letterSpacing: "0.32em" } : fontStyle; const ops: string[] = []; if (spec.skew) ops.push(`skewX(${spec.skew})`); if (stretch !== 1 && !isSub) ops.push(`scale(${stretch} 1)`); const transform = ops.length ? `translate(${f(x)} ${f(y)}) ${ops.join(" ")} translate(${f(-x)} ${f(-y)})` : undefined; const acc = spec.accent; const k = main && acc && !isSub ? l.indexOf(acc.text) : -1; return ( {k >= 0 && acc ? ( <> {l.slice(0, k)} {acc.text} {l.slice(k + acc.text.length)} ) : ( l )} ); }); return ( {Array.isArray(spec.fill) ? ( ) : null} {eyebrow ? ( {spec.eyebrow} ) : null} {spec.glow && v !== "tile" ? render({ fill: spec.glow, fillOpacity: 0.5, style: { filter: `blur(${f(size * 0.16)}px)` } }, "g") : null} {spec.shadow ? render({ fill: spec.shadow, transform: `translate(${f(size * 0.07)} ${f(size * 0.07)})` }, "s") : null} {spec.outline ? render({ fill: "none", stroke: spec.outline, strokeWidth: f(size * 0.09), strokeLinejoin: "round" }, "o") : null} {render({ fill }, "t", true)} ); } /* ----------------------------------------------------------------- GameArt */ export function GameArt({ slug, name, palette, variant = "card", className, showName = true, label }: GameArtProps) { const reactId = React.useId().replace(/[^a-zA-Z0-9]/g, ""); const id = `ga${reactId}`; const game = GAMES_BY_SLUG.get(slug); const { w: W, h: H } = DIMS[variant]; const showTitle = showName; const { cx, cy, R } = focal(variant, W, H, showTitle && variant === "card"); const rnd = mulberry32(hashString(`${slug}:${variant}`)); const fallback: SymbolStyle = { shape: "gem", color: palette.primary, accent: palette.glow, glow: 0.8 }; const bySymbol = new Map((game?.symbols ?? []).map((x) => [x.id, x])); const stage: Stage = { W, H, v: variant, slug, cx, cy, R, p: palette, id, rnd, game, sym: (sid) => bySymbol.get(sid)?.style ?? fallback, tier: (sid) => bySymbol.get(sid)?.tier ?? "premium", compact: variant === "tile", }; const scene = SCENES[slug] ?? Generic; const bgBottom = slug === "obsidian" ? "#000" : "#04050a"; return ( {/* Scenes are called as plain functions so the seeded PRNG is consumed exactly once per render (SSR == client, StrictMode-safe). */} {scene(stage)} {showTitle && variant !== "tile" ? : null} ); }