Commit e26cb40f authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(ludo): JUICY flat dice — CSS dot rendering, slam, shake, golden glow on 6

Dice overhaul:
- Flat white die with CSS-rendered dots (3x3 grid, proper pip layout)
- Roll animation: rapid random faces + violent shake + blur (55ms per frame, 14 frames)
- SLAM landing: scale 1.25 → 1.0 with spring easing
- Heavy haptic on every roll
- Dice area shakes on land
- Rolling 6: golden box-shadow glow + 8 star particles + success haptic
- Bot rolls: same dot renderer with subtle scale pulse
- Button fades to 0.5 opacity when disabled (clear visual state)
- Faster animation interval (55ms vs 70ms) for more frantic feel
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 8d556485
...@@ -35,9 +35,10 @@ export function mountGame(el, params) { ...@@ -35,9 +35,10 @@ export function mountGame(el, params) {
<div class="pp" id="pp-2" style="--pc:${COLORS[2]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[2]}</span></div> <div class="pp" id="pp-2" style="--pc:${COLORS[2]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[2]}</span></div>
<div class="pp" id="pp-3" style="--pc:${COLORS[3]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[3]}</span></div> <div class="pp" id="pp-3" style="--pc:${COLORS[3]};"><div class="pp-dot"></div><span>${PLAYER_NAMES[3]}</span></div>
</div> </div>
<div style="display:flex;align-items:center;gap:16px;padding:12px 16px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.06);justify-content:center;"> <div id="dice-area" style="display:flex;align-items:center;gap:16px;padding:14px 16px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.06);justify-content:center;">
<div id="dice-box" style="width:56px;height:56px;background:#f8fafc;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:26px;font-weight:900;color:#1a1a2e;box-shadow:0 3px 10px rgba(0,0,0,0.3);">🎲</div> <div id="dice-box" style="width:62px;height:62px;background:#f8fafc;border-radius:12px;display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:8px;box-shadow:0 4px 14px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.8);transition:transform 0.15s cubic-bezier(0.34,1.56,0.64,1);">
<button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:14px 36px;">ارمِ النرد</button> </div>
<button class="btn btn-primary" id="roll-btn" style="font-size:16px;padding:16px 40px;min-height:56px;letter-spacing:0.5px;">ارمِ النرد</button>
</div> </div>
</div> </div>
<style> <style>
...@@ -56,6 +57,7 @@ export function mountGame(el, params) { ...@@ -56,6 +57,7 @@ export function mountGame(el, params) {
c.style.cssText = 'border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.4);'; c.style.cssText = 'border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,0.4);';
canvas = c; ctx = cx; canvas = c; ctx = cx;
renderDiceFace(el.querySelector('#dice-box'), 1);
drawBoard(); drawBoard();
updatePanels(el); updatePanels(el);
el.querySelector('#roll-btn').addEventListener('click', () => handleRoll(el)); el.querySelector('#roll-btn').addEventListener('click', () => handleRoll(el));
...@@ -70,41 +72,75 @@ export function mountGame(el, params) { ...@@ -70,41 +72,75 @@ export function mountGame(el, params) {
bus.emit('game:started', { gameKey: 'ludo', mode }); bus.emit('game:started', { gameKey: 'ludo', mode });
} }
function renderDiceFace(diceBox, value) {
// Flat dice with CSS dots in a 3x3 grid
const dots = {
1: [0,0,0,0,1,0,0,0,0],
2: [0,0,1,0,0,0,1,0,0],
3: [0,0,1,0,1,0,1,0,0],
4: [1,0,1,0,0,0,1,0,1],
5: [1,0,1,0,1,0,1,0,1],
6: [1,0,1,1,0,1,1,0,1],
};
const pattern = dots[value] || dots[1];
diceBox.innerHTML = pattern.map(d =>
`<div style="width:10px;height:10px;border-radius:50%;background:${d ? '#1a1a2e' : 'transparent'};margin:auto;transition:transform 0.1s;${d ? 'box-shadow:0 1px 2px rgba(0,0,0,0.3);' : ''}"></div>`
).join('');
}
function handleRoll(el) { function handleRoll(el) {
if (diceAnimating || game.rolled || game.gameOver || game.currentPlayer !== 0) return; if (diceAnimating || game.rolled || game.gameOver || game.currentPlayer !== 0) return;
diceAnimating = true; diceAnimating = true;
const btn = el.querySelector('#roll-btn'); const btn = el.querySelector('#roll-btn');
const diceBox = el.querySelector('#dice-box'); const diceBox = el.querySelector('#dice-box');
const diceArea = el.querySelector('#dice-area');
btn.disabled = true; btn.disabled = true;
btn.style.opacity = '0.5';
const faces = ['⚀','⚁','⚂','⚃','⚄','⚅']; // Rapid shake animation with random faces
let count = 0; let count = 0;
const anim = setInterval(() => { const shakeAnim = setInterval(() => {
diceBox.textContent = faces[Math.floor(Math.random()*6)]; const randVal = Math.floor(Math.random()*6)+1;
diceBox.style.transform = `rotate(${Math.random()*20-10}deg)`; renderDiceFace(diceBox, randVal);
const rx = (Math.random()-0.5)*16;
const ry = (Math.random()-0.5)*16;
const rot = (Math.random()-0.5)*30;
diceBox.style.transform = `translate(${rx}px,${ry}px) rotate(${rot}deg) scale(0.9)`;
count++; count++;
if (count > 10) { if (count > 14) {
clearInterval(anim); clearInterval(shakeAnim);
// SLAM — final value
const dice = rules.rollDice(); const dice = rules.rollDice();
game.diceValue = dice; game.diceValue = dice;
game.rolled = true; game.rolled = true;
diceBox.textContent = faces[dice-1]; renderDiceFace(diceBox, dice);
diceBox.style.transform = 'none';
// Slam effect
diceBox.style.transform = 'scale(1.25)';
setTimeout(() => { diceBox.style.transform = 'scale(1)'; }, 150);
diceAnimating = false; diceAnimating = false;
audio.play('dice', 'game'); audio.play('dice', 'game');
juice.hapticMedium(); juice.hapticHeavy();
juice.slamIn(diceBox, { scale: 1.3, duration: 300 }); juice.shake(diceArea, 3, 150);
if (dice === 6) juice.starBurst(diceBox.getBoundingClientRect().left+28, diceBox.getBoundingClientRect().top+28, 5);
// 6 = golden glow + stars
if (dice === 6) {
diceBox.style.boxShadow = '0 0 20px #E4AC38, 0 4px 14px rgba(0,0,0,0.4)';
const rect = diceBox.getBoundingClientRect();
juice.starBurst(rect.left+31, rect.top+31, 8);
juice.hapticSuccess();
setTimeout(() => { diceBox.style.boxShadow = '0 4px 14px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.8)'; }, 600);
}
validMoves = rules.getValidMoves(game, 0, dice); validMoves = rules.getValidMoves(game, 0, dice);
if (validMoves.length === 0) { if (validMoves.length === 0) {
setTimeout(() => { game.rolled = false; rules.nextTurn(game); btn.disabled = false; updatePanels(el); drawBoard(); if (game.currentPlayer !== 0) botLoop(el); }, 800); setTimeout(() => { game.rolled = false; rules.nextTurn(game); btn.disabled = false; btn.style.opacity = '1'; updatePanels(el); drawBoard(); if (game.currentPlayer !== 0) botLoop(el); }, 800);
} else { } else {
const best = validMoves.find(m=>m.type==='capture') || validMoves.find(m=>m.type==='finish') || validMoves.find(m=>m.type==='enter') || validMoves[0]; const best = validMoves.find(m=>m.type==='capture') || validMoves.find(m=>m.type==='finish') || validMoves.find(m=>m.type==='enter') || validMoves[0];
setTimeout(() => doMove(el, best), 400); setTimeout(() => doMove(el, best), 400);
} }
} }
}, 70); }, 55);
} }
function doMove(el, move) { function doMove(el, move) {
...@@ -119,7 +155,7 @@ function doMove(el, move) { ...@@ -119,7 +155,7 @@ function doMove(el, move) {
updatePanels(el); updatePanels(el);
drawBoard(); drawBoard();
const btn = el.querySelector('#roll-btn'); const btn = el.querySelector('#roll-btn');
if (game.currentPlayer === 0) btn.disabled = false; if (game.currentPlayer === 0) { btn.disabled = false; btn.style.opacity = '1'; }
else setTimeout(() => botLoop(el), 400); else setTimeout(() => botLoop(el), 400);
} }
...@@ -128,8 +164,9 @@ function botLoop(el) { ...@@ -128,8 +164,9 @@ function botLoop(el) {
const dice = rules.rollDice(); const dice = rules.rollDice();
game.diceValue = dice; game.diceValue = dice;
const diceBox = el.querySelector('#dice-box'); const diceBox = el.querySelector('#dice-box');
const faces = ['⚀','⚁','⚂','⚃','⚄','⚅']; renderDiceFace(diceBox, dice);
diceBox.textContent = faces[dice-1]; diceBox.style.transform = 'scale(1.1)';
setTimeout(() => { diceBox.style.transform = 'scale(1)'; }, 100);
const move = rules.getBotMove(game, game.currentPlayer, dice); const move = rules.getBotMove(game, game.currentPlayer, dice);
if (move) { rules.applyMove(game, game.currentPlayer, move); if (move.type === 'capture') audio.play('capture','game'); } if (move) { rules.applyMove(game, game.currentPlayer, move); if (move.type === 'capture') audio.play('capture','game'); }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment