Dados que de fato rolam: o novo lançamento em 3D
Os dados do tabuleiro agora são cubos 3D de verdade que tomam impulso, rolam e param nos seus números — sem atraso falso e gentis com quem reduz o movimento.

Em palavras simples
Antes o lançamento era plano: os números apenas viravam. Agora os dois dados de movimento — e o dado Rápido — são cubos 3D de verdade que se agacham, saltam e rolam pelo ar antes de pousar com a face para cima na mesa.
E aqui está a parte boa: os dados só continuam girando enquanto o jogo calcula a sua jogada, e então caem e param exatamente nos números que você tirou. Não há pausa falsa de enchimento — assim que o seu resultado fica pronto, eles caem. Em uma conexão rápida, tudo acaba num piscar de olhos.
- Um impulso rápido no instante em que você toca em Lançar, para parecer responsivo na hora.
- Uma rolagem de verdade pelo ar enquanto a sua jogada é calculada — ela se estende naturalmente se o servidor estiver lento, em vez de mostrar um marcador de posição.
- Um pouso suave nos seus números reais, com o seu peão começando a se mover assim que os dados param.
- O dado Rápido também entra na brincadeira e ainda mostra um discreto «?» entre os lançamentos para você sempre saber que ele está em jogo.

É uma mudança puramente visual — as regras, as probabilidades e o tabuleiro funcionam exatamente como antes. Os dados finalmente parecem dados de verdade.
Para os mais técnicos
Cada dado é um cubo CSS 3D de seis faces movido por uma pequena máquina de estados que percorre idle → windup → loop → settle → jitter → landed. A orientação é armazenada como um quaternion para que possamos girar em torno de qualquer eixo e converter em um único rotate3d() de CSS por quadro; FACE_Q mapeia cada valor 1–6 para a orientação que traz aquela face de pontos para a frente.
// 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),
};O lançamento é otimista: um toque incrementa um nonce otimista, disparando o windup de 180ms e um loop de rolagem indefinido. Quando o lançamento autoritativo chega (um incremento de rollSeq), o dado em rotação se acomoda imediatamente pela rotação para a frente mais curta até FACE_Q[value] e cai — não há piso mínimo de giro, então uma resposta rápida resulta num lançamento rápido.
// 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 decisão do dono, os ressaltos de pouso decrescentes do handoff foram removidos (restituição 0), de modo que a acomodação é uma única descida suave e acelerada que toca exatamente o valor, deixando apenas uma breve oscilação amortecida. Um único loop rAF compartilhado roda apenas enquanto um dado está animando, cada fase usa marcas de tempo absolutas de performance.now() (para que uma aba em segundo plano avance corretamente), e um mecanismo de segurança se acomoda nos valores atuais do store se nenhum lançamento chegar. A redução de movimento pula o nonce otimista por completo e apenas troca as faces. Somente web: o motor, o daemon e o banco de dados ficam intactos.
