Commit a2876e3e authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: bigger stacked pawns + 15s turn timer with auto-play

- Increased stacked pawn scale (2 pawns: 0.82, 3+: 0.7) so they stay
  visible when sharing a cell
- Added 15-second turn timer bar above the dice area
- Timer counts down visually (green→yellow→red gradient)
- If player doesn't roll in time, auto-rolls for them
- If player doesn't pick a piece in time, bot logic picks the best move
- Timer works in both single-player and multiplayer modes
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 8c14559d
...@@ -30,6 +30,9 @@ let livePoller = null; ...@@ -30,6 +30,9 @@ let livePoller = null;
let myPlayerIndex = 0; let myPlayerIndex = 0;
let matchId = null; let matchId = null;
let isHost = false; let isHost = false;
let turnTimer = null;
let turnTimerStart = 0;
const TURN_TIMEOUT = 15000;
function renderPanel(p) { function renderPanel(p) {
return ` return `
...@@ -96,6 +99,7 @@ export function mountGame(el, params) { ...@@ -96,6 +99,7 @@ export function mountGame(el, params) {
${renderPanel(panels[0])} ${renderPanel(panels[0])}
${renderPanel(panels[3])} ${renderPanel(panels[3])}
</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;">
<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>
...@@ -243,6 +247,7 @@ function isMyTurn() { ...@@ -243,6 +247,7 @@ function isMyTurn() {
async function handleRoll(el) { async function handleRoll(el) {
if (diceAnimating || game.rolled || game.gameOver || !isMyTurn() || game.players[myPlayerIndex].finished) return; if (diceAnimating || game.rolled || game.gameOver || !isMyTurn() || game.players[myPlayerIndex].finished) return;
clearTurnTimer();
diceAnimating = true; diceAnimating = true;
updatePanels(el); updatePanels(el);
...@@ -273,6 +278,13 @@ async function handleRoll(el) { ...@@ -273,6 +278,13 @@ async function handleRoll(el) {
} else { } else {
highlightMovablePieces(validMoves); highlightMovablePieces(validMoves);
waitForPieceSelection(el, validMoves); waitForPieceSelection(el, validMoves);
startTurnTimer(el, () => {
// Auto-pick best move on timeout
if (selectionListener) { canvas.removeEventListener('click', selectionListener); selectionListener = null; }
highlightedPieces = [];
const best = rules.getBotMove(game, myPlayerIndex, dice);
if (best) doMove(el, best);
});
} }
} }
...@@ -920,7 +932,7 @@ function drawPieces(cs) { ...@@ -920,7 +932,7 @@ function drawPieces(cs) {
cellOccupants.forEach((occupants) => { cellOccupants.forEach((occupants) => {
const count = occupants.length; const count = occupants.length;
const offsets = STACK_OFFSETS[Math.min(count, 4) - 1]; const offsets = STACK_OFFSETS[Math.min(count, 4) - 1];
const scale = count === 1 ? 1 : count === 2 ? 0.72 : 0.6; const scale = count === 1 ? 1 : count === 2 ? 0.82 : 0.7;
occupants.forEach((occ, idx) => { occupants.forEach((occ, idx) => {
const { pIdx, pieceIdx, piece, pos } = occ; const { pIdx, pieceIdx, piece, pos } = occ;
...@@ -979,7 +991,6 @@ function updatePanels(el) { ...@@ -979,7 +991,6 @@ function updatePanels(el) {
const isNowActive = i === game.currentPlayer; const isNowActive = i === game.currentPlayer;
p.classList.toggle('active', isNowActive); p.classList.toggle('active', isNowActive);
if (game.players[i].finished) p.style.opacity = '0.5'; if (game.players[i].finished) p.style.opacity = '0.5';
// Hide dice when turn changes away from this player
if (wasActive && !isNowActive) { if (wasActive && !isNowActive) {
const md = el.querySelector(`#dice-${i}`); const md = el.querySelector(`#dice-${i}`);
if (md) { setTimeout(() => { md.className = 'pp-dice'; md.style.animation = ''; }, 1200); } if (md) { setTimeout(() => { md.className = 'pp-dice'; md.style.animation = ''; }, 1200); }
...@@ -989,6 +1000,7 @@ function updatePanels(el) { ...@@ -989,6 +1000,7 @@ function updatePanels(el) {
const diceArea = el.querySelector('#dice-area'); const diceArea = el.querySelector('#dice-area');
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 myTurn = isMyTurn() && !game.players[myPlayerIndex].finished && !game.gameOver; const myTurn = isMyTurn() && !game.players[myPlayerIndex].finished && !game.gameOver;
if (diceArea) { if (diceArea) {
diceArea.style.opacity = myTurn ? '1' : '0.3'; diceArea.style.opacity = myTurn ? '1' : '0.3';
...@@ -1000,7 +1012,38 @@ function updatePanels(el) { ...@@ -1000,7 +1012,38 @@ function updatePanels(el) {
btn.disabled = !canRoll; btn.disabled = !canRoll;
btn.style.opacity = canRoll ? '1' : '0.4'; btn.style.opacity = canRoll ? '1' : '0.4';
btn.style.visibility = myTurn ? 'visible' : 'hidden'; btn.style.visibility = myTurn ? 'visible' : 'hidden';
if (canRoll && !turnTimer) {
startTurnTimer(el, () => handleRoll(el));
}
} }
if (timerBar) timerBar.style.opacity = myTurn ? '1' : '0.3';
if (!myTurn) clearTurnTimer();
}
function startTurnTimer(el, onTimeout) {
clearTurnTimer();
turnTimerStart = performance.now();
const fill = el.querySelector('#turn-timer-fill');
if (fill) { fill.style.transition = 'none'; fill.style.transform = 'scaleX(1)'; }
const timerBar = el.querySelector('#turn-timer-bar');
if (timerBar) timerBar.style.opacity = '1';
function tick() {
const elapsed = performance.now() - turnTimerStart;
const pct = Math.max(0, 1 - elapsed / TURN_TIMEOUT);
if (fill) fill.style.transform = `scaleX(${pct})`;
if (pct <= 0) {
clearTurnTimer();
onTimeout();
return;
}
turnTimer = requestAnimationFrame(tick);
}
turnTimer = requestAnimationFrame(tick);
}
function clearTurnTimer() {
if (turnTimer) { cancelAnimationFrame(turnTimer); turnTimer = null; }
} }
function animateDice(el, playerIdx) { function animateDice(el, playerIdx) {
...@@ -1085,6 +1128,7 @@ function handleExit(el) { ...@@ -1085,6 +1128,7 @@ function handleExit(el) {
if (!confirm('هل تريد الخروج من المباراة؟')) return; if (!confirm('هل تريد الخروج من المباراة؟')) return;
game.gameOver = true; game.gameOver = true;
clearTurnTimer();
stopRenderLoop(); stopRenderLoop();
audio.play('click'); audio.play('click');
...@@ -1102,6 +1146,7 @@ function handleExit(el) { ...@@ -1102,6 +1146,7 @@ function handleExit(el) {
function endGame(el) { function endGame(el) {
game.gameOver = true; game.gameOver = true;
clearTurnTimer();
stopRenderLoop(); stopRenderLoop();
if (matchId) matchLive.session?.destroy?.(); if (matchId) matchLive.session?.destroy?.();
......
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