390 lines
13 KiB
JavaScript
390 lines
13 KiB
JavaScript
/* ============================================
|
|
LEADERBOARD & PLAYER STATISTICS
|
|
============================================ */
|
|
|
|
/**
|
|
* Manager du classement
|
|
*/
|
|
class LeaderboardManager {
|
|
static KEY_LEADERBOARD = 'nextgn_leaderboard';
|
|
static MAX_SCORES = 100;
|
|
|
|
/**
|
|
* Ajouter un score au classement
|
|
*/
|
|
static addScore(gameData) {
|
|
const scores = this.getAllScores();
|
|
|
|
const newScore = {
|
|
sessionId: generateUUID(),
|
|
playerName: getPlayerName(),
|
|
userId: getUserId(),
|
|
score: gameData.score,
|
|
moves: gameData.moves,
|
|
timeSeconds: gameData.duration,
|
|
difficulty: gameData.difficulty,
|
|
date: new Date().toISOString(),
|
|
isPerfect: gameData.moves === DIFFICULTY_MODES[gameData.difficulty].paires,
|
|
won: gameData.won !== false
|
|
};
|
|
|
|
scores.push(newScore);
|
|
|
|
// Limiter à MAX_SCORES (garder les plus récents)
|
|
if (scores.length > this.MAX_SCORES) {
|
|
scores.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
scores.splice(this.MAX_SCORES);
|
|
}
|
|
|
|
localStorage.setItem(this.KEY_LEADERBOARD, JSON.stringify(scores));
|
|
|
|
DEBUG.log('Score added:', newScore);
|
|
|
|
return newScore;
|
|
}
|
|
|
|
/**
|
|
* Obtenir tous les scores
|
|
*/
|
|
static getAllScores() {
|
|
try {
|
|
const scores = JSON.parse(localStorage.getItem(this.KEY_LEADERBOARD)) || [];
|
|
return scores;
|
|
} catch (e) {
|
|
console.error('Leaderboard error:', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtenir le classement (top X)
|
|
*/
|
|
static getLeaderboard(difficulty = 'all', limit = 10) {
|
|
let scores = this.getAllScores();
|
|
|
|
// Filtrer par difficulté
|
|
if (difficulty !== 'all') {
|
|
scores = scores.filter(s => s.difficulty === difficulty);
|
|
}
|
|
|
|
// Trier par score (décroissant)
|
|
scores.sort((a, b) => {
|
|
if (b.score !== a.score) {
|
|
return b.score - a.score;
|
|
}
|
|
// En cas d'égalité, par temps
|
|
return a.timeSeconds - b.timeSeconds;
|
|
});
|
|
|
|
return scores.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* Obtenir les stats d'un joueur
|
|
*/
|
|
static getPlayerStats(userId) {
|
|
const scores = this.getAllScores().filter(s => s.userId === userId);
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Obtenir le rang d'un score
|
|
*/
|
|
static getPlayerRank(userId, difficulty = 'all') {
|
|
const leaderboard = this.getLeaderboard(difficulty, Infinity);
|
|
const rank = leaderboard.findIndex(s => s.userId === userId) + 1;
|
|
return rank > 0 ? rank : null;
|
|
}
|
|
|
|
/**
|
|
* Vérifier si c'est un nouveau record personnel
|
|
*/
|
|
static isPersonalRecord(userId, newScore) {
|
|
const stats = this.getPlayerStats(userId);
|
|
return newScore > stats.bestScore;
|
|
}
|
|
|
|
/**
|
|
* Obtenir les statistiques globales
|
|
*/
|
|
static getGlobalStats() {
|
|
const scores = this.getAllScores();
|
|
|
|
if (scores.length === 0) {
|
|
return {
|
|
totalGames: 0,
|
|
totalPlayers: 0,
|
|
averageScore: 0,
|
|
highestScore: 0,
|
|
fastestCompletion: Infinity
|
|
};
|
|
}
|
|
|
|
const uniquePlayers = new Set(scores.map(s => s.userId));
|
|
const totalScore = scores.reduce((a, b) => a + b.score, 0);
|
|
const highestScore = Math.max(...scores.map(s => s.score), 0);
|
|
const fastestCompletion = Math.min(...scores.map(s => s.timeSeconds), Infinity);
|
|
|
|
return {
|
|
totalGames: scores.length,
|
|
totalPlayers: uniquePlayers.size,
|
|
averageScore: Math.round(totalScore / scores.length),
|
|
highestScore: highestScore,
|
|
fastestCompletion: fastestCompletion === Infinity ? 0 : fastestCompletion
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mettre à jour l'affichage du classement
|
|
*/
|
|
static updateDisplay(difficulty = 'all') {
|
|
const leaderboard = this.getLeaderboard(difficulty, 10);
|
|
const leaderboardList = document.getElementById('leaderboard-list');
|
|
|
|
if (!leaderboardList) return;
|
|
|
|
if (leaderboard.length === 0) {
|
|
leaderboardList.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎮</div>
|
|
<div class="empty-state-text">Aucun score pour le moment</div>
|
|
<div class="empty-state-hint">Jouez une partie pour apparaître au classement!</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
leaderboardList.innerHTML = '';
|
|
|
|
leaderboard.forEach((score, index) => {
|
|
const rankItem = document.createElement('div');
|
|
rankItem.className = 'rank-item';
|
|
|
|
const rankNumber = this.getRankDisplay(index);
|
|
const medal = index < 3 ? `class="rank-number medal-${index + 1}"` : 'class="rank-number"';
|
|
|
|
rankItem.innerHTML = `
|
|
<div ${medal}>${rankNumber}</div>
|
|
<div class="rank-info">
|
|
<div class="player-name">${this.escapeHTML(score.playerName)}</div>
|
|
<div class="player-stats">
|
|
<span>⏱ ${formatTime(score.timeSeconds)}</span>
|
|
<span>🎯 ${score.moves} mouvements</span>
|
|
${score.isPerfect ? '<span>✨ Parfait</span>' : ''}
|
|
</div>
|
|
</div>
|
|
<div class="rank-score">${formatNumber(score.score)}</div>
|
|
<div class="rank-difficulty ${score.difficulty}">${DIFFICULTY_MODES[score.difficulty].name}</div>
|
|
`;
|
|
|
|
leaderboardList.appendChild(rankItem);
|
|
});
|
|
|
|
analyticsManager.track('leaderboard_viewed', { filter: difficulty });
|
|
}
|
|
|
|
/**
|
|
* Mettre à jour les statistiques du joueur
|
|
*/
|
|
static updatePlayerStats() {
|
|
const userId = getUserId();
|
|
const stats = this.getPlayerStats(userId);
|
|
|
|
// Remplir les éléments HTML
|
|
document.getElementById('total-games').textContent = stats.totalGames;
|
|
document.getElementById('best-score').textContent = formatNumber(stats.bestScore);
|
|
document.getElementById('best-time').textContent = stats.bestTime === Infinity ? '--' : formatTime(stats.bestTime);
|
|
document.getElementById('win-rate').textContent = `${stats.winRate}%`;
|
|
|
|
// Ajouter des classes pour animation si valeurs élevées
|
|
if (stats.bestScore > 5000) {
|
|
document.getElementById('best-score').parentElement.classList.add('high-value');
|
|
}
|
|
|
|
if (stats.winRate >= 50) {
|
|
document.getElementById('win-rate').parentElement.classList.add('high-value');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Afficher le rang du joueur
|
|
*/
|
|
static updatePlayerRank(difficulty = 'all') {
|
|
const userId = getUserId();
|
|
const rank = this.getPlayerRank(userId, difficulty);
|
|
|
|
if (rank) {
|
|
const rankDisplay = document.getElementById('player-rank');
|
|
if (rankDisplay) {
|
|
rankDisplay.textContent = `Vous êtes #{rank}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Afficher les statistiques globales
|
|
*/
|
|
static displayGlobalStats() {
|
|
const globalStats = this.getGlobalStats();
|
|
DEBUG.log('Global Statistics:', globalStats);
|
|
|
|
return globalStats;
|
|
}
|
|
|
|
/**
|
|
* Nettoyer les scores (DEBUG)
|
|
*/
|
|
static clearAll() {
|
|
if (confirm('Êtes-vous ABSOLUMENT certain de vouloir supprimer TOUS les scores?')) {
|
|
localStorage.removeItem(this.KEY_LEADERBOARD);
|
|
this.updateDisplay();
|
|
this.updatePlayerStats();
|
|
DEBUG.log('✓ Tous les scores ont été supprimés');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Exporter les scores en CSV
|
|
*/
|
|
static exportCSV() {
|
|
const scores = this.getAllScores();
|
|
|
|
let csv = 'Rang,Joueur,Score,Difficulté,Temps (s),Mouvements,Parfait,Date\n';
|
|
|
|
scores.forEach((score, index) => {
|
|
csv += `${index + 1},"${score.playerName}",${score.score},"${score.difficulty}",${score.timeSeconds},${score.moves},${score.isPerfect ? 'Oui' : 'Non'},"${score.date}"\n`;
|
|
});
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `leaderboard-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Statistiques avancées
|
|
*/
|
|
class AdvancedStats {
|
|
/**
|
|
* Obtenir la distribution des scores par difficulté
|
|
*/
|
|
static getDistributionByDifficulty() {
|
|
const scores = LeaderboardManager.getAllScores();
|
|
const distribution = {};
|
|
|
|
Object.keys(DIFFICULTY_MODES).forEach(difficulty => {
|
|
distribution[difficulty] = scores.filter(s => s.difficulty === difficulty).length;
|
|
});
|
|
|
|
return distribution;
|
|
}
|
|
|
|
/**
|
|
* Obtenir la tendance temporelle
|
|
*/
|
|
static getTimelineTrend(days = 7) {
|
|
const scores = LeaderboardManager.getAllScores();
|
|
const now = new Date();
|
|
const timeline = {};
|
|
|
|
for (let i = 0; i < days; i++) {
|
|
const date = new Date(now);
|
|
date.setDate(date.getDate() - i);
|
|
const dateStr = date.toISOString().slice(0, 10);
|
|
timeline[dateStr] = scores.filter(s => s.date.slice(0, 10) === dateStr).length;
|
|
}
|
|
|
|
return timeline;
|
|
}
|
|
|
|
/**
|
|
* Obtenir la moyenne de temps par difficulté
|
|
*/
|
|
static getAverageTimeByDifficulty() {
|
|
const scores = LeaderboardManager.getAllScores();
|
|
const averages = {};
|
|
|
|
Object.keys(DIFFICULTY_MODES).forEach(difficulty => {
|
|
const diffScores = scores.filter(s => s.difficulty === difficulty);
|
|
if (diffScores.length > 0) {
|
|
const totalTime = diffScores.reduce((a, b) => a + b.timeSeconds, 0);
|
|
averages[difficulty] = Math.round(totalTime / diffScores.length);
|
|
}
|
|
});
|
|
|
|
return averages;
|
|
}
|
|
|
|
/**
|
|
* Taux de réussite global
|
|
*/
|
|
static getGlobalSuccessRate() {
|
|
const scores = LeaderboardManager.getAllScores();
|
|
if (scores.length === 0) return 0;
|
|
|
|
const perfectGames = scores.filter(s => s.isPerfect).length;
|
|
return Math.round((perfectGames / scores.length) * 100);
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// INITIALISATION
|
|
// ============================================
|
|
|
|
// Exporter pour utilisation globale
|
|
window.LeaderboardManager = LeaderboardManager;
|
|
window.AdvancedStats = AdvancedStats;
|
|
|
|
DEBUG.log('Leaderboard Manager initialized');
|