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) => (
+
+ ))}
{