Кубики, которые по-настоящему кувыркаются: новый 3D-бросок
Кубики на доске теперь настоящие 3D-кубики: они замахиваются, кувыркаются и приземляются на ваши числа — без поддельной задержки и бережно к режиму уменьшения движения.

Простыми словами
Раньше бросок был плоским: числа просто переворачивались. Теперь два кубика движения — и скоростной кубик — это настоящие 3D-кубики, которые приседают, подпрыгивают и кувыркаются в воздухе, прежде чем улечься нужной гранью вверх на столе.
А вот и приятная часть: кубики крутятся лишь пока игра вычисляет ваш ход, а затем падают и приземляются ровно на те числа, что вы выбросили. Никакой искусственной паузы для вида — как только результат готов, они падают. При быстром соединении всё заканчивается в мгновение ока.
- Быстрый замах в тот же миг, как вы нажимаете «Бросить», так что отклик чувствуется сразу.
- Настоящий полёт с кувырками, пока вычисляется ваш ход, — он естественно растягивается, если сервер медлит, вместо того чтобы показывать заглушку.
- Плавное приземление на ваши настоящие числа, и фишка трогается с места в тот момент, когда кубики улеглись.
- Скоростной кубик тоже участвует и по-прежнему показывает тихий «?» между бросками, чтобы вы всегда знали, что он в игре.

Это чисто визуальное изменение — правила, шансы и доска играются точно так же, как прежде. Просто кубики наконец-то ощущаются как кубики.
Для тех, кому интересны детали
Каждый кубик — это шестигранный 3D-куб на CSS, управляемый небольшим конечным автоматом, который проходит idle → windup → loop → settle → jitter → landed. Ориентация хранится в виде quaternion, чтобы можно было вращать вокруг любой оси и преобразовывать в один CSS rotate3d() за кадр; FACE_Q сопоставляет каждому значению 1–6 ориентацию, которая выводит нужную грань с точками вперёд.
// 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),
};Бросок оптимистичный: нажатие увеличивает оптимистичный nonce, запуская 180ms-замах windup и бесконечный цикл кувырка loop. Когда приходит авторитетный бросок (увеличение rollSeq), крутящийся кубик немедленно укладывается по кратчайшему повороту вперёд на FACE_Q[value] и падает — никакого минимального порога вращения, поэтому быстрый ответ даёт быстрый бросок.
// 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 */);
});
}По решению владельца затухающие отскоки при приземлении из хэндоффа были убраны (restitution 0), так что укладка — это одно плавное ускоряющееся падение, которое касается ровно нужного значения, оставляя лишь короткое затухающее покачивание. Один общий цикл rAF работает только пока кубик анимируется, каждая фаза использует абсолютные метки времени performance.now() (чтобы вкладка в фоне корректно прокручивалась вперёд), а запасной механизм укладывает кубики на текущие значения из стора, если бросок так и не приходит. Режим уменьшения движения полностью пропускает оптимистичный nonce и просто меняет грани. Только веб: движок, daemon и база данных не затронуты.
