真正会翻滚的骰子:全新 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 loop 仅在有骰子正在动画时运行,每个阶段都使用 performance.now() 的绝对时间戳(这样后台标签页能正确快进),若没有掷骰结果到达,则有一个兜底机制停到 store 中的当前点数。减少动态效果会完全跳过乐观 nonce,只是切换骰面。仅限网页端:引擎、daemon 和数据库均未改动。
