Commit 52037552 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: gameplay improvements — dynamic dice, turbo mode, endgame tension, game modes

Ludo:
- Replace pure-random dice with weighted system (drama when stuck, catch-up boost, domination penalty)
- Enhanced VFX: screen flash on captures, bigger confetti on wins, panel pulse on finish
- Turbo mode after 12 min: 60% faster bots, doubled animation speed, fewer dice frames

Domino:
- Fix tile proportions from 40x70 (1.75:1) to 36x72 (correct 2:1 ratio)
- Increase chain layout GAP from 3 to 6 for better readability
- Add target score selection (50/100/150) in bot mode room picker
- Endgame tension alerts when opponent has 1-2 tiles remaining
- Last-tile confetti burst + enhanced result screen with crown
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 98d9abef
...@@ -193,6 +193,14 @@ export function flash(color = '#fff', duration = 150) { ...@@ -193,6 +193,14 @@ export function flash(color = '#fff', duration = 150) {
.onfinish = () => el.remove(); .onfinish = () => el.remove();
} }
// ============ SCREEN FLASH (full-screen color overlay) ============
export function screenFlash(color = 'rgba(255,255,255,0.2)', duration = 400) {
const el = document.createElement('div');
el.style.cssText = `position:fixed;inset:0;background:${color};pointer-events:none;z-index:9998;`;
document.body.appendChild(el);
el.animate([{opacity:1},{opacity:0}], {duration}).onfinish = () => el.remove();
}
// ============ FLOATING / BREATHING ============ // ============ FLOATING / BREATHING ============
export function breathe(el, options = {}) { export function breathe(el, options = {}) {
if (!el) return; if (!el) return;
......
...@@ -119,7 +119,7 @@ export class DominoHand { ...@@ -119,7 +119,7 @@ export class DominoHand {
getStyle() { getStyle() {
return ` return `
.dh-tile { .dh-tile {
width:40px;height:70px; width:36px;height:72px;
background:#F5F0E8; background:#F5F0E8;
border:2px solid rgba(255,255,255,0.15); border:2px solid rgba(255,255,255,0.15);
border-radius:6px; border-radius:6px;
...@@ -155,7 +155,7 @@ export class DominoHand { ...@@ -155,7 +155,7 @@ export class DominoHand {
.dh-divider { width:80%;height:1px;background:#B8A88A;flex-shrink:0; } .dh-divider { width:80%;height:1px;background:#B8A88A;flex-shrink:0; }
.dh-top, .dh-bottom { .dh-top, .dh-bottom {
display:flex;flex-wrap:wrap; display:flex;flex-wrap:wrap;
width:22px;height:22px; width:24px;height:28px;
align-items:center;justify-content:center; align-items:center;justify-content:center;
gap:1px; gap:1px;
} }
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
export const TILE_W = 24; // narrow dimension export const TILE_W = 24; // narrow dimension
export const TILE_H = 48; // long dimension export const TILE_H = 48; // long dimension
export const GAP = 3; export const GAP = 6;
const MARGIN = 30; const MARGIN = 30;
......
...@@ -19,10 +19,10 @@ let botTimeout = null; ...@@ -19,10 +19,10 @@ let botTimeout = null;
let autoPassTimeout = null; let autoPassTimeout = null;
export function mountGame(el, params) { export function mountGame(el, params) {
const { mode = 'bot', matchId, playerIndex = 0, players, botLevel = 'intermediate' } = params; const { mode = 'bot', matchId, playerIndex = 0, players, botLevel = 'intermediate', targetScore } = params;
scene.enterGameMode(); scene.enterGameMode();
state = createInitialState(mode, matchId, playerIndex, botLevel, players); state = createInitialState(mode, matchId, playerIndex, botLevel, players, targetScore);
if (mode !== 'live') { if (mode !== 'live') {
dealNewRound(); dealNewRound();
...@@ -71,14 +71,14 @@ export function mountGame(el, params) { ...@@ -71,14 +71,14 @@ export function mountGame(el, params) {
bus.emit('game:started', { gameKey: 'domino', mode, matchId }); bus.emit('game:started', { gameKey: 'domino', mode, matchId });
} }
function createInitialState(mode, matchId, playerIndex, botLevel, players) { function createInitialState(mode, matchId, playerIndex, botLevel, players, targetScore) {
return { return {
mode, matchId, myPlayerIndex: playerIndex, mode, matchId, myPlayerIndex: playerIndex,
players: players || [], players: players || [],
hands: [[], []], boneyard: [], chain: [], hands: [[], []], boneyard: [], chain: [],
leftEnd: null, rightEnd: null, leftEnd: null, rightEnd: null,
currentPlayer: 0, numPlayers: 2, currentPlayer: 0, numPlayers: 2,
matchScores: [0, 0], roundNumber: 1, targetScore: 100, matchScores: [0, 0], roundNumber: 1, targetScore: targetScore || 100,
selectedTile: null, gameOver: false, matchOver: false, selectedTile: null, gameOver: false, matchOver: false,
moveCount: 0, lastSyncMoveCount: 0, moveCount: 0, lastSyncMoveCount: 0,
botPersonality: bot.getPersonality(botLevel), botPersonality: bot.getPersonality(botLevel),
...@@ -98,6 +98,7 @@ function dealNewRound() { ...@@ -98,6 +98,7 @@ function dealNewRound() {
state.rightEnd = null; state.rightEnd = null;
state.gameOver = false; state.gameOver = false;
state.selectedTile = null; state.selectedTile = null;
state.tensionReset = true;
} }
function buildLayout(mode) { function buildLayout(mode) {
...@@ -132,7 +133,7 @@ function buildLayout(mode) { ...@@ -132,7 +133,7 @@ function buildLayout(mode) {
<span style="font-size:11px;color:#334155;">|</span> <span style="font-size:11px;color:#334155;">|</span>
<span style="font-size:12px;color:#94a3b8;">خصم: <b id="opp-score">0</b></span> <span style="font-size:12px;color:#94a3b8;">خصم: <b id="opp-score">0</b></span>
<span style="font-size:11px;color:#334155;">|</span> <span style="font-size:11px;color:#334155;">|</span>
<span style="font-size:11px;color:#64748b;">${emoji('target', '🎯', 12)} 100</span> <span id="target-display" style="font-size:11px;color:#64748b;">${emoji('target', '🎯', 12)} ${state.targetScore}</span>
<span style="font-size:11px;color:#334155;">|</span> <span style="font-size:11px;color:#334155;">|</span>
<span style="font-size:11px;color:#64748b;">ج<span id="round-num">1</span></span> <span style="font-size:11px;color:#64748b;">ج<span id="round-num">1</span></span>
</div> </div>
...@@ -615,6 +616,12 @@ function executePlacement(el, tile, end) { ...@@ -615,6 +616,12 @@ function executePlacement(el, tile, end) {
audio.play('place', 'game'); audio.play('place', 'game');
juice.hapticMedium?.(); juice.hapticMedium?.();
// Last tile fanfare
if (state.hands[state.myPlayerIndex].length === 0) {
juice.confetti(window.innerWidth / 2, window.innerHeight / 2, 20);
juice.hapticSuccess?.();
}
syncMoveToServer(tile, actualEnd); syncMoveToServer(tile, actualEnd);
if (checkRoundEnd(el)) return; if (checkRoundEnd(el)) return;
...@@ -913,9 +920,33 @@ function updateUI(el) { ...@@ -913,9 +920,33 @@ function updateUI(el) {
} }
const oppCountEl = el.querySelector('#opp-count'); const oppCountEl = el.querySelector('#opp-count');
const oppIdx = 1 - state.myPlayerIndex;
const oppHandLen = state.hands[oppIdx]?.length || 0;
if (oppCountEl) { if (oppCountEl) {
const oppIdx = 1 - state.myPlayerIndex; oppCountEl.textContent = `${oppHandLen} قطع`;
oppCountEl.textContent = `${state.hands[oppIdx]?.length || 0} قطع`; }
// Reset tension on new round
if (state.tensionReset) {
const oppBar = el.querySelector('#domino-opp-bar');
if (oppBar) { delete oppBar.dataset.tensionShown; oppBar.style.borderBottom = ''; }
state.tensionReset = false;
}
// Endgame tension alert
if (oppHandLen > 0 && oppHandLen <= 2) {
const oppBar = el.querySelector('#domino-opp-bar');
if (oppBar && !oppBar.dataset.tensionShown) {
oppBar.dataset.tensionShown = '1';
oppBar.style.borderBottom = '2px solid #ef4444';
oppBar.animate([{background:'rgba(239,68,68,0.15)'},{background:'#0f0f1e'}], {duration:800});
juice.hapticLight();
const alert = document.createElement('div');
alert.style.cssText = 'position:absolute;top:56px;left:50%;transform:translateX(-50%);background:rgba(239,68,68,0.9);color:#fff;padding:6px 16px;border-radius:8px;font-size:12px;font-weight:700;z-index:50;';
alert.textContent = oppHandLen === 1 ? '⚠️ قطعة واحدة!' : '⚠️ قطعتين!';
el.querySelector('#domino-wrap')?.appendChild(alert);
alert.animate([{opacity:0,transform:'translateX(-50%) translateY(-10px)'},{opacity:1,transform:'translateX(-50%) translateY(0)'},{opacity:0,transform:'translateX(-50%) translateY(-10px)'}], {duration:2500}).onfinish = () => alert.remove();
}
} }
const boneEl = el.querySelector('#boneyard-count'); const boneEl = el.querySelector('#boneyard-count');
......
...@@ -11,7 +11,7 @@ export function mountResult(el, params) { ...@@ -11,7 +11,7 @@ export function mountResult(el, params) {
const isWin = result === 'win'; const isWin = result === 'win';
const isDraw = result === 'draw'; const isDraw = result === 'draw';
const icon = isWin ? emoji('trophy', '🏆', 56) : isDraw ? emoji('handshake', '🤝', 56) : emoji('skull', '💀', 56); const icon = isWin ? `${emoji('crown', '👑', 32)}<br>${emoji('trophy', '🏆', 56)}` : isDraw ? emoji('handshake', '🤝', 56) : emoji('skull', '💀', 56);
const title = resigned ? 'استسلمت' : reason === 'resign' ? 'الخصم استسلم!' : reason === 'abandon' ? 'الخصم انقطع' : isWin ? 'فوز!' : isDraw ? 'تعادل' : 'خسارة'; const title = resigned ? 'استسلمت' : reason === 'resign' ? 'الخصم استسلم!' : reason === 'abandon' ? 'الخصم انقطع' : isWin ? 'فوز!' : isDraw ? 'تعادل' : 'خسارة';
const titleColor = isWin ? '#4ade80' : isDraw ? '#fbbf24' : '#fca5a5'; const titleColor = isWin ? '#4ade80' : isDraw ? '#fbbf24' : '#fca5a5';
...@@ -70,9 +70,15 @@ export function mountResult(el, params) { ...@@ -70,9 +70,15 @@ export function mountResult(el, params) {
const iconEl = el.querySelector('#result-icon'); const iconEl = el.querySelector('#result-icon');
if (iconEl) { if (iconEl) {
const rect = iconEl.getBoundingClientRect(); const rect = iconEl.getBoundingClientRect();
juice.confetti?.(rect.left + rect.width / 2, rect.top + rect.height / 2, 30); juice.confetti?.(rect.left + rect.width / 2, rect.top + rect.height / 2, 50);
juice.starBurst?.(rect.left + rect.width / 2, rect.top - 20, 10);
} }
}, 400); }, 400);
// Gold glow on score
setTimeout(() => {
const scoreEl = el.querySelector('#score-me');
if (scoreEl) juice.pulseElement?.(scoreEl, '#fbbf24', 800);
}, 800);
} }
completeOnServer(el, matchId, result, mode); completeOnServer(el, matchId, result, mode);
......
...@@ -25,7 +25,7 @@ function renderMenu(el) { ...@@ -25,7 +25,7 @@ function renderMenu(el) {
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:20px;padding:24px;background:linear-gradient(180deg,#0a0a14 0%,#0f0f1e 100%);"> <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:20px;padding:24px;background:linear-gradient(180deg,#0a0a14 0%,#0f0f1e 100%);">
<div style="font-size:48px;">${emoji('domino_tile', '🁣', 48)}</div> <div style="font-size:48px;">${emoji('domino_tile', '🁣', 48)}</div>
<div style="font-size:22px;font-weight:800;color:#f8fafc;">دومينو</div> <div style="font-size:22px;font-weight:800;color:#f8fafc;">دومينو</div>
<div style="font-size:13px;color:#E4AC38;text-align:center;max-width:260px;">أول من يوصل 100 نقطة يفوز!</div> <div style="font-size:13px;color:#E4AC38;text-align:center;max-width:260px;">أول من يوصل الهدف يفوز!</div>
<div style="display:flex;flex-direction:column;gap:10px;width:100%;max-width:300px;margin-top:12px;"> <div style="display:flex;flex-direction:column;gap:10px;width:100%;max-width:300px;margin-top:12px;">
<button class="btn btn-primary" id="btn-bot" style="min-height:56px;border-radius:16px;font-size:16px;font-weight:700;background:linear-gradient(135deg,#E4AC38,#d4940a);display:flex;align-items:center;justify-content:center;gap:8px;"> <button class="btn btn-primary" id="btn-bot" style="min-height:56px;border-radius:16px;font-size:16px;font-weight:700;background:linear-gradient(135deg,#E4AC38,#d4940a);display:flex;align-items:center;justify-content:center;gap:8px;">
...@@ -99,8 +99,7 @@ function renderBotPicker(el) { ...@@ -99,8 +99,7 @@ function renderBotPicker(el) {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
audio.play('click'); audio.play('click');
const level = btn.dataset.level; const level = btn.dataset.level;
scene.enterGameMode(); renderTargetPicker(el, level);
scene.replace('domino-game', { mode: 'bot', botLevel: level });
}); });
}); });
...@@ -110,6 +109,53 @@ function renderBotPicker(el) { ...@@ -110,6 +109,53 @@ function renderBotPicker(el) {
}); });
} }
function renderTargetPicker(el, botLevel) {
const targets = [
{ value: 50, label: '50 نقطة', desc: 'مباراة سريعة', icon: '⚡', color: '#4ade80' },
{ value: 100, label: '100 نقطة', desc: 'كلاسيك', icon: '🎯', color: '#fbbf24' },
{ value: 150, label: '150 نقطة', desc: 'مباراة طويلة', icon: '🔥', color: '#f87171' }
];
el.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:20px;padding:24px;background:linear-gradient(180deg,#0a0a14 0%,#0f0f1e 100%);">
<div style="font-size:20px;font-weight:700;color:#f8fafc;">اختر الهدف</div>
<div style="font-size:13px;color:#94a3b8;">أول من يوصل النقاط يفوز</div>
<div style="display:flex;flex-direction:column;gap:10px;width:100%;max-width:300px;">
${targets.map(t => `
<button class="btn-target" data-target="${t.value}" style="
display:flex;align-items:center;gap:12px;padding:16px;
background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);
border-radius:14px;cursor:pointer;width:100%;text-align:right;
transition:transform 0.1s,border-color 0.2s;
">
<span style="font-size:28px;">${t.icon}</span>
<div style="flex:1;">
<div style="font-size:15px;font-weight:700;color:${t.color};">${t.label}</div>
<div style="font-size:12px;color:#94a3b8;margin-top:2px;">${t.desc}</div>
</div>
<div style="font-size:18px;color:#475569;">←</div>
</button>
`).join('')}
</div>
<button class="btn btn-secondary" id="btn-back-target" style="margin-top:8px;font-size:13px;color:#64748b;background:none;border:none;">رجوع</button>
</div>
`;
el.querySelectorAll('.btn-target').forEach(btn => {
btn.addEventListener('click', () => {
audio.play('click');
const targetScore = parseInt(btn.dataset.target);
scene.enterGameMode();
scene.replace('domino-game', { mode: 'bot', botLevel, targetScore });
});
});
el.querySelector('#btn-back-target').addEventListener('click', () => {
audio.play('click');
renderBotPicker(el);
});
}
function renderLobby(el, { challengeId, friendId, friendName }) { function renderLobby(el, { challengeId, friendId, friendName }) {
const isHost = !challengeId; const isHost = !challengeId;
el.innerHTML = ` el.innerHTML = `
......
...@@ -35,8 +35,76 @@ export function createGame(numPlayers = 4) { ...@@ -35,8 +35,76 @@ export function createGame(numPlayers = 4) {
}; };
} }
export function rollDice() { export function rollDice(game, playerIdx) {
return Math.floor(Math.random() * 6) + 1; if (!game || playerIdx == null) return Math.floor(Math.random() * 6) + 1;
const ctx = getDiceContext(game, playerIdx);
const weights = computeDiceWeights(ctx);
return weightedRoll(weights);
}
function getDiceContext(game, playerIdx) {
const player = game.players[playerIdx];
const piecesHome = player.pieces.filter(p => p.pos === -1 && !p.finished).length;
const progress = player.pieces.reduce((sum, p) => {
if (p.finished) return sum + 58;
if (p.pos === -1) return sum;
return sum + p.pos;
}, 0) / (58 * 4) * 100;
const othersProgress = game.players
.filter((_, i) => i !== playerIdx && !game.players[i].finished)
.map(op => op.pieces.reduce((s, p) => s + (p.finished ? 58 : p.pos === -1 ? 0 : p.pos), 0) / (58 * 4) * 100);
const avgOthersProgress = othersProgress.length ? othersProgress.reduce((a, b) => a + b, 0) / othersProgress.length : 0;
let killValues = 0;
for (let d = 1; d <= 6; d++) {
const moves = getValidMoves(game, playerIdx, d);
if (moves.some(m => m.type === 'capture')) killValues++;
}
const elapsed = game.startTime ? (Date.now() - game.startTime) / 60000 : 0;
return { piecesHome, progress, avgOthersProgress, killValues, elapsed };
}
function computeDiceWeights(ctx) {
const w = [1, 1, 1, 1, 1, 1];
if (ctx.piecesHome === 4) {
w[5] += 1.5;
} else if (ctx.piecesHome >= 3) {
w[5] += 0.6;
}
const deficit = ctx.avgOthersProgress - ctx.progress;
if (deficit > 15) {
w[5] += Math.min(0.8, deficit / 40);
}
if (ctx.killValues >= 2 && ctx.progress < ctx.avgOthersProgress) {
w[1] += 0.2; w[2] += 0.2; w[3] += 0.2; w[4] += 0.2;
}
if (ctx.progress - ctx.avgOthersProgress > 20 && ctx.piecesHome === 0) {
w[5] = Math.max(0.5, w[5] - 0.5);
}
if (ctx.elapsed > 12) {
w[5] += 0.3;
}
return w;
}
function weightedRoll(weights) {
const total = weights.reduce((a, b) => a + b, 0);
let r = Math.random() * total;
for (let i = 0; i < 6; i++) {
r -= weights[i];
if (r <= 0) return i + 1;
}
return 6;
} }
export function getValidMoves(game, playerIdx, dice) { export function getValidMoves(game, playerIdx, dice) {
......
...@@ -59,6 +59,8 @@ export function mountGame(el, params) { ...@@ -59,6 +59,8 @@ export function mountGame(el, params) {
game = rules.createGame(numPlayers); game = rules.createGame(numPlayers);
game.mode = mode; game.mode = mode;
game.startTime = Date.now();
game.turboMode = false;
game.turnCount = 0; game.turnCount = 0;
validMoves = []; validMoves = [];
diceAnimating = false; diceAnimating = false;
...@@ -277,8 +279,8 @@ async function botLoop(el) { ...@@ -277,8 +279,8 @@ async function botLoop(el) {
const personality = personalities[game.currentPlayer] || personalities[1]; const personality = personalities[game.currentPlayer] || personalities[1];
// === BOT HUMANIZATION: delays that mimic real player === // === BOT HUMANIZATION: delays that mimic real player ===
// 1. "Thinking" before rolling — based on personality const turboMul = game.turboMode ? 0.4 : 1;
const thinkDelay = personality.thinkMin + Math.random() * (personality.thinkMax - personality.thinkMin); const thinkDelay = (personality.thinkMin + Math.random() * (personality.thinkMax - personality.thinkMin)) * turboMul;
// Show "thinking" indicator on the bot's player panel // Show "thinking" indicator on the bot's player panel
const botPanel = el.querySelector(`#pp-${game.currentPlayer}`); const botPanel = el.querySelector(`#pp-${game.currentPlayer}`);
...@@ -294,7 +296,7 @@ async function botLoop(el) { ...@@ -294,7 +296,7 @@ async function botLoop(el) {
const dice = await animateDice(el, game.currentPlayer); const dice = await animateDice(el, game.currentPlayer);
// 3. "Deciding" which piece to move — personality-driven delay // 3. "Deciding" which piece to move — personality-driven delay
const decideDelay = personality.thinkMin * 0.5 + Math.random() * (personality.thinkMax - personality.thinkMin) * 0.5; const decideDelay = (personality.thinkMin * 0.5 + Math.random() * (personality.thinkMax - personality.thinkMin) * 0.5) * turboMul;
// Show thinking indicator again while deciding // Show thinking indicator again while deciding
if (botPanelSpan) botPanelSpan.textContent = 'يفكر...'; if (botPanelSpan) botPanelSpan.textContent = 'يفكر...';
...@@ -328,11 +330,12 @@ async function botLoop(el) { ...@@ -328,11 +330,12 @@ async function botLoop(el) {
const toPos = move.to; const toPos = move.to;
if (move.type !== 'enter' && fromPos >= 0 && toPos > fromPos && toPos - fromPos <= 6) { if (move.type !== 'enter' && fromPos >= 0 && toPos > fromPos && toPos - fromPos <= 6) {
const stepDelay = game.turboMode ? 50 : 100;
for (let i = 1; i <= toPos - fromPos; i++) { for (let i = 1; i <= toPos - fromPos; i++) {
piece.pos = fromPos + i; piece.pos = fromPos + i;
drawBoard(); drawBoard();
audio.play('move', 'game'); audio.play('move', 'game');
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, stepDelay));
} }
piece.pos = fromPos; piece.pos = fromPos;
} }
...@@ -572,17 +575,16 @@ async function animateMove(el, move) { ...@@ -572,17 +575,16 @@ async function animateMove(el, move) {
} }
// Hop one square at a time with vertical bounce // Hop one square at a time with vertical bounce
const hopDelay = game.turboMode ? 30 : 60;
for (let i = 1; i <= steps; i++) { for (let i = 1; i <= steps; i++) {
piece.pos = fromPos + i; piece.pos = fromPos + i;
// Bounce up: draw piece 2px higher
piece._bounceOffset = -2; piece._bounceOffset = -2;
drawBoard(); drawBoard();
audio.play('move', 'game'); audio.play('move', 'game');
await new Promise(r => setTimeout(r, 60)); await new Promise(r => setTimeout(r, hopDelay));
// Bounce down: return to normal
piece._bounceOffset = 0; piece._bounceOffset = 0;
drawBoard(); drawBoard();
await new Promise(r => setTimeout(r, 60)); await new Promise(r => setTimeout(r, hopDelay));
} }
// Apply capture/finish effects after animation // Apply capture/finish effects after animation
...@@ -595,12 +597,20 @@ async function animateMove(el, move) { ...@@ -595,12 +597,20 @@ async function animateMove(el, move) {
function afterMove(el, move) { function afterMove(el, move) {
game.rolled = false; game.rolled = false;
if (move.type === 'capture') { audio.play('capture','game'); juice.shake(el,4,200); juice.hapticHeavy(); juice.confetti(window.innerWidth/2, window.innerHeight/2, 15); } if (move.type === 'capture') {
else if (move.type === 'finish') { audio.play('capture','game');
audio.play('win','reward'); juice.hapticSuccess(); juice.starBurst(window.innerWidth/2, window.innerHeight/2, 8); juice.shake(el, 6, 300);
// Extra particle burst at center of the board juice.hapticHeavy();
juice.confetti(window.innerWidth/2, window.innerHeight/2, 25);
juice.screenFlash('rgba(229,57,53,0.15)', 400);
} else if (move.type === 'finish') {
audio.play('win','reward');
juice.hapticSuccess();
juice.starBurst(window.innerWidth/2, window.innerHeight/2, 12);
const boardRect = canvas.getBoundingClientRect(); const boardRect = canvas.getBoundingClientRect();
juice.starBurst(boardSize/2 + boardRect.left, boardSize/2 + boardRect.top, 10); juice.starBurst(boardSize/2 + boardRect.left, boardSize/2 + boardRect.top, 10);
const panel = el.querySelector(`#pp-${game.currentPlayer}`);
if (panel) panel.animate([{background:'rgba(76,175,80,0.3)'},{background:'transparent'}], {duration:600});
} }
if (game.gameOver) { if (game.gameOver) {
...@@ -758,7 +768,22 @@ function drawBoard() { ...@@ -758,7 +768,22 @@ function drawBoard() {
}); });
} }
function checkTurboMode(el) {
if (game.turboMode) return;
const elapsed = (Date.now() - game.startTime) / 60000;
if (elapsed >= 12) {
game.turboMode = true;
const banner = document.createElement('div');
banner.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(228,172,56,0.95);color:#000;padding:10px 24px;border-radius:12px;font-size:14px;font-weight:700;z-index:100;';
banner.textContent = '⚡ وضع السرعة!';
el.querySelector('#ludo-wrap')?.appendChild(banner);
banner.animate([{opacity:0,transform:'translate(-50%,-50%) scale(0.5)'},{opacity:1,transform:'translate(-50%,-50%) scale(1)'},{opacity:0,transform:'translate(-50%,-50%) translateY(-20px)'}], {duration:2000}).onfinish = () => banner.remove();
juice.hapticHeavy();
}
}
function updatePanels(el) { function updatePanels(el) {
checkTurboMode(el);
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const p = el.querySelector(`#pp-${i}`); const p = el.querySelector(`#pp-${i}`);
if (p) p.classList.toggle('active', i === game.currentPlayer); if (p) p.classList.toggle('active', i === game.currentPlayer);
...@@ -786,9 +811,9 @@ function animateDice(el, playerIdx) { ...@@ -786,9 +811,9 @@ function animateDice(el, playerIdx) {
mainDice.style.transform = `translate(${rx}px,${ry}px) rotate(${rot}deg) scale(0.9)`; mainDice.style.transform = `translate(${rx}px,${ry}px) rotate(${rot}deg) scale(0.9)`;
if (miniDice) miniDice.style.transform = `rotate(${rot * 0.5}deg) scale(0.85)`; if (miniDice) miniDice.style.transform = `rotate(${rot * 0.5}deg) scale(0.85)`;
count++; count++;
if (count > 14) { if (count > (game.turboMode ? 8 : 14)) {
clearInterval(shakeAnim); clearInterval(shakeAnim);
const dice = rules.rollDice(); const dice = rules.rollDice(game, game.currentPlayer);
game.diceValue = dice; game.diceValue = dice;
renderDiceFace(mainDice, dice); renderDiceFace(mainDice, dice);
if (miniDice) renderMiniDice(miniDice, dice); if (miniDice) renderMiniDice(miniDice, dice);
...@@ -843,7 +868,15 @@ function endGame(el) { ...@@ -843,7 +868,15 @@ function endGame(el) {
game.gameOver = true; game.gameOver = true;
if (matchId) matchLive.session?.destroy?.(); if (matchId) matchLive.session?.destroy?.();
const result = game.winners[0] === myPlayerIndex ? 'win' : 'loss'; const result = game.winners[0] === myPlayerIndex ? 'win' : 'loss';
if (result === 'win') { juice.confetti(window.innerWidth/2, window.innerHeight/3, 40); juice.hapticSuccess(); audio.play('win','reward'); } if (result === 'win') {
else { audio.play('lose','game'); juice.hapticError(); } juice.confetti(window.innerWidth/2, window.innerHeight/3, 50);
juice.starBurst(window.innerWidth/2, window.innerHeight/3, 15);
juice.hapticSuccess();
audio.play('win','reward');
juice.screenFlash('rgba(76,175,80,0.12)', 600);
} else {
audio.play('lose','game');
juice.hapticError();
}
setTimeout(() => { scene.exitGameMode(); scene.replace('ludo-result', { result, winners: game.winners }); bus.emit('game:ended', { gameKey: 'ludo', result }); }, 1500); setTimeout(() => { scene.exitGameMode(); scene.replace('ludo-result', { result, winners: game.winners }); bus.emit('game:ended', { gameKey: 'ludo', result }); }, 1500);
} }
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