Corrige la ligne de danger de la Corne (franchissement complet, pas au contact) et redesigne l'échelle de repère
Build and deploy / deploy (push) Successful in 36s

La pièce devait déjà entièrement dépasser la ligne rouge (bord bas, pas juste le sommet) pour compter dans le délai de grâce avant game over. L'échelle des tailles en bas de page passe d'une seule ligne à défilement horizontal à un flex-wrap sans scroll, avec un titre — l'intérêt d'une échelle de repère est de tout voir d'un coup d'œil. Corrige aussi une violation de react-hooks/purity (Date.now() en argument direct de useRef) introduite par le mécanisme anti-triche fusionné depuis main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-25 16:21:11 +02:00
parent 6134bf22d0
commit 50b354f11c
+43 -24
View File
@@ -84,7 +84,14 @@ export function MelonGame({
const pendingMergesRef = useRef<PendingMerge[]>([]); const pendingMergesRef = useRef<PendingMerge[]>([]);
const mergingBodyIdsRef = useRef(new Set<number>()); const mergingBodyIdsRef = useRef(new Set<number>());
const dangerSinceRef = useRef<number | null>(null); const dangerSinceRef = useRef<number | null>(null);
const runStartedAtRef = useRef(Date.now()); // Pas Date.now() directement en argument de useRef (impur pendant le
// rendu, react-hooks/purity) : la vraie valeur est posée dans l'effet de
// montage juste en dessous — resetGame() (rejouer après une partie) la
// réécrit de toute façon à chaque nouvelle partie.
const runStartedAtRef = useRef(0);
useEffect(() => {
runStartedAtRef.current = Date.now();
}, []);
const dropCountRef = useRef(0); const dropCountRef = useRef(0);
const mergeCountRef = useRef(0); const mergeCountRef = useRef(0);
@@ -326,12 +333,16 @@ export function MelonGame({
// Renvoie les corps qui dépassent la ligne de danger (au repos, pas un // Renvoie les corps qui dépassent la ligne de danger (au repos, pas un
// simple rebond) — utilisé à la fois pour déclencher/prolonger le // simple rebond) — utilisé à la fois pour déclencher/prolonger le
// délai de grâce et pour savoir quelles pièces doivent clignoter. // délai de grâce et pour savoir quelles pièces doivent clignoter. Le
// cercle doit avoir ENTIÈREMENT franchi la ligne (son bord bas,
// position.y + radius) plutôt que simplement y toucher (son bord haut,
// position.y - radius) : une pièce qui dépasse juste du sommet ne doit
// pas encore compter.
function overflowingBodyIds(): Set<number> { function overflowingBodyIds(): Set<number> {
const ids = new Set<number>(); const ids = new Set<number>();
for (const [id, body] of bodiesRef.current) { for (const [id, body] of bodiesRef.current) {
const radius = body.circleRadius ?? 0; const radius = body.circleRadius ?? 0;
if (body.position.y - radius < DANGER_LINE_Y && body.speed < 0.5) { if (body.position.y + radius < DANGER_LINE_Y && body.speed < 0.5) {
ids.add(id); ids.add(id);
} }
} }
@@ -528,27 +539,35 @@ export function MelonGame({
{/* Échelle de repère : ordre de fusion complet, du plus petit au plus {/* Échelle de repère : ordre de fusion complet, du plus petit au plus
grand — taille d'affichage proportionnelle au vrai rayon du grand — taille d'affichage proportionnelle au vrai rayon du
palier (mais réduite et plafonnée) pour que la progression se palier (mais réduite et plafonnée) pour que la progression se
voie d'un coup d'œil, sans dépendre du nombre de Citoyens. Une voie d'un coup d'œil, sans dépendre du nombre de Citoyens.
seule ligne défilante (pas de flex-wrap). items-center (pas flex-wrap (pas de scroll horizontal comme avant) : tout le monde
items-end) : aligner sur le bas avec des tailles très variables doit être visible sans interaction, l'intérêt d'une échelle de
donnait un décalage qui grandissait avec la taille (la flèche, repère est justement de tout voir d'un coup plutôt que de devoir
à décalage fixe, n'était plus au bon endroit pour les gros faire défiler. items-center (pas items-end) : aligner sur le bas
ronds) — centrer chaque rond sur son propre milieu reste avec des tailles très variables donnait un décalage qui
symétrique quelle que soit sa taille. */} grandissait avec la taille (la flèche, à décalage fixe, n'était
<div plus au bon endroit pour les gros ronds) — centrer chaque rond
className="marble-surface flex w-full items-center gap-x-1 gap-y-2 overflow-x-auto rounded-lg border border-gold/25 p-2" sur son propre milieu reste symétrique quelle que soit sa
style={{ maxWidth: CONTAINER_WIDTH }} taille. Largeur plafonnée à max-w-sm (pas CONTAINER_WIDTH comme
> avant) : la page (corne-view.tsx) est bien plus large que le bac
{citizens.map((citizen, tier) => ( de jeu, la contrainte à 300px n'avait pas lieu d'être et forçait
<div key={citizen.id} className="flex shrink-0 items-center gap-1"> le scroll pour rien. */}
<Avatar <div className="marble-surface w-full max-w-sm rounded-lg border border-gold/25 p-3">
pseudo={citizen.pseudo} <p className="mb-2 text-center font-heading text-[0.65rem] tracking-[0.15em] text-text-mut uppercase">
avatarUrl={citizen.avatar_url} Ordre de fusion
size={Math.round(16 + (tierRadii[tier] / tierRadii[maxTier]) * 24)} </p>
/> <div className="flex flex-wrap items-center justify-center gap-x-1.5 gap-y-3">
{tier < citizens.length - 1 && <span className="text-xs text-text-mut"></span>} {citizens.map((citizen, tier) => (
</div> <div key={citizen.id} className="flex items-center gap-1.5">
))} <Avatar
pseudo={citizen.pseudo}
avatarUrl={citizen.avatar_url}
size={Math.round(18 + (tierRadii[tier] / tierRadii[maxTier]) * 26)}
/>
{tier < citizens.length - 1 && <span className="text-xs text-gold/50"></span>}
</div>
))}
</div>
</div> </div>
</div> </div>
); );