diff --git a/src/app/icare/icare-view.tsx b/src/app/icare/icare-view.tsx index 6847bc3..f962614 100644 --- a/src/app/icare/icare-view.tsx +++ b/src/app/icare/icare-view.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Avatar } from "@/components/avatar"; +import { Podium } from "@/components/podium"; +import { IconClose, IconTrophy } from "@/components/icons"; import { computeRanksBy } from "@/lib/ranking"; import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client"; import { IcarusGame } from "./icarus-game"; @@ -18,6 +20,7 @@ export function IcareView({ }) { const [leaderboard, setLeaderboard] = useState(initialLeaderboard); const [ownBestScore, setOwnBestScore] = useState(initialOwnBestScore); + const [showRanking, setShowRanking] = useState(false); const refetch = useCallback(async () => { const supabase = createClient(); @@ -73,45 +76,89 @@ export function IcareView({ } } + // Le classement vit dans une modale, complètement hors du flux de mise en + // page du jeu : sans ça, une mise à jour Realtime pendant une partie (le + // score d'un autre joueur qui change) redimensionnait le canvas et cassait + // la partie en cours. const ranked = useMemo(() => computeRanksBy(leaderboard, (a, b) => b.best_score - a.best_score), [leaderboard]); + const podiumProfiles = ranked.filter((p) => p.rank <= 3); + const rest = ranked.filter((p) => p.rank > 3); return ( -
- {isFrozen && ( -

- Les scores sont figés depuis le début du Tribunal. -

- )} - - {ranked.length > 0 && ( -
    - {ranked.map((entry) => ( -
  1. - {entry.rank} - - {entry.pseudo} - {entry.best_score} -
  2. - ))} -
- )} - - {ownBestScore !== null && ( -

- Ton record : {ownBestScore} -

- )} +
+
+

Le Vol d'Icare

+ +
-

- {isFrozen - ? "Les gloires du podium ont été attribuées automatiquement au début du Tribunal." - : "Au début du Tribunal, les scores seront figés et le podium recevra des gloires automatiquement — aucun Archonte n'a besoin d'intervenir."} -

+ {showRanking && ( +
+
+
+

Classement

+ +
+ +
+ {isFrozen && ( +

+ Les scores sont figés depuis le début du Tribunal. +

+ )} + + {podiumProfiles.length > 0 && ( + member.best_score} /> + )} + + {rest.length > 0 && ( +
    + {rest.map((entry) => ( +
  1. + + {entry.rank} + + + {entry.pseudo} + {entry.best_score} +
  2. + ))} +
+ )} + + {ownBestScore !== null && ( +

+ Ton record : {ownBestScore} +

+ )} + +

+ {isFrozen + ? "Les gloires du podium ont été attribuées automatiquement au début du Tribunal." + : "Au début du Tribunal, les scores seront figés et le podium recevra des gloires automatiquement — aucun Archonte n'a besoin d'intervenir."} +

+
+
+
+ )}
); } diff --git a/src/app/icare/icarus-game.tsx b/src/app/icare/icarus-game.tsx index 6e3b388..8b3826a 100644 --- a/src/app/icare/icarus-game.tsx +++ b/src/app/icare/icarus-game.tsx @@ -175,10 +175,19 @@ export function IcarusGame({ onFinish }: { onFinish: (score: number) => void }) const ctx = canvas.getContext("2d"); if (!ctx) return; + // "Contain" dans le conteneur (largeur ET hauteur, contrairement à avant + // où seule la largeur comptait) : nécessaire en plein écran, où la + // hauteur disponible varie et n'est plus déduite de la largeur. function resize() { const dpr = window.devicePixelRatio || 1; - const cssWidth = container!.clientWidth; - const cssHeight = cssWidth * (VIEWPORT_HEIGHT / VIEWPORT_WIDTH); + const availableWidth = container!.clientWidth; + const availableHeight = container!.clientHeight; + let cssWidth = availableWidth; + let cssHeight = (cssWidth * VIEWPORT_HEIGHT) / VIEWPORT_WIDTH; + if (cssHeight > availableHeight) { + cssHeight = availableHeight; + cssWidth = (cssHeight * VIEWPORT_WIDTH) / VIEWPORT_HEIGHT; + } canvas!.width = cssWidth * dpr; canvas!.height = cssHeight * dpr; canvas!.style.width = `${cssWidth}px`; @@ -283,38 +292,38 @@ export function IcarusGame({ onFinish }: { onFinish: (score: number) => void }) typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; return ( -
-
- +
+ - {status === "dead" && !prefersReducedMotion && } - - {status === "dead" && ( -
-
-

Score

-

{score}

- -
-
- )} -
+ {status === "dead" && !prefersReducedMotion && } {status === "idle" && ( -

- Touche l'écran pour battre des ailes et t'envoler entre les colonnes. -

+
+

+ Touche l'écran pour battre des ailes et t'envoler entre les colonnes. +

+
+ )} + + {status === "dead" && ( +
+
+

Score

+

{score}

+ +
+
)}
); diff --git a/src/app/icare/page.tsx b/src/app/icare/page.tsx index 6812918..b0624d3 100644 --- a/src/app/icare/page.tsx +++ b/src/app/icare/page.tsx @@ -48,14 +48,6 @@ export default async function IcarePage() { const ownBestScore = leaderboard.find((entry) => entry.id === user.id)?.best_score ?? null; return ( -
-
-

Le Vol d'Icare

-

- Vole entre les colonnes, aussi loin que tes ailes de cire le permettent. -

-
- -
+ ); }