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) {
</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">
<button class="bgr-btn bgr-btn-primary" id="btn-rematch">
${emoji('fire', '🔥', 18)} العب مرة ثانية
......@@ -65,6 +76,9 @@ export function mountResult(el, params) {
};
if (didWin) startConfetti(el.querySelector('#confetti-canvas'));
bus.emit('coins:earned', { amount: didWin ? 50 : 10 });
bus.emit('xp:earned', { amount: 15 });
}
function startConfetti(canvas) {
......
......@@ -186,8 +186,8 @@ function renderReview(el, results, moves, playerColor) {
</table>
</div>
<!-- Back to analysis button -->
<button class="btn btn-secondary w-full" id="btn-full-analysis" style="font-size:13px;margin-bottom:12px;">${emoji('chart', '📊', 13)} التحليل التفصيلي</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>
`;
// Render eval graph
......@@ -195,7 +195,7 @@ function renderReview(el, results, moves, playerColor) {
el.querySelector('#btn-full-analysis')?.addEventListener('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) {
}
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 });
}
......@@ -1426,7 +1432,13 @@ function endGame(el) {
setTimeout(() => {
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 });
}, 1500);
}
......
......@@ -5,61 +5,111 @@ import * as juice from '../../../core/juice.js';
import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
const PLACE_EMOJI = ['🥇', '🥈', '🥉'];
const PLACE_LABEL = ['المركز الأول', 'المركز الثاني', 'المركز الثالث'];
const PLACE_COLOR = ['#FFD700', '#C0C0C0', '#CD7F32'];
const PLACE_EMOJI = ['🥇', '🥈', '🥉', '4️⃣'];
const PLACE_LABEL = ['الأول', 'الثاني', 'الثالث', 'الرابع'];
const PLACE_COLOR = ['#FFD700', '#C0C0C0', '#CD7F32', '#64748b'];
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';
let content;
if (resigned) {
content = `
<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>
`;
} else if (isWin && place >= 1 && place <= 3) {
const idx = place - 1;
content = `
<div style="font-size:72px;animation:float 2s ease-in-out infinite;">${emoji('medal_' + place, PLACE_EMOJI[idx], 72)}</div>
<div style="font-size:28px;font-weight:800;color:${PLACE_COLOR[idx]};">${PLACE_LABEL[idx]}</div>
<div style="font-size:16px;color:#94a3b8;margin-top:4px;">${place === 1 ? 'مبروك! أنت البطل' : 'أحسنت!'}</div>
`;
} else {
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>
<div style="font-size:16px;color:#94a3b8;margin-top:4px;">حظ أوفر المرة القادمة!</div>
`;
const coins = rewards?.coins_earned ?? (isWin ? 50 : place === 2 ? 25 : place === 3 ? 15 : 10);
const xp = rewards?.xp_earned ?? (isWin ? 30 : 15);
const ratingChange = rewards?.rating_change ?? (isWin ? 12 : place === 2 ? 3 : place === 3 ? -3 : -8);
// Build leaderboard: winners array has player indices in finish order
const leaderboard = [];
if (!resigned && winners.length > 0) {
for (let rank = 0; rank < winners.length; rank++) {
const pIdx = winners[rank];
leaderboard.push({
rank: rank + 1,
name: playerNames[pIdx] || `لاعب ${pIdx + 1}`,
color: playerColors[pIdx] || '#94a3b8',
isMe: pIdx === 0,
});
}
// Add any unfinished players (not in winners)
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 coins = rewards?.coins_earned ?? (isWin ? 50 : 10);
const xp = rewards?.xp_earned ?? 15;
const ratingChange = rewards?.rating_change ?? 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 heroTitle = resigned ? 'انسحبت من المباراة'
: place === 1 ? 'مبروك! أنت البطل'
: place === 2 ? 'المركز الثاني — أحسنت!'
: place === 3 ? 'المركز الثالث'
: 'المركز الرابع';
const heroColor = resigned ? '#EF4444'
: place === 1 ? '#FFD700'
: place === 2 ? '#C0C0C0'
: place === 3 ? '#CD7F32'
: '#EF4444';
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);">
${content}
<div class="lr-result-wrap">
<!-- 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 -->
<div style="display:flex;gap:12px;margin-top:8px;">
<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;">+${coins}</span>
<div class="lr-rewards">
<div class="lr-reward-pill lr-reward-coins">
<span>${emoji('coin', '🪙', 16)}</span>
<span>+${coins}</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;">+${xp} XP</span>
<div class="lr-reward-pill lr-reward-xp">
<span>${emoji('star', '⭐', 16)}</span>
<span>+${xp} XP</span>
</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>
${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-secondary" id="btn-back">${t('game.back')}</button>
</div>
</div>
${getResultStyles()}
`;
if (isWin) {
......@@ -73,7 +123,7 @@ export function mountResult(el, params) {
el.querySelector('#btn-again').addEventListener('click', () => {
audio.play('click');
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 {
scene.replace('ludo-game', {
mode: params.mode || 'bot',
......@@ -90,3 +140,52 @@ export function mountResult(el, params) {
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