From 0dfd3e7e90527682148a18a6066672a2e289e74d Mon Sep 17 00:00:00 2001 From: Valentin ROBIN Date: Sun, 23 Aug 2026 22:41:08 +0200 Subject: [PATCH] =?UTF-8?q?Passe=20Le=20Vol=20d'Icare=20en=20plein=20?= =?UTF-8?q?=C3=A9cran=20avec=20podium=20anim=C3=A9=20pour=20le=20top=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le classement (dont un nouveau podium top 3, réutilisant celui de L'Agora) vit maintenant dans une modale entièrement détachée de la mise en page du jeu : une mise à jour Realtime du classement pendant une partie ne peut plus redimensionner le canvas et casser la partie en cours. Le jeu occupe tout l'écran disponible (contain avec letterboxing) au lieu d'une carte fixe en haut de page. Corrige aussi le score du classement, en bleu au lieu de doré (contraste insuffisant sur fond marbre clair). Co-Authored-By: Claude Sonnet 5 --- src/app/icare/icare-view.tsx | 113 ++++++++++++++++++++++++---------- src/app/icare/icarus-game.tsx | 71 +++++++++++---------- src/app/icare/page.tsx | 10 +-- 3 files changed, 121 insertions(+), 73 deletions(-) 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. -

-
- -
+ ); }