SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%

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 16 days ago (Sep 8, 2026) parent f61a85f

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 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 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 670 c.addChild(g);
315 671 return c;
modified apps/web/src/components/lobby/game-art.tsx +1167 −533
@@ -1,9 +1,15 @@
1 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 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.
6 Deterministic per slug (seeded PRNG, no Math.random at render).
8 + Spinza key art — 20 hand-composed posters, one scene per game.
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 15 export type ArtVariant = "card" | "hero" | "banner" | "tile";
@@ -29,39 +35,8 @@ export interface GameArtProps {
29 35 label?: string;
30 36 }
31 37
32 /** Presentation hints per game (the public /api/games payload does not carry them). */
33 export const GAME_PRESENTATION: Record<string, { backdrop: string; particles: string }> = {
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 };
38 +/** Presentation hints per game, derived from the game library. */
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 }]));
65 40
66 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 69 W: number;
90 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 76 p: ArtPalette;
93 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;
97 const between = (rnd: () => number, a: number, b: number) => a + (b - a) * rnd();
98
99 /* ------------------------------------------------------------------ motifs */
100
101 function Vault({ W, H, rnd, p, id }: Ctx) {
102 const cx = W * 0.62;
103 const cy = H * 0.42;
104 const maxR = Math.max(W, H) * 0.55;
105 const rings: React.ReactNode[] = [];
106 for (let i = 1; i <= 7; i++) {
107 const r = (maxR / 7) * i;
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} />);
109 if (i === 3 || i === 5) {
110 for (let k = 0; k < 8; k++) {
111 const a = (Math.PI * 2 * k) / 8 + rnd() * 0.2;
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} />);
113 }
114 }
87 +const DIMS: Record<ArtVariant, { w: number; h: number }> = {
88 + card: { w: 360, h: 480 },
89 + tile: { w: 240, h: 240 },
90 + hero: { w: 1680, h: 720 },
91 + banner: { w: 1600, h: 600 },
92 +};
93 +
94 +function focal(v: ArtVariant, W: number, H: number, showName: boolean): { cx: number; cy: number; R: number } {
95 + switch (v) {
96 + case "card":
97 + return { cx: W / 2, cy: showName ? H * 0.41 : H * 0.5, R: W * 0.42 };
98 + case "tile":
99 + return { cx: W / 2, cy: H / 2, R: W * 0.4 };
100 + case "hero":
101 + return { cx: W * 0.7, cy: H * 0.5, R: H * 0.42 };
102 + case "banner":
103 + return { cx: W * 0.72, cy: H * 0.5, R: H * 0.4 };
115 104 }
116 const step = W / 8;
117 const grid: React.ReactNode[] = [];
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} />);
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} />);
105 +}
106 +
107 +/* ---------------------------------------------------------------- helpers */
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 130 return (
121 <g>
122 {grid}
123 {rings}
124 <circle cx={cx} cy={cy} r={r2(maxR / 14)} fill={`url(#${id}-glow)`} />
125 <circle cx={cx} cy={cy} r={r2(maxR / 28)} fill={p.glow} fillOpacity={0.9} />
131 + <g opacity={opacity}>
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} />
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" }}>
134 + {text}
135 + </text>
126 136 </g>
127 137 );
128 138 }
129 139
130 function Nebula({ W, H, rnd, p, id }: Ctx) {
131 const blobs: React.ReactNode[] = [];
132 for (let i = 0; i < 5; i++) {
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)`} />);
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 }) {
141 + return (
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}>
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[] = [];
136 for (let i = 0; i < 60; i++) {
137 const big = rnd() > 0.85;
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))} />);
186 + let ticks = "";
187 + for (let i = 0; i < 12; i++) {
188 + const a = (i / 12) * PI * 2;
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 234 return (
141 235 <g>
142 {blobs}
143 {stars}
236 + <defs>
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 265 </g>
145 266 );
146 267 }
147 268
148 function Pillars({ W, H, rnd, p, id }: Ctx) {
149 const n = W > H ? 9 : 5;
150 const gap = W / n;
151 const cols: React.ReactNode[] = [];
152 for (let i = 0; i < n; i++) {
153 const cw = gap * 0.42;
154 const x = gap * i + (gap - cw) / 2;
155 const top = H * between(rnd, 0.12, 0.22);
156 cols.push(
157 <g key={i}>
158 <rect x={r2(x)} y={r2(top)} width={r2(cw)} height={r2(H - top)} fill={`url(#${id}-col)`} />
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} />
160 <rect x={r2(x + cw * 0.3)} y={r2(top)} width={2} height={r2(H - top)} fill={p.glow} fillOpacity={0.12} />
161 </g>,
162 );
163 }
269 +/* 03 — GOLDEN EMPEROR: symmetrical imperial court, gold pillars, dragon pearls ×2 ×3 ×5 */
270 +function GoldenEmperor(s: Stage) {
271 + const { H, cx, cy, R, id, p, compact } = s;
272 + const pw = R * 0.22;
273 + const px1 = cx - R * 1.02;
274 + const px2 = cx + R * 1.02 - pw;
275 + const top = cy - R * 1.15;
276 + const flecks = scatterDots(s, 30, [cx - R * 1.2, cy - R * 1.2, R * 2.4, R * 2.4], 0.8, 2.2);
277 + const pearls = [
278 + { x: cx - R * 0.66, m: "×2" },
279 + { x: cx, m: "×3" },
280 + { x: cx + R * 0.66, m: "×5" },
281 + ];
282 + const py = cy + R * 0.62;
164 283 return (
165 284 <g>
166 <circle cx={W * 0.5} cy={H * 0.3} r={Math.min(W, H) * 0.22} fill={`url(#${id}-glow)`} />
167 {cols}
168 <rect x={0} y={H * 0.88} width={W} height={H * 0.12} fill={p.secondary} fillOpacity={0.25} />
285 + <defs>
286 + <Radial id={`${id}-ge1`} color={p.secondary} o0={0.65} o1={0} />
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 312 </g>
170 313 );
171 314 }
172 315
173 function Aurora({ W, H, rnd, p, id }: Ctx) {
174 const bands: React.ReactNode[] = [];
175 for (let i = 0; i < 4; i++) {
176 const y = H * (0.2 + i * 0.14);
177 const amp = H * between(rnd, 0.04, 0.1);
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)}`;
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)`} />);
316 +/* 04 — DIAMOND HEIST: laser-cut vault door, diamond coins with SC values, jackpot plaques */
317 +function DiamondHeist(s: Stage) {
318 + const { W, H, cx, cy, R, id, p, compact } = s;
319 + let spokes = "";
320 + for (let i = 0; i < 24; i++) {
321 + const a = (i / 24) * PI * 2;
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[] = [];
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))} />);
183 const peaks: number[] = [];
184 for (let x = 0; x <= W; x += W / 9) peaks.push(H * between(rnd, 0.72, 0.88));
185 const ridge = `M0 ${H} ` + peaks.map((y, i) => `L${r2((i * W) / 9)} ${r2(y)}`).join(" ") + ` L${W} ${H} Z`;
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`;
330 + let sparkles = "";
331 + for (let i = 0; i < 14; i++) sparkles += sparkle(s.rnd() * W, s.rnd() * H, R * (0.03 + s.rnd() * 0.05));
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)}`;
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 394 return (
187 395 <g>
188 {stars}
189 {bands}
190 <path d={ridge} fill={p.bg} fillOpacity={0.95} />
191 <path d={ridge} fill={p.primary} fillOpacity={0.08} />
396 + <defs>
397 + <Linear id={`${id}-af1`} stops={[[0, "#0b2a3a"], [0.6, p.bg], [1, "#02060d"]]} />
398 + <filter id={`${id}-blur`} x="-20%" y="-50%" width="140%" height="200%">
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 425 </g>
193 426 );
194 427 }
195 428
196 function Lava({ W, H, rnd, p, id }: Ctx) {
197 const cracks: React.ReactNode[] = [];
429 +/* 06 — INFERNO REELS: cracked lava floor, Magma Core shockwave, embers */
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 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 436 let y = H;
201 const pts = [`M${r2(x)} ${r2(y)}`];
202 while (y > H * 0.15) {
203 x += between(rnd, -W * 0.08, W * 0.08);
204 y -= between(rnd, H * 0.06, H * 0.14);
205 pts.push(`L${r2(x)} ${r2(y)}`);
437 + cracks += `M${f(x)} ${f(y)}`;
438 + while (y > floorY + R * 0.05) {
439 + x += (s.rnd() - 0.5) * R * 0.3;
440 + y -= R * (0.1 + s.rnd() * 0.15);
441 + cracks += `L${f(x)} ${f(y)}`;
206 442 }
207 const d = pts.join(" ");
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" />);
443 + cracks += " ";
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 448 return (
212 449 <g>
213 <rect x={0} y={H * 0.6} width={W} height={H * 0.4} fill={`url(#${id}-heat)`} />
214 {cracks}
450 + <defs>
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 480 </g>
216 481 );
217 482 }
218 483
219 function Circuit({ W, H, rnd, p }: Ctx) {
220 const traces: React.ReactNode[] = [];
221 const step = Math.min(W, H) / 10;
222 for (let i = 0; i < 16; i++) {
223 let x = Math.round(between(rnd, 0, W / step)) * step;
224 let y = Math.round(between(rnd, 0, H / step)) * step;
225 const pts = [`M${r2(x)} ${r2(y)}`];
226 const segs = 2 + Math.floor(rnd() * 3);
227 for (let s = 0; s < segs; s++) {
228 const horiz = rnd() > 0.5;
229 const len = step * (1 + Math.floor(rnd() * 3));
230 if (horiz) x += rnd() > 0.5 ? len : -len;
231 else y += rnd() > 0.5 ? len : -len;
232 pts.push(`L${r2(x)} ${r2(y)}`);
484 +/* 07 — QUANTUM JACKPOT: a processor splitting into ghost copies ×2 ×3 ×4 over circuit traces */
485 +function QuantumJackpot(s: Stage) {
486 + const { W, H, cx, cy, R, id, p, compact } = s;
487 + const step = R * 0.22;
488 + let t1 = "";
489 + let t2 = "";
490 + const pads: Array<[number, number, number]> = [];
491 + for (let i = 0; i < 14; i++) {
492 + let x = Math.round((s.rnd() * W) / step) * step;
493 + let y = Math.round((s.rnd() * H) / step) * step;
494 + let d = `M${f(x)} ${f(y)}`;
495 + for (let k = 0; k < 3; k++) {
496 + if (s.rnd() > 0.5) x += (s.rnd() > 0.5 ? 1 : -1) * step * (1 + Math.floor(s.rnd() * 3));
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;
235 traces.push(<path key={`t${i}`} d={pts.join(" ")} fill="none" stroke={color} strokeOpacity={0.35} strokeWidth={2} strokeLinejoin="round" />);
236 traces.push(<circle key={`p${i}`} cx={r2(x)} cy={r2(y)} r={4} fill={p.bg} stroke={color} strokeWidth={2} />);
500 + if (i % 3 === 0) t2 += d + " ";
501 + else t1 += d + " ";
502 + pads.push([x, y, 3]);
237 503 }
238 return <g>{traces}</g>;
239 }
240
241 function Skyline({ W, H, rnd, p, id }: Ctx) {
242 const buildings: React.ReactNode[] = [];
243 let x = -10;
244 let i = 0;
245 while (x < W + 10) {
246 const bw = between(rnd, W * 0.05, W * 0.14);
247 const bh = H * between(rnd, 0.25, 0.7);
248 const y = H - bh;
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} />);
250 const cols = Math.max(1, Math.floor(bw / 14));
251 const rows = Math.floor(bh / 18);
252 for (let c = 0; c < cols; c++) {
253 for (let r = 0; r < rows; r++) {
254 if (rnd() > 0.45) continue;
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))} />);
256 }
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(" ")}`;
505 + const gx = cx - R * 0.42;
506 + return (
507 + <g>
508 + <defs>
509 + <Radial id={`${id}-qj1`} color={p.secondary} o0={0.35} />
510 + <Radial id={`${id}-qj2`} color={p.primary} o0={0.35} />
511 + </defs>
512 + <circle cx={f(cx + R * 0.6)} cy={f(cy - R * 0.5)} r={f(R * 1.3)} fill={`url(#${id}-qj1)`} />
513 + <circle cx={f(cx - R * 0.8)} cy={f(cy + R * 0.7)} r={f(R * 1.1)} fill={`url(#${id}-qj2)`} />
514 + <path d={t1} stroke={p.primary} strokeOpacity={0.3} strokeWidth={1.6} fill="none" strokeLinejoin="round" />
515 + <path d={t2} stroke={p.secondary} strokeOpacity={0.35} strokeWidth={1.6} fill="none" strokeLinejoin="round" />
516 + <path d={dots(pads)} fill={p.bg} stroke={p.primary} strokeOpacity={0.6} strokeWidth={1.5} />
517 + <path d={wave} stroke={p.primary} strokeOpacity={0.55} strokeWidth={1.5} fill="none" />
518 + {!compact ? (
519 + <>
520 + <Sym s={s} id="P1" x={gx + R * 1.15} y={cy - R * 0.12} size={R * 0.7} opacity={0.22} />
521 + <Sym s={s} id="P1" x={gx + R * 0.78} y={cy - R * 0.08} size={R * 0.74} opacity={0.42} />
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} />);
259 x += bw + between(rnd, 4, 14);
260 i++;
556 + x += bw + W * 0.008;
557 + }
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 567 return (
263 568 <g>
264 <circle cx={W * 0.72} cy={H * 0.28} r={Math.min(W, H) * 0.14} fill={`url(#${id}-glow)`} />
265 <circle cx={W * 0.72} cy={H * 0.28} r={Math.min(W, H) * 0.07} fill={p.primary} fillOpacity={0.55} />
266 {buildings}
267 <rect x={0} y={H * 0.995} width={W} height={H * 0.005} fill={p.secondary} />
569 + <defs>
570 + <Linear id={`${id}-mt1`} stops={[[0, "#1a0630"], [0.55, p.bg], [1, "#3a0a3a"]]} />
571 + <Linear id={`${id}-mt2`} stops={[[0, p.primary, 0.35], [1, p.primary, 0]]} />
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 599 </g>
269 600 );
270 601 }
271 602
272 function Pyramid({ W, H, rnd, p, id }: Ctx) {
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} />;
274 const rays: React.ReactNode[] = [];
275 for (let i = 0; i < 14; i++) {
276 const a = -Math.PI * 0.95 + (i / 13) * Math.PI * 0.9;
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} />);
603 +/* 09 — PHARAOH PROTOCOL: pyramid with a scanning laser, artifact meter */
604 +function PharaohProtocol(s: Stage) {
605 + const { W, H, cx, cy, R, id, p, compact } = s;
606 + const base = cy + R * 0.85;
607 + const apex = cy - R * 0.85;
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 625 return (
280 626 <g>
281 {rays}
282 <circle cx={W * 0.5} cy={H * 0.36} r={Math.min(W, H) * 0.22} fill={`url(#${id}-glow)`} />
283 <circle cx={W * 0.5} cy={H * 0.36} r={Math.min(W, H) * 0.1} fill={p.primary} fillOpacity={0.9} />
284 {tri(W * 0.2, W * 0.5, H * 0.34, "l", "#120c05", 1)}
285 {tri(W * 0.82, W * 0.55, H * 0.4, "r", "#0f0a04", 1)}
286 {tri(W * 0.5, W * 0.72, H * 0.56, "c", "#1a1208", 1)}
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} />
288 <rect x={0} y={H * 0.86} width={W} height={H * 0.14} fill={p.secondary} fillOpacity={0.1} />
627 + <defs>
628 + <Linear id={`${id}-pp1`} stops={[[0, "#2a1608"], [0.45, p.bg], [1, "#1a1006"]]} />
629 + <Linear id={`${id}-pp2`} stops={[[0, "#8a5a1a"], [0.5, "#f1c86a"], [1, "#5a3a10"]]} dir="h" />
630 + <Radial id={`${id}-pp3`} color="#fb923c" o0={0.7} mid={[0.4, p.primary, 0.3]} />
631 + <clipPath id={`${id}-pp4`}>
632 + <polygon points={tri} />
633 + </clipPath>
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 659 </g>
290 660 );
291 661 }
292 662
293 function Arcade({ W, H, rnd, p }: Ctx) {
294 const size = Math.min(W, H) / 12;
295 const cells: React.ReactNode[] = [];
296 const colors = [p.primary, p.secondary, p.glow];
297 for (let y = 0; y < H; y += size) {
298 for (let x = 0; x < W; x += size) {
299 const r = rnd();
300 if (r > 0.22) continue;
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))} />);
663 +/* 10 — LUCKY CIRCUIT: retro arcade bezel, mystery "?" cartridge, confetti */
664 +function LuckyCircuit(s: Stage) {
665 + const { W, H, cx, cy, R, id, p, compact } = s;
666 + const bw = R * 2.2;
667 + const bh = R * 2.05;
668 + const bx = cx - bw / 2;
669 + const by = cy - bh / 2 - R * 0.05;
670 + let scan = "";
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 682 return (
305 683 <g>
306 {cells}
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} />
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} />
684 + <defs>
685 + <pattern id={`${id}-lc0`} width="14" height="14" patternUnits="userSpaceOnUse">
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 710 </g>
310 711 );
311 712 }
312 713
313 function Asteroids({ W, H, rnd, p, id }: Ctx) {
314 const rocks: React.ReactNode[] = [];
315 for (let i = 0; i < 9; i++) {
316 const cx = between(rnd, 0, W);
317 const cy = between(rnd, 0, H * 0.8);
318 const rad = between(rnd, Math.min(W, H) * 0.03, Math.min(W, H) * 0.14);
319 const n = 7 + Math.floor(rnd() * 4);
320 const pts: string[] = [];
714 +/* 11 — VOID MINER: asteroid field, dark-matter core with level meter, drill beam */
715 +function VoidMiner(s: Stage) {
716 + const { W, H, cx, cy, R, id, p, compact } = s;
717 + const stars = scatterDots(s, 60, [0, 0, W, H], 0.5, 1.3);
718 + const rock = (x: number, y: number, r: number, n: number) => {
719 + const list: string[] = [];
321 720 for (let k = 0; k < n; k++) {
322 const a = (k / n) * Math.PI * 2;
323 const rr = rad * between(rnd, 0.72, 1.1);
324 pts.push(`${r2(cx + Math.cos(a) * rr)},${r2(cy + Math.sin(a) * rr)}`);
721 + const a = (k / n) * PI * 2;
722 + const rr = r * (0.72 + s.rnd() * 0.36);
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" />);
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} />);
328 }
329 const stars: React.ReactNode[] = [];
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))} />);
725 + return list.join(" ");
726 + };
727 + const rocks = [
728 + [cx - R * 1.0, cy - R * 0.95, R * 0.28, 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 738 return (
332 739 <g>
333 {stars}
334 <circle cx={W * 0.5} cy={H * 0.45} r={Math.min(W, H) * 0.2} fill={`url(#${id}-glow)`} />
335 {rocks}
740 + <defs>
741 + <Radial id={`${id}-vm1`} color={p.primary} o0={0.5} />
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 764 </g>
337 765 );
338 766 }
339 767
340 function Track({ W, H, p }: Ctx) {
341 const lanes: React.ReactNode[] = [];
342 const cx = W * 1.05;
343 const cy = H * 0.55;
344 for (let i = 0; i < 6; i++) {
345 const r = Math.max(W, H) * (0.28 + i * 0.13);
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} />);
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" />);
348 }
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} />);
768 +/* 12 — ROYAL CIRCUIT: race-track sweep, lap counter, turbo boost ×2/×3 */
769 +function RoyalCircuit(s: Stage) {
770 + const { W, cx, cy, R, id, p, compact } = s;
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)}`;
772 + let streaks = "";
773 + for (let i = 0; i < 9; i++) {
774 + const y = cy - R * 1.1 + s.rnd() * R * 2.2;
775 + const x0 = s.rnd() * W * 0.5;
776 + streaks += `M${f(x0)} ${f(y)}h${f(R * (0.4 + s.rnd() * 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 783 return (
356 784 <g>
357 {lanes}
358 {kerb}
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" />
785 + <defs>
786 + <pattern id={`${id}-rc0`} width={f(R * 0.16)} height={f(R * 0.16)} patternUnits="userSpaceOnUse">
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 815 </g>
361 816 );
362 817 }
363 818
364 function hexPoints(cx: number, cy: number, r: number, rot = 0): string {
365 const pts: string[] = [];
366 for (let k = 0; k < 6; k++) {
367 const a = rot + (k / 6) * Math.PI * 2;
368 pts.push(`${r2(cx + Math.cos(a) * r)},${r2(cy + Math.sin(a) * r)}`);
369 }
370 return pts.join(" ");
371 }
372
373 function Reactor({ W, H, rnd, p, id }: Ctx) {
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))} />);
819 +/* 13 — DRAGON CORE: reactor containment with a dragon charge gauge 0/20 */
820 +function DragonCore(s: Stage) {
821 + const { cx, cy, R, id, p, compact } = s;
822 + let ticks = "";
823 + let lit = "";
824 + for (let i = 0; i < 20; i++) {
825 + const a = -PI * 1.25 + (i / 19) * PI * 1.5;
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)} `;
827 + if (i < 13) lit += d;
828 + else ticks += d;
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 836 return (
392 837 <g>
393 {cells}
394 {spokes}
395 <circle cx={cx} cy={cy} r={base * 1.6} fill={`url(#${id}-glow)`} />
396 {rings}
397 <polygon points={hexPoints(cx, cy, base * 0.55, Math.PI / 6)} fill={p.glow} fillOpacity={0.9} />
838 + <defs>
839 + <Radial id={`${id}-dc1`} color={p.primary} o0={0.55} mid={[0.5, p.glow, 0.2]} />
840 + <Radial id={`${id}-dc2`} color={p.glow} o0={0.9} mid={[0.5, p.primary, 0.5]} />
841 + </defs>
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 861 </g>
399 862 );
400 863 }
401 864
402 function Abyss({ W, H, rnd, p, id }: Ctx) {
403 const bubbles: React.ReactNode[] = [];
404 for (let i = 0; i < 26; i++) {
405 const r = between(rnd, 2, Math.min(W, H) * 0.035);
406 const cx = rnd() * W;
407 const cy = between(rnd, H * 0.2, H);
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} />);
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} />);
410 }
411 const rays: React.ReactNode[] = [];
412 for (let i = 0; i < 5; i++) {
413 const x = W * (0.2 + i * 0.15);
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)`} />);
415 }
865 +/* 14 — DEEP TREASURE: descending depth levels with treasure chests, bubbles */
866 +function DeepTreasure(s: Stage) {
867 + const { W, H, cx, cy, R, id, p, compact } = s;
868 + const bubbles: Array<[number, number, number]> = [];
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]);
870 + const levels = ["×1", "×2", "×3", "×4", "×6", "×8", "×10"];
871 + const top = cy - R * 1.05;
872 + const lh = R * 0.36;
873 + const chest = (x: number, y: number, w: number, key: string) => (
874 + <g key={key}>
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} />
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} />
877 + <circle cx={f(x)} cy={f(y + w * 0.02)} r={f(w * 0.08)} fill="#fde68a" />
878 + </g>
879 + );
416 880 return (
417 881 <g>
418 {rays}
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)`} />
420 {bubbles}
882 + <defs>
883 + <Linear id={`${id}-dt1`} stops={[[0, "#0a5a6e"], [0.35, "#063a4c"], [1, "#010610"]]} />
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 910 </g>
422 911 );
423 912 }
424 913
425 function Moon({ W, H, rnd, p, id }: Ctx) {
426 const R = Math.min(W, H) * 0.42;
427 const cx = W * 0.62;
428 const cy = H * 0.5;
429 const craters: React.ReactNode[] = [];
430 for (let i = 0; i < 12; i++) {
431 const a = rnd() * Math.PI * 2;
432 const d = rnd() * R * 0.8;
433 const r = between(rnd, R * 0.04, R * 0.16);
434 const x = cx + Math.cos(a) * d;
435 const y = cy + Math.sin(a) * d;
436 craters.push(<circle key={`c${i}`} cx={r2(x)} cy={r2(y)} r={r2(r)} fill="#000" fillOpacity={0.35} />);
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} />);
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} />);
439 }
440 const stars: React.ReactNode[] = [];
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))} />);
914 +/* 15 — MOONBASE 77: retro lunar base, moon horizon, drifting wilds */
915 +function Moonbase77(s: Stage) {
916 + const { W, H, cx, cy, R, id, p, compact } = s;
917 + const stars = scatterDots(s, 70, [0, 0, W, H * 0.8], 0.5, 1.4);
918 + const mR = R * 3.2;
919 + const mY = cy + R * 0.95 + mR;
920 + const craters: Array<[number, number, number]> = [];
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)]);
922 + const domeX = cx - R * 0.55;
923 + const domeY = cy + R * 0.95;
924 + const domeR = R * 0.55;
925 + let trail = "";
926 + for (const [x, y] of [
927 + [cx - R * 0.92, cy - R * 0.72],
928 + [cx - R * 0.15, cy - R * 0.9],
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)} `;
442 930 return (
443 931 <g>
444 {stars}
445 <circle cx={cx} cy={cy} r={R * 1.15} fill={`url(#${id}-glow)`} />
446 <circle cx={cx} cy={cy} r={R} fill={`url(#${id}-moon)`} />
447 {craters}
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})`} />
449 <line x1={0} y1={H * 0.82} x2={W} y2={H * 0.82} stroke={p.primary} strokeOpacity={0.5} strokeWidth={1.5} />
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} />
451 <rect x={W * 0.13} y={H * 0.75} width={W * 0.04} height={H * 0.03} fill={p.primary} fillOpacity={0.9} />
932 + <defs>
933 + <Radial id={`${id}-mb1`} color="#3a3d4a" o0={1} o1={1} mid={[0.6, "#232633", 1]} />
934 + <Radial id={`${id}-mb2`} color="#60a5fa" o0={1} mid={[0.55, "#1e3a8a", 1]} o1={1} />
935 + <Linear id={`${id}-mb3`} stops={[[0, "#2b2f3d"], [1, "#151824"]]} />
936 + </defs>
937 + <path d={stars} fill="#fff" fillOpacity={0.8} />
938 + <circle cx={f(cx + R * 0.95)} cy={f(cy - R * 1.05)} r={f(R * 0.16)} fill={`url(#${id}-mb2)`} />
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 958 </g>
453 959 );
454 960 }
455 961
456 function Temple({ W, H, rnd, p, id }: Ctx) {
457 const steps: React.ReactNode[] = [];
458 const n = 6;
962 +/* 16 — WILD TEMPLE: stepped stone chambers, jaguar wild with moving arrows, torches */
963 +function WildTemple(s: Stage) {
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 969 for (let i = 0; i < n; i++) {
460 const wdt = W * (0.95 - i * 0.14);
461 const h = H * 0.11;
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} />);
970 + const w = R * (2.7 - i * 0.48);
971 + steps += `M${f(cx - w / 2)} ${f(baseY - sh * (i + 1))}h${f(w)}v${f(sh)}h${f(-w)}z `;
466 972 }
467 const vines: React.ReactNode[] = [];
468 for (let i = 0; i < 8; i++) {
469 const x = between(rnd, W * 0.05, W * 0.95);
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" />);
973 + let joints = "";
974 + for (let i = 0; i < n; i++) {
975 + const w = R * (2.7 - i * 0.48);
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 984 return (
473 985 <g>
474 <circle cx={W * 0.5} cy={H * 0.3} r={Math.min(W, H) * 0.24} fill={`url(#${id}-glow)`} />
475 <circle cx={W * 0.5} cy={H * 0.3} r={Math.min(W, H) * 0.08} fill={p.secondary} fillOpacity={0.85} />
476 {steps}
477 {vines}
986 + <defs>
987 + <Radial id={`${id}-wt1`} color={p.secondary} o0={0.8} mid={[0.4, p.secondary, 0.25]} />
988 + <Linear id={`${id}-wt2`} stops={[[0, p.bg, 0], [1, "#000", 0.8]]} />
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 1014 </g>
479 1015 );
480 1016 }
481 1017
482 function Gravity({ W, H, rnd, p, id }: Ctx) {
483 const cx = W * 0.5;
484 const cy = H * 0.45;
485 const orbits: React.ReactNode[] = [];
486 for (let i = 0; i < 5; i++) {
487 const rx = Math.min(W, H) * (0.22 + i * 0.1);
488 const ry = rx * between(rnd, 0.3, 0.5);
489 const rot = between(rnd, -60, 60);
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})`} />);
491 const a = rnd() * Math.PI * 2;
492 const px = cx + Math.cos(a) * rx;
493 const py = cy + Math.sin(a) * ry;
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})`} />);
495 }
496 const stars: React.ReactNode[] = [];
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))} />);
1018 +/* 17 — ZERO GRAVITY: three grids 5×3 → 6×4 → 7×5 floating in orbit */
1019 +function ZeroGravity(s: Stage) {
1020 + const { W, H, cx, cy, R, id, p, compact } = s;
1021 + const stars = scatterDots(s, 70, [0, 0, W, H], 0.5, 1.4);
1022 + const grid = (x: number, y: number, cols: number, rows: number, cell: number, op: number, key: string, label: string, skew: number) => {
1023 + const w = cols * cell;
1024 + const h = rows * cell;
1025 + let lines = "";
1026 + for (let c = 1; c < cols; c++) lines += `M${f(x + c * cell)} ${f(y)}v${f(h)} `;
1027 + for (let r = 1; r < rows; r++) lines += `M${f(x)} ${f(y + r * cell)}h${f(w)} `;
1028 + return (
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))})`}>
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} />
1031 + <path d={lines} stroke={p.primary} strokeOpacity={0.55} strokeWidth={1} />
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" />
1033 + </g>
1034 + );
1035 + };
1036 + const c1 = R * 0.13;
1037 + const c2 = R * 0.155;
1038 + const c3 = R * 0.19;
498 1039 return (
499 1040 <g>
500 {stars}
501 <circle cx={cx} cy={cy} r={Math.min(W, H) * 0.16} fill={`url(#${id}-glow)`} />
502 <circle cx={cx} cy={cy} r={Math.min(W, H) * 0.045} fill="#fff" fillOpacity={0.95} />
503 {orbits}
1041 + <defs>
1042 + <Radial id={`${id}-zg1`} color={p.secondary} o0={0.35} />
1043 + </defs>
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 1063 </g>
505 1064 );
506 1065 }
507 1066
508 function Core({ W, H, rnd, p, id }: Ctx) {
509 const cx = W * 0.5;
510 const cy = H * 0.44;
511 const R = Math.min(W, H) * 0.22;
512 const arcs: React.ReactNode[] = [];
513 for (let i = 0; i < 3; i++) {
514 const r = R * (1.5 + i * 0.35);
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})`} />);
516 }
517 const embers: React.ReactNode[] = [];
518 for (let i = 0; i < 30; i++) {
519 const a = rnd() * Math.PI * 2;
520 const d = R * between(rnd, 1.1, 3.2);
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))} />);
522 }
1067 +/* 18 — REEL REACTOR: heat gauge 0–100 with meltdown zone, hazard stripes */
1068 +function ReelReactor(s: Stage) {
1069 + const { W, cx, cy, R, id, p, compact } = s;
1070 + const gR = R * 1.0;
1071 + const a0 = PI * 0.78;
1072 + const a1 = PI * 2.22;
1073 + const at = (t: number) => a0 + (a1 - a0) * t;
1074 + const needle = at(0.72);
1075 + let hazard = "";
1076 + const hy = cy + R * 1.08;
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 `;
1078 + const tick = (t: number, label: string, color: string) => {
1079 + const a = at(t);
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" />;
1081 + };
523 1082 return (
524 1083 <g>
525 {embers}
526 <circle cx={cx} cy={cy} r={R * 2.2} fill={`url(#${id}-glow)`} />
527 {arcs}
528 <circle cx={cx} cy={cy} r={R} fill={`url(#${id}-sphere)`} />
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)`} />
1084 + <defs>
1085 + <Radial id={`${id}-rr1`} color={p.glow} o0={0.55} />
1086 + </defs>
1087 + <circle cx={f(cx)} cy={f(cy)} r={f(R * 1.3)} fill={`url(#${id}-rr1)`} />
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 1108 </g>
531 1109 );
532 1110 }
533 1111
534 function Minimal({ W, H, p }: Ctx) {
535 const y = H * 0.5;
1112 +/* 19 — OBSIDIAN: pure black, one thin gold line, one white-gold wild, a whisper of 50,000× */
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 1117 return (
537 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} />
539 <line x1={W * 0.1} y1={y} x2={W * 0.9} y2={y} stroke={p.secondary} strokeWidth={6} strokeOpacity={0.15} />
540 <circle cx={W * 0.9} cy={y} r={3} fill={p.primary} />
541 <circle cx={W * 0.1} cy={y} r={1.5} fill={p.secondary} />
542 <line x1={W * 0.5} y1={H * 0.3} x2={W * 0.5} y2={H * 0.34} stroke={p.secondary} strokeOpacity={0.5} />
543 <line x1={W * 0.5} y1={H * 0.66} x2={W * 0.5} y2={H * 0.7} stroke={p.secondary} strokeOpacity={0.5} />
1119 + <rect width={W} height={H} fill="#000" />
1120 + <path d={dust} fill={p.secondary} fillOpacity={0.4} />
1121 + <polygon points={svgPolygon(cx, cy - R * 0.05, R * 0.78, 6)} fill="#050505" stroke={p.primary} strokeOpacity={0.14} strokeWidth={1} />
1122 + <Sym s={s} id="W" x={cx} y={cy - R * 0.05} size={R * 0.72} plate={false} halo={false} />
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} />
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 1126 </g>
545 1127 );
546 1128 }
547 1129
548 function Universe({ W, H, rnd, p, id }: Ctx) {
549 const cx = W * 0.5;
550 const cy = H * 0.44;
551 const R = Math.min(W, H) * 0.3;
552 const rays: React.ReactNode[] = [];
553 for (let i = 0; i < 48; i++) {
554 const a = (i / 48) * Math.PI * 2;
555 const len = R * between(rnd, 1.2, 2.6);
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" />);
1130 +/* 20 — SPINZA ORIGINAL: the Spinza ring mark, Mystery Orb, Mega Meter, cascading symbols */
1131 +function SpinzaOriginal(s: Stage) {
1132 + const { W, H, cx, cy, R, id, p, compact } = s;
1133 + const stars = scatterDots(s, 60, [0, 0, W, H], 0.5, 1.4);
1134 + let rays = "";
1135 + for (let i = 0; i < 28; i++) {
1136 + const a = (i / 28) * PI * 2;
1137 + const len = R * (1.15 + s.rnd() * 0.7);
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[] = [];
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))} />);
1140 + const mR = R * 0.78;
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 1145 return (
561 1146 <g>
562 {stars}
563 <circle cx={cx} cy={cy} r={R * 2.4} fill={`url(#${id}-glow)`} />
564 {rays}
565 <circle cx={cx} cy={cy} r={R} fill="none" stroke={`url(#${id}-ring)`} strokeWidth={Math.max(3, R * 0.06)} />
566 <circle cx={cx} cy={cy} r={R * 0.82} fill="none" stroke={p.secondary} strokeOpacity={0.35} strokeWidth={1.5} strokeDasharray="6 10" />
567 <circle cx={cx} cy={cy} r={R * 0.12} fill="#fff" />
1147 + <defs>
1148 + <Radial id={`${id}-so1`} color={p.primary} o0={0.6} mid={[0.5, p.glow, 0.18]} />
1149 + <Linear id={`${id}-so2`} stops={[[0, p.primary], [0.5, "#fff"], [1, p.secondary]]} dir="d" />
1150 + </defs>
1151 + <path d={stars} fill="#fff" fillOpacity={0.75} />
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 1172 </g>
569 1173 );
570 1174 }
571 1175
572 function PerspectiveGrid({ W, H, p }: Ctx) {
573 const horizon = H * 0.42;
574 const lines: React.ReactNode[] = [];
575 for (let i = -8; i <= 8; i++) {
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 }
1176 +/* Fallback for unknown slugs: palette glow + the game's top symbols if known. */
1177 +function Generic(s: Stage) {
1178 + const { W, H, cx, cy, R, id, p, compact } = s;
1179 + const stars = scatterDots(s, 40, [0, 0, W, H], 0.5, 1.4);
583 1180 return (
584 1181 <g>
585 <circle cx={W * 0.5} cy={horizon} r={Math.min(W, H) * 0.2} fill={p.secondary} fillOpacity={0.25} />
586 <rect x={0} y={horizon} width={W} height={H - horizon} fill="#000" fillOpacity={0.35} />
587 {lines}
588 <line x1={0} y1={horizon} x2={W} y2={horizon} stroke={p.secondary} strokeOpacity={0.8} strokeWidth={2} />
1182 + <defs>
1183 + <Radial id={`${id}-gn1`} color={p.primary} o0={0.5} />
1184 + </defs>
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 1195 </g>
590 1196 );
591 1197 }
592 1198
593 const MOTIFS: Record<Backdrop, (c: Ctx) => React.ReactNode> = {
594 vault: Vault,
595 nebula: Nebula,
596 pillars: Pillars,
597 aurora: Aurora,
598 lava: Lava,
599 circuit: Circuit,
600 skyline: Skyline,
601 pyramid: Pyramid,
602 arcade: Arcade,
603 asteroids: Asteroids,
604 track: Track,
605 reactor: Reactor,
606 abyss: Abyss,
607 moon: Moon,
608 temple: Temple,
609 gravity: Gravity,
610 core: Core,
611 minimal: Minimal,
612 universe: Universe,
613 grid: PerspectiveGrid,
1199 +const SCENES: Record<string, (s: Stage) => React.ReactNode> = {
1200 + "neon-vault": NeonVault,
1201 + "cosmic-collapse": CosmicCollapse,
1202 + "golden-emperor": GoldenEmperor,
1203 + "diamond-heist": DiamondHeist,
1204 + "arctic-fortune": ArcticFortune,
1205 + "inferno-reels": InfernoReels,
1206 + "quantum-jackpot": QuantumJackpot,
1207 + "midnight-tokyo": MidnightTokyo,
1208 + "pharaoh-protocol": PharaohProtocol,
1209 + "lucky-circuit": LuckyCircuit,
1210 + "void-miner": VoidMiner,
1211 + "royal-circuit": RoyalCircuit,
1212 + "dragon-core": DragonCore,
1213 + "deep-treasure": DeepTreasure,
1214 + "moonbase-77": Moonbase77,
1215 + "wild-temple": WildTemple,
1216 + "zero-gravity": ZeroGravity,
1217 + "reel-reactor": ReelReactor,
1218 + obsidian: Obsidian,
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 }) {
619 const out: React.ReactNode[] = [];
620 const n = 22;
621 for (let i = 0; i < n; i++) {
622 const x = r2(rnd() * W);
623 const y = r2(rnd() * H);
624 const op = r2(between(rnd, 0.25, 0.8));
625 const s = between(rnd, 1.5, 4);
626 switch (kind) {
627 case "snow":
628 out.push(<circle key={i} cx={x} cy={y} r={r2(s * 0.7)} fill="#fff" fillOpacity={op} />);
629 break;
630 case "coins":
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} />);
632 break;
633 case "diamonds":
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} />);
635 break;
636 case "fire":
637 case "sparks":
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" />);
639 break;
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>;
1224 +interface TitleSpec {
1225 + caps?: boolean;
1226 + /** Letter spacing in em. */
1227 + tracking?: number;
1228 + weight?: 700 | 800;
1229 + /** Solid fill colour or a [from, to] gradient. */
1230 + fill?: string | [string, string];
1231 + glow?: string;
1232 + eyebrow?: string;
1233 + eyebrowColor?: string;
1234 + align?: "left" | "center";
1235 + /** Force two lines on the card (long names). */
1236 + split?: boolean;
1237 + /** Second line rendered small with wide tracking (e.g. PHARAOH / protocol). */
1238 + subline?: boolean;
1239 + skew?: number;
1240 + /** Hard offset shadow colour (retro). */
1241 + shadow?: string;
1242 + /** Carved outline. */
1243 + outline?: string;
1244 + /** Scale factor applied to the fitted size (Obsidian whispers). */
1245 + scale?: number;
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[] {
662 if (oneLine) return [name];
1271 +function splitLines(name: string, spec: TitleSpec, v: ArtVariant): string[] {
663 1272 const words = name.trim().split(/\s+/);
664 if (words.length <= 1) return words;
665 if (words.length === 2) return words;
1273 + if (words.length < 2) return [name];
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 1277 const mid = Math.ceil(words.length / 2);
667 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 }) {
671 const lines = splitName(name.toUpperCase(), oneLine);
672 const longest = Math.max(...lines.map((l) => l.length), 1);
673 const size = Math.min(maxFont, ((W - pad * 2) / longest) * 1.55);
674 const lineH = size * 0.92;
675 const baseY = H - pad - (lines.length - 1) * lineH;
676 return (
677 <g fontFamily="var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif" fontWeight={700} style={{ letterSpacing: "-0.045em" }}>
678 {lines.map((l, i) => (
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)" }}>
1281 +function estimateWidth(text: string, caps: boolean, tracking: number): number {
1282 + const cw = caps ? 0.7 : 0.6;
1283 + return text.length * (cw + tracking) - tracking;
1284 +}
1285 +
1286 +function Title({ s, name, showName }: { s: Stage; name: string; showName: boolean }) {
1287 + if (!showName) return null;
1288 + const { W, H, v, id, p } = s;
1289 + const spec = TITLES[s.game?.slug ?? ""] ?? { caps: true, tracking: -0.03, weight: 800, glow: p.glow };
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 1320 {l}
681 1321 </text>
682 ))}
683 {lines.map((l, i) => (
684 <text key={i} x={pad} y={r2(baseY + i * lineH)} fontSize={r2(size)} fill="#ffffff">
685 {l}
1322 + );
1323 + });
1324 +
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 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 1341 </g>
689 1342 );
690 1343 }
691 1344
692 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 1348 const reactId = React.useId().replace(/[^a-zA-Z0-9]/g, "");
696 1349 const id = `ga${reactId}`;
697 const preset = GAME_PRESENTATION[slug];
698 const bd = (backdrop ?? preset?.backdrop ?? BACKDROPS[hashString(slug) % BACKDROPS.length]) as string;
699 const motifKey: Backdrop = (BACKDROPS as readonly string[]).includes(bd) ? (bd as Backdrop) : "grid";
700 const pt = particles ?? preset?.particles ?? "dust";
701 const { w: W, h: H, pad, maxFont, oneLine } = DIMS[variant];
1350 + const game = GAMES_BY_SLUG.get(slug);
1351 + const { w: W, h: H } = DIMS[variant];
1352 + const showTitle = showName;
1353 + const { cx, cy, R } = focal(variant, W, H, showTitle && variant === "card");
702 1354 const rnd = mulberry32(hashString(`${slug}:${variant}`));
703 const ctx: Ctx = { W, H, rnd, p: palette, id };
704 const motif = MOTIFS[motifKey];
1355 + const fallback: SymbolStyle = { shape: "gem", color: palette.primary, accent: palette.glow, glow: 0.8 };
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 1375 return (
707 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 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 1379 <stop offset="0" stopColor={palette.bg} />
711 <stop offset="1" stopColor="#05060a" />
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" />
1380 + <stop offset="1" stopColor={bgBottom} />
721 1381 </linearGradient>
722 <linearGradient id={`${id}-col`} x1="0" y1="0" x2="1" y2="0">
723 <stop offset="0" stopColor="#1c1410" />
724 <stop offset="0.5" stopColor="#2a1d14" />
725 <stop offset="1" stopColor="#120c08" />
1382 + <linearGradient id={`${id}-vig`} x1="0" y1="0" x2="0" y2="1">
1383 + <stop offset="0.45" stopColor="#000" stopOpacity="0" />
1384 + <stop offset="1" stopColor="#020308" stopOpacity={variant === "card" ? 0.88 : 0.7} />
726 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 1386 </defs>
752 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. */}
754 {motif(ctx)}
755 {Particles({ ...ctx, kind: pt })}
756 <rect width={W} height={H} fill={`url(#${id}-vignette)`} />
757 <rect x="0.5" y="0.5" width={W - 1} height={H - 1} fill="none" stroke="#fff" strokeOpacity="0.06" />
758 {showName ? <Title name={name} W={W} H={H} pad={pad} maxFont={maxFont} oneLine={oneLine} p={palette} /> : null}
1388 + {/* Scenes are called as plain functions so the seeded PRNG is consumed exactly once per render (SSR == client, StrictMode-safe). */}
1389 + {scene(stage)}
1390 + {showTitle && variant !== "tile" ? <rect width={W} height={H} fill={`url(#${id}-vig)`} /> : null}
1391 + <rect x="0.5" y="0.5" width={W - 1} height={H - 1} fill="none" stroke="#fff" strokeOpacity="0.07" />
1392 + <Title s={stage} name={name} showName={showTitle} />
759 1393 </svg>
760 1394 );
761 1395 }
modified games/src/arctic-fortune/index.ts +3 −3
@@ -23,14 +23,14 @@ export const arcticFortune = defineGame({
23 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 24 scatterSym("S", "Northern Compass", 1.3, { shape: "star", color: "#a5f3fc", accent: "#ffffff", glow: 0.9 }),
25 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 }),
27 sym("H2", "Ice Axe", "high", { 3: 1.5, 4: 4, 5: 12 }, 4.2, { shape: "bolt", color: "#94a3b8", accent: "#e2e8f0", glow: 0.5 }),
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: "fish", color: "#94a3b8", accent: "#e2e8f0", glow: 0.5 }),
28 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 29 sym("M2", "Compass Rose", "mid", { 3: 0.8, 4: 2, 5: 5 }, 6, { shape: "ring", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }),
30 30 sym("L1", "Blue Shard", "low", { 3: 0.4, 4: 1, 5: 2.5 }, 6.5, { shape: "crystal", color: "#60a5fa", glow: 0.2 }),
31 31 sym("L2", "Teal Shard", "low", { 3: 0.4, 4: 1, 5: 2.5 }, 6.5, { shape: "diamond", color: "#2dd4bf", glow: 0.2 }),
32 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 34 sym("L5", "Amber Shard", "low", { 3: 0.3, 4: 0.8, 5: 2 }, 6.5, { shape: "star", color: "#fbbf24", glow: 0.2 }),
35 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 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 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 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 }),
29 sym("M2", "Asteroid", "mid", { 3: 0.8, 4: 2, 5: 5, 6: 12 }, 6, { shape: "cube", 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 }),
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: "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: "planet", color: "#60a5fa", glow: 0.2 }),
31 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 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 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 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 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 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 }),
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 }),
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: "shell", color: "#64748b", accent: "#cbd5e1", glow: 0.2 }),
30 30 sym("L1", "Coral", "low", { 3: 1.2, 4: 3, 5: 7.5, 6: 18 }, 6, { shape: "crystal", color: "#fb7185", glow: 0.2 }),
31 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 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 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 37 betDivisor: 50,
38 38 wild: { id: "W" },
modified games/src/diamond-heist/index.ts +10 −10
@@ -20,18 +20,18 @@ export const diamondHeist = defineGame({
20 20 volatility: "high",
21 21 maxMultiplier: 5000,
22 22 symbols: [
23 wildSym("W", "Skeleton Key", [0, 2, 2, 2, 0], { shape: "bolt", color: "#e5e7eb", accent: "#7dd3fc", glow: 1 }),
24 scatterSym("S", "Blueprint", 1.4, { shape: "chip", color: "#38bdf8", accent: "#ffffff", label: "PLN", glow: 0.8 }),
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: "scroll", color: "#38bdf8", accent: "#e0f2fe", glow: 0.8 }),
25 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 }),
27 sym("H1", "Laser Cutter", "high", { 3: 50, 4: 150, 5: 500 }, 3.8, { shape: "flame", color: "#7dd3fc", accent: "#e0f2fe", glow: 0.6 }),
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: "drill", color: "#7dd3fc", accent: "#e0f2fe", glow: 0.6 }),
28 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 }),
30 sym("M2", "Signal Jammer", "mid", { 3: 20, 4: 62, 5: 200 }, 6, { shape: "cube", 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" }),
32 sym("L2", "Code K", "low", { 3: 12, 4: 38, 5: 125 }, 9, { shape: "letter", color: "#94a3b8", label: "K" }),
33 sym("L3", "Code Q", "low", { 3: 10, 4: 30, 5: 100 }, 10, { shape: "letter", color: "#67e8f9", label: "Q" }),
34 sym("L4", "Code J", "low", { 3: 10, 4: 30, 5: 100 }, 10, { shape: "letter", color: "#a5b4fc", label: "J" }),
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: "satellite", color: "#60a5fa", accent: "#dbeafe", glow: 0.3 }),
31 + sym("L1", "Loot Bag", "low", { 3: 12, 4: 38, 5: 125 }, 9, { shape: "bag", color: "#cbd5e1", accent: "#7dd3fc" }),
32 + sym("L2", "Climbing Rope", "low", { 3: 12, 4: 38, 5: 125 }, 9, { shape: "rope", color: "#94a3b8", accent: "#e2e8f0" }),
33 + sym("L3", "Padlock", "low", { 3: 10, 4: 30, 5: 100 }, 10, { shape: "lock", color: "#67e8f9", accent: "#ffffff" }),
34 + sym("L4", "Keycode", "low", { 3: 10, 4: 30, 5: 100 }, 10, { shape: "code", color: "#a5b4fc", label: "****" }),
35 35 ],
36 36 betDivisor: 20,
37 37 wild: { id: "W" },
modified games/src/dragon-core/index.ts +1 −1
@@ -25,7 +25,7 @@ export const dragonCore = defineGame({
25 25 sym("DC", "Dragon Scale", "special", null, 1.2, { shape: "shield", color: "#b91c1c", accent: "#ffd166", glow: 0.8 }, { kind: "charge" }),
26 26 sym("P1", "Dragon Heart", "premium", { 3: 5, 4: 14, 5: 48 }, 2.8, { shape: "gem", color: "#ef4444", accent: "#fff1c2", glow: 0.9 }),
27 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 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 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 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 24 scatterSym("S", "Imperial Seal", 1.4, { shape: "shield", color: "#c8102e", accent: "#ffd166", glow: 0.9 }),
25 25 sym("P1", "Golden Emperor", "premium", { 3: 80, 4: 300, 5: 1000 }, 2.6, { shape: "crown", color: "#ffcc33", accent: "#fff5cc", glow: 0.9 }),
26 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 }),
28 sym("M1", "Red Lantern", "mid", { 3: 20, 4: 60, 5: 200 }, 5.5, { shape: "circle", 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 }),
30 sym("L1", "Dynasty A", "low", { 3: 10, 4: 30, 5: 100 }, 9, { shape: "letter", color: "#d9a441", label: "A" }),
31 sym("L2", "Dynasty K", "low", { 3: 10, 4: 30, 5: 100 }, 9, { shape: "letter", color: "#b3413a", label: "K" }),
32 sym("L3", "Dynasty Q", "low", { 3: 8, 4: 24, 5: 80 }, 10, { shape: "letter", color: "#3f8f6b", label: "Q" }),
33 sym("L4", "Dynasty J", "low", { 3: 8, 4: 24, 5: 80 }, 10, { shape: "letter", color: "#8a5cc7", label: "J" }),
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: "lantern", color: "#e02424", accent: "#ffb3b3", 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", "Imperial Scroll", "low", { 3: 10, 4: 30, 5: 100 }, 9, { shape: "scroll", color: "#d9a441", accent: "#f5e6c8" }),
31 + sym("L2", "Lucky Coin", "low", { 3: 10, 4: 30, 5: 100 }, 9, { shape: "coin", color: "#b3413a", accent: "#ffd166", label: "福" }),
32 + sym("L3", "Jade Fish", "low", { 3: 8, 4: 24, 5: 80 }, 10, { shape: "fish", color: "#3f8f6b", accent: "#c8f5df" }),
33 + sym("L4", "Paper Fan", "low", { 3: 8, 4: 24, 5: 80 }, 10, { shape: "fan", color: "#8a5cc7", accent: "#e9d5ff" }),
34 34 ],
35 35 betDivisor: 20,
36 36 wild: {
modified games/src/inferno-reels/index.ts +3 −3
@@ -23,13 +23,13 @@ export const infernoReels = defineGame({
23 23 wildSym("W", "Magma Core", [0, 1.4, 1.4, 1.4, 0], { shape: "orb", color: "#ff4500", accent: "#ffd166", glow: 1 }),
24 24 scatterSym("S", "Eruption", 1.4, { shape: "flame", color: "#ffb347", accent: "#ffffff", glow: 0.9 }),
25 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 27 sym("H2", "Ember Serpent", "high", { 3: 7, 4: 18, 5: 55 }, 4.2, { shape: "wave", color: "#ff7a1a", accent: "#ffe0b3", glow: 0.6 }),
28 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 30 sym("L1", "Cinder", "low", { 3: 2, 4: 5, 5: 12 }, 9, { shape: "circle", color: "#f97316", glow: 0.2 }),
31 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 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 35 betDivisor: 25,
modified games/src/lucky-circuit/index.ts +4 −4
@@ -24,12 +24,12 @@ export const luckyCircuit = defineGame({
24 24 scatterSym("S", "Bonus Token", 1.3, { shape: "coin", color: "#f472b6", accent: "#ffffff", glow: 0.9 }),
25 25 sym("MY", "Mystery Cartridge", "special", null, 1.5, { shape: "cube", color: "#1e1b4b", accent: "#22d3ee", label: "?", glow: 0.9 }, { kind: "mystery" }),
26 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 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 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" }),
32 sym("L2", "Bell Bit", "low", { 3: 16, 4: 40, 5: 100 }, 9, { shape: "letter", color: "#a78bfa", label: "B" }),
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: "bell", color: "#a78bfa", accent: "#facc15" }),
33 33 sym("L3", "Star Bit", "low", { 2: 3, 3: 16, 4: 40, 5: 100 }, 10, { shape: "hex", color: "#5eead4", glow: 0.2 }),
34 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 22 symbols: [
23 23 wildSym("W", "Neon Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "bolt", color: "#ff2bd6", accent: "#22d3ee", glow: 1 }),
24 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 }),
26 sym("H1", "Neon Katana", "high", { 3: 56, 4: 168, 5: 560 }, 3.8, { shape: "flame", 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 }),
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: "katana", color: "#22d3ee", accent: "#e0fbff", 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 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 }),
30 sym("L1", "Neon A", "low", { 3: 14, 4: 42, 5: 140 }, 9, { shape: "letter", color: "#f472b6", label: "A" }),
31 sym("L2", "Neon K", "low", { 3: 14, 4: 42, 5: 140 }, 9, { shape: "letter", color: "#67e8f9", label: "K" }),
32 sym("L3", "Neon Q", "low", { 3: 11, 4: 34, 5: 112 }, 10, { shape: "letter", color: "#c084fc", label: "Q" }),
33 sym("L4", "Neon J", "low", { 3: 11, 4: 34, 5: 112 }, 10, { shape: "letter", color: "#fbbf24", label: "J" }),
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", "Torii Gate", "low", { 3: 14, 4: 42, 5: 140 }, 9, { shape: "torii", color: "#f472b6", accent: "#ffffff" }),
31 + sym("L2", "Paper Lantern", "low", { 3: 14, 4: 42, 5: 140 }, 9, { shape: "lantern", color: "#67e8f9", accent: "#ffffff" }),
32 + sym("L3", "Kanji Sign", "low", { 3: 11, 4: 34, 5: 112 }, 10, { shape: "code", color: "#c084fc", label: "夜" }),
33 + sym("L4", "Vending Token", "low", { 3: 11, 4: 34, 5: 112 }, 10, { shape: "coin", color: "#fbbf24", accent: "#fff7ed", label: "¥" }),
34 34 ],
35 35 betDivisor: 25,
36 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 23 wildSym("W", "Gravity Wild", [0, 1.6, 1.6, 1.6, 0], { shape: "moon", color: "#e5e7eb", accent: "#fb923c", glow: 1 }),
24 24 scatterSym("S", "Orbital Beacon", 1.3, { shape: "star", color: "#fb923c", accent: "#ffffff", glow: 0.9 }),
25 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 }),
27 sym("H2", "Habitat Dome", "high", { 3: 36, 4: 120, 5: 360 }, 4.2, { shape: "ring", 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 }),
29 sym("M2", "Solar Panel", "mid", { 3: 19, 4: 60, 5: 190 }, 6, { shape: "chip", color: "#1e3a8a", accent: "#fbbf24", label: "SP", glow: 0.3 }),
30 sym("L1", "Moon Rock", "low", { 3: 12, 4: 36, 5: 120 }, 9, { shape: "circle", 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 }),
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: "igloo", color: "#22d3ee", accent: "#cffafe", glow: 0.6 }),
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", "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: "meteor", color: "#9ca3af", glow: 0.1 }),
31 + sym("L2", "Blue Planet", "low", { 3: 12, 4: 36, 5: 120 }, 9, { shape: "planet", color: "#a8a29e", glow: 0.1 }),
32 32 sym("L3", "Regolith Shard", "low", { 3: 10, 4: 30, 5: 96 }, 10, { shape: "crystal", color: "#78716c", accent: "#d6d3d1", glow: 0.1 }),
33 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 21 maxMultiplier: 5000,
22 22 symbols: [
23 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 25 sym("V", "Vault", "special", null, 0.9, { shape: "hex", color: "#ffd166", accent: "#fff2b3", glow: 0.8 }, { kind: "bonus" }),
26 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 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 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" }),
32 sym("L2", "Cipher K", "low", { 3: 0.4, 4: 1, 5: 3 }, 9, { shape: "letter", color: "#a78bfa", label: "K" }),
33 sym("L3", "Cipher Q", "low", { 3: 0.3, 4: 0.8, 5: 2.5 }, 10, { shape: "letter", color: "#f472b6", label: "Q" }),
34 sym("L4", "Cipher J", "low", { 3: 0.3, 4: 0.8, 5: 2.5 }, 10, { shape: "letter", color: "#fb923c", label: "J" }),
31 + sym("L1", "Padlock", "low", { 3: 0.4, 4: 1, 5: 3 }, 9, { shape: "lock", color: "#5eead4", accent: "#ffffff" }),
32 + sym("L2", "Binary Block", "low", { 3: 0.4, 4: 1, 5: 3 }, 9, { shape: "code", color: "#a78bfa", label: "0110" }),
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", "Pressure Valve", "low", { 3: 0.3, 4: 0.8, 5: 2.5 }, 10, { shape: "valve", color: "#fb923c", accent: "#ffedd5" }),
35 35 ],
36 36 betDivisor: 25,
37 37 wild: { id: "W", expanding: true },
modified games/src/pharaoh-protocol/index.ts +8 −8
@@ -21,17 +21,17 @@ export const pharaohProtocol = defineGame({
21 21 maxMultiplier: 3000,
22 22 symbols: [
23 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 }),
25 sym("AR", "Artifact", "special", null, 1.1, { shape: "cube", color: "#f59e0b", accent: "#22d3ee", glow: 0.8 }, { kind: "charge" }),
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: "ankh", color: "#f59e0b", accent: "#22d3ee", glow: 0.8 }, { kind: "charge" }),
26 26 sym("P1", "Pharaoh Core", "premium", { 3: 68, 4: 255, 5: 850 }, 2.6, { shape: "crown", color: "#ffd166", accent: "#fff7d6", glow: 0.9 }),
27 27 sym("H1", "Anubis Unit", "high", { 3: 34, 4: 100, 5: 340 }, 3.8, { shape: "skull", color: "#1e293b", accent: "#ffd166", glow: 0.6 }),
28 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 }),
30 sym("M2", "Papyrus Code", "mid", { 3: 14, 4: 42, 5: 135 }, 6, { shape: "chip", color: "#a3e635", accent: "#ecfccb", label: "PY", glow: 0.3 }),
31 sym("L1", "Glyph A", "low", { 3: 8, 4: 25, 5: 85 }, 9, { shape: "letter", color: "#fbbf24", label: "A" }),
32 sym("L2", "Glyph K", "low", { 3: 8, 4: 25, 5: 85 }, 9, { shape: "letter", color: "#22d3ee", label: "K" }),
33 sym("L3", "Glyph Q", "low", { 3: 7, 4: 20, 5: 70 }, 10, { shape: "letter", color: "#f97316", label: "Q" }),
34 sym("L4", "Glyph J", "low", { 3: 7, 4: 20, 5: 70 }, 10, { shape: "letter", color: "#a78bfa", label: "J" }),
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: "scroll", color: "#a3e635", accent: "#ecfccb", glow: 0.3 }),
31 + sym("L1", "Ankh Glyph", "low", { 3: 8, 4: 25, 5: 85 }, 9, { shape: "ankh", color: "#fbbf24", accent: "#22d3ee" }),
32 + sym("L2", "Canopic Jar", "low", { 3: 8, 4: 25, 5: 85 }, 9, { shape: "barrel", color: "#22d3ee", accent: "#ffd166" }),
33 + sym("L3", "Sand Hourglass", "low", { 3: 7, 4: 20, 5: 70 }, 10, { shape: "hourglass", color: "#f97316", accent: "#ffd166" }),
34 + sym("L4", "Cobra Glyph", "low", { 3: 7, 4: 20, 5: 70 }, 10, { shape: "snake", color: "#a78bfa", accent: "#ffd166" }),
35 35 ],
36 36 betDivisor: 20,
37 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 24 scatterSym("S", "Entangler", 1.3, { shape: "ring", color: "#c084fc", accent: "#ffffff", glow: 0.9 }),
25 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 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 }),
28 sym("M1", "Photon Gate", "mid", { 3: 8, 4: 20, 5: 56 }, 5.5, { shape: "gem", color: "#34d399", accent: "#d1fae5", glow: 0.3 }),
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: "torii", color: "#34d399", accent: "#d1fae5", glow: 0.3 }),
29 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" }),
31 sym("L2", "Bit Zero", "low", { 3: 3, 4: 8, 5: 20 }, 6.5, { shape: "letter", color: "#a5b4fc", label: "0" }),
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: "code", color: "#a5b4fc", label: "0" }),
32 32 sym("L3", "Psi State", "low", { 3: 2.5, 4: 6.5, 5: 16 }, 6.5, { shape: "letter", color: "#c084fc", label: "Ψ" }),
33 33 sym("L4", "Phi State", "low", { 3: 2.5, 4: 6.5, 5: 16 }, 6.5, { shape: "letter", color: "#5eead4", label: "Φ" }),
34 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 21 maxMultiplier: 5000,
22 22 symbols: [
23 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 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 }),
27 sym("H2", "Plasma Coil", "high", { 3: 9, 4: 24, 5: 72 }, 4.2, { shape: "ring", 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 }),
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 }),
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: "valve", color: "#38bdf8", accent: "#e0f2fe", glow: 0.6 }),
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 Sign", "mid", { 3: 4.8, 4: 12, 5: 30 }, 6, { shape: "warning", color: "#facc15", accent: "#111827", glow: 0.3 }),
30 30 sym("L1", "Isotope Alpha", "low", { 3: 2.4, 4: 6, 5: 15 }, 9, { shape: "letter", color: "#4ade80", label: "α" }),
31 31 sym("L2", "Isotope Beta", "low", { 3: 2.4, 4: 6, 5: 15 }, 9, { shape: "letter", color: "#facc15", label: "β" }),
32 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 21 maxMultiplier: 3000,
22 22 symbols: [
23 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 }),
25 sym("LAP", "Lap Counter", "special", null, 0.9, { shape: "ring", color: "#22d3ee", accent: "#ffffff", glow: 0.8 }, { kind: "charge" }),
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: "gauge", color: "#22d3ee", accent: "#ffffff", glow: 0.8 }, { kind: "charge" }),
26 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 }),
28 sym("H2", "Gold Trophy", "high", { 3: 22, 4: 75, 5: 225 }, 4.2, { shape: "gem", 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 }),
30 sym("M2", "Fuel Can", "mid", { 3: 12, 4: 38, 5: 120 }, 6, { shape: "cube", 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" }),
32 sym("L2", "Grid K", "low", { 3: 7.5, 4: 22, 5: 75 }, 9, { shape: "letter", color: "#ef4444", label: "K" }),
33 sym("L3", "Grid Q", "low", { 3: 6, 4: 18, 5: 60 }, 10, { shape: "letter", color: "#22d3ee", label: "Q" }),
34 sym("L4", "Grid J", "low", { 3: 6, 4: 18, 5: 60 }, 10, { shape: "letter", color: "#e5e7eb", label: "J" }),
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: "trophy", color: "#f59e0b", accent: "#fef3c7", glow: 0.6 }),
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: "barrel", color: "#10b981", accent: "#d1fae5", glow: 0.3 }),
31 + sym("L1", "Pit Wrench", "low", { 3: 7.5, 4: 22, 5: 75 }, 9, { shape: "valve", color: "#fbbf24", accent: "#fff7ed" }),
32 + sym("L2", "Traffic Cone", "low", { 3: 7.5, 4: 22, 5: 75 }, 9, { shape: "warning", color: "#ef4444", accent: "#ffffff" }),
33 + sym("L3", "Stopwatch", "low", { 3: 6, 4: 18, 5: 60 }, 10, { shape: "hourglass", color: "#22d3ee", accent: "#ffffff" }),
34 + sym("L4", "Grid Marker", "low", { 3: 6, 4: 18, 5: 60 }, 10, { shape: "code", color: "#e5e7eb", label: "P1" }),
35 35 ],
36 36 betDivisor: 20,
37 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 20 volatility: "medium",
21 21 maxMultiplier: 4000,
22 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 24 scatterSym("S", "Warp Beacon", 1.3, { shape: "star", color: "#c084fc", accent: "#ffffff", glow: 0.9 }),
25 25 sym("DM", "Dark Matter", "special", null, 1.1, { shape: "orb", color: "#1e1b4b", accent: "#a78bfa", glow: 0.9 }, { kind: "charge" }),
26 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 27 sym("H1", "Platinum Ore", "high", { 3: 5, 4: 12, 5: 37 }, 3.8, { shape: "gem", color: "#e5e7eb", accent: "#ffffff", glow: 0.6 }),
28 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 }),
30 sym("M2", "Cobalt Ore", "mid", { 3: 2, 4: 5, 5: 12 }, 6, { shape: "cube", color: "#3b82f6", accent: "#bfdbfe", 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: "drill", color: "#3b82f6", accent: "#bfdbfe", glow: 0.3 }),
31 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 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 34 sym("L4", "Ice Rock", "low", { 3: 0.75, 4: 2, 5: 5 }, 6.5, { shape: "snow", color: "#7dd3fc", glow: 0.2 }),
35 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 21 maxMultiplier: 3000,
22 22 symbols: [
23 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 }),
25 sym("P1", "Temple King Mask", "premium", { 3: 36, 4: 108, 5: 360 }, 2.8, { shape: "skull", 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 }),
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: "mask", color: "#10b981", accent: "#fbbf24", glow: 0.9 }),
26 + sym("H1", "Jade Serpent", "high", { 3: 24, 4: 60, 5: 180 }, 3.8, { shape: "snake", color: "#34d399", accent: "#d1fae5", glow: 0.6 }),
27 27 sym("H2", "Ritual Torch", "high", { 3: 18, 4: 48, 5: 144 }, 4.2, { shape: "flame", color: "#f97316", accent: "#ffedd5", glow: 0.6 }),
28 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 }),
30 sym("L1", "Glyph A", "low", { 3: 5, 4: 12, 5: 30 }, 9, { shape: "letter", color: "#fbbf24", label: "A" }),
31 sym("L2", "Glyph K", "low", { 3: 5, 4: 12, 5: 30 }, 9, { shape: "letter", color: "#34d399", label: "K" }),
32 sym("L3", "Glyph Q", "low", { 3: 3.6, 4: 9.6, 5: 24 }, 10, { shape: "letter", color: "#f97316", label: "Q" }),
33 sym("L4", "Glyph J", "low", { 3: 3.6, 4: 9.6, 5: 24 }, 10, { shape: "letter", color: "#a78bfa", label: "J" }),
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", "Jaguar Paw", "low", { 3: 5, 4: 12, 5: 30 }, 9, { shape: "paw", color: "#fbbf24", accent: "#111827" }),
31 + sym("L2", "Tribal Shell", "low", { 3: 5, 4: 12, 5: 30 }, 9, { shape: "shell", color: "#34d399", accent: "#d1fae5" }),
32 + sym("L3", "Offering Bowl", "low", { 3: 3.6, 4: 9.6, 5: 24 }, 10, { shape: "chest", color: "#f97316", accent: "#ffedd5" }),
33 + sym("L4", "Step Pyramid", "low", { 3: 3.6, 4: 9.6, 5: 24 }, 10, { shape: "pyramid", color: "#a78bfa", accent: "#e9d5ff" }),
34 34 ],
35 35 betDivisor: 25,
36 36 wild: { id: "W", moving: true },
modified games/src/zero-gravity/index.ts +3 −3
@@ -23,14 +23,14 @@ export const zeroGravity = defineGame({
23 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 24 scatterSym("S", "Gravity Well", 1.3, { shape: "ring", color: "#a78bfa", accent: "#ffffff", glow: 0.9 }),
25 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 }),
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 }),
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: "rocket", color: "#60a5fa", accent: "#dbeafe", label: "SAT", glow: 0.6 }),
28 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 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 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 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 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 35 betDivisor: 25,
36 36 wild: { id: "W" },
modified packages/game-core/src/types.ts +51 −1
@@ -37,7 +37,57 @@ export type SymbolShape =
37 37 | "atom"
38 38 | "crown"
39 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 92 export interface SymbolStyle {
43 93 shape: SymbolShape;
44 94