Resserre les plafonds de score, ajoute une vérif par temps réel écoulé sur Icare, corrige le contraste de l'Urne
Build and deploy / deploy (push) Successful in 36s

5000 restait trop haut pour être un vrai plafond réaliste : Icare abaissé à 500 (calculé sur le rythme le plus rapide théoriquement atteignable dans le jeu), Corne à 2000 (estimation plus prudente, économie de score plus dure à borner). Ajoute icarus_runs/start_icarus_run() : le serveur enregistre l'instant réel de début de partie et rejette un score incohérent avec le temps écoulé, sans jamais faire confiance à une durée envoyée par le client. Corrige aussi le nombre de jetons de /urne, peu lisible en gold-bright sur fond marbre (passe à text-sea, même convention qu'Icare/Corne).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-24 11:28:02 +02:00
parent dc99af6bb3
commit 35704e50fc
7 changed files with 151 additions and 25 deletions
+26 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Avatar } from "@/components/avatar";
import { Podium } from "@/components/podium";
import { IconClose, IconTrophy } from "@/components/icons";
@@ -21,6 +21,12 @@ export function IcareView({
const [leaderboard, setLeaderboard] = useState(initialLeaderboard);
const [ownBestScore, setOwnBestScore] = useState(initialOwnBestScore);
const [showRanking, setShowRanking] = useState(false);
// Promesse plutôt qu'une simple valeur : évite une course si la partie se
// termine avant que start_icarus_run() n'ait eu le temps de répondre —
// handleFinish attend cette même promesse (déjà résolue dans l'immense
// majorité des cas, une partie dure toujours largement plus longtemps que
// cet aller-retour réseau).
const runIdPromiseRef = useRef<Promise<string | null>>(Promise.resolve(null));
const refetch = useCallback(async () => {
const supabase = createClient();
@@ -67,9 +73,26 @@ export function IcareView({
};
}, [refetch]);
// Démarre le suivi serveur du temps de jeu (icarus_runs) dès le premier
// battement d'aile — soumis avec le score à la fin pour rejeter un score
// incohérent avec le temps réellement écoulé (voir submit_icarus_score,
// supabase/schema.sql).
function handleStart() {
const supabase = createClient();
runIdPromiseRef.current = (async () => {
try {
const { data } = await supabase.rpc("start_icarus_run");
return (data as string | null) ?? null;
} catch {
return null;
}
})();
}
async function handleFinish(score: number) {
const supabase = createClient();
const { data, error } = await supabase.rpc("submit_icarus_score", { p_score: score });
const runId = await runIdPromiseRef.current;
const { data, error } = await supabase.rpc("submit_icarus_score", { p_score: score, p_run_id: runId });
if (!error && data) {
setOwnBestScore(data.best_score);
refetch();
@@ -98,7 +121,7 @@ export function IcareView({
</button>
</div>
<IcarusGame onFinish={handleFinish} />
<IcarusGame onStart={handleStart} onFinish={handleFinish} />
{showRanking && (
<div className="fixed inset-0 z-30 flex items-center justify-center bg-ink/80 p-4 backdrop-blur-sm">
+13 -1
View File
@@ -271,12 +271,23 @@ function drawSpeedLines(ctx: CanvasRenderingContext2D, centerX: number, centerY:
ctx.restore();
}
export function IcarusGame({ onFinish }: { onFinish: (score: number) => void }) {
export function IcarusGame({
onStart,
onFinish,
}: {
onStart: () => void;
onFinish: (score: number) => void;
}) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [status, setStatus] = useState<Status>("idle");
const [score, setScore] = useState(0);
const onStartRef = useRef(onStart);
useEffect(() => {
onStartRef.current = onStart;
}, [onStart]);
const onFinishRef = useRef(onFinish);
useEffect(() => {
onFinishRef.current = onFinish;
@@ -318,6 +329,7 @@ export function IcarusGame({ onFinish }: { onFinish: (score: number) => void })
if (statusRef.current === "idle") {
statusRef.current = "flying";
setStatus("flying");
onStartRef.current();
} else if (statusRef.current === "dead") {
resetGame();
}
+1 -1
View File
@@ -254,7 +254,7 @@ export function UrneView({
<span className={`truncate ${count === 0 ? "text-text-mut/60" : "text-text-marble"}`}>
{citizen.pseudo}
</span>
<span className="ml-auto shrink-0 font-heading text-gold-bright">
<span className="ml-auto shrink-0 font-heading text-sea">
{count > 0 ? `×${count}` : "—"}
</span>
</button>
+1 -1
View File
@@ -80,4 +80,4 @@ export const MIN_SCORE = 0;
// Doit rester aligné avec la contrainte icarus_scores_best_score_check
// (supabase/schema.sql) — celle-ci est la vraie garantie, cette constante
// n'est qu'une valeur de référence côté client.
export const MAX_SCORE = 5000;
export const MAX_SCORE = 500;
+1 -1
View File
@@ -45,7 +45,7 @@ export const MIN_SCORE = 0;
// Doit rester aligné avec la contrainte melon_scores_best_score_check
// (supabase/schema.sql) — celle-ci est la vraie garantie, cette constante
// n'est qu'une valeur de référence côté client.
export const MAX_SCORE = 5000;
export const MAX_SCORE = 2000;
// Positions figées à la main pour certains Citoyens (id de profil → index
// de palier, 0 = le plus petit) — les Citoyens restants remplissent les