Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Catch Up: Neon Chase</title> | |
| <!-- Importation de la police Google Fonts (Orbitron pour le look Sci-Fi) --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Roboto:wght@300;400&display=swap" rel="stylesheet"> | |
| <!-- Importation de FontAwesome pour les icônes --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-color: #0b0c15; | |
| --primary-color: #00f3ff; /* Cyan Néon */ | |
| --secondary-color: #ff0055; /* Rouge Néon */ | |
| --accent-color: #00ff88; /* Vert Néon */ | |
| --text-color: #ffffff; | |
| --glass-bg: rgba(255, 255, 255, 0.05); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| --font-display: 'Orbitron', sans-serif; | |
| --font-body: 'Roboto', sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| user-select: none; /* Empêche la sélection de texte pendant le jeu */ | |
| -webkit-user-select: none; | |
| } | |
| body { | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| font-family: var(--font-body); | |
| overflow: hidden; /* Empêche le défilement */ | |
| width: 100vw; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| z-index: 100; | |
| pointer-events: none; /* Laisse passer les clics vers le canvas si besoin */ | |
| } | |
| .brand { | |
| font-family: var(--font-display); | |
| font-size: 1.5rem; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| color: var(--primary-color); | |
| text-shadow: 0 0 10px var(--primary-color); | |
| pointer-events: auto; | |
| } | |
| .credits { | |
| font-size: 0.9rem; | |
| opacity: 0.8; | |
| pointer-events: auto; | |
| } | |
| .credits a { | |
| color: var(--accent-color); | |
| text-decoration: none; | |
| font-weight: bold; | |
| transition: color 0.3s ease; | |
| } | |
| .credits a:hover { | |
| color: var(--primary-color); | |
| text-shadow: 0 0 8px var(--primary-color); | |
| } | |
| /* --- Canvas Container --- */ | |
| #game-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| canvas { | |
| display: block; | |
| box-shadow: 0 0 50px rgba(0, 0, 0, 0.5); | |
| } | |
| /* --- UI Overlays (Start / Game Over) --- */ | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(11, 12, 21, 0.85); | |
| backdrop-filter: blur(8px); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 50; | |
| transition: opacity 0.4s ease; | |
| } | |
| .overlay.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .menu-card { | |
| background: var(--glass-bg); | |
| border: 1px solid var(--glass-border); | |
| padding: 3rem; | |
| border-radius: 20px; | |
| text-align: center; | |
| box-shadow: 0 0 30px rgba(0, 243, 255, 0.1); | |
| max-width: 500px; | |
| width: 90%; | |
| animation: float 6s ease-in-out infinite; | |
| } | |
| h1 { | |
| font-family: var(--font-display); | |
| font-size: 3rem; | |
| margin-bottom: 0.5rem; | |
| background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| p.subtitle { | |
| font-size: 1.1rem; | |
| margin-bottom: 2rem; | |
| color: #aaa; | |
| } | |
| .score-display { | |
| font-family: var(--font-display); | |
| font-size: 4rem; | |
| margin: 1rem 0; | |
| color: var(--text-color); | |
| } | |
| .instructions { | |
| margin-bottom: 2rem; | |
| font-size: 0.95rem; | |
| line-height: 1.6; | |
| color: #ddd; | |
| text-align: left; | |
| background: rgba(0,0,0,0.3); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| } | |
| .instructions ul { | |
| list-style: none; | |
| } | |
| .instructions li { | |
| margin-bottom: 0.5rem; | |
| } | |
| .instructions i { | |
| margin-right: 10px; | |
| width: 20px; | |
| text-align: center; | |
| } | |
| .btn { | |
| background: linear-gradient(45deg, var(--primary-color), #00a8ff); | |
| color: #000; | |
| font-family: var(--font-display); | |
| font-weight: 700; | |
| font-size: 1.2rem; | |
| padding: 1rem 3rem; | |
| border: none; | |
| border-radius: 50px; | |
| cursor: pointer; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .btn:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 0 20px var(--primary-color); | |
| } | |
| .btn:active { | |
| transform: translateY(1px); | |
| } | |
| /* --- HUD In-Game --- */ | |
| #hud { | |
| position: absolute; | |
| top: 20px; | |
| width: 100%; | |
| padding: 0 40px; | |
| display: flex; | |
| justify-content: space-between; | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| .hud-item { | |
| font-family: var(--font-display); | |
| font-size: 1.5rem; | |
| color: var(--text-color); | |
| text-shadow: 0 0 5px rgba(0,0,0,0.5); | |
| } | |
| .hud-label { | |
| font-size: 0.8rem; | |
| color: #888; | |
| display: block; | |
| text-transform: uppercase; | |
| } | |
| /* --- Animations --- */ | |
| @keyframes float { | |
| 0% { transform: translateY(0px); } | |
| 50% { transform: translateY(-10px); } | |
| 100% { transform: translateY(0px); } | |
| } | |
| /* --- Mobile Adjustments --- */ | |
| @media (max-width: 600px) { | |
| h1 { font-size: 2rem; } | |
| .menu-card { padding: 1.5rem; } | |
| #hud { padding: 0 20px; } | |
| .hud-item { font-size: 1.2rem; } | |
| header { padding: 1rem; } | |
| .brand { font-size: 1.1rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header avec le lien requis --> | |
| <header> | |
| <div class="brand"> | |
| <i class="fa-solid fa-bolt"></i> Catch Up | |
| </div> | |
| <div class="credits"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </div> | |
| </header> | |
| <!-- HUD (Score en jeu) --> | |
| <div id="hud" class="hidden"> | |
| <div class="hud-item"> | |
| <span class="hud-label">Score</span> | |
| <span id="current-score">0</span> | |
| </div> | |
| <div class="hud-item" style="text-align: right;"> | |
| <span class="hud-label">Record</span> | |
| <span id="high-score">0</span> | |
| </div> | |
| </div> | |
| <!-- Conteneur principal du jeu --> | |
| <div id="game-container"> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- Écran de Démarrage --> | |
| <div id="start-screen" class="overlay"> | |
| <div class="menu-card"> | |
| <h1>Catch Up</h1> | |
| <p class="subtitle">Neon Chase Edition</p> | |
| <div class="instructions"> | |
| <ul> | |
| <li><i class="fa-solid fa-computer-mouse" style="color: var(--primary-color)"></i> Bougez la souris ou glissez pour contrôler le point bleu.</li> | |
| <li><i class="fa-solid fa-bullseye" style="color: var(--accent-color)"></i> Attrapez les orbes <strong>VERTS</strong> pour gagner.</li> | |
| <li><i class="fa-solid fa-skull" style="color: var(--secondary-color)"></i> Évitez les triangles <strong>ROUGES</strong>.</li> | |
| <li><i class="fa-solid fa-gauge-high"></i> La vitesse augmente à chaque point !</li> | |
| </ul> | |
| </div> | |
| <button class="btn" id="start-btn">Jouer</button> | |
| </div> | |
| </div> | |
| <!-- Écran Game Over --> | |
| <div id="game-over-screen" class="overlay hidden"> | |
| <div class="menu-card"> | |
| <h1 style="color: var(--secondary-color);">Partie Terminée</h1> | |
| <p class="subtitle">Vous avez été attrapé !</p> | |
| <div class="score-display" id="final-score">0</div> | |
| <p style="margin-bottom: 2rem; color: #888;">Meilleur Score: <span id="final-high-score">0</span></p> | |
| <button class="btn" id="restart-btn">Réessayer</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * Logique du Jeu "Catch Up" | |
| * Utilise HTML5 Canvas pour le rendu | |
| */ | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Éléments UI | |
| const startScreen = document.getElementById('start-screen'); | |
| const gameOverScreen = document.getElementById('game-over-screen'); | |
| const hud = document.getElementById('hud'); | |
| const currentScoreEl = document.getElementById('current-score'); | |
| const highScoreEl = document.getElementById('high-score'); | |
| const finalScoreEl = document.getElementById('final-score'); | |
| const finalHighScoreEl = document.getElementById('final-high-score'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const restartBtn = document.getElementById('restart-btn'); | |
| // État du jeu | |
| let gameRunning = false; | |
| let score = 0; | |
| let highScore = localStorage.getItem('catchUpHighScore') || 0; | |
| let animationId; | |
| let frameCount = 0; | |
| // Configuration | |
| const config = { | |
| playerSpeed: 0.15, // Lerp factor | |
| enemyBaseSpeed: 2, | |
| enemySpeedIncrement: 0.2, | |
| colors: { | |
| player: '#00f3ff', | |
| target: '#00ff88', | |
| enemy: '#ff0055', | |
| particle: '#ffffff' | |
| } | |
| }; | |
| // Dimensions du canevas | |
| let width, height; | |
| function resize() { | |
| width = window.innerWidth; | |
| height = window.innerHeight; | |
| canvas.width = width; | |
| canvas.height = height; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| // --- Classes du Jeu --- | |
| class Player { | |
| constructor() { | |
| this.x = width / 2; | |
| this.y = height / 2; | |
| this.radius = 15; | |
| this.targetX = this.x; | |
| this.targetY = this.y; | |
| this.trail = []; // Queue visuelle | |
| } | |
| update(mouseX, mouseY) { | |
| // Lissage du mouvement (Lerp) | |
| if (mouseX !== undefined && mouseY !== undefined) { | |
| this.targetX = mouseX; | |
| this.targetY = mouseY; | |
| } | |
| this.x += (this.targetX - this.x) * config.playerSpeed; | |
| this.y += (this.targetY - this.y) * config.playerSpeed; | |
| // Ajout à la traînée | |
| this.trail.push({ x: this.x, y: this.y, alpha: 1.0 }); | |
| if (this.trail.length > 20) { | |
| this.trail.shift(); | |
| } | |
| // Mise à jour de l'alpha de la traînée | |
| this.trail.forEach(t => t.alpha -= 0.05); | |
| } | |
| draw() { | |
| // Dessin de la traînée | |
| ctx.save(); | |
| for (let i = 0; i < this.trail.length; i++) { | |
| const point = this.trail[i]; | |
| ctx.beginPath(); | |
| ctx.arc(point.x, point.y, this.radius * (i / this.trail.length), 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(0, 243, 255, ${point.alpha * 0.5})`; | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| // Dessin du joueur | |
| ctx.save(); | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = config.colors.player; | |
| ctx.fillStyle = config.colors.player; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Noyau blanc | |
| ctx.fillStyle = '#fff'; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius * 0.4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| class Target { | |
| constructor() { | |
| this.radius = 12; | |
| this.respawn(); | |
| this.pulse = 0; | |
| } | |
| respawn() { | |
| // Marge de sécurité pour ne pas apparaître sur les bords | |
| const margin = 50; | |
| this.x = margin + Math.random() * (width - margin * 2); | |
| this.y = margin + Math.random() * (height - margin * 2); | |
| } | |
| update() { | |
| this.pulse += 0.1; | |
| } | |
| draw() { | |
| const scale = 1 + Math.sin(this.pulse) * 0.1; | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.scale(scale, scale); | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = config.colors.target; | |
| ctx.fillStyle = config.colors.target; | |
| // Forme de losange | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -this.radius); | |
| ctx.lineTo(this.radius, 0); | |
| ctx.lineTo(0, this.radius); | |
| ctx.lineTo(-this.radius, 0); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| class Enemy { | |
| constructor() { | |
| this.radius = 18; | |
| this.resetPosition(); | |
| this.angle = Math.random() * Math.PI * 2; | |
| this.speed = config.enemyBaseSpeed + (score * config.enemySpeedIncrement); | |
| } | |
| resetPosition() { | |
| // Apparaît en dehors de l'écran | |
| if (Math.random() < 0.5) { | |
| this.x = Math.random() < 0.5 ? -50 : width + 50; | |
| this.y = Math.random() * height; | |
| } else { | |
| this.x = Math.random() * width; | |
| this.y = Math.random() < 0.5 ? -50 : height + 50; | |
| } | |
| } | |
| update(playerX, playerY) { | |
| // Suit le joueur | |
| const dx = playerX - this.x; | |
| const dy = playerY - this.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| this.x += (dx / distance) * this.speed; | |
| this.y += (dy / distance) * this.speed; | |
| // Rotation visuelle | |
| this.angle += 0.05; | |
| } | |
| draw() { | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.angle); | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = config.colors.enemy; | |
| ctx.fillStyle = config.colors.enemy; | |
| // Forme de triangle | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -this.radius); | |
| ctx.lineTo(this.radius, this.radius); | |
| ctx.lineTo(-this.radius, this.radius); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| class Particle { | |
| constructor(x, y, color) { | |
| this.x = x; | |
| this.y = y; | |
| this.color = color; | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = Math.random() * 4 + 1; | |
| this.vx = Math.cos(angle) * speed; | |
| this.vy = Math.sin(angle) * speed; | |
| this.life = 1.0; | |
| this.decay = Math.random() * 0.03 + 0.02; | |
| } | |
| update() { | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| this.life -= this.decay; | |
| } | |
| draw() { | |
| ctx.save(); | |
| ctx.globalAlpha = this.life; | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| // --- Gestion du Jeu --- | |
| let player; | |
| let target; | |
| let enemies = []; | |
| let particles = []; | |
| let mouseX = width / 2; | |
| let mouseY = height / 2; | |
| function init() { | |
| player = new Player(); | |
| target = new Target(); | |
| enemies = []; | |
| particles = []; | |
| score = 0; | |
| updateScoreUI(); | |
| // Spawn initial d'ennemis | |
| spawnEnemy(); | |
| } | |
| function spawnEnemy() { | |
| enemies.push(new Enemy()); | |
| } | |
| function createExplosion(x, y, color) { | |
| for (let i = 0; i < 15; i++) { | |
| particles.push(new Particle(x, y, color)); | |
| } | |
| } | |
| function updateScoreUI() { | |
| currentScoreEl.textContent = score; | |
| highScoreEl.textContent = highScore; | |
| } | |
| function gameOver() { | |
| gameRunning = false; | |
| cancelAnimationFrame(animationId); | |
| if (score > highScore) { | |
| highScore = score; | |
| localStorage.setItem('catchUpHighScore', highScore); | |
| } | |
| finalScoreEl.textContent = score; | |
| finalHighScoreEl.textContent = highScore; | |
| hud.classList.add('hidden'); | |
| gameOverScreen.classList.remove('hidden'); | |
| } | |
| function gameLoop() { | |
| if (!gameRunning) return; | |
| // Nettoyage de l'écran avec une traînée légère | |
| ctx.fillStyle = 'rgba(11, 12, 21, 0.3)'; | |
| ctx.fillRect(0, 0, width, height); | |
| // Effet de grille subtil en arrière-plan | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)'; | |
| ctx.lineWidth = 1; | |
| const gridSize = 50; | |
| const offset = (frameCount * 0.5) % gridSize; | |
| ctx.beginPath(); | |
| // Lignes verticales | |
| for (let x = offset; x < width; x += gridSize) { | |
| ctx.moveTo(x, 0); | |
| ctx.lineTo(x, height); | |
| } | |
| // Lignes horizontales | |
| for (let y = offset; y < height; y += gridSize) { | |
| ctx.moveTo(0, y); | |
| ctx.lineTo(width, y); | |
| } | |
| ctx.stroke(); | |
| // Logique Joueur | |
| player.update(mouseX, mouseY); | |
| player.draw(); | |
| // Logique Cible | |
| target.update(); | |
| target.draw(); | |
| // Collision Joueur - Cible | |
| const distToTarget = Math.hypot(player.x - target.x, player.y - target.y); | |
| if (distToTarget < player.radius + target.radius) { | |
| score++; | |
| createExplosion(target.x, target.y, config.colors.target); | |
| updateScoreUI(); | |
| target.respawn(); | |
| // Difficulté : Ajouter un ennemi tous les 3 points | |
| if (score % 3 === 0) { | |
| spawnEnemy(); | |
| } | |
| } | |
| // Logique Ennemis | |
| enemies.forEach(enemy => { | |
| enemy.update(player.x, player.y); | |
| enemy.draw(); | |
| // Collision Joueur - Ennemi | |
| const distToEnemy = Math.hypot(player.x - enemy.x, player.y - enemy.y); | |
| if (distToEnemy < player.radius + enemy.radius - 5) { // -5 pour hitbox plus indulgente | |
| createExplosion(player.x, player.y, config.colors.player); | |
| gameOver(); | |
| } | |
| }); | |
| // Logique Particules | |
| particles.forEach((p, index) => { | |
| p.update(); | |
| p.draw(); | |
| if (p.life <= 0) { | |
| particles.splice(index, 1); | |
| } | |
| }); | |
| frameCount++; | |
| animationId = requestAnimationFrame(gameLoop); | |
| } | |
| // --- Entrées Utilisateur --- | |
| // Souris | |
| window.addEventListener('mousemove', (e) => { | |
| if (gameRunning) { | |
| mouseX = e.clientX; | |
| mouseY = e.clientY; | |
| } | |
| }); | |
| // Tactile | |
| window.addEventListener('touchmove', (e) => { | |
| if (gameRunning) { | |
| e.preventDefault(); // Empêche le scroll | |
| mouseX = e.touches[0].clientX; | |
| mouseY = e.touches[0].clientY; | |
| } | |
| }, { passive: false }); | |
| window.addEventListener('touchstart', (e) => { | |
| if (gameRunning) { | |
| mouseX = e.touches[0].clientX; | |
| mouseY = e.touches[0].clientY; | |
| } | |
| }, { passive: false }); | |
| // Boutons | |
| startBtn.addEventListener('click', () => { | |
| startScreen.classList.add('hidden'); | |
| hud.classList.remove('hidden'); | |
| init(); | |
| gameRunning = true; | |
| mouseX = width / 2; // Réinitialiser la position de la souris virtuelle | |
| mouseY = height / 2; | |
| gameLoop(); | |
| }); | |
| restartBtn.addEventListener('click', () => { | |
| gameOverScreen.classList.add('hidden'); | |
| hud.classList.remove('hidden'); | |
| init(); | |
| gameRunning = true; | |
| mouseX = width / 2; | |
| mouseY = height / 2; | |
| gameLoop(); | |
| }); | |
| // Initialisation de l'affichage High Score | |
| highScoreEl.textContent = highScore; | |
| </script> | |
| </body> | |
| </html> |