2380 lines
82 KiB
JavaScript
2380 lines
82 KiB
JavaScript
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');
|
|
|
|
// === 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');
|
|
|
|
let usernameConfirmed = false;
|
|
|
|
const CELL_SIZE = 20;
|
|
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() {
|
|
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;
|
|
}
|
|
|
|
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(180, 360 - (level - 1) * 30);
|
|
for (let ghost of ghosts) {
|
|
ghost.isVulnerable = true;
|
|
ghost.vulnerableTimer = vulnerableTime;
|
|
}
|
|
|
|
// Afficher l'indicateur de poursuite
|
|
if (pursuitIndicator) {
|
|
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]);
|
|
|
|
const hue = (this.colorAnimation * 180 / Math.PI) % 360;
|
|
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
|
|
|
|
ctx.beginPath();
|
|
|
|
if (this.mouthOpen) {
|
|
ctx.arc(0, 0, CELL_SIZE * 0.4, 0.2, Math.PI * 2 - 0.2);
|
|
} else {
|
|
ctx.arc(0, 0, CELL_SIZE * 0.4, 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.25;
|
|
} else if (type === GHOST_INVISIBLE && level >= 5) {
|
|
this.isInvisible = true;
|
|
this.invisibleTimer = 300;
|
|
}
|
|
}
|
|
|
|
updateSpeed() {
|
|
let speedMultiplier = 1 + (level - 1) * 0.2;
|
|
if (this.type === GHOST_FAST) {
|
|
speedMultiplier *= 1.5;
|
|
}
|
|
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(8, 20 - (level - 1) * 2);
|
|
|
|
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 {
|
|
// Comportement selon le type
|
|
if (this.type === GHOST_HUNTER) {
|
|
this.direction = this.getDirectionToPacman(possibleDirections);
|
|
} else if (this.type === GHOST_PATROL) {
|
|
this.direction = this.getPatrolDirection(possibleDirections);
|
|
} else if (this.type === GHOST_FAST || this.type === GHOST_INVISIBLE) {
|
|
this.direction = this.getDirectionToPacman(possibleDirections);
|
|
} else {
|
|
// Normal : toujours poursuivre
|
|
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;
|
|
|
|
// Si Pacman bouge, prédire où il sera
|
|
if (pacman.direction !== undefined) {
|
|
const futureX = pacman.x + dx[pacman.direction] * 2;
|
|
const futureY = pacman.y + dy[pacman.direction] * 2;
|
|
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) {
|
|
// Éviter la direction opposée sauf si c'est la seule option
|
|
if (possibleDirections.length > 1 && dir === oppositeDirection) {
|
|
continue;
|
|
}
|
|
|
|
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();
|
|
saveScore();
|
|
} 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) 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
|
|
if (pursuitIndicator && (cherryEatenTimer > 0 || frenzyModeActive)) {
|
|
const timer = frenzyModeActive ? frenzyTimer : cherryEatenTimer;
|
|
const seconds = Math.ceil(timer / 60);
|
|
if (pursuitTimerElement) {
|
|
pursuitTimerElement.textContent = seconds;
|
|
}
|
|
} 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
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() {
|
|
let username;
|
|
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 = '<div class="empty-leaderboard">Aucun score enregistré</div>';
|
|
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 = '<div class="empty-leaderboard" style="text-align: center; color: #888; padding: 20px; font-size: 0.7em;">Aucun score</div>';
|
|
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;
|
|
gameOverlay.classList.add('active');
|
|
restartBtn.style.display = 'block';
|
|
}
|
|
|
|
function hideGameOver() {
|
|
gameOverlay.classList.remove('active');
|
|
}
|
|
|
|
overlayRestartBtn.addEventListener('click', () => {
|
|
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é
|
|
initGame();
|
|
});
|
|
|
|
// 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';
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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 = '<div class="empty-leaderboard">Aucun score enregistré</div>';
|
|
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();
|
|
|