نرد يتدحرج فعلاً: رمية ثلاثية الأبعاد الجديدة
أصبح نرد اللوحة الآن مكعبات ثلاثية الأبعاد حقيقية تتأهب وتتدحرج وتستقر على أرقامك، بلا تأخير زائف، ولطيفة مع وضع تقليل الحركة.

بكلمات بسيطة
كانت الرمية مسطحة من قبل: مجرد أرقام تنقلب. أما الآن فمكعّبا الحركة، إلى جانب النرد السريع، أصبحت مكعبات ثلاثية الأبعاد حقيقية تنحني ثم تقفز وتتدحرج في الهواء قبل أن تستقر بوجهها لأعلى على الطاولة.
وإليك الجزء اللطيف: يستمر النرد في الدوران فقط بينما تحسب اللعبة حركتك، ثم يسقط ويستقر على الأرقام نفسها التي رميتها. لا توجد وقفة زائفة محشوّة، فما إن تجهز نتيجتك حتى يسقط النرد. وعلى اتصال سريع ينتهي كل ذلك في طرفة عين.
- تأهّب سريع في اللحظة التي تنقر فيها على ارمِ النرد، فيبدو مستجيباً على الفور.
- تدحرج حقيقي في الهواء بينما تُحسب حركتك، ويمتد بشكل طبيعي إن كان الخادم بطيئاً بدلاً من عرض عنصر نائب.
- هبوط سلس على أرقامك الفعلية، مع بدء قطعتك بالتحرك في اللحظة التي يستقر فيها النرد.
- ينضم النرد السريع أيضاً، ولا يزال يعرض علامة «؟» هادئة بين الرميات لتعرف دائماً أنه قيد اللعب.

إنه تغيير بصري بحت، فالقواعد والاحتمالات واللوحة كلها تُلعب تماماً كما كانت. صار النرد أخيراً يشبه النرد فعلاً.
للمهتمين بالتفاصيل التقنية
كل قطعة نرد هي مكعب CSS ثلاثي الأبعاد بستة أوجه يحرّكه آلة حالات صغيرة تمر بـ idle → windup → loop → settle → jitter → landed. يُخزَّن الاتجاه على هيئة quaternion كي نتمكن من الدوران حول أي محور والتحويل إلى rotate3d() واحدة في CSS لكل إطار؛ ويربط 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 متفائلاً، فتُطلق تأهب الـ windup مدته 180ms وحلقة تدحرج 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 صفر)، فصار الاستقرار هبوطاً واحداً سلساً متسارعاً يلامس القيمة بالضبط، ولا يترك سوى اهتزاز مكبوح قصير. تعمل حلقة rAF مشتركة واحدة فقط أثناء تحريك قطعة نرد، وتستخدم كل مرحلة طوابع زمنية مطلقة من performance.now() (كي يقفز التبويب الخلفي إلى الأمام بشكل صحيح)، ويستقر احتياطٌ على القيم الحالية في المتجر إن لم تصل أي رمية. ووضع تقليل الحركة يتخطى الـ nonce المتفائل تماماً ويكتفي بتبديل الأوجه. ويب فقط: المحرك والخادم وقاعدة البيانات لم تُمَس.
