diff --git a/src/app/corne/melon-game.tsx b/src/app/corne/melon-game.tsx index 38f2d15..6982969 100644 --- a/src/app/corne/melon-game.tsx +++ b/src/app/corne/melon-game.tsx @@ -32,6 +32,54 @@ function randomTier(min: number, max: number): number { return min + Math.floor(Math.random() * (max - min + 1)); } +// Composant à part entière (pas juste un fragment JSX dans le .map() de +// MelonGame) uniquement pour pouvoir lui donner un ref stable via son +// propre useCallback([piece.id]) — un ref créé comme fonction fléchée +// inline dans une boucle de rendu est une NOUVELLE fonction à chaque +// rendu, ce que React interprète comme "le ref a changé" : il rappelle +// alors la fonction avec null puis avec le nœud, pour CHAQUE pièce déjà +// posée (pas seulement celle qui vient de changer), à chaque mise à jour +// de `pieces` (chaque lâcher, chaque fusion). Sur un plateau qui accumule +// des dizaines de pièces après plusieurs minutes de jeu, ce cycle inutile +// devient un vrai coût CPU répété très souvent — plausiblement la vraie +// cause du ralentissement/rechargement forcé constaté sur iPhone (la +// fuite du Set de fusion, corrigée séparément, n'était pas suffisante à +// elle seule). useCallback garde la même fonction tant que piece.id ne +// change pas, ce qu'une fonction fléchée inline ne peut pas faire. +function PieceView({ + piece, + citizen, + diameter, + registerNode, +}: { + piece: Piece; + citizen: MemberRow | undefined; + diameter: number; + registerNode: (id: number, node: HTMLDivElement | null) => void; +}) { + const ref = useCallback((node: HTMLDivElement | null) => registerNode(piece.id, node), [piece.id, registerNode]); + + return ( +
+ {/* La position/rotation (transform) de ce conteneur est pilotée + impérativement à chaque frame depuis la boucle physique — + l'animation d'apparition vit sur cet enfant, jamais sur le + conteneur, pour ne pas entrer en conflit avec transform. Idem + pour animate-danger-blink (ajoutée/retirée sur ce même conteneur + par la boucle physique quand la pièce dépasse la ligne de + danger). rounded-full ici : sans ça le halo de l'animation + dessinait un carré visible autour du rond. animate-fade-in (pas + animate-reveal) : ce dernier passe par 82% puis 104% avant de se + stabiliser à 100%, ce qui faisait paraître une pièce tout juste + lâchée d'une taille différente de celle annoncée dans la + preview. */} +
+ +
+
+ ); +} + export type MelonRunTelemetry = { v: 1; duration_ms: number; @@ -52,6 +100,13 @@ export function MelonGame({ const previewRef = useRef(null); const guideLineRef = useRef(null); const nodeRefs = useRef(new Map()); + // Stable (jamais recréée) : passée à PieceView, qui la mémorise elle-même + // par pièce via son propre useCallback([piece.id]) — voir la remarque + // juste au-dessus de PieceView sur pourquoi ça compte. + const registerPieceNode = useCallback((id: number, node: HTMLDivElement | null) => { + if (node) nodeRefs.current.set(id, node); + else nodeRefs.current.delete(id); + }, []); // Un palier de taille par Citoyen, pas un nombre arbitraire — voir // buildTierRadii. citizens[tier] (indexation directe, sans tirage) donne @@ -484,36 +539,15 @@ export function MelonGame({ className="pointer-events-none absolute top-0 bottom-0 w-px border-l border-dashed border-gold/40" /> - {pieces.map((piece) => { - const citizen = citizens[piece.tier]; - const diameter = tierRadii[piece.tier] * 2; - return ( -
{ - if (node) nodeRefs.current.set(piece.id, node); - }} - className="pointer-events-none absolute top-0 left-0" - > - {/* La position/rotation (transform) de ce conteneur est - pilotée impérativement à chaque frame depuis la boucle - physique — l'animation d'apparition vit sur cet enfant, - jamais sur le conteneur, pour ne pas entrer en conflit - avec transform. Idem pour animate-danger-blink (ajoutée/ - retirée sur ce même conteneur par la boucle physique quand - la pièce dépasse la ligne de danger). rounded-full ici : - sans ça le halo de l'animation dessinait un carré visible - autour du rond. animate-fade-in (pas animate-reveal) : ce - dernier passe par 82% puis 104% avant de se stabiliser à - 100%, ce qui faisait paraître une pièce tout juste lâchée - d'une taille différente de celle annoncée dans la - preview. */} -
- -
-
- ); - })} + {pieces.map((piece) => ( + + ))}
{