Commit 92bb521f authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: overhaul Ludo multiplayer UI/UX for turn transitions

- Replace opacity 0.3 dim with proper contextual states
- Add turn-status badge showing "دورك!" / "دور [player]" / "Bot يفكر..."
- Roll button shows contextual text: "ارمِ النرد" / "اختر قطعة" / "انتظر دورك" / "Bot يلعب..."
- Dice box stays visible during opponent turns (greyed, not hidden)
- Emote button always accessible (social action, not gated by turn)
- Smooth CSS transitions on dice-area background, button color, dice opacity
- Flash notification (inset glow + haptic) when turn comes to you
- Timer bar fades more during opponent turn for clearer contrast
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 3d56ef64
...@@ -127,13 +127,14 @@ export function mountGame(el, params) { ...@@ -127,13 +127,14 @@ export function mountGame(el, params) {
${panels.length > 3 ? renderPanel(panels[3]) : '<div></div>'} ${panels.length > 3 ? renderPanel(panels[3]) : '<div></div>'}
</div> </div>
<div id="turn-timer-bar" style="height:4px;background:rgba(255,255,255,0.05);position:relative;overflow:hidden;"><div id="turn-timer-fill" style="position:absolute;inset:0;background:linear-gradient(90deg,#4ade80,#E4AC38,#EF4444);transform:scaleX(1);transform-origin:left;transition:none;"></div></div> <div id="turn-timer-bar" style="height:4px;background:rgba(255,255,255,0.05);position:relative;overflow:hidden;"><div id="turn-timer-fill" style="position:absolute;inset:0;background:linear-gradient(90deg,#4ade80,#E4AC38,#EF4444);transform:scaleX(1);transform-origin:left;transition:none;"></div></div>
<div id="dice-area" style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:linear-gradient(180deg,#12122a,#0a0a1a);border-top:1px solid rgba(228,172,56,0.15);justify-content:center;padding-bottom:max(14px, env(safe-area-inset-bottom, 0px));position:relative;overflow:hidden;"> <div id="dice-area" style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:linear-gradient(180deg,#12122a,#0a0a1a);border-top:1px solid rgba(228,172,56,0.15);justify-content:center;padding-bottom:max(14px, env(safe-area-inset-bottom, 0px));position:relative;overflow:hidden;transition:background 0.4s ease;">
<div style="position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0%,rgba(228,172,56,0.06) 0%,transparent 70%);pointer-events:none;"></div> <div style="position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0%,rgba(228,172,56,0.06) 0%,transparent 70%);pointer-events:none;"></div>
<button class="btn btn-secondary" id="exit-btn" style="min-height:44px;min-width:44px;padding:0;font-size:13px;color:#EF4444;border-radius:50%;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);">✕</button> <button class="btn btn-secondary" id="exit-btn" style="min-height:44px;min-width:44px;padding:0;font-size:13px;color:#EF4444;border-radius:50%;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2);">✕</button>
<button class="btn btn-secondary" id="emote-btn" style="min-height:44px;min-width:44px;padding:0;font-size:18px;border-radius:50%;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);">😄</button> <button class="btn btn-secondary" id="emote-btn" style="min-height:44px;min-width:44px;padding:0;font-size:18px;border-radius:50%;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);">😄</button>
<div id="dice-box" style="width:56px;height:56px;background:linear-gradient(145deg,#ffffff,#f0ede8);border-radius:12px;display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:7px;box-shadow:0 4px 12px rgba(0,0,0,0.4),inset 0 2px 0 rgba(255,255,255,0.9),0 0 0 2px rgba(228,172,56,0.15);transition:transform 0.15s cubic-bezier(0.34,1.56,0.64,1);"> <div id="dice-box" style="width:56px;height:56px;background:linear-gradient(145deg,#ffffff,#f0ede8);border-radius:12px;display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:7px;box-shadow:0 4px 12px rgba(0,0,0,0.4),inset 0 2px 0 rgba(255,255,255,0.9),0 0 0 2px rgba(228,172,56,0.15);transition:transform 0.15s cubic-bezier(0.34,1.56,0.64,1),opacity 0.3s ease,filter 0.3s ease;">
</div> </div>
<button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:14px 32px;min-height:52px;border-radius:14px;background:linear-gradient(135deg,#E4AC38,#F59E0B);box-shadow:0 4px 14px rgba(228,172,56,0.3);font-weight:800;" disabled>ارمِ النرد</button> <button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:14px 32px;min-height:52px;border-radius:14px;background:linear-gradient(135deg,#E4AC38,#F59E0B);box-shadow:0 4px 14px rgba(228,172,56,0.3);font-weight:800;transition:background 0.3s ease,color 0.3s ease,opacity 0.3s ease;" disabled>ارمِ النرد</button>
<div id="turn-status" style="position:absolute;top:0;left:50%;transform:translateX(-50%);font-size:11px;font-weight:700;padding:2px 12px;border-radius:0 0 8px 8px;display:none;"></div>
</div> </div>
</div> </div>
<style> <style>
...@@ -299,6 +300,7 @@ async function handleRoll(el) { ...@@ -299,6 +300,7 @@ async function handleRoll(el) {
game.diceValue = dice; game.diceValue = dice;
game.rolled = true; game.rolled = true;
diceAnimating = false; diceAnimating = false;
updatePanels(el);
// Star burst on 6 // Star burst on 6
if (dice === 6) { if (dice === 6) {
...@@ -579,7 +581,7 @@ function startLudoPolling(el) { ...@@ -579,7 +581,7 @@ function startLudoPolling(el) {
if (isMyTurn()) { if (isMyTurn()) {
stopLudoPolling(); stopLudoPolling();
audio.play('sfx_turn_start', 'ui'); audio.play('sfx_turn_start', 'ui');
// Reset dice display for my fresh turn notifyMyTurn(el);
const mainDice = el.querySelector('#dice-box'); const mainDice = el.querySelector('#dice-box');
if (mainDice) renderDiceFace(mainDice, 1); if (mainDice) renderDiceFace(mainDice, 1);
} else { } else {
...@@ -1113,25 +1115,86 @@ function updatePanels(el) { ...@@ -1113,25 +1115,86 @@ function updatePanels(el) {
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 timerBar = el.querySelector('#turn-timer-bar'); const timerBar = el.querySelector('#turn-timer-bar');
const turnStatus = el.querySelector('#turn-status');
const myTurn = isMyTurn() && !game.players[myPlayerIndex].finished && !game.gameOver; const myTurn = isMyTurn() && !game.players[myPlayerIndex].finished && !game.gameOver;
const currentName = PLAYER_NAMES[game.currentPlayer] || '';
const isBot = currentName.startsWith('Bot');
// Dice area: always visible, style changes per state
if (diceArea) { if (diceArea) {
diceArea.style.opacity = myTurn ? '1' : '0.3'; diceArea.style.opacity = '1';
diceArea.style.pointerEvents = myTurn ? 'auto' : 'none'; diceArea.style.pointerEvents = 'auto';
if (!myTurn) {
diceArea.style.background = 'linear-gradient(180deg,#0a0a18,#060612)';
} else {
diceArea.style.background = 'linear-gradient(180deg,#12122a,#0a0a1a)';
}
}
// Turn status badge at top of dice area
if (turnStatus) {
if (game.gameOver) {
turnStatus.style.display = 'none';
} else if (myTurn) {
turnStatus.style.display = 'block';
turnStatus.style.background = 'linear-gradient(135deg,#E4AC38,#F59E0B)';
turnStatus.style.color = '#000';
turnStatus.textContent = 'دورك!';
turnStatus.style.animation = 'fadeIn 0.3s ease-out';
} else {
turnStatus.style.display = 'block';
turnStatus.style.background = 'rgba(255,255,255,0.08)';
turnStatus.style.color = '#94a3b8';
turnStatus.textContent = isBot ? `${currentName} يفكر...` : `دور ${currentName}`;
turnStatus.style.animation = 'fadeIn 0.3s ease-out';
}
}
// Dice box: always visible (shows last roll or opponent animation)
if (diceBox) {
diceBox.style.visibility = 'visible';
if (!myTurn && !diceAnimating) {
diceBox.style.opacity = '0.4';
diceBox.style.filter = 'grayscale(0.5)';
} else {
diceBox.style.opacity = '1';
diceBox.style.filter = 'none';
}
} }
if (diceBox) diceBox.style.visibility = myTurn || diceAnimating ? 'visible' : 'hidden';
// Roll button: contextual text, never hidden
if (btn) { if (btn) {
const canRoll = myTurn && !game.rolled && !diceAnimating; const canRoll = myTurn && !game.rolled && !diceAnimating;
btn.disabled = !canRoll; if (myTurn) {
btn.style.opacity = canRoll ? '1' : '0.4'; btn.style.visibility = 'visible';
btn.style.visibility = myTurn ? 'visible' : 'hidden'; btn.disabled = !canRoll;
if (canRoll && !turnTimer) { btn.textContent = game.rolled ? 'اختر قطعة' : 'ارمِ النرد';
startTurnTimer(el, () => handleRoll(el)); btn.style.opacity = canRoll ? '1' : '0.5';
btn.style.background = 'linear-gradient(135deg,#E4AC38,#F59E0B)';
btn.style.color = '#000';
if (canRoll && !turnTimer) {
startTurnTimer(el, () => handleRoll(el));
}
} else {
btn.style.visibility = 'visible';
btn.disabled = true;
btn.textContent = isBot ? 'Bot يلعب...' : 'انتظر دورك';
btn.style.opacity = '0.5';
btn.style.background = 'rgba(255,255,255,0.06)';
btn.style.color = '#64748b';
} }
} }
if (timerBar) timerBar.style.opacity = myTurn ? '1' : '0.3';
// Emote button: always enabled (social action)
const emoteBtn = el.querySelector('#emote-btn');
if (emoteBtn) {
emoteBtn.style.pointerEvents = 'auto';
emoteBtn.style.opacity = '1';
}
if (timerBar) timerBar.style.opacity = myTurn ? '1' : '0.15';
if (!myTurn) { if (!myTurn) {
clearTurnTimer(); clearTurnTimer();
// Safety: if diceAnimating is stuck and it's not our turn, force reset
if (diceAnimating && !game.rolled) diceAnimating = false; if (diceAnimating && !game.rolled) diceAnimating = false;
} }
} }
...@@ -1162,6 +1225,18 @@ function clearTurnTimer() { ...@@ -1162,6 +1225,18 @@ function clearTurnTimer() {
if (turnTimer) { cancelAnimationFrame(turnTimer); turnTimer = null; } if (turnTimer) { cancelAnimationFrame(turnTimer); turnTimer = null; }
} }
function notifyMyTurn(el) {
const diceArea = el.querySelector('#dice-area');
if (diceArea) {
diceArea.animate([
{ boxShadow: 'inset 0 0 0 0 rgba(228,172,56,0)' },
{ boxShadow: 'inset 0 0 30px 4px rgba(228,172,56,0.4)' },
{ boxShadow: 'inset 0 0 0 0 rgba(228,172,56,0)' }
], { duration: 800, easing: 'ease-out' });
}
juice.hapticLight();
}
function animateDice(el, playerIdx) { function animateDice(el, playerIdx) {
return new Promise(resolve => { return new Promise(resolve => {
const mainDice = el.querySelector('#dice-box'); const mainDice = el.querySelector('#dice-box');
......
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