Des dés qui culbutent vraiment : le nouveau lancer en 3D
Les dés du plateau sont désormais de vrais cubes 3D qui prennent leur élan, culbutent et retombent sur vos chiffres — sans fausse attente, et tout en douceur avec le mode mouvement réduit.

En clair
Avant, le lancer était plat : les chiffres se contentaient de se retourner. Maintenant, les deux dés de déplacement — et le dé Rapide — sont de vrais cubes 3D qui s’accroupissent, bondissent et culbutent dans les airs avant de se poser face visible sur la table.
Et voici le plus agréable : les dés ne tournent que pendant que la partie calcule votre déplacement, puis ils retombent et se posent exactement sur les chiffres que vous avez obtenus. Pas de fausse pause de remplissage — dès que votre résultat est prêt, ils tombent. Sur une connexion rapide, tout est terminé en un clin d’œil.
- Un élan rapide à l’instant où vous appuyez sur Lancer, pour une sensation de réactivité immédiate.
- Une vraie culbute dans les airs pendant que votre déplacement est calculé — elle s’allonge naturellement si le serveur est lent, au lieu d’afficher un espace réservé.
- Un atterrissage en douceur sur vos chiffres réels, votre pion commençant à bouger dès que les dés se posent.
- Le dé Rapide se joint à la fête lui aussi, et affiche toujours un discret « ? » entre les lancers pour que vous sachiez toujours qu’il est en jeu.

C’est un changement purement visuel — les règles, les probabilités et le plateau se jouent exactement comme avant. Les dés ont enfin la sensation de vrais dés.
Pour les curieux techniques
Chaque dé est un cube CSS 3D à six faces piloté par une petite machine à états qui parcourt idle → windup → loop → settle → jitter → landed. L’orientation est stockée sous forme de quaternion afin de pouvoir pivoter autour de n’importe quel axe et de la convertir en un seul rotate3d() CSS par image ; FACE_Q associe chaque valeur 1–6 à l’orientation qui amène cette face de points vers l’avant.
// apps/web/app/[locale]/game/[id]/dice3d.ts
// quaternion per face → one CSS rotate3d() at rest
const FACE_Q: Record<DieValue, Quat> = {
1: Q.identity,
2: Q.axis([0, 1, 0], +Math.PI / 2),
3: Q.axis([0, 1, 0], -Math.PI / 2),
4: Q.axis([1, 0, 0], +Math.PI / 2),
5: Q.axis([1, 0, 0], -Math.PI / 2),
6: Q.axis([1, 0, 0], Math.PI),
};Le lancer est optimiste : un appui incrémente un nonce optimiste, ce qui déclenche le windup de 180ms et une boucle de culbute loop indéfinie. Quand le lancer faisant autorité arrive (un incrément de rollSeq), le dé en rotation se pose immédiatement en suivant la plus courte rotation vers l’avant jusqu’à FACE_Q[value] puis tombe — il n’y a pas de plancher de rotation minimal, donc une réponse rapide donne un lancer rapide.
// authoritative values arrive → settle now, no spin floor
onRollSeq(values) {
values.forEach((v, i) => {
if (controller[i].isSpinning) controller[i].setResult(v, arrival + i * 78);
else controller[i].startRoll(arrival), controller[i].setResult(v, /* after windup */);
});
}Par décision du propriétaire, les rebonds d’atterrissage décroissants du handoff ont été supprimés (restitution 0), si bien que la pose est une seule descente fluide et accélérée qui touche exactement la valeur, ne laissant qu’une brève oscillation amortie. Une unique boucle rAF partagée ne s’exécute que pendant qu’un dé est animé, chaque phase utilise des horodatages absolus de performance.now() (pour qu’un onglet en arrière-plan avance correctement), et un filet de sécurité se pose sur les valeurs actuelles du store si aucun lancer n’arrive. Le mouvement réduit saute entièrement le nonce optimiste et se contente de changer les faces. Web uniquement : le moteur, le daemon et la base de données restent intacts.
