/* ============================================ LEADERBOARD & PLAYER STATISTICS ============================================ Le CLASSEMENT est désormais partagé EN LIGNE : tous les joueurs enregistrent leurs scores via l'API PHP (api/scores.php) et voient le même top 10 par mode de jeu. Les STATISTIQUES PERSONNELLES (« Vos statistiques ») restent stockées localement dans le navigateur du joueur, car le serveur ne conserve que le top 10 de chaque mode (et non l'historique complet de chacun). ============================================ */ /** * Manager du classement */ class LeaderboardManager { // Point d'entrée de l'API (relatif à memory.html) — ASP.NET + SQL Server static API_URL = 'api/scores.ashx'; // Historique local des parties du joueur (pour ses stats perso) static KEY_HISTORY = 'nextgn_player_history'; static MAX_HISTORY = 500; // Cache mémoire du dernier classement récupéré (par difficulté) static _cache = {}; /** * Enregistrer une partie : * - envoie le score au classement en ligne * - conserve la partie dans l'historique local (stats perso) * @returns {Promise} résultat de l'API ({ ok, isTop, rank } ou { ok:false }) */ static async addScore(gameData) { const record = { playerName: getPlayerName(), userId: getUserId(), score: gameData.score, moves: gameData.moves, timeSeconds: gameData.duration, difficulty: gameData.difficulty, isPerfect: gameData.moves === DIFFICULTY_MODES[gameData.difficulty].paires, won: gameData.won !== false, date: new Date().toISOString() }; // 1) Historique local (statistiques personnelles) this._recordLocalHistory(record); // 2) Classement en ligne try { const res = await fetch(this.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(record) }); const data = await res.json(); DEBUG.log('Score submitted:', data); return data; } catch (e) { console.error('Leaderboard submit error:', e); return { ok: false, error: 'network' }; } } /** * Ajouter la partie à l'historique local du joueur */ static _recordLocalHistory(record) { const history = this._getLocalHistory(); history.push(record); if (history.length > this.MAX_HISTORY) { history.splice(0, history.length - this.MAX_HISTORY); } try { localStorage.setItem(this.KEY_HISTORY, JSON.stringify(history)); } catch (e) { console.error('History save error:', e); } } /** * Historique local des parties du joueur */ static _getLocalHistory() { try { return JSON.parse(localStorage.getItem(this.KEY_HISTORY)) || []; } catch (e) { return []; } } /** * Récupérer le classement en ligne (top X) pour un mode donné. * @returns {Promise} */ static async getLeaderboard(difficulty = 'all', limit = 10) { const url = `${this.API_URL}?difficulty=${encodeURIComponent(difficulty)}&limit=${limit}`; const res = await fetch(url, { headers: { 'Accept': 'application/json' } }); const data = await res.json(); if (!data || data.ok !== true || !Array.isArray(data.scores)) { throw new Error('Réponse invalide du serveur'); } this._cache[difficulty] = data.scores; return data.scores; } /** * Statistiques du joueur, calculées à partir de son historique LOCAL. */ static getPlayerStats() { const scores = this._getLocalHistory(); if (scores.length === 0) { return { totalGames: 0, bestScore: 0, averageScore: 0, averageTime: 0, bestTime: Infinity, winRate: 0, perfectGames: 0 }; } const bestScore = Math.max(...scores.map(s => s.score), 0); const totalScore = scores.reduce((a, b) => a + b.score, 0); const totalTime = scores.reduce((a, b) => a + b.timeSeconds, 0); const bestTime = Math.min(...scores.map(s => s.timeSeconds), Infinity); const perfectGames = scores.filter(s => s.isPerfect).length; const winRate = (perfectGames / scores.length) * 100; return { totalGames: scores.length, bestScore: bestScore, averageScore: Math.round(totalScore / scores.length), averageTime: Math.round(totalTime / scores.length), bestTime: bestTime === Infinity ? 0 : bestTime, winRate: Math.round(winRate), perfectGames: perfectGames }; } /** * Mettre à jour l'affichage du classement (asynchrone). */ static async updateDisplay(difficulty = 'all') { const leaderboardList = document.getElementById('leaderboard-list'); if (!leaderboardList) return; // État de chargement leaderboardList.innerHTML = `
Chargement du classement…
`; let leaderboard; try { leaderboard = await this.getLeaderboard(difficulty, 10); } catch (e) { console.error('Leaderboard load error:', e); leaderboardList.innerHTML = `
⚠️
Classement indisponible
Vérifiez votre connexion et réessayez.
`; return; } if (leaderboard.length === 0) { leaderboardList.innerHTML = `
🎮
Aucun score pour le moment
Jouez une partie pour apparaître au classement!
`; return; } const myUserId = getUserId(); leaderboardList.innerHTML = ''; leaderboard.forEach((score, index) => { const rankItem = document.createElement('div'); rankItem.className = 'rank-item'; if (score.userId === myUserId) { rankItem.classList.add('is-me'); } const rankNumber = this.getRankDisplay(index); const medal = index < 3 ? `class="rank-number medal-${index + 1}"` : 'class="rank-number"'; rankItem.innerHTML = `
${rankNumber}
${formatNumber(score.score)}
${this.escapeHTML(score.playerName)}
⏱ ${formatTime(score.timeSeconds)} 🎯 ${score.moves} ${score.isPerfect ? '' : ''}
`; leaderboardList.appendChild(rankItem); }); if (typeof analyticsManager !== 'undefined') { analyticsManager.track('leaderboard_viewed', { filter: difficulty }); } } /** * Mettre à jour les statistiques du joueur (locales). */ static updatePlayerStats() { const stats = this.getPlayerStats(); const totalGamesEl = document.getElementById('total-games'); const bestScoreEl = document.getElementById('best-score'); const bestTimeEl = document.getElementById('best-time'); const winRateEl = document.getElementById('win-rate'); if (totalGamesEl) totalGamesEl.textContent = stats.totalGames; if (bestScoreEl) bestScoreEl.textContent = formatNumber(stats.bestScore); if (bestTimeEl) bestTimeEl.textContent = stats.bestTime === Infinity || stats.bestTime === 0 ? '--' : formatTime(stats.bestTime); if (winRateEl) winRateEl.textContent = `${stats.winRate}%`; if (bestScoreEl && stats.bestScore > 5000) { bestScoreEl.parentElement.classList.add('high-value'); } if (winRateEl && stats.winRate >= 50) { winRateEl.parentElement.classList.add('high-value'); } } /** * Effacer l'historique local du joueur (DEBUG). * N'affecte PAS le classement en ligne partagé. */ static clearLocalHistory() { if (confirm('Effacer VOS statistiques locales ? (le classement en ligne n\'est pas touché)')) { localStorage.removeItem(this.KEY_HISTORY); this.updatePlayerStats(); DEBUG.log('✓ Historique local effacé'); return true; } return false; } /** * Obtenir l'affichage du rang (numéro ou médaille) */ static getRankDisplay(index) { const medals = ['🥇', '🥈', '🥉']; return medals[index] || `${index + 1}`; } /** * Échapper le HTML (sécurité) */ static escapeHTML(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } } // ============================================ // INITIALISATION // ============================================ // Exporter pour utilisation globale window.LeaderboardManager = LeaderboardManager; DEBUG.log('Leaderboard Manager initialized (online mode)');