360 lines
10 KiB
JavaScript
360 lines
10 KiB
JavaScript
/* ============================================
|
||
GAME STATE & SCORING ENGINE
|
||
============================================ */
|
||
|
||
/**
|
||
* Moteur de scoring avancé
|
||
* Calcule les points avec bonus temps et efficacité
|
||
*/
|
||
class ScoringEngine {
|
||
constructor() {
|
||
this.basePoints = 1000;
|
||
this.difficultyMultiplier = 1;
|
||
this.timeBonus = 0;
|
||
this.efficiencyBonus = 0;
|
||
}
|
||
|
||
/**
|
||
* Calculer le score final
|
||
* Formule : basePoints × difficultyMultiplier + timeBonus + efficiencyBonus
|
||
*/
|
||
calculateFinalScore(moves, timeSeconds, pairesTotal, difficulty) {
|
||
const mode = DIFFICULTY_MODES[difficulty];
|
||
this.difficultyMultiplier = mode.scoreMultiplier;
|
||
|
||
// Bonus temps : points restants
|
||
const timeRemaining = Math.max(0, mode.timeLimit - timeSeconds);
|
||
this.timeBonus = Math.round((timeRemaining / mode.timeLimit) * 500);
|
||
|
||
// Bonus efficacité : pénalité pour erreurs
|
||
// Jeu parfait = pairesTotal mouvements (2 cartes par paire)
|
||
const perfectMoves = pairesTotal;
|
||
const efficiency = Math.max(0, 1 - ((moves - perfectMoves) / (perfectMoves * 2)));
|
||
this.efficiencyBonus = Math.round(efficiency * 500);
|
||
|
||
// Score final
|
||
const baseScore = this.basePoints * this.difficultyMultiplier;
|
||
const finalScore = Math.round(baseScore + this.timeBonus + this.efficiencyBonus);
|
||
|
||
return {
|
||
baseScore: baseScore,
|
||
timeBonus: this.timeBonus,
|
||
efficiencyBonus: this.efficiencyBonus,
|
||
totalScore: finalScore,
|
||
duration: timeSeconds,
|
||
breakdown: {
|
||
base: baseScore,
|
||
temps: this.timeBonus,
|
||
efficacite: this.efficiencyBonus,
|
||
total: finalScore
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Détecter les achievements
|
||
*/
|
||
detectAchievements(moves, timeSeconds, pairesTotal, difficulty) {
|
||
const achievements = [];
|
||
|
||
// 🏅 Jeu parfait (0 erreurs)
|
||
if (moves === pairesTotal) {
|
||
achievements.push({
|
||
id: 'perfect_game',
|
||
name: '🏅 Jeu Parfait',
|
||
icon: '🏅',
|
||
description: 'Trouvé toutes les paires sans erreur!'
|
||
});
|
||
}
|
||
|
||
// ⚡ Vitesse extrême
|
||
if (timeSeconds < DIFFICULTY_MODES[difficulty].timeLimit * 0.3) {
|
||
achievements.push({
|
||
id: 'speedrun',
|
||
name: '⚡ Vitesse Éclair',
|
||
icon: '⚡',
|
||
description: `Complété en ${timeSeconds}s!`
|
||
});
|
||
}
|
||
|
||
// 🔥 Difficile réussi
|
||
if (difficulty === 'hard') {
|
||
achievements.push({
|
||
id: 'hard_mode',
|
||
name: '🔥 Maître du Mode Difficile',
|
||
icon: '🔥',
|
||
description: 'Défi chronométré réussi!'
|
||
});
|
||
}
|
||
|
||
// 🎯 Score parfait
|
||
const perfectScore = 1000 * DIFFICULTY_MODES[difficulty].scoreMultiplier;
|
||
if ((this.basePoints * this.difficultyMultiplier + this.timeBonus + this.efficiencyBonus) >= perfectScore + 900) {
|
||
achievements.push({
|
||
id: 'perfect_score',
|
||
name: '🎯 Score Parfait',
|
||
icon: '🎯',
|
||
description: 'Score quasi-maximal!'
|
||
});
|
||
}
|
||
|
||
// 🏃 Rapidité respectable
|
||
if (timeSeconds < 60 && pairesTotal === 9) {
|
||
achievements.push({
|
||
id: 'fast_completion',
|
||
name: '🏃 Très Rapide',
|
||
icon: '🏃',
|
||
description: 'Moins d\'une minute!'
|
||
});
|
||
}
|
||
|
||
return achievements;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Manager pour le localStorage et persistance
|
||
*/
|
||
class StorageManager {
|
||
static setItem(key, value, isSession = false) {
|
||
try {
|
||
const storage = isSession ? sessionStorage : localStorage;
|
||
storage.setItem(key, JSON.stringify(value));
|
||
return true;
|
||
} catch (e) {
|
||
console.error('Storage error:', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static getItem(key, isSession = false) {
|
||
try {
|
||
const storage = isSession ? sessionStorage : localStorage;
|
||
const item = storage.getItem(key);
|
||
return item ? JSON.parse(item) : null;
|
||
} catch (e) {
|
||
console.error('Storage error:', e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
static removeItem(key, isSession = false) {
|
||
try {
|
||
const storage = isSession ? sessionStorage : localStorage;
|
||
storage.removeItem(key);
|
||
return true;
|
||
} catch (e) {
|
||
console.error('Storage error:', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
static clear(isSession = false) {
|
||
try {
|
||
const storage = isSession ? sessionStorage : localStorage;
|
||
storage.clear();
|
||
return true;
|
||
} catch (e) {
|
||
console.error('Storage error:', e);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Session state - données temporaires du jeu actuel
|
||
*/
|
||
class GameSessionState {
|
||
constructor() {
|
||
this.sessionId = generateUUID();
|
||
this.userId = getUserId();
|
||
this.playerName = getPlayerName();
|
||
this.startTime = null;
|
||
this.endTime = null;
|
||
this.difficulty = 'normal';
|
||
this.moves = 0;
|
||
this.score = 0;
|
||
this.matched = 0;
|
||
this.isPaused = false;
|
||
this.gameActive = false;
|
||
}
|
||
|
||
/**
|
||
* Démarrer une nouvelle session
|
||
*/
|
||
startSession(difficulty) {
|
||
this.sessionId = generateUUID();
|
||
this.startTime = Date.now();
|
||
this.difficulty = difficulty;
|
||
this.moves = 0;
|
||
this.score = 0;
|
||
this.matched = 0;
|
||
this.gameActive = true;
|
||
this.isPaused = false;
|
||
}
|
||
|
||
/**
|
||
* Terminer la session
|
||
*/
|
||
endSession() {
|
||
this.endTime = Date.now();
|
||
this.gameActive = false;
|
||
}
|
||
|
||
/**
|
||
* Obtenir la durée en secondes
|
||
*/
|
||
getDuration() {
|
||
const end = this.endTime || Date.now();
|
||
return Math.floor((end - this.startTime) / 1000);
|
||
}
|
||
|
||
/**
|
||
* Sérialiser pour transmission
|
||
*/
|
||
toJSON() {
|
||
return {
|
||
sessionId: this.sessionId,
|
||
userId: this.userId,
|
||
playerName: this.playerName,
|
||
startTime: this.startTime,
|
||
endTime: this.endTime,
|
||
difficulty: this.difficulty,
|
||
moves: this.moves,
|
||
score: this.score,
|
||
matched: this.matched,
|
||
duration: this.getDuration()
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gestionnaire des notifications
|
||
*/
|
||
class NotificationManager {
|
||
static show(message, type = 'info', duration = 3000) {
|
||
// Créer la notification
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification notification-${type}`;
|
||
notification.textContent = message;
|
||
notification.style.cssText = `
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background: ${this.getTypeColor(type)};
|
||
color: white;
|
||
padding: 16px 24px;
|
||
border-radius: 8px;
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
z-index: 3000;
|
||
animation: slideUp 0.3s ease-out;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
`;
|
||
|
||
document.body.appendChild(notification);
|
||
|
||
// Auto-remove après durée
|
||
setTimeout(() => {
|
||
notification.style.animation = 'fadeOut 0.3s ease-out';
|
||
setTimeout(() => notification.remove(), 300);
|
||
}, duration);
|
||
}
|
||
|
||
static success(message) {
|
||
this.show(message, 'success');
|
||
}
|
||
|
||
static error(message) {
|
||
this.show(message, 'error');
|
||
}
|
||
|
||
static warning(message) {
|
||
this.show(message, 'warning');
|
||
}
|
||
|
||
static info(message) {
|
||
this.show(message, 'info');
|
||
}
|
||
|
||
static getTypeColor(type) {
|
||
const colors = {
|
||
success: '#4CAF50',
|
||
error: '#F44336',
|
||
warning: '#FF9800',
|
||
info: '#2196F3'
|
||
};
|
||
return colors[type] || colors.info;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gestionnaire des préférences utilisateur
|
||
*/
|
||
class PreferencesManager {
|
||
static KEY_PREFERENCES = 'nextgn_preferences';
|
||
|
||
static defaults = {
|
||
soundEnabled: true,
|
||
animationsEnabled: true,
|
||
darkMode: false,
|
||
showStats: true,
|
||
notifications: true
|
||
};
|
||
|
||
static get() {
|
||
const saved = localStorage.getItem(this.KEY_PREFERENCES);
|
||
return saved ? JSON.parse(saved) : this.defaults;
|
||
}
|
||
|
||
static set(preferences) {
|
||
localStorage.setItem(this.KEY_PREFERENCES, JSON.stringify(preferences));
|
||
this.applyPreferences(preferences);
|
||
}
|
||
|
||
static update(key, value) {
|
||
const prefs = this.get();
|
||
prefs[key] = value;
|
||
this.set(prefs);
|
||
}
|
||
|
||
static applyPreferences(prefs) {
|
||
// Appliquer le dark mode
|
||
if (prefs.darkMode) {
|
||
document.body.style.colorScheme = 'dark';
|
||
} else {
|
||
document.body.style.colorScheme = 'light';
|
||
}
|
||
|
||
// Désactiver les animations si demandé
|
||
if (!prefs.animationsEnabled) {
|
||
document.documentElement.style.setProperty('--animation-duration', '0ms');
|
||
}
|
||
}
|
||
|
||
static toggle(key) {
|
||
const prefs = this.get();
|
||
prefs[key] = !prefs[key];
|
||
this.set(prefs);
|
||
return prefs[key];
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// INITIALISATION DES SINGLETONS
|
||
// ============================================
|
||
|
||
// Créer une instance de session au chargement
|
||
const gameSession = new GameSessionState();
|
||
|
||
// Appliquer les préférences utilisateur
|
||
PreferencesManager.applyPreferences(PreferencesManager.get());
|
||
|
||
// Exporter pour utilisation globale
|
||
window.ScoringEngine = ScoringEngine;
|
||
window.StorageManager = StorageManager;
|
||
window.GameSessionState = GameSessionState;
|
||
window.NotificationManager = NotificationManager;
|
||
window.PreferencesManager = PreferencesManager;
|