Commit ac959931 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: overhaul game result screens for each game's specific needs

Chess:
- "التحليل التفصيلي" button in review now navigates to chess-analysis
  (interactive move-by-move board with eval bar) instead of scene.pop()

Ludo:
- Complete leaderboard showing all players ranked 1st-4th
- Each player shown with their name, color dot, and medal/rank
- Current player highlighted with gold border
- Proper rewards display (coins + XP + rating change)
- Pass playerNames and playerColors from game to result scene

Backgammon:
- Add rewards display (coins + XP) to result screen
- Emit coins:earned and xp:earned events for HUD sync
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent e8941162
...@@ -39,6 +39,17 @@ export function mountResult(el, params) { ...@@ -39,6 +39,17 @@ export function mountResult(el, params) {
</div> </div>
</div> </div>
<div class="bgr-rewards" style="display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap;justify-content:center;">
<div style="display:flex;align-items:center;gap:4px;padding:8px 14px;background:rgba(251,191,36,0.1);border:1px solid rgba(251,191,36,0.2);border-radius:10px;">
<span style="font-size:16px;">${emoji('coin', '🪙', 16)}</span>
<span style="font-size:14px;font-weight:700;color:#fbbf24;">+${didWin ? 50 : 10}</span>
</div>
<div style="display:flex;align-items:center;gap:4px;padding:8px 14px;background:rgba(139,92,246,0.1);border:1px solid rgba(139,92,246,0.2);border-radius:10px;">
<span style="font-size:16px;">${emoji('star', '⭐', 16)}</span>
<span style="font-size:14px;font-weight:700;color:#a78bfa;">+15 XP</span>
</div>
</div>
<div class="bgr-buttons"> <div class="bgr-buttons">
<button class="bgr-btn bgr-btn-primary" id="btn-rematch"> <button class="bgr-btn bgr-btn-primary" id="btn-rematch">
${emoji('fire', '🔥', 18)} العب مرة ثانية ${emoji('fire', '🔥', 18)} العب مرة ثانية
...@@ -65,6 +76,9 @@ export function mountResult(el, params) { ...@@ -65,6 +76,9 @@ export function mountResult(el, params) {
}; };
if (didWin) startConfetti(el.querySelector('#confetti-canvas')); if (didWin) startConfetti(el.querySelector('#confetti-canvas'));
bus.emit('coins:earned', { amount: didWin ? 50 : 10 });
bus.emit('xp:earned', { amount: 15 });
} }
function startConfetti(canvas) { function startConfetti(canvas) {
......
...@@ -186,8 +186,8 @@ function renderReview(el, results, moves, playerColor) { ...@@ -186,8 +186,8 @@ function renderReview(el, results, moves, playerColor) {
</table> </table>
</div> </div>
<!-- Back to analysis button --> <!-- Deep analysis button -->
<button class="btn btn-secondary w-full" id="btn-full-analysis" style="font-size:13px;margin-bottom:12px;">${emoji('chart', '📊', 13)} التحليل التفصيلي</button> <button class="btn btn-secondary w-full" id="btn-full-analysis" style="font-size:13px;margin-bottom:12px;">${emoji('chart', '📊', 13)} التحليل التفصيلي (نقلة بنقلة)</button>
`; `;
// Render eval graph // Render eval graph
...@@ -195,7 +195,7 @@ function renderReview(el, results, moves, playerColor) { ...@@ -195,7 +195,7 @@ function renderReview(el, results, moves, playerColor) {
el.querySelector('#btn-full-analysis')?.addEventListener('click', () => { el.querySelector('#btn-full-analysis')?.addEventListener('click', () => {
audio.play('click'); audio.play('click');
scene.pop(); scene.push('chess-analysis', { moveHistory: moves, playerColor, pgn: moves.map(m => m.san).join(' ') });
}); });
} }
......
...@@ -1372,7 +1372,13 @@ async function handleExit(el) { ...@@ -1372,7 +1372,13 @@ async function handleExit(el) {
} }
scene.exitGameMode(); scene.exitGameMode();
scene.replace('ludo-result', { result: 'loss', resigned: true, mode: game.mode, numPlayers: game.numPlayers, seats: game.activeSeats, humanCount: game.humanPlayers?.length, difficulty: game.difficulty }); scene.replace('ludo-result', {
result: 'loss', resigned: true, mode: game.mode,
numPlayers: game.numPlayers, seats: game.activeSeats,
humanCount: game.humanPlayers?.length, difficulty: game.difficulty,
playerNames: [...PLAYER_NAMES],
playerColors: game.activeSeats.map(s => COLORS[s]),
});
bus.emit('game:ended', { gameKey: 'ludo', result: 'loss', resigned: true }); bus.emit('game:ended', { gameKey: 'ludo', result: 'loss', resigned: true });
} }
...@@ -1426,7 +1432,13 @@ function endGame(el) { ...@@ -1426,7 +1432,13 @@ function endGame(el) {
setTimeout(() => { setTimeout(() => {
scene.exitGameMode(); scene.exitGameMode();
scene.replace('ludo-result', { result, place, winners: game.winners, mode: game.mode, numPlayers: game.numPlayers, seats: game.activeSeats, humanCount: game.humanPlayers?.length, difficulty: game.difficulty }); scene.replace('ludo-result', {
result, place, winners: game.winners, mode: game.mode,
numPlayers: game.numPlayers, seats: game.activeSeats,
humanCount: game.humanPlayers?.length, difficulty: game.difficulty,
playerNames: [...PLAYER_NAMES],
playerColors: game.activeSeats.map(s => COLORS[s]),
});
bus.emit('game:ended', { gameKey: 'ludo', result, place }); bus.emit('game:ended', { gameKey: 'ludo', result, place });
}, 1500); }, 1500);
} }
......
...@@ -5,61 +5,111 @@ import * as juice from '../../../core/juice.js'; ...@@ -5,61 +5,111 @@ import * as juice from '../../../core/juice.js';
import { t } from '../../../core/i18n.js'; import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js'; import { emoji } from '../../../core/theme.js';
const PLACE_EMOJI = ['🥇', '🥈', '🥉']; const PLACE_EMOJI = ['🥇', '🥈', '🥉', '4️⃣'];
const PLACE_LABEL = ['المركز الأول', 'المركز الثاني', 'المركز الثالث']; const PLACE_LABEL = ['الأول', 'الثاني', 'الثالث', 'الرابع'];
const PLACE_COLOR = ['#FFD700', '#C0C0C0', '#CD7F32']; const PLACE_COLOR = ['#FFD700', '#C0C0C0', '#CD7F32', '#64748b'];
export function mountResult(el, params) { export function mountResult(el, params) {
const { result, place, resigned, rewards } = params; const { result, place, resigned, rewards, winners = [], playerNames = [], playerColors = [] } = params;
const numPlayers = params.numPlayers || params.playerCount || 4;
const isWin = result === 'win'; const isWin = result === 'win';
let content; const coins = rewards?.coins_earned ?? (isWin ? 50 : place === 2 ? 25 : place === 3 ? 15 : 10);
if (resigned) { const xp = rewards?.xp_earned ?? (isWin ? 30 : 15);
content = ` const ratingChange = rewards?.rating_change ?? (isWin ? 12 : place === 2 ? 3 : place === 3 ? -3 : -8);
<div style="font-size:64px;animation:float 2s ease-in-out infinite;">${emoji('skull', '💀', 64)}</div>
<div style="font-size:24px;font-weight:800;color:var(--loss);">انسحبت من المباراة</div> // Build leaderboard: winners array has player indices in finish order
`; const leaderboard = [];
} else if (isWin && place >= 1 && place <= 3) { if (!resigned && winners.length > 0) {
const idx = place - 1; for (let rank = 0; rank < winners.length; rank++) {
content = ` const pIdx = winners[rank];
<div style="font-size:72px;animation:float 2s ease-in-out infinite;">${emoji('medal_' + place, PLACE_EMOJI[idx], 72)}</div> leaderboard.push({
<div style="font-size:28px;font-weight:800;color:${PLACE_COLOR[idx]};">${PLACE_LABEL[idx]}</div> rank: rank + 1,
<div style="font-size:16px;color:#94a3b8;margin-top:4px;">${place === 1 ? 'مبروك! أنت البطل' : 'أحسنت!'}</div> name: playerNames[pIdx] || `لاعب ${pIdx + 1}`,
`; color: playerColors[pIdx] || '#94a3b8',
} else { isMe: pIdx === 0,
content = ` });
<div style="font-size:64px;animation:float 2s ease-in-out infinite;">${emoji('skull', '💀', 64)}</div> }
<div style="font-size:28px;font-weight:800;color:var(--loss);">المركز الرابع</div> // Add any unfinished players (not in winners)
<div style="font-size:16px;color:#94a3b8;margin-top:4px;">حظ أوفر المرة القادمة!</div> for (let i = 0; i < numPlayers; i++) {
`; if (!winners.includes(i)) {
leaderboard.push({
rank: leaderboard.length + 1,
name: playerNames[i] || `لاعب ${i + 1}`,
color: playerColors[i] || '#94a3b8',
isMe: i === 0,
});
}
} }
}
const heroIcon = resigned
? emoji('skull', '💀', 64)
: place === 1 ? emoji('trophy', '🏆', 64)
: place === 2 ? emoji('medal_2', '🥈', 64)
: place === 3 ? emoji('medal_3', '🥉', 64)
: emoji('skull', '💀', 64);
const coins = rewards?.coins_earned ?? (isWin ? 50 : 10); const heroTitle = resigned ? 'انسحبت من المباراة'
const xp = rewards?.xp_earned ?? 15; : place === 1 ? 'مبروك! أنت البطل'
const ratingChange = rewards?.rating_change ?? 0; : place === 2 ? 'المركز الثاني — أحسنت!'
: place === 3 ? 'المركز الثالث'
: 'المركز الرابع';
const heroColor = resigned ? '#EF4444'
: place === 1 ? '#FFD700'
: place === 2 ? '#C0C0C0'
: place === 3 ? '#CD7F32'
: '#EF4444';
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:var(--s-6);padding:var(--s-6);"> <div class="lr-result-wrap">
${content} <!-- Hero -->
<div class="lr-hero-section">
<div class="lr-hero-icon">${heroIcon}</div>
<div class="lr-hero-title" style="color:${heroColor};">${heroTitle}</div>
</div>
<!-- Leaderboard -->
${leaderboard.length > 0 ? `
<div class="lr-leaderboard">
<div class="lr-lb-title">ترتيب اللاعبين</div>
${leaderboard.map(p => `
<div class="lr-lb-row ${p.isMe ? 'lr-lb-me' : ''}" style="--pc:${p.color};">
<div class="lr-lb-rank" style="color:${PLACE_COLOR[p.rank - 1] || '#64748b'};">
${p.rank <= 3 ? PLACE_EMOJI[p.rank - 1] : p.rank}
</div>
<div class="lr-lb-color" style="background:${p.color};"></div>
<div class="lr-lb-name">${p.name}${p.isMe ? ' (أنت)' : ''}</div>
<div class="lr-lb-place">${PLACE_LABEL[p.rank - 1] || ''}</div>
</div>
`).join('')}
</div>` : ''}
<!-- Rewards --> <!-- Rewards -->
<div style="display:flex;gap:12px;margin-top:8px;"> <div class="lr-rewards">
<div style="display:flex;align-items:center;gap:4px;padding:8px 14px;background:rgba(251,191,36,0.1);border:1px solid rgba(251,191,36,0.2);border-radius:10px;"> <div class="lr-reward-pill lr-reward-coins">
<span style="font-size:16px;">${emoji('coin', '🪙', 16)}</span> <span>${emoji('coin', '🪙', 16)}</span>
<span style="font-size:14px;font-weight:700;color:#fbbf24;">+${coins}</span> <span>+${coins}</span>
</div> </div>
<div style="display:flex;align-items:center;gap:4px;padding:8px 14px;background:rgba(139,92,246,0.1);border:1px solid rgba(139,92,246,0.2);border-radius:10px;"> <div class="lr-reward-pill lr-reward-xp">
<span style="font-size:16px;">${emoji('star', '⭐', 16)}</span> <span>${emoji('star', '⭐', 16)}</span>
<span style="font-size:14px;font-weight:700;color:#a78bfa;">+${xp} XP</span> <span>+${xp} XP</span>
</div> </div>
${ratingChange ? `
<div class="lr-reward-pill" style="color:${ratingChange > 0 ? '#4ade80' : '#fca5a5'};border-color:${ratingChange > 0 ? 'rgba(74,222,128,0.2)' : 'rgba(252,165,165,0.2)'};">
<span>📈</span>
<span>${ratingChange > 0 ? '+' : ''}${ratingChange}</span>
</div>` : ''}
</div> </div>
${ratingChange ? `<div style="font-size:14px;font-weight:600;color:${ratingChange > 0 ? '#4ade80' : '#fca5a5'};">التصنيف: ${ratingChange > 0 ? '+' : ''}${ratingChange}</div>` : ''}
<div style="display:flex;gap:var(--s-3);margin-top:var(--s-6);"> <!-- Actions -->
<div class="lr-actions">
<button class="btn btn-primary" id="btn-again">${t('game.rematch')}</button> <button class="btn btn-primary" id="btn-again">${t('game.rematch')}</button>
<button class="btn btn-secondary" id="btn-back">${t('game.back')}</button> <button class="btn btn-secondary" id="btn-back">${t('game.back')}</button>
</div> </div>
</div> </div>
${getResultStyles()}
`; `;
if (isWin) { if (isWin) {
...@@ -73,7 +123,7 @@ export function mountResult(el, params) { ...@@ -73,7 +123,7 @@ export function mountResult(el, params) {
el.querySelector('#btn-again').addEventListener('click', () => { el.querySelector('#btn-again').addEventListener('click', () => {
audio.play('click'); audio.play('click');
if (params.mode === 'live') { if (params.mode === 'live') {
scene.replace('play-queue', { game: 'ludo', mode: 'human', playerCount: params.numPlayers || params.playerCount || 4, seats: params.seats }); scene.replace('play-queue', { game: 'ludo', mode: 'human', playerCount: numPlayers, seats: params.seats });
} else { } else {
scene.replace('ludo-game', { scene.replace('ludo-game', {
mode: params.mode || 'bot', mode: params.mode || 'bot',
...@@ -90,3 +140,52 @@ export function mountResult(el, params) { ...@@ -90,3 +140,52 @@ export function mountResult(el, params) {
bus.emit('navigate', { world: 'play', scene: 'play-table' }); bus.emit('navigate', { world: 'play', scene: 'play-table' });
}); });
} }
function getResultStyles() {
return `<style>
.lr-result-wrap {
display:flex;flex-direction:column;align-items:center;justify-content:center;
height:100%;gap:20px;padding:24px;
background:linear-gradient(180deg,#070d14 0%,#0a1420 40%,#0d1a2a 100%);
}
.lr-hero-section { text-align:center; }
.lr-hero-icon { font-size:64px;animation:lrFloat 2.5s ease-in-out infinite; }
.lr-hero-title { font-size:22px;font-weight:800;margin-top:8px; }
@keyframes lrFloat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} }
.lr-leaderboard {
width:100%;max-width:320px;
background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);
border-radius:16px;padding:14px;
}
.lr-lb-title {
font-size:12px;font-weight:700;color:#64748b;text-align:center;
margin-bottom:10px;text-transform:uppercase;letter-spacing:0.05em;
}
.lr-lb-row {
display:flex;align-items:center;gap:10px;
padding:10px 12px;border-radius:10px;
transition:background 0.2s;
}
.lr-lb-me {
background:rgba(228,172,56,0.08);border:1px solid rgba(228,172,56,0.2);
}
.lr-lb-rank { font-size:18px;min-width:28px;text-align:center; }
.lr-lb-color { width:12px;height:12px;border-radius:50%;flex-shrink:0; }
.lr-lb-name { flex:1;font-size:14px;font-weight:600;color:#f8fafc; }
.lr-lb-place { font-size:11px;color:#64748b;font-weight:500; }
.lr-rewards { display:flex;gap:10px;flex-wrap:wrap;justify-content:center; }
.lr-reward-pill {
display:flex;align-items:center;gap:4px;
padding:8px 14px;border-radius:10px;
font-size:14px;font-weight:700;
border:1px solid transparent;
}
.lr-reward-coins { color:#fbbf24;background:rgba(251,191,36,0.1);border-color:rgba(251,191,36,0.2); }
.lr-reward-xp { color:#a78bfa;background:rgba(139,92,246,0.1);border-color:rgba(139,92,246,0.2); }
.lr-actions { display:flex;gap:10px;width:100%;max-width:300px; }
.lr-actions .btn { flex:1;min-height:48px;border-radius:14px;font-size:15px;font-weight:700; }
</style>`;
}
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