Compare commits

..

8 Commits

3 changed files with 1164 additions and 61 deletions

695
game.js
View File

@ -7,11 +7,15 @@ 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');
@ -27,10 +31,29 @@ 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;
const CELL_SIZE = 20;
// 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;
@ -232,7 +255,7 @@ let traps = [];
let specialZones = [];
class Pacman {
constructor() {
constructor(color = null, shape = null) {
this.x = 14;
this.y = 23;
this.direction = 0;
@ -244,6 +267,8 @@ class Pacman {
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() {
@ -353,14 +378,14 @@ class Pacman {
cherryEatenTimer = Math.max(150, 300 - (level - 1) * 20);
// Rendre tous les fantômes vulnérables
const vulnerableTime = Math.max(180, 360 - (level - 1) * 30);
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
if (pursuitIndicator) {
// Afficher l'indicateur de poursuite (sauf en mode plein écran)
if (pursuitIndicator && !isFullscreen()) {
pursuitIndicator.style.display = 'block';
}
@ -448,20 +473,94 @@ class Pacman {
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%)`;
// 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.mouthOpen) {
ctx.arc(0, 0, CELL_SIZE * 0.4, 0.2, Math.PI * 2 - 0.2);
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 {
ctx.arc(0, 0, CELL_SIZE * 0.4, 0, Math.PI * 2);
// 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.lineTo(0, 0);
ctx.fill();
ctx.restore();
}
}
@ -490,7 +589,7 @@ class Ghost {
// Ajustements selon le type
if (type === GHOST_FAST) {
this.baseSpeed = 0.25;
this.baseSpeed = 0.16;
} else if (type === GHOST_INVISIBLE && level >= 5) {
this.isInvisible = true;
this.invisibleTimer = 300;
@ -498,9 +597,11 @@ class Ghost {
}
updateSpeed() {
let speedMultiplier = 1 + (level - 1) * 0.2;
// Vitesse fixe comme au niveau 3 (x1.4) pour tous les niveaux
let speedMultiplier = 1.4;
if (this.type === GHOST_FAST) {
speedMultiplier *= 1.5;
speedMultiplier *= 1.1;
}
this.speed = this.baseSpeed * speedMultiplier;
}
@ -535,7 +636,7 @@ class Ghost {
}
}
this.moveInterval = Math.max(8, 20 - (level - 1) * 2);
this.moveInterval = Math.max(5, 15 - (level - 1) * 1.5);
this.moveCounter++;
@ -552,15 +653,20 @@ class Ghost {
// 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);
// 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 {
// Normal : toujours poursuivre
// Tous les autres chassent directement
this.direction = this.getDirectionToPacman(possibleDirections);
}
}
@ -592,10 +698,13 @@ class Ghost {
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] * 2;
const futureY = pacman.y + dy[pacman.direction] * 2;
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;
@ -609,10 +718,8 @@ class Ghost {
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;
}
// 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];
@ -1221,7 +1328,7 @@ function checkCollisions() {
gameRunning = false;
statusElement.textContent = 'Game Over !';
showGameOver();
saveScore();
// Le score sera sauvegardé quand l'utilisateur clique sur "Sauvegarder"
} else {
restartCurrentLevel();
}
@ -1305,7 +1412,7 @@ function gameLoop() {
ghost.vulnerableTimer = FRENZY_DURATION;
}
if (frenzyIndicator) frenzyIndicator.style.display = 'block';
if (pursuitIndicator) pursuitIndicator.style.display = 'block';
if (pursuitIndicator && !isFullscreen()) pursuitIndicator.style.display = 'block';
}
} else {
frenzyTimer--;
@ -1330,13 +1437,16 @@ function gameLoop() {
pacman.update();
pacman.draw();
// Mettre à jour l'indicateur de poursuite
if (pursuitIndicator && (cherryEatenTimer > 0 || frenzyModeActive)) {
// 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';
}
@ -1748,6 +1858,77 @@ function placeBonuses() {
}
}
// === 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
@ -1812,6 +1993,11 @@ function initGame() {
placeBonuses();
countDots();
updateGameLeaderboard();
// Redimensionner le canvas après l'initialisation
setTimeout(resizeCanvas, 100);
gameLoop();
}
@ -1859,9 +2045,15 @@ function getScores() {
return scoresJson ? JSON.parse(scoresJson) : [];
}
function saveScore() {
function saveScore(usernameForScore = null) {
let username;
if (!usernameConfirmed) {
// 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';
@ -1879,6 +2071,7 @@ function saveScore() {
const topScores = scores.slice(0, 10);
localStorage.setItem('pacmanScores', JSON.stringify(topScores));
updateLeaderboard();
updateGameLeaderboard();
updateBestScore(); // Mettre à jour le meilleur score dans le menu
}
}
@ -1920,6 +2113,48 @@ function updateLeaderboard() {
});
}
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();
});
@ -1989,6 +2224,21 @@ usernameInput.addEventListener('keypress', (e) => {
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';
}
@ -1997,7 +2247,39 @@ 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();
});
@ -2214,7 +2496,11 @@ playBtn.addEventListener('click', () => {
resetUsernameInput();
// Toujours initialiser le jeu quand on clique sur JOUER
gameRunning = false; // S'assurer que le jeu est arrêté
initGame();
// Attendre un peu pour que le DOM soit rendu avant de redimensionner
setTimeout(() => {
initGame();
resizeCanvas();
}, 50);
});
// Retour au menu depuis le jeu
@ -2262,6 +2548,262 @@ if (rulesModal) {
});
}
// === 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', () => {
@ -2332,3 +2874,80 @@ function updateBestScore() {
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);

View File

@ -21,6 +21,9 @@
<button class="menu-btn" id="playBtn">
<span class="btn-icon"></span> JOUER
</button>
<button class="menu-btn" id="customizeBtn">
<span class="btn-icon">🎨</span> PERSONNALISER
</button>
<button class="menu-btn" id="scoresBtn">
<span class="btn-icon">🏆</span> CLASSEMENT
</button>
@ -38,20 +41,21 @@
<div class="game-header">
<button class="back-btn" id="backToMenuBtn">← Menu</button>
<h2>OULVIC</h2>
<button class="fullscreen-btn" id="fullscreenBtn" title="Plein écran">⛶ Plein écran</button>
</div>
<div class="user-input-section">
<label for="username">Nom d'utilisateur:</label>
<input type="text" id="username" placeholder="Entrez votre nom" maxlength="15">
<div class="user-input-section">
<label for="username">Nom d'utilisateur:</label>
<input type="text" id="username" placeholder="Entrez votre nom" maxlength="15">
<button id="confirmUsernameBtn" class="confirm-btn">✓ Confirmer</button>
</div>
<div class="game-info">
<div class="score">Score: <span id="score">0</span></div>
<div class="level">Niveau: <span id="level">1</span></div>
<div class="lives">Vies: <span id="lives"><span class="heart"></span><span class="heart"></span><span class="heart"></span></span></div>
</div>
<div class="game-info">
<div class="score">Score: <span id="score">0</span></div>
<div class="level">Niveau: <span id="level">1</span></div>
<div class="lives">Vies: <span id="lives"><span class="heart"></span><span class="heart"></span><span class="heart"></span></span></div>
<div class="dots-remaining">Pastilles: <span id="dotsRemaining">0</span></div>
<div class="combo-display" id="comboDisplay" style="display: none;">Combo x<span id="comboValue">1</span></div>
<div class="status" id="status">Prêt à jouer</div>
</div>
<div class="status" id="status">Prêt à jouer</div>
</div>
<div class="power-ups-display" id="powerUpsDisplay">
<div class="power-up-item" id="speedBoostIndicator" style="display: none;">⚡ Vitesse</div>
<div class="power-up-item" id="shieldIndicator" style="display: none;">🛡 Bouclier</div>
@ -63,7 +67,7 @@
<div class="pursuit-indicator" id="pursuitIndicator" style="display: none;">
<span class="pursuit-text">👻 Poursuite active: <span id="pursuitTimer">0</span>s</span>
</div>
<canvas id="gameCanvas" width="600" height="600"></canvas>
<canvas id="gameCanvas" width="750" height="750"></canvas>
<!-- Contrôles tactiles mobile -->
<div class="mobile-controls" id="mobileControls">
@ -75,8 +79,8 @@
<button class="ctrl-btn" data-dir="2"></button>
</div>
<div class="instructions">
<p>Utilisez les flèches directionnelles pour déplacer Oulvic</p>
<div class="instructions">
<p>Utilisez les flèches directionnelles pour déplacer Oulvic</p>
<button id="restartBtn" style="display: none;">REJOUER</button>
</div>
@ -86,23 +90,37 @@
<h2 class="game-over-title">GAME OVER</h2>
<p class="final-score">Score Final: <span id="finalScore">0</span></p>
<p class="final-level">Niveau Atteint: <span id="finalLevel">1</span></p>
<div class="game-over-username-section">
<label for="gameOverUsername">Nom pour le classement:</label>
<input type="text" id="gameOverUsername" placeholder="Entrez votre nom" maxlength="15">
<button id="saveScoreBtn" class="save-score-btn">💾 Sauvegarder</button>
</div>
<p class="score-saved-msg" id="scoreSavedMsg" style="display: none; color: #00ff00; font-size: 0.7em; margin-top: 10px;">Score sauvegardé !</p>
<button id="overlayRestartBtn">REJOUER</button>
</div>
</div>
</div>
<!-- CLASSEMENT À DROITE (hors du container) -->
<div class="game-leaderboard" id="gameLeaderboard">
<div class="game-leaderboard-header">
<h3>🏆 Classement</h3>
</div>
<div id="gameLeaderboardContent" class="game-leaderboard-content"></div>
</div>
<!-- CLASSEMENT (caché par défaut) -->
<div class="leaderboard-container" id="leaderboardContainer" style="display: none;">
<div class="leaderboard-header">
<h2>Classement</h2>
<h2>Classement</h2>
<button class="close-leaderboard-btn" id="closeLeaderboardBtn"></button>
</div>
<div id="leaderboard"></div>
</div>
<div id="leaderboard"></div>
</div>
<footer>
<p>By Ludo and Syoul</p>
</footer>
</div>
<footer>
<p>By Ludo and Syoul</p>
</footer>
</div>
<!-- MODAL CLASSEMENT (pour le menu) -->
@ -170,10 +188,20 @@
<div class="rule-section">
<h3>👻 Fantômes</h3>
<p>Évitez les fantômes ! Si vous les touchez, vous perdez une vie. Vous avez 3 vies au départ.</p>
<p><strong>Types de fantômes :</strong></p>
<ul>
<li><strong>Niveau 1-2</strong> : 4 fantômes normaux</li>
<li><strong>Niveau 3-4</strong> : 5 fantômes (dont des chasseurs et patrouilleurs)</li>
<li><strong>Niveau 5+</strong> : 6-7 fantômes (rapides, invisibles, etc.)</li>
<li><strong>👤 Fantôme Normal</strong> : Poursuit directement le joueur (Niveau 1+)</li>
<li><strong>🎯 Chasseur</strong> : Poursuit agressivement avec prédiction de mouvement (Niveau 2+)</li>
<li><strong>🛡 Patrouilleur</strong> : Bloque les passages stratégiques et patrouille (Niveau 3+)</li>
<li><strong>⚡ Rapide</strong> : 1.5x plus rapide que les autres fantômes (Niveau 4+)</li>
<li><strong>👻 Invisible</strong> : Devient invisible 50% du temps, difficile à voir (Niveau 5+)</li>
</ul>
<p><strong>Nombre de fantômes par niveau :</strong></p>
<ul>
<li><strong>Niveau 1-2</strong> : 4 fantômes</li>
<li><strong>Niveau 3-4</strong> : 5 fantômes</li>
<li><strong>Niveau 5-9</strong> : 6 fantômes</li>
<li><strong>Niveau 10+</strong> : 7 fantômes</li>
</ul>
</div>
@ -203,6 +231,73 @@
</div>
</div>
<!-- MODAL PERSONNALISATION -->
<div class="customize-modal" id="customizeModal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h2>🎨 PERSONNALISER VOTRE JOUEUR</h2>
<button class="close-modal-btn" id="closeCustomizeBtn">×</button>
</div>
<div class="customize-content">
<div class="customize-section">
<h3>Couleur du joueur</h3>
<div class="color-options">
<div class="color-option" data-color="yellow" data-name="Jaune">
<div class="color-preview" style="background: #ffd700;"></div>
<span>Jaune (Classique)</span>
</div>
<div class="color-option" data-color="red" data-name="Rouge">
<div class="color-preview" style="background: #ff4444;"></div>
<span>Rouge</span>
</div>
<div class="color-option" data-color="blue" data-name="Bleu">
<div class="color-preview" style="background: #4444ff;"></div>
<span>Bleu</span>
</div>
<div class="color-option" data-color="green" data-name="Vert">
<div class="color-preview" style="background: #44ff44;"></div>
<span>Vert</span>
</div>
<div class="color-option" data-color="purple" data-name="Violet">
<div class="color-preview" style="background: #aa44ff;"></div>
<span>Violet</span>
</div>
<div class="color-option" data-color="orange" data-name="Orange">
<div class="color-preview" style="background: #ff8844;"></div>
<span>Orange</span>
</div>
<div class="color-option active" data-color="rainbow" data-name="Arc-en-ciel">
<div class="color-preview rainbow-preview"></div>
<span>Arc-en-ciel (Animé)</span>
</div>
<div class="color-option" data-color="pink" data-name="Rose">
<div class="color-preview" style="background: #ff44aa;"></div>
<span>Rose</span>
</div>
</div>
</div>
<div class="customize-section">
<h3>Forme du joueur</h3>
<div class="shape-options">
<div class="shape-option active" data-shape="round" data-name="Rond">
<div class="shape-preview round-preview"></div>
<span>Rond (Classique)</span>
</div>
<div class="shape-option" data-shape="triangle" data-name="Triangle">
<div class="shape-preview triangle-preview"></div>
<span>Triangle</span>
</div>
</div>
</div>
<div class="customize-preview">
<h3>Aperçu</h3>
<canvas id="playerPreviewCanvas" width="200" height="200"></canvas>
</div>
<button class="save-customize-btn" id="saveCustomizeBtn">💾 Sauvegarder</button>
</div>
</div>
</div>
<script src="game.js"></script>
</body>
</html>

391
style.css
View File

@ -19,6 +19,7 @@ body {
padding: 20px;
position: relative;
overflow-x: hidden;
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='32' viewBox='0 0 36 32'%3E%3Cdefs%3E%3Cfilter id='glow'%3E%3CfeGaussianBlur stdDeviation='1.5' result='coloredBlur'/%3E%3CfeMerge%3E%3CfeMergeNode in='coloredBlur'/%3E%3CfeMergeNode in='SourceGraphic'/%3E%3C/feMerge%3E%3C/filter%3E%3ClinearGradient id='ghostGrad' x1='0%25' y1='0%25' x2='0%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23ff4444;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23cc0000;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M18 4 Q22 2 26 4 Q30 6 30 10 L30 20 Q30 24 28 26 Q26 28 24 28 Q22 30 20 28 Q18 30 16 28 Q14 30 12 28 Q10 30 8 28 Q6 30 4 28 Q2 26 2 22 L2 10 Q2 6 6 4 Q10 2 14 4 Q16 2 18 4 Z' fill='url(%23ghostGrad)' filter='url(%23glow)' stroke='%23ffffff' stroke-width='0.3'/%3E%3Cpath d='M8 22 Q6 24 8 26 Q9 24 8 22 M14 22 Q12 24 14 26 Q15 24 14 22 M20 22 Q18 24 20 26 Q21 24 20 22 M26 22 Q24 24 26 26 Q27 24 26 22' fill='url(%23ghostGrad)' filter='url(%23glow)'/%3E%3Ccircle cx='12' cy='10' r='3' fill='%23ffffff'/%3E%3Ccircle cx='24' cy='10' r='3' fill='%23ffffff'/%3E%3Ccircle cx='12' cy='10' r='1.8' fill='%23000000'/%3E%3Ccircle cx='24' cy='10' r='1.8' fill='%23000000'/%3E%3Cellipse cx='12.5' cy='9.5' rx='0.6' ry='0.8' fill='%23ffffff'/%3E%3Cellipse cx='24.5' cy='9.5' rx='0.6' ry='0.8' fill='%23ffffff'/%3E%3C/svg%3E") 18 16, auto;
}
body::before {
@ -143,6 +144,7 @@ body::after {
height: 100%;
z-index: 1000;
overflow: hidden;
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='32' viewBox='0 0 36 32'%3E%3Cdefs%3E%3Cfilter id='glow'%3E%3CfeGaussianBlur stdDeviation='1.5' result='coloredBlur'/%3E%3CfeMerge%3E%3CfeMergeNode in='coloredBlur'/%3E%3CfeMergeNode in='SourceGraphic'/%3E%3C/feMerge%3E%3C/filter%3E%3ClinearGradient id='ghostGrad' x1='0%25' y1='0%25' x2='0%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23ff4444;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23cc0000;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M18 4 Q22 2 26 4 Q30 6 30 10 L30 20 Q30 24 28 26 Q26 28 24 28 Q22 30 20 28 Q18 30 16 28 Q14 30 12 28 Q10 30 8 28 Q6 30 4 28 Q2 26 2 22 L2 10 Q2 6 6 4 Q10 2 14 4 Q16 2 18 4 Z' fill='url(%23ghostGrad)' filter='url(%23glow)' stroke='%23ffffff' stroke-width='0.3'/%3E%3Cpath d='M8 22 Q6 24 8 26 Q9 24 8 22 M14 22 Q12 24 14 26 Q15 24 14 22 M20 22 Q18 24 20 26 Q21 24 20 22 M26 22 Q24 24 26 26 Q27 24 26 22' fill='url(%23ghostGrad)' filter='url(%23glow)'/%3E%3Ccircle cx='12' cy='10' r='3' fill='%23ffffff'/%3E%3Ccircle cx='24' cy='10' r='3' fill='%23ffffff'/%3E%3Ccircle cx='12' cy='10' r='1.8' fill='%23000000'/%3E%3Ccircle cx='24' cy='10' r='1.8' fill='%23000000'/%3E%3Cellipse cx='12.5' cy='9.5' rx='0.6' ry='0.8' fill='%23ffffff'/%3E%3Cellipse cx='24.5' cy='9.5' rx='0.6' ry='0.8' fill='%23ffffff'/%3E%3C/svg%3E") 18 16, auto;
}
/* === CANVAS ARRIERE-PLAN MENU === */
@ -321,6 +323,26 @@ body::after {
transform: translateX(-3px);
}
.fullscreen-btn {
padding: 10px 20px;
font-size: 0.7em;
font-family: 'Press Start 2P', cursive;
background: rgba(255, 255, 255, 0.1);
color: #ffd700;
border: 2px solid #ffd700;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
line-height: 1.2;
white-space: nowrap;
}
.fullscreen-btn:hover {
background: rgba(255, 215, 0, 0.2);
transform: scale(1.1);
box-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
/* === MODAL CLASSEMENT === */
.leaderboard-modal {
position: fixed;
@ -512,7 +534,7 @@ body::after {
0 10px 30px rgba(0, 0, 0, 0.8),
0 0 50px rgba(255, 0, 0, 0.2),
inset 0 0 30px rgba(255, 215, 0, 0.05);
flex-shrink: 0;
flex: 0 0 auto;
position: relative;
z-index: 1;
border: 3px solid rgba(255, 215, 0, 0.4);
@ -693,6 +715,14 @@ h1 {
0 0 40px rgba(255, 215, 0, 0.3),
inset 0 0 60px rgba(0, 0, 255, 0.1);
animation: canvasGlow 3s ease-in-out infinite alternate;
max-width: 100%;
max-height: 90vh;
width: auto;
height: auto;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
/* === INPUT UTILISATEUR === */
@ -911,6 +941,83 @@ h1 {
text-align: right;
}
/* === CLASSEMENT DANS LE JEU (à droite) === */
.game-leaderboard {
background: rgba(0, 0, 0, 0.85);
padding: 15px;
border-radius: 15px;
border: 2px solid rgba(255, 215, 0, 0.4);
box-shadow:
0 5px 20px rgba(0, 0, 0, 0.6),
0 0 30px rgba(255, 0, 0, 0.1);
width: 250px;
max-height: 600px;
overflow-y: auto;
}
.game-leaderboard-header {
text-align: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid rgba(255, 215, 0, 0.3);
}
.game-leaderboard-header h3 {
color: #ffd700;
font-size: 0.9em;
margin: 0;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
.game-leaderboard-content {
max-height: 550px;
overflow-y: auto;
}
.game-leaderboard-content::-webkit-scrollbar {
width: 6px;
}
.game-leaderboard-content::-webkit-scrollbar-track {
background: rgba(255, 215, 0, 0.1);
border-radius: 3px;
}
.game-leaderboard-content::-webkit-scrollbar-thumb {
background: rgba(255, 215, 0, 0.5);
border-radius: 3px;
}
.game-leaderboard-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 215, 0, 0.7);
}
.game-leaderboard .leaderboard-item {
padding: 8px 10px;
margin-bottom: 8px;
font-size: 0.7em;
}
.game-leaderboard .leaderboard-rank {
min-width: 40px;
font-size: 0.8em;
}
.game-leaderboard .leaderboard-name {
font-size: 0.65em;
margin-left: 5px;
}
.game-leaderboard .leaderboard-level {
min-width: 50px;
font-size: 0.65em;
}
.game-leaderboard .leaderboard-score {
min-width: 60px;
font-size: 0.7em;
}
.empty-leaderboard {
text-align: center;
color: #aaa;
@ -1016,6 +1123,71 @@ h1 {
transform: translateY(0) scale(0.98);
}
.game-over-username-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin: 20px 0;
}
.game-over-username-section label {
font-size: 0.7em;
color: #ffd700;
font-weight: bold;
}
#gameOverUsername {
padding: 10px 15px;
font-size: 0.8em;
font-family: 'Press Start 2P', cursive;
border: 2px solid #ffd700;
border-radius: 10px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
outline: none;
max-width: 250px;
width: 100%;
text-align: center;
transition: all 0.3s;
}
#gameOverUsername:focus {
border-color: #ffed4e;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.6);
background: rgba(0, 0, 0, 0.9);
}
.save-score-btn {
padding: 10px 20px;
font-size: 0.7em;
font-family: 'Press Start 2P', cursive;
background: linear-gradient(180deg, #00ff00 0%, #00cc00 100%);
color: #000;
border: 2px solid #00ff00;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 10px rgba(0, 255, 0, 0.3);
}
.save-score-btn:hover {
background: linear-gradient(180deg, #00ff88 0%, #00ff00 100%);
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 255, 0, 0.5);
}
.save-score-btn:active {
transform: translateY(0) scale(0.98);
}
.save-score-btn:disabled {
background: rgba(100, 100, 100, 0.5);
border-color: #666;
cursor: not-allowed;
opacity: 0.5;
}
/* === CONTROLES TACTILES MOBILE === */
.mobile-controls {
display: none;
@ -1069,6 +1241,12 @@ footer {
align-items: center;
}
.game-leaderboard {
width: 100%;
max-width: 600px;
margin-top: 20px;
}
.leaderboard-container {
width: 100%;
max-width: 600px;
@ -1155,3 +1333,214 @@ footer {
font-size: 0.9em;
}
}
/* === MODAL PERSONNALISATION === */
.customize-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 1000;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease-out;
}
.customize-modal.active {
display: flex;
}
.customize-content {
display: flex;
flex-direction: column;
gap: 30px;
max-width: 800px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.customize-section {
background: rgba(0, 0, 0, 0.6);
padding: 20px;
border-radius: 15px;
border: 2px solid #ffd700;
}
.customize-section h3 {
color: #ffd700;
font-size: 1em;
margin-bottom: 15px;
text-align: center;
}
.color-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.color-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 215, 0, 0.3);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.color-option:hover {
background: rgba(255, 215, 0, 0.1);
border-color: #ffd700;
transform: scale(1.05);
}
.color-option.active {
background: rgba(255, 215, 0, 0.2);
border-color: #ffd700;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
}
.color-preview {
width: 60px;
height: 60px;
border-radius: 50%;
border: 3px solid #fff;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.rainbow-preview {
background: linear-gradient(45deg, #ff0000, #ff8800, #ffff00, #00ff00, #0088ff, #0000ff, #8800ff);
animation: rainbowRotate 3s linear infinite;
}
@keyframes rainbowRotate {
0% { filter: hue-rotate(0deg); }
100% { filter: hue-rotate(360deg); }
}
.color-option span {
font-size: 0.7em;
color: #fff;
text-align: center;
}
.customize-preview {
background: rgba(0, 0, 0, 0.6);
padding: 20px;
border-radius: 15px;
border: 2px solid #ffd700;
display: flex;
flex-direction: column;
align-items: center;
}
.customize-preview h3 {
color: #ffd700;
font-size: 1em;
margin-bottom: 15px;
}
#playerPreviewCanvas {
border: 3px solid #ffd700;
border-radius: 10px;
background: #000;
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
.save-customize-btn {
padding: 15px 30px;
font-size: 0.9em;
font-family: 'Press Start 2P', cursive;
background: linear-gradient(180deg, #00ff00 0%, #00cc00 100%);
color: #000;
border: 2px solid #00ff00;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 10px rgba(0, 255, 0, 0.3);
align-self: center;
}
.save-customize-btn:hover {
background: linear-gradient(180deg, #00ff88 0%, #00ff00 100%);
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 255, 0, 0.5);
}
.save-customize-btn:active {
transform: translateY(0) scale(0.98);
}
.shape-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.shape-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 215, 0, 0.3);
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.shape-option:hover {
background: rgba(255, 215, 0, 0.1);
border-color: #ffd700;
transform: scale(1.05);
}
.shape-option.active {
background: rgba(255, 215, 0, 0.2);
border-color: #ffd700;
box-shadow: 0 0 15px rgba(255, 215, 0, 0.5);
}
.shape-preview {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.round-preview {
width: 50px;
height: 50px;
border-radius: 50%;
background: #ffd700;
border: 3px solid #fff;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.triangle-preview {
width: 0;
height: 0;
border-left: 25px solid transparent;
border-right: 25px solid transparent;
border-bottom: 45px solid #ffd700;
border-top: none;
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.3));
}
.shape-option span {
font-size: 0.7em;
color: #fff;
text-align: center;
}