Dados que de verdad ruedan: la nueva tirada en 3D
Los dados del tablero ahora son cubos 3D reales que toman impulso, ruedan y caen en tus números: sin demoras falsas y considerados con quien reduce el movimiento.

En palabras sencillas
Antes la tirada era plana: los números simplemente se volteaban. Ahora los dos dados de movimiento, y el dado Rápido, son cubos 3D reales que se agachan, saltan y ruedan por el aire antes de posarse boca arriba sobre la mesa.
Aquí está lo bueno: los dados siguen girando solo mientras el juego calcula tu jugada, y luego caen y se posan en los números exactos que sacaste. No hay una pausa falsa de relleno: en cuanto tu resultado está listo, caen. Con una conexión rápida, todo termina en un abrir y cerrar de ojos.
- Un impulso rápido en cuanto tocas Tirar, para que se sienta receptivo de inmediato.
- Una rodada real por el aire mientras se calcula tu jugada: se alarga de forma natural si el servidor va lento, en lugar de mostrar un marcador de posición.
- Un aterrizaje suave sobre tus números reales, con tu ficha empezando a moverse en cuanto los dados se posan.
- El dado Rápido también se suma, y sigue mostrando un discreto «?» entre tiradas para que siempre sepas que está en juego.

Es un cambio puramente visual: las reglas, las probabilidades y el tablero se juegan exactamente igual que antes. Los dados por fin se sienten como dados.
Para los más técnicos
Cada dado es un cubo CSS 3D de seis caras impulsado por una pequeña máquina de estados que recorre idle → windup → loop → settle → jitter → landed. La orientación se almacena como un quaternion para poder rotar alrededor de cualquier eje y convertirla en un único rotate3d() de CSS por fotograma; FACE_Q asigna cada valor 1–6 a la orientación que trae al frente esa cara de puntos.
// 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),
};La tirada es optimista: un toque incrementa un nonce optimista, lo que dispara el windup de 180ms y un bucle de rodada indefinido. Cuando llega la tirada autoritativa (un incremento de rollSeq), el dado que gira se posa de inmediato siguiendo la rotación hacia delante más corta hasta FACE_Q[value] y cae: no hay un mínimo de giro, así que una respuesta rápida produce una tirada rápida.
// 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 */);
});
}Por decisión del propietario se eliminaron los rebotes de aterrizaje decrecientes del handoff (restitución 0), de modo que el asentamiento es un único descenso suave y acelerado que toca exactamente el valor, dejando solo una breve oscilación amortiguada. Un único bucle rAF compartido se ejecuta solo mientras un dado se anima, cada fase usa marcas de tiempo absolutas de performance.now() (para que una pestaña en segundo plano avance correctamente) y un respaldo se posa en los valores actuales del store si no llega ninguna tirada. La reducción de movimiento se salta por completo el nonce optimista y simplemente cambia las caras. Solo en la web: el motor, el daemon y la base de datos quedan intactos.
