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
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:
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user