const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const scoreElement = document.getElementById('score'); const levelElement = document.getElementById('level'); const livesElement = document.getElementById('lives'); const statusElement = document.getElementById('status'); const restartBtn = document.getElementById('restartBtn'); const usernameInput = document.getElementById('username'); const leaderboardElement = document.getElementById('leaderboard'); const gameLeaderboardContent = document.getElementById('gameLeaderboardContent'); const gameOverlay = document.getElementById('gameOverlay'); const finalScoreElement = document.getElementById('finalScore'); const finalLevelElement = document.getElementById('finalLevel'); const overlayRestartBtn = document.getElementById('overlayRestartBtn'); const confirmUsernameBtn = document.getElementById('confirmUsernameBtn'); const gameOverUsernameInput = document.getElementById('gameOverUsername'); const saveScoreBtn = document.getElementById('saveScoreBtn'); const scoreSavedMsg = document.getElementById('scoreSavedMsg'); // === GESTION DU MENU === const mainMenu = document.getElementById('mainMenu'); const gameWrapper = document.getElementById('gameWrapper'); const playBtn = document.getElementById('playBtn'); const scoresBtn = document.getElementById('scoresBtn'); const rulesBtn = document.getElementById('rulesBtn'); const backToMenuBtn = document.getElementById('backToMenuBtn'); const leaderboardModal = document.getElementById('leaderboardModal'); const closeModalBtn = document.getElementById('closeModalBtn'); const rulesModal = document.getElementById('rulesModal'); const closeRulesBtn = document.getElementById('closeRulesBtn'); const menuLeaderboard = document.getElementById('menuLeaderboard'); const leaderboardContainer = document.getElementById('leaderboardContainer'); const closeLeaderboardBtn = document.getElementById('closeLeaderboardBtn'); const fullscreenBtn = document.getElementById('fullscreenBtn'); const customizeBtn = document.getElementById('customizeBtn'); const customizeModal = document.getElementById('customizeModal'); const closeCustomizeBtn = document.getElementById('closeCustomizeBtn'); const saveCustomizeBtn = document.getElementById('saveCustomizeBtn'); const playerPreviewCanvas = document.getElementById('playerPreviewCanvas'); const colorOptions = document.querySelectorAll('.color-option'); const shapeOptions = document.querySelectorAll('.shape-option'); // Fonction pour vérifier si on est en mode plein écran function isFullscreen() { return !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); } let usernameConfirmed = false; let gameOverUsername = null; // Personnalisation du joueur let playerColor = localStorage.getItem('playerColor') || 'rainbow'; let playerShape = localStorage.getItem('playerShape') || 'round'; const CELL_SIZE = 25; const COLS = 30; const ROWS = 30; const WALL = 1; const DOT = 2; const EMPTY = 0; const TUNNEL = 3; const BONUS_CHERRY = 4; const BONUS_LUDO = 5; const BONUS_SPEED = 6; const BONUS_SHIELD = 7; const BONUS_BOMB = 8; const BONUS_MULTIPLIER = 9; // Types de fantômes const GHOST_HUNTER = 'hunter'; const GHOST_PATROL = 'patrol'; const GHOST_FAST = 'fast'; const GHOST_INVISIBLE = 'invisible'; const GHOST_NORMAL = 'normal'; // Zones spéciales const ZONE_TELEPORT = 10; const ZONE_BONUS = 11; const ZONE_DANGER = 12; // Fonction pour obtenir le type de fruit selon le niveau function getFruitType() { const fruits = [ { name: 'cerise', color: '#ff0000', stemColor: '#00ff00', level: 1 }, { name: 'banane', color: '#ffff00', stemColor: '#00aa00', level: 2 }, { name: 'orange', color: '#ff8800', stemColor: '#00aa00', level: 3 }, { name: 'pomme', color: '#ff0000', stemColor: '#8b4513', level: 4 }, { name: 'raisin', color: '#8b00ff', stemColor: '#00aa00', level: 5 }, { name: 'fraise', color: '#ff0066', stemColor: '#00ff00', level: 6 }, { name: 'ananas', color: '#ffd700', stemColor: '#228b22', level: 7 } ]; // Trouver le fruit correspondant au niveau (avec rotation pour les niveaux élevés) const fruitIndex = Math.min(level - 1, fruits.length - 1); return fruits[fruitIndex]; } const originalMaze1 = [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,1,1,1,3,3,3,3,1,1,1,0,1,1,2,1,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,0,2,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,2,2,1,1,2,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,2,1,1,2,2,2,1], [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] ]; const originalMaze2 = [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,1,1,1,3,3,3,3,1,1,1,0,1,1,2,1,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,0,2,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,2,2,1,1,2,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,2,1,1,2,2,2,1], [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] ]; const originalMaze3 = [ [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,1,1,2,1], [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], [1,1,1,1,1,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,1,2,1,1,1,1,1,0,1,1,1,1,0,1,1,1,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,1,1,1,3,3,3,3,1,1,1,0,1,1,2,1,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,0,2,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,0,0,0,0,0,0,0,0,1,0,1,1,2,1,1,1,1,1,1], [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0], [0,0,0,0,0,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,0,0,0,0,0], [1,1,1,1,1,1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,2,1,1,1,1,1,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,1,1,1,1,2,1,1,1,1,1,1,2,1,1,2,1,1,1,1,1,1,2,1,1,1,1,2,1], [1,2,2,2,1,1,2,2,2,2,2,2,2,2,0,0,2,2,2,2,2,2,2,2,1,1,2,2,2,1], [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], [1,1,1,2,1,1,2,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,2,1,1,2,1,1,1], [1,2,2,2,2,2,2,1,1,2,2,2,2,1,1,1,1,2,2,2,2,1,1,2,2,2,2,2,2,1], [1,2,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,2,1], [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1], [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] ]; const mazeVariants = [originalMaze1, originalMaze2, originalMaze3]; let originalMaze = originalMaze1; let currentMazeIndex = 0; let maze = originalMaze.map(row => [...row]); let score = 0; let level = 1; let gameRunning = true; let isPaused = false; let totalDots = 0; let cherriesEaten = 0; let isChangingLevel = false; let cherryEatenRecently = false; let cherryEatenTimer = 0; let lives = 3; let dotsRemainingElement = document.getElementById('dotsRemaining'); let pursuitIndicator = document.getElementById('pursuitIndicator'); let pursuitTimerElement = document.getElementById('pursuitTimer'); let comboDisplay = document.getElementById('comboDisplay'); let comboValueElement = document.getElementById('comboValue'); let frenzyIndicator = document.getElementById('frenzyIndicator'); let speedBoostIndicator = document.getElementById('speedBoostIndicator'); let shieldIndicator = document.getElementById('shieldIndicator'); let multiplierIndicator = document.getElementById('multiplierIndicator'); let multiplierValueElement = document.getElementById('multiplierValue'); // Système de combo let comboCount = 0; let comboMultiplier = 1; let lastDotTime = 0; const COMBO_TIMEOUT = 3000; // 3 secondes // Power-ups actifs let speedBoostActive = false; let speedBoostTimer = 0; let shieldActive = false; let shieldTimer = 0; let scoreMultiplierActive = false; let scoreMultiplierTimer = 0; let scoreMultiplierValue = 1; // Mode Frenzy let frenzyModeActive = false; let frenzyTimer = 0; let frenzyCooldown = 0; const FRENZY_INTERVAL = 30000; // 30 secondes const FRENZY_DURATION = 10000; // 10 secondes // Obstacles temporaires let temporaryWalls = []; let slowZones = []; let traps = []; // Zones spéciales let specialZones = []; class Pacman { constructor(color = null, shape = null) { this.x = 14; this.y = 23; this.direction = 0; this.nextDirection = 0; this.mouthAngle = 0; this.mouthOpen = true; this.baseSpeed = 0.25; this.speed = this.baseSpeed; this.pixelX = this.x * CELL_SIZE + CELL_SIZE / 2; this.pixelY = this.y * CELL_SIZE + CELL_SIZE / 2; this.colorAnimation = 0; this.color = color || playerColor; this.shape = shape || playerShape; } update() { if (!gameRunning || isPaused) return; this.mouthAngle += 0.2; if (this.mouthAngle > Math.PI * 2) { this.mouthAngle = 0; this.mouthOpen = !this.mouthOpen; } this.colorAnimation += 0.3; if (this.colorAnimation > Math.PI * 2) { this.colorAnimation = 0; } const gridX = Math.floor(this.pixelX / CELL_SIZE); const gridY = Math.floor(this.pixelY / CELL_SIZE); if (this.canMove(this.nextDirection)) { this.direction = this.nextDirection; } if (this.canMove(this.direction)) { const dx = [0, 1, 0, -1]; const dy = [-1, 0, 1, 0]; this.pixelX += dx[this.direction] * this.speed * CELL_SIZE; this.pixelY += dy[this.direction] * this.speed * CELL_SIZE; if (this.pixelX < 0) { this.pixelX = COLS * CELL_SIZE; } else if (this.pixelX > COLS * CELL_SIZE) { this.pixelX = 0; } } else { this.pixelX = gridX * CELL_SIZE + CELL_SIZE / 2; this.pixelY = gridY * CELL_SIZE + CELL_SIZE / 2; } this.x = Math.floor(this.pixelX / CELL_SIZE); this.y = Math.floor(this.pixelY / CELL_SIZE); this.collectDot(); } canMove(direction) { const dx = [0, 1, 0, -1]; const dy = [-1, 0, 1, 0]; const nextX = Math.floor((this.pixelX + dx[direction] * CELL_SIZE * 0.5) / CELL_SIZE); const nextY = Math.floor((this.pixelY + dy[direction] * CELL_SIZE * 0.5) / CELL_SIZE); if (nextX < 0 || nextX >= COLS || nextY < 0 || nextY >= ROWS) { return true; } return maze[nextY][nextX] !== WALL; } collectDot() { if (maze[this.y][this.x] === DOT) { maze[this.y][this.x] = EMPTY; // Système de combo const currentTime = Date.now(); if (currentTime - lastDotTime < COMBO_TIMEOUT) { comboCount++; comboMultiplier = Math.min(1 + Math.floor(comboCount / 5), 5); // Max x5 } else { comboCount = 1; comboMultiplier = 1; } lastDotTime = currentTime; // Afficher le combo if (comboDisplay && comboValueElement) { if (comboMultiplier > 1) { comboDisplay.style.display = 'block'; comboValueElement.textContent = comboMultiplier; } else { comboDisplay.style.display = 'none'; } } let points = 10 * comboMultiplier * (frenzyModeActive ? 3 : 1) * scoreMultiplierValue; score += Math.floor(points); scoreElement.textContent = score; totalDots--; if (dotsRemainingElement) { dotsRemainingElement.textContent = totalDots; } } else if (maze[this.y][this.x] === BONUS_CHERRY) { if (isChangingLevel) { console.log('Changement de niveau en cours, cerise ignorée'); return; } console.log('Cerise collectée, cherriesEaten:', cherriesEaten); maze[this.y][this.x] = EMPTY; score += 100; scoreElement.textContent = score; bonuses = bonuses.filter(b => !(b.x === this.x && b.y === this.y && b.type === BONUS_CHERRY)); cherriesEaten++; cherryEatenRecently = true; cherryEatenTimer = Math.max(150, 300 - (level - 1) * 20); // Rendre tous les fantômes vulnérables const vulnerableTime = Math.max(60, 180 - (level - 1) * 15); for (let ghost of ghosts) { ghost.isVulnerable = true; ghost.vulnerableTimer = vulnerableTime; } // Afficher l'indicateur de poursuite (sauf en mode plein écran) if (pursuitIndicator && !isFullscreen()) { pursuitIndicator.style.display = 'block'; } console.log('Après incrémentation, cherriesEaten:', cherriesEaten, 'isChangingLevel:', isChangingLevel); if (cherriesEaten >= 4 && !isChangingLevel) { console.log('4 cerises mangées, passage au niveau suivant'); cherriesEaten = 0; nextLevel(); } } else if (maze[this.y][this.x] === BONUS_LUDO) { maze[this.y][this.x] = EMPTY; let points = 200 * (frenzyModeActive ? 3 : 1) * scoreMultiplierValue; score += Math.floor(points); scoreElement.textContent = score; bonuses = bonuses.filter(b => !(b.x === this.x && b.y === this.y && b.type === BONUS_LUDO)); } else if (maze[this.y][this.x] === BONUS_SPEED) { maze[this.y][this.x] = EMPTY; speedBoostActive = true; speedBoostTimer = 600; // 10 secondes à 60 FPS this.speed = this.baseSpeed * 2; bonuses = bonuses.filter(b => !(b.x === this.x && b.y === this.y && b.type === BONUS_SPEED)); if (speedBoostIndicator) speedBoostIndicator.style.display = 'block'; } else if (maze[this.y][this.x] === BONUS_SHIELD) { maze[this.y][this.x] = EMPTY; shieldActive = true; shieldTimer = 900; // 15 secondes bonuses = bonuses.filter(b => !(b.x === this.x && b.y === this.y && b.type === BONUS_SHIELD)); if (shieldIndicator) shieldIndicator.style.display = 'block'; } else if (maze[this.y][this.x] === BONUS_BOMB) { maze[this.y][this.x] = EMPTY; // Repousser tous les fantômes for (let ghost of ghosts) { const dx = ghost.x - this.x; const dy = ghost.y - this.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 5) { ghost.x = Math.max(0, Math.min(COLS - 1, ghost.x + Math.sign(dx) * 3)); ghost.y = Math.max(0, Math.min(ROWS - 1, ghost.y + Math.sign(dy) * 3)); ghost.pixelX = ghost.x * CELL_SIZE + CELL_SIZE / 2; ghost.pixelY = ghost.y * CELL_SIZE + CELL_SIZE / 2; } } bonuses = bonuses.filter(b => !(b.x === this.x && b.y === this.y && b.type === BONUS_BOMB)); } else if (maze[this.y][this.x] === BONUS_MULTIPLIER) { maze[this.y][this.x] = EMPTY; scoreMultiplierActive = true; scoreMultiplierTimer = 1800; // 30 secondes scoreMultiplierValue = 2; bonuses = bonuses.filter(b => !(b.x === this.x && b.y === this.y && b.type === BONUS_MULTIPLIER)); if (multiplierIndicator && multiplierValueElement) { multiplierIndicator.style.display = 'block'; multiplierValueElement.textContent = scoreMultiplierValue; } } else if (maze[this.y][this.x] === ZONE_TELEPORT) { // Téléportation aléatoire let newX, newY; do { newX = Math.floor(Math.random() * COLS); newY = Math.floor(Math.random() * ROWS); } while (maze[newY][newX] === WALL || (newX === this.x && newY === this.y)); this.x = newX; this.y = newY; this.pixelX = newX * CELL_SIZE + CELL_SIZE / 2; this.pixelY = newY * CELL_SIZE + CELL_SIZE / 2; } else if (maze[this.y][this.x] === ZONE_BONUS) { // Zone bonus : double les points // Déjà géré par le système de combo } else if (maze[this.y][this.x] === ZONE_DANGER) { // Zone danger : ralentit Pacman this.speed = this.baseSpeed * 0.5; setTimeout(() => { if (!speedBoostActive) { this.speed = this.baseSpeed; } }, 2000); } } draw() { ctx.save(); ctx.translate(this.pixelX, this.pixelY); const rotation = [Math.PI * 1.5, 0, Math.PI * 0.5, Math.PI]; ctx.rotate(rotation[this.direction]); // Utiliser la couleur personnalisée let baseFillColor; if (this.color === 'rainbow') { const hue = (this.colorAnimation * 180 / Math.PI) % 360; baseFillColor = `hsl(${hue}, 100%, 50%)`; ctx.fillStyle = baseFillColor; } else { const colorMap = { 'yellow': '#ffd700', 'red': '#ff4444', 'blue': '#4444ff', 'green': '#44ff44', 'purple': '#aa44ff', 'orange': '#ff8844', 'pink': '#ff44aa' }; baseFillColor = colorMap[this.color] || '#ffd700'; ctx.fillStyle = baseFillColor; } const size = CELL_SIZE * 0.4; ctx.beginPath(); if (this.shape === 'triangle') { const triangleSize = size * 1.5; // Plus grand pour une meilleure visibilité // Dessiner le triangle avec contour et yeux ctx.beginPath(); if (this.mouthOpen) { // Triangle avec bouche ouverte - ouverture plus grande et visible ctx.moveTo(0, -triangleSize * 0.9); ctx.lineTo(-triangleSize * 0.85, triangleSize * 0.7); ctx.lineTo(-triangleSize * 0.4, triangleSize * 0.4); ctx.lineTo(0, triangleSize * 0.2); ctx.lineTo(triangleSize * 0.4, triangleSize * 0.4); ctx.lineTo(triangleSize * 0.85, triangleSize * 0.7); ctx.closePath(); } else { // Triangle équilatéral complet ctx.moveTo(0, -triangleSize); ctx.lineTo(-triangleSize * 0.866, triangleSize * 0.5); // cos(30°) ≈ 0.866 ctx.lineTo(triangleSize * 0.866, triangleSize * 0.5); ctx.closePath(); } // Remplir le triangle ctx.fill(); // Contour noir épais pour plus de définition ctx.strokeStyle = '#000000'; ctx.lineWidth = 2.5; ctx.stroke(); // Dessiner les yeux ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(-triangleSize * 0.2, -triangleSize * 0.15, triangleSize * 0.12, 0, Math.PI * 2); ctx.arc(triangleSize * 0.2, -triangleSize * 0.15, triangleSize * 0.12, 0, Math.PI * 2); ctx.fill(); // Pupilles ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.arc(-triangleSize * 0.2, -triangleSize * 0.15, triangleSize * 0.06, 0, Math.PI * 2); ctx.arc(triangleSize * 0.2, -triangleSize * 0.15, triangleSize * 0.06, 0, Math.PI * 2); ctx.fill(); // Reflets dans les yeux pour plus de vie ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(-triangleSize * 0.22, -triangleSize * 0.17, triangleSize * 0.03, 0, Math.PI * 2); ctx.arc(triangleSize * 0.18, -triangleSize * 0.17, triangleSize * 0.03, 0, Math.PI * 2); ctx.fill(); // Restaurer la couleur originale ctx.fillStyle = baseFillColor; } else { // Forme ronde (par défaut) if (this.mouthOpen) { ctx.arc(0, 0, size, 0.2, Math.PI * 2 - 0.2); } else { ctx.arc(0, 0, size, 0, Math.PI * 2); } ctx.lineTo(0, 0); ctx.fill(); } ctx.restore(); } } class Ghost { constructor(x, y, color, type = GHOST_NORMAL) { this.x = x; this.y = y; this.color = color; this.type = type; this.direction = Math.floor(Math.random() * 4); this.baseSpeed = 0.15; this.speed = this.baseSpeed; this.pixelX = this.x * CELL_SIZE + CELL_SIZE / 2; this.pixelY = this.y * CELL_SIZE + CELL_SIZE / 2; this.moveCounter = 0; this.moveInterval = 30; this.isVulnerable = false; this.vulnerableTimer = 0; this.startX = x; this.startY = y; this.isInvisible = false; this.invisibleTimer = 0; this.patrolTarget = null; this.patrolIndex = 0; // Ajustements selon le type if (type === GHOST_FAST) { this.baseSpeed = 0.16; } else if (type === GHOST_INVISIBLE && level >= 5) { this.isInvisible = true; this.invisibleTimer = 300; } } updateSpeed() { // Vitesse fixe comme au niveau 3 (x1.4) pour tous les niveaux let speedMultiplier = 1.4; if (this.type === GHOST_FAST) { speedMultiplier *= 1.1; } this.speed = this.baseSpeed * speedMultiplier; } update() { if (!gameRunning || isPaused) return; // Gestion de l'invisibilité if (this.type === GHOST_INVISIBLE && level >= 5) { this.invisibleTimer--; if (this.invisibleTimer <= 0) { this.isInvisible = !this.isInvisible; this.invisibleTimer = this.isInvisible ? 300 : 300; } } // Gestion de la vulnérabilité if (this.isVulnerable) { this.vulnerableTimer--; if (this.vulnerableTimer <= 0) { this.isVulnerable = false; } } if (cherryEatenTimer > 0) { cherryEatenTimer--; } else { cherryEatenRecently = false; // Rendre les fantômes vulnérables quand une cerise est mangée if (!this.isVulnerable && cherryEatenTimer === 0) { // La vulnérabilité est gérée dans collectDot() } } this.moveInterval = Math.max(5, 15 - (level - 1) * 1.5); this.moveCounter++; if (this.moveCounter > this.moveInterval || !this.canMove(this.direction)) { const possibleDirections = []; for (let i = 0; i < 4; i++) { if (this.canMove(i)) { possibleDirections.push(i); } } if (possibleDirections.length > 0) { if (this.isVulnerable) { // Fuir le joueur quand vulnérable this.direction = this.getDirectionAwayFromPacman(possibleDirections); } else { // Tous les fantômes chassent le joueur (sauf patrouilleurs qui patrouillent si très loin) if (this.type === GHOST_PATROL) { const distance = Math.sqrt( Math.pow(pacman.x - this.x, 2) + Math.pow(pacman.y - this.y, 2) ); // Si proche, chasser, sinon patrouiller (seulement si très loin) if (distance < 12) { this.direction = this.getDirectionToPacman(possibleDirections); } else { this.direction = this.getPatrolDirection(possibleDirections); } } else { // Tous les autres chassent directement this.direction = this.getDirectionToPacman(possibleDirections); } } } this.moveCounter = 0; } const dx = [0, 1, 0, -1]; const dy = [-1, 0, 1, 0]; this.pixelX += dx[this.direction] * this.speed * CELL_SIZE; this.pixelY += dy[this.direction] * this.speed * CELL_SIZE; if (this.pixelX < 0) { this.pixelX = COLS * CELL_SIZE; } else if (this.pixelX > COLS * CELL_SIZE) { this.pixelX = 0; } this.x = Math.floor(this.pixelX / CELL_SIZE); this.y = Math.floor(this.pixelY / CELL_SIZE); } getDirectionToPacman(possibleDirections) { const dx = [0, 1, 0, -1]; const dy = [-1, 0, 1, 0]; // Prédire la position future de Pacman basée sur sa direction let targetX = pacman.x; let targetY = pacman.y; // Prédiction améliorée pour tous les niveaux const predictionSteps = level >= 3 ? 4 : 3; // Si Pacman bouge, prédire où il sera if (pacman.direction !== undefined) { const futureX = pacman.x + dx[pacman.direction] * predictionSteps; const futureY = pacman.y + dy[pacman.direction] * predictionSteps; if (futureX >= 0 && futureX < COLS && futureY >= 0 && futureY < ROWS) { targetX = futureX; targetY = futureY; } } let bestDirection = possibleDirections[0]; let minDistance = Infinity; // Éviter de revenir en arrière si possible const oppositeDirection = (this.direction + 2) % 4; for (let dir of possibleDirections) { // Permettre le retour en arrière si c'est la meilleure direction // (pas de restriction pour une poursuite plus agressive) const nextX = this.x + dx[dir]; const nextY = this.y + dy[dir]; const distance = Math.sqrt( Math.pow(targetX - nextX, 2) + Math.pow(targetY - nextY, 2) ); if (distance < minDistance) { minDistance = distance; bestDirection = dir; } } return bestDirection; } getDirectionAwayFromPacman(possibleDirections) { const dx = [0, 1, 0, -1]; const dy = [-1, 0, 1, 0]; let bestDirection = possibleDirections[0]; let maxDistance = -1; for (let dir of possibleDirections) { const nextX = this.x + dx[dir]; const nextY = this.y + dy[dir]; const distance = Math.sqrt( Math.pow(pacman.x - nextX, 2) + Math.pow(pacman.y - nextY, 2) ); if (distance > maxDistance) { maxDistance = distance; bestDirection = dir; } } return bestDirection; } getPatrolDirection(possibleDirections) { // Patrouille : bloque les passages stratégiques const dx = [0, 1, 0, -1]; const dy = [-1, 0, 1, 0]; // Chercher à se placer entre Pacman et les sorties const pacmanX = pacman.x; const pacmanY = pacman.y; // Si proche de Pacman, le poursuivre const distance = Math.sqrt( Math.pow(pacmanX - this.x, 2) + Math.pow(pacmanY - this.y, 2) ); if (distance < 5) { return this.getDirectionToPacman(possibleDirections); } // Sinon, patrouiller vers les zones centrales const centerX = COLS / 2; const centerY = ROWS / 2; let bestDirection = possibleDirections[0]; let minDistance = Infinity; for (let dir of possibleDirections) { const nextX = this.x + dx[dir]; const nextY = this.y + dy[dir]; const dist = Math.sqrt( Math.pow(centerX - nextX, 2) + Math.pow(centerY - nextY, 2) ); if (dist < minDistance) { minDistance = dist; bestDirection = dir; } } return bestDirection; } canMove(direction) { const dx = [0, 1, 0, -1]; const dy = [-1, 0, 1, 0]; const nextX = Math.floor((this.pixelX + dx[direction] * CELL_SIZE * 0.5) / CELL_SIZE); const nextY = Math.floor((this.pixelY + dy[direction] * CELL_SIZE * 0.5) / CELL_SIZE); if (nextX < 0 || nextX >= COLS || nextY < 0 || nextY >= ROWS) { return true; } return maze[nextY][nextX] !== WALL; } draw() { // Si invisible et niveau >= 5, ne pas dessiner 50% du temps if (this.type === GHOST_INVISIBLE && this.isInvisible && level >= 5) { const shouldDraw = Math.floor(Date.now() / 200) % 2 === 0; if (!shouldDraw) { return; // Ne pas dessiner quand invisible } } ctx.save(); ctx.translate(this.pixelX, this.pixelY); const size = CELL_SIZE * 0.75; // Si vulnérable, afficher en bleu clignotant if (this.isVulnerable) { const flash = Math.floor(this.vulnerableTimer / 10) % 2; ctx.fillStyle = flash === 0 ? '#0000ff' : '#ffffff'; } else { ctx.fillStyle = this.color; // Fantôme invisible : opacité réduite if (this.type === GHOST_INVISIBLE && this.isInvisible && level >= 5) { ctx.globalAlpha = 0.5; } } ctx.strokeStyle = '#000000'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, -size * 0.3, size * 0.5, Math.PI, 0, false); ctx.fill(); ctx.stroke(); ctx.fillRect(-size * 0.5, -size * 0.3, size * 1.0, size * 0.7); ctx.strokeRect(-size * 0.5, -size * 0.3, size * 1.0, size * 0.7); const waveHeight = size * 0.15; const waveWidth = size * 0.2; ctx.beginPath(); ctx.moveTo(-size * 0.5, size * 0.4); for (let i = 0; i < 5; i++) { const x = -size * 0.5 + i * waveWidth; const y = size * 0.4 + (i % 2 === 0 ? 0 : waveHeight); ctx.lineTo(x, y); } ctx.lineTo(size * 0.5, size * 0.4); ctx.lineTo(size * 0.5, size * 0.7); ctx.lineTo(-size * 0.5, size * 0.7); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(-size * 0.2, -size * 0.1, size * 0.12, 0, Math.PI * 2); ctx.arc(size * 0.2, -size * 0.1, size * 0.12, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.arc(-size * 0.2, -size * 0.1, size * 0.06, 0, Math.PI * 2); ctx.arc(size * 0.2, -size * 0.1, size * 0.06, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1.0; ctx.restore(); } } class Bonus { constructor(x, y, type) { this.x = x; this.y = y; this.type = type; this.animation = 0; } update() { this.animation += 0.1; if (this.animation > Math.PI * 2) { this.animation = 0; } } draw() { const cellX = this.x * CELL_SIZE + CELL_SIZE / 2; const cellY = this.y * CELL_SIZE + CELL_SIZE / 2; const scale = 1 + Math.sin(this.animation) * 0.2; ctx.save(); ctx.translate(cellX, cellY); ctx.scale(scale, scale); if (this.type === BONUS_CHERRY) { const fruit = getFruitType(); // Dessiner le fruit selon le type if (fruit.name === 'cerise') { // Cerise (rouge avec feuille verte) ctx.fillStyle = fruit.color; ctx.beginPath(); ctx.arc(0, 0, CELL_SIZE * 0.25, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = fruit.stemColor; ctx.beginPath(); ctx.arc(-CELL_SIZE * 0.15, -CELL_SIZE * 0.2, CELL_SIZE * 0.1, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#00aa00'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-CELL_SIZE * 0.15, -CELL_SIZE * 0.3); ctx.lineTo(-CELL_SIZE * 0.25, -CELL_SIZE * 0.4); ctx.stroke(); } else if (fruit.name === 'banane') { // Banane (jaune courbée) ctx.fillStyle = fruit.color; ctx.beginPath(); // Dessiner une forme de banane courbée ctx.moveTo(-CELL_SIZE * 0.25, -CELL_SIZE * 0.1); ctx.quadraticCurveTo(0, CELL_SIZE * 0.2, CELL_SIZE * 0.25, -CELL_SIZE * 0.1); ctx.quadraticCurveTo(0, -CELL_SIZE * 0.3, -CELL_SIZE * 0.25, -CELL_SIZE * 0.1); ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#ffaa00'; ctx.lineWidth = 2; ctx.stroke(); // Tige ctx.strokeStyle = fruit.stemColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-CELL_SIZE * 0.2, -CELL_SIZE * 0.15); ctx.lineTo(-CELL_SIZE * 0.25, -CELL_SIZE * 0.25); ctx.stroke(); } else if (fruit.name === 'orange') { // Orange (cercle orange) ctx.fillStyle = fruit.color; ctx.beginPath(); ctx.arc(0, 0, CELL_SIZE * 0.25, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#ff6600'; ctx.lineWidth = 2; ctx.stroke(); // Tige ctx.strokeStyle = fruit.stemColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, -CELL_SIZE * 0.25); ctx.lineTo(0, -CELL_SIZE * 0.35); ctx.stroke(); } else if (fruit.name === 'pomme') { // Pomme (rouge avec tige brune) ctx.fillStyle = fruit.color; ctx.beginPath(); ctx.arc(0, CELL_SIZE * 0.05, CELL_SIZE * 0.25, 0, Math.PI * 2); ctx.fill(); // Feuille (ovale) ctx.fillStyle = '#00ff00'; ctx.beginPath(); ctx.scale(1, 1.5); ctx.arc(CELL_SIZE * 0.15, -CELL_SIZE * 0.07, CELL_SIZE * 0.08, 0, Math.PI * 2); ctx.scale(1, 1/1.5); ctx.fill(); // Tige ctx.strokeStyle = fruit.stemColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, -CELL_SIZE * 0.2); ctx.lineTo(0, -CELL_SIZE * 0.3); ctx.stroke(); } else if (fruit.name === 'raisin') { // Raisin (grappes violettes) ctx.fillStyle = fruit.color; for (let i = 0; i < 3; i++) { for (let j = 0; j < 2; j++) { ctx.beginPath(); ctx.arc((i - 1) * CELL_SIZE * 0.15, (j - 0.5) * CELL_SIZE * 0.15, CELL_SIZE * 0.1, 0, Math.PI * 2); ctx.fill(); } } // Tige ctx.strokeStyle = fruit.stemColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, -CELL_SIZE * 0.3); ctx.lineTo(0, -CELL_SIZE * 0.4); ctx.stroke(); } else if (fruit.name === 'fraise') { // Fraise (rouge avec graines) ctx.fillStyle = fruit.color; ctx.beginPath(); ctx.arc(0, CELL_SIZE * 0.05, CELL_SIZE * 0.25, 0, Math.PI * 2); ctx.fill(); // Graines ctx.fillStyle = '#ffff00'; for (let i = 0; i < 5; i++) { ctx.beginPath(); ctx.arc((i - 2) * CELL_SIZE * 0.1, CELL_SIZE * 0.05, 2, 0, Math.PI * 2); ctx.fill(); } // Feuilles ctx.fillStyle = fruit.stemColor; ctx.beginPath(); ctx.arc(-CELL_SIZE * 0.15, -CELL_SIZE * 0.1, CELL_SIZE * 0.08, 0, Math.PI * 2); ctx.arc(CELL_SIZE * 0.15, -CELL_SIZE * 0.1, CELL_SIZE * 0.08, 0, Math.PI * 2); ctx.fill(); } else if (fruit.name === 'ananas') { // Ananas (jaune avec texture) ctx.fillStyle = fruit.color; ctx.beginPath(); // Forme ovale pour l'ananas ctx.scale(0.7, 1); ctx.arc(0, 0, CELL_SIZE * 0.3, 0, Math.PI * 2); ctx.scale(1/0.7, 1); ctx.fill(); // Texture ctx.strokeStyle = '#ffaa00'; ctx.lineWidth = 1; for (let i = -2; i <= 2; i++) { ctx.beginPath(); ctx.moveTo(i * CELL_SIZE * 0.08, -CELL_SIZE * 0.3); ctx.lineTo(i * CELL_SIZE * 0.08, CELL_SIZE * 0.3); ctx.stroke(); } // Feuilles ctx.fillStyle = fruit.stemColor; ctx.beginPath(); ctx.moveTo(0, -CELL_SIZE * 0.3); ctx.lineTo(-CELL_SIZE * 0.1, -CELL_SIZE * 0.4); ctx.lineTo(CELL_SIZE * 0.1, -CELL_SIZE * 0.4); ctx.closePath(); ctx.fill(); } } else if (this.type === BONUS_LUDO) { const size = CELL_SIZE * 0.45; ctx.fillStyle = '#ffd700'; ctx.strokeStyle = '#ffaa00'; ctx.lineWidth = 3; ctx.beginPath(); const spikes = 5; const outerRadius = size; const innerRadius = size * 0.4; for (let i = 0; i < spikes * 2; i++) { const angle = (i * Math.PI) / spikes; const radius = i % 2 === 0 ? outerRadius : innerRadius; const x = Math.cos(angle) * radius; const y = Math.sin(angle) * radius; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ffff00'; ctx.beginPath(); ctx.arc(0, 0, size * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#ffd700'; ctx.font = `bold ${size * 0.6}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('L', 0, 0); } else if (this.type === BONUS_SPEED) { // Étoile de vitesse ctx.fillStyle = '#00ffff'; ctx.strokeStyle = '#0088ff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, CELL_SIZE * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ffffff'; ctx.font = `bold ${CELL_SIZE * 0.4}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⚡', 0, 0); } else if (this.type === BONUS_SHIELD) { // Bouclier ctx.fillStyle = '#00ff00'; ctx.strokeStyle = '#008800'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, CELL_SIZE * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ffffff'; ctx.font = `bold ${CELL_SIZE * 0.4}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('🛡', 0, 0); } else if (this.type === BONUS_BOMB) { // Bombe ctx.fillStyle = '#ff0000'; ctx.strokeStyle = '#880000'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, CELL_SIZE * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ffffff'; ctx.font = `bold ${CELL_SIZE * 0.4}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('💣', 0, 0); } else if (this.type === BONUS_MULTIPLIER) { // Multiplicateur ctx.fillStyle = '#ff00ff'; ctx.strokeStyle = '#880088'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, CELL_SIZE * 0.3, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ffffff'; ctx.font = `bold ${CELL_SIZE * 0.4}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('✨', 0, 0); } ctx.restore(); } } let pacman = new Pacman(); let ghosts = []; function createGhosts() { ghosts = []; const ghostColors = ['#ff0000', '#ff00ff', '#00ffff', '#ffa500', '#00ff00', '#ffff00', '#ff00ff', '#00ffff']; const ghostTypes = [GHOST_NORMAL, GHOST_HUNTER, GHOST_PATROL, GHOST_FAST, GHOST_INVISIBLE]; // Nombre de fantômes selon le niveau let numGhosts = 4; if (level >= 3 && level < 5) numGhosts = 5; else if (level >= 5 && level < 10) numGhosts = 6; else if (level >= 10) numGhosts = 7; const positions = [ {x: 14, y: 11}, {x: 15, y: 11}, {x: 14, y: 12}, {x: 15, y: 12}, {x: 13, y: 11}, {x: 16, y: 11}, {x: 13, y: 12}, {x: 16, y: 12} ]; for (let i = 0; i < numGhosts; i++) { const pos = positions[i % positions.length]; let type = GHOST_NORMAL; // Types selon le niveau if (level >= 2 && i === 1) type = GHOST_HUNTER; if (level >= 3 && i === 2) type = GHOST_PATROL; if (level >= 4 && i === 3) type = GHOST_FAST; if (level >= 5 && i >= 4) { const rand = Math.random(); if (rand < 0.3) type = GHOST_INVISIBLE; else if (rand < 0.6) type = GHOST_FAST; else type = GHOST_HUNTER; } ghosts.push(new Ghost(pos.x, pos.y, ghostColors[i % ghostColors.length], type)); } for (let ghost of ghosts) { ghost.updateSpeed(); } } let bonuses = []; function countDots() { totalDots = 0; if (!maze || maze.length === 0) { console.error('countDots() - maze est vide ou undefined'); return; } for (let y = 0; y < ROWS; y++) { if (!maze[y]) { console.error(`countDots() - maze[${y}] est undefined`); continue; } for (let x = 0; x < COLS; x++) { if (maze[y][x] === DOT) { totalDots++; } } } // Mettre à jour l'affichage if (dotsRemainingElement) { dotsRemainingElement.textContent = totalDots; } } function drawMaze() { ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height); for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { const cellX = x * CELL_SIZE; const cellY = y * CELL_SIZE; if (maze[y][x] === WALL) { ctx.fillStyle = '#0000ff'; ctx.fillRect(cellX, cellY, CELL_SIZE, CELL_SIZE); ctx.strokeStyle = '#000080'; ctx.strokeRect(cellX, cellY, CELL_SIZE, CELL_SIZE); } else if (maze[y][x] === DOT) { ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(cellX + CELL_SIZE / 2, cellY + CELL_SIZE / 2, 2, 0, Math.PI * 2); ctx.fill(); } else if (maze[y][x] === ZONE_TELEPORT) { // Zone de téléportation ctx.fillStyle = 'rgba(255, 0, 255, 0.3)'; ctx.fillRect(cellX, cellY, CELL_SIZE, CELL_SIZE); ctx.strokeStyle = '#ff00ff'; ctx.lineWidth = 2; ctx.strokeRect(cellX, cellY, CELL_SIZE, CELL_SIZE); } else if (maze[y][x] === ZONE_BONUS) { // Zone bonus ctx.fillStyle = 'rgba(255, 215, 0, 0.3)'; ctx.fillRect(cellX, cellY, CELL_SIZE, CELL_SIZE); ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2; ctx.strokeRect(cellX, cellY, CELL_SIZE, CELL_SIZE); } else if (maze[y][x] === ZONE_DANGER) { // Zone danger ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'; ctx.fillRect(cellX, cellY, CELL_SIZE, CELL_SIZE); ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 2; ctx.strokeRect(cellX, cellY, CELL_SIZE, CELL_SIZE); } } } for (let bonus of bonuses) { bonus.update(); bonus.draw(); } } function checkCollisions() { if (!gameRunning || isPaused) return; for (let ghost of ghosts) { const distance = Math.sqrt( Math.pow(pacman.pixelX - ghost.pixelX, 2) + Math.pow(pacman.pixelY - ghost.pixelY, 2) ); if (distance < CELL_SIZE * 0.6) { if (ghost.isVulnerable) { // Manger le fantôme score += 200; scoreElement.textContent = score; ghost.isVulnerable = false; ghost.vulnerableTimer = 0; // Réinitialiser la position du fantôme ghost.x = ghost.startX; ghost.y = ghost.startY; ghost.pixelX = ghost.x * CELL_SIZE + CELL_SIZE / 2; ghost.pixelY = ghost.y * CELL_SIZE + CELL_SIZE / 2; ghost.direction = Math.floor(Math.random() * 4); } else { // Vérifier le bouclier if (shieldActive) { // Bouclier actif : repousser le fantôme sans perdre de vie shieldActive = false; shieldTimer = 0; if (shieldIndicator) shieldIndicator.style.display = 'none'; // Repousser le fantôme const dx = ghost.x - pacman.x; const dy = ghost.y - pacman.y; ghost.x = Math.max(0, Math.min(COLS - 1, ghost.x + Math.sign(dx) * 2)); ghost.y = Math.max(0, Math.min(ROWS - 1, ghost.y + Math.sign(dy) * 2)); ghost.pixelX = ghost.x * CELL_SIZE + CELL_SIZE / 2; ghost.pixelY = ghost.y * CELL_SIZE + CELL_SIZE / 2; } else { // Perdre une vie lives--; updateLivesDisplay(); if (lives <= 0) { gameRunning = false; statusElement.textContent = 'Game Over !'; showGameOver(); // Le score sera sauvegardé quand l'utilisateur clique sur "Sauvegarder" } else { restartCurrentLevel(); } } } } } } function gameLoop() { if (isChangingLevel || !gameRunning) { if (isChangingLevel) { console.log('gameLoop() - Changement de niveau en cours, arrêt'); } return; } drawMaze(); // Gestion de la pause if (isPaused) { ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#ffd700'; ctx.font = 'bold 48px "Press Start 2P"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('PAUSE', canvas.width/2, canvas.height/2); ctx.font = 'bold 16px "Press Start 2P"'; ctx.fillText('Appuyez sur ESPACE ou ECHAP', canvas.width/2, canvas.height/2 + 40); requestAnimationFrame(gameLoop); return; } // Gestion des power-ups if (speedBoostActive) { speedBoostTimer--; if (speedBoostTimer <= 0) { speedBoostActive = false; pacman.speed = pacman.baseSpeed; if (speedBoostIndicator) speedBoostIndicator.style.display = 'none'; } else if (speedBoostIndicator) { const seconds = Math.ceil(speedBoostTimer / 60); speedBoostIndicator.textContent = `⚡ Vitesse (${seconds}s)`; } } if (shieldActive) { shieldTimer--; if (shieldTimer <= 0) { shieldActive = false; if (shieldIndicator) shieldIndicator.style.display = 'none'; } else if (shieldIndicator) { const seconds = Math.ceil(shieldTimer / 60); shieldIndicator.textContent = `🛡 Bouclier (${seconds}s)`; } } if (scoreMultiplierActive) { scoreMultiplierTimer--; if (scoreMultiplierTimer <= 0) { scoreMultiplierActive = false; scoreMultiplierValue = 1; if (multiplierIndicator) multiplierIndicator.style.display = 'none'; } else if (multiplierIndicator && multiplierValueElement) { const seconds = Math.ceil(scoreMultiplierTimer / 60); multiplierIndicator.textContent = `✨ x${scoreMultiplierValue} (${seconds}s)`; } } // Mode Frenzy if (!frenzyModeActive) { frenzyCooldown++; if (frenzyCooldown >= FRENZY_INTERVAL) { frenzyModeActive = true; frenzyTimer = FRENZY_DURATION; frenzyCooldown = 0; // Rendre tous les fantômes vulnérables for (let ghost of ghosts) { ghost.isVulnerable = true; ghost.vulnerableTimer = FRENZY_DURATION; } if (frenzyIndicator) frenzyIndicator.style.display = 'block'; if (pursuitIndicator && !isFullscreen()) pursuitIndicator.style.display = 'block'; } } else { frenzyTimer--; if (frenzyTimer <= 0) { frenzyModeActive = false; for (let ghost of ghosts) { ghost.isVulnerable = false; ghost.vulnerableTimer = 0; } if (frenzyIndicator) frenzyIndicator.style.display = 'none'; if (pursuitIndicator) pursuitIndicator.style.display = 'none'; } } // Combo timeout if (comboCount > 0 && Date.now() - lastDotTime > COMBO_TIMEOUT) { comboCount = 0; comboMultiplier = 1; if (comboDisplay) comboDisplay.style.display = 'none'; } pacman.update(); pacman.draw(); // Mettre à jour l'indicateur de poursuite (masquer en mode plein écran) if (isFullscreen() && pursuitIndicator) { pursuitIndicator.style.display = 'none'; } else if (pursuitIndicator && (cherryEatenTimer > 0 || frenzyModeActive) && !isFullscreen()) { const timer = frenzyModeActive ? frenzyTimer : cherryEatenTimer; const seconds = Math.ceil(timer / 60); if (pursuitTimerElement) { pursuitTimerElement.textContent = seconds; } pursuitIndicator.style.display = 'block'; } else if (pursuitIndicator && !frenzyModeActive) { pursuitIndicator.style.display = 'none'; } for (let ghost of ghosts) { ghost.update(); ghost.draw(); } checkCollisions(); if (gameRunning && !isChangingLevel) { requestAnimationFrame(gameLoop); } else { console.log('gameLoop() - Arrêt de la boucle, gameRunning:', gameRunning, 'isChangingLevel:', isChangingLevel); } } function updateLivesDisplay() { const hearts = livesElement.querySelectorAll('.heart'); hearts.forEach((heart, index) => { if (index < lives) { heart.style.opacity = '1'; } else { heart.style.opacity = '0.3'; } }); } function restartCurrentLevel() { if (isChangingLevel) return; console.log('Redémarrage du niveau actuel'); isChangingLevel = true; gameRunning = false; requestAnimationFrame(() => { const currentLevel = level; const mazeIndex = (currentLevel - 1) % mazeVariants.length; const currentMaze = mazeVariants[mazeIndex]; maze = []; for (let i = 0; i < currentMaze.length; i++) { maze[i] = [...currentMaze[i]]; } fillEmptySpaces(); randomizeMaze(); countDots(); bonuses = []; placeBonuses(); pacman = new Pacman(); pacman.speed = pacman.baseSpeed * (1 + (level - 1) * 0.05); createGhosts(); for (let ghost of ghosts) { ghost.isVulnerable = false; ghost.vulnerableTimer = 0; } cherriesEaten = 0; cherryEatenRecently = false; cherryEatenTimer = 0; if (pursuitIndicator) { pursuitIndicator.style.display = 'none'; } statusElement.textContent = `Niveau ${level} - Recommencement !`; statusElement.style.color = '#ff6b6b'; drawMaze(); pacman.draw(); for (let ghost of ghosts) { ghost.draw(); } setTimeout(() => { isChangingLevel = false; gameRunning = true; statusElement.textContent = 'En jeu'; statusElement.style.color = '#ffd700'; gameLoop(); }, 2000); }); } function nextLevel() { console.log('=== nextLevel() appelée ==='); console.log('gameRunning:', gameRunning, 'isChangingLevel:', isChangingLevel); if (!gameRunning || isChangingLevel) { console.log('nextLevel() annulée - gameRunning:', gameRunning, 'isChangingLevel:', isChangingLevel); return; } console.log('Début du changement de niveau'); isChangingLevel = true; const wasRunning = gameRunning; gameRunning = false; console.log('gameRunning mis à false, wasRunning:', wasRunning); requestAnimationFrame(() => { console.log('Dans requestAnimationFrame, changement du niveau'); level++; console.log('Nouveau niveau:', level); levelElement.textContent = level; cherriesEaten = 0; const mazeIndex = (level - 1) % mazeVariants.length; console.log('Index du labyrinthe:', mazeIndex); currentMazeIndex = mazeIndex; const newMaze = mazeVariants[mazeIndex]; console.log('Création du nouveau labyrinthe'); console.log('newMaze.length:', newMaze.length, 'ROWS:', ROWS); console.log('newMaze[0]?.length:', newMaze[0]?.length, 'COLS:', COLS); if (!newMaze || newMaze.length !== ROWS) { console.error('Erreur: Le nouveau labyrinthe a une taille incorrecte:', newMaze?.length); isChangingLevel = false; gameRunning = wasRunning; return; } maze = []; for (let i = 0; i < newMaze.length; i++) { if (!newMaze[i] || newMaze[i].length !== COLS) { console.error(`Erreur: Ligne ${i} du labyrinthe a une taille incorrecte:`, newMaze[i]?.length); } maze[i] = [...newMaze[i]]; } console.log('Labyrinthe copié, maze.length:', maze.length); fillEmptySpaces(); randomizeMaze(); countDots(); console.log('Total dots:', totalDots); bonuses = []; console.log('Réinitialisation de Pacman et des fantômes'); pacman = new Pacman(); pacman.speed = pacman.baseSpeed * (1 + (level - 1) * 0.05); createGhosts(); for (let ghost of ghosts) { ghost.isVulnerable = false; ghost.vulnerableTimer = 0; } cherryEatenTimer = 0; if (pursuitIndicator) { pursuitIndicator.style.display = 'none'; } placeBonuses(); console.log('Bonus placés, nombre:', bonuses.length); statusElement.textContent = `Niveau ${level} - Labyrinthe ${mazeIndex + 1} !`; statusElement.style.color = '#00ff00'; console.log('Redessin du labyrinthe'); drawMaze(); pacman.draw(); for (let ghost of ghosts) { ghost.draw(); } console.log('Attente de 2 secondes avant redémarrage'); setTimeout(() => { console.log('Redémarrage de la boucle de jeu'); isChangingLevel = false; gameRunning = wasRunning; console.log('isChangingLevel:', isChangingLevel, 'gameRunning:', gameRunning); if (gameRunning) { statusElement.textContent = 'En jeu'; statusElement.style.color = '#ffd700'; console.log('Appel de gameLoop() pour redémarrer'); gameLoop(); } }, 2000); }); } function fillEmptySpaces() { console.log('Remplissage de tous les espaces vides avec des chemins'); // Première passe : remplir tous les empty for (let y = 1; y < ROWS - 1; y++) { for (let x = 1; x < COLS - 1; x++) { if (maze[y][x] === EMPTY) { maze[y][x] = DOT; } } } // Détecter et remplir les grandes zones vides for (let iteration = 0; iteration < 6; iteration++) { for (let y = 1; y < ROWS - 1; y++) { for (let x = 1; x < COLS - 1; x++) { if (maze[y][x] === WALL) { const neighbors = [ maze[y-1][x], maze[y+1][x], maze[y][x-1], maze[y][x+1] ]; const emptyCount = neighbors.filter(c => c === EMPTY).length; const dotCount = neighbors.filter(c => c === DOT).length; const wallCount = neighbors.filter(c => c === WALL).length; // Si entouré d'espaces vides ou de beaucoup de chemins, transformer en chemin if (emptyCount >= 1) { maze[y][x] = DOT; } else if (wallCount <= 1 && dotCount >= 2) { if (Math.random() < 0.9) { maze[y][x] = DOT; } } else if (wallCount === 0) { maze[y][x] = DOT; } } else if (maze[y][x] === EMPTY) { maze[y][x] = DOT; } } } // Détecter les grandes zones vides (empty) et les remplir for (let y = 2; y < ROWS - 2; y++) { for (let x = 2; x < COLS - 2; x++) { if (maze[y][x] === EMPTY) { let emptyArea = 0; const visited = new Set(); const stack = [[x, y]]; while (stack.length > 0 && emptyArea < 20) { const [cx, cy] = stack.pop(); const key = `${cy},${cx}`; if (visited.has(key)) continue; visited.add(key); if (maze[cy][cx] === EMPTY) { emptyArea++; if (cy > 1) stack.push([cx, cy-1]); if (cy < ROWS-2) stack.push([cx, cy+1]); if (cx > 1) stack.push([cx-1, cy]); if (cx < COLS-2) stack.push([cx+1, cy]); } } if (emptyArea >= 5) { for (const key of visited) { const [cy, cx] = key.split(',').map(Number); if (maze[cy][cx] === EMPTY) { maze[cy][cx] = DOT; } } } } } } } // Créer une petite cavité au centre seulement const centerX = Math.floor(COLS / 2); const centerY = Math.floor(ROWS / 2); if (maze[centerY] && maze[centerY][centerX] !== WALL) { maze[centerY][centerX] = EMPTY; } console.log('Tous les grands espaces réduits, chemins denses créés'); } function randomizeMaze() { const modificationRate = 0.05; const changes = Math.floor(ROWS * COLS * modificationRate); console.log('Modification aléatoire du labyrinthe,', changes, 'changements'); const centerX = Math.floor(COLS / 2); const centerY = Math.floor(ROWS / 2); for (let i = 0; i < changes; i++) { const x = Math.floor(Math.random() * COLS); const y = Math.floor(Math.random() * ROWS); if (x === 0 || x === COLS - 1 || y === 0 || y === ROWS - 1) { continue; } const distX = Math.abs(x - centerX); const distY = Math.abs(y - centerY); if (distX <= 1 && distY <= 1) { continue; } const currentCell = maze[y][x]; if (currentCell === WALL) { if (Math.random() < 0.8) { maze[y][x] = DOT; } } else if (currentCell === EMPTY) { maze[y][x] = DOT; } } // Remplir tous les empty restants for (let y = 1; y < ROWS - 1; y++) { for (let x = 1; x < COLS - 1; x++) { const distX = Math.abs(x - centerX); const distY = Math.abs(y - centerY); if (distX > 1 || distY > 1) { if (maze[y][x] === EMPTY) { maze[y][x] = DOT; } } } } // Transformer les murs isolés en chemins for (let iteration = 0; iteration < 3; iteration++) { for (let y = 1; y < ROWS - 1; y++) { for (let x = 1; x < COLS - 1; x++) { const distX = Math.abs(x - centerX); const distY = Math.abs(y - centerY); if (distX <= 1 && distY <= 1) { continue; } if (maze[y][x] === WALL) { const neighbors = [ maze[y-1][x], maze[y+1][x], maze[y][x-1], maze[y][x+1] ]; const wallCount = neighbors.filter(c => c === WALL).length; const dotCount = neighbors.filter(c => c === DOT).length; if (wallCount <= 1 && dotCount >= 2 && Math.random() < 0.85) { maze[y][x] = DOT; } else if (wallCount === 0) { maze[y][x] = DOT; } } } } } console.log('Labyrinthe modifié avec chemins denses, grands espaces réduits'); } function placeBonuses() { bonuses = []; const bonusPositions = [ {x: 1, y: 1, type: BONUS_CHERRY}, {x: 28, y: 1, type: BONUS_CHERRY}, {x: 1, y: 28, type: BONUS_CHERRY}, {x: 28, y: 28, type: BONUS_CHERRY}, {x: 14, y: 14, type: BONUS_LUDO}, {x: 15, y: 14, type: BONUS_LUDO} ]; // Ajouter des power-ups selon le niveau if (level >= 2) { bonusPositions.push({x: 5, y: 5, type: BONUS_SPEED}); bonusPositions.push({x: 25, y: 5, type: BONUS_SHIELD}); } if (level >= 3) { bonusPositions.push({x: 5, y: 25, type: BONUS_BOMB}); bonusPositions.push({x: 25, y: 25, type: BONUS_MULTIPLIER}); } // Zones spéciales selon le niveau if (level >= 4) { // Zones de téléportation const teleportZones = [ {x: 7, y: 7}, {x: 23, y: 7}, {x: 7, y: 23}, {x: 23, y: 23} ]; for (let zone of teleportZones) { if (maze[zone.y] && maze[zone.y][zone.x] === EMPTY) { maze[zone.y][zone.x] = ZONE_TELEPORT; specialZones.push({x: zone.x, y: zone.y, type: ZONE_TELEPORT}); } } } if (level >= 5) { // Zones bonus const bonusZones = [ {x: 10, y: 10}, {x: 20, y: 10}, {x: 10, y: 20}, {x: 20, y: 20} ]; for (let zone of bonusZones) { if (maze[zone.y] && maze[zone.y][zone.x] === EMPTY) { maze[zone.y][zone.x] = ZONE_BONUS; specialZones.push({x: zone.x, y: zone.y, type: ZONE_BONUS}); } } } for (let pos of bonusPositions) { if (maze[pos.y] && (maze[pos.y][pos.x] === EMPTY || maze[pos.y][pos.x] === DOT)) { maze[pos.y][pos.x] = pos.type; bonuses.push(new Bonus(pos.x, pos.y, pos.type)); } } } // === REDIMENSIONNEMENT ADAPTATIF DU CANVAS === function resizeCanvas() { if (!gameWrapper || gameWrapper.style.display === 'none') { return; // Ne pas redimensionner si le jeu n'est pas visible } const container = document.querySelector('.container'); const gameHeader = document.querySelector('.game-header'); const gameInfo = document.querySelector('.game-info'); const userInput = document.querySelector('.user-input-section'); const powerUps = document.querySelector('.power-ups-display'); const frenzyIndicator = document.querySelector('.frenzy-indicator'); const pursuitIndicator = document.querySelector('.pursuit-indicator'); const instructions = document.querySelector('.instructions'); const leaderboard = document.querySelector('.game-leaderboard'); if (!container || !canvas) return; // Calculer l'espace disponible en hauteur const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; // Hauteur des éléments autour du canvas let usedHeight = 0; if (gameHeader) usedHeight += gameHeader.offsetHeight + 20; if (userInput && userInput.offsetHeight > 0) usedHeight += userInput.offsetHeight + 10; if (gameInfo) usedHeight += gameInfo.offsetHeight + 20; if (powerUps && powerUps.offsetHeight > 0) usedHeight += powerUps.offsetHeight + 10; if (frenzyIndicator && frenzyIndicator.offsetHeight > 0 && frenzyIndicator.style.display !== 'none') { usedHeight += frenzyIndicator.offsetHeight + 10; } if (pursuitIndicator && pursuitIndicator.offsetHeight > 0 && pursuitIndicator.style.display !== 'none') { usedHeight += pursuitIndicator.offsetHeight + 10; } if (instructions) usedHeight += instructions.offsetHeight + 20; // Padding du container (30px top + 30px bottom) usedHeight += 60; // Marges et espacement usedHeight += 40; // Largeur disponible (en tenant compte du leaderboard à droite si visible) let usedWidth = 0; const containerPadding = 60; // 30px left + 30px right const gaps = 60; // Espacement entre éléments if (leaderboard && window.innerWidth > 1200) { const leaderboardRect = leaderboard.getBoundingClientRect(); if (leaderboardRect.width > 0) { usedWidth += leaderboardRect.width + 30; // + gap } } usedWidth += containerPadding + gaps; const availableHeight = windowHeight - usedHeight; const availableWidth = windowWidth - usedWidth; // La taille du canvas sera le minimum entre hauteur et largeur disponible // Utilise 95% de l'espace disponible avec une taille minimale de 400px const maxSize = Math.min(availableHeight, availableWidth); const minSize = 400; const canvasDisplaySize = Math.max(minSize, Math.min(maxSize * 0.95, 1200)); // Appliquer la taille au canvas via CSS canvas.style.width = canvasDisplaySize + 'px'; canvas.style.height = canvasDisplaySize + 'px'; canvas.style.maxWidth = '100%'; canvas.style.maxHeight = 'calc(100vh - ' + usedHeight + 'px)'; } function initGame() { resetUsernameInput(); // Réinitialiser le champ nom currentMazeIndex = 0; originalMaze = mazeVariants[0]; maze = originalMaze.map(row => [...row]); countDots(); score = 0; level = 1; lives = 3; cherriesEaten = 0; isPaused = false; // Réinitialiser les systèmes comboCount = 0; comboMultiplier = 1; lastDotTime = 0; speedBoostActive = false; speedBoostTimer = 0; shieldActive = false; shieldTimer = 0; scoreMultiplierActive = false; scoreMultiplierTimer = 0; scoreMultiplierValue = 1; frenzyModeActive = false; frenzyTimer = 0; frenzyCooldown = 0; specialZones = []; temporaryWalls = []; slowZones = []; traps = []; scoreElement.textContent = score; levelElement.textContent = level; updateLivesDisplay(); gameRunning = true; statusElement.textContent = 'En jeu'; statusElement.style.color = '#ffd700'; restartBtn.style.display = 'none'; hideGameOver(); if (pursuitIndicator) { pursuitIndicator.style.display = 'none'; } if (comboDisplay) comboDisplay.style.display = 'none'; if (frenzyIndicator) frenzyIndicator.style.display = 'none'; if (speedBoostIndicator) speedBoostIndicator.style.display = 'none'; if (shieldIndicator) shieldIndicator.style.display = 'none'; if (multiplierIndicator) multiplierIndicator.style.display = 'none'; pacman = new Pacman(); pacman.speed = pacman.baseSpeed * (1 + (level - 1) * 0.05); createGhosts(); for (let ghost of ghosts) { ghost.isVulnerable = false; ghost.vulnerableTimer = 0; } if (pursuitIndicator) { pursuitIndicator.style.display = 'none'; } placeBonuses(); countDots(); updateGameLeaderboard(); // Redimensionner le canvas après l'initialisation setTimeout(resizeCanvas, 100); gameLoop(); } document.addEventListener('keydown', (e) => { // Système de pause if (e.key === 'Escape' || e.key === ' ') { if (gameRunning && !isChangingLevel) { isPaused = !isPaused; if (isPaused) { statusElement.textContent = 'PAUSE'; statusElement.style.color = '#ffd700'; } else { statusElement.textContent = 'En jeu'; statusElement.style.color = '#ffd700'; } e.preventDefault(); return; } } if (!gameRunning || isPaused) return; switch(e.key) { case 'ArrowUp': pacman.nextDirection = 0; e.preventDefault(); break; case 'ArrowRight': pacman.nextDirection = 1; e.preventDefault(); break; case 'ArrowDown': pacman.nextDirection = 2; e.preventDefault(); break; case 'ArrowLeft': pacman.nextDirection = 3; e.preventDefault(); break; } }); function getScores() { const scoresJson = localStorage.getItem('pacmanScores'); return scoresJson ? JSON.parse(scoresJson) : []; } function saveScore(usernameForScore = null) { let username; // Priorité au nom passé en paramètre, puis au nom du Game Over, puis au nom confirmé, sinon "Anonyme" if (usernameForScore) { username = usernameForScore.trim() || 'Anonyme'; } else if (gameOverUsername) { username = gameOverUsername.trim() || 'Anonyme'; } else if (!usernameConfirmed) { username = 'Anonyme'; } else { username = usernameInput.value.trim() || 'Anonyme'; } if (score > 0) { const scores = getScores(); scores.push({ username: username, score: score, level: level, date: new Date().toISOString() }); scores.sort((a, b) => b.score - a.score); const topScores = scores.slice(0, 10); localStorage.setItem('pacmanScores', JSON.stringify(topScores)); updateLeaderboard(); updateGameLeaderboard(); updateBestScore(); // Mettre à jour le meilleur score dans le menu } } function updateLeaderboard() { const scores = getScores(); leaderboardElement.innerHTML = ''; if (scores.length === 0) { leaderboardElement.innerHTML = '
Aucun score enregistré
'; return; } scores.forEach((entry, index) => { const item = document.createElement('div'); item.className = 'leaderboard-item' + (index < 3 ? ' top' : ''); const rank = document.createElement('div'); rank.className = 'leaderboard-rank'; rank.textContent = (index + 1) + '.'; const name = document.createElement('div'); name.className = 'leaderboard-name'; name.textContent = entry.username; const levelDiv = document.createElement('div'); levelDiv.className = 'leaderboard-level'; levelDiv.textContent = 'Niv. ' + (entry.level || 1); const scoreDiv = document.createElement('div'); scoreDiv.className = 'leaderboard-score'; scoreDiv.textContent = entry.score; item.appendChild(rank); item.appendChild(name); item.appendChild(levelDiv); item.appendChild(scoreDiv); leaderboardElement.appendChild(item); }); } function updateGameLeaderboard() { if (!gameLeaderboardContent) return; const scores = getScores(); gameLeaderboardContent.innerHTML = ''; if (scores.length === 0) { gameLeaderboardContent.innerHTML = '
Aucun score
'; return; } // Afficher seulement les 10 premiers const topScores = scores.slice(0, 10); topScores.forEach((entry, index) => { const item = document.createElement('div'); item.className = 'leaderboard-item' + (index < 3 ? ' top' : ''); const rank = document.createElement('div'); rank.className = 'leaderboard-rank'; rank.textContent = (index + 1) + '.'; const name = document.createElement('div'); name.className = 'leaderboard-name'; name.textContent = entry.username || 'Anonyme'; const levelDiv = document.createElement('div'); levelDiv.className = 'leaderboard-level'; levelDiv.textContent = 'N' + (entry.level || 1); const scoreDiv = document.createElement('div'); scoreDiv.className = 'leaderboard-score'; scoreDiv.textContent = entry.score; item.appendChild(rank); item.appendChild(name); item.appendChild(levelDiv); item.appendChild(scoreDiv); gameLeaderboardContent.appendChild(item); }); } restartBtn.addEventListener('click', () => { initGame(); }); // === GESTION DU NOM D'UTILISATEUR === function confirmUsername() { const username = usernameInput.value.trim(); if (username.length === 0) { alert('Veuillez entrer un nom d\'utilisateur !'); usernameInput.focus(); return; } usernameConfirmed = true; usernameInput.disabled = true; confirmUsernameBtn.disabled = true; confirmUsernameBtn.textContent = '✓ Confirmé'; // Supprimer les anciens messages de confirmation s'ils existent const existingMsg = usernameInput.parentElement.querySelector('.username-confirmed'); if (existingMsg) { existingMsg.remove(); } // Afficher un message de confirmation const confirmationMsg = document.createElement('div'); confirmationMsg.className = 'username-confirmed'; confirmationMsg.textContent = `Bienvenue, ${username} !`; usernameInput.parentElement.appendChild(confirmationMsg); // Masquer le message après 3 secondes setTimeout(() => { confirmationMsg.style.opacity = '0'; setTimeout(() => confirmationMsg.remove(), 300); }, 3000); } function resetUsernameInput() { usernameConfirmed = false; usernameInput.disabled = false; usernameInput.value = ''; confirmUsernameBtn.disabled = false; confirmUsernameBtn.textContent = '✓ Confirmer'; // Supprimer les messages de confirmation existants const existingMsg = usernameInput.parentElement.querySelector('.username-confirmed'); if (existingMsg) { existingMsg.remove(); } // Focus sur le champ après un court délai setTimeout(() => usernameInput.focus(), 100); } // Confirmation avec le bouton confirmUsernameBtn.addEventListener('click', confirmUsername); // Confirmation avec la touche Entrée usernameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !usernameConfirmed) { confirmUsername(); } }); // === GAME OVER OVERLAY === function showGameOver() { finalScoreElement.textContent = score; finalLevelElement.textContent = level; // Réinitialiser le champ de nom et cacher le message if (gameOverUsernameInput) { gameOverUsernameInput.value = usernameInput.value.trim() || ''; gameOverUsernameInput.disabled = false; gameOverUsernameInput.focus(); } if (saveScoreBtn) { saveScoreBtn.disabled = false; } if (scoreSavedMsg) { scoreSavedMsg.style.display = 'none'; } gameOverUsername = null; // Réinitialiser gameOverlay.classList.add('active'); restartBtn.style.display = 'block'; } function hideGameOver() { gameOverlay.classList.remove('active'); } // Sauvegarder le score avec le nom du Game Over if (saveScoreBtn && gameOverUsernameInput) { saveScoreBtn.addEventListener('click', () => { const username = gameOverUsernameInput.value.trim(); if (username.length === 0) { alert('Veuillez entrer un nom d\'utilisateur !'); gameOverUsernameInput.focus(); return; } gameOverUsername = username; saveScore(username); // Afficher le message de confirmation if (scoreSavedMsg) { scoreSavedMsg.style.display = 'block'; } // Désactiver le champ et le bouton gameOverUsernameInput.disabled = true; saveScoreBtn.disabled = true; }); // Permettre de sauvegarder avec la touche Entrée gameOverUsernameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !saveScoreBtn.disabled) { saveScoreBtn.click(); } }); } overlayRestartBtn.addEventListener('click', () => { hideGameOver(); initGame(); }); // === CONTROLES TACTILES MOBILE === document.querySelectorAll('.ctrl-btn').forEach(btn => { // Touch events btn.addEventListener('touchstart', (e) => { e.preventDefault(); const dir = parseInt(btn.dataset.dir); if (!isNaN(dir) && gameRunning) { pacman.nextDirection = dir; } }); // Click events (for testing on desktop) btn.addEventListener('click', () => { const dir = parseInt(btn.dataset.dir); if (!isNaN(dir) && gameRunning) { pacman.nextDirection = dir; } }); }); // === ANIMATION DU SCORE === let lastScore = 0; function animateScore() { if (score !== lastScore) { const scoreParent = scoreElement.parentElement; scoreParent.classList.remove('updated'); void scoreParent.offsetWidth; // Force reflow scoreParent.classList.add('updated'); lastScore = score; } requestAnimationFrame(animateScore); } animateScore(); // === FANTOMES ANIMES EN ARRIERE-PLAN DU MENU === const menuCanvas = document.getElementById('menuBackgroundCanvas'); const menuCtx = menuCanvas.getContext('2d'); // Ajuster la taille du canvas function resizeMenuCanvas() { menuCanvas.width = window.innerWidth; menuCanvas.height = window.innerHeight; } resizeMenuCanvas(); window.addEventListener('resize', resizeMenuCanvas); // Classe pour les fantômes du menu class MenuGhost { constructor() { this.x = Math.random() * menuCanvas.width; this.y = Math.random() * menuCanvas.height; this.speedX = (Math.random() - 0.5) * 2; this.speedY = (Math.random() - 0.5) * 2; this.size = 30 + Math.random() * 20; this.color = ['#ff0000', '#ff00ff', '#00ffff', '#ffa500'][Math.floor(Math.random() * 4)]; this.animation = Math.random() * Math.PI * 2; } update() { this.x += this.speedX; this.y += this.speedY; this.animation += 0.05; // Rebondir sur les bords if (this.x < 0 || this.x > menuCanvas.width) { this.speedX *= -1; } if (this.y < 0 || this.y > menuCanvas.height) { this.speedY *= -1; } // Garder dans les limites this.x = Math.max(0, Math.min(menuCanvas.width, this.x)); this.y = Math.max(0, Math.min(menuCanvas.height, this.y)); } draw() { menuCtx.save(); menuCtx.translate(this.x, this.y); const size = this.size; const waveOffset = Math.sin(this.animation) * 2; // Corps du fantôme menuCtx.fillStyle = this.color; menuCtx.strokeStyle = '#000000'; menuCtx.lineWidth = 2; // Tête (demi-cercle) menuCtx.beginPath(); menuCtx.arc(0, -size * 0.3, size * 0.5, Math.PI, 0, false); menuCtx.fill(); menuCtx.stroke(); // Corps (rectangle) menuCtx.fillRect(-size * 0.5, -size * 0.3, size * 1.0, size * 0.7); menuCtx.strokeRect(-size * 0.5, -size * 0.3, size * 1.0, size * 0.7); // Jambes ondulées const waveHeight = size * 0.15; const waveWidth = size * 0.2; menuCtx.beginPath(); menuCtx.moveTo(-size * 0.5, size * 0.4); for (let i = 0; i < 5; i++) { const x = -size * 0.5 + i * waveWidth; const y = size * 0.4 + (i % 2 === 0 ? 0 : waveHeight) + waveOffset; menuCtx.lineTo(x, y); } menuCtx.lineTo(size * 0.5, size * 0.4); menuCtx.lineTo(size * 0.5, size * 0.7); menuCtx.lineTo(-size * 0.5, size * 0.7); menuCtx.closePath(); menuCtx.fill(); menuCtx.stroke(); // Yeux menuCtx.fillStyle = '#ffffff'; menuCtx.beginPath(); menuCtx.arc(-size * 0.2, -size * 0.1, size * 0.12, 0, Math.PI * 2); menuCtx.arc(size * 0.2, -size * 0.1, size * 0.12, 0, Math.PI * 2); menuCtx.fill(); menuCtx.fillStyle = '#000000'; menuCtx.beginPath(); menuCtx.arc(-size * 0.2, -size * 0.1, size * 0.06, 0, Math.PI * 2); menuCtx.arc(size * 0.2, -size * 0.1, size * 0.06, 0, Math.PI * 2); menuCtx.fill(); menuCtx.restore(); } } // Créer plusieurs fantômes const menuGhosts = []; const numGhosts = 6; for (let i = 0; i < numGhosts; i++) { menuGhosts.push(new MenuGhost()); } // Animation du menu let menuAnimationRunning = false; let menuAnimationFrame = null; function animateMenu() { if (!menuAnimationRunning) return; // Effacer le canvas avec un effet de traînée menuCtx.fillStyle = 'rgba(10, 10, 10, 0.1)'; menuCtx.fillRect(0, 0, menuCanvas.width, menuCanvas.height); // Mettre à jour et dessiner les fantômes menuGhosts.forEach(ghost => { ghost.update(); ghost.draw(); }); menuAnimationFrame = requestAnimationFrame(animateMenu); } // Démarrer l'animation seulement si le menu est visible function startMenuAnimation() { if (!menuAnimationRunning && mainMenu && mainMenu.style.display !== 'none') { menuAnimationRunning = true; animateMenu(); } } function stopMenuAnimation() { menuAnimationRunning = false; if (menuAnimationFrame) { cancelAnimationFrame(menuAnimationFrame); } } // Démarrer l'animation au chargement si le menu est visible // Attendre que le DOM soit complètement chargé if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { if (mainMenu && mainMenu.style.display !== 'none') { startMenuAnimation(); } }); } else { // DOM déjà chargé if (mainMenu && mainMenu.style.display !== 'none') { startMenuAnimation(); } } // Observer les changements de visibilité du menu if (mainMenu) { const menuObserver = new MutationObserver(() => { if (mainMenu.style.display !== 'none') { if (!menuAnimationRunning) { startMenuAnimation(); } } else { stopMenuAnimation(); } }); menuObserver.observe(mainMenu, { attributes: true, attributeFilter: ['style'] }); } // === GESTION DU MENU === // Afficher le jeu playBtn.addEventListener('click', () => { mainMenu.style.display = 'none'; gameWrapper.style.display = 'block'; resetUsernameInput(); // Toujours initialiser le jeu quand on clique sur JOUER gameRunning = false; // S'assurer que le jeu est arrêté // Attendre un peu pour que le DOM soit rendu avant de redimensionner setTimeout(() => { initGame(); resizeCanvas(); }, 50); }); // Retour au menu depuis le jeu backToMenuBtn.addEventListener('click', () => { if (confirm('Voulez-vous vraiment retourner au menu ? Votre partie en cours sera perdue.')) { gameWrapper.style.display = 'none'; mainMenu.style.display = 'flex'; gameRunning = false; updateBestScore(); // Mettre à jour le meilleur score startMenuAnimation(); // Redémarrer l'animation des fantômes } }); // Afficher le classement depuis le menu scoresBtn.addEventListener('click', () => { updateMenuLeaderboard(); leaderboardModal.style.display = 'flex'; }); // Ouvrir la modal des règles if (rulesBtn) { rulesBtn.addEventListener('click', () => { rulesModal.style.display = 'flex'; }); } // Fermer le modal de classement closeModalBtn.addEventListener('click', () => { leaderboardModal.style.display = 'none'; }); // Fermer la modal des règles if (closeRulesBtn) { closeRulesBtn.addEventListener('click', () => { rulesModal.style.display = 'none'; }); } // Fermer en cliquant en dehors de la modal des règles if (rulesModal) { rulesModal.addEventListener('click', (e) => { if (e.target === rulesModal) { rulesModal.style.display = 'none'; } }); } // === GESTION DU MODAL DE PERSONNALISATION === let selectedColor = playerColor; let selectedShape = playerShape; // Ouvrir le modal de personnalisation if (customizeBtn) { customizeBtn.addEventListener('click', () => { if (customizeModal) { customizeModal.style.display = 'flex'; selectedColor = playerColor; selectedShape = playerShape; updateColorSelection(); updateShapeSelection(); updatePlayerPreview(); startPreviewAnimation(); } }); } // Fermer le modal de personnalisation if (closeCustomizeBtn) { closeCustomizeBtn.addEventListener('click', () => { if (customizeModal) { customizeModal.style.display = 'none'; stopPreviewAnimation(); } }); } // Fermer en cliquant en dehors du modal if (customizeModal) { customizeModal.addEventListener('click', (e) => { if (e.target === customizeModal) { customizeModal.style.display = 'none'; stopPreviewAnimation(); } }); } // Gérer la sélection de couleur if (colorOptions) { colorOptions.forEach(option => { option.addEventListener('click', () => { selectedColor = option.getAttribute('data-color'); updateColorSelection(); updatePlayerPreview(); }); }); } // Gérer la sélection de forme if (shapeOptions) { shapeOptions.forEach(option => { option.addEventListener('click', () => { selectedShape = option.getAttribute('data-shape'); updateShapeSelection(); updatePlayerPreview(); }); }); } // Mettre à jour la sélection visuelle function updateColorSelection() { if (colorOptions) { colorOptions.forEach(option => { if (option.getAttribute('data-color') === selectedColor) { option.classList.add('active'); } else { option.classList.remove('active'); } }); } } function updateShapeSelection() { if (shapeOptions) { shapeOptions.forEach(option => { if (option.getAttribute('data-shape') === selectedShape) { option.classList.add('active'); } else { option.classList.remove('active'); } }); } } // Prévisualisation du joueur let previewAnimationId = null; function startPreviewAnimation() { if (!playerPreviewCanvas) return; let angle = 0; let mouthOpen = true; function animate() { if (!playerPreviewCanvas) return; const ctx = playerPreviewCanvas.getContext('2d'); ctx.clearRect(0, 0, playerPreviewCanvas.width, playerPreviewCanvas.height); // Fond ctx.fillStyle = '#000'; ctx.fillRect(0, 0, playerPreviewCanvas.width, playerPreviewCanvas.height); // Dessiner le joueur au centre ctx.save(); ctx.translate(playerPreviewCanvas.width / 2, playerPreviewCanvas.height / 2); // Couleur if (selectedColor === 'rainbow') { angle += 0.05; const hue = (angle * 180 / Math.PI) % 360; ctx.fillStyle = `hsl(${hue}, 100%, 50%)`; } else { const colorMap = { 'yellow': '#ffd700', 'red': '#ff4444', 'blue': '#4444ff', 'green': '#44ff44', 'purple': '#aa44ff', 'orange': '#ff8844', 'pink': '#ff44aa' }; ctx.fillStyle = colorMap[selectedColor] || '#ffd700'; } // Animation de la bouche if (Math.floor(angle * 5) % 2 === 0) { mouthOpen = !mouthOpen; } const size = 40; ctx.beginPath(); if (selectedShape === 'triangle') { const triangleSize = size * 1.5; // Plus grand pour une meilleure visibilité // Dessiner le triangle avec contour et yeux ctx.beginPath(); if (mouthOpen) { // Triangle avec bouche ouverte ctx.moveTo(0, -triangleSize * 0.9); ctx.lineTo(-triangleSize * 0.85, triangleSize * 0.7); ctx.lineTo(-triangleSize * 0.4, triangleSize * 0.4); ctx.lineTo(0, triangleSize * 0.2); ctx.lineTo(triangleSize * 0.4, triangleSize * 0.4); ctx.lineTo(triangleSize * 0.85, triangleSize * 0.7); ctx.closePath(); } else { // Triangle équilatéral complet ctx.moveTo(0, -triangleSize); ctx.lineTo(-triangleSize * 0.866, triangleSize * 0.5); ctx.lineTo(triangleSize * 0.866, triangleSize * 0.5); ctx.closePath(); } // Remplir le triangle ctx.fill(); // Contour noir épais ctx.strokeStyle = '#000000'; ctx.lineWidth = 2.5; ctx.stroke(); // Dessiner les yeux ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(-triangleSize * 0.2, -triangleSize * 0.15, triangleSize * 0.12, 0, Math.PI * 2); ctx.arc(triangleSize * 0.2, -triangleSize * 0.15, triangleSize * 0.12, 0, Math.PI * 2); ctx.fill(); // Pupilles ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.arc(-triangleSize * 0.2, -triangleSize * 0.15, triangleSize * 0.06, 0, Math.PI * 2); ctx.arc(triangleSize * 0.2, -triangleSize * 0.15, triangleSize * 0.06, 0, Math.PI * 2); ctx.fill(); // Reflets dans les yeux ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(-triangleSize * 0.22, -triangleSize * 0.17, triangleSize * 0.03, 0, Math.PI * 2); ctx.arc(triangleSize * 0.18, -triangleSize * 0.17, triangleSize * 0.03, 0, Math.PI * 2); ctx.fill(); // Restaurer la couleur originale if (selectedColor === 'rainbow') { const hue = (angle * 180 / Math.PI) % 360; ctx.fillStyle = `hsl(${hue}, 100%, 50%)`; } else { const colorMap = { 'yellow': '#ffd700', 'red': '#ff4444', 'blue': '#4444ff', 'green': '#44ff44', 'purple': '#aa44ff', 'orange': '#ff8844', 'pink': '#ff44aa' }; ctx.fillStyle = colorMap[selectedColor] || '#ffd700'; } } else { // Dessiner un cercle if (mouthOpen) { ctx.arc(0, 0, size, 0.2, Math.PI * 2 - 0.2); } else { ctx.arc(0, 0, size, 0, Math.PI * 2); } ctx.lineTo(0, 0); } ctx.fill(); ctx.restore(); previewAnimationId = requestAnimationFrame(animate); } animate(); } function stopPreviewAnimation() { if (previewAnimationId) { cancelAnimationFrame(previewAnimationId); previewAnimationId = null; } } function updatePlayerPreview() { // L'animation se mettra à jour automatiquement car elle utilise selectedColor } // Sauvegarder la personnalisation if (saveCustomizeBtn) { saveCustomizeBtn.addEventListener('click', () => { playerColor = selectedColor; playerShape = selectedShape; localStorage.setItem('playerColor', playerColor); localStorage.setItem('playerShape', playerShape); // Mettre à jour le joueur actuel si le jeu est en cours if (pacman) { pacman.color = playerColor; pacman.shape = playerShape; } // Fermer le modal if (customizeModal) { customizeModal.style.display = 'none'; stopPreviewAnimation(); } // Message de confirmation alert('Personnalisation sauvegardée !'); }); } // Fermer le classement dans le jeu if (closeLeaderboardBtn) { closeLeaderboardBtn.addEventListener('click', () => { leaderboardContainer.style.display = 'none'; }); } // Fermer le modal en cliquant à l'extérieur leaderboardModal.addEventListener('click', (e) => { if (e.target === leaderboardModal) { leaderboardModal.style.display = 'none'; } }); // Fonction pour mettre à jour le classement du menu function updateMenuLeaderboard() { const scores = getScores(); menuLeaderboard.innerHTML = ''; if (scores.length === 0) { menuLeaderboard.innerHTML = '
Aucun score enregistré
'; return; } scores.forEach((entry, index) => { const item = document.createElement('div'); item.className = 'leaderboard-item' + (index < 3 ? ' top' : ''); const rank = document.createElement('div'); rank.className = 'leaderboard-rank'; rank.textContent = (index + 1) + '.'; const name = document.createElement('div'); name.className = 'leaderboard-name'; name.textContent = entry.username; const levelDiv = document.createElement('div'); levelDiv.className = 'leaderboard-level'; levelDiv.textContent = 'Niv. ' + (entry.level || 1); const scoreDiv = document.createElement('div'); scoreDiv.className = 'leaderboard-score'; scoreDiv.textContent = entry.score; item.appendChild(rank); item.appendChild(name); item.appendChild(levelDiv); item.appendChild(scoreDiv); menuLeaderboard.appendChild(item); }); } // Fonction pour mettre à jour le meilleur score dans le menu function updateBestScore() { const scores = getScores(); const bestScoreElement = document.getElementById('bestScoreValue'); if (scores.length > 0) { const bestScore = scores[0].score; const bestLevel = scores[0].level || 1; bestScoreElement.textContent = `${bestScore} (Niv. ${bestLevel})`; } else { bestScoreElement.textContent = '0'; } } // Initialiser seulement le menu au chargement updateMenuLeaderboard(); updateBestScore(); // Appeler au chargement, au redimensionnement et quand le jeu devient visible window.addEventListener('resize', () => { if (gameWrapper && gameWrapper.style.display !== 'none') { resizeCanvas(); } }); // Redimensionner quand le jeu est affiché if (gameWrapper) { const observer = new MutationObserver(() => { if (gameWrapper && gameWrapper.style.display !== 'none') { // Attendre un peu pour que le DOM soit complètement rendu setTimeout(resizeCanvas, 100); } }); observer.observe(gameWrapper, { attributes: true, attributeFilter: ['style'] }); } // === GESTION DU PLEIN ÉCRAN === function toggleFullscreen() { const gameWrapper = document.getElementById('gameWrapper'); if (!document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement && !document.msFullscreenElement) { // Entrer en plein écran if (gameWrapper.requestFullscreen) { gameWrapper.requestFullscreen(); } else if (gameWrapper.webkitRequestFullscreen) { gameWrapper.webkitRequestFullscreen(); } else if (gameWrapper.mozRequestFullScreen) { gameWrapper.mozRequestFullScreen(); } else if (gameWrapper.msRequestFullscreen) { gameWrapper.msRequestFullscreen(); } } else { // Quitter le plein écran if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } } } function updateFullscreenButton() { const isFullscreenMode = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; if (fullscreenBtn) { fullscreenBtn.textContent = isFullscreenMode ? '✕ Sortir' : '⛶ Plein écran'; fullscreenBtn.title = isFullscreenMode ? 'Quitter le plein écran' : 'Plein écran'; } // Cacher l'indicateur de poursuite en mode plein écran if (isFullscreenMode && pursuitIndicator) { pursuitIndicator.style.display = 'none'; } // Redimensionner le canvas après changement de mode plein écran setTimeout(resizeCanvas, 100); } // Ajouter l'event listener au bouton if (fullscreenBtn) { fullscreenBtn.addEventListener('click', toggleFullscreen); } // Écouter les changements de mode plein écran document.addEventListener('fullscreenchange', updateFullscreenButton); document.addEventListener('webkitfullscreenchange', updateFullscreenButton); document.addEventListener('mozfullscreenchange', updateFullscreenButton); document.addEventListener('MSFullscreenChange', updateFullscreenButton);