SPB Git

spb/spinza

Public

games: theme-specific symbol vocabulary (45 new shapes) replaces generic royals/cubes across the library; presentation exposed on game cards

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 8, 2026) parent f61a85f Browse files

21 changed files +1,672 −632

modified apps/web/src/components/game/symbol-art.ts +356 −0
@@ -310,6 +310,362 @@ export function drawSymbol(style: SymbolStyle, opts: SymbolArtOptions): Containe
310 g.poly([0, 0, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r]).fill(darken(style.color, 0.3)); 310 g.poly([0, 0, r * 0.87, -r * 0.5, r * 0.87, r * 0.5, 0, r]).fill(darken(style.color, 0.3));
311 break; 311 break;
312 } 312 }
313+ /* ----------------------------------------------------------- themed set */
314+ case "lock": {
315+ g.roundRect(-r * 0.75, -r * 0.1, r * 1.5, r * 1.1, r * 0.2).fill(main);
316+ g.arc(0, -r * 0.15, r * 0.5, Math.PI, 0).stroke({ color: accent, width: size * 0.05 });
317+ g.circle(0, r * 0.35, r * 0.16).fill(0x0b0d14);
318+ g.rect(-r * 0.06, r * 0.35, r * 0.12, r * 0.35).fill(0x0b0d14);
319+ break;
320+ }
321+ case "key": {
322+ g.circle(-r * 0.45, -r * 0.35, r * 0.42).stroke({ color: main, width: size * 0.06 });
323+ g.moveTo(-r * 0.15, -r * 0.05).lineTo(r * 0.85, r * 0.95).stroke({ color: main, width: size * 0.06 });
324+ g.moveTo(r * 0.45, r * 0.55).lineTo(r * 0.7, r * 0.3).stroke({ color: accent, width: size * 0.05 });
325+ g.moveTo(r * 0.65, r * 0.75).lineTo(r * 0.9, r * 0.5).stroke({ color: accent, width: size * 0.05 });
326+ break;
327+ }
328+ case "code": {
329+ g.roundRect(-r, -r * 0.7, r * 2, r * 1.4, r * 0.15).fill({ color: darken(style.color, 0.45), alpha: 0.95 });
330+ g.roundRect(-r, -r * 0.7, r * 2, r * 1.4, r * 0.15).stroke({ color: main, width: size * 0.02 });
331+ c.addChild(label(style.label ?? "</>", size * 0.26, main, true));
332+ break;
333+ }
334+ case "badge": {
335+ g.roundRect(-r * 0.7, -r, r * 1.4, r * 2, r * 0.18).fill(main);
336+ g.circle(0, -r * 0.4, r * 0.28).fill(0x0b0d14);
337+ g.roundRect(-r * 0.45, r * 0.05, r * 0.9, r * 0.14, r * 0.05).fill(accent);
338+ g.roundRect(-r * 0.45, r * 0.35, r * 0.6, r * 0.14, r * 0.05).fill(accent);
339+ break;
340+ }
341+ case "mask": {
342+ g.ellipse(0, 0, r, r * 0.6).fill(main);
343+ g.ellipse(-r * 0.4, -r * 0.05, r * 0.28, r * 0.18).fill(0x0b0d14);
344+ g.ellipse(r * 0.4, -r * 0.05, r * 0.28, r * 0.18).fill(0x0b0d14);
345+ g.moveTo(-r * 0.95, -r * 0.1).lineTo(-r * 1.3, -r * 0.6).stroke({ color: accent, width: size * 0.025 });
346+ g.moveTo(r * 0.95, -r * 0.1).lineTo(r * 1.3, -r * 0.6).stroke({ color: accent, width: size * 0.025 });
347+ break;
348+ }
349+ case "glove": {
350+ g.roundRect(-r * 0.55, -r * 0.2, r * 1.1, r * 1.1, r * 0.25).fill(main);
351+ for (let i = 0; i < 4; i++) g.roundRect(-r * 0.55 + i * r * 0.28, -r, r * 0.22, r * 0.9, r * 0.1).fill(main);
352+ g.roundRect(-r * 0.9, -r * 0.1, r * 0.35, r * 0.7, r * 0.15).fill(main);
353+ g.roundRect(-r * 0.55, r * 0.6, r * 1.1, r * 0.3, r * 0.08).fill(accent);
354+ break;
355+ }
356+ case "rope": {
357+ g.circle(0, 0, r * 0.75).stroke({ color: main, width: size * 0.09 });
358+ g.circle(0, 0, r * 0.75).stroke({ color: accent, width: size * 0.02, alpha: 0.6 });
359+ for (let i = 0; i < 10; i++) {
360+ const a = (i / 10) * Math.PI * 2;
361+ g.moveTo(Math.cos(a) * r * 0.62, Math.sin(a) * r * 0.62).lineTo(Math.cos(a + 0.3) * r * 0.88, Math.sin(a + 0.3) * r * 0.88).stroke({ color: darken(style.color, 0.3), width: size * 0.02 });
362+ }
363+ break;
364+ }
365+ case "bag": {
366+ g.moveTo(-r * 0.75, r * 0.9).bezierCurveTo(-r * 1.1, -r * 0.3, -r * 0.3, -r * 0.6, 0, -r * 0.55).bezierCurveTo(r * 0.3, -r * 0.6, r * 1.1, -r * 0.3, r * 0.75, r * 0.9).lineTo(-r * 0.75, r * 0.9).fill(main);
367+ g.roundRect(-r * 0.3, -r * 0.95, r * 0.6, r * 0.35, r * 0.1).fill(accent);
368+ g.moveTo(-r * 0.35, r * 0.3).lineTo(r * 0.35, r * 0.3).stroke({ color: darken(style.color, 0.3), width: size * 0.03 });
369+ break;
370+ }
371+ case "lantern": {
372+ g.roundRect(-r * 0.7, -r * 0.6, r * 1.4, r * 1.2, r * 0.45).fill(main);
373+ g.rect(-r * 0.35, -r * 0.9, r * 0.7, r * 0.2).fill(accent);
374+ g.rect(-r * 0.35, r * 0.6, r * 0.7, r * 0.2).fill(accent);
375+ g.moveTo(0, r * 0.8).lineTo(0, r).stroke({ color: accent, width: size * 0.03 });
376+ for (let i = -1; i <= 1; i++) g.moveTo(i * r * 0.3, -r * 0.55).lineTo(i * r * 0.3, r * 0.55).stroke({ color: darken(style.color, 0.3), width: size * 0.015 });
377+ break;
378+ }
379+ case "fan": {
380+ for (let i = -3; i <= 3; i++) {
381+ const a = -Math.PI / 2 + i * 0.28;
382+ g.moveTo(0, r * 0.7).lineTo(Math.cos(a) * r * 1.05, r * 0.7 + Math.sin(a) * r * 1.05).stroke({ color: i % 2 ? main : accent, width: size * 0.07 });
383+ }
384+ g.circle(0, r * 0.7, r * 0.14).fill(0x0b0d14);
385+ break;
386+ }
387+ case "scroll": {
388+ g.roundRect(-r * 0.7, -r * 0.85, r * 1.4, r * 1.7, r * 0.1).fill({ color: accent, alpha: 0.9 });
389+ g.roundRect(-r * 0.85, -r, r * 1.7, r * 0.3, r * 0.15).fill(main);
390+ g.roundRect(-r * 0.85, r * 0.7, r * 1.7, r * 0.3, r * 0.15).fill(main);
391+ for (let i = 0; i < 3; i++) g.rect(-r * 0.45, -r * 0.5 + i * r * 0.32, r * 0.9, r * 0.08).fill({ color: darken(style.color, 0.4), alpha: 0.7 });
392+ break;
393+ }
394+ case "cherry": {
395+ g.moveTo(-r * 0.3, r * 0.3).bezierCurveTo(-r * 0.2, -r * 0.5, r * 0.2, -r * 0.9, r * 0.5, -r).stroke({ color: 0x3ddc97, width: size * 0.035 });
396+ g.moveTo(r * 0.45, r * 0.3).bezierCurveTo(r * 0.5, -r * 0.4, r * 0.5, -r * 0.8, r * 0.5, -r).stroke({ color: 0x3ddc97, width: size * 0.035 });
397+ g.circle(-r * 0.35, r * 0.45, r * 0.42).fill(main);
398+ g.circle(r * 0.45, r * 0.45, r * 0.42).fill(main);
399+ g.circle(-r * 0.5, r * 0.3, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 });
400+ g.circle(r * 0.3, r * 0.3, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 });
401+ break;
402+ }
403+ case "seven": {
404+ g.poly([-r * 0.8, -r, r * 0.8, -r, r * 0.8, -r * 0.55, -r * 0.05, r, -r * 0.55, r, r * 0.2, -r * 0.55, -r * 0.8, -r * 0.55]).fill(main);
405+ g.poly([-r * 0.8, -r, r * 0.8, -r, r * 0.8, -r * 0.55, -r * 0.05, r, -r * 0.55, r, r * 0.2, -r * 0.55, -r * 0.8, -r * 0.55]).stroke({ color: accent, width: size * 0.03 });
406+ break;
407+ }
408+ case "bell": {
409+ g.moveTo(-r * 0.75, r * 0.5).bezierCurveTo(-r * 0.75, -r * 0.5, -r * 0.3, -r * 0.9, 0, -r * 0.9).bezierCurveTo(r * 0.3, -r * 0.9, r * 0.75, -r * 0.5, r * 0.75, r * 0.5).lineTo(-r * 0.75, r * 0.5).fill(main);
410+ g.roundRect(-r * 0.9, r * 0.45, r * 1.8, r * 0.18, r * 0.09).fill(accent);
411+ g.circle(0, r * 0.8, r * 0.15).fill(accent);
412+ g.circle(0, -r * 0.95, r * 0.1).fill(accent);
413+ break;
414+ }
415+ case "joystick": {
416+ g.roundRect(-r * 0.9, r * 0.3, r * 1.8, r * 0.6, r * 0.15).fill(darken(style.color, 0.4));
417+ g.rect(-r * 0.08, -r * 0.4, r * 0.16, r * 0.8).fill(0x9aa3b5);
418+ g.circle(0, -r * 0.55, r * 0.35).fill(main);
419+ g.circle(-r * 0.12, -r * 0.65, r * 0.1).fill({ color: 0xffffff, alpha: 0.5 });
420+ g.circle(r * 0.5, r * 0.6, r * 0.12).fill(accent);
421+ break;
422+ }
423+ case "torii": {
424+ g.rect(-r, -r * 0.85, r * 2, r * 0.18).fill(main);
425+ g.rect(-r * 0.8, -r * 0.5, r * 1.6, r * 0.12).fill(main);
426+ g.rect(-r * 0.6, -r * 0.5, r * 0.16, r * 1.5).fill(main);
427+ g.rect(r * 0.44, -r * 0.5, r * 0.16, r * 1.5).fill(main);
428+ g.rect(-r * 0.08, -r * 0.5, r * 0.16, r * 0.35).fill(accent);
429+ break;
430+ }
431+ case "katana": {
432+ g.moveTo(-r * 0.9, r * 0.9).lineTo(r * 0.7, -r * 0.7).stroke({ color: 0xdfe6f0, width: size * 0.05 });
433+ g.moveTo(-r * 0.9, r * 0.9).lineTo(r * 0.7, -r * 0.7).stroke({ color: 0xffffff, width: size * 0.015, alpha: 0.7 });
434+ g.moveTo(-r * 0.55, r * 0.35).lineTo(-r * 0.25, r * 0.65).stroke({ color: accent, width: size * 0.05 });
435+ g.moveTo(-r * 0.9, r * 0.9).lineTo(-r * 0.6, r * 0.6).stroke({ color: main, width: size * 0.08 });
436+ break;
437+ }
438+ case "cassette": {
439+ g.roundRect(-r, -r * 0.65, r * 2, r * 1.3, r * 0.12).fill(main);
440+ g.roundRect(-r * 0.75, -r * 0.4, r * 1.5, r * 0.55, r * 0.08).fill(darken(style.color, 0.45));
441+ g.circle(-r * 0.4, -r * 0.12, r * 0.16).fill(0x0b0d14).circle(r * 0.4, -r * 0.12, r * 0.16).fill(0x0b0d14);
442+ g.rect(-r * 0.6, r * 0.3, r * 1.2, r * 0.14).fill(accent);
443+ break;
444+ }
445+ case "ankh": {
446+ g.ellipse(0, -r * 0.55, r * 0.35, r * 0.42).stroke({ color: main, width: size * 0.06 });
447+ g.rect(-r * 0.08, -r * 0.1, r * 0.16, r * 1.1).fill(main);
448+ g.rect(-r * 0.7, -r * 0.1, r * 1.4, r * 0.16).fill(main);
449+ g.circle(0, -r * 0.55, r * 0.08).fill(accent);
450+ break;
451+ }
452+ case "scarab": {
453+ g.ellipse(0, r * 0.15, r * 0.6, r * 0.7).fill(main);
454+ g.circle(0, -r * 0.6, r * 0.28).fill(darken(style.color, 0.25));
455+ g.moveTo(0, -r * 0.4).lineTo(0, r * 0.8).stroke({ color: accent, width: size * 0.02 });
456+ for (const sgn of [-1, 1]) {
457+ g.moveTo(sgn * r * 0.55, -r * 0.1).lineTo(sgn * r * 0.95, -r * 0.5).stroke({ color: accent, width: size * 0.035 });
458+ g.moveTo(sgn * r * 0.6, r * 0.3).lineTo(sgn * r * 1.0, r * 0.4).stroke({ color: accent, width: size * 0.035 });
459+ }
460+ break;
461+ }
462+ case "pyramid": {
463+ g.poly([0, -r, r, r * 0.75, -r, r * 0.75]).fill(main);
464+ g.poly([0, -r, r, r * 0.75, 0, r * 0.75]).fill(darken(style.color, 0.25));
465+ g.poly([0, -r, r * 0.25, -r * 0.55, -r * 0.25, -r * 0.55]).fill(accent);
466+ break;
467+ }
468+ case "warning": {
469+ g.poly([0, -r, r * 0.95, r * 0.75, -r * 0.95, r * 0.75]).fill(main);
470+ g.poly([0, -r * 0.6, r * 0.55, r * 0.55, -r * 0.55, r * 0.55]).fill(0x0b0d14);
471+ g.rect(-r * 0.08, -r * 0.3, r * 0.16, r * 0.5).fill(main);
472+ g.circle(0, r * 0.38, r * 0.09).fill(main);
473+ break;
474+ }
475+ case "valve": {
476+ g.circle(0, 0, r * 0.85).stroke({ color: main, width: size * 0.07 });
477+ for (let i = 0; i < 4; i++) {
478+ const a = (i / 4) * Math.PI * 2;
479+ g.moveTo(0, 0).lineTo(Math.cos(a) * r * 0.85, Math.sin(a) * r * 0.85).stroke({ color: main, width: size * 0.05 });
480+ }
481+ g.circle(0, 0, r * 0.25).fill(accent);
482+ break;
483+ }
484+ case "gauge": {
485+ g.arc(0, r * 0.2, r * 0.9, Math.PI, 0).stroke({ color: darken(style.color, 0.3), width: size * 0.09 });
486+ g.arc(0, r * 0.2, r * 0.9, Math.PI, Math.PI * 1.6).stroke({ color: main, width: size * 0.09 });
487+ g.moveTo(0, r * 0.2).lineTo(r * 0.45, -r * 0.5).stroke({ color: accent, width: size * 0.04 });
488+ g.circle(0, r * 0.2, r * 0.12).fill(accent);
489+ break;
490+ }
491+ case "barrel": {
492+ g.roundRect(-r * 0.65, -r * 0.9, r * 1.3, r * 1.8, r * 0.2).fill(main);
493+ g.rect(-r * 0.65, -r * 0.45, r * 1.3, r * 0.12).fill(darken(style.color, 0.4));
494+ g.rect(-r * 0.65, r * 0.33, r * 1.3, r * 0.12).fill(darken(style.color, 0.4));
495+ polygon(g, 0, 0, r * 0.28, 3, -Math.PI / 2).fill(accent);
496+ break;
497+ }
498+ case "wheel": {
499+ g.circle(0, 0, r * 0.95).fill(0x0b0d14);
500+ g.circle(0, 0, r * 0.95).stroke({ color: main, width: size * 0.05 });
501+ g.circle(0, 0, r * 0.6).stroke({ color: accent, width: size * 0.03 });
502+ for (let i = 0; i < 5; i++) {
503+ const a = (i / 5) * Math.PI * 2;
504+ g.moveTo(0, 0).lineTo(Math.cos(a) * r * 0.6, Math.sin(a) * r * 0.6).stroke({ color: accent, width: size * 0.035 });
505+ }
506+ g.circle(0, 0, r * 0.14).fill(main);
507+ break;
508+ }
509+ case "helmet": {
510+ g.arc(0, r * 0.1, r * 0.85, Math.PI, 0).fill(main);
511+ g.rect(-r * 0.85, r * 0.1, r * 1.7, r * 0.45).fill(main);
512+ g.roundRect(-r * 0.6, -r * 0.05, r * 1.4, r * 0.4, r * 0.1).fill(0x0b0d14);
513+ g.moveTo(-r * 0.3, -r * 0.7).lineTo(r * 0.3, -r * 0.7).stroke({ color: accent, width: size * 0.04 });
514+ break;
515+ }
516+ case "flag": {
517+ g.rect(-r * 0.85, -r, r * 0.1, r * 2).fill(0x9aa3b5);
518+ for (let i = 0; i < 4; i++) for (let j = 0; j < 3; j++) g.rect(-r * 0.75 + i * r * 0.4, -r + j * r * 0.33, r * 0.4, r * 0.33).fill((i + j) % 2 ? main : accent);
519+ break;
520+ }
521+ case "trophy": {
522+ g.moveTo(-r * 0.7, -r).lineTo(r * 0.7, -r).lineTo(r * 0.45, r * 0.15).lineTo(-r * 0.45, r * 0.15).fill(main);
523+ g.arc(-r * 0.75, -r * 0.55, r * 0.3, Math.PI * 0.5, Math.PI * 1.5).stroke({ color: main, width: size * 0.04 });
524+ g.arc(r * 0.75, -r * 0.55, r * 0.3, -Math.PI * 0.5, Math.PI * 0.5).stroke({ color: main, width: size * 0.04 });
525+ g.rect(-r * 0.1, r * 0.15, r * 0.2, r * 0.4).fill(main);
526+ g.roundRect(-r * 0.5, r * 0.55, r, r * 0.3, r * 0.06).fill(accent);
527+ break;
528+ }
529+ case "idol": {
530+ g.roundRect(-r * 0.55, -r * 0.9, r * 1.1, r * 1.8, r * 0.15).fill(main);
531+ g.rect(-r * 0.4, -r * 0.55, r * 0.8, r * 0.14).fill(0x0b0d14);
532+ g.rect(-r * 0.4, -r * 0.1, r * 0.8, r * 0.14).fill(0x0b0d14);
533+ g.poly([-r * 0.3, r * 0.35, r * 0.3, r * 0.35, 0, r * 0.7]).fill(accent);
534+ break;
535+ }
536+ case "totem": {
537+ for (let i = 0; i < 3; i++) {
538+ g.roundRect(-r * 0.55, -r + i * r * 0.68, r * 1.1, r * 0.6, r * 0.08).fill(i % 2 ? darken(style.color, 0.25) : main);
539+ g.circle(-r * 0.22, -r * 0.7 + i * r * 0.68, r * 0.08).fill(accent);
540+ g.circle(r * 0.22, -r * 0.7 + i * r * 0.68, r * 0.08).fill(accent);
541+ }
542+ break;
543+ }
544+ case "snake": {
545+ g.moveTo(-r * 0.8, r * 0.8).bezierCurveTo(-r * 0.9, -r * 0.2, r * 0.9, r * 0.6, r * 0.6, -r * 0.5).bezierCurveTo(r * 0.4, -r * 1.0, -r * 0.2, -r * 0.6, 0, -r * 0.3).stroke({ color: main, width: size * 0.08 });
546+ g.circle(0, -r * 0.3, r * 0.2).fill(main);
547+ g.circle(r * 0.06, -r * 0.36, r * 0.06).fill(accent);
548+ break;
549+ }
550+ case "rocket": {
551+ g.moveTo(0, -r).bezierCurveTo(r * 0.6, -r * 0.4, r * 0.5, r * 0.4, r * 0.35, r * 0.6).lineTo(-r * 0.35, r * 0.6).bezierCurveTo(-r * 0.5, r * 0.4, -r * 0.6, -r * 0.4, 0, -r).fill(main);
552+ g.poly([-r * 0.35, r * 0.2, -r * 0.75, r * 0.8, -r * 0.35, r * 0.6]).fill(accent);
553+ g.poly([r * 0.35, r * 0.2, r * 0.75, r * 0.8, r * 0.35, r * 0.6]).fill(accent);
554+ g.circle(0, -r * 0.25, r * 0.18).fill(0x0b0d14);
555+ g.poly([-r * 0.2, r * 0.6, r * 0.2, r * 0.6, 0, r]).fill(0xffb347);
556+ break;
557+ }
558+ case "anchor": {
559+ g.circle(0, -r * 0.75, r * 0.2).stroke({ color: main, width: size * 0.05 });
560+ g.rect(-r * 0.07, -r * 0.55, r * 0.14, r * 1.4).fill(main);
561+ g.rect(-r * 0.55, -r * 0.35, r * 1.1, r * 0.12).fill(main);
562+ g.arc(0, r * 0.15, r * 0.75, Math.PI * 0.15, Math.PI * 0.85).stroke({ color: main, width: size * 0.07 });
563+ g.circle(-r * 0.7, r * 0.4, r * 0.08).fill(accent).circle(r * 0.7, r * 0.4, r * 0.08).fill(accent);
564+ break;
565+ }
566+ case "chest": {
567+ g.roundRect(-r * 0.9, -r * 0.2, r * 1.8, r * 1.0, r * 0.1).fill(main);
568+ g.roundRect(-r * 0.9, -r * 0.75, r * 1.8, r * 0.6, r * 0.3).fill(darken(style.color, 0.2));
569+ g.rect(-r * 0.9, -r * 0.2, r * 1.8, r * 0.12).fill(accent);
570+ g.roundRect(-r * 0.15, -r * 0.2, r * 0.3, r * 0.35, r * 0.05).fill(accent);
571+ break;
572+ }
573+ case "trident": {
574+ g.rect(-r * 0.07, -r * 0.4, r * 0.14, r * 1.4).fill(main);
575+ for (const dx of [-0.5, 0, 0.5]) {
576+ g.moveTo(dx * r, -r * 0.3).lineTo(dx * r, -r).stroke({ color: main, width: size * 0.05 });
577+ g.poly([dx * r - r * 0.12, -r * 0.85, dx * r + r * 0.12, -r * 0.85, dx * r, -r * 1.1]).fill(accent);
578+ }
579+ g.rect(-r * 0.5, -r * 0.4, r, r * 0.12).fill(main);
580+ break;
581+ }
582+ case "shell": {
583+ g.arc(0, r * 0.4, r * 0.95, Math.PI, 0).fill(main);
584+ for (let i = -3; i <= 3; i++) g.moveTo(0, r * 0.4).lineTo(Math.cos(-Math.PI / 2 + i * 0.38) * r * 0.95, r * 0.4 + Math.sin(-Math.PI / 2 + i * 0.38) * r * 0.95).stroke({ color: darken(style.color, 0.25), width: size * 0.02 });
585+ g.rect(-r * 0.95, r * 0.4, r * 1.9, r * 0.18).fill(accent);
586+ break;
587+ }
588+ case "planet": {
589+ g.circle(0, 0, r * 0.7).fill(main);
590+ g.ellipse(0, r * 0.05, r * 1.05, r * 0.28).stroke({ color: accent, width: size * 0.04 });
591+ g.circle(-r * 0.25, -r * 0.25, r * 0.18).fill({ color: 0xffffff, alpha: 0.3 });
592+ g.ellipse(r * 0.15, r * 0.15, r * 0.3, r * 0.12).fill({ color: darken(style.color, 0.25), alpha: 0.8 });
593+ break;
594+ }
595+ case "comet": {
596+ g.moveTo(r * 0.5, -r * 0.5).lineTo(-r * 1.0, r * 0.15).lineTo(-r * 0.6, r * 0.6).lineTo(r * 0.5, -r * 0.5).fill({ color: accent, alpha: 0.5 });
597+ g.moveTo(r * 0.5, -r * 0.5).lineTo(-r * 0.7, r * 0.7).stroke({ color: accent, alpha: 0.4, width: size * 0.05 });
598+ g.circle(r * 0.5, -r * 0.5, r * 0.4).fill(main);
599+ g.circle(r * 0.4, -r * 0.6, r * 0.12).fill({ color: 0xffffff, alpha: 0.6 });
600+ break;
601+ }
602+ case "satellite": {
603+ g.roundRect(-r * 0.3, -r * 0.3, r * 0.6, r * 0.6, r * 0.1).fill(main);
604+ g.rect(-r * 1.0, -r * 0.18, r * 0.6, r * 0.36).fill(accent);
605+ g.rect(r * 0.4, -r * 0.18, r * 0.6, r * 0.36).fill(accent);
606+ for (const x of [-0.8, -0.6, 0.6, 0.8]) g.moveTo(x * r, -r * 0.18).lineTo(x * r, r * 0.18).stroke({ color: darken(style.color, 0.4), width: size * 0.012 });
607+ g.arc(0, -r * 0.55, r * 0.3, Math.PI, 0).stroke({ color: main, width: size * 0.03 });
608+ break;
609+ }
610+ case "pickaxe": {
611+ g.moveTo(-r * 0.7, r * 0.9).lineTo(r * 0.5, -r * 0.4).stroke({ color: 0x8a6d2e, width: size * 0.06 });
612+ g.moveTo(r * 0.0, -r * 0.9).bezierCurveTo(r * 0.7, -r * 0.9, r * 1.0, -r * 0.4, r * 0.95, r * 0.15).lineTo(r * 0.55, -r * 0.35).fill(main);
613+ g.moveTo(r * 0.0, -r * 0.9).lineTo(r * 0.55, -r * 0.35).stroke({ color: accent, width: size * 0.03 });
614+ break;
615+ }
616+ case "ore": {
617+ polygon(g, 0, r * 0.1, r * 0.9, 7).fill(0x3a3f4a);
618+ g.poly([-r * 0.3, -r * 0.1, 0, -r * 0.5, r * 0.35, -r * 0.05, r * 0.1, r * 0.35, -r * 0.25, r * 0.3]).fill(main);
619+ g.poly([-r * 0.3, -r * 0.1, 0, -r * 0.5, 0, 0]).fill(accent);
620+ break;
621+ }
622+ case "drill": {
623+ g.rect(-r * 0.4, -r * 0.9, r * 0.8, r * 0.7).fill(main);
624+ g.poly([-r * 0.4, -r * 0.2, r * 0.4, -r * 0.2, 0, r]).fill(0xc8d0dc);
625+ for (let i = 0; i < 4; i++) g.moveTo(-r * 0.32 + i * r * 0.1, -r * 0.2 + i * r * 0.28).lineTo(r * 0.32 - i * r * 0.1, -r * 0.2 + i * r * 0.28).stroke({ color: 0x5c6270, width: size * 0.015 });
626+ g.rect(-r * 0.15, -r * 0.6, r * 0.3, r * 0.15).fill(accent);
627+ break;
628+ }
629+ case "ingot": {
630+ g.poly([-r * 0.7, -r * 0.3, r * 0.7, -r * 0.3, r * 0.95, r * 0.5, -r * 0.95, r * 0.5]).fill(main);
631+ g.poly([-r * 0.7, -r * 0.3, r * 0.7, -r * 0.3, r * 0.5, 0, -r * 0.5, 0]).fill(accent);
632+ g.moveTo(-r * 0.5, r * 0.15).lineTo(r * 0.5, r * 0.15).stroke({ color: darken(style.color, 0.3), width: size * 0.02 });
633+ break;
634+ }
635+ case "hourglass": {
636+ g.poly([-r * 0.7, -r, r * 0.7, -r, r * 0.1, 0, r * 0.7, r, -r * 0.7, r, -r * 0.1, 0]).stroke({ color: main, width: size * 0.04 });
637+ g.poly([-r * 0.45, -r * 0.8, r * 0.45, -r * 0.8, 0, -r * 0.1]).fill({ color: accent, alpha: 0.8 });
638+ g.poly([-r * 0.55, r * 0.9, r * 0.55, r * 0.9, 0, r * 0.35]).fill({ color: accent, alpha: 0.8 });
639+ break;
640+ }
641+ case "igloo": {
642+ g.arc(0, r * 0.4, r * 0.95, Math.PI, 0).fill(main);
643+ g.rect(-r * 0.95, r * 0.4, r * 1.9, r * 0.2).fill(main);
644+ g.arc(-r * 0.3, r * 0.5, r * 0.35, Math.PI, 0).fill(0x0b0d14);
645+ for (let i = 0; i < 3; i++) g.arc(0, r * 0.4, r * (0.35 + i * 0.25), Math.PI, 0).stroke({ color: accent, width: size * 0.015, alpha: 0.6 });
646+ break;
647+ }
648+ case "paw": {
649+ g.ellipse(0, r * 0.35, r * 0.55, r * 0.45).fill(main);
650+ for (const [dx, dy] of [[-0.6, -0.25], [-0.22, -0.6], [0.22, -0.6], [0.6, -0.25]]) g.circle(dx * r, dy * r, r * 0.2).fill(main);
651+ g.ellipse(0, r * 0.4, r * 0.3, r * 0.22).fill({ color: accent, alpha: 0.6 });
652+ break;
653+ }
654+ case "fish": {
655+ g.ellipse(-r * 0.1, 0, r * 0.7, r * 0.42).fill(main);
656+ g.poly([r * 0.5, 0, r, -r * 0.45, r, r * 0.45]).fill(accent);
657+ g.circle(-r * 0.5, -r * 0.1, r * 0.08).fill(0x0b0d14);
658+ g.moveTo(-r * 0.2, -r * 0.4).bezierCurveTo(0, -r * 0.7, r * 0.2, -r * 0.5, r * 0.25, -r * 0.3).fill({ color: accent, alpha: 0.8 });
659+ break;
660+ }
661+ case "meteor": {
662+ polygon(g, r * 0.2, r * 0.2, r * 0.6, 7, 0.3).fill(main);
663+ g.circle(r * 0.35, 0, r * 0.12).fill({ color: darken(style.color, 0.3) });
664+ g.circle(0, r * 0.4, r * 0.1).fill({ color: darken(style.color, 0.3) });
665+ g.moveTo(-r * 0.2, -r * 0.3).lineTo(-r * 1.0, -r * 1.0).stroke({ color: accent, alpha: 0.6, width: size * 0.06 });
666+ g.moveTo(r * 0.1, -r * 0.55).lineTo(-r * 0.5, -r * 1.05).stroke({ color: accent, alpha: 0.4, width: size * 0.04 });
667+ break;
668+ }
313 } 669 }
314 c.addChild(g); 670 c.addChild(g);
315 return c; 671 return c;
modified apps/web/src/components/lobby/game-art.tsx +1167 −533
@@ -1,9 +1,15 @@
1 import * as React from "react"; 1 import * as React from "react";
2+import { GAMES_BY_SLUG } from "@spinza/games/client";
3+import type { GameDefinition, SymbolStyle, SymbolTier } from "@spinza/game-core/client";
2 import { cn } from "@/lib/utils"; 4 import { cn } from "@/lib/utils";
5+import { SymbolSvg, svgArc, svgPolygon, svgStar } from "./symbol-svg";
3 6
4 /* ------------------------------------------------------------------------ 7 /* ------------------------------------------------------------------------
5− Spinza key art — original procedural artwork, rendered as inline SVG. 8+ Spinza key art — 20 hand-composed posters, one scene per game.
6− Deterministic per slug (seeded PRNG, no Math.random at render). 9+ Every poster is built from the game's own reel symbols (SymbolSvg mirrors
10+ the PixiJS renderer) arranged in a composition unique to that title, on a
11+ theme-specific backdrop. Pure inline SVG, deterministic (seeded PRNG only
12+ for star fields / particles), no images, filters used sparingly.
7 ------------------------------------------------------------------------ */ 13 ------------------------------------------------------------------------ */
8 14
9 export type ArtVariant = "card" | "hero" | "banner" | "tile"; 15 export type ArtVariant = "card" | "hero" | "banner" | "tile";
@@ -29,39 +35,8 @@ export interface GameArtProps {
29 label?: string; 35 label?: string;
30 } 36 }
31 37
32−/** Presentation hints per game (the public /api/games payload does not carry them). */ 38+/** Presentation hints per game, derived from the game library. */
33−export const GAME_PRESENTATION: Record<string, { backdrop: string; particles: string }> = { 39+export const GAME_PRESENTATION: Record<string, { backdrop: string; particles: string }> = Object.fromEntries([...GAMES_BY_SLUG.values()].map((g) => [g.slug, { backdrop: g.presentation.backdrop, particles: g.presentation.particles }]));
34− "arctic-fortune": { backdrop: "aurora", particles: "snow" },
35− "cosmic-collapse": { backdrop: "nebula", particles: "stars" },
36− "deep-treasure": { backdrop: "abyss", particles: "bubbles" },
37− "diamond-heist": { backdrop: "vault", particles: "diamonds" },
38− "dragon-core": { backdrop: "core", particles: "fire" },
39− "golden-emperor": { backdrop: "pillars", particles: "coins" },
40− "inferno-reels": { backdrop: "lava", particles: "fire" },
41− "lucky-circuit": { backdrop: "arcade", particles: "confetti" },
42− "midnight-tokyo": { backdrop: "skyline", particles: "sparks" },
43− "moonbase-77": { backdrop: "moon", particles: "stars" },
44− "neon-vault": { backdrop: "vault", particles: "sparks" },
45− obsidian: { backdrop: "minimal", particles: "dust" },
46− "pharaoh-protocol": { backdrop: "pyramid", particles: "dust" },
47− "quantum-jackpot": { backdrop: "circuit", particles: "energy" },
48− "reel-reactor": { backdrop: "reactor", particles: "energy" },
49− "royal-circuit": { backdrop: "track", particles: "sparks" },
50− "spinza-original": { backdrop: "universe", particles: "energy" },
51− "void-miner": { backdrop: "asteroids", particles: "dust" },
52− "wild-temple": { backdrop: "temple", particles: "dust" },
53− "zero-gravity": { backdrop: "gravity", particles: "stars" },
54−};
55−
56−const BACKDROPS = ["vault", "nebula", "pillars", "aurora", "lava", "circuit", "skyline", "pyramid", "arcade", "asteroids", "track", "reactor", "abyss", "moon", "temple", "gravity", "core", "minimal", "universe", "grid"] as const;
57−type Backdrop = (typeof BACKDROPS)[number];
58−
59−const DIMS: Record<ArtVariant, { w: number; h: number; pad: number; maxFont: number; oneLine: boolean }> = {
60− card: { w: 360, h: 480, pad: 24, maxFont: 54, oneLine: false },
61− tile: { w: 240, h: 240, pad: 16, maxFont: 30, oneLine: false },
62− hero: { w: 1600, h: 900, pad: 72, maxFont: 132, oneLine: true },
63− banner: { w: 1200, h: 400, pad: 48, maxFont: 84, oneLine: true },
64−};
65 40
66 /* ------------------------------------------------------------ seeded PRNG */ 41 /* ------------------------------------------------------------ seeded PRNG */
67 42
@@ -85,677 +60,1336 @@ function mulberry32(seed: number): () => number {
85 }; 60 };
86 } 61 }
87 62
88−interface Ctx { 63+/* ------------------------------------------------------------------ stage */
64+
65+const FONT = "var(--font-geist-sans), Geist, Inter, ui-sans-serif, system-ui, sans-serif";
66+const f = (n: number) => Math.round(n * 100) / 100;
67+
68+interface Stage {
89 W: number; 69 W: number;
90 H: number; 70 H: number;
91− rnd: () => number; 71+ v: ArtVariant;
72+ /** Focal centre and unit radius of the composition. */
73+ cx: number;
74+ cy: number;
75+ R: number;
92 p: ArtPalette; 76 p: ArtPalette;
93 id: string; 77 id: string;
78+ rnd: () => number;
79+ game?: GameDefinition;
80+ /** Reel symbol style by id (falls back to a palette gem). */
81+ sym: (id: string) => SymbolStyle;
82+ tier: (id: string) => SymbolTier;
83+ /** Tile variant: drop fine detail. */
84+ compact: boolean;
94 } 85 }
95 86
96−const r2 = (n: number) => Math.round(n * 100) / 100; 87+const DIMS: Record<ArtVariant, { w: number; h: number }> = {
97−const between = (rnd: () => number, a: number, b: number) => a + (b - a) * rnd(); 88+ card: { w: 360, h: 480 },
98− 89+ tile: { w: 240, h: 240 },
99−/* ------------------------------------------------------------------ motifs */ 90+ hero: { w: 1680, h: 720 },
100− 91+ banner: { w: 1600, h: 600 },
101−function Vault({ W, H, rnd, p, id }: Ctx) { 92+};
102− const cx = W * 0.62; 93+
103− const cy = H * 0.42; 94+function focal(v: ArtVariant, W: number, H: number, showName: boolean): { cx: number; cy: number; R: number } {
104− const maxR = Math.max(W, H) * 0.55; 95+ switch (v) {
105− const rings: React.ReactNode[] = []; 96+ case "card":
106− for (let i = 1; i <= 7; i++) { 97+ return { cx: W / 2, cy: showName ? H * 0.41 : H * 0.5, R: W * 0.42 };
107− const r = (maxR / 7) * i; 98+ case "tile":
108− rings.push(<circle key={`r${i}`} cx={cx} cy={cy} r={r2(r)} fill="none" stroke={i % 2 ? p.primary : p.secondary} strokeOpacity={r2(0.06 + (7 - i) * 0.035)} strokeWidth={i === 3 ? 6 : 1.5} />); 99+ return { cx: W / 2, cy: H / 2, R: W * 0.4 };
109− if (i === 3 || i === 5) { 100+ case "hero":
110− for (let k = 0; k < 8; k++) { 101+ return { cx: W * 0.7, cy: H * 0.5, R: H * 0.42 };
111− const a = (Math.PI * 2 * k) / 8 + rnd() * 0.2; 102+ case "banner":
112− rings.push(<circle key={`b${i}${k}`} cx={r2(cx + Math.cos(a) * r)} cy={r2(cy + Math.sin(a) * r)} r={i === 3 ? 4 : 2.5} fill={p.glow} fillOpacity={0.7} />); 103+ return { cx: W * 0.72, cy: H * 0.5, R: H * 0.4 };
113− }
114− }
115 } 104 }
116− const step = W / 8; 105+}
117− const grid: React.ReactNode[] = []; 106+
118− for (let x = step / 2; x < W; x += step) grid.push(<line key={`gx${x}`} x1={r2(x)} y1={0} x2={r2(x)} y2={H} stroke={p.primary} strokeOpacity={0.05} />); 107+/* ---------------------------------------------------------------- helpers */
119− for (let y = step / 2; y < H; y += step) grid.push(<line key={`gy${y}`} x1={0} y1={r2(y)} x2={W} y2={r2(y)} stroke={p.primary} strokeOpacity={0.05} />); 108+
109+/** Many small discs as a single path (star fields, snow, embers, bubbles). */
110+function dots(list: Array<[number, number, number]>): string {
111+ let d = "";
112+ 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 `;
113+ return d;
114+}
115+
116+function scatterDots(s: Stage, n: number, box: [number, number, number, number], rMin: number, rMax: number): string {
117+ const list: Array<[number, number, number]> = [];
118+ 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)]);
119+ return dots(list);
120+}
121+
122+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 }) {
123+ return <SymbolSvg style={s.sym(id)} tier={s.tier(id)} size={size} x={x} y={y} rotate={rotate} opacity={opacity} plate={plate} halo={halo} />;
124+}
125+
126+/** Small rounded label pill (feature tags, multiplier badges). */
127+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 }) {
128+ const width = w ?? text.length * size * 0.66 + size * 1.2;
129+ const h = size * 1.7;
120 return ( 130 return (
121− <g> 131+ <g opacity={opacity}>
122− {grid} 132+ <rect x={f(x - width / 2)} y={f(y - h / 2)} width={f(width)} height={f(h)} rx={f(h / 2)} fill={bg} fillOpacity={0.72} stroke={color} strokeOpacity={0.8} strokeWidth={1} />
123− {rings} 133+ <text x={f(x)} y={f(y)} textAnchor="middle" dominantBaseline="central" fontSize={f(size)} fontWeight={700} fill={ink} style={{ fontFamily: FONT, letterSpacing: "0.08em" }}>
124− <circle cx={cx} cy={cy} r={r2(maxR / 14)} fill={`url(#${id}-glow)`} /> 134+ {text}
125− <circle cx={cx} cy={cy} r={r2(maxR / 28)} fill={p.glow} fillOpacity={0.9} /> 135+ </text>
126 </g> 136 </g>
127 ); 137 );
128 } 138 }
129 139
130−function Nebula({ W, H, rnd, p, id }: Ctx) { 140+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 }) {
131− const blobs: React.ReactNode[] = []; 141+ return (
132− for (let i = 0; i < 5; i++) { 142+ <text x={f(x)} y={f(y)} textAnchor={anchor} dominantBaseline="central" fontSize={f(size)} fontWeight={weight} fill={color} fillOpacity={opacity} style={{ fontFamily: FONT, letterSpacing: tracking }} transform={rotate ? `rotate(${rotate} ${f(x)} ${f(y)})` : undefined}>
133− blobs.push(<ellipse key={i} cx={r2(between(rnd, W * 0.2, W * 0.8))} cy={r2(between(rnd, H * 0.15, H * 0.7))} rx={r2(between(rnd, W * 0.18, W * 0.42))} ry={r2(between(rnd, H * 0.1, H * 0.28))} fill={i % 2 ? p.primary : p.secondary} fillOpacity={r2(between(rnd, 0.18, 0.34))} transform={`rotate(${r2(between(rnd, -30, 30))} ${W / 2} ${H / 2})`} filter={`url(#${id}-blur)`} />); 143+ {text}
144+ </text>
145+ );
146+}
147+
148+function Radial({ id, color, o0 = 0.6, o1 = 0, mid }: { id: string; color: string; o0?: number; o1?: number; mid?: [number, string, number] }) {
149+ return (
150+ <radialGradient id={id} cx="0.5" cy="0.5" r="0.5">
151+ <stop offset="0" stopColor={color} stopOpacity={o0} />
152+ {mid ? <stop offset={mid[0]} stopColor={mid[1]} stopOpacity={mid[2]} /> : null}
153+ <stop offset="1" stopColor={color} stopOpacity={o1} />
154+ </radialGradient>
155+ );
156+}
157+
158+function Linear({ id, stops, dir = "v" }: { id: string; stops: Array<[number, string, number?]>; dir?: "v" | "h" | "d" }) {
159+ 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 };
160+ return (
161+ <linearGradient id={id} {...c}>
162+ {stops.map(([o, col, op], i) => (
163+ <stop key={i} offset={o} stopColor={col} stopOpacity={op ?? 1} />
164+ ))}
165+ </linearGradient>
166+ );
167+}
168+
169+const PI = Math.PI;
170+
171+/* ======================================================================= */
172+/* SCENES */
173+/* ======================================================================= */
174+
175+/* 01 — NEON VAULT: cyan vault ring, Override Wild core, keycards scanning a grid */
176+function NeonVault(s: Stage) {
177+ const { W, H, cx, cy, R, id, p, compact } = s;
178+ const horizon = cy + R * 0.7;
179+ let grid = "";
180+ for (let i = -6; i <= 6; i++) grid += `M${f(cx)} ${f(horizon)}L${f(cx + i * W * 0.22)} ${f(H)} `;
181+ for (let k = 1; k <= 6; k++) {
182+ const t = k / 6;
183+ const y = horizon + (H - horizon) * t * t;
184+ grid += `M0 ${f(y)}H${f(W)} `;
134 } 185 }
135− const stars: React.ReactNode[] = []; 186+ let ticks = "";
136− for (let i = 0; i < 60; i++) { 187+ for (let i = 0; i < 12; i++) {
137− const big = rnd() > 0.85; 188+ const a = (i / 12) * PI * 2;
138− stars.push(<circle key={i} cx={r2(rnd() * W)} cy={r2(rnd() * H)} r={big ? r2(between(rnd, 1.6, 2.6)) : r2(between(rnd, 0.5, 1.2))} fill={big ? p.glow : "#fff"} fillOpacity={r2(between(rnd, 0.35, 0.95))} />); 189+ 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)} `;
139 } 190 }
191+ const scanY = cy - R * 0.22;
192+ return (
193+ <g>
194+ <defs>
195+ <Radial id={`${id}-nv1`} color={p.secondary} o0={0.5} />
196+ <Radial id={`${id}-nv2`} color={p.primary} o0={0.55} />
197+ <Linear id={`${id}-nv3`} stops={[[0, p.secondary, 0], [0.5, p.secondary, 0.35], [1, p.secondary, 0]]} dir="h" />
198+ </defs>
199+ <circle cx={f(W * 0.85)} cy={f(H * 0.08)} r={f(R * 1.2)} fill={`url(#${id}-nv1)`} />
200+ <path d={grid} stroke={p.primary} strokeOpacity={0.22} strokeWidth={1.2} fill="none" />
201+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.95)} fill={`url(#${id}-nv2)`} />
202+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.94)} fill="none" stroke={p.primary} strokeOpacity={0.12} strokeWidth={f(R * 0.14)} />
203+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.02)} fill="none" stroke={p.primary} strokeOpacity={0.5} strokeWidth={2} />
204+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.72)} fill="none" stroke="#fff" strokeOpacity={0.12} strokeWidth={1.5} strokeDasharray="3 9" />
205+ <path d={ticks} stroke={p.primary} strokeOpacity={0.9} strokeWidth={3} strokeLinecap="round" fill="none" />
206+ <Sym s={s} id="W" x={cx} y={cy} size={R * 0.92} />
207+ {!compact ? (
208+ <>
209+ <Sym s={s} id="M1" x={cx - R * 0.8} y={cy + R * 0.42} size={R * 0.42} rotate={-14} />
210+ <Sym s={s} id="M1" x={cx + R * 0.82} y={cy - R * 0.55} size={R * 0.38} rotate={12} />
211+ <Sym s={s} id="S" x={cx + R * 0.78} y={cy + R * 0.5} size={R * 0.42} rotate={6} />
212+ <Sym s={s} id="V" x={cx - R * 0.78} y={cy - R * 0.5} size={R * 0.4} rotate={-8} />
213+ <rect x={f(cx - R * 1.1)} y={f(scanY - R * 0.05)} width={f(R * 2.2)} height={f(R * 0.1)} fill={`url(#${id}-nv3)`} />
214+ <line x1={f(cx - R * 1.1)} y1={f(scanY)} x2={f(cx + R * 1.1)} y2={f(scanY)} stroke={p.secondary} strokeWidth={1.5} strokeOpacity={0.9} />
215+ <Small x={cx} y={cy + R * 1.18} text="×2 ×3 ×5 ×8 ×12 ×20 ×30 ×50" color={p.primary} size={R * 0.075} tracking="0.18em" opacity={0.7} />
216+ </>
217+ ) : null}
218+ </g>
219+ );
220+}
221+
222+/* 02 — COSMIC COLLAPSE: symbols spiral into a black hole, gravity ladder ×1→×13 */
223+function CosmicCollapse(s: Stage) {
224+ const { W, H, cx, cy, R, id, p, compact } = s;
225+ const stars = scatterDots(s, 90, [0, 0, W, H], 0.5, 1.5);
226+ const bright = scatterDots(s, 10, [0, 0, W, H], 1.6, 2.6);
227+ const orbit = [
228+ { id: "P1", a: -0.55, d: 1.05, k: 0.5, rot: 28 },
229+ { id: "H1", a: 0.95, d: 0.98, k: 0.42, rot: -20 },
230+ { id: "H2", a: 2.35, d: 0.9, k: 0.36, rot: 35 },
231+ { id: "M1", a: 3.7, d: 0.78, k: 0.3, rot: -40 },
232+ { id: "M2", a: 4.9, d: 0.66, k: 0.24, rot: 15 },
233+ ];
140 return ( 234 return (
141 <g> 235 <g>
142− {blobs} 236+ <defs>
143− {stars} 237+ <Radial id={`${id}-cc1`} color={p.primary} o0={0.4} />
238+ <Radial id={`${id}-cc2`} color={p.secondary} o0={0.3} />
239+ <Linear id={`${id}-cc3`} stops={[[0, p.secondary, 0], [0.3, "#fde68a", 0.95], [0.6, p.glow, 0.9], [1, p.primary, 0]]} dir="h" />
240+ <filter id={`${id}-blur`} x="-30%" y="-30%" width="160%" height="160%">
241+ <feGaussianBlur stdDeviation={f(R * 0.08)} />
242+ </filter>
243+ </defs>
244+ <ellipse cx={f(cx - R * 0.6)} cy={f(cy - R * 0.5)} rx={f(R * 1.3)} ry={f(R * 0.6)} fill={`url(#${id}-cc1)`} transform={`rotate(-25 ${f(cx)} ${f(cy)})`} />
245+ <ellipse cx={f(cx + R * 0.7)} cy={f(cy + R * 0.6)} rx={f(R * 1.1)} ry={f(R * 0.5)} fill={`url(#${id}-cc2)`} transform={`rotate(20 ${f(cx)} ${f(cy)})`} />
246+ <path d={stars} fill="#fff" fillOpacity={0.7} />
247+ <path d={bright} fill={p.glow} fillOpacity={0.9} />
248+ <g transform={`rotate(-22 ${f(cx)} ${f(cy)})`}>
249+ <ellipse cx={f(cx)} cy={f(cy)} rx={f(R * 1.05)} ry={f(R * 0.32)} fill="none" stroke={`url(#${id}-cc3)`} strokeWidth={f(R * 0.16)} filter={`url(#${id}-blur)`} />
250+ <ellipse cx={f(cx)} cy={f(cy)} rx={f(R * 1.05)} ry={f(R * 0.32)} fill="none" stroke={`url(#${id}-cc3)`} strokeWidth={f(R * 0.05)} />
251+ <ellipse cx={f(cx)} cy={f(cy)} rx={f(R * 0.74)} ry={f(R * 0.2)} fill="none" stroke="#fff" strokeOpacity={0.35} strokeWidth={1.5} strokeDasharray="2 7" />
252+ </g>
253+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.4)} fill="#000" />
254+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.41)} fill="none" stroke={p.glow} strokeWidth={2.5} strokeOpacity={0.9} />
255+ <Sym s={s} id="W" x={cx} y={cy} size={R * 0.95} plate={false} />
256+ {!compact
257+ ? orbit.map((o) => <Sym key={o.id} s={s} id={o.id} x={cx + Math.cos(o.a) * R * o.d} y={cy + Math.sin(o.a) * R * o.d * 0.72} size={R * o.k} rotate={o.rot} />)
258+ : null}
259+ {!compact ? (
260+ <text x={f(cx)} y={f(cy + R * 1.22)} textAnchor="middle" dominantBaseline="central" fontWeight={700} fill={p.primary} style={{ fontFamily: FONT, letterSpacing: "0.1em" }}>
261+ <tspan fontSize={f(R * 0.07)} fillOpacity={0.35}>×1 ×2 ×3 ×5 ×8 </tspan>
262+ <tspan fontSize={f(R * 0.11)} fill={p.glow} fillOpacity={0.9}>×13</tspan>
263+ </text>
264+ ) : null}
144 </g> 265 </g>
145 ); 266 );
146 } 267 }
147 268
148−function Pillars({ W, H, rnd, p, id }: Ctx) { 269+/* 03 — GOLDEN EMPEROR: symmetrical imperial court, gold pillars, dragon pearls ×2 ×3 ×5 */
149− const n = W > H ? 9 : 5; 270+function GoldenEmperor(s: Stage) {
150− const gap = W / n; 271+ const { H, cx, cy, R, id, p, compact } = s;
151− const cols: React.ReactNode[] = []; 272+ const pw = R * 0.22;
152− for (let i = 0; i < n; i++) { 273+ const px1 = cx - R * 1.02;
153− const cw = gap * 0.42; 274+ const px2 = cx + R * 1.02 - pw;
154− const x = gap * i + (gap - cw) / 2; 275+ const top = cy - R * 1.15;
155− const top = H * between(rnd, 0.12, 0.22); 276+ const flecks = scatterDots(s, 30, [cx - R * 1.2, cy - R * 1.2, R * 2.4, R * 2.4], 0.8, 2.2);
156− cols.push( 277+ const pearls = [
157− <g key={i}> 278+ { x: cx - R * 0.66, m: "×2" },
158− <rect x={r2(x)} y={r2(top)} width={r2(cw)} height={r2(H - top)} fill={`url(#${id}-col)`} /> 279+ { x: cx, m: "×3" },
159− <rect x={r2(x - cw * 0.18)} y={r2(top - 10)} width={r2(cw * 1.36)} height={12} fill={p.primary} fillOpacity={0.5} /> 280+ { x: cx + R * 0.66, m: "×5" },
160− <rect x={r2(x + cw * 0.3)} y={r2(top)} width={2} height={r2(H - top)} fill={p.glow} fillOpacity={0.12} /> 281+ ];
161− </g>, 282+ const py = cy + R * 0.62;
162− );
163− }
164 return ( 283 return (
165 <g> 284 <g>
166− <circle cx={W * 0.5} cy={H * 0.3} r={Math.min(W, H) * 0.22} fill={`url(#${id}-glow)`} /> 285+ <defs>
167− {cols} 286+ <Radial id={`${id}-ge1`} color={p.secondary} o0={0.65} o1={0} />
168− <rect x={0} y={H * 0.88} width={W} height={H * 0.12} fill={p.secondary} fillOpacity={0.25} /> 287+ <Linear id={`${id}-ge2`} stops={[[0, "#3a2410"], [0.45, "#b8862e"], [0.55, "#f5d98a"], [1, "#4a2c10"]]} dir="h" />
288+ <Linear id={`${id}-ge3`} stops={[[0, "#5a0d16", 0.95], [1, "#2a0409", 0.98]]} />
289+ </defs>
290+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.5)} fill={`url(#${id}-ge1)`} />
291+ <rect x={f(px1 + pw * 1.5)} y={f(top)} width={f(px2 - px1 - pw * 2)} height={f(H)} fill={`url(#${id}-ge3)`} />
292+ <rect x={f(px1 + pw * 1.5)} y={f(top)} width={f(px2 - px1 - pw * 2)} height={f(H)} fill="none" stroke={p.primary} strokeOpacity={0.35} strokeWidth={1.5} />
293+ <rect x={f(px1)} y={f(top)} width={f(pw)} height={f(H)} fill={`url(#${id}-ge2)`} />
294+ <rect x={f(px2)} y={f(top)} width={f(pw)} height={f(H)} fill={`url(#${id}-ge2)`} />
295+ <path d={`M${f(px1 - pw * 0.3)} ${f(top)}h${f(pw * 1.6)}v${f(pw * 0.35)}h${f(-pw * 1.6)}z M${f(px2 - pw * 0.3)} ${f(top)}h${f(pw * 1.6)}v${f(pw * 0.35)}h${f(-pw * 1.6)}z`} fill={p.primary} />
296+ <path d={`M${f(cx - R * 0.9)} ${f(cy - R * 0.98)}H${f(cx + R * 0.9)} M${f(cx - R * 0.9)} ${f(cy - R * 0.92)}H${f(cx + R * 0.9)}`} stroke={p.primary} strokeOpacity={0.7} strokeWidth={1.5} />
297+ <path d={flecks} fill={p.glow} fillOpacity={0.55} />
298+ <Sym s={s} id="P1" x={cx} y={cy - R * 0.3} size={R * 0.98} />
299+ {!compact ? (
300+ <>
301+ {pearls.map((o) => (
302+ <g key={o.m}>
303+ <Sym s={s} id="W" x={o.x} y={py} size={R * 0.5} />
304+ <Pill x={o.x} y={py + R * 0.38} text={o.m} color={p.primary} ink={p.glow} size={R * 0.085} />
305+ </g>
306+ ))}
307+ <Sym s={s} id="S" x={cx - R * 0.7} y={cy - R * 0.4} size={R * 0.4} />
308+ <Sym s={s} id="S" x={cx + R * 0.7} y={cy - R * 0.4} size={R * 0.4} />
309+ <path d={`M${f(cx)} ${f(cy - R * 0.83)}l${f(R * 0.03)} ${f(R * 0.03)}l${f(-R * 0.03)} ${f(R * 0.03)}l${f(-R * 0.03)} ${f(-R * 0.03)}z`} fill={p.primary} />
310+ </>
311+ ) : null}
169 </g> 312 </g>
170 ); 313 );
171 } 314 }
172 315
173−function Aurora({ W, H, rnd, p, id }: Ctx) { 316+/* 04 — DIAMOND HEIST: laser-cut vault door, diamond coins with SC values, jackpot plaques */
174− const bands: React.ReactNode[] = []; 317+function DiamondHeist(s: Stage) {
175− for (let i = 0; i < 4; i++) { 318+ const { W, H, cx, cy, R, id, p, compact } = s;
176− const y = H * (0.2 + i * 0.14); 319+ let spokes = "";
177− const amp = H * between(rnd, 0.04, 0.1); 320+ for (let i = 0; i < 24; i++) {
178− const d = `M${-W * 0.1} ${r2(y)} C ${r2(W * 0.2)} ${r2(y - amp)}, ${r2(W * 0.35)} ${r2(y + amp)}, ${r2(W * 0.55)} ${r2(y)} S ${r2(W * 0.9)} ${r2(y - amp)}, ${r2(W * 1.1)} ${r2(y + amp * 0.5)}`; 321+ const a = (i / 24) * PI * 2;
179− bands.push(<path key={i} d={d} fill="none" stroke={i % 2 ? p.primary : p.secondary} strokeOpacity={r2(0.55 - i * 0.08)} strokeWidth={r2(H * between(rnd, 0.05, 0.09))} strokeLinecap="round" filter={`url(#${id}-blur)`} />); 322+ 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)} `;
323+ }
324+ const rivets: Array<[number, number, number]> = [];
325+ for (let i = 0; i < 12; i++) {
326+ const a = (i / 12) * PI * 2 + PI / 12;
327+ rivets.push([cx + Math.cos(a) * R * 1.06, cy + Math.sin(a) * R * 1.06, R * 0.025]);
180 } 328 }
181− const stars: React.ReactNode[] = []; 329+ 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`;
182− for (let i = 0; i < 30; i++) stars.push(<circle key={i} cx={r2(rnd() * W)} cy={r2(rnd() * H * 0.5)} r={r2(between(rnd, 0.6, 1.6))} fill="#fff" fillOpacity={r2(between(rnd, 0.3, 0.9))} />); 330+ let sparkles = "";
183− const peaks: number[] = []; 331+ for (let i = 0; i < 14; i++) sparkles += sparkle(s.rnd() * W, s.rnd() * H, R * (0.03 + s.rnd() * 0.05));
184− for (let x = 0; x <= W; x += W / 9) peaks.push(H * between(rnd, 0.72, 0.88)); 332+ 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)}`;
185− const ridge = `M0 ${H} ` + peaks.map((y, i) => `L${r2((i * W) / 9)} ${r2(y)}`).join(" ") + ` L${W} ${H} Z`; 333+ const tiers = ["MINI", "MINOR", "MAJOR", "GRAND"];
334+ const coins = [
335+ { x: cx - R * 0.72, y: cy + R * 0.62, v: "5×" },
336+ { x: cx, y: cy + R * 0.8, v: "20×" },
337+ { x: cx + R * 0.72, y: cy + R * 0.62, v: "10×" },
338+ ];
339+ return (
340+ <g>
341+ <defs>
342+ <pattern id={`${id}-dh0`} width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
343+ <line x1="0" y1="0" x2="0" y2="10" stroke="#fff" strokeOpacity="0.06" strokeWidth="1" />
344+ </pattern>
345+ <Radial id={`${id}-dh1`} color="#2a3038" o0={1} o1={0} mid={[0.7, "#14181e", 1]} />
346+ <Radial id={`${id}-dh2`} color={p.glow} o0={0.5} />
347+ </defs>
348+ <rect width={W} height={H} fill={`url(#${id}-dh0)`} />
349+ <path d={lasers} stroke="#ff3b5c" strokeOpacity={0.35} strokeWidth={5} strokeLinecap="round" />
350+ <path d={lasers} stroke="#ff8fa3" strokeOpacity={0.9} strokeWidth={1.2} />
351+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.1)} fill={`url(#${id}-dh1)`} stroke={p.secondary} strokeOpacity={0.5} strokeWidth={2} />
352+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.0)} fill="none" stroke="#fff" strokeOpacity={0.1} strokeWidth={1} />
353+ <path d={spokes} stroke={p.primary} strokeOpacity={0.55} strokeWidth={1.5} fill="none" />
354+ <path d={dots(rivets)} fill={p.secondary} fillOpacity={0.8} />
355+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.56)} fill="#0a0d12" stroke={p.primary} strokeWidth={2.5} strokeOpacity={0.95} />
356+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.56)} fill={`url(#${id}-dh2)`} />
357+ <Sym s={s} id="D" x={cx} y={cy} size={R * 0.9} plate={false} />
358+ {!compact ? (
359+ <>
360+ {coins.map((c) => (
361+ <g key={c.v}>
362+ <Sym s={s} id="D" x={c.x} y={c.y} size={R * 0.4} />
363+ <Pill x={c.x} y={c.y + R * 0.3} text={c.v} color={p.primary} ink={p.glow} size={R * 0.08} />
364+ </g>
365+ ))}
366+ {tiers.map((t, i) => {
367+ const y = cy - R * 1.15 + i * R * 0.27;
368+ const grand = i === 3;
369+ return <Pill key={t} x={cx + R * 1.02} y={y} text={t} color={grand ? "#ffd166" : p.secondary} ink={grand ? "#ffd166" : "#dfe4ea"} bg={grand ? "#2a2008" : "#000"} size={R * 0.075} w={R * 0.5} />;
370+ })}
371+ <Sym s={s} id="W" x={cx - R * 0.98} y={cy - R * 0.55} size={R * 0.5} rotate={-30} />
372+ <Sym s={s} id="P1" x={cx - R * 1.0} y={cy - R * 1.08} size={R * 0.38} />
373+ </>
374+ ) : null}
375+ <path d={sparkles} fill="#fff" fillOpacity={0.85} />
376+ </g>
377+ );
378+}
379+
380+/* 05 — ARCTIC FORTUNE: aurora over a ridge, frozen sticky wilds in ice blocks, snowfall */
381+function ArcticFortune(s: Stage) {
382+ const { W, H, cx, cy, R, id, p, compact } = s;
383+ const stars = scatterDots(s, 40, [0, 0, W, H * 0.5], 0.5, 1.3);
384+ const snow = scatterDots(s, 45, [0, 0, W, H], 1, 3.2);
385+ 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)}`;
386+ const ridgeY = cy + R * 0.95;
387+ 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`;
388+ const blocks = [
389+ { x: cx - R * 0.72, y: cy + R * 0.35 },
390+ { x: cx - R * 0.1, y: cy + R * 0.05 },
391+ { x: cx + R * 0.55, y: cy - R * 0.25 },
392+ ];
393+ const bs = R * 0.56;
186 return ( 394 return (
187 <g> 395 <g>
188− {stars} 396+ <defs>
189− {bands} 397+ <Linear id={`${id}-af1`} stops={[[0, "#0b2a3a"], [0.6, p.bg], [1, "#02060d"]]} />
190− <path d={ridge} fill={p.bg} fillOpacity={0.95} /> 398+ <filter id={`${id}-blur`} x="-20%" y="-50%" width="140%" height="200%">
191− <path d={ridge} fill={p.primary} fillOpacity={0.08} /> 399+ <feGaussianBlur stdDeviation={f(R * 0.09)} />
400+ </filter>
401+ </defs>
402+ <rect width={W} height={H} fill={`url(#${id}-af1)`} />
403+ <path d={stars} fill="#fff" fillOpacity={0.8} />
404+ <path d={band(cy - R * 0.95, R * 0.25)} stroke="#34d399" strokeOpacity={0.55} strokeWidth={f(R * 0.22)} fill="none" strokeLinecap="round" filter={`url(#${id}-blur)`} />
405+ <path d={band(cy - R * 0.62, R * 0.2)} stroke={p.primary} strokeOpacity={0.5} strokeWidth={f(R * 0.2)} fill="none" strokeLinecap="round" filter={`url(#${id}-blur)`} />
406+ <path d={band(cy - R * 0.32, R * 0.16)} stroke={p.secondary} strokeOpacity={0.4} strokeWidth={f(R * 0.16)} fill="none" strokeLinecap="round" filter={`url(#${id}-blur)`} />
407+ <path d={ridge} fill="#050b16" stroke="#bfe9ff" strokeOpacity={0.35} strokeWidth={1.5} />
408+ {!compact ? (
409+ <>
410+ {blocks.map((b, i) => (
411+ <g key={i}>
412+ <rect x={f(b.x - bs / 2)} y={f(b.y - bs / 2)} width={f(bs)} height={f(bs)} rx={f(bs * 0.12)} fill="#bfe9ff" fillOpacity={0.14} stroke="#e8f8ff" strokeOpacity={0.75} strokeWidth={2} />
413+ <Sym s={s} id="W" x={b.x} y={b.y} size={bs * 0.92} plate={false} />
414+ </g>
415+ ))}
416+ <Sym s={s} id="P1" x={cx + R * 0.72} y={cy + R * 0.5} size={R * 0.62} />
417+ <Sym s={s} id="H1" x={cx - R * 0.78} y={cy - R * 0.45} size={R * 0.44} />
418+ <Sym s={s} id="S" x={cx + R * 0.1} y={cy - R * 0.7} size={R * 0.38} />
419+ <Small x={cx} y={cy + R * 1.22} text="×1 ×2 ×3 ×4 ×5" color="#e8f8ff" size={R * 0.08} tracking="0.16em" opacity={0.75} />
420+ </>
421+ ) : (
422+ <Sym s={s} id="W" x={cx} y={cy} size={R * 0.9} />
423+ )}
424+ <path d={snow} fill="#fff" fillOpacity={0.75} />
192 </g> 425 </g>
193 ); 426 );
194 } 427 }
195 428
196−function Lava({ W, H, rnd, p, id }: Ctx) { 429+/* 06 — INFERNO REELS: cracked lava floor, Magma Core shockwave, embers */
197− const cracks: React.ReactNode[] = []; 430+function InfernoReels(s: Stage) {
431+ const { W, H, cx, cy, R, id, p, compact } = s;
432+ const floorY = cy + R * 0.75;
433+ let cracks = "";
198 for (let i = 0; i < 6; i++) { 434 for (let i = 0; i < 6; i++) {
199− let x = between(rnd, W * 0.05, W * 0.95); 435+ let x = W * (0.05 + i * 0.18) + s.rnd() * W * 0.08;
200 let y = H; 436 let y = H;
201− const pts = [`M${r2(x)} ${r2(y)}`]; 437+ cracks += `M${f(x)} ${f(y)}`;
202− while (y > H * 0.15) { 438+ while (y > floorY + R * 0.05) {
203− x += between(rnd, -W * 0.08, W * 0.08); 439+ x += (s.rnd() - 0.5) * R * 0.3;
204− y -= between(rnd, H * 0.06, H * 0.14); 440+ y -= R * (0.1 + s.rnd() * 0.15);
205− pts.push(`L${r2(x)} ${r2(y)}`); 441+ cracks += `L${f(x)} ${f(y)}`;
206 } 442 }
207− const d = pts.join(" "); 443+ cracks += " ";
208− cracks.push(<path key={`g${i}`} d={d} fill="none" stroke={p.glow} strokeOpacity={0.55} strokeWidth={8} strokeLinecap="round" strokeLinejoin="round" filter={`url(#${id}-blur)`} />);
209− cracks.push(<path key={`c${i}`} d={d} fill="none" stroke={p.primary} strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" />);
210 } 444 }
445+ const embers = scatterDots(s, 40, [cx - R * 1.3, cy - R * 1.3, R * 2.6, R * 2.4], 0.8, 2.6);
446+ const embers2 = scatterDots(s, 16, [cx - R * 1.2, cy - R * 1.2, R * 2.4, R * 2], 1.5, 3.5);
447+ const burst = svgStar(cx, cy, R * 0.98, R * 0.62, 14);
211 return ( 448 return (
212 <g> 449 <g>
213− <rect x={0} y={H * 0.6} width={W} height={H * 0.4} fill={`url(#${id}-heat)`} /> 450+ <defs>
214− {cracks} 451+ <Linear id={`${id}-ir1`} stops={[[0, p.primary, 0], [1, p.primary, 0.55]]} />
452+ <Radial id={`${id}-ir2`} color={p.glow} o0={0.75} mid={[0.45, p.primary, 0.35]} />
453+ <filter id={`${id}-tex`} x="0" y="0" width="100%" height="100%">
454+ <feTurbulence type="fractalNoise" baseFrequency="0.012 0.03" numOctaves="2" seed="7" />
455+ <feColorMatrix values="0 0 0 0 1 0 0 0 0 0.35 0 0 0 0 0.05 0 0 0 0.55 0" />
456+ </filter>
457+ </defs>
458+ <rect x={0} y={f(floorY - R * 0.4)} width={W} height={f(H - floorY + R * 0.4)} fill={`url(#${id}-ir1)`} />
459+ <rect x={0} y={f(floorY)} width={W} height={f(H - floorY)} fill="#160604" />
460+ <rect x={0} y={f(floorY)} width={W} height={f(H - floorY)} filter={`url(#${id}-tex)`} opacity={0.5} />
461+ <path d={cracks} stroke={p.glow} strokeOpacity={0.5} strokeWidth={7} strokeLinecap="round" strokeLinejoin="round" fill="none" />
462+ <path d={cracks} stroke="#ffd166" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" fill="none" />
463+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.3)} fill={`url(#${id}-ir2)`} />
464+ <polygon points={burst} fill={p.glow} fillOpacity={0.28} />
465+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.7)} fill="none" stroke={p.secondary} strokeOpacity={0.85} strokeWidth={3} />
466+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.9)} fill="none" stroke={p.primary} strokeOpacity={0.5} strokeWidth={2} />
467+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.12)} fill="none" stroke={p.primary} strokeOpacity={0.22} strokeWidth={1.5} />
468+ <Sym s={s} id="W" x={cx} y={cy} size={R * 0.95} plate={false} />
469+ {!compact ? (
470+ <>
471+ <Sym s={s} id="P1" x={cx - R * 0.92} y={cy - R * 0.72} size={R * 0.5} rotate={-18} />
472+ <Sym s={s} id="S" x={cx + R * 0.92} y={cy - R * 0.75} size={R * 0.46} rotate={16} />
473+ <Sym s={s} id="H1" x={cx - R * 0.95} y={cy + R * 0.55} size={R * 0.42} rotate={22} />
474+ <Sym s={s} id="H2" x={cx + R * 0.95} y={cy + R * 0.55} size={R * 0.42} rotate={-20} />
475+ <Small x={cx} y={cy + R * 1.2} text="×2 · ×3 · ×5 · ×10" color="#ffd166" size={R * 0.085} tracking="0.16em" weight={700} opacity={0.9} />
476+ </>
477+ ) : null}
478+ <path d={embers} fill={p.glow} fillOpacity={0.8} />
479+ <path d={embers2} fill="#ffd166" fillOpacity={0.85} />
215 </g> 480 </g>
216 ); 481 );
217 } 482 }
218 483
219−function Circuit({ W, H, rnd, p }: Ctx) { 484+/* 07 — QUANTUM JACKPOT: a processor splitting into ghost copies ×2 ×3 ×4 over circuit traces */
220− const traces: React.ReactNode[] = []; 485+function QuantumJackpot(s: Stage) {
221− const step = Math.min(W, H) / 10; 486+ const { W, H, cx, cy, R, id, p, compact } = s;
222− for (let i = 0; i < 16; i++) { 487+ const step = R * 0.22;
223− let x = Math.round(between(rnd, 0, W / step)) * step; 488+ let t1 = "";
224− let y = Math.round(between(rnd, 0, H / step)) * step; 489+ let t2 = "";
225− const pts = [`M${r2(x)} ${r2(y)}`]; 490+ const pads: Array<[number, number, number]> = [];
226− const segs = 2 + Math.floor(rnd() * 3); 491+ for (let i = 0; i < 14; i++) {
227− for (let s = 0; s < segs; s++) { 492+ let x = Math.round((s.rnd() * W) / step) * step;
228− const horiz = rnd() > 0.5; 493+ let y = Math.round((s.rnd() * H) / step) * step;
229− const len = step * (1 + Math.floor(rnd() * 3)); 494+ let d = `M${f(x)} ${f(y)}`;
230− if (horiz) x += rnd() > 0.5 ? len : -len; 495+ for (let k = 0; k < 3; k++) {
231− else y += rnd() > 0.5 ? len : -len; 496+ if (s.rnd() > 0.5) x += (s.rnd() > 0.5 ? 1 : -1) * step * (1 + Math.floor(s.rnd() * 3));
232− pts.push(`L${r2(x)} ${r2(y)}`); 497+ else y += (s.rnd() > 0.5 ? 1 : -1) * step * (1 + Math.floor(s.rnd() * 3));
498+ d += `L${f(x)} ${f(y)}`;
233 } 499 }
234− const color = i % 3 === 0 ? p.secondary : p.primary; 500+ if (i % 3 === 0) t2 += d + " ";
235− traces.push(<path key={`t${i}`} d={pts.join(" ")} fill="none" stroke={color} strokeOpacity={0.35} strokeWidth={2} strokeLinejoin="round" />); 501+ else t1 += d + " ";
236− traces.push(<circle key={`p${i}`} cx={r2(x)} cy={r2(y)} r={4} fill={p.bg} stroke={color} strokeWidth={2} />); 502+ pads.push([x, y, 3]);
237 } 503 }
238− return <g>{traces}</g>; 504+ 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(" ")}`;
239−} 505+ const gx = cx - R * 0.42;
240− 506+ return (
241−function Skyline({ W, H, rnd, p, id }: Ctx) { 507+ <g>
242− const buildings: React.ReactNode[] = []; 508+ <defs>
243− let x = -10; 509+ <Radial id={`${id}-qj1`} color={p.secondary} o0={0.35} />
244− let i = 0; 510+ <Radial id={`${id}-qj2`} color={p.primary} o0={0.35} />
245− while (x < W + 10) { 511+ </defs>
246− const bw = between(rnd, W * 0.05, W * 0.14); 512+ <circle cx={f(cx + R * 0.6)} cy={f(cy - R * 0.5)} r={f(R * 1.3)} fill={`url(#${id}-qj1)`} />
247− const bh = H * between(rnd, 0.25, 0.7); 513+ <circle cx={f(cx - R * 0.8)} cy={f(cy + R * 0.7)} r={f(R * 1.1)} fill={`url(#${id}-qj2)`} />
248− const y = H - bh; 514+ <path d={t1} stroke={p.primary} strokeOpacity={0.3} strokeWidth={1.6} fill="none" strokeLinejoin="round" />
249− buildings.push(<rect key={`b${i}`} x={r2(x)} y={r2(y)} width={r2(bw)} height={r2(bh)} fill={i % 2 ? "#0a0a12" : "#0e0d18"} stroke={p.primary} strokeOpacity={0.35} strokeWidth={1} />); 515+ <path d={t2} stroke={p.secondary} strokeOpacity={0.35} strokeWidth={1.6} fill="none" strokeLinejoin="round" />
250− const cols = Math.max(1, Math.floor(bw / 14)); 516+ <path d={dots(pads)} fill={p.bg} stroke={p.primary} strokeOpacity={0.6} strokeWidth={1.5} />
251− const rows = Math.floor(bh / 18); 517+ <path d={wave} stroke={p.primary} strokeOpacity={0.55} strokeWidth={1.5} fill="none" />
252− for (let c = 0; c < cols; c++) { 518+ {!compact ? (
253− for (let r = 0; r < rows; r++) { 519+ <>
254− if (rnd() > 0.45) continue; 520+ <Sym s={s} id="P1" x={gx + R * 1.15} y={cy - R * 0.12} size={R * 0.7} opacity={0.22} />
255− buildings.push(<rect key={`w${i}-${c}-${r}`} x={r2(x + 5 + c * 14)} y={r2(y + 8 + r * 18)} width={5} height={7} fill={rnd() > 0.7 ? p.secondary : p.primary} fillOpacity={r2(between(rnd, 0.4, 0.95))} />); 521+ <Sym s={s} id="P1" x={gx + R * 0.78} y={cy - R * 0.08} size={R * 0.74} opacity={0.42} />
256− } 522+ <Sym s={s} id="P1" x={gx + R * 0.4} y={cy - R * 0.04} size={R * 0.78} opacity={0.7} />
523+ </>
524+ ) : null}
525+ <Sym s={s} id="P1" x={gx} y={cy} size={R * 0.84} />
526+ {!compact ? (
527+ <>
528+ <Pill x={gx + R * 0.4} y={cy + R * 0.55} text="×2" color={p.primary} ink={p.glow} size={R * 0.085} />
529+ <Pill x={gx + R * 0.78} y={cy + R * 0.55} text="×3" color={p.primary} ink={p.glow} size={R * 0.085} opacity={0.75} />
530+ <Pill x={gx + R * 1.15} y={cy + R * 0.55} text="×4" color={p.primary} ink={p.glow} size={R * 0.085} opacity={0.5} />
531+ <Sym s={s} id="W" x={cx - R * 0.85} y={cy - R * 0.85} size={R * 0.48} />
532+ <Sym s={s} id="H1" x={cx + R * 0.85} y={cy - R * 0.9} size={R * 0.44} />
533+ <Sym s={s} id="S" x={cx - R * 0.9} y={cy + R * 0.7} size={R * 0.4} />
534+ <Small x={cx} y={cy + R * 1.25} text="MINI 20× · MINOR 50× · MAJOR 200× · GRAND 1,000×" color={p.glow} size={R * 0.062} tracking="0.1em" opacity={0.8} />
535+ </>
536+ ) : null}
537+ </g>
538+ );
539+}
540+
541+/* 08 — MIDNIGHT TOKYO: rain-wet skyline, neon signage, stacked wild column */
542+function MidnightTokyo(s: Stage) {
543+ const { W, H, cx, cy, R, id, p, compact } = s;
544+ const horizon = cy + R * 0.85;
545+ let sky = `M0 ${f(horizon)}`;
546+ const wins: Array<[number, number, number]> = [];
547+ const wins2: Array<[number, number, number]> = [];
548+ let x = 0;
549+ while (x < W) {
550+ const bw = W * (0.04 + s.rnd() * 0.09);
551+ const bh = R * (0.4 + s.rnd() * 1.4);
552+ sky += `L${f(x)} ${f(horizon - bh)}L${f(x + bw)} ${f(horizon - bh)}`;
553+ for (let k = 0; k < bh / (R * 0.16) - 1; k++) {
554+ 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]);
257 } 555 }
258− if (rnd() > 0.7) buildings.push(<line key={`a${i}`} x1={r2(x + bw / 2)} y1={r2(y)} x2={r2(x + bw / 2)} y2={r2(y - H * 0.08)} stroke={p.primary} strokeOpacity={0.7} strokeWidth={1.5} />); 556+ x += bw + W * 0.008;
259− x += bw + between(rnd, 4, 14); 557+ }
260− i++; 558+ sky += `L${f(W)} ${f(horizon)}Z`;
559+ let rain = "";
560+ for (let i = 0; i < 45; i++) {
561+ const rx = s.rnd() * W;
562+ const ry = s.rnd() * H;
563+ rain += `M${f(rx)} ${f(ry)}l${f(-R * 0.05)} ${f(R * 0.22)} `;
261 } 564 }
565+ const colX = cx + R * 0.72;
566+ const colH = R * 1.75;
262 return ( 567 return (
263 <g> 568 <g>
264− <circle cx={W * 0.72} cy={H * 0.28} r={Math.min(W, H) * 0.14} fill={`url(#${id}-glow)`} /> 569+ <defs>
265− <circle cx={W * 0.72} cy={H * 0.28} r={Math.min(W, H) * 0.07} fill={p.primary} fillOpacity={0.55} /> 570+ <Linear id={`${id}-mt1`} stops={[[0, "#1a0630"], [0.55, p.bg], [1, "#3a0a3a"]]} />
266− {buildings} 571+ <Linear id={`${id}-mt2`} stops={[[0, p.primary, 0.35], [1, p.primary, 0]]} />
267− <rect x={0} y={H * 0.995} width={W} height={H * 0.005} fill={p.secondary} /> 572+ <Radial id={`${id}-mt3`} color={p.primary} o0={0.55} />
573+ </defs>
574+ <rect width={W} height={H} fill={`url(#${id}-mt1)`} />
575+ <ellipse cx={f(cx)} cy={f(horizon)} rx={f(W * 0.7)} ry={f(R * 0.9)} fill={`url(#${id}-mt3)`} />
576+ <path d={sky} fill="#0a0514" stroke={p.primary} strokeOpacity={0.5} strokeWidth={1.2} />
577+ <path d={dots(wins)} fill="#ffd166" fillOpacity={0.8} />
578+ <path d={dots(wins2)} fill={p.secondary} fillOpacity={0.85} />
579+ <rect x={0} y={f(horizon)} width={W} height={f(H - horizon)} fill={`url(#${id}-mt2)`} />
580+ <line x1={0} y1={f(horizon)} x2={W} y2={f(horizon)} stroke={p.secondary} strokeWidth={1.5} strokeOpacity={0.9} />
581+ <path d={rain} stroke="#9be7ff" strokeOpacity={0.28} strokeWidth={1} fill="none" strokeLinecap="round" />
582+ {!compact ? (
583+ <>
584+ <rect x={f(colX - R * 0.32)} y={f(cy - colH / 2)} width={f(R * 0.64)} height={f(colH)} rx={f(R * 0.06)} fill="#120820" fillOpacity={0.9} stroke={p.primary} strokeOpacity={0.9} strokeWidth={2} />
585+ <Sym s={s} id="W" x={colX} y={cy - R * 0.58} size={R * 0.58} />
586+ <Sym s={s} id="W" x={colX} y={cy} size={R * 0.58} />
587+ <Sym s={s} id="W" x={colX} y={cy + R * 0.58} size={R * 0.58} />
588+ <rect x={f(cx - R * 1.12)} y={f(cy - R * 1.05)} width={f(R * 0.34)} height={f(R * 0.95)} rx={4} fill="#1a0a2a" stroke={p.secondary} strokeWidth={2} />
589+ <Small x={cx - R * 0.95} y={cy - R * 0.58} text="TOKYO" color={p.secondary} size={R * 0.13} weight={800} tracking="0.1em" rotate={-90} />
590+ <rect x={f(cx - R * 0.66)} y={f(cy - R * 1.0)} width={f(R * 0.9)} height={f(R * 0.24)} rx={4} fill={p.primary} fillOpacity={0.9} />
591+ <Small x={cx - R * 0.21} y={cy - R * 0.88} text="24H NEON" color="#fff" size={R * 0.1} weight={800} tracking="0.16em" />
592+ <Sym s={s} id="P1" x={cx - R * 0.25} y={cy + R * 0.02} size={R * 0.82} />
593+ <Sym s={s} id="H1" x={cx - R * 0.85} y={cy + R * 0.42} size={R * 0.44} rotate={-25} />
594+ <Sym s={s} id="S" x={cx + R * 0.12} y={cy - R * 0.62} size={R * 0.36} />
595+ </>
596+ ) : (
597+ <Sym s={s} id="P1" x={cx} y={cy} size={R * 0.9} />
598+ )}
268 </g> 599 </g>
269 ); 600 );
270 } 601 }
271 602
272−function Pyramid({ W, H, rnd, p, id }: Ctx) { 603+/* 09 — PHARAOH PROTOCOL: pyramid with a scanning laser, artifact meter */
273− const tri = (cx: number, base: number, h: number, key: string, fill: string, op: number) => <polygon key={key} points={`${r2(cx - base / 2)},${r2(H * 0.86)} ${r2(cx)},${r2(H * 0.86 - h)} ${r2(cx + base / 2)},${r2(H * 0.86)}`} fill={fill} fillOpacity={op} stroke={p.primary} strokeOpacity={0.4} />; 604+function PharaohProtocol(s: Stage) {
274− const rays: React.ReactNode[] = []; 605+ const { W, H, cx, cy, R, id, p, compact } = s;
275− for (let i = 0; i < 14; i++) { 606+ const base = cy + R * 0.85;
276− const a = -Math.PI * 0.95 + (i / 13) * Math.PI * 0.9; 607+ const apex = cy - R * 0.85;
277− rays.push(<line key={i} x1={W * 0.5} y1={H * 0.36} x2={r2(W * 0.5 + Math.cos(a) * W)} y2={r2(H * 0.36 + Math.sin(a) * W)} stroke={p.primary} strokeOpacity={0.06 + (rnd() > 0.5 ? 0.05 : 0)} strokeWidth={1.5} />); 608+ const half = R * 1.05;
609+ const tri = `${f(cx - half)},${f(base)} ${f(cx)},${f(apex)} ${f(cx + half)},${f(base)}`;
610+ let bricks = "";
611+ for (let k = 1; k < 9; k++) bricks += `M${f(cx - half)} ${f(apex + (base - apex) * (k / 9))}H${f(cx + half)} `;
612+ const scanY = cy - R * 0.18;
613+ const dust = scatterDots(s, 35, [0, 0, W, H], 0.6, 1.8);
614+ const segs = 12;
615+ const mw = R * 1.4;
616+ const mx = cx - mw / 2;
617+ const my = cy + R * 1.12;
618+ let segOff = "";
619+ let segOn = "";
620+ for (let i = 0; i < segs; i++) {
621+ 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 `;
622+ if (i < 7) segOn += d;
623+ else segOff += d;
278 } 624 }
279 return ( 625 return (
280 <g> 626 <g>
281− {rays} 627+ <defs>
282− <circle cx={W * 0.5} cy={H * 0.36} r={Math.min(W, H) * 0.22} fill={`url(#${id}-glow)`} /> 628+ <Linear id={`${id}-pp1`} stops={[[0, "#2a1608"], [0.45, p.bg], [1, "#1a1006"]]} />
283− <circle cx={W * 0.5} cy={H * 0.36} r={Math.min(W, H) * 0.1} fill={p.primary} fillOpacity={0.9} /> 629+ <Linear id={`${id}-pp2`} stops={[[0, "#8a5a1a"], [0.5, "#f1c86a"], [1, "#5a3a10"]]} dir="h" />
284− {tri(W * 0.2, W * 0.5, H * 0.34, "l", "#120c05", 1)} 630+ <Radial id={`${id}-pp3`} color="#fb923c" o0={0.7} mid={[0.4, p.primary, 0.3]} />
285− {tri(W * 0.82, W * 0.55, H * 0.4, "r", "#0f0a04", 1)} 631+ <clipPath id={`${id}-pp4`}>
286− {tri(W * 0.5, W * 0.72, H * 0.56, "c", "#1a1208", 1)} 632+ <polygon points={tri} />
287− <polygon points={`${r2(W * 0.5)},${r2(H * 0.3)} ${r2(W * 0.86)},${r2(H * 0.86)} ${r2(W * 0.5)},${r2(H * 0.86)}`} fill={p.primary} fillOpacity={0.12} /> 633+ </clipPath>
288− <rect x={0} y={H * 0.86} width={W} height={H * 0.14} fill={p.secondary} fillOpacity={0.1} /> 634+ </defs>
635+ <rect width={W} height={H} fill={`url(#${id}-pp1)`} />
636+ <circle cx={f(cx + R * 0.75)} cy={f(cy - R * 0.72)} r={f(R * 0.55)} fill={`url(#${id}-pp3)`} />
637+ <circle cx={f(cx + R * 0.75)} cy={f(cy - R * 0.72)} r={f(R * 0.2)} fill="#fb923c" fillOpacity={0.9} />
638+ <ellipse cx={f(cx - R * 0.6)} cy={f(base + R * 0.15)} rx={f(W * 0.7)} ry={f(R * 0.3)} fill="#3a2810" />
639+ <ellipse cx={f(cx + R * 0.9)} cy={f(base + R * 0.25)} rx={f(W * 0.6)} ry={f(R * 0.28)} fill="#2a1c0a" />
640+ <polygon points={tri} fill={`url(#${id}-pp2)`} />
641+ <path d={bricks} stroke="#3a2408" strokeOpacity={0.7} strokeWidth={1.2} clipPath={`url(#${id}-pp4)`} />
642+ <polygon points={`${f(cx)},${f(apex)} ${f(cx + half)},${f(base)} ${f(cx)},${f(base)}`} fill="#000" fillOpacity={0.35} />
643+ <rect x={f(cx - half)} y={f(scanY - R * 0.12)} width={f(half * 2)} height={f(R * 0.24)} fill={p.secondary} fillOpacity={0.18} clipPath={`url(#${id}-pp4)`} />
644+ <line x1={f(cx - half * 1.1)} y1={f(scanY)} x2={f(cx + half * 1.1)} y2={f(scanY)} stroke={p.secondary} strokeWidth={2} strokeOpacity={0.95} />
645+ <Sym s={s} id="W" x={cx} y={apex - R * 0.05} size={R * 0.72} plate={false} />
646+ {!compact ? (
647+ <>
648+ <Sym s={s} id="P1" x={cx + R * 0.62} y={cy + R * 0.35} size={R * 0.62} />
649+ <Sym s={s} id="H1" x={cx - R * 0.8} y={cy + R * 0.3} size={R * 0.5} />
650+ <Sym s={s} id="AR" x={cx - R * 0.55} y={cy + R * 0.72} size={R * 0.36} />
651+ <Sym s={s} id="AR" x={cx - R * 0.2} y={cy + R * 0.78} size={R * 0.36} />
652+ <Sym s={s} id="AR" x={cx + R * 0.15} y={cy + R * 0.72} size={R * 0.36} />
653+ <path d={segOff} fill="#fff" fillOpacity={0.12} />
654+ <path d={segOn} fill={p.secondary} fillOpacity={0.95} />
655+ <Small x={cx} y={my + R * 0.16} text="ARTIFACT SCANNER · 7 / 12" color={p.secondary} size={R * 0.065} tracking="0.2em" opacity={0.85} />
656+ </>
657+ ) : null}
658+ <path d={dust} fill={p.primary} fillOpacity={0.5} />
289 </g> 659 </g>
290 ); 660 );
291 } 661 }
292 662
293−function Arcade({ W, H, rnd, p }: Ctx) { 663+/* 10 — LUCKY CIRCUIT: retro arcade bezel, mystery "?" cartridge, confetti */
294− const size = Math.min(W, H) / 12; 664+function LuckyCircuit(s: Stage) {
295− const cells: React.ReactNode[] = []; 665+ const { W, H, cx, cy, R, id, p, compact } = s;
296− const colors = [p.primary, p.secondary, p.glow]; 666+ const bw = R * 2.2;
297− for (let y = 0; y < H; y += size) { 667+ const bh = R * 2.05;
298− for (let x = 0; x < W; x += size) { 668+ const bx = cx - bw / 2;
299− const r = rnd(); 669+ const by = cy - bh / 2 - R * 0.05;
300− if (r > 0.22) continue; 670+ let scan = "";
301− cells.push(<rect key={`${x}-${y}`} x={r2(x + 2)} y={r2(y + 2)} width={r2(size - 4)} height={r2(size - 4)} rx={3} fill={colors[Math.floor(rnd() * 3)]} fillOpacity={r2(between(rnd, 0.25, 0.85))} />); 671+ for (let y = by + 8; y < by + bh; y += 6) scan += `M${f(bx)} ${f(y)}H${f(bx + bw)} `;
672+ const confetti = (color: string, n: number) => {
673+ let d = "";
674+ for (let i = 0; i < n; i++) {
675+ const x = s.rnd() * W;
676+ const y = s.rnd() * H;
677+ const k = 3 + s.rnd() * 4;
678+ d += `M${f(x)} ${f(y)}h${f(k * 1.6)}v${f(k)}h${f(-k * 1.6)}z `;
302 } 679 }
303− } 680+ return <path d={d} fill={color} fillOpacity={0.85} />;
681+ };
304 return ( 682 return (
305 <g> 683 <g>
306− {cells} 684+ <defs>
307− <rect x={W * 0.3} y={H * 0.36} width={W * 0.4} height={size * 0.6} rx={size * 0.3} fill={p.primary} fillOpacity={0.9} /> 685+ <pattern id={`${id}-lc0`} width="14" height="14" patternUnits="userSpaceOnUse">
308− <rect x={W * 0.3 + size * 0.3} y={H * 0.36 + size * 0.15} width={W * 0.4 - size * 0.6} height={size * 0.3} rx={size * 0.15} fill="#fff" fillOpacity={0.35} /> 686+ <circle cx="7" cy="7" r="1.2" fill={p.secondary} fillOpacity="0.35" />
687+ </pattern>
688+ <Radial id={`${id}-lc1`} color={p.glow} o0={0.5} />
689+ </defs>
690+ <rect width={W} height={H} fill={`url(#${id}-lc0)`} />
691+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.4)} fill={`url(#${id}-lc1)`} />
692+ <rect x={f(bx - R * 0.1)} y={f(by - R * 0.1)} width={f(bw + R * 0.2)} height={f(bh + R * 0.2)} rx={f(R * 0.14)} fill="#1b1638" stroke={p.primary} strokeWidth={f(R * 0.06)} />
693+ <rect x={f(bx)} y={f(by)} width={f(bw)} height={f(bh)} rx={f(R * 0.06)} fill="#07061a" stroke={p.secondary} strokeWidth={2} />
694+ <path d={scan} stroke="#fff" strokeOpacity={0.05} strokeWidth={2} />
695+ {!compact ? confetti(p.primary, 14) : null}
696+ {!compact ? confetti(p.glow, 12) : null}
697+ {!compact ? confetti(p.secondary, 12) : null}
698+ <Sym s={s} id="MY" x={cx} y={cy - R * 0.08} size={R * 1.0} />
699+ {!compact ? (
700+ <>
701+ <Sym s={s} id="W" x={cx - R * 0.78} y={cy - R * 0.72} size={R * 0.48} rotate={-12} />
702+ <Sym s={s} id="P1" x={cx + R * 0.78} y={cy - R * 0.72} size={R * 0.48} rotate={10} />
703+ <Sym s={s} id="H1" x={cx - R * 0.78} y={cy + R * 0.6} size={R * 0.46} />
704+ <Sym s={s} id="M2" x={cx + R * 0.78} y={cy + R * 0.6} size={R * 0.46} />
705+ <Sym s={s} id="S" x={cx} y={cy + R * 0.72} size={R * 0.38} />
706+ <rect x={f(cx - R * 0.55)} y={f(by - R * 0.42)} width={f(R * 1.1)} height={f(R * 0.24)} rx={f(R * 0.05)} fill={p.glow} />
707+ <Small x={cx} y={by - R * 0.3} text="INSERT COIN" color="#fff" size={R * 0.1} weight={800} tracking="0.2em" />
708+ </>
709+ ) : null}
309 </g> 710 </g>
310 ); 711 );
311 } 712 }
312 713
313−function Asteroids({ W, H, rnd, p, id }: Ctx) { 714+/* 11 — VOID MINER: asteroid field, dark-matter core with level meter, drill beam */
314− const rocks: React.ReactNode[] = []; 715+function VoidMiner(s: Stage) {
315− for (let i = 0; i < 9; i++) { 716+ const { W, H, cx, cy, R, id, p, compact } = s;
316− const cx = between(rnd, 0, W); 717+ const stars = scatterDots(s, 60, [0, 0, W, H], 0.5, 1.3);
317− const cy = between(rnd, 0, H * 0.8); 718+ const rock = (x: number, y: number, r: number, n: number) => {
318− const rad = between(rnd, Math.min(W, H) * 0.03, Math.min(W, H) * 0.14); 719+ const list: string[] = [];
319− const n = 7 + Math.floor(rnd() * 4);
320− const pts: string[] = [];
321 for (let k = 0; k < n; k++) { 720 for (let k = 0; k < n; k++) {
322− const a = (k / n) * Math.PI * 2; 721+ const a = (k / n) * PI * 2;
323− const rr = rad * between(rnd, 0.72, 1.1); 722+ const rr = r * (0.72 + s.rnd() * 0.36);
324− pts.push(`${r2(cx + Math.cos(a) * rr)},${r2(cy + Math.sin(a) * rr)}`); 723+ list.push(`${f(x + Math.cos(a) * rr)},${f(y + Math.sin(a) * rr)}`);
325 } 724 }
326− rocks.push(<polygon key={i} points={pts.join(" ")} fill={i % 3 === 0 ? "#171226" : "#100c1c"} stroke={p.primary} strokeOpacity={0.45} strokeWidth={1.5} strokeLinejoin="round" />); 725+ return list.join(" ");
327− if (rnd() > 0.5) rocks.push(<circle key={`v${i}`} cx={r2(cx + rad * 0.2)} cy={r2(cy - rad * 0.1)} r={r2(rad * 0.18)} fill={p.secondary} fillOpacity={0.8} />); 726+ };
328− } 727+ const rocks = [
329− const stars: React.ReactNode[] = []; 728+ [cx - R * 1.0, cy - R * 0.95, R * 0.28, 8],
330− for (let i = 0; i < 40; i++) stars.push(<circle key={i} cx={r2(rnd() * W)} cy={r2(rnd() * H)} r={r2(between(rnd, 0.5, 1.3))} fill="#fff" fillOpacity={r2(between(rnd, 0.3, 0.8))} />); 729+ [cx + R * 1.0, cy - R * 0.2, R * 0.22, 7],
730+ [cx - R * 0.95, cy + R * 0.35, R * 0.2, 9],
731+ [cx + R * 0.7, cy + R * 0.95, R * 0.3, 8],
732+ [cx - R * 0.15, cy - R * 1.12, R * 0.16, 7],
733+ ] as const;
734+ const drillX = cx + R * 0.72;
735+ const drillY = cy - R * 0.72;
736+ const circ = 2 * PI * R * 0.66;
737+ const seg = circ / 5;
331 return ( 738 return (
332 <g> 739 <g>
333− {stars} 740+ <defs>
334− <circle cx={W * 0.5} cy={H * 0.45} r={Math.min(W, H) * 0.2} fill={`url(#${id}-glow)`} /> 741+ <Radial id={`${id}-vm1`} color={p.primary} o0={0.5} />
335− {rocks} 742+ <Radial id={`${id}-vm2`} color={p.secondary} o0={0.35} />
743+ </defs>
744+ <path d={stars} fill="#fff" fillOpacity={0.7} />
745+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.15)} fill={`url(#${id}-vm1)`} />
746+ {rocks.map((r, i) => (
747+ <polygon key={i} points={rock(r[0], r[1], r[2], r[3])} fill={i % 2 ? "#171226" : "#0f0b1a"} stroke={p.primary} strokeOpacity={0.5} strokeWidth={1.5} strokeLinejoin="round" />
748+ ))}
749+ <circle cx={f(cx + R * 0.7)} cy={f(cy + R * 0.95)} r={f(R * 0.5)} fill={`url(#${id}-vm2)`} />
750+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.66)} fill="none" stroke="#fff" strokeOpacity={0.12} strokeWidth={f(R * 0.07)} strokeDasharray={`${f(seg - 6)} 6`} transform={`rotate(-90 ${f(cx)} ${f(cy)})`} />
751+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.66)} fill="none" stroke={p.secondary} strokeOpacity={0.95} strokeWidth={f(R * 0.07)} strokeDasharray={`${f(seg - 6)} 6 ${f(seg - 6)} 6 ${f(seg - 6)} ${f(circ)}`} transform={`rotate(-90 ${f(cx)} ${f(cy)})`} strokeLinecap="butt" />
752+ <Sym s={s} id="DM" x={cx} y={cy} size={R * 0.9} plate={false} />
753+ {!compact ? (
754+ <>
755+ <line x1={f(drillX)} y1={f(drillY)} x2={f(cx + R * 1.0)} y2={f(cy - R * 0.2)} stroke={p.secondary} strokeWidth={2} strokeOpacity={0.9} strokeDasharray="6 4" />
756+ <Sym s={s} id="W" x={drillX} y={drillY} size={R * 0.56} rotate={30} />
757+ <Sym s={s} id="P1" x={cx - R * 0.72} y={cy - R * 0.5} size={R * 0.56} />
758+ <Sym s={s} id="H1" x={cx - R * 0.55} y={cy + R * 0.82} size={R * 0.44} />
759+ <Sym s={s} id="H2" x={cx + R * 0.15} y={cy + R * 0.95} size={R * 0.4} />
760+ <Small x={cx + R * 0.02} y={cy - R * 0.98} text="CORE LV 3 / 5" color={p.secondary} size={R * 0.075} tracking="0.2em" weight={700} />
761+ <Small x={cx + R * 1.12} y={cy + R * 0.4} text="×1 ×2 ×3 ×4 ×5" color={p.primary} size={R * 0.07} tracking="0.16em" rotate={-90} opacity={0.75} />
762+ </>
763+ ) : null}
336 </g> 764 </g>
337 ); 765 );
338 } 766 }
339 767
340−function Track({ W, H, p }: Ctx) { 768+/* 12 — ROYAL CIRCUIT: race-track sweep, lap counter, turbo boost ×2/×3 */
341− const lanes: React.ReactNode[] = []; 769+function RoyalCircuit(s: Stage) {
342− const cx = W * 1.05; 770+ const { W, cx, cy, R, id, p, compact } = s;
343− const cy = H * 0.55; 771+ 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)}`;
344− for (let i = 0; i < 6; i++) { 772+ let streaks = "";
345− const r = Math.max(W, H) * (0.28 + i * 0.13); 773+ for (let i = 0; i < 9; i++) {
346− lanes.push(<circle key={`l${i}`} cx={cx} cy={cy} r={r2(r)} fill="none" stroke={i % 5 === 0 ? p.primary : "#ffffff"} strokeOpacity={i % 5 === 0 ? 0.5 : 0.12} strokeWidth={i % 5 === 0 ? 5 : 2} />); 774+ const y = cy - R * 1.1 + s.rnd() * R * 2.2;
347− if (i > 0 && i < 5) lanes.push(<circle key={`d${i}`} cx={cx} cy={cy} r={r2(r - Math.max(W, H) * 0.065)} fill="none" stroke="#fff" strokeOpacity={0.18} strokeWidth={2} strokeDasharray="18 22" />); 775+ const x0 = s.rnd() * W * 0.5;
348− } 776+ streaks += `M${f(x0)} ${f(y)}h${f(R * (0.4 + s.rnd() * 0.9))} `;
349− const kerb: React.ReactNode[] = [];
350− const kr = Math.max(W, H) * 0.28;
351− for (let k = 0; k < 40; k++) {
352− const a = Math.PI * 0.5 + (k / 40) * Math.PI;
353− kerb.push(<circle key={k} cx={r2(cx + Math.cos(a) * kr)} cy={r2(cy + Math.sin(a) * kr)} r={4} fill={k % 2 ? p.secondary : "#f5f5f5"} fillOpacity={0.9} />);
354 } 777 }
778+ const lapX = cx - R * 0.75;
779+ const lapY = cy - R * 0.62;
780+ const lapR = R * 0.36;
781+ const lapC = 2 * PI * lapR;
782+ const lseg = lapC / 8;
355 return ( 783 return (
356 <g> 784 <g>
357− {lanes} 785+ <defs>
358− {kerb} 786+ <pattern id={`${id}-rc0`} width={f(R * 0.16)} height={f(R * 0.16)} patternUnits="userSpaceOnUse">
359− <line x1={W * 0.05} y1={H * 0.2} x2={W * 0.55} y2={H * 0.2} stroke={p.glow} strokeOpacity={0.5} strokeWidth={2} strokeDasharray="4 10" /> 787+ <rect width={f(R * 0.08)} height={f(R * 0.08)} fill="#fff" fillOpacity="0.85" />
788+ <rect x={f(R * 0.08)} y={f(R * 0.08)} width={f(R * 0.08)} height={f(R * 0.08)} fill="#fff" fillOpacity="0.85" />
789+ </pattern>
790+ <Radial id={`${id}-rc1`} color={p.primary} o0={0.35} />
791+ </defs>
792+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.3)} fill={`url(#${id}-rc1)`} />
793+ <path d={track} stroke="#1d1d26" strokeWidth={f(R * 0.62)} fill="none" strokeLinecap="round" />
794+ <path d={track} stroke="#f5f5f5" strokeWidth={f(R * 0.66)} fill="none" strokeDasharray={`${f(R * 0.14)} ${f(R * 0.14)}`} strokeOpacity={0.9} />
795+ <path d={track} stroke={p.secondary} strokeWidth={f(R * 0.66)} fill="none" strokeDasharray={`${f(R * 0.14)} ${f(R * 0.14)}`} strokeDashoffset={f(R * 0.14)} strokeOpacity={0.9} />
796+ <path d={track} stroke="#1d1d26" strokeWidth={f(R * 0.56)} fill="none" strokeLinecap="round" />
797+ <path d={track} stroke="#fff" strokeOpacity={0.35} strokeWidth={2} fill="none" strokeDasharray="14 18" />
798+ <path d={streaks} stroke={p.primary} strokeOpacity={0.55} strokeWidth={2} strokeLinecap="round" />
799+ <rect x={f(cx - R * 1.3)} y={f(cy + R * 1.02)} width={f(R * 2.6)} height={f(R * 0.16)} fill={`url(#${id}-rc0)`} />
800+ <Sym s={s} id="W" x={cx + R * 0.1} y={cy + R * 0.08} size={R * 0.96} rotate={-10} />
801+ {!compact ? (
802+ <>
803+ <Pill x={cx + R * 0.68} y={cy - R * 0.42} text="×2" color={p.primary} ink={p.primary} size={R * 0.1} />
804+ <Pill x={cx + R * 0.95} y={cy - R * 0.1} text="×3" color={p.secondary} ink="#fff" size={R * 0.1} />
805+ <circle cx={f(lapX)} cy={f(lapY)} r={f(lapR)} fill="#0b0b10" fillOpacity={0.85} stroke="#fff" strokeOpacity={0.1} strokeWidth={f(R * 0.07)} />
806+ <circle cx={f(lapX)} cy={f(lapY)} r={f(lapR)} fill="none" stroke={p.glow} strokeWidth={f(R * 0.07)} strokeDasharray={`${f(lseg - 4)} 4`} transform={`rotate(-90 ${f(lapX)} ${f(lapY)})`} strokeOpacity={0.95} />
807+ <circle cx={f(lapX)} cy={f(lapY)} r={f(lapR)} fill="none" stroke="#0b0b10" strokeWidth={f(R * 0.08)} strokeDasharray={`0 ${f(lseg * 6)} ${f(lapC)}`} transform={`rotate(-90 ${f(lapX)} ${f(lapY)})`} />
808+ <Small x={lapX} y={lapY - R * 0.05} text="LAP" color="#fff" size={R * 0.08} tracking="0.2em" opacity={0.7} />
809+ <Small x={lapX} y={lapY + R * 0.1} text="6 / 8" color={p.glow} size={R * 0.13} weight={800} tracking="0.02em" />
810+ <Sym s={s} id="S" x={cx + R * 0.85} y={cy - R * 0.98} size={R * 0.44} rotate={8} />
811+ <Sym s={s} id="P1" x={cx + R * 0.85} y={cy + R * 0.62} size={R * 0.5} />
812+ <Sym s={s} id="H1" x={cx - R * 0.85} y={cy + R * 0.5} size={R * 0.46} />
813+ </>
814+ ) : null}
360 </g> 815 </g>
361 ); 816 );
362 } 817 }
363 818
364−function hexPoints(cx: number, cy: number, r: number, rot = 0): string { 819+/* 13 — DRAGON CORE: reactor containment with a dragon charge gauge 0/20 */
365− const pts: string[] = []; 820+function DragonCore(s: Stage) {
366− for (let k = 0; k < 6; k++) { 821+ const { cx, cy, R, id, p, compact } = s;
367− const a = rot + (k / 6) * Math.PI * 2; 822+ let ticks = "";
368− pts.push(`${r2(cx + Math.cos(a) * r)},${r2(cy + Math.sin(a) * r)}`); 823+ let lit = "";
369− } 824+ for (let i = 0; i < 20; i++) {
370− return pts.join(" "); 825+ const a = -PI * 1.25 + (i / 19) * PI * 1.5;
371−} 826+ 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)} `;
372− 827+ if (i < 13) lit += d;
373−function Reactor({ W, H, rnd, p, id }: Ctx) { 828+ else ticks += d;
374− const cx = W * 0.5;
375− const cy = H * 0.42;
376− const base = Math.min(W, H) * 0.12;
377− const rings: React.ReactNode[] = [];
378− for (let i = 1; i <= 4; i++) rings.push(<polygon key={i} points={hexPoints(cx, cy, base * i, Math.PI / 6)} fill="none" stroke={i % 2 ? p.primary : p.secondary} strokeOpacity={r2(0.5 - i * 0.09)} strokeWidth={i === 1 ? 3 : 1.5} />);
379− const spokes: React.ReactNode[] = [];
380− for (let k = 0; k < 12; k++) {
381− const a = (k / 12) * Math.PI * 2;
382− const len = base * between(rnd, 3.2, 5);
383− spokes.push(<line key={k} x1={r2(cx + Math.cos(a) * base * 1.2)} y1={r2(cy + Math.sin(a) * base * 1.2)} x2={r2(cx + Math.cos(a) * len)} y2={r2(cy + Math.sin(a) * len)} stroke={p.primary} strokeOpacity={0.18} strokeWidth={1.5} />);
384− }
385− const cells: React.ReactNode[] = [];
386− for (let i = 0; i < 14; i++) {
387− const a = rnd() * Math.PI * 2;
388− const d = base * between(rnd, 3.5, 6);
389− cells.push(<polygon key={i} points={hexPoints(cx + Math.cos(a) * d, cy + Math.sin(a) * d, base * between(rnd, 0.25, 0.5), Math.PI / 6)} fill={p.primary} fillOpacity={r2(between(rnd, 0.08, 0.25))} />);
390 } 829 }
830+ const embers = scatterDots(s, 30, [cx - R * 1.3, cy - R * 1.3, R * 2.6, R * 2.6], 0.8, 2.4);
831+ const scales = [
832+ { x: cx + R * 0.95, y: cy + R * 0.45, r: 15 },
833+ { x: cx + R * 0.72, y: cy + R * 0.8, r: 30 },
834+ { x: cx + R * 0.35, y: cy + R * 1.0, r: 45 },
835+ ];
391 return ( 836 return (
392 <g> 837 <g>
393− {cells} 838+ <defs>
394− {spokes} 839+ <Radial id={`${id}-dc1`} color={p.primary} o0={0.55} mid={[0.5, p.glow, 0.2]} />
395− <circle cx={cx} cy={cy} r={base * 1.6} fill={`url(#${id}-glow)`} /> 840+ <Radial id={`${id}-dc2`} color={p.glow} o0={0.9} mid={[0.5, p.primary, 0.5]} />
396− {rings} 841+ </defs>
397− <polygon points={hexPoints(cx, cy, base * 0.55, Math.PI / 6)} fill={p.glow} fillOpacity={0.9} /> 842+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.5)} fill={`url(#${id}-dc1)`} />
843+ <polygon points={svgPolygon(cx, cy, R * 1.28, 6, PI / 6)} fill="none" stroke={p.secondary} strokeOpacity={0.4} strokeWidth={2} />
844+ <polygon points={svgPolygon(cx, cy, R * 0.84, 6, PI / 6)} fill="#1a0608" fillOpacity={0.8} stroke={p.secondary} strokeOpacity={0.85} strokeWidth={2.5} />
845+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.62)} fill={`url(#${id}-dc2)`} />
846+ <path d={ticks} stroke="#fff" strokeOpacity={0.18} strokeWidth={3} strokeLinecap="round" />
847+ <path d={lit} stroke={p.secondary} strokeWidth={3.5} strokeLinecap="round" />
848+ <Sym s={s} id="P1" x={cx} y={cy} size={R * 0.9} plate={false} />
849+ {!compact ? (
850+ <>
851+ {scales.map((sc, i) => (
852+ <Sym key={i} s={s} id="DC" x={sc.x} y={sc.y} size={R * 0.4} rotate={sc.r} />
853+ ))}
854+ <Sym s={s} id="W" x={cx - R * 0.9} y={cy + R * 0.7} size={R * 0.56} rotate={-12} />
855+ <Sym s={s} id="S" x={cx - R * 0.95} y={cy - R * 0.75} size={R * 0.42} />
856+ <Sym s={s} id="H1" x={cx + R * 0.95} y={cy - R * 0.75} size={R * 0.42} />
857+ <Small x={cx} y={cy + R * 1.24} text="DRAGON CHARGE 13 / 20" color={p.secondary} size={R * 0.075} tracking="0.2em" weight={700} opacity={0.9} />
858+ </>
859+ ) : null}
860+ <path d={embers} fill={p.secondary} fillOpacity={0.75} />
398 </g> 861 </g>
399 ); 862 );
400 } 863 }
401 864
402−function Abyss({ W, H, rnd, p, id }: Ctx) { 865+/* 14 — DEEP TREASURE: descending depth levels with treasure chests, bubbles */
403− const bubbles: React.ReactNode[] = []; 866+function DeepTreasure(s: Stage) {
404− for (let i = 0; i < 26; i++) { 867+ const { W, H, cx, cy, R, id, p, compact } = s;
405− const r = between(rnd, 2, Math.min(W, H) * 0.035); 868+ const bubbles: Array<[number, number, number]> = [];
406− const cx = rnd() * W; 869+ 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]);
407− const cy = between(rnd, H * 0.2, H); 870+ const levels = ["×1", "×2", "×3", "×4", "×6", "×8", "×10"];
408− bubbles.push(<circle key={`b${i}`} cx={r2(cx)} cy={r2(cy)} r={r2(r)} fill="none" stroke={p.primary} strokeOpacity={r2(between(rnd, 0.25, 0.6))} strokeWidth={1.2} />); 871+ const top = cy - R * 1.05;
409− bubbles.push(<circle key={`h${i}`} cx={r2(cx - r * 0.35)} cy={r2(cy - r * 0.35)} r={r2(r * 0.22)} fill="#fff" fillOpacity={0.5} />); 872+ const lh = R * 0.36;
410− } 873+ const chest = (x: number, y: number, w: number, key: string) => (
411− const rays: React.ReactNode[] = []; 874+ <g key={key}>
412− for (let i = 0; i < 5; i++) { 875+ <rect x={f(x - w / 2)} y={f(y - w * 0.28)} width={f(w)} height={f(w * 0.62)} rx={f(w * 0.06)} fill="#7a4a12" stroke="#fbbf24" strokeWidth={1.5} />
413− const x = W * (0.2 + i * 0.15); 876+ <path d={`M${f(x - w / 2)} ${f(y - w * 0.28)}a${f(w / 2)} ${f(w * 0.3)} 0 0 1 ${f(w)} 0z`} fill="#b8741c" stroke="#fbbf24" strokeWidth={1.5} />
414− rays.push(<polygon key={i} points={`${r2(x)},0 ${r2(x + W * 0.05)},0 ${r2(x + W * 0.2)},${H} ${r2(x - W * 0.02)},${H}`} fill={`url(#${id}-ray)`} />); 877+ <circle cx={f(x)} cy={f(y + w * 0.02)} r={f(w * 0.08)} fill="#fde68a" />
415− } 878+ </g>
879+ );
416 return ( 880 return (
417 <g> 881 <g>
418− {rays} 882+ <defs>
419− <ellipse cx={W * 0.5} cy={H * 0.95} rx={W * 0.5} ry={H * 0.12} fill={p.secondary} fillOpacity={0.18} filter={`url(#${id}-blur)`} /> 883+ <Linear id={`${id}-dt1`} stops={[[0, "#0a5a6e"], [0.35, "#063a4c"], [1, "#010610"]]} />
420− {bubbles} 884+ <Linear id={`${id}-dt2`} stops={[[0, "#bff6ff", 0.32], [1, "#bff6ff", 0]]} />
885+ </defs>
886+ <rect width={W} height={H} fill={`url(#${id}-dt1)`} />
887+ <polygon points={`${f(cx - R * 0.9)},0 ${f(cx - R * 0.55)},0 ${f(cx + R * 0.1)},${f(H)} ${f(cx - R * 0.9)},${f(H)}`} fill={`url(#${id}-dt2)`} />
888+ <polygon points={`${f(cx + R * 0.1)},0 ${f(cx + R * 0.5)},0 ${f(cx + R * 1.5)},${f(H)} ${f(cx + R * 0.8)},${f(H)}`} fill={`url(#${id}-dt2)`} />
889+ {!compact
890+ ? levels.map((l, i) => (
891+ <g key={l}>
892+ <line x1={f(cx - R * 1.3)} y1={f(top + i * lh)} x2={f(cx + R * 1.3)} y2={f(top + i * lh)} stroke={p.primary} strokeOpacity={0.18 + i * 0.03} strokeWidth={1} />
893+ <Small x={cx + R * 1.12} y={top + i * lh + lh * 0.5} text={l} color={i === 6 ? p.secondary : p.primary} size={R * (i === 6 ? 0.11 : 0.08)} weight={700} tracking="0.05em" opacity={0.55 + i * 0.06} anchor="end" />
894+ </g>
895+ ))
896+ : null}
897+ {!compact ? chest(cx - R * 0.78, cy - R * 0.5, R * 0.42, "c1") : null}
898+ {!compact ? chest(cx - R * 0.3, cy + R * 0.15, R * 0.5, "c2") : null}
899+ {chest(cx + R * 0.3, cy + R * 0.85, R * 0.6, "c3")}
900+ <Sym s={s} id="P1" x={cx + R * 0.3} y={cy + R * 0.2} size={R * 0.9} />
901+ {!compact ? (
902+ <>
903+ <Sym s={s} id="W" x={cx - R * 0.55} y={cy - R * 0.95} size={R * 0.6} />
904+ <Sym s={s} id="H1" x={cx + R * 0.85} y={cy - R * 0.62} size={R * 0.46} />
905+ <Sym s={s} id="H2" x={cx - R * 0.95} y={cy + R * 0.78} size={R * 0.42} />
906+ <Sym s={s} id="S" x={cx + R * 0.32} y={cy - R * 0.95} size={R * 0.36} />
907+ </>
908+ ) : null}
909+ <path d={dots(bubbles)} fill="none" stroke="#dffaff" strokeOpacity={0.55} strokeWidth={1.2} />
421 </g> 910 </g>
422 ); 911 );
423 } 912 }
424 913
425−function Moon({ W, H, rnd, p, id }: Ctx) { 914+/* 15 — MOONBASE 77: retro lunar base, moon horizon, drifting wilds */
426− const R = Math.min(W, H) * 0.42; 915+function Moonbase77(s: Stage) {
427− const cx = W * 0.62; 916+ const { W, H, cx, cy, R, id, p, compact } = s;
428− const cy = H * 0.5; 917+ const stars = scatterDots(s, 70, [0, 0, W, H * 0.8], 0.5, 1.4);
429− const craters: React.ReactNode[] = []; 918+ const mR = R * 3.2;
430− for (let i = 0; i < 12; i++) { 919+ const mY = cy + R * 0.95 + mR;
431− const a = rnd() * Math.PI * 2; 920+ const craters: Array<[number, number, number]> = [];
432− const d = rnd() * R * 0.8; 921+ 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)]);
433− const r = between(rnd, R * 0.04, R * 0.16); 922+ const domeX = cx - R * 0.55;
434− const x = cx + Math.cos(a) * d; 923+ const domeY = cy + R * 0.95;
435− const y = cy + Math.sin(a) * d; 924+ const domeR = R * 0.55;
436− craters.push(<circle key={`c${i}`} cx={r2(x)} cy={r2(y)} r={r2(r)} fill="#000" fillOpacity={0.35} />); 925+ let trail = "";
437− craters.push(<circle key={`r${i}`} cx={r2(x)} cy={r2(y)} r={r2(r)} fill="none" stroke={p.glow} strokeOpacity={0.35} strokeWidth={1.5} />); 926+ for (const [x, y] of [
438− craters.push(<circle key={`s${i}`} cx={r2(x + r * 0.3)} cy={r2(y + r * 0.3)} r={r2(r * 0.55)} fill="#000" fillOpacity={0.2} />); 927+ [cx - R * 0.92, cy - R * 0.72],
439− } 928+ [cx - R * 0.15, cy - R * 0.9],
440− const stars: React.ReactNode[] = []; 929+ ]) 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)} `;
441− for (let i = 0; i < 40; i++) stars.push(<circle key={i} cx={r2(rnd() * W)} cy={r2(rnd() * H)} r={r2(between(rnd, 0.5, 1.4))} fill="#fff" fillOpacity={r2(between(rnd, 0.3, 0.9))} />);
442 return ( 930 return (
443 <g> 931 <g>
444− {stars} 932+ <defs>
445− <circle cx={cx} cy={cy} r={R * 1.15} fill={`url(#${id}-glow)`} /> 933+ <Radial id={`${id}-mb1`} color="#3a3d4a" o0={1} o1={1} mid={[0.6, "#232633", 1]} />
446− <circle cx={cx} cy={cy} r={R} fill={`url(#${id}-moon)`} /> 934+ <Radial id={`${id}-mb2`} color="#60a5fa" o0={1} mid={[0.55, "#1e3a8a", 1]} o1={1} />
447− {craters} 935+ <Linear id={`${id}-mb3`} stops={[[0, "#2b2f3d"], [1, "#151824"]]} />
448− <path d={`M${r2(cx - R)} ${r2(cy)} A ${r2(R)} ${r2(R)} 0 0 0 ${r2(cx + R)} ${r2(cy)} Z`} fill="#000" fillOpacity={0.18} transform={`rotate(-28 ${cx} ${cy})`} /> 936+ </defs>
449− <line x1={0} y1={H * 0.82} x2={W} y2={H * 0.82} stroke={p.primary} strokeOpacity={0.5} strokeWidth={1.5} /> 937+ <path d={stars} fill="#fff" fillOpacity={0.8} />
450− <rect x={W * 0.1} y={H * 0.72} width={W * 0.16} height={H * 0.1} fill={p.bg} stroke={p.primary} strokeOpacity={0.6} /> 938+ <circle cx={f(cx + R * 0.95)} cy={f(cy - R * 1.05)} r={f(R * 0.16)} fill={`url(#${id}-mb2)`} />
451− <rect x={W * 0.13} y={H * 0.75} width={W * 0.04} height={H * 0.03} fill={p.primary} fillOpacity={0.9} /> 939+ <circle cx={f(cx)} cy={f(mY)} r={f(mR)} fill={`url(#${id}-mb1)`} />
940+ <path d={dots(craters)} fill="#000" fillOpacity={0.35} stroke="#9aa0b4" strokeOpacity={0.35} strokeWidth={1} />
941+ <path d={`M${f(domeX - domeR)} ${f(domeY)}a${f(domeR)} ${f(domeR)} 0 0 1 ${f(domeR * 2)} 0z`} fill={`url(#${id}-mb3)`} stroke={p.secondary} strokeOpacity={0.8} strokeWidth={2} />
942+ <path d={`M${f(domeX)} ${f(domeY - domeR)}V${f(domeY)} M${f(domeX - domeR * 0.7)} ${f(domeY - domeR * 0.7)}L${f(domeX + domeR * 0.7)} ${f(domeY - domeR * 0.7)}`} stroke={p.secondary} strokeOpacity={0.35} strokeWidth={1.2} />
943+ <rect x={f(domeX - domeR * 0.28)} y={f(domeY - domeR * 0.45)} width={f(domeR * 0.56)} height={f(domeR * 0.32)} rx={3} fill={p.primary} fillOpacity={0.9} />
944+ <path d={`M${f(domeX + domeR * 0.85)} ${f(domeY)}V${f(domeY - domeR * 1.6)}h${f(domeR * 0.25)}`} stroke="#c9cdd8" strokeWidth={2} fill="none" />
945+ <circle cx={f(domeX + domeR * 1.1)} cy={f(domeY - domeR * 1.6)} r={3} fill="#ff4d4d" />
946+ <path d={`M${f(cx - R * 1.3)} ${f(cy + R * 0.98)}H${f(cx + R * 1.3)}`} stroke={p.primary} strokeOpacity={0.9} strokeWidth={2} />
947+ <Sym s={s} id="P1" x={cx + R * 0.52} y={cy + R * 0.28} size={R * 0.98} />
948+ {!compact ? (
949+ <>
950+ <path d={trail} stroke="#fff" strokeOpacity={0.4} strokeWidth={1.5} strokeDasharray="2 6" fill="none" />
951+ <Sym s={s} id="W" x={cx - R * 0.92} y={cy - R * 0.72} size={R * 0.56} rotate={-14} />
952+ <Sym s={s} id="W" x={cx - R * 0.15} y={cy - R * 0.9} size={R * 0.42} rotate={10} />
953+ <Sym s={s} id="H1" x={cx - R * 0.55} y={cy + R * 0.15} size={R * 0.5} />
954+ <Sym s={s} id="S" x={cx + R * 0.35} y={cy - R * 0.62} size={R * 0.4} />
955+ <Sym s={s} id="H2" x={cx - R * 1.02} y={cy - R * 0.05} size={R * 0.38} />
956+ </>
957+ ) : null}
452 </g> 958 </g>
453 ); 959 );
454 } 960 }
455 961
456−function Temple({ W, H, rnd, p, id }: Ctx) { 962+/* 16 — WILD TEMPLE: stepped stone chambers, jaguar wild with moving arrows, torches */
457− const steps: React.ReactNode[] = []; 963+function WildTemple(s: Stage) {
458− const n = 6; 964+ const { W, H, cx, cy, R, id, p, compact } = s;
965+ let steps = "";
966+ const n = 5;
967+ const baseY = cy + R * 1.15;
968+ const sh = R * 0.3;
459 for (let i = 0; i < n; i++) { 969 for (let i = 0; i < n; i++) {
460− const wdt = W * (0.95 - i * 0.14); 970+ const w = R * (2.7 - i * 0.48);
461− const h = H * 0.11; 971+ steps += `M${f(cx - w / 2)} ${f(baseY - sh * (i + 1))}h${f(w)}v${f(sh)}h${f(-w)}z `;
462− const y = H - h * (i + 1);
463− steps.push(<rect key={i} x={r2((W - wdt) / 2)} y={r2(y)} width={r2(wdt)} height={r2(h)} fill={i % 2 ? "#08170f" : "#0a1c12"} stroke={p.primary} strokeOpacity={0.35} />);
464− const blocks = Math.floor(wdt / (W * 0.08));
465− for (let b = 0; b < blocks; b++) if (rnd() > 0.5) steps.push(<line key={`${i}-${b}`} x1={r2((W - wdt) / 2 + (b + 1) * (wdt / (blocks + 1)))} y1={r2(y)} x2={r2((W - wdt) / 2 + (b + 1) * (wdt / (blocks + 1)))} y2={r2(y + h)} stroke={p.primary} strokeOpacity={0.15} />);
466 } 972 }
467− const vines: React.ReactNode[] = []; 973+ let joints = "";
468− for (let i = 0; i < 8; i++) { 974+ for (let i = 0; i < n; i++) {
469− const x = between(rnd, W * 0.05, W * 0.95); 975+ const w = R * (2.7 - i * 0.48);
470− vines.push(<path key={i} d={`M${r2(x)} ${H} q ${r2(between(rnd, -30, 30))} ${r2(-H * 0.2)} ${r2(between(rnd, -20, 20))} ${r2(-H * between(rnd, 0.3, 0.6))}`} fill="none" stroke={p.primary} strokeOpacity={0.35} strokeWidth={2} strokeLinecap="round" />); 976+ const y = baseY - sh * (i + 1);
977+ 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)} `;
471 } 978 }
979+ const motes = scatterDots(s, 30, [0, 0, W, H], 0.6, 1.8);
980+ 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)}`;
981+ 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)}`;
982+ const jx = cx;
983+ const jy = cy - R * 0.05;
472 return ( 984 return (
473 <g> 985 <g>
474− <circle cx={W * 0.5} cy={H * 0.3} r={Math.min(W, H) * 0.24} fill={`url(#${id}-glow)`} /> 986+ <defs>
475− <circle cx={W * 0.5} cy={H * 0.3} r={Math.min(W, H) * 0.08} fill={p.secondary} fillOpacity={0.85} /> 987+ <Radial id={`${id}-wt1`} color={p.secondary} o0={0.8} mid={[0.4, p.secondary, 0.25]} />
476− {steps} 988+ <Linear id={`${id}-wt2`} stops={[[0, p.bg, 0], [1, "#000", 0.8]]} />
477− {vines} 989+ <Radial id={`${id}-wt3`} color={p.glow} o0={0.35} />
990+ </defs>
991+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.5)} fill={`url(#${id}-wt3)`} />
992+ <ellipse cx={f(cx - R * 1.1)} cy={f(cy - R * 1.3)} rx={f(R * 0.9)} ry={f(R * 0.5)} fill="#041108" />
993+ <ellipse cx={f(cx + R * 1.15)} cy={f(cy - R * 1.25)} rx={f(R * 0.85)} ry={f(R * 0.45)} fill="#041108" />
994+ <circle cx={f(cx)} cy={f(cy - R * 1.02)} r={f(R * 0.5)} fill={`url(#${id}-wt1)`} />
995+ <path d={steps} fill="#0d2417" stroke="#3fbf8a" strokeOpacity={0.55} strokeWidth={1.5} />
996+ <path d={joints} stroke="#3fbf8a" strokeOpacity={0.22} strokeWidth={1} />
997+ <rect x={f(cx - R * 0.42)} y={f(baseY - sh * 3)} width={f(R * 0.84)} height={f(sh * 3)} fill="#02100a" />
998+ <rect x={f(cx - R * 0.42)} y={f(baseY - sh * 3)} width={f(R * 0.84)} height={f(sh * 3)} fill={`url(#${id}-wt2)`} />
999+ <path d={vines} stroke="#22c55e" strokeOpacity={0.6} strokeWidth={2.5} fill="none" strokeLinecap="round" />
1000+ <Sym s={s} id="S" x={cx} y={cy - R * 1.02} size={R * 0.48} plate={false} />
1001+ <Sym s={s} id="W" x={jx} y={jy} size={R * 0.92} />
1002+ {!compact ? (
1003+ <>
1004+ <path d={`${chevron(jx - R * 0.62, jy, -1, 1)} ${chevron(jx + R * 0.62, jy, 1, 1)}`} stroke={p.secondary} strokeWidth={3} fill="none" strokeLinecap="round" strokeLinejoin="round" />
1005+ <path d={`${chevron(jx - R * 0.78, jy, -1, 1)} ${chevron(jx + R * 0.78, jy, 1, 1)}`} stroke={p.secondary} strokeWidth={3} fill="none" strokeLinecap="round" strokeLinejoin="round" opacity={0.45} />
1006+ <Sym s={s} id="P1" x={cx - R * 0.98} y={cy + R * 0.62} size={R * 0.54} />
1007+ <Sym s={s} id="H1" x={cx + R * 0.98} y={cy + R * 0.62} size={R * 0.5} />
1008+ <Sym s={s} id="H2" x={cx - R * 1.05} y={cy - R * 0.45} size={R * 0.4} />
1009+ <Sym s={s} id="H2" x={cx + R * 1.05} y={cy - R * 0.45} size={R * 0.4} />
1010+ <Sym s={s} id="M1" x={cx + R * 0.5} y={cy + R * 1.0} size={R * 0.34} rotate={30} />
1011+ </>
1012+ ) : null}
1013+ <path d={motes} fill={p.secondary} fillOpacity={0.55} />
478 </g> 1014 </g>
479 ); 1015 );
480 } 1016 }
481 1017
482−function Gravity({ W, H, rnd, p, id }: Ctx) { 1018+/* 17 — ZERO GRAVITY: three grids 5×3 → 6×4 → 7×5 floating in orbit */
483− const cx = W * 0.5; 1019+function ZeroGravity(s: Stage) {
484− const cy = H * 0.45; 1020+ const { W, H, cx, cy, R, id, p, compact } = s;
485− const orbits: React.ReactNode[] = []; 1021+ const stars = scatterDots(s, 70, [0, 0, W, H], 0.5, 1.4);
486− for (let i = 0; i < 5; i++) { 1022+ const grid = (x: number, y: number, cols: number, rows: number, cell: number, op: number, key: string, label: string, skew: number) => {
487− const rx = Math.min(W, H) * (0.22 + i * 0.1); 1023+ const w = cols * cell;
488− const ry = rx * between(rnd, 0.3, 0.5); 1024+ const h = rows * cell;
489− const rot = between(rnd, -60, 60); 1025+ let lines = "";
490− orbits.push(<ellipse key={`o${i}`} cx={cx} cy={cy} rx={r2(rx)} ry={r2(ry)} fill="none" stroke={i % 2 ? p.primary : p.secondary} strokeOpacity={r2(0.55 - i * 0.08)} strokeWidth={1.5} transform={`rotate(${r2(rot)} ${cx} ${cy})`} />); 1026+ for (let c = 1; c < cols; c++) lines += `M${f(x + c * cell)} ${f(y)}v${f(h)} `;
491− const a = rnd() * Math.PI * 2; 1027+ for (let r = 1; r < rows; r++) lines += `M${f(x)} ${f(y + r * cell)}h${f(w)} `;
492− const px = cx + Math.cos(a) * rx; 1028+ return (
493− const py = cy + Math.sin(a) * ry; 1029+ <g key={key} opacity={op} transform={`translate(${f(x + w / 2)} ${f(y + h / 2)}) skewY(${skew}) translate(${f(-(x + w / 2))} ${f(-(y + h / 2))})`}>
494− orbits.push(<circle key={`p${i}`} cx={r2(px)} cy={r2(py)} r={r2(between(rnd, 3, 8))} fill={i % 2 ? p.secondary : p.primary} transform={`rotate(${r2(rot)} ${cx} ${cy})`} />); 1030+ <rect x={f(x)} y={f(y)} width={f(w)} height={f(h)} rx={f(cell * 0.15)} fill={p.bg} fillOpacity={0.75} stroke={p.primary} strokeWidth={2} />
495− } 1031+ <path d={lines} stroke={p.primary} strokeOpacity={0.55} strokeWidth={1} />
496− const stars: React.ReactNode[] = []; 1032+ <Small x={x + w / 2} y={y - cell * 0.45} text={label} color={p.glow} size={cell * 0.55} weight={800} tracking="0.05em" />
497− for (let i = 0; i < 40; i++) stars.push(<circle key={i} cx={r2(rnd() * W)} cy={r2(rnd() * H)} r={r2(between(rnd, 0.5, 1.3))} fill="#fff" fillOpacity={r2(between(rnd, 0.3, 0.8))} />); 1033+ </g>
1034+ );
1035+ };
1036+ const c1 = R * 0.13;
1037+ const c2 = R * 0.155;
1038+ const c3 = R * 0.19;
498 return ( 1039 return (
499 <g> 1040 <g>
500− {stars} 1041+ <defs>
501− <circle cx={cx} cy={cy} r={Math.min(W, H) * 0.16} fill={`url(#${id}-glow)`} /> 1042+ <Radial id={`${id}-zg1`} color={p.secondary} o0={0.35} />
502− <circle cx={cx} cy={cy} r={Math.min(W, H) * 0.045} fill="#fff" fillOpacity={0.95} /> 1043+ </defs>
503− {orbits} 1044+ <path d={stars} fill="#fff" fillOpacity={0.75} />
1045+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.4)} fill={`url(#${id}-zg1)`} />
1046+ <ellipse cx={f(cx)} cy={f(cy)} rx={f(R * 1.35)} ry={f(R * 0.5)} fill="none" stroke={p.primary} strokeOpacity={0.35} strokeWidth={1.2} transform={`rotate(-18 ${f(cx)} ${f(cy)})`} />
1047+ <ellipse cx={f(cx)} cy={f(cy)} rx={f(R * 1.1)} ry={f(R * 0.75)} fill="none" stroke={p.secondary} strokeOpacity={0.3} strokeWidth={1.2} transform={`rotate(32 ${f(cx)} ${f(cy)})`} />
1048+ {!compact ? grid(cx - R * 1.12, cy - R * 1.02, 5, 3, c1, 0.5, "g1", "5×3", -8) : null}
1049+ {!compact ? grid(cx - R * 0.35, cy - R * 0.62, 6, 4, c2, 0.72, "g2", "6×4", -5) : null}
1050+ {grid(cx - R * 0.66, cy + R * 0.05, 7, 5, c3, 1, "g3", "7×5", -3)}
1051+ {!compact ? (
1052+ <path d={`M${f(cx - R * 0.42)} ${f(cy - R * 0.72)}l${f(R * 0.14)} ${f(R * 0.02)}l${f(-R * 0.05)} ${f(-R * 0.07)} M${f(cx + R * 0.02)} ${f(cy - R * 0.02)}l${f(R * 0.04)} ${f(R * 0.14)}l${f(-R * 0.1)} ${f(-R * 0.02)}`} stroke={p.glow} strokeWidth={2} fill="none" strokeLinecap="round" />
1053+ ) : null}
1054+ <Sym s={s} id="W" x={cx + R * 0.72} y={cy - R * 0.62} size={R * 0.82} plate={false} />
1055+ {!compact ? (
1056+ <>
1057+ <Sym s={s} id="P1" x={cx - R * 0.95} y={cy + R * 0.02} size={R * 0.48} />
1058+ <Sym s={s} id="S" x={cx + R * 1.05} y={cy + R * 0.42} size={R * 0.42} />
1059+ <Sym s={s} id="H2" x={cx + R * 0.02} y={cy - R * 1.12} size={R * 0.38} rotate={12} />
1060+ <Sym s={s} id="M1" x={cx + R * 0.98} y={cy + R * 1.0} size={R * 0.34} rotate={-30} />
1061+ </>
1062+ ) : null}
504 </g> 1063 </g>
505 ); 1064 );
506 } 1065 }
507 1066
508−function Core({ W, H, rnd, p, id }: Ctx) { 1067+/* 18 — REEL REACTOR: heat gauge 0–100 with meltdown zone, hazard stripes */
509− const cx = W * 0.5; 1068+function ReelReactor(s: Stage) {
510− const cy = H * 0.44; 1069+ const { W, cx, cy, R, id, p, compact } = s;
511− const R = Math.min(W, H) * 0.22; 1070+ const gR = R * 1.0;
512− const arcs: React.ReactNode[] = []; 1071+ const a0 = PI * 0.78;
513− for (let i = 0; i < 3; i++) { 1072+ const a1 = PI * 2.22;
514− const r = R * (1.5 + i * 0.35); 1073+ const at = (t: number) => a0 + (a1 - a0) * t;
515− arcs.push(<circle key={i} cx={cx} cy={cy} r={r2(r)} fill="none" stroke={i ? p.secondary : p.primary} strokeOpacity={0.35 - i * 0.08} strokeWidth={i ? 1.5 : 3} strokeDasharray={`${r2(r * 1.2)} ${r2(r * 0.9)}`} transform={`rotate(${r2(rnd() * 360)} ${cx} ${cy})`} />); 1074+ const needle = at(0.72);
516− } 1075+ let hazard = "";
517− const embers: React.ReactNode[] = []; 1076+ const hy = cy + R * 1.08;
518− for (let i = 0; i < 30; i++) { 1077+ 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 `;
519− const a = rnd() * Math.PI * 2; 1078+ const tick = (t: number, label: string, color: string) => {
520− const d = R * between(rnd, 1.1, 3.2); 1079+ const a = at(t);
521− embers.push(<circle key={i} cx={r2(cx + Math.cos(a) * d)} cy={r2(cy + Math.sin(a) * d * 0.8)} r={r2(between(rnd, 1, 3))} fill={rnd() > 0.5 ? p.secondary : p.primary} fillOpacity={r2(between(rnd, 0.3, 0.9))} />); 1080+ return <Small key={label} x={cx + Math.cos(a) * gR * 1.22} y={cy + Math.sin(a) * gR * 1.22} text={label} color={color} size={R * 0.08} weight={700} tracking="0.02em" />;
522− } 1081+ };
523 return ( 1082 return (
524 <g> 1083 <g>
525− {embers} 1084+ <defs>
526− <circle cx={cx} cy={cy} r={R * 2.2} fill={`url(#${id}-glow)`} /> 1085+ <Radial id={`${id}-rr1`} color={p.glow} o0={0.55} />
527− {arcs} 1086+ </defs>
528− <circle cx={cx} cy={cy} r={R} fill={`url(#${id}-sphere)`} /> 1087+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.3)} fill={`url(#${id}-rr1)`} />
529− <circle cx={r2(cx - R * 0.3)} cy={r2(cy - R * 0.35)} r={R * 0.28} fill="#fff" fillOpacity={0.35} filter={`url(#${id}-blur)`} /> 1088+ <path d={svgArc(cx, cy, gR, a0, a1)} fill="none" stroke="#0f1a0c" strokeWidth={f(R * 0.2)} strokeLinecap="round" />
1089+ <path d={svgArc(cx, cy, gR, at(0), at(0.4))} fill="none" stroke={p.glow} strokeWidth={f(R * 0.14)} />
1090+ <path d={svgArc(cx, cy, gR, at(0.4), at(0.8))} fill="none" stroke={p.secondary} strokeWidth={f(R * 0.14)} />
1091+ <path d={svgArc(cx, cy, gR, at(0.8), at(1))} fill="none" stroke="#ef4444" strokeWidth={f(R * 0.14)} />
1092+ <path d={svgArc(cx, cy, gR, at(0.8), at(1))} fill="none" stroke="#fff" strokeWidth={f(R * 0.14)} strokeDasharray="4 8" strokeOpacity={0.35} />
1093+ {!compact ? [tick(0, "0", "#c9d1c4"), tick(0.4, "40", p.secondary), tick(0.8, "80", "#ef4444"), tick(1, "100", "#ef4444")] : null}
1094+ <line x1={f(cx)} y1={f(cy)} x2={f(cx + Math.cos(needle) * gR * 0.92)} y2={f(cy + Math.sin(needle) * gR * 0.92)} stroke="#fff" strokeWidth={3} strokeLinecap="round" />
1095+ <Sym s={s} id="P1" x={cx} y={cy} size={R * 0.78} plate={false} />
1096+ {!compact ? (
1097+ <>
1098+ <Small x={cx} y={cy + R * 0.62} text="HEAT 72" color={p.secondary} size={R * 0.12} weight={800} tracking="0.06em" />
1099+ <Pill x={cx + R * 0.72} y={cy - R * 1.1} text="MELTDOWN" color="#ef4444" ink="#ff8a8a" bg="#2a0808" size={R * 0.07} />
1100+ <Sym s={s} id="W" x={cx - R * 0.9} y={cy + R * 0.62} size={R * 0.5} />
1101+ <Sym s={s} id="S" x={cx + R * 0.9} y={cy + R * 0.62} size={R * 0.5} rotate={15} />
1102+ <Sym s={s} id="H1" x={cx - R * 1.05} y={cy - R * 0.55} size={R * 0.4} />
1103+ <Sym s={s} id="M2" x={cx + R * 1.05} y={cy - R * 0.5} size={R * 0.4} />
1104+ </>
1105+ ) : null}
1106+ <path d={hazard} fill={p.secondary} fillOpacity={0.9} />
1107+ <rect x={0} y={f(hy)} width={W} height={f(R * 0.12)} fill="none" stroke="#000" strokeOpacity={0.6} strokeWidth={1} />
530 </g> 1108 </g>
531 ); 1109 );
532 } 1110 }
533 1111
534−function Minimal({ W, H, p }: Ctx) { 1112+/* 19 — OBSIDIAN: pure black, one thin gold line, one white-gold wild, a whisper of 50,000× */
535− const y = H * 0.5; 1113+function Obsidian(s: Stage) {
1114+ const { W, H, cx, cy, R, p, compact } = s;
1115+ const dust = scatterDots(s, 14, [0, 0, W, H], 0.4, 1.0);
1116+ const lineY = cy + R * 0.62;
536 return ( 1117 return (
537 <g> 1118 <g>
538− <line x1={W * 0.1} y1={y} x2={W * 0.9} y2={y} stroke={p.secondary} strokeWidth={1.5} strokeOpacity={0.85} /> 1119+ <rect width={W} height={H} fill="#000" />
539− <line x1={W * 0.1} y1={y} x2={W * 0.9} y2={y} stroke={p.secondary} strokeWidth={6} strokeOpacity={0.15} /> 1120+ <path d={dust} fill={p.secondary} fillOpacity={0.4} />
540− <circle cx={W * 0.9} cy={y} r={3} fill={p.primary} /> 1121+ <polygon points={svgPolygon(cx, cy - R * 0.05, R * 0.78, 6)} fill="#050505" stroke={p.primary} strokeOpacity={0.14} strokeWidth={1} />
541− <circle cx={W * 0.1} cy={y} r={1.5} fill={p.secondary} /> 1122+ <Sym s={s} id="W" x={cx} y={cy - R * 0.05} size={R * 0.72} plate={false} halo={false} />
542− <line x1={W * 0.5} y1={H * 0.3} x2={W * 0.5} y2={H * 0.34} stroke={p.secondary} strokeOpacity={0.5} /> 1123+ <line x1={f(W * 0.12)} y1={f(lineY)} x2={f(W * 0.88)} y2={f(lineY)} stroke={p.secondary} strokeWidth={1} strokeOpacity={0.9} />
543− <line x1={W * 0.5} y1={H * 0.66} x2={W * 0.5} y2={H * 0.7} stroke={p.secondary} strokeOpacity={0.5} /> 1124+ <circle cx={f(W * 0.88)} cy={f(lineY)} r={2} fill={p.primary} />
1125+ {!compact ? <Small x={W * 0.88} y={lineY + R * 0.14} text="50,000×" color={p.secondary} size={R * 0.07} tracking="0.3em" anchor="end" opacity={0.85} /> : null}
544 </g> 1126 </g>
545 ); 1127 );
546 } 1128 }
547 1129
548−function Universe({ W, H, rnd, p, id }: Ctx) { 1130+/* 20 — SPINZA ORIGINAL: the Spinza ring mark, Mystery Orb, Mega Meter, cascading symbols */
549− const cx = W * 0.5; 1131+function SpinzaOriginal(s: Stage) {
550− const cy = H * 0.44; 1132+ const { W, H, cx, cy, R, id, p, compact } = s;
551− const R = Math.min(W, H) * 0.3; 1133+ const stars = scatterDots(s, 60, [0, 0, W, H], 0.5, 1.4);
552− const rays: React.ReactNode[] = []; 1134+ let rays = "";
553− for (let i = 0; i < 48; i++) { 1135+ for (let i = 0; i < 28; i++) {
554− const a = (i / 48) * Math.PI * 2; 1136+ const a = (i / 28) * PI * 2;
555− const len = R * between(rnd, 1.2, 2.6); 1137+ const len = R * (1.15 + s.rnd() * 0.7);
556− rays.push(<line key={i} x1={r2(cx + Math.cos(a) * R * 0.5)} y1={r2(cy + Math.sin(a) * R * 0.5)} x2={r2(cx + Math.cos(a) * len)} y2={r2(cy + Math.sin(a) * len)} stroke={i % 4 === 0 ? p.secondary : p.primary} strokeOpacity={r2(between(rnd, 0.1, 0.45))} strokeWidth={i % 6 === 0 ? 2 : 1} strokeLinecap="round" />); 1138+ 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)} `;
557 } 1139 }
558− const stars: React.ReactNode[] = []; 1140+ const mR = R * 0.78;
559− for (let i = 0; i < 50; i++) stars.push(<circle key={i} cx={r2(rnd() * W)} cy={r2(rnd() * H)} r={r2(between(rnd, 0.5, 1.5))} fill="#fff" fillOpacity={r2(between(rnd, 0.3, 0.9))} />); 1141+ const circ = 2 * PI * mR;
1142+ const seg = circ / 30;
1143+ const colX = cx + R * 0.98;
1144+ const fall = ["P1", "H1", "H2", "M1"];
560 return ( 1145 return (
561 <g> 1146 <g>
562− {stars} 1147+ <defs>
563− <circle cx={cx} cy={cy} r={R * 2.4} fill={`url(#${id}-glow)`} /> 1148+ <Radial id={`${id}-so1`} color={p.primary} o0={0.6} mid={[0.5, p.glow, 0.18]} />
564− {rays} 1149+ <Linear id={`${id}-so2`} stops={[[0, p.primary], [0.5, "#fff"], [1, p.secondary]]} dir="d" />
565− <circle cx={cx} cy={cy} r={R} fill="none" stroke={`url(#${id}-ring)`} strokeWidth={Math.max(3, R * 0.06)} /> 1150+ </defs>
566− <circle cx={cx} cy={cy} r={R * 0.82} fill="none" stroke={p.secondary} strokeOpacity={0.35} strokeWidth={1.5} strokeDasharray="6 10" /> 1151+ <path d={stars} fill="#fff" fillOpacity={0.75} />
567− <circle cx={cx} cy={cy} r={R * 0.12} fill="#fff" /> 1152+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.5)} fill={`url(#${id}-so1)`} />
1153+ <path d={rays} stroke={p.primary} strokeOpacity={0.35} strokeWidth={1.5} strokeLinecap="round" />
1154+ <circle cx={f(cx)} cy={f(cy)} r={f(mR)} fill="none" stroke="#fff" strokeOpacity={0.12} strokeWidth={f(R * 0.06)} strokeDasharray={`${f(seg - 3)} 3`} transform={`rotate(-90 ${f(cx)} ${f(cy)})`} />
1155+ <circle cx={f(cx)} cy={f(cy)} r={f(mR)} fill="none" stroke={`url(#${id}-so2)`} strokeWidth={f(R * 0.06)} strokeDasharray={`${f(seg * 19 - 3)} ${f(circ)}`} transform={`rotate(-90 ${f(cx)} ${f(cy)})`} />
1156+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.6)} fill="none" stroke={p.secondary} strokeOpacity={0.4} strokeWidth={1.5} strokeDasharray="4 8" />
1157+ <Sym s={s} id="W" x={cx} y={cy} size={R * 0.9} plate={false} />
1158+ {!compact ? (
1159+ <>
1160+ <Small x={cx} y={cy + R * 0.98} text="MEGA METER 19 / 30" color={p.glow} size={R * 0.07} tracking="0.22em" weight={700} opacity={0.9} />
1161+ <Sym s={s} id="MY" x={cx - R * 0.98} y={cy - R * 0.62} size={R * 0.62} />
1162+ <Sym s={s} id="Z" x={cx - R * 0.98} y={cy + R * 0.3} size={R * 0.5} />
1163+ <rect x={f(colX - R * 0.26)} y={f(cy - R * 1.25)} width={f(R * 0.52)} height={f(R * 2.4)} rx={f(R * 0.05)} fill="#fff" fillOpacity={0.04} stroke="#fff" strokeOpacity={0.12} />
1164+ {fall.map((idd, i) => (
1165+ <Sym key={idd} s={s} id={idd} x={colX} y={cy - R * 0.95 + i * R * 0.56} size={R * 0.46} opacity={0.55 + i * 0.15} />
1166+ ))}
1167+ <Pill x={cx - R * 0.5} y={cy - R * 1.18} text="WILD DROP" color={p.secondary} ink={p.secondary} size={R * 0.065} />
1168+ <Pill x={cx + R * 0.3} y={cy - R * 1.18} text="MULTIPLIER DROP" color={p.glow} ink={p.glow} size={R * 0.065} />
1169+ <Small x={cx} y={cy + R * 1.2} text="×1 ×2 ×3 ×4 ×6 ×8 ×10 ×15" color={p.primary} size={R * 0.07} tracking="0.16em" opacity={0.7} />
1170+ </>
1171+ ) : null}
568 </g> 1172 </g>
569 ); 1173 );
570 } 1174 }
571 1175
572−function PerspectiveGrid({ W, H, p }: Ctx) { 1176+/* Fallback for unknown slugs: palette glow + the game's top symbols if known. */
573− const horizon = H * 0.42; 1177+function Generic(s: Stage) {
574− const lines: React.ReactNode[] = []; 1178+ const { W, H, cx, cy, R, id, p, compact } = s;
575− for (let i = -8; i <= 8; i++) { 1179+ const stars = scatterDots(s, 40, [0, 0, W, H], 0.5, 1.4);
576− lines.push(<line key={`v${i}`} x1={W * 0.5} y1={horizon} x2={r2(W * 0.5 + i * W * 0.28)} y2={H} stroke={p.primary} strokeOpacity={0.3} strokeWidth={1.2} />);
577− }
578− for (let k = 1; k <= 10; k++) {
579− const t = k / 10;
580− const y = horizon + (H - horizon) * t * t;
581− lines.push(<line key={`h${k}`} x1={0} y1={r2(y)} x2={W} y2={r2(y)} stroke={p.primary} strokeOpacity={r2(0.15 + t * 0.35)} strokeWidth={1.2} />);
582− }
583 return ( 1180 return (
584 <g> 1181 <g>
585− <circle cx={W * 0.5} cy={horizon} r={Math.min(W, H) * 0.2} fill={p.secondary} fillOpacity={0.25} /> 1182+ <defs>
586− <rect x={0} y={horizon} width={W} height={H - horizon} fill="#000" fillOpacity={0.35} /> 1183+ <Radial id={`${id}-gn1`} color={p.primary} o0={0.5} />
587− {lines} 1184+ </defs>
588− <line x1={0} y1={horizon} x2={W} y2={horizon} stroke={p.secondary} strokeOpacity={0.8} strokeWidth={2} /> 1185+ <path d={stars} fill="#fff" fillOpacity={0.6} />
1186+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.3)} fill={`url(#${id}-gn1)`} />
1187+ <circle cx={f(cx)} cy={f(cy)} r={f(R * 0.9)} fill="none" stroke={p.secondary} strokeOpacity={0.5} strokeWidth={2} />
1188+ <Sym s={s} id="W" x={cx} y={cy} size={R * 0.9} />
1189+ {!compact ? (
1190+ <>
1191+ <Sym s={s} id="P1" x={cx - R * 0.85} y={cy + R * 0.55} size={R * 0.5} />
1192+ <Sym s={s} id="S" x={cx + R * 0.85} y={cy - R * 0.55} size={R * 0.5} />
1193+ </>
1194+ ) : null}
589 </g> 1195 </g>
590 ); 1196 );
591 } 1197 }
592 1198
593−const MOTIFS: Record<Backdrop, (c: Ctx) => React.ReactNode> = { 1199+const SCENES: Record<string, (s: Stage) => React.ReactNode> = {
594− vault: Vault, 1200+ "neon-vault": NeonVault,
595− nebula: Nebula, 1201+ "cosmic-collapse": CosmicCollapse,
596− pillars: Pillars, 1202+ "golden-emperor": GoldenEmperor,
597− aurora: Aurora, 1203+ "diamond-heist": DiamondHeist,
598− lava: Lava, 1204+ "arctic-fortune": ArcticFortune,
599− circuit: Circuit, 1205+ "inferno-reels": InfernoReels,
600− skyline: Skyline, 1206+ "quantum-jackpot": QuantumJackpot,
601− pyramid: Pyramid, 1207+ "midnight-tokyo": MidnightTokyo,
602− arcade: Arcade, 1208+ "pharaoh-protocol": PharaohProtocol,
603− asteroids: Asteroids, 1209+ "lucky-circuit": LuckyCircuit,
604− track: Track, 1210+ "void-miner": VoidMiner,
605− reactor: Reactor, 1211+ "royal-circuit": RoyalCircuit,
606− abyss: Abyss, 1212+ "dragon-core": DragonCore,
607− moon: Moon, 1213+ "deep-treasure": DeepTreasure,
608− temple: Temple, 1214+ "moonbase-77": Moonbase77,
609− gravity: Gravity, 1215+ "wild-temple": WildTemple,
610− core: Core, 1216+ "zero-gravity": ZeroGravity,
611− minimal: Minimal, 1217+ "reel-reactor": ReelReactor,
612− universe: Universe, 1218+ obsidian: Obsidian,
613− grid: PerspectiveGrid, 1219+ "spinza-original": SpinzaOriginal,
614 }; 1220 };
615 1221
616−/* --------------------------------------------------------------- particles */ 1222+/* ------------------------------------------------------------------- title */
617 1223
618−function Particles({ W, H, rnd, p, kind }: Ctx & { kind: string }) { 1224+interface TitleSpec {
619− const out: React.ReactNode[] = []; 1225+ caps?: boolean;
620− const n = 22; 1226+ /** Letter spacing in em. */
621− for (let i = 0; i < n; i++) { 1227+ tracking?: number;
622− const x = r2(rnd() * W); 1228+ weight?: 700 | 800;
623− const y = r2(rnd() * H); 1229+ /** Solid fill colour or a [from, to] gradient. */
624− const op = r2(between(rnd, 0.25, 0.8)); 1230+ fill?: string | [string, string];
625− const s = between(rnd, 1.5, 4); 1231+ glow?: string;
626− switch (kind) { 1232+ eyebrow?: string;
627− case "snow": 1233+ eyebrowColor?: string;
628− out.push(<circle key={i} cx={x} cy={y} r={r2(s * 0.7)} fill="#fff" fillOpacity={op} />); 1234+ align?: "left" | "center";
629− break; 1235+ /** Force two lines on the card (long names). */
630− case "coins": 1236+ split?: boolean;
631− out.push(<ellipse key={i} cx={x} cy={y} rx={r2(s * 1.2)} ry={r2(s * 0.8)} fill={p.primary} fillOpacity={op} />); 1237+ /** Second line rendered small with wide tracking (e.g. PHARAOH / protocol). */
632− break; 1238+ subline?: boolean;
633− case "diamonds": 1239+ skew?: number;
634− out.push(<polygon key={i} points={`${x},${r2(y - s)} ${r2(x + s)},${y} ${x},${r2(y + s)} ${r2(x - s)},${y}`} fill={p.glow} fillOpacity={op} />); 1240+ /** Hard offset shadow colour (retro). */
635− break; 1241+ shadow?: string;
636− case "fire": 1242+ /** Carved outline. */
637− case "sparks": 1243+ outline?: string;
638− out.push(<line key={i} x1={x} y1={y} x2={r2(x + between(rnd, -6, 6))} y2={r2(y - between(rnd, 4, 12))} stroke={rnd() > 0.5 ? p.primary : p.secondary} strokeOpacity={op} strokeWidth={1.5} strokeLinecap="round" />); 1244+ /** Scale factor applied to the fitted size (Obsidian whispers). */
639− break; 1245+ scale?: number;
640− case "confetti":
641− out.push(<rect key={i} x={x} y={y} width={r2(s * 1.4)} height={r2(s * 0.7)} fill={[p.primary, p.secondary, p.glow][i % 3]} fillOpacity={op} transform={`rotate(${r2(rnd() * 90)} ${x} ${y})`} />);
642− break;
643− case "bubbles":
644− out.push(<circle key={i} cx={x} cy={y} r={r2(s)} fill="none" stroke="#fff" strokeOpacity={op * 0.6} />);
645− break;
646− case "energy":
647− out.push(<circle key={i} cx={x} cy={y} r={r2(s * 0.5)} fill={p.secondary} fillOpacity={op} />);
648− break;
649− case "dust":
650− out.push(<circle key={i} cx={x} cy={y} r={r2(s * 0.35)} fill={p.primary} fillOpacity={op * 0.7} />);
651− break;
652− default:
653− out.push(<circle key={i} cx={x} cy={y} r={r2(s * 0.4)} fill="#fff" fillOpacity={op} />);
654− }
655− }
656− return <g>{out}</g>;
657 } 1246 }
658 1247
659−/* ------------------------------------------------------------------- title */ 1248+const TITLES: Record<string, TitleSpec> = {
1249+ "neon-vault": { caps: true, tracking: -0.03, weight: 800, glow: "#00f0ff", eyebrow: "CRACK THE GRID", eyebrowColor: "#ff2bd6" },
1250+ "cosmic-collapse": { caps: false, tracking: -0.05, weight: 800, fill: ["#ffffff", "#c084fc"], glow: "#a78bfa", eyebrow: "EVENT HORIZON", eyebrowColor: "#67e8f9" },
1251+ "golden-emperor": { caps: true, tracking: 0.14, weight: 700, fill: ["#fff1c2", "#d4a72c"], glow: "#ffd166", align: "center", eyebrow: "IMPERIAL DYNASTY", eyebrowColor: "#e8b4b8", split: true },
1252+ "diamond-heist": { caps: true, tracking: -0.02, weight: 800, fill: ["#ffffff", "#bfefff"], glow: "#7dd3fc", eyebrow: "MINI · MINOR · MAJOR · GRAND", eyebrowColor: "#c0c0c0" },
1253+ "arctic-fortune": { caps: true, tracking: 0.02, weight: 700, fill: ["#ffffff", "#bfe9ff"], glow: "#7dd3fc", eyebrow: "AVALANCHE MULTIPLIER", eyebrowColor: "#a78bfa" },
1254+ "inferno-reels": { caps: true, tracking: -0.04, weight: 800, fill: ["#ffd166", "#ff4500"], glow: "#ff7a1a", eyebrow: "EXPLODING WILDS", eyebrowColor: "#ffb347" },
1255+ "quantum-jackpot": { caps: false, tracking: -0.04, weight: 800, fill: ["#ffffff", "#67e8f9"], glow: "#22d3ee", eyebrow: "ONE SYMBOL · SEVERAL STATES", eyebrowColor: "#c084fc" },
1256+ "midnight-tokyo": { caps: true, tracking: 0.06, weight: 800, fill: ["#ffffff", "#ff2bd6"], glow: "#ff2bd6", eyebrow: "SHIBUYA · 02:00", eyebrowColor: "#22d3ee" },
1257+ "pharaoh-protocol": { caps: true, tracking: 0.04, weight: 800, fill: ["#fff7d6", "#ffd166"], glow: "#fbbf24", subline: true, eyebrowColor: "#22d3ee" },
1258+ "lucky-circuit": { caps: true, tracking: 0.0, weight: 800, fill: "#facc15", shadow: "#f472b6", eyebrow: "INSERT COIN · WIN OFTEN", eyebrowColor: "#22d3ee" },
1259+ "void-miner": { caps: true, tracking: 0.1, weight: 700, fill: ["#e9d5ff", "#fbbf24"], glow: "#a78bfa", eyebrow: "DARK MATTER CORE", eyebrowColor: "#fbbf24" },
1260+ "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" },
1261+ "dragon-core": { caps: true, tracking: -0.03, weight: 800, fill: ["#ffd166", "#ef4444"], glow: "#f97316", eyebrow: "TWENTY SCALES", eyebrowColor: "#22d3ee" },
1262+ "deep-treasure": { caps: false, tracking: -0.04, weight: 800, fill: ["#dffaff", "#22d3ee"], glow: "#22d3ee", eyebrow: "DESCENT ×1 → ×10", eyebrowColor: "#fbbf24" },
1263+ "moonbase-77": { caps: true, tracking: 0.12, weight: 700, fill: "#fb923c", glow: "#fb923c", eyebrow: "LOW GRAVITY · WILDS DRIFT", eyebrowColor: "#22d3ee" },
1264+ "wild-temple": { caps: true, tracking: 0.05, weight: 800, fill: ["#fde68a", "#fbbf24"], outline: "#0a1a10", glow: "#34d399", eyebrow: "THE JAGUAR MOVES", eyebrowColor: "#34d399" },
1265+ "zero-gravity": { caps: true, tracking: 0.22, weight: 700, fill: ["#ffffff", "#a78bfa"], glow: "#22d3ee", eyebrow: "5×3 → 6×4 → 7×5", eyebrowColor: "#67e8f9" },
1266+ "reel-reactor": { caps: true, tracking: -0.03, weight: 800, fill: "#a3e635", glow: "#22c55e", eyebrow: "EVERY DEAD SPIN HEATS THE CORE", eyebrowColor: "#facc15" },
1267+ obsidian: { caps: true, tracking: 0.42, weight: 700, fill: "#f5e6c8", align: "center", scale: 0.62 },
1268+ "spinza-original": { caps: true, tracking: -0.02, weight: 800, fill: ["#ffffff", "#c4b5fd"], glow: "#8b5cf6", subline: true, eyebrowColor: "#22d3ee" },
1269+};
660 1270
661−function splitName(name: string, oneLine: boolean): string[] { 1271+function splitLines(name: string, spec: TitleSpec, v: ArtVariant): string[] {
662− if (oneLine) return [name];
663 const words = name.trim().split(/\s+/); 1272 const words = name.trim().split(/\s+/);
664− if (words.length <= 1) return words; 1273+ if (words.length < 2) return [name];
665− if (words.length === 2) return words; 1274+ if (spec.subline) return [words[0], words.slice(1).join(" ")];
1275+ const twoLines = v === "card" ? spec.split || name.length > 9 : v === "hero" || v === "banner" ? name.length > 14 : name.length > 11;
1276+ if (!twoLines) return [name];
666 const mid = Math.ceil(words.length / 2); 1277 const mid = Math.ceil(words.length / 2);
667 return [words.slice(0, mid).join(" "), words.slice(mid).join(" ")]; 1278 return [words.slice(0, mid).join(" "), words.slice(mid).join(" ")];
668 } 1279 }
669 1280
670−function Title({ name, W, H, pad, maxFont, oneLine, p }: { name: string; W: number; H: number; pad: number; maxFont: number; oneLine: boolean; p: ArtPalette }) { 1281+function estimateWidth(text: string, caps: boolean, tracking: number): number {
671− const lines = splitName(name.toUpperCase(), oneLine); 1282+ const cw = caps ? 0.7 : 0.6;
672− const longest = Math.max(...lines.map((l) => l.length), 1); 1283+ return text.length * (cw + tracking) - tracking;
673− const size = Math.min(maxFont, ((W - pad * 2) / longest) * 1.55); 1284+}
674− const lineH = size * 0.92; 1285+
675− const baseY = H - pad - (lines.length - 1) * lineH; 1286+function Title({ s, name, showName }: { s: Stage; name: string; showName: boolean }) {
676− return ( 1287+ if (!showName) return null;
677− <g fontFamily="var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif" fontWeight={700} style={{ letterSpacing: "-0.045em" }}> 1288+ const { W, H, v, id, p } = s;
678− {lines.map((l, i) => ( 1289+ const spec = TITLES[s.game?.slug ?? ""] ?? { caps: true, tracking: -0.03, weight: 800, glow: p.glow };
679− <text key={`s${i}`} x={pad} y={r2(baseY + i * lineH)} fontSize={r2(size)} fill={p.glow} fillOpacity={0.45} style={{ filter: "blur(10px)" }}> 1290+ const caps = spec.caps !== false;
1291+ const tracking = spec.tracking ?? -0.03;
1292+ const weight = spec.weight ?? 800;
1293+ const rawLines = splitLines(name, spec, v);
1294+ const lines = caps ? rawLines.map((l) => l.toUpperCase()) : rawLines;
1295+ 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 };
1296+ const fitLines = spec.subline ? [lines[0]] : lines;
1297+ const est = Math.max(...fitLines.map((l) => estimateWidth(l, caps, tracking)), 1);
1298+ let size = Math.min(cfg.maxFont, cfg.avail / est);
1299+ if (spec.scale) size *= spec.scale;
1300+ const subSize = size * 0.34;
1301+ const lineH = size * 0.94;
1302+ const totalH = spec.subline ? size + subSize * 1.9 : lineH * lines.length;
1303+ const center = spec.align === "center";
1304+ const x = center ? W / 2 : cfg.pad;
1305+ const anchor = center ? "middle" : "start";
1306+ const baseY = v === "card" || v === "tile" ? H - cfg.pad - (totalH - size) : H / 2 + size / 2 - (totalH - size) / 2;
1307+ const fill = Array.isArray(spec.fill) ? `url(#${id}-title)` : (spec.fill ?? "#fff");
1308+ const eyebrow = spec.eyebrow && cfg.eye > 0 && v !== "tile";
1309+ const eyeSize = v === "hero" ? size * 0.14 : v === "banner" ? size * 0.15 : Math.max(9.5, size * 0.2);
1310+ const fontStyle: React.CSSProperties = { fontFamily: FONT, letterSpacing: `${tracking}em` };
1311+ const transform = spec.skew ? `skewX(${spec.skew})` : undefined;
1312+
1313+ const render = (props: React.SVGProps<SVGTextElement>, key: string) =>
1314+ lines.map((l, i) => {
1315+ const isSub = spec.subline && i === 1;
1316+ const y = isSub ? baseY + subSize * 1.7 : baseY + i * lineH;
1317+ const base: React.CSSProperties = isSub ? { fontFamily: FONT, letterSpacing: "0.32em" } : fontStyle;
1318+ return (
1319+ <text key={`${key}${i}`} x={f(x)} y={f(y)} textAnchor={anchor} fontSize={f(isSub ? subSize : size)} fontWeight={isSub ? 700 : weight} transform={transform} {...props} style={{ ...base, ...props.style }}>
680 {l} 1320 {l}
681 </text> 1321 </text>
682− ))} 1322+ );
683− {lines.map((l, i) => ( 1323+ });
684− <text key={i} x={pad} y={r2(baseY + i * lineH)} fontSize={r2(size)} fill="#ffffff"> 1324+
685− {l} 1325+ return (
1326+ <g>
1327+ {Array.isArray(spec.fill) ? (
1328+ <defs>
1329+ <Linear id={`${id}-title`} stops={[[0, spec.fill[0]], [1, spec.fill[1]]]} dir="v" />
1330+ </defs>
1331+ ) : null}
1332+ {eyebrow ? (
1333+ <text x={f(x)} y={f(baseY - size * 1.02)} textAnchor={anchor} fontSize={f(eyeSize)} fontWeight={700} fill={spec.eyebrowColor ?? p.secondary} style={{ fontFamily: FONT, letterSpacing: "0.22em" }}>
1334+ {spec.eyebrow}
686 </text> 1335 </text>
687− ))} 1336+ ) : null}
1337+ {spec.glow && v !== "tile" ? render({ fill: spec.glow, fillOpacity: 0.5, style: { filter: `blur(${f(size * 0.16)}px)` } }, "g") : null}
1338+ {spec.shadow ? render({ fill: spec.shadow, transform: `translate(${f(size * 0.07)} ${f(size * 0.07)})` }, "s") : null}
1339+ {spec.outline ? render({ fill: "none", stroke: spec.outline, strokeWidth: f(size * 0.09), strokeLinejoin: "round" }, "o") : null}
1340+ {render({ fill }, "t")}
688 </g> 1341 </g>
689 ); 1342 );
690 } 1343 }
691 1344
692 /* ----------------------------------------------------------------- GameArt */ 1345 /* ----------------------------------------------------------------- GameArt */
693 1346
694−export function GameArt({ slug, name, palette, backdrop, particles, variant = "card", className, showName = true, label }: GameArtProps) { 1347+export function GameArt({ slug, name, palette, variant = "card", className, showName = true, label }: GameArtProps) {
695 const reactId = React.useId().replace(/[^a-zA-Z0-9]/g, ""); 1348 const reactId = React.useId().replace(/[^a-zA-Z0-9]/g, "");
696 const id = `ga${reactId}`; 1349 const id = `ga${reactId}`;
697− const preset = GAME_PRESENTATION[slug]; 1350+ const game = GAMES_BY_SLUG.get(slug);
698− const bd = (backdrop ?? preset?.backdrop ?? BACKDROPS[hashString(slug) % BACKDROPS.length]) as string; 1351+ const { w: W, h: H } = DIMS[variant];
699− const motifKey: Backdrop = (BACKDROPS as readonly string[]).includes(bd) ? (bd as Backdrop) : "grid"; 1352+ const showTitle = showName;
700− const pt = particles ?? preset?.particles ?? "dust"; 1353+ const { cx, cy, R } = focal(variant, W, H, showTitle && variant === "card");
701− const { w: W, h: H, pad, maxFont, oneLine } = DIMS[variant];
702 const rnd = mulberry32(hashString(`${slug}:${variant}`)); 1354 const rnd = mulberry32(hashString(`${slug}:${variant}`));
703− const ctx: Ctx = { W, H, rnd, p: palette, id }; 1355+ const fallback: SymbolStyle = { shape: "gem", color: palette.primary, accent: palette.glow, glow: 0.8 };
704− const motif = MOTIFS[motifKey]; 1356+ const bySymbol = new Map((game?.symbols ?? []).map((x) => [x.id, x]));
1357+ const stage: Stage = {
1358+ W,
1359+ H,
1360+ v: variant,
1361+ cx,
1362+ cy,
1363+ R,
1364+ p: palette,
1365+ id,
1366+ rnd,
1367+ game,
1368+ sym: (sid) => bySymbol.get(sid)?.style ?? fallback,
1369+ tier: (sid) => bySymbol.get(sid)?.tier ?? "premium",
1370+ compact: variant === "tile",
1371+ };
1372+ const scene = SCENES[slug] ?? Generic;
1373+ const bgBottom = slug === "obsidian" ? "#000" : "#04050a";
705 1374
706 return ( 1375 return (
707 <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid slice" className={cn("block h-full w-full", className)} role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true}> 1376 <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid slice" className={cn("block h-full w-full", className)} role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true}>
708 <defs> 1377 <defs>
709− <linearGradient id={`${id}-bg`} x1="0" y1="0" x2="0.4" y2="1"> 1378+ <linearGradient id={`${id}-bg`} x1="0" y1="0" x2="0.3" y2="1">
710 <stop offset="0" stopColor={palette.bg} /> 1379 <stop offset="0" stopColor={palette.bg} />
711− <stop offset="1" stopColor="#05060a" /> 1380+ <stop offset="1" stopColor={bgBottom} />
712− </linearGradient>
713− <radialGradient id={`${id}-glow`} cx="0.5" cy="0.5" r="0.5">
714− <stop offset="0" stopColor={palette.glow} stopOpacity="0.55" />
715− <stop offset="0.5" stopColor={palette.primary} stopOpacity="0.18" />
716− <stop offset="1" stopColor={palette.primary} stopOpacity="0" />
717− </radialGradient>
718− <linearGradient id={`${id}-vignette`} x1="0" y1="0" x2="0" y2="1">
719− <stop offset="0.35" stopColor={palette.bg} stopOpacity="0" />
720− <stop offset="1" stopColor="#03040a" stopOpacity="0.92" />
721 </linearGradient> 1381 </linearGradient>
722− <linearGradient id={`${id}-col`} x1="0" y1="0" x2="1" y2="0"> 1382+ <linearGradient id={`${id}-vig`} x1="0" y1="0" x2="0" y2="1">
723− <stop offset="0" stopColor="#1c1410" /> 1383+ <stop offset="0.45" stopColor="#000" stopOpacity="0" />
724− <stop offset="0.5" stopColor="#2a1d14" /> 1384+ <stop offset="1" stopColor="#020308" stopOpacity={variant === "card" ? 0.88 : 0.7} />
725− <stop offset="1" stopColor="#120c08" />
726 </linearGradient> 1385 </linearGradient>
727− <linearGradient id={`${id}-heat`} x1="0" y1="0" x2="0" y2="1">
728− <stop offset="0" stopColor={palette.primary} stopOpacity="0" />
729− <stop offset="1" stopColor={palette.primary} stopOpacity="0.45" />
730− </linearGradient>
731− <linearGradient id={`${id}-ray`} x1="0" y1="0" x2="0" y2="1">
732− <stop offset="0" stopColor={palette.primary} stopOpacity="0.16" />
733− <stop offset="1" stopColor={palette.primary} stopOpacity="0" />
734− </linearGradient>
735− <radialGradient id={`${id}-moon`} cx="0.35" cy="0.3" r="0.8">
736− <stop offset="0" stopColor="#3a3d4a" />
737− <stop offset="1" stopColor="#0d0e14" />
738− </radialGradient>
739− <radialGradient id={`${id}-sphere`} cx="0.4" cy="0.35" r="0.7">
740− <stop offset="0" stopColor={palette.glow} />
741− <stop offset="0.55" stopColor={palette.primary} />
742− <stop offset="1" stopColor={palette.bg} />
743− </radialGradient>
744− <linearGradient id={`${id}-ring`} x1="0" y1="0" x2="1" y2="1">
745− <stop offset="0" stopColor={palette.primary} />
746− <stop offset="1" stopColor={palette.secondary} />
747− </linearGradient>
748− <filter id={`${id}-blur`} x="-30%" y="-30%" width="160%" height="160%">
749− <feGaussianBlur stdDeviation={Math.max(W, H) / 60} />
750− </filter>
751 </defs> 1386 </defs>
752 <rect width={W} height={H} fill={`url(#${id}-bg)`} /> 1387 <rect width={W} height={H} fill={`url(#${id}-bg)`} />
753− {/* Called as plain functions (not components) so the seeded PRNG sequence is consumed exactly once per render — keeps SSR and client output identical even under StrictMode double-rendering. */} 1388+ {/* Scenes are called as plain functions so the seeded PRNG is consumed exactly once per render (SSR == client, StrictMode-safe). */}
754− {motif(ctx)} 1389+ {scene(stage)}
755− {Particles({ ...ctx, kind: pt })} 1390+ {showTitle && variant !== "tile" ? <rect width={W} height={H} fill={`url(#${id}-vig)`} /> : null}
756− <rect width={W} height={H} fill={`url(#${id}-vignette)`} /> 1391+ <rect x="0.5" y="0.5" width={W - 1} height={H - 1} fill="none" stroke="#fff" strokeOpacity="0.07" />
757− <rect x="0.5" y="0.5" width={W - 1} height={H - 1} fill="none" stroke="#fff" strokeOpacity="0.06" /> 1392+ <Title s={stage} name={name} showName={showTitle} />
758− {showName ? <Title name={name} W={W} H={H} pad={pad} maxFont={maxFont} oneLine={oneLine} p={palette} /> : null}
759 </svg> 1393 </svg>
760 ); 1394 );
761 } 1395 }
modified games/src/arctic-fortune/index.ts +3 −3
@@ -23,14 +23,14 @@ export const arcticFortune = defineGame({
23 wildSym("W", "Glacier Wild", [0, 1.4, 1.4, 1.4, 0], { shape: "snow", color: "#e0f7ff", accent: "#7dd3fc", glow: 1 }, { freeSpinWeight: [0, 0.7, 0.7, 0.7, 0] }), 23 wildSym("W", "Glacier Wild", [0, 1.4, 1.4, 1.4, 0], { shape: "snow", color: "#e0f7ff", accent: "#7dd3fc", glow: 1 }, { freeSpinWeight: [0, 0.7, 0.7, 0.7, 0] }),
24 scatterSym("S", "Northern Compass", 1.3, { shape: "star", color: "#a5f3fc", accent: "#ffffff", glow: 0.9 }), 24 scatterSym("S", "Northern Compass", 1.3, { shape: "star", color: "#a5f3fc", accent: "#ffffff", glow: 0.9 }),
25 sym("P1", "Frozen Relic", "premium", { 3: 3, 4: 9, 5: 30 }, 2.8, { shape: "gem", color: "#7dd3fc", accent: "#f0fbff", glow: 0.9 }), 25 sym("P1", "Frozen Relic", "premium", { 3: 3, 4: 9, 5: 30 }, 2.8, { shape: "gem", color: "#7dd3fc", accent: "#f0fbff", glow: 0.9 }),
26− sym("H1", "Snow Owl", "high", { 3: 2, 4: 5, 5: 15 }, 3.8, { shape: "eye", color: "#e2e8f0", accent: "#ffffff", glow: 0.6 }), 26+ sym("H1", "Snow Owl", "high", { 3: 2, 4: 5, 5: 15 }, 3.8, { shape: "paw", color: "#e2e8f0", accent: "#ffffff", glow: 0.6 }),
27− sym("H2", "Ice Axe", "high", { 3: 1.5, 4: 4, 5: 12 }, 4.2, { shape: "bolt", color: "#94a3b8", accent: "#e2e8f0", glow: 0.5 }), 27+ sym("H2", "Ice Axe", "high", { 3: 1.5, 4: 4, 5: 12 }, 4.2, { shape: "fish", color: "#94a3b8", accent: "#e2e8f0", glow: 0.5 }),
28 sym("M1", "Expedition Lantern", "mid", { 3: 1, 4: 2.5, 5: 7 }, 5.5, { shape: "circle", color: "#fbbf24", accent: "#fef3c7", glow: 0.3 }), 28 sym("M1", "Expedition Lantern", "mid", { 3: 1, 4: 2.5, 5: 7 }, 5.5, { shape: "circle", color: "#fbbf24", accent: "#fef3c7", glow: 0.3 }),
29 sym("M2", "Compass Rose", "mid", { 3: 0.8, 4: 2, 5: 5 }, 6, { shape: "ring", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }), 29 sym("M2", "Compass Rose", "mid", { 3: 0.8, 4: 2, 5: 5 }, 6, { shape: "ring", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }),
30 sym("L1", "Blue Shard", "low", { 3: 0.4, 4: 1, 5: 2.5 }, 6.5, { shape: "crystal", color: "#60a5fa", glow: 0.2 }), 30 sym("L1", "Blue Shard", "low", { 3: 0.4, 4: 1, 5: 2.5 }, 6.5, { shape: "crystal", color: "#60a5fa", glow: 0.2 }),
31 sym("L2", "Teal Shard", "low", { 3: 0.4, 4: 1, 5: 2.5 }, 6.5, { shape: "diamond", color: "#2dd4bf", glow: 0.2 }), 31 sym("L2", "Teal Shard", "low", { 3: 0.4, 4: 1, 5: 2.5 }, 6.5, { shape: "diamond", color: "#2dd4bf", glow: 0.2 }),
32 sym("L3", "Violet Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "hex", color: "#a78bfa", glow: 0.2 }), 32 sym("L3", "Violet Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "hex", color: "#a78bfa", glow: 0.2 }),
33− sym("L4", "Frost Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "cube", color: "#cbd5e1", glow: 0.2 }), 33+ sym("L4", "Frost Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "igloo", color: "#cbd5e1", glow: 0.2 }),
34 sym("L5", "Amber Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "star", color: "#fbbf24", glow: 0.2 }), 34 sym("L5", "Amber Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "star", color: "#fbbf24", glow: 0.2 }),
35 sym("L6", "Rose Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "gem", color: "#fb7185", glow: 0.2 }), 35 sym("L6", "Rose Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "gem", color: "#fb7185", glow: 0.2 }),
36 ], 36 ],
modified games/src/cosmic-collapse/index.ts +3 −3
@@ -25,9 +25,9 @@ export const cosmicCollapse = defineGame({
25 sym("P1", "Quasar", "premium", { 3: 3, 4: 8, 5: 25, 6: 80 }, 2.6, { shape: "ring", color: "#fde68a", accent: "#fff7d6", glow: 0.9 }), 25 sym("P1", "Quasar", "premium", { 3: 3, 4: 8, 5: 25, 6: 80 }, 2.6, { shape: "ring", color: "#fde68a", accent: "#fff7d6", glow: 0.9 }),
26 sym("H1", "Pulsar", "high", { 3: 2, 4: 5, 5: 15, 6: 40 }, 3.6, { shape: "atom", color: "#67e8f9", accent: "#ecfeff", glow: 0.6 }), 26 sym("H1", "Pulsar", "high", { 3: 2, 4: 5, 5: 15, 6: 40 }, 3.6, { shape: "atom", color: "#67e8f9", accent: "#ecfeff", glow: 0.6 }),
27 sym("H2", "Nebula", "high", { 3: 1.5, 4: 4, 5: 10, 6: 30 }, 4, { shape: "wave", color: "#c084fc", accent: "#f3e8ff", glow: 0.6 }), 27 sym("H2", "Nebula", "high", { 3: 1.5, 4: 4, 5: 10, 6: 30 }, 4, { shape: "wave", color: "#c084fc", accent: "#f3e8ff", glow: 0.6 }),
28− sym("M1", "Comet", "mid", { 3: 1, 4: 2.5, 5: 6, 6: 15 }, 5.5, { shape: "bolt", color: "#fb7185", accent: "#ffe4e6", glow: 0.3 }), 28+ sym("M1", "Comet", "mid", { 3: 1, 4: 2.5, 5: 6, 6: 15 }, 5.5, { shape: "comet", color: "#fb7185", accent: "#ffe4e6", glow: 0.3 }),
29− sym("M2", "Asteroid", "mid", { 3: 0.8, 4: 2, 5: 5, 6: 12 }, 6, { shape: "cube", color: "#a3a3a3", accent: "#e5e5e5", glow: 0.2 }), 29+ sym("M2", "Asteroid", "mid", { 3: 0.8, 4: 2, 5: 5, 6: 12 }, 6, { shape: "meteor", color: "#a3a3a3", accent: "#e5e5e5", glow: 0.2 }),
30− sym("L1", "Blue Dwarf", "low", { 3: 0.4, 4: 1, 5: 2.5, 6: 6 }, 8.5, { shape: "circle", color: "#60a5fa", glow: 0.2 }), 30+ sym("L1", "Blue Dwarf", "low", { 3: 0.4, 4: 1, 5: 2.5, 6: 6 }, 8.5, { shape: "planet", color: "#60a5fa", glow: 0.2 }),
31 sym("L2", "Red Dwarf", "low", { 3: 0.4, 4: 1, 5: 2.5, 6: 6 }, 8.5, { shape: "circle", color: "#f87171", glow: 0.2 }), 31 sym("L2", "Red Dwarf", "low", { 3: 0.4, 4: 1, 5: 2.5, 6: 6 }, 8.5, { shape: "circle", color: "#f87171", glow: 0.2 }),
32 sym("L3", "Ion Shard", "low", { 3: 0.3, 4: 0.8, 5: 2, 6: 5 }, 9.5, { shape: "crystal", color: "#34d399", glow: 0.2 }), 32 sym("L3", "Ion Shard", "low", { 3: 0.3, 4: 0.8, 5: 2, 6: 5 }, 9.5, { shape: "crystal", color: "#34d399", glow: 0.2 }),
33 sym("L4", "Dust Mote", "low", { 3: 0.3, 4: 0.8, 5: 2, 6: 5 }, 9.5, { shape: "diamond", color: "#fbbf24", glow: 0.2 }), 33 sym("L4", "Dust Mote", "low", { 3: 0.3, 4: 0.8, 5: 2, 6: 5 }, 9.5, { shape: "diamond", color: "#fbbf24", glow: 0.2 }),
modified games/src/deep-treasure/index.ts +4 −4
@@ -25,14 +25,14 @@ export const deepTreasure = defineGame({
25 sym("P1", "Sunken Crown", "premium", { 3: 9, 4: 24, 5: 75, 6: 240 }, 2.6, { shape: "crown", color: "#fbbf24", accent: "#fff7d6", glow: 0.9 }), 25 sym("P1", "Sunken Crown", "premium", { 3: 9, 4: 24, 5: 75, 6: 240 }, 2.6, { shape: "crown", color: "#fbbf24", accent: "#fff7d6", glow: 0.9 }),
26 sym("H1", "Golden Idol", "high", { 3: 6, 4: 15, 5: 45, 6: 120 }, 3.6, { shape: "coin", color: "#f59e0b", accent: "#fef3c7", glow: 0.6 }), 26 sym("H1", "Golden Idol", "high", { 3: 6, 4: 15, 5: 45, 6: 120 }, 3.6, { shape: "coin", color: "#f59e0b", accent: "#fef3c7", glow: 0.6 }),
27 sym("H2", "Black Pearl", "high", { 3: 4.5, 4: 12, 5: 30, 6: 90 }, 4, { shape: "orb", color: "#312e81", accent: "#e0e7ff", glow: 0.6 }), 27 sym("H2", "Black Pearl", "high", { 3: 4.5, 4: 12, 5: 30, 6: 90 }, 4, { shape: "orb", color: "#312e81", accent: "#e0e7ff", glow: 0.6 }),
28− sym("M1", "Trident", "mid", { 3: 3, 4: 7.5, 5: 18, 6: 45 }, 5.5, { shape: "bolt", color: "#22d3ee", accent: "#cffafe", glow: 0.3 }), 28+ sym("M1", "Trident", "mid", { 3: 3, 4: 7.5, 5: 18, 6: 45 }, 5.5, { shape: "trident", color: "#22d3ee", accent: "#cffafe", glow: 0.3 }),
29− sym("M2", "Ship Anchor", "mid", { 3: 2.4, 4: 6, 5: 15, 6: 36 }, 6, { shape: "hex", color: "#64748b", accent: "#cbd5e1", glow: 0.2 }), 29+ sym("M2", "Ship Anchor", "mid", { 3: 2.4, 4: 6, 5: 15, 6: 36 }, 6, { shape: "shell", color: "#64748b", accent: "#cbd5e1", glow: 0.2 }),
30 sym("L1", "Coral", "low", { 3: 1.2, 4: 3, 5: 7.5, 6: 18 }, 6, { shape: "crystal", color: "#fb7185", glow: 0.2 }), 30 sym("L1", "Coral", "low", { 3: 1.2, 4: 3, 5: 7.5, 6: 18 }, 6, { shape: "crystal", color: "#fb7185", glow: 0.2 }),
31 sym("L2", "Sea Glass", "low", { 3: 1.2, 4: 3, 5: 7.5, 6: 18 }, 6, { shape: "gem", color: "#2dd4bf", glow: 0.2 }), 31 sym("L2", "Sea Glass", "low", { 3: 1.2, 4: 3, 5: 7.5, 6: 18 }, 6, { shape: "gem", color: "#2dd4bf", glow: 0.2 }),
32 sym("L3", "Nautilus Shell", "low", { 3: 0.9, 4: 2.4, 5: 6, 6: 15 }, 6, { shape: "circle", color: "#fcd34d", glow: 0.2 }), 32 sym("L3", "Nautilus Shell", "low", { 3: 0.9, 4: 2.4, 5: 6, 6: 15 }, 6, { shape: "circle", color: "#fcd34d", glow: 0.2 }),
33− sym("L4", "Sand Dollar", "low", { 3: 0.9, 4: 2.4, 5: 6, 6: 15 }, 6, { shape: "ring", color: "#d6d3d1", glow: 0.2 }), 33+ sym("L4", "Sand Dollar", "low", { 3: 0.9, 4: 2.4, 5: 6, 6: 15 }, 6, { shape: "anchor", color: "#d6d3d1", glow: 0.2 }),
34 sym("L5", "Sea Urchin", "low", { 3: 0.9, 4: 2.4, 5: 6, 6: 15 }, 6, { shape: "star", color: "#a78bfa", glow: 0.2 }), 34 sym("L5", "Sea Urchin", "low", { 3: 0.9, 4: 2.4, 5: 6, 6: 15 }, 6, { shape: "star", color: "#a78bfa", glow: 0.2 }),
35− sym("L6", "Drift Stone", "low", { 3: 0.9, 4: 2.4, 5: 6, 6: 15 }, 6, { shape: "cube", color: "#78716c", accent: "#a8a29e", glow: 0.1 }), 35+ sym("L6", "Drift Stone", "low", { 3: 0.9, 4: 2.4, 5: 6, 6: 15 }, 6, { shape: "chest", color: "#78716c", accent: "#a8a29e", glow: 0.1 }),
36 ], 36 ],
37 betDivisor: 50, 37 betDivisor: 50,
38 wild: { id: "W" }, 38 wild: { id: "W" },
modified games/src/diamond-heist/index.ts +10 −10
@@ -20,18 +20,18 @@ export const diamondHeist = defineGame({
20 volatility: "high", 20 volatility: "high",
21 maxMultiplier: 5000, 21 maxMultiplier: 5000,
22 symbols: [ 22 symbols: [
23− wildSym("W", "Skeleton Key", [0, 2, 2, 2, 0], { shape: "bolt", color: "#e5e7eb", accent: "#7dd3fc", glow: 1 }), 23+ wildSym("W", "Skeleton Key", [0, 2, 2, 2, 0], { shape: "key", color: "#e5e7eb", accent: "#7dd3fc", glow: 1 }),
24− scatterSym("S", "Blueprint", 1.4, { shape: "chip", color: "#38bdf8", accent: "#ffffff", label: "PLN", glow: 0.8 }), 24+ scatterSym("S", "Blueprint", 1.4, { shape: "scroll", color: "#38bdf8", accent: "#e0f2fe", glow: 0.8 }),
25 sym("D", "Diamond", "special", null, 8.5, { shape: "diamond", color: "#bfefff", accent: "#ffffff", glow: 1 }, { kind: "coin" }), 25 sym("D", "Diamond", "special", null, 8.5, { shape: "diamond", color: "#bfefff", accent: "#ffffff", glow: 1 }, { kind: "coin" }),
26− sym("P1", "The Mastermind", "premium", { 3: 100, 4: 375, 5: 1250 }, 2.6, { shape: "eye", color: "#c0c0c0", accent: "#ffffff", glow: 0.9 }), 26+ sym("P1", "The Mastermind", "premium", { 3: 100, 4: 375, 5: 1250 }, 2.6, { shape: "mask", color: "#c0c0c0", accent: "#ffffff", glow: 0.9 }),
27− sym("H1", "Laser Cutter", "high", { 3: 50, 4: 150, 5: 500 }, 3.8, { shape: "flame", color: "#7dd3fc", accent: "#e0f2fe", glow: 0.6 }), 27+ sym("H1", "Laser Cutter", "high", { 3: 50, 4: 150, 5: 500 }, 3.8, { shape: "drill", color: "#7dd3fc", accent: "#e0f2fe", glow: 0.6 }),
28 sym("H2", "Penthouse Vault", "high", { 3: 38, 4: 125, 5: 375 }, 4.2, { shape: "vault", color: "#9ca3af", accent: "#f3f4f6", glow: 0.6 }), 28 sym("H2", "Penthouse Vault", "high", { 3: 38, 4: 125, 5: 375 }, 4.2, { shape: "vault", color: "#9ca3af", accent: "#f3f4f6", glow: 0.6 }),
29− sym("M1", "Night Goggles", "mid", { 3: 25, 4: 75, 5: 250 }, 5.5, { shape: "ring", color: "#4ade80", accent: "#dcfce7", glow: 0.3 }), 29+ sym("M1", "Night Goggles", "mid", { 3: 25, 4: 75, 5: 250 }, 5.5, { shape: "glove", color: "#4ade80", accent: "#dcfce7", glow: 0.3 }),
30− sym("M2", "Signal Jammer", "mid", { 3: 20, 4: 62, 5: 200 }, 6, { shape: "cube", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }), 30+ sym("M2", "Signal Jammer", "mid", { 3: 20, 4: 62, 5: 200 }, 6, { shape: "satellite", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }),
31− sym("L1", "Code A", "low", { 3: 12, 4: 38, 5: 125 }, 9, { shape: "letter", color: "#cbd5e1", label: "A" }), 31+ sym("L1", "Loot Bag", "low", { 3: 12, 4: 38, 5: 125 }, 9, { shape: "bag", color: "#cbd5e1", accent: "#7dd3fc" }),
32− sym("L2", "Code K", "low", { 3: 12, 4: 38, 5: 125 }, 9, { shape: "letter", color: "#94a3b8", label: "K" }), 32+ sym("L2", "Climbing Rope", "low", { 3: 12, 4: 38, 5: 125 }, 9, { shape: "rope", color: "#94a3b8", accent: "#e2e8f0" }),
33− sym("L3", "Code Q", "low", { 3: 10, 4: 30, 5: 100 }, 10, { shape: "letter", color: "#67e8f9", label: "Q" }), 33+ sym("L3", "Padlock", "low", { 3: 10, 4: 30, 5: 100 }, 10, { shape: "lock", color: "#67e8f9", accent: "#ffffff" }),
34− sym("L4", "Code J", "low", { 3: 10, 4: 30, 5: 100 }, 10, { shape: "letter", color: "#a5b4fc", label: "J" }), 34+ sym("L4", "Keycode", "low", { 3: 10, 4: 30, 5: 100 }, 10, { shape: "code", color: "#a5b4fc", label: "****" }),
35 ], 35 ],
36 betDivisor: 20, 36 betDivisor: 20,
37 wild: { id: "W" }, 37 wild: { id: "W" },
modified games/src/dragon-core/index.ts +1 −1
@@ -25,7 +25,7 @@ export const dragonCore = defineGame({
25 sym("DC", "Dragon Scale", "special", null, 1.2, { shape: "shield", color: "#b91c1c", accent: "#ffd166", glow: 0.8 }, { kind: "charge" }), 25 sym("DC", "Dragon Scale", "special", null, 1.2, { shape: "shield", color: "#b91c1c", accent: "#ffd166", glow: 0.8 }, { kind: "charge" }),
26 sym("P1", "Dragon Heart", "premium", { 3: 5, 4: 14, 5: 48 }, 2.8, { shape: "gem", color: "#ef4444", accent: "#fff1c2", glow: 0.9 }), 26 sym("P1", "Dragon Heart", "premium", { 3: 5, 4: 14, 5: 48 }, 2.8, { shape: "gem", color: "#ef4444", accent: "#fff1c2", glow: 0.9 }),
27 sym("H1", "Dragon Eye", "high", { 3: 3.2, 4: 8, 5: 24 }, 3.8, { shape: "eye", color: "#f59e0b", accent: "#fef3c7", glow: 0.6 }), 27 sym("H1", "Dragon Eye", "high", { 3: 3.2, 4: 8, 5: 24 }, 3.8, { shape: "eye", color: "#f59e0b", accent: "#fef3c7", glow: 0.6 }),
28− sym("H2", "Claw Talon", "high", { 3: 2.4, 4: 6.5, 5: 19 }, 4.2, { shape: "bolt", color: "#e5e7eb", accent: "#ffffff", glow: 0.6 }), 28+ sym("H2", "Claw Talon", "high", { 3: 2.4, 4: 6.5, 5: 19 }, 4.2, { shape: "gauge", color: "#e5e7eb", accent: "#ffffff", glow: 0.6 }),
29 sym("M1", "Ember Crystal", "mid", { 3: 1.6, 4: 4, 5: 11 }, 5.5, { shape: "crystal", color: "#fb923c", accent: "#ffedd5", glow: 0.3 }), 29 sym("M1", "Ember Crystal", "mid", { 3: 1.6, 4: 4, 5: 11 }, 5.5, { shape: "crystal", color: "#fb923c", accent: "#ffedd5", glow: 0.3 }),
30 sym("M2", "Rune Stone", "mid", { 3: 1.3, 4: 3.2, 5: 8 }, 6, { shape: "hex", color: "#6b7280", accent: "#22d3ee", glow: 0.3 }), 30 sym("M2", "Rune Stone", "mid", { 3: 1.3, 4: 3.2, 5: 8 }, 6, { shape: "hex", color: "#6b7280", accent: "#22d3ee", glow: 0.3 }),
31 sym("L1", "Ruby Shard", "low", { 3: 0.65, 4: 1.6, 5: 4 }, 6.5, { shape: "diamond", color: "#dc2626", glow: 0.2 }), 31 sym("L1", "Ruby Shard", "low", { 3: 0.65, 4: 1.6, 5: 4 }, 6.5, { shape: "diamond", color: "#dc2626", glow: 0.2 }),
modified games/src/golden-emperor/index.ts +7 −7
@@ -24,13 +24,13 @@ export const goldenEmperor = defineGame({
24 scatterSym("S", "Imperial Seal", 1.4, { shape: "shield", color: "#c8102e", accent: "#ffd166", glow: 0.9 }), 24 scatterSym("S", "Imperial Seal", 1.4, { shape: "shield", color: "#c8102e", accent: "#ffd166", glow: 0.9 }),
25 sym("P1", "Golden Emperor", "premium", { 3: 80, 4: 300, 5: 1000 }, 2.6, { shape: "crown", color: "#ffcc33", accent: "#fff5cc", glow: 0.9 }), 25 sym("P1", "Golden Emperor", "premium", { 3: 80, 4: 300, 5: 1000 }, 2.6, { shape: "crown", color: "#ffcc33", accent: "#fff5cc", glow: 0.9 }),
26 sym("H1", "Jade Empress", "high", { 3: 40, 4: 120, 5: 400 }, 3.8, { shape: "gem", color: "#2fb37a", accent: "#c8f5df", glow: 0.6 }), 26 sym("H1", "Jade Empress", "high", { 3: 40, 4: 120, 5: 400 }, 3.8, { shape: "gem", color: "#2fb37a", accent: "#c8f5df", glow: 0.6 }),
27− sym("H2", "Phoenix Fan", "high", { 3: 30, 4: 100, 5: 300 }, 4.2, { shape: "flame", color: "#ff7a1a", accent: "#ffd8a8", glow: 0.6 }), 27+ sym("H2", "Phoenix Fan", "high", { 3: 30, 4: 100, 5: 300 }, 4.2, { shape: "fan", color: "#ff7a1a", accent: "#ffd8a8", glow: 0.6 }),
28− sym("M1", "Red Lantern", "mid", { 3: 20, 4: 60, 5: 200 }, 5.5, { shape: "circle", color: "#e02424", accent: "#ffb3b3", glow: 0.3 }), 28+ sym("M1", "Red Lantern", "mid", { 3: 20, 4: 60, 5: 200 }, 5.5, { shape: "lantern", color: "#e02424", accent: "#ffb3b3", glow: 0.3 }),
29− sym("M2", "Gold Ingot", "mid", { 3: 16, 4: 50, 5: 160 }, 6, { shape: "coin", color: "#f5b942", accent: "#fff1c2", glow: 0.3 }), 29+ sym("M2", "Gold Ingot", "mid", { 3: 16, 4: 50, 5: 160 }, 6, { shape: "ingot", color: "#f5b942", accent: "#fff1c2", glow: 0.3 }),
30− sym("L1", "Dynasty A", "low", { 3: 10, 4: 30, 5: 100 }, 9, { shape: "letter", color: "#d9a441", label: "A" }), 30+ sym("L1", "Imperial Scroll", "low", { 3: 10, 4: 30, 5: 100 }, 9, { shape: "scroll", color: "#d9a441", accent: "#f5e6c8" }),
31− sym("L2", "Dynasty K", "low", { 3: 10, 4: 30, 5: 100 }, 9, { shape: "letter", color: "#b3413a", label: "K" }), 31+ sym("L2", "Lucky Coin", "low", { 3: 10, 4: 30, 5: 100 }, 9, { shape: "coin", color: "#b3413a", accent: "#ffd166", label: "福" }),
32− sym("L3", "Dynasty Q", "low", { 3: 8, 4: 24, 5: 80 }, 10, { shape: "letter", color: "#3f8f6b", label: "Q" }), 32+ sym("L3", "Jade Fish", "low", { 3: 8, 4: 24, 5: 80 }, 10, { shape: "fish", color: "#3f8f6b", accent: "#c8f5df" }),
33− sym("L4", "Dynasty J", "low", { 3: 8, 4: 24, 5: 80 }, 10, { shape: "letter", color: "#8a5cc7", label: "J" }), 33+ sym("L4", "Paper Fan", "low", { 3: 8, 4: 24, 5: 80 }, 10, { shape: "fan", color: "#8a5cc7", accent: "#e9d5ff" }),
34 ], 34 ],
35 betDivisor: 20, 35 betDivisor: 20,
36 wild: { 36 wild: {
modified games/src/inferno-reels/index.ts +3 −3
@@ -23,13 +23,13 @@ export const infernoReels = defineGame({
23 wildSym("W", "Magma Core", [0, 1.4, 1.4, 1.4, 0], { shape: "orb", color: "#ff4500", accent: "#ffd166", glow: 1 }), 23 wildSym("W", "Magma Core", [0, 1.4, 1.4, 1.4, 0], { shape: "orb", color: "#ff4500", accent: "#ffd166", glow: 1 }),
24 scatterSym("S", "Eruption", 1.4, { shape: "flame", color: "#ffb347", accent: "#ffffff", glow: 0.9 }), 24 scatterSym("S", "Eruption", 1.4, { shape: "flame", color: "#ffb347", accent: "#ffffff", glow: 0.9 }),
25 sym("P1", "Fire Titan", "premium", { 3: 13, 4: 40, 5: 135 }, 2.8, { shape: "skull", color: "#ff3b3b", accent: "#ffd166", glow: 0.9 }), 25 sym("P1", "Fire Titan", "premium", { 3: 13, 4: 40, 5: 135 }, 2.8, { shape: "skull", color: "#ff3b3b", accent: "#ffd166", glow: 0.9 }),
26− sym("H1", "Obsidian Blade", "high", { 3: 9, 4: 22, 5: 65 }, 3.8, { shape: "bolt", color: "#1f1f2e", accent: "#ff7a1a", glow: 0.6 }), 26+ sym("H1", "Obsidian Blade", "high", { 3: 9, 4: 22, 5: 65 }, 3.8, { shape: "warning", color: "#1f1f2e", accent: "#ff7a1a", glow: 0.6 }),
27 sym("H2", "Ember Serpent", "high", { 3: 7, 4: 18, 5: 55 }, 4.2, { shape: "wave", color: "#ff7a1a", accent: "#ffe0b3", glow: 0.6 }), 27 sym("H2", "Ember Serpent", "high", { 3: 7, 4: 18, 5: 55 }, 4.2, { shape: "wave", color: "#ff7a1a", accent: "#ffe0b3", glow: 0.6 }),
28 sym("M1", "Sulfur Gem", "mid", { 3: 4.5, 4: 11, 5: 30 }, 5.5, { shape: "gem", color: "#facc15", accent: "#fef9c3", glow: 0.3 }), 28 sym("M1", "Sulfur Gem", "mid", { 3: 4.5, 4: 11, 5: 30 }, 5.5, { shape: "gem", color: "#facc15", accent: "#fef9c3", glow: 0.3 }),
29− sym("M2", "Basalt Block", "mid", { 3: 3.5, 4: 9, 5: 22 }, 6, { shape: "cube", color: "#57534e", accent: "#a8a29e", glow: 0.2 }), 29+ sym("M2", "Basalt Block", "mid", { 3: 3.5, 4: 9, 5: 22 }, 6, { shape: "meteor", color: "#57534e", accent: "#a8a29e", glow: 0.2 }),
30 sym("L1", "Cinder", "low", { 3: 2, 4: 5, 5: 12 }, 9, { shape: "circle", color: "#f97316", glow: 0.2 }), 30 sym("L1", "Cinder", "low", { 3: 2, 4: 5, 5: 12 }, 9, { shape: "circle", color: "#f97316", glow: 0.2 }),
31 sym("L2", "Spark", "low", { 3: 2, 4: 5, 5: 12 }, 9, { shape: "star", color: "#fbbf24", glow: 0.2 }), 31 sym("L2", "Spark", "low", { 3: 2, 4: 5, 5: 12 }, 9, { shape: "star", color: "#fbbf24", glow: 0.2 }),
32− sym("L3", "Ash", "low", { 3: 1.5, 4: 4, 5: 10 }, 10, { shape: "hex", color: "#9ca3af", glow: 0.1 }), 32+ sym("L3", "Ash", "low", { 3: 1.5, 4: 4, 5: 10 }, 10, { shape: "barrel", color: "#9ca3af", glow: 0.1 }),
33 sym("L4", "Coal", "low", { 3: 1.5, 4: 4, 5: 10 }, 10, { shape: "diamond", color: "#44403c", accent: "#78716c", glow: 0.1 }), 33 sym("L4", "Coal", "low", { 3: 1.5, 4: 4, 5: 10 }, 10, { shape: "diamond", color: "#44403c", accent: "#78716c", glow: 0.1 }),
34 ], 34 ],
35 betDivisor: 25, 35 betDivisor: 25,
modified games/src/lucky-circuit/index.ts +4 −4
@@ -24,12 +24,12 @@ export const luckyCircuit = defineGame({
24 scatterSym("S", "Bonus Token", 1.3, { shape: "coin", color: "#f472b6", accent: "#ffffff", glow: 0.9 }), 24 scatterSym("S", "Bonus Token", 1.3, { shape: "coin", color: "#f472b6", accent: "#ffffff", glow: 0.9 }),
25 sym("MY", "Mystery Cartridge", "special", null, 1.5, { shape: "cube", color: "#1e1b4b", accent: "#22d3ee", label: "?", glow: 0.9 }, { kind: "mystery" }), 25 sym("MY", "Mystery Cartridge", "special", null, 1.5, { shape: "cube", color: "#1e1b4b", accent: "#22d3ee", label: "?", glow: 0.9 }, { kind: "mystery" }),
26 sym("P1", "Pixel Crown", "premium", { 3: 80, 4: 250, 5: 800 }, 3, { shape: "crown", color: "#facc15", accent: "#fef9c3", glow: 0.9 }), 26 sym("P1", "Pixel Crown", "premium", { 3: 80, 4: 250, 5: 800 }, 3, { shape: "crown", color: "#facc15", accent: "#fef9c3", glow: 0.9 }),
27− sym("H1", "Cherry Bomb", "high", { 3: 48, 4: 130, 5: 380 }, 4, { shape: "circle", color: "#ef4444", accent: "#fecaca", glow: 0.6 }), 27+ sym("H1", "Cherry Bomb", "high", { 3: 48, 4: 130, 5: 380 }, 4, { shape: "cherry", color: "#ef4444", accent: "#fecaca", glow: 0.6 }),
28 sym("H2", "Laser Blaster", "high", { 3: 38, 4: 110, 5: 320 }, 4.5, { shape: "bolt", color: "#22d3ee", accent: "#cffafe", glow: 0.6 }), 28 sym("H2", "Laser Blaster", "high", { 3: 38, 4: 110, 5: 320 }, 4.5, { shape: "bolt", color: "#22d3ee", accent: "#cffafe", glow: 0.6 }),
29− sym("M1", "Pixel Heart", "mid", { 3: 25, 4: 65, 5: 190 }, 6, { shape: "gem", color: "#f472b6", accent: "#fce7f3", glow: 0.3 }), 29+ sym("M1", "Arcade Stick", "mid", { 3: 25, 4: 65, 5: 190 }, 6, { shape: "joystick", color: "#f472b6", accent: "#fce7f3", glow: 0.3 }),
30 sym("M2", "Coin Slot", "mid", { 3: 20, 4: 55, 5: 160 }, 6, { shape: "chip", color: "#a3e635", accent: "#ecfccb", label: "1UP", glow: 0.3 }), 30 sym("M2", "Coin Slot", "mid", { 3: 20, 4: 55, 5: 160 }, 6, { shape: "chip", color: "#a3e635", accent: "#ecfccb", label: "1UP", glow: 0.3 }),
31− sym("L1", "Lucky Seven", "low", { 3: 16, 4: 40, 5: 100 }, 9, { shape: "letter", color: "#fb923c", label: "7" }), 31+ sym("L1", "Lucky Seven", "low", { 3: 16, 4: 40, 5: 100 }, 9, { shape: "seven", color: "#fb923c", accent: "#fff7ed" }),
32− sym("L2", "Bell Bit", "low", { 3: 16, 4: 40, 5: 100 }, 9, { shape: "letter", color: "#a78bfa", label: "B" }), 32+ sym("L2", "Bell Bit", "low", { 3: 16, 4: 40, 5: 100 }, 9, { shape: "bell", color: "#a78bfa", accent: "#facc15" }),
33 sym("L3", "Star Bit", "low", { 2: 3, 3: 16, 4: 40, 5: 100 }, 10, { shape: "hex", color: "#5eead4", glow: 0.2 }), 33 sym("L3", "Star Bit", "low", { 2: 3, 3: 16, 4: 40, 5: 100 }, 10, { shape: "hex", color: "#5eead4", glow: 0.2 }),
34 sym("L4", "Dot Bit", "low", { 2: 3, 3: 16, 4: 40, 5: 100 }, 10, { shape: "diamond", color: "#60a5fa", glow: 0.2 }), 34 sym("L4", "Dot Bit", "low", { 2: 3, 3: 16, 4: 40, 5: 100 }, 10, { shape: "diamond", color: "#60a5fa", glow: 0.2 }),
35 ], 35 ],
modified games/src/midnight-tokyo/index.ts +8 −8
@@ -22,15 +22,15 @@ export const midnightTokyo = defineGame({
22 symbols: [ 22 symbols: [
23 wildSym("W", "Neon Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "bolt", color: "#ff2bd6", accent: "#22d3ee", glow: 1 }), 23 wildSym("W", "Neon Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "bolt", color: "#ff2bd6", accent: "#22d3ee", glow: 1 }),
24 scatterSym("S", "Shibuya Sign", 1.3, { shape: "star", color: "#22d3ee", accent: "#ffffff", glow: 0.9 }), 24 scatterSym("S", "Shibuya Sign", 1.3, { shape: "star", color: "#22d3ee", accent: "#ffffff", glow: 0.9 }),
25− sym("P1", "Kitsune Mask", "premium", { 3: 112, 4: 420, 5: 1400 }, 2.6, { shape: "eye", color: "#ff2bd6", accent: "#ffffff", glow: 0.9 }), 25+ sym("P1", "Kitsune Mask", "premium", { 3: 112, 4: 420, 5: 1400 }, 2.6, { shape: "mask", color: "#ff2bd6", accent: "#ffffff", glow: 0.9 }),
26− sym("H1", "Neon Katana", "high", { 3: 56, 4: 168, 5: 560 }, 3.8, { shape: "flame", color: "#22d3ee", accent: "#e0fbff", glow: 0.6 }), 26+ sym("H1", "Neon Katana", "high", { 3: 56, 4: 168, 5: 560 }, 3.8, { shape: "katana", color: "#22d3ee", accent: "#e0fbff", glow: 0.6 }),
27− sym("H2", "Cyber Koi", "high", { 3: 42, 4: 140, 5: 420 }, 4.2, { shape: "wave", color: "#a855f7", accent: "#f3e8ff", glow: 0.6 }), 27+ sym("H2", "Cyber Koi", "high", { 3: 42, 4: 140, 5: 420 }, 4.2, { shape: "fish", color: "#a855f7", accent: "#f3e8ff", glow: 0.6 }),
28 sym("M1", "Ramen Bowl", "mid", { 3: 28, 4: 84, 5: 280 }, 5.5, { shape: "circle", color: "#fb7185", accent: "#ffe4e6", glow: 0.3 }), 28 sym("M1", "Ramen Bowl", "mid", { 3: 28, 4: 84, 5: 280 }, 5.5, { shape: "circle", color: "#fb7185", accent: "#ffe4e6", glow: 0.3 }),
29− sym("M2", "Synth Cassette", "mid", { 3: 22, 4: 70, 5: 225 }, 6, { shape: "chip", color: "#facc15", accent: "#fef9c3", label: "TK", glow: 0.3 }), 29+ sym("M2", "Synth Cassette", "mid", { 3: 22, 4: 70, 5: 225 }, 6, { shape: "cassette", color: "#facc15", accent: "#fef9c3", glow: 0.3 }),
30− sym("L1", "Neon A", "low", { 3: 14, 4: 42, 5: 140 }, 9, { shape: "letter", color: "#f472b6", label: "A" }), 30+ sym("L1", "Torii Gate", "low", { 3: 14, 4: 42, 5: 140 }, 9, { shape: "torii", color: "#f472b6", accent: "#ffffff" }),
31− sym("L2", "Neon K", "low", { 3: 14, 4: 42, 5: 140 }, 9, { shape: "letter", color: "#67e8f9", label: "K" }), 31+ sym("L2", "Paper Lantern", "low", { 3: 14, 4: 42, 5: 140 }, 9, { shape: "lantern", color: "#67e8f9", accent: "#ffffff" }),
32− sym("L3", "Neon Q", "low", { 3: 11, 4: 34, 5: 112 }, 10, { shape: "letter", color: "#c084fc", label: "Q" }), 32+ sym("L3", "Kanji Sign", "low", { 3: 11, 4: 34, 5: 112 }, 10, { shape: "code", color: "#c084fc", label: "夜" }),
33− sym("L4", "Neon J", "low", { 3: 11, 4: 34, 5: 112 }, 10, { shape: "letter", color: "#fbbf24", label: "J" }), 33+ sym("L4", "Vending Token", "low", { 3: 11, 4: 34, 5: 112 }, 10, { shape: "coin", color: "#fbbf24", accent: "#fff7ed", label: "¥" }),
34 ], 34 ],
35 betDivisor: 25, 35 betDivisor: 25,
36 wild: { id: "W", stacked: 0.35 }, 36 wild: { id: "W", stacked: 0.35 },
modified games/src/moonbase-77/index.ts +6 −6
@@ -23,12 +23,12 @@ export const moonbase77 = defineGame({
23 wildSym("W", "Gravity Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "moon", color: "#e5e7eb", accent: "#fb923c", glow: 1 }), 23 wildSym("W", "Gravity Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "moon", color: "#e5e7eb", accent: "#fb923c", glow: 1 }),
24 scatterSym("S", "Orbital Beacon", 1.3, { shape: "star", color: "#fb923c", accent: "#ffffff", glow: 0.9 }), 24 scatterSym("S", "Orbital Beacon", 1.3, { shape: "star", color: "#fb923c", accent: "#ffffff", glow: 0.9 }),
25 sym("P1", "Commander 77", "premium", { 3: 96, 4: 360, 5: 1200 }, 2.6, { shape: "letter", color: "#fb923c", accent: "#fff7ed", label: "77", glow: 0.9 }), 25 sym("P1", "Commander 77", "premium", { 3: 96, 4: 360, 5: 1200 }, 2.6, { shape: "letter", color: "#fb923c", accent: "#fff7ed", label: "77", glow: 0.9 }),
26− sym("H1", "Lunar Rover", "high", { 3: 48, 4: 144, 5: 480 }, 3.8, { shape: "cube", color: "#e5e7eb", accent: "#fb923c", glow: 0.6 }), 26+ sym("H1", "Lunar Rover", "high", { 3: 48, 4: 144, 5: 480 }, 3.8, { shape: "rocket", color: "#e5e7eb", accent: "#fb923c", glow: 0.6 }),
27− sym("H2", "Habitat Dome", "high", { 3: 36, 4: 120, 5: 360 }, 4.2, { shape: "ring", color: "#22d3ee", accent: "#cffafe", glow: 0.6 }), 27+ sym("H2", "Habitat Dome", "high", { 3: 36, 4: 120, 5: 360 }, 4.2, { shape: "igloo", color: "#22d3ee", accent: "#cffafe", glow: 0.6 }),
28− sym("M1", "Oxygen Tank", "mid", { 3: 24, 4: 72, 5: 240 }, 5.5, { shape: "hex", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }), 28+ sym("M1", "Oxygen Tank", "mid", { 3: 24, 4: 72, 5: 240 }, 5.5, { shape: "barrel", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }),
29− sym("M2", "Solar Panel", "mid", { 3: 19, 4: 60, 5: 190 }, 6, { shape: "chip", color: "#1e3a8a", accent: "#fbbf24", label: "SP", glow: 0.3 }), 29+ sym("M2", "Comm Satellite", "mid", { 3: 19, 4: 60, 5: 190 }, 6, { shape: "satellite", color: "#1e3a8a", accent: "#fbbf24", glow: 0.3 }),
30− sym("L1", "Moon Rock", "low", { 3: 12, 4: 36, 5: 120 }, 9, { shape: "circle", color: "#9ca3af", glow: 0.1 }), 30+ sym("L1", "Moon Rock", "low", { 3: 12, 4: 36, 5: 120 }, 9, { shape: "meteor", color: "#9ca3af", glow: 0.1 }),
31− sym("L2", "Crater Dust", "low", { 3: 12, 4: 36, 5: 120 }, 9, { shape: "diamond", color: "#a8a29e", glow: 0.1 }), 31+ sym("L2", "Blue Planet", "low", { 3: 12, 4: 36, 5: 120 }, 9, { shape: "planet", color: "#a8a29e", glow: 0.1 }),
32 sym("L3", "Regolith Shard", "low", { 3: 10, 4: 30, 5: 96 }, 10, { shape: "crystal", color: "#78716c", accent: "#d6d3d1", glow: 0.1 }), 32 sym("L3", "Regolith Shard", "low", { 3: 10, 4: 30, 5: 96 }, 10, { shape: "crystal", color: "#78716c", accent: "#d6d3d1", glow: 0.1 }),
33 sym("L4", "Ice Core", "low", { 3: 10, 4: 30, 5: 96 }, 10, { shape: "snow", color: "#bae6fd", glow: 0.2 }), 33 sym("L4", "Ice Core", "low", { 3: 10, 4: 30, 5: 96 }, 10, { shape: "snow", color: "#bae6fd", glow: 0.2 }),
34 ], 34 ],
modified games/src/neon-vault/index.ts +7 −7
@@ -21,17 +21,17 @@ export const neonVault = defineGame({
21 maxMultiplier: 5000, 21 maxMultiplier: 5000,
22 symbols: [ 22 symbols: [
23 wildSym("W", "Override Wild", [0, 3, 3, 3, 0], { shape: "vault", color: "#00f0ff", accent: "#ffffff", glow: 1 }), 23 wildSym("W", "Override Wild", [0, 3, 3, 3, 0], { shape: "vault", color: "#00f0ff", accent: "#ffffff", glow: 1 }),
24− scatterSym("S", "Access Key", 1.6, { shape: "letter", color: "#ff2bd6", accent: "#ffffff", label: "KEY", glow: 0.9 }, { 3: 2, 4: 10, 5: 50 }), 24+ scatterSym("S", "Access Key", 1.6, { shape: "key", color: "#ff2bd6", accent: "#ffffff", glow: 0.9 }, { 3: 2, 4: 10, 5: 50 }),
25 sym("V", "Vault", "special", null, 0.9, { shape: "hex", color: "#ffd166", accent: "#fff2b3", glow: 0.8 }, { kind: "bonus" }), 25 sym("V", "Vault", "special", null, 0.9, { shape: "hex", color: "#ffd166", accent: "#fff2b3", glow: 0.8 }, { kind: "bonus" }),
26 sym("P1", "Core Processor", "premium", { 3: 3, 4: 10, 5: 40 }, 3, { shape: "atom", color: "#7cf5ff", accent: "#e8fdff", glow: 0.7 }), 26 sym("P1", "Core Processor", "premium", { 3: 3, 4: 10, 5: 40 }, 3, { shape: "atom", color: "#7cf5ff", accent: "#e8fdff", glow: 0.7 }),
27− sym("H1", "Security Drone", "high", { 3: 2, 4: 6, 5: 20 }, 4, { shape: "eye", color: "#c084fc", accent: "#f3e8ff", glow: 0.5 }), 27+ sym("H1", "Security Drone", "high", { 3: 2, 4: 6, 5: 20 }, 4, { shape: "satellite", color: "#c084fc", accent: "#f3e8ff", glow: 0.5 }),
28 sym("H2", "Laser Grid", "high", { 3: 1.5, 4: 4, 5: 15 }, 4.5, { shape: "bolt", color: "#ff5c8a", accent: "#ffe3ec", glow: 0.5 }), 28 sym("H2", "Laser Grid", "high", { 3: 1.5, 4: 4, 5: 15 }, 4.5, { shape: "bolt", color: "#ff5c8a", accent: "#ffe3ec", glow: 0.5 }),
29− sym("M1", "Keycard", "mid", { 3: 1, 4: 2.5, 5: 8 }, 6, { shape: "chip", color: "#34d399", accent: "#d1fae5", label: "ID", glow: 0.3 }), 29+ sym("M1", "Keycard", "mid", { 3: 1, 4: 2.5, 5: 8 }, 6, { shape: "badge", color: "#34d399", accent: "#d1fae5", glow: 0.3 }),
30 sym("M2", "Data Chip", "mid", { 3: 0.8, 4: 2, 5: 6 }, 6, { shape: "cube", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }), 30 sym("M2", "Data Chip", "mid", { 3: 0.8, 4: 2, 5: 6 }, 6, { shape: "cube", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }),
31− sym("L1", "Cipher A", "low", { 3: 0.4, 4: 1, 5: 3 }, 9, { shape: "letter", color: "#5eead4", label: "A" }), 31+ sym("L1", "Padlock", "low", { 3: 0.4, 4: 1, 5: 3 }, 9, { shape: "lock", color: "#5eead4", accent: "#ffffff" }),
32− sym("L2", "Cipher K", "low", { 3: 0.4, 4: 1, 5: 3 }, 9, { shape: "letter", color: "#a78bfa", label: "K" }), 32+ sym("L2", "Binary Block", "low", { 3: 0.4, 4: 1, 5: 3 }, 9, { shape: "code", color: "#a78bfa", label: "0110" }),
33− sym("L3", "Cipher Q", "low", { 3: 0.3, 4: 0.8, 5: 2.5 }, 10, { shape: "letter", color: "#f472b6", label: "Q" }), 33+ sym("L3", "Hazard Panel", "low", { 3: 0.3, 4: 0.8, 5: 2.5 }, 10, { shape: "warning", color: "#f472b6", accent: "#ffffff" }),
34− sym("L4", "Cipher J", "low", { 3: 0.3, 4: 0.8, 5: 2.5 }, 10, { shape: "letter", color: "#fb923c", label: "J" }), 34+ sym("L4", "Pressure Valve", "low", { 3: 0.3, 4: 0.8, 5: 2.5 }, 10, { shape: "valve", color: "#fb923c", accent: "#ffedd5" }),
35 ], 35 ],
36 betDivisor: 25, 36 betDivisor: 25,
37 wild: { id: "W", expanding: true }, 37 wild: { id: "W", expanding: true },
modified games/src/pharaoh-protocol/index.ts +8 −8
@@ -21,17 +21,17 @@ export const pharaohProtocol = defineGame({
21 maxMultiplier: 3000, 21 maxMultiplier: 3000,
22 symbols: [ 22 symbols: [
23 wildSym("W", "Eye of Horus", [0, 1.8, 1.8, 1.8, 0], { shape: "eye", color: "#ffd166", accent: "#22d3ee", glow: 1 }), 23 wildSym("W", "Eye of Horus", [0, 1.8, 1.8, 1.8, 0], { shape: "eye", color: "#ffd166", accent: "#22d3ee", glow: 1 }),
24− scatterSym("S", "Scarab Drive", 1.3, { shape: "star", color: "#22d3ee", accent: "#ffffff", glow: 0.9 }), 24+ scatterSym("S", "Scarab Drive", 1.3, { shape: "scarab", color: "#22d3ee", accent: "#ffffff", glow: 0.9 }),
25− sym("AR", "Artifact", "special", null, 1.1, { shape: "cube", color: "#f59e0b", accent: "#22d3ee", glow: 0.8 }, { kind: "charge" }), 25+ sym("AR", "Artifact", "special", null, 1.1, { shape: "ankh", color: "#f59e0b", accent: "#22d3ee", glow: 0.8 }, { kind: "charge" }),
26 sym("P1", "Pharaoh Core", "premium", { 3: 68, 4: 255, 5: 850 }, 2.6, { shape: "crown", color: "#ffd166", accent: "#fff7d6", glow: 0.9 }), 26 sym("P1", "Pharaoh Core", "premium", { 3: 68, 4: 255, 5: 850 }, 2.6, { shape: "crown", color: "#ffd166", accent: "#fff7d6", glow: 0.9 }),
27 sym("H1", "Anubis Unit", "high", { 3: 34, 4: 100, 5: 340 }, 3.8, { shape: "skull", color: "#1e293b", accent: "#ffd166", glow: 0.6 }), 27 sym("H1", "Anubis Unit", "high", { 3: 34, 4: 100, 5: 340 }, 3.8, { shape: "skull", color: "#1e293b", accent: "#ffd166", glow: 0.6 }),
28 sym("H2", "Sun Disk", "high", { 3: 25, 4: 85, 5: 255 }, 4.2, { shape: "circle", color: "#fb923c", accent: "#fff1c2", glow: 0.6 }), 28 sym("H2", "Sun Disk", "high", { 3: 25, 4: 85, 5: 255 }, 4.2, { shape: "circle", color: "#fb923c", accent: "#fff1c2", glow: 0.6 }),
29− sym("M1", "Obelisk", "mid", { 3: 17, 4: 50, 5: 170 }, 5.5, { shape: "hex", color: "#c2b280", accent: "#f5edd6", glow: 0.3 }), 29+ sym("M1", "Obelisk", "mid", { 3: 17, 4: 50, 5: 170 }, 5.5, { shape: "pyramid", color: "#c2b280", accent: "#f5edd6", glow: 0.3 }),
30− sym("M2", "Papyrus Code", "mid", { 3: 14, 4: 42, 5: 135 }, 6, { shape: "chip", color: "#a3e635", accent: "#ecfccb", label: "PY", glow: 0.3 }), 30+ sym("M2", "Papyrus Code", "mid", { 3: 14, 4: 42, 5: 135 }, 6, { shape: "scroll", color: "#a3e635", accent: "#ecfccb", glow: 0.3 }),
31− sym("L1", "Glyph A", "low", { 3: 8, 4: 25, 5: 85 }, 9, { shape: "letter", color: "#fbbf24", label: "A" }), 31+ sym("L1", "Ankh Glyph", "low", { 3: 8, 4: 25, 5: 85 }, 9, { shape: "ankh", color: "#fbbf24", accent: "#22d3ee" }),
32− sym("L2", "Glyph K", "low", { 3: 8, 4: 25, 5: 85 }, 9, { shape: "letter", color: "#22d3ee", label: "K" }), 32+ sym("L2", "Canopic Jar", "low", { 3: 8, 4: 25, 5: 85 }, 9, { shape: "barrel", color: "#22d3ee", accent: "#ffd166" }),
33− sym("L3", "Glyph Q", "low", { 3: 7, 4: 20, 5: 70 }, 10, { shape: "letter", color: "#f97316", label: "Q" }), 33+ sym("L3", "Sand Hourglass", "low", { 3: 7, 4: 20, 5: 70 }, 10, { shape: "hourglass", color: "#f97316", accent: "#ffd166" }),
34− sym("L4", "Glyph J", "low", { 3: 7, 4: 20, 5: 70 }, 10, { shape: "letter", color: "#a78bfa", label: "J" }), 34+ sym("L4", "Cobra Glyph", "low", { 3: 7, 4: 20, 5: 70 }, 10, { shape: "snake", color: "#a78bfa", accent: "#ffd166" }),
35 ], 35 ],
36 betDivisor: 20, 36 betDivisor: 20,
37 wild: { id: "W", floating: { chance: 0.08, min: 1, max: 3 } }, 37 wild: { id: "W", floating: { chance: 0.08, min: 1, max: 3 } },
modified games/src/quantum-jackpot/index.ts +4 −4
@@ -24,11 +24,11 @@ export const quantumJackpot = defineGame({
24 scatterSym("S", "Entangler", 1.3, { shape: "ring", color: "#c084fc", accent: "#ffffff", glow: 0.9 }), 24 scatterSym("S", "Entangler", 1.3, { shape: "ring", color: "#c084fc", accent: "#ffffff", glow: 0.9 }),
25 sym("P1", "Quantum Processor", "premium", { 3: 24, 4: 72, 5: 240 }, 2.8, { shape: "chip", color: "#22d3ee", accent: "#ecfeff", label: "Q", glow: 0.9 }), 25 sym("P1", "Quantum Processor", "premium", { 3: 24, 4: 72, 5: 240 }, 2.8, { shape: "chip", color: "#22d3ee", accent: "#ecfeff", label: "Q", glow: 0.9 }),
26 sym("H1", "Superposition Orb", "high", { 3: 16, 4: 40, 5: 120 }, 3.8, { shape: "orb", color: "#a78bfa", accent: "#f3e8ff", glow: 0.6 }), 26 sym("H1", "Superposition Orb", "high", { 3: 16, 4: 40, 5: 120 }, 3.8, { shape: "orb", color: "#a78bfa", accent: "#f3e8ff", glow: 0.6 }),
27− sym("H2", "Cryo Chamber", "high", { 3: 12, 4: 32, 5: 96 }, 4.2, { shape: "hex", color: "#38bdf8", accent: "#e0f2fe", glow: 0.6 }), 27+ sym("H2", "Cryo Chamber", "high", { 3: 12, 4: 32, 5: 96 }, 4.2, { shape: "barrel", color: "#38bdf8", accent: "#e0f2fe", glow: 0.6 }),
28− sym("M1", "Photon Gate", "mid", { 3: 8, 4: 20, 5: 56 }, 5.5, { shape: "gem", color: "#34d399", accent: "#d1fae5", glow: 0.3 }), 28+ sym("M1", "Photon Gate", "mid", { 3: 8, 4: 20, 5: 56 }, 5.5, { shape: "torii", color: "#34d399", accent: "#d1fae5", glow: 0.3 }),
29 sym("M2", "Wave Function", "mid", { 3: 6.5, 4: 16, 5: 40 }, 6, { shape: "wave", color: "#f472b6", accent: "#fce7f3", glow: 0.3 }), 29 sym("M2", "Wave Function", "mid", { 3: 6.5, 4: 16, 5: 40 }, 6, { shape: "wave", color: "#f472b6", accent: "#fce7f3", glow: 0.3 }),
30− sym("L1", "Bit One", "low", { 3: 3, 4: 8, 5: 20 }, 6.5, { shape: "letter", color: "#67e8f9", label: "1" }), 30+ sym("L1", "Bit One", "low", { 3: 3, 4: 8, 5: 20 }, 6.5, { shape: "code", color: "#67e8f9", label: "1" }),
31− sym("L2", "Bit Zero", "low", { 3: 3, 4: 8, 5: 20 }, 6.5, { shape: "letter", color: "#a5b4fc", label: "0" }), 31+ sym("L2", "Bit Zero", "low", { 3: 3, 4: 8, 5: 20 }, 6.5, { shape: "code", color: "#a5b4fc", label: "0" }),
32 sym("L3", "Psi State", "low", { 3: 2.5, 4: 6.5, 5: 16 }, 6.5, { shape: "letter", color: "#c084fc", label: "Ψ" }), 32 sym("L3", "Psi State", "low", { 3: 2.5, 4: 6.5, 5: 16 }, 6.5, { shape: "letter", color: "#c084fc", label: "Ψ" }),
33 sym("L4", "Phi State", "low", { 3: 2.5, 4: 6.5, 5: 16 }, 6.5, { shape: "letter", color: "#5eead4", label: "Φ" }), 33 sym("L4", "Phi State", "low", { 3: 2.5, 4: 6.5, 5: 16 }, 6.5, { shape: "letter", color: "#5eead4", label: "Φ" }),
34 sym("L5", "Theta State", "low", { 3: 2.5, 4: 6.5, 5: 16 }, 6.5, { shape: "letter", color: "#fbbf24", label: "Θ" }), 34 sym("L5", "Theta State", "low", { 3: 2.5, 4: 6.5, 5: 16 }, 6.5, { shape: "letter", color: "#fbbf24", label: "Θ" }),
modified games/src/reel-reactor/index.ts +5 −5
@@ -21,12 +21,12 @@ export const reelReactor = defineGame({
21 maxMultiplier: 5000, 21 maxMultiplier: 5000,
22 symbols: [ 22 symbols: [
23 wildSym("W", "Fusion Wild", [0, 2, 2, 2, 0], { shape: "atom", color: "#a3e635", accent: "#ffffff", glow: 1 }), 23 wildSym("W", "Fusion Wild", [0, 2, 2, 2, 0], { shape: "atom", color: "#a3e635", accent: "#ffffff", glow: 1 }),
24− scatterSym("S", "Control Rod", 1.2, { shape: "bolt", color: "#facc15", accent: "#ffffff", glow: 0.9 }), 24+ scatterSym("S", "Control Rod", 1.2, { shape: "gauge", color: "#facc15", accent: "#ffffff", glow: 0.9 }),
25 sym("P1", "Reactor Core", "premium", { 3: 18, 4: 54, 5: 180 }, 2.8, { shape: "orb", color: "#22c55e", accent: "#dcfce7", glow: 0.9 }), 25 sym("P1", "Reactor Core", "premium", { 3: 18, 4: 54, 5: 180 }, 2.8, { shape: "orb", color: "#22c55e", accent: "#dcfce7", glow: 0.9 }),
26− sym("H1", "Uranium Cell", "high", { 3: 12, 4: 30, 5: 90 }, 3.8, { shape: "hex", color: "#a3e635", accent: "#ecfccb", glow: 0.6 }), 26+ sym("H1", "Uranium Cell", "high", { 3: 12, 4: 30, 5: 90 }, 3.8, { shape: "barrel", color: "#a3e635", accent: "#ecfccb", glow: 0.6 }),
27− sym("H2", "Plasma Coil", "high", { 3: 9, 4: 24, 5: 72 }, 4.2, { shape: "ring", color: "#38bdf8", accent: "#e0f2fe", glow: 0.6 }), 27+ sym("H2", "Plasma Coil", "high", { 3: 9, 4: 24, 5: 72 }, 4.2, { shape: "valve", color: "#38bdf8", accent: "#e0f2fe", glow: 0.6 }),
28− sym("M1", "Cooling Tower", "mid", { 3: 6, 4: 15, 5: 42 }, 5.5, { shape: "cube", color: "#94a3b8", accent: "#e2e8f0", glow: 0.3 }), 28+ sym("M1", "Cooling Tower", "mid", { 3: 6, 4: 15, 5: 42 }, 5.5, { shape: "drill", color: "#94a3b8", accent: "#e2e8f0", glow: 0.3 }),
29− sym("M2", "Hazard Gauge", "mid", { 3: 4.8, 4: 12, 5: 30 }, 6, { shape: "chip", color: "#facc15", accent: "#111827", label: "RAD", glow: 0.3 }), 29+ sym("M2", "Hazard Sign", "mid", { 3: 4.8, 4: 12, 5: 30 }, 6, { shape: "warning", color: "#facc15", accent: "#111827", glow: 0.3 }),
30 sym("L1", "Isotope Alpha", "low", { 3: 2.4, 4: 6, 5: 15 }, 9, { shape: "letter", color: "#4ade80", label: "α" }), 30 sym("L1", "Isotope Alpha", "low", { 3: 2.4, 4: 6, 5: 15 }, 9, { shape: "letter", color: "#4ade80", label: "α" }),
31 sym("L2", "Isotope Beta", "low", { 3: 2.4, 4: 6, 5: 15 }, 9, { shape: "letter", color: "#facc15", label: "β" }), 31 sym("L2", "Isotope Beta", "low", { 3: 2.4, 4: 6, 5: 15 }, 9, { shape: "letter", color: "#facc15", label: "β" }),
32 sym("L3", "Isotope Gamma", "low", { 3: 1.8, 4: 4.8, 5: 12 }, 10, { shape: "letter", color: "#38bdf8", label: "γ" }), 32 sym("L3", "Isotope Gamma", "low", { 3: 1.8, 4: 4.8, 5: 12 }, 10, { shape: "letter", color: "#38bdf8", label: "γ" }),
modified games/src/royal-circuit/index.ts +10 −10
@@ -21,17 +21,17 @@ export const royalCircuit = defineGame({
21 maxMultiplier: 3000, 21 maxMultiplier: 3000,
22 symbols: [ 22 symbols: [
23 wildSym("W", "Turbo Wild", [0, 1.8, 1.8, 1.8, 0], { shape: "bolt", color: "#ffd166", accent: "#ef4444", glow: 1 }), 23 wildSym("W", "Turbo Wild", [0, 1.8, 1.8, 1.8, 0], { shape: "bolt", color: "#ffd166", accent: "#ef4444", glow: 1 }),
24− scatterSym("S", "Chequered Flag", 1.3, { shape: "chip", color: "#f8fafc", accent: "#111827", label: "GP", glow: 0.9 }), 24+ scatterSym("S", "Chequered Flag", 1.3, { shape: "flag", color: "#f8fafc", accent: "#111827", glow: 0.9 }),
25− sym("LAP", "Lap Counter", "special", null, 0.9, { shape: "ring", color: "#22d3ee", accent: "#ffffff", glow: 0.8 }, { kind: "charge" }), 25+ sym("LAP", "Lap Counter", "special", null, 0.9, { shape: "gauge", color: "#22d3ee", accent: "#ffffff", glow: 0.8 }, { kind: "charge" }),
26 sym("P1", "Royal Hypercar", "premium", { 3: 60, 4: 225, 5: 750 }, 2.6, { shape: "crown", color: "#ffd166", accent: "#fff7d6", glow: 0.9 }), 26 sym("P1", "Royal Hypercar", "premium", { 3: 60, 4: 225, 5: 750 }, 2.6, { shape: "crown", color: "#ffd166", accent: "#fff7d6", glow: 0.9 }),
27− sym("H1", "Champion Helmet", "high", { 3: 30, 4: 90, 5: 300 }, 3.8, { shape: "shield", color: "#ef4444", accent: "#fecaca", glow: 0.6 }), 27+ sym("H1", "Champion Helmet", "high", { 3: 30, 4: 90, 5: 300 }, 3.8, { shape: "helmet", color: "#ef4444", accent: "#fecaca", glow: 0.6 }),
28− sym("H2", "Gold Trophy", "high", { 3: 22, 4: 75, 5: 225 }, 4.2, { shape: "gem", color: "#f59e0b", accent: "#fef3c7", glow: 0.6 }), 28+ sym("H2", "Gold Trophy", "high", { 3: 22, 4: 75, 5: 225 }, 4.2, { shape: "trophy", color: "#f59e0b", accent: "#fef3c7", glow: 0.6 }),
29− sym("M1", "Racing Tyre", "mid", { 3: 15, 4: 45, 5: 150 }, 5.5, { shape: "circle", color: "#374151", accent: "#9ca3af", glow: 0.3 }), 29+ sym("M1", "Racing Tyre", "mid", { 3: 15, 4: 45, 5: 150 }, 5.5, { shape: "wheel", color: "#374151", accent: "#9ca3af", glow: 0.3 }),
30− sym("M2", "Fuel Can", "mid", { 3: 12, 4: 38, 5: 120 }, 6, { shape: "cube", color: "#10b981", accent: "#d1fae5", glow: 0.3 }), 30+ sym("M2", "Fuel Can", "mid", { 3: 12, 4: 38, 5: 120 }, 6, { shape: "barrel", color: "#10b981", accent: "#d1fae5", glow: 0.3 }),
31− sym("L1", "Grid A", "low", { 3: 7.5, 4: 22, 5: 75 }, 9, { shape: "letter", color: "#fbbf24", label: "A" }), 31+ sym("L1", "Pit Wrench", "low", { 3: 7.5, 4: 22, 5: 75 }, 9, { shape: "valve", color: "#fbbf24", accent: "#fff7ed" }),
32− sym("L2", "Grid K", "low", { 3: 7.5, 4: 22, 5: 75 }, 9, { shape: "letter", color: "#ef4444", label: "K" }), 32+ sym("L2", "Traffic Cone", "low", { 3: 7.5, 4: 22, 5: 75 }, 9, { shape: "warning", color: "#ef4444", accent: "#ffffff" }),
33− sym("L3", "Grid Q", "low", { 3: 6, 4: 18, 5: 60 }, 10, { shape: "letter", color: "#22d3ee", label: "Q" }), 33+ sym("L3", "Stopwatch", "low", { 3: 6, 4: 18, 5: 60 }, 10, { shape: "hourglass", color: "#22d3ee", accent: "#ffffff" }),
34− sym("L4", "Grid J", "low", { 3: 6, 4: 18, 5: 60 }, 10, { shape: "letter", color: "#e5e7eb", label: "J" }), 34+ sym("L4", "Grid Marker", "low", { 3: 6, 4: 18, 5: 60 }, 10, { shape: "code", color: "#e5e7eb", label: "P1" }),
35 ], 35 ],
36 betDivisor: 20, 36 betDivisor: 20,
37 wild: { id: "W", multiplier: { values: [2, 3], weights: [70, 30], combine: "multiply" } }, 37 wild: { id: "W", multiplier: { values: [2, 3], weights: [70, 30], combine: "multiply" } },
modified games/src/void-miner/index.ts +4 −4
@@ -20,16 +20,16 @@ export const voidMiner = defineGame({
20 volatility: "medium", 20 volatility: "medium",
21 maxMultiplier: 4000, 21 maxMultiplier: 4000,
22 symbols: [ 22 symbols: [
23− wildSym("W", "Drill Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "bolt", color: "#fbbf24", accent: "#ffffff", glow: 1 }), 23+ wildSym("W", "Drill Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "pickaxe", color: "#fbbf24", accent: "#ffffff", glow: 1 }),
24 scatterSym("S", "Warp Beacon", 1.3, { shape: "star", color: "#c084fc", accent: "#ffffff", glow: 0.9 }), 24 scatterSym("S", "Warp Beacon", 1.3, { shape: "star", color: "#c084fc", accent: "#ffffff", glow: 0.9 }),
25 sym("DM", "Dark Matter", "special", null, 1.1, { shape: "orb", color: "#1e1b4b", accent: "#a78bfa", glow: 0.9 }, { kind: "charge" }), 25 sym("DM", "Dark Matter", "special", null, 1.1, { shape: "orb", color: "#1e1b4b", accent: "#a78bfa", glow: 0.9 }, { kind: "charge" }),
26 sym("P1", "Void Diamond", "premium", { 3: 7.5, 4: 22, 5: 75 }, 2.8, { shape: "diamond", color: "#e9d5ff", accent: "#ffffff", glow: 0.9 }), 26 sym("P1", "Void Diamond", "premium", { 3: 7.5, 4: 22, 5: 75 }, 2.8, { shape: "diamond", color: "#e9d5ff", accent: "#ffffff", glow: 0.9 }),
27 sym("H1", "Platinum Ore", "high", { 3: 5, 4: 12, 5: 37 }, 3.8, { shape: "gem", color: "#e5e7eb", accent: "#ffffff", glow: 0.6 }), 27 sym("H1", "Platinum Ore", "high", { 3: 5, 4: 12, 5: 37 }, 3.8, { shape: "gem", color: "#e5e7eb", accent: "#ffffff", glow: 0.6 }),
28 sym("H2", "Plasma Vein", "high", { 3: 3.75, 4: 10, 5: 30 }, 4.2, { shape: "flame", color: "#f472b6", accent: "#fce7f3", glow: 0.6 }), 28 sym("H2", "Plasma Vein", "high", { 3: 3.75, 4: 10, 5: 30 }, 4.2, { shape: "flame", color: "#f472b6", accent: "#fce7f3", glow: 0.6 }),
29− sym("M1", "Titanium Ore", "mid", { 3: 2.5, 4: 6, 5: 17 }, 5.5, { shape: "hex", color: "#94a3b8", accent: "#e2e8f0", glow: 0.3 }), 29+ sym("M1", "Titanium Ore", "mid", { 3: 2.5, 4: 6, 5: 17 }, 5.5, { shape: "ore", color: "#94a3b8", accent: "#e2e8f0", glow: 0.3 }),
30− sym("M2", "Cobalt Ore", "mid", { 3: 2, 4: 5, 5: 12 }, 6, { shape: "cube", color: "#3b82f6", accent: "#bfdbfe", glow: 0.3 }), 30+ sym("M2", "Cobalt Ore", "mid", { 3: 2, 4: 5, 5: 12 }, 6, { shape: "drill", color: "#3b82f6", accent: "#bfdbfe", glow: 0.3 }),
31 sym("L1", "Iron Ore", "low", { 3: 1, 4: 2.5, 5: 6 }, 6.5, { shape: "circle", color: "#b45309", glow: 0.2 }), 31 sym("L1", "Iron Ore", "low", { 3: 1, 4: 2.5, 5: 6 }, 6.5, { shape: "circle", color: "#b45309", glow: 0.2 }),
32− sym("L2", "Nickel Ore", "low", { 3: 1, 4: 2.5, 5: 6 }, 6.5, { shape: "ring", color: "#a8a29e", glow: 0.2 }), 32+ sym("L2", "Nickel Ore", "low", { 3: 1, 4: 2.5, 5: 6 }, 6.5, { shape: "satellite", color: "#a8a29e", glow: 0.2 }),
33 sym("L3", "Carbon Chunk", "low", { 3: 0.75, 4: 2, 5: 5 }, 6.5, { shape: "crystal", color: "#4b5563", accent: "#9ca3af", glow: 0.1 }), 33 sym("L3", "Carbon Chunk", "low", { 3: 0.75, 4: 2, 5: 5 }, 6.5, { shape: "crystal", color: "#4b5563", accent: "#9ca3af", glow: 0.1 }),
34 sym("L4", "Ice Rock", "low", { 3: 0.75, 4: 2, 5: 5 }, 6.5, { shape: "snow", color: "#7dd3fc", glow: 0.2 }), 34 sym("L4", "Ice Rock", "low", { 3: 0.75, 4: 2, 5: 5 }, 6.5, { shape: "snow", color: "#7dd3fc", glow: 0.2 }),
35 sym("L5", "Copper Ore", "low", { 3: 0.75, 4: 2, 5: 5 }, 6.5, { shape: "diamond", color: "#f97316", glow: 0.2 }), 35 sym("L5", "Copper Ore", "low", { 3: 0.75, 4: 2, 5: 5 }, 6.5, { shape: "diamond", color: "#f97316", glow: 0.2 }),
modified games/src/wild-temple/index.ts +8 −8
@@ -21,16 +21,16 @@ export const wildTemple = defineGame({
21 maxMultiplier: 3000, 21 maxMultiplier: 3000,
22 symbols: [ 22 symbols: [
23 wildSym("W", "Jaguar Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "eye", color: "#f59e0b", accent: "#111827", glow: 1 }), 23 wildSym("W", "Jaguar Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "eye", color: "#f59e0b", accent: "#111827", glow: 1 }),
24− scatterSym("S", "Sun Idol", 1.3, { shape: "circle", color: "#fbbf24", accent: "#ffffff", glow: 1 }, { 3: 2, 4: 10, 5: 50 }), 24+ scatterSym("S", "Sun Idol", 1.3, { shape: "idol", color: "#fbbf24", accent: "#ffffff", glow: 1 }, { 3: 2, 4: 10, 5: 50 }),
25− sym("P1", "Temple King Mask", "premium", { 3: 36, 4: 108, 5: 360 }, 2.8, { shape: "skull", color: "#10b981", accent: "#fbbf24", glow: 0.9 }), 25+ sym("P1", "Temple King Mask", "premium", { 3: 36, 4: 108, 5: 360 }, 2.8, { shape: "mask", color: "#10b981", accent: "#fbbf24", glow: 0.9 }),
26− sym("H1", "Jade Serpent", "high", { 3: 24, 4: 60, 5: 180 }, 3.8, { shape: "wave", color: "#34d399", accent: "#d1fae5", glow: 0.6 }), 26+ sym("H1", "Jade Serpent", "high", { 3: 24, 4: 60, 5: 180 }, 3.8, { shape: "snake", color: "#34d399", accent: "#d1fae5", glow: 0.6 }),
27 sym("H2", "Ritual Torch", "high", { 3: 18, 4: 48, 5: 144 }, 4.2, { shape: "flame", color: "#f97316", accent: "#ffedd5", glow: 0.6 }), 27 sym("H2", "Ritual Torch", "high", { 3: 18, 4: 48, 5: 144 }, 4.2, { shape: "flame", color: "#f97316", accent: "#ffedd5", glow: 0.6 }),
28 sym("M1", "Sacred Leaf", "mid", { 3: 12, 4: 30, 5: 84 }, 5.5, { shape: "leaf", color: "#22c55e", accent: "#bbf7d0", glow: 0.3 }), 28 sym("M1", "Sacred Leaf", "mid", { 3: 12, 4: 30, 5: 84 }, 5.5, { shape: "leaf", color: "#22c55e", accent: "#bbf7d0", glow: 0.3 }),
29− sym("M2", "Stone Tablet", "mid", { 3: 9.6, 4: 24, 5: 60 }, 6, { shape: "cube", color: "#78716c", accent: "#d6d3d1", glow: 0.2 }), 29+ sym("M2", "Stone Totem", "mid", { 3: 9.6, 4: 24, 5: 60 }, 6, { shape: "totem", color: "#78716c", accent: "#d6d3d1", glow: 0.2 }),
30− sym("L1", "Glyph A", "low", { 3: 5, 4: 12, 5: 30 }, 9, { shape: "letter", color: "#fbbf24", label: "A" }), 30+ sym("L1", "Jaguar Paw", "low", { 3: 5, 4: 12, 5: 30 }, 9, { shape: "paw", color: "#fbbf24", accent: "#111827" }),
31− sym("L2", "Glyph K", "low", { 3: 5, 4: 12, 5: 30 }, 9, { shape: "letter", color: "#34d399", label: "K" }), 31+ sym("L2", "Tribal Shell", "low", { 3: 5, 4: 12, 5: 30 }, 9, { shape: "shell", color: "#34d399", accent: "#d1fae5" }),
32− sym("L3", "Glyph Q", "low", { 3: 3.6, 4: 9.6, 5: 24 }, 10, { shape: "letter", color: "#f97316", label: "Q" }), 32+ sym("L3", "Offering Bowl", "low", { 3: 3.6, 4: 9.6, 5: 24 }, 10, { shape: "chest", color: "#f97316", accent: "#ffedd5" }),
33− sym("L4", "Glyph J", "low", { 3: 3.6, 4: 9.6, 5: 24 }, 10, { shape: "letter", color: "#a78bfa", label: "J" }), 33+ sym("L4", "Step Pyramid", "low", { 3: 3.6, 4: 9.6, 5: 24 }, 10, { shape: "pyramid", color: "#a78bfa", accent: "#e9d5ff" }),
34 ], 34 ],
35 betDivisor: 25, 35 betDivisor: 25,
36 wild: { id: "W", moving: true }, 36 wild: { id: "W", moving: true },
modified games/src/zero-gravity/index.ts +3 −3
@@ -23,14 +23,14 @@ export const zeroGravity = defineGame({
23 wildSym("W", "Singularity Wild", [0, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5], { shape: "orb", color: "#0f0f1a", accent: "#22d3ee", glow: 1 }), 23 wildSym("W", "Singularity Wild", [0, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5], { shape: "orb", color: "#0f0f1a", accent: "#22d3ee", glow: 1 }),
24 scatterSym("S", "Gravity Well", 1.3, { shape: "ring", color: "#a78bfa", accent: "#ffffff", glow: 0.9 }), 24 scatterSym("S", "Gravity Well", 1.3, { shape: "ring", color: "#a78bfa", accent: "#ffffff", glow: 0.9 }),
25 sym("P1", "Antigrav Core", "premium", { 3: 42, 4: 112, 5: 350, 6: 700, 7: 1400 }, 2.8, { shape: "atom", color: "#22d3ee", accent: "#ffffff", glow: 0.9 }), 25 sym("P1", "Antigrav Core", "premium", { 3: 42, 4: 112, 5: 350, 6: 700, 7: 1400 }, 2.8, { shape: "atom", color: "#22d3ee", accent: "#ffffff", glow: 0.9 }),
26− sym("H1", "Orbital Station", "high", { 3: 28, 4: 70, 5: 210, 6: 400, 7: 800 }, 3.8, { shape: "hex", color: "#e5e7eb", accent: "#a78bfa", glow: 0.6 }), 26+ sym("H1", "Orbital Station", "high", { 3: 28, 4: 70, 5: 210, 6: 400, 7: 800 }, 3.8, { shape: "planet", color: "#e5e7eb", accent: "#a78bfa", glow: 0.6 }),
27− sym("H2", "Relay Satellite", "high", { 3: 21, 4: 56, 5: 140, 6: 280, 7: 560 }, 4.2, { shape: "chip", color: "#60a5fa", accent: "#dbeafe", label: "SAT", glow: 0.6 }), 27+ sym("H2", "Relay Satellite", "high", { 3: 21, 4: 56, 5: 140, 6: 280, 7: 560 }, 4.2, { shape: "rocket", color: "#60a5fa", accent: "#dbeafe", label: "SAT", glow: 0.6 }),
28 sym("M1", "Meteor", "mid", { 3: 14, 4: 35, 5: 84, 6: 150, 7: 300 }, 5.5, { shape: "flame", color: "#fb923c", accent: "#ffedd5", glow: 0.3 }), 28 sym("M1", "Meteor", "mid", { 3: 14, 4: 35, 5: 84, 6: 150, 7: 300 }, 5.5, { shape: "flame", color: "#fb923c", accent: "#ffedd5", glow: 0.3 }),
29 sym("M2", "Comet Ice", "mid", { 3: 11, 4: 28, 5: 70, 6: 120, 7: 240 }, 6, { shape: "snow", color: "#7dd3fc", accent: "#e0f2fe", glow: 0.3 }), 29 sym("M2", "Comet Ice", "mid", { 3: 11, 4: 28, 5: 70, 6: 120, 7: 240 }, 6, { shape: "snow", color: "#7dd3fc", accent: "#e0f2fe", glow: 0.3 }),
30 sym("L1", "Blue Planet", "low", { 3: 5.5, 4: 14, 5: 35, 6: 50, 7: 100 }, 9, { shape: "circle", color: "#3b82f6", glow: 0.2 }), 30 sym("L1", "Blue Planet", "low", { 3: 5.5, 4: 14, 5: 35, 6: 50, 7: 100 }, 9, { shape: "circle", color: "#3b82f6", glow: 0.2 }),
31 sym("L2", "Red Planet", "low", { 3: 5.5, 4: 14, 5: 35, 6: 50, 7: 100 }, 9, { shape: "circle", color: "#ef4444", glow: 0.2 }), 31 sym("L2", "Red Planet", "low", { 3: 5.5, 4: 14, 5: 35, 6: 50, 7: 100 }, 9, { shape: "circle", color: "#ef4444", glow: 0.2 }),
32 sym("L3", "Star Dust", "low", { 3: 4.2, 4: 11, 5: 28, 6: 40, 7: 80 }, 10, { shape: "star", color: "#fde68a", glow: 0.2 }), 32 sym("L3", "Star Dust", "low", { 3: 4.2, 4: 11, 5: 28, 6: 40, 7: 80 }, 10, { shape: "star", color: "#fde68a", glow: 0.2 }),
33− sym("L4", "Void Cube", "low", { 3: 4.2, 4: 11, 5: 28, 6: 40, 7: 80 }, 10, { shape: "cube", color: "#6b7280", accent: "#9ca3af", glow: 0.1 }), 33+ sym("L4", "Void Cube", "low", { 3: 4.2, 4: 11, 5: 28, 6: 40, 7: 80 }, 10, { shape: "satellite", color: "#6b7280", accent: "#9ca3af", glow: 0.1 }),
34 ], 34 ],
35 betDivisor: 25, 35 betDivisor: 25,
36 wild: { id: "W" }, 36 wild: { id: "W" },
modified packages/game-core/src/types.ts +51 −1
@@ -37,7 +37,57 @@ export type SymbolShape =
37 | "atom" 37 | "atom"
38 | "crown" 38 | "crown"
39 | "wave" 39 | "wave"
40− | "cube"; 40+ | "cube"
41+ // Theme-specific vocabulary (added 2026-09-08 so every game has its own symbol set).
42+ | "lock"
43+ | "key"
44+ | "code"
45+ | "badge"
46+ | "mask"
47+ | "glove"
48+ | "rope"
49+ | "bag"
50+ | "lantern"
51+ | "fan"
52+ | "scroll"
53+ | "cherry"
54+ | "seven"
55+ | "bell"
56+ | "joystick"
57+ | "torii"
58+ | "katana"
59+ | "cassette"
60+ | "ankh"
61+ | "scarab"
62+ | "pyramid"
63+ | "warning"
64+ | "valve"
65+ | "gauge"
66+ | "barrel"
67+ | "wheel"
68+ | "helmet"
69+ | "flag"
70+ | "trophy"
71+ | "idol"
72+ | "totem"
73+ | "snake"
74+ | "rocket"
75+ | "anchor"
76+ | "chest"
77+ | "trident"
78+ | "shell"
79+ | "planet"
80+ | "comet"
81+ | "satellite"
82+ | "pickaxe"
83+ | "ore"
84+ | "drill"
85+ | "ingot"
86+ | "hourglass"
87+ | "igloo"
88+ | "paw"
89+ | "fish"
90+ | "meteor";
41 91
42 export interface SymbolStyle { 92 export interface SymbolStyle {
43 shape: SymbolShape; 93 shape: SymbolShape;
44 94
Menu

Theme