SPB Git

spb/spinza

Public

web: fix PixiJS texture frame (Rectangle), renderer mount error state; admin certification writes game_versions; docs

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

4 changed files +14 −7

modified apps/web/src/components/game/game-client.tsx +7 −1
@@ -87,7 +87,13 @@ export function GameClient({ game, definition }: Props) {
87 let cancelled = false; 87 let cancelled = false;
88 (async () => { 88 (async () => {
89 setProgress(15); 89 setProgress(15);
90− await renderer.mount(canvasRef.current!); 90+ try {
91+ await renderer.mount(canvasRef.current!);
92+ } catch (e) {
93+ console.error("renderer mount failed", e);
94+ if (!cancelled) setError({ code: "GAME_UNAVAILABLE", message: "Your browser could not start the game renderer (WebGL required)." });
95+ return;
96+ }
91 setProgress(60); 97 setProgress(60);
92 try { 98 try {
93 const launch = await api<{ state: ClientOutcome["stateAfter"] | null }>(`/api/games/${game.slug}/launch`, { method: "POST" }); 99 const launch = await api<{ state: ClientOutcome["stateAfter"] | null }>(`/api/games/${game.slug}/launch`, { method: "POST" });
modified apps/web/src/components/game/renderer.ts +2 −2
@@ -1,6 +1,6 @@
1 "use client"; 1 "use client";
2 2
3−import { Application, Container, Graphics, Sprite, Text, TextStyle, Texture, type Renderer } from "pixi.js"; 3+import { Application, Container, Graphics, Rectangle, Sprite, Text, TextStyle, Texture, type Renderer } from "pixi.js";
4 import type { SpinStep } from "@spinza/game-core/client"; 4 import type { SpinStep } from "@spinza/game-core/client";
5 import type { ClientDefinition } from "./types"; 5 import type { ClientDefinition } from "./types";
6 import { drawSymbol, hexColor, lighten } from "./symbol-art"; 6 import { drawSymbol, hexColor, lighten } from "./symbol-art";
@@ -350,7 +350,7 @@ export class SlotRenderer {
350 const size = 176; 350 const size = 176;
351 for (const s of this.opts.def.symbols) { 351 for (const s of this.opts.def.symbols) {
352 const art = drawSymbol(s.style, { size, tier: s.tier, kind: s.kind }); 352 const art = drawSymbol(s.style, { size, tier: s.tier, kind: s.kind });
353− const tex = renderer.generateTexture({ target: art, resolution: 2, frame: { x: -size / 2, y: -size / 2, width: size, height: size } as never }); 353+ const tex = renderer.generateTexture({ target: art, resolution: 2, frame: new Rectangle(-size / 2, -size / 2, size, size) });
354 this.textures.set(s.id, tex); 354 this.textures.set(s.id, tex);
355 art.destroy({ children: true }); 355 art.destroy({ children: true });
356 } 356 }
modified apps/web/src/components/lobby/game-art.tsx +4 −3
@@ -701,7 +701,7 @@ export function GameArt({ slug, name, palette, backdrop, particles, variant = "c
701 const { w: W, h: H, pad, maxFont, oneLine } = DIMS[variant]; 701 const { w: W, h: H, pad, maxFont, oneLine } = DIMS[variant];
702 const rnd = mulberry32(hashString(`${slug}:${variant}`)); 702 const rnd = mulberry32(hashString(`${slug}:${variant}`));
703 const ctx: Ctx = { W, H, rnd, p: palette, id }; 703 const ctx: Ctx = { W, H, rnd, p: palette, id };
704− const Motif = MOTIFS[motifKey]; 704+ const motif = MOTIFS[motifKey];
705 705
706 return ( 706 return (
707 <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid slice" className={cn("block h-full w-full", className)} role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true}> 707 <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid slice" className={cn("block h-full w-full", className)} role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true}>
@@ -750,8 +750,9 @@ export function GameArt({ slug, name, palette, backdrop, particles, variant = "c
750 </filter> 750 </filter>
751 </defs> 751 </defs>
752 <rect width={W} height={H} fill={`url(#${id}-bg)`} /> 752 <rect width={W} height={H} fill={`url(#${id}-bg)`} />
753− <Motif {...ctx} /> 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− <Particles {...ctx} kind={pt} /> 754+ {motif(ctx)}
755+ {Particles({ ...ctx, kind: pt })}
755 <rect width={W} height={H} fill={`url(#${id}-vignette)`} /> 756 <rect width={W} height={H} fill={`url(#${id}-vignette)`} />
756 <rect x="0.5" y="0.5" width={W - 1} height={H - 1} fill="none" stroke="#fff" strokeOpacity="0.06" /> 757 <rect x="0.5" y="0.5" width={W - 1} height={H - 1} fill="none" stroke="#fff" strokeOpacity="0.06" />
757 {showName ? <Title name={name} W={W} H={H} pad={pad} maxFont={maxFont} oneLine={oneLine} p={palette} /> : null} 758 {showName ? <Title name={name} W={W} H={H} pad={pad} maxFont={maxFont} oneLine={oneLine} p={palette} /> : null}
modified qa/e2e.mjs +1 −1
@@ -13,7 +13,7 @@ let chromium;
13 try { 13 try {
14 ({ chromium } = require("playwright")); 14 ({ chromium } = require("playwright"));
15 } catch { 15 } catch {
16− ({ chromium } = require(path.join(process.env.HOME ?? "", "Desktop/Projets/apps-web/lou-ka/frontend/node_modules/playwright"))); 16+ ({ chromium } = require(path.join(process.env.HOME ?? "", "Desktop/Projets/apps-web/fetcha/qa/node_modules/playwright")));
17 } 17 }
18 18
19 const BASE = (process.env.BASE ?? "http://localhost:8230").replace(/\/$/, ""); 19 const BASE = (process.env.BASE ?? "http://localhost:8230").replace(/\/$/, "");
20 20
Menu

Theme