Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Drop Dodger - Rhythm Dodge Game</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Rubik:wght@400;700;900&display=swap'); | |
:root { | |
--primary: #6c5ce7; | |
--secondary: #00cec9; | |
--danger: #ff7675; | |
--dark: #2d3436; | |
--light: #f5f6fa; | |
} | |
body { | |
font-family: 'Rubik', sans-serif; | |
background-color: var(--dark); | |
color: var(--light); | |
overflow: hidden; | |
touch-action: manipulation; | |
} | |
.game-font { | |
font-family: 'Press Start 2P', cursive; | |
} | |
.game-container { | |
perspective: 1000px; | |
} | |
.game-lane { | |
transition: transform 0.1s ease-out; | |
} | |
.player-dot { | |
box-shadow: 0 0 15px 5px rgba(108, 92, 231, 0.7); | |
animation: pulse 1.5s infinite alternate; | |
} | |
.obstacle { | |
transition: transform 0.05s linear; | |
will-change: transform; | |
} | |
.beat-explosion { | |
position: absolute; | |
border-radius: 50%; | |
background: radial-gradient(circle, var(--secondary), transparent 70%); | |
transform: translate(-50%, -50%); | |
pointer-events: none; | |
z-index: 10; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); opacity: 1; } | |
100% { transform: scale(1.2); opacity: 0.8; } | |
} | |
@keyframes laneSwitch { | |
0% { transform: translateX(0); } | |
50% { transform: translateX(5px); } | |
100% { transform: translateX(0); } | |
} | |
.lane-switch-animation { | |
animation: laneSwitch 0.2s ease-out; | |
} | |
.progress-bar { | |
background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
height: 5px; | |
transition: width 0.1s linear; | |
} | |
.combo-pop { | |
animation: comboPop 0.3s ease-out; | |
transform-origin: center; | |
} | |
@keyframes comboPop { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.5); } | |
100% { transform: scale(1); } | |
} | |
.game-over-screen { | |
background: rgba(45, 52, 54, 0.9); | |
backdrop-filter: blur(5px); | |
transition: opacity 0.5s ease; | |
} | |
.intro-screen { | |
background: rgba(45, 52, 54, 0.95); | |
backdrop-filter: blur(5px); | |
transition: opacity 0.5s ease; | |
} | |
.demo-obstacle { | |
animation: demoObstacle 2s linear infinite; | |
} | |
@keyframes demoObstacle { | |
0% { transform: translateY(-50px); } | |
100% { transform: translateY(calc(100vh - 100px)); } | |
} | |
.demo-player { | |
animation: demoPlayer 2s ease-in-out infinite; | |
} | |
@keyframes demoPlayer { | |
0%, 100% { left: 25%; } | |
50% { left: 75%; } | |
} | |
.countdown { | |
animation: countdownPop 0.5s ease-out; | |
} | |
@keyframes countdownPop { | |
0% { transform: scale(0.5); opacity: 0; } | |
80% { transform: scale(1.2); opacity: 1; } | |
100% { transform: scale(1); opacity: 1; } | |
} | |
.tutorial-highlight { | |
animation: tutorialHighlight 1.5s ease-in-out infinite alternate; | |
} | |
@keyframes tutorialHighlight { | |
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(108, 92, 231, 0); } | |
100% { transform: scale(1.05); box-shadow: 0 0 20px 10px rgba(108, 92, 231, 0.4); } | |
} | |
</style> | |
</head> | |
<body class="h-screen flex flex-col"> | |
<!-- Header Section --> | |
<header class="p-4 flex justify-between items-center bg-gray-900/50 border-b border-gray-700"> | |
<div class="flex items-center space-x-2"> | |
<i class="fas fa-gamepad text-2xl text-purple-500"></i> | |
<h1 class="game-font text-xl md:text-2xl text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400"> | |
DROP DODGER | |
</h1> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<div class="hidden md:flex items-center space-x-2"> | |
<i class="fas fa-volume-up text-cyan-400"></i> | |
<input type="range" min="0" max="100" value="70" class="w-20 accent-cyan-400"> | |
</div> | |
<button id="pause-btn" class="px-3 py-1 rounded-full bg-gray-700 hover:bg-gray-600 transition"> | |
<i class="fas fa-pause"></i> | |
</button> | |
</div> | |
</header> | |
<!-- Game Stats --> | |
<div class="p-4 flex justify-between items-center"> | |
<div class="flex items-center space-x-4"> | |
<div class="flex items-center space-x-2"> | |
<i class="fas fa-bolt text-yellow-400"></i> | |
<span id="score" class="game-font">0</span> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<i class="fas fa-fire text-red-400"></i> | |
<span id="combo" class="game-font">0x</span> | |
</div> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<i class="fas fa-tachometer-alt text-green-400"></i> | |
<span id="speed" class="game-font">1.0x</span> | |
</div> | |
</div> | |
<!-- Progress Bar --> | |
<div class="px-4"> | |
<div class="h-1 bg-gray-700 rounded-full overflow-hidden"> | |
<div id="progress-bar" class="progress-bar rounded-full" style="width: 0%"></div> | |
</div> | |
</div> | |
<!-- Game Container --> | |
<div class="game-container flex-1 relative overflow-hidden"> | |
<div id="game-area" class="h-full w-full relative"> | |
<!-- Lanes --> | |
<div class="absolute top-0 left-0 w-full h-full flex"> | |
<div id="left-lane" class="game-lane w-1/2 h-full border-r border-gray-700/50"></div> | |
<div id="right-lane" class="game-lane w-1/2 h-full"></div> | |
</div> | |
<!-- Player --> | |
<div id="player" class="player-dot absolute w-6 h-6 rounded-full bg-gradient-to-br from-purple-500 to-cyan-400 left-1/4 bottom-20 transform -translate-x-1/2 z-10"></div> | |
<!-- Obstacles will be added here dynamically --> | |
</div> | |
<!-- Intro Screen --> | |
<div id="intro-screen" class="intro-screen absolute inset-0 flex flex-col items-center justify-center z-30"> | |
<div class="text-center p-8 rounded-xl bg-gray-800/90 border border-gray-700 max-w-md w-full"> | |
<h2 class="game-font text-3xl mb-6 text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400"> | |
DROP DODGER | |
</h2> | |
<!-- Demo Area --> | |
<div class="relative h-40 mb-6 bg-gray-900 rounded-lg overflow-hidden"> | |
<div class="absolute top-0 left-0 w-full h-full flex"> | |
<div class="w-1/2 h-full border-r border-gray-700/50"></div> | |
<div class="w-1/2 h-full"></div> | |
</div> | |
<!-- Demo Player --> | |
<div class="demo-player absolute w-4 h-4 rounded-full bg-gradient-to-br from-purple-500 to-cyan-400 bottom-5 transform -translate-x-1/2 z-10"></div> | |
<!-- Demo Obstacles --> | |
<div class="demo-obstacle absolute w-1/2 h-4 left-0 top-0 bg-gradient-to-b from-red-500 to-pink-500 rounded-b-lg" style="animation-delay: 0.5s;"></div> | |
<div class="demo-obstacle absolute w-1/2 h-4 right-0 top-0 bg-gradient-to-b from-blue-500 to-cyan-400 rounded-b-lg" style="animation-delay: 1s;"></div> | |
<div class="demo-obstacle absolute w-1/2 h-4 left-0 top-0 bg-gradient-to-b from-red-500 to-pink-500 rounded-b-lg" style="animation-delay: 1.5s;"></div> | |
</div> | |
<div class="mb-6 text-left space-y-3"> | |
<div class="flex items-start space-x-3"> | |
<div class="flex-shrink-0 mt-1"> | |
<i class="fas fa-hand-pointer text-purple-400"></i> | |
</div> | |
<p>TAP anywhere to switch lanes and dodge obstacles</p> | |
</div> | |
<div class="flex items-start space-x-3"> | |
<div class="flex-shrink-0 mt-1"> | |
<i class="fas fa-bolt text-yellow-400"></i> | |
</div> | |
<p>Score increases as you dodge obstacles</p> | |
</div> | |
<div class="flex items-start space-x-3"> | |
<div class="flex-shrink-0 mt-1"> | |
<i class="fas fa-tachometer-alt text-green-400"></i> | |
</div> | |
<p>Game speeds up as you progress</p> | |
</div> | |
</div> | |
<button id="start-btn" class="tutorial-highlight px-6 py-3 rounded-full bg-gradient-to-r from-purple-500 to-cyan-500 hover:from-purple-600 hover:to-cyan-600 transition transform hover:scale-105 w-full"> | |
<i class="fas fa-play mr-2"></i> START GAME | |
</button> | |
</div> | |
</div> | |
<!-- Countdown Screen --> | |
<div id="countdown-screen" class="absolute inset-0 bg-gray-900/80 flex items-center justify-center opacity-0 pointer-events-none z-20"> | |
<div class="text-center"> | |
<div id="countdown" class="game-font text-8xl text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">3</div> | |
</div> | |
</div> | |
<!-- Game Over Screen --> | |
<div id="game-over-screen" class="game-over-screen absolute inset-0 flex flex-col items-center justify-center opacity-0 pointer-events-none z-20"> | |
<div class="text-center p-8 rounded-xl bg-gray-800/90 border border-gray-700 max-w-md w-full"> | |
<h2 class="game-font text-3xl mb-4 text-transparent bg-clip-text bg-gradient-to-r from-red-400 to-purple-500"> | |
GAME OVER | |
</h2> | |
<div class="space-y-4 mb-6"> | |
<div class="flex justify-between"> | |
<span class="text-gray-400">Score:</span> | |
<span id="final-score" class="game-font text-xl">0</span> | |
</div> | |
<div class="flex justify-between"> | |
<span class="text-gray-400">Max Combo:</span> | |
<span id="final-combo" class="game-font text-xl">0x</span> | |
</div> | |
<div class="flex justify-between"> | |
<span class="text-gray-400">Difficulty:</span> | |
<span id="final-difficulty" class="game-font text-xl">1.0x</span> | |
</div> | |
</div> | |
<div class="flex justify-center space-x-4"> | |
<button id="restart-btn" class="px-6 py-2 rounded-full bg-gradient-to-r from-purple-500 to-cyan-500 hover:from-purple-600 hover:to-cyan-600 transition transform hover:scale-105"> | |
<i class="fas fa-redo mr-2"></i> Play Again | |
</button> | |
<button id="menu-btn" class="px-6 py-2 rounded-full bg-gray-700 hover:bg-gray-600 transition"> | |
<i class="fas fa-home mr-2"></i> Menu | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Pause Screen --> | |
<div id="pause-screen" class="absolute inset-0 bg-gray-900/80 flex items-center justify-center opacity-0 pointer-events-none z-20"> | |
<div class="text-center p-8 rounded-xl bg-gray-800 border border-gray-700 max-w-md w-full"> | |
<h2 class="game-font text-3xl mb-6 text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-cyan-400"> | |
PAUSED | |
</h2> | |
<div class="flex justify-center space-x-4"> | |
<button id="resume-btn" class="px-6 py-2 rounded-full bg-gradient-to-r from-purple-500 to-cyan-500 hover:from-purple-600 hover:to-cyan-600 transition transform hover:scale-105"> | |
<i class="fas fa-play mr-2"></i> Resume | |
</button> | |
<button id="quit-btn" class="px-6 py-2 rounded-full bg-gray-700 hover:bg-gray-600 transition"> | |
<i class="fas fa-sign-out-alt mr-2"></i> Quit | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Controls Info --> | |
<div class="p-4 text-center text-sm text-gray-400 border-t border-gray-700/50"> | |
<p>TAP anywhere to switch lanes • Dodge the obstacles to the beat!</p> | |
</div> | |
<script> | |
// Game State | |
const gameState = { | |
score: 0, | |
combo: 0, | |
maxCombo: 0, | |
speed: 1.0, | |
isPlaying: false, | |
isPaused: false, | |
gameOver: false, | |
playerPosition: 'left', // 'left' or 'right' | |
obstacleInterval: null, | |
speedInterval: null, | |
lastObstacleTime: 0, | |
obstacles: [], | |
beatExplosions: [], | |
difficultyCurve: [ | |
{ threshold: 0, speed: 1.0, spawnRate: 1500 }, | |
{ threshold: 20, speed: 1.2, spawnRate: 1300 }, | |
{ threshold: 50, speed: 1.5, spawnRate: 1000 }, | |
{ threshold: 100, speed: 1.8, spawnRate: 800 }, | |
{ threshold: 200, speed: 2.2, spawnRate: 600 }, | |
{ threshold: 500, speed: 2.8, spawnRate: 400 } | |
], | |
currentDifficulty: 0, | |
isIntro: true | |
}; | |
// DOM Elements | |
const elements = { | |
gameArea: document.getElementById('game-area'), | |
leftLane: document.getElementById('left-lane'), | |
rightLane: document.getElementById('right-lane'), | |
player: document.getElementById('player'), | |
scoreEl: document.getElementById('score'), | |
comboEl: document.getElementById('combo'), | |
speedEl: document.getElementById('speed'), | |
progressBar: document.getElementById('progress-bar'), | |
gameOverScreen: document.getElementById('game-over-screen'), | |
finalScore: document.getElementById('final-score'), | |
finalCombo: document.getElementById('final-combo'), | |
finalDifficulty: document.getElementById('final-difficulty'), | |
pauseScreen: document.getElementById('pause-screen'), | |
pauseBtn: document.getElementById('pause-btn'), | |
resumeBtn: document.getElementById('resume-btn'), | |
quitBtn: document.getElementById('quit-btn'), | |
restartBtn: document.getElementById('restart-btn'), | |
menuBtn: document.getElementById('menu-btn'), | |
introScreen: document.getElementById('intro-screen'), | |
startBtn: document.getElementById('start-btn'), | |
countdownScreen: document.getElementById('countdown-screen'), | |
countdown: document.getElementById('countdown') | |
}; | |
// Initialize game | |
function initGame() { | |
resetGameState(); | |
setupEventListeners(); | |
// Show intro screen by default | |
elements.introScreen.style.display = 'flex'; | |
gameState.isIntro = true; | |
} | |
// Reset game state | |
function resetGameState() { | |
gameState.score = 0; | |
gameState.combo = 0; | |
gameState.maxCombo = 0; | |
gameState.speed = 1.0; | |
gameState.isPlaying = false; | |
gameState.isPaused = false; | |
gameState.gameOver = false; | |
gameState.playerPosition = 'left'; | |
gameState.obstacles = []; | |
gameState.beatExplosions = []; | |
gameState.currentDifficulty = 0; | |
// Clear all obstacles | |
document.querySelectorAll('.obstacle').forEach(el => el.remove()); | |
document.querySelectorAll('.beat-explosion').forEach(el => el.remove()); | |
// Reset UI | |
updateScore(); | |
updateCombo(); | |
updateSpeed(); | |
elements.progressBar.style.width = '0%'; | |
elements.gameOverScreen.style.opacity = '0'; | |
elements.gameOverScreen.style.pointerEvents = 'none'; | |
elements.pauseScreen.style.opacity = '0'; | |
elements.pauseScreen.style.pointerEvents = 'none'; | |
elements.countdownScreen.style.opacity = '0'; | |
elements.countdownScreen.style.pointerEvents = 'none'; | |
// Reset player position and visibility | |
elements.player.style.left = '25%'; | |
elements.player.style.opacity = '1'; | |
elements.player.classList.remove('lane-switch-animation'); | |
void elements.player.offsetWidth; // Trigger reflow | |
} | |
// Start game with countdown | |
function startGameWithCountdown() { | |
gameState.isIntro = false; | |
elements.introScreen.style.display = 'none'; | |
elements.countdownScreen.style.opacity = '1'; | |
elements.countdownScreen.style.pointerEvents = 'auto'; | |
let count = 3; | |
elements.countdown.textContent = count; | |
elements.countdown.classList.add('countdown'); | |
const countdownInterval = setInterval(() => { | |
count--; | |
if (count > 0) { | |
elements.countdown.textContent = count; | |
elements.countdown.classList.remove('countdown'); | |
void elements.countdown.offsetWidth; | |
elements.countdown.classList.add('countdown'); | |
} else { | |
clearInterval(countdownInterval); | |
elements.countdownScreen.style.opacity = '0'; | |
elements.countdownScreen.style.pointerEvents = 'none'; | |
startGame(); | |
} | |
}, 1000); | |
} | |
// Start game | |
function startGame() { | |
gameState.isPlaying = true; | |
// Start obstacle spawner | |
gameState.obstacleInterval = setInterval(spawnObstacle, gameState.difficultyCurve[0].spawnRate); | |
// Start difficulty progression | |
gameState.speedInterval = setInterval(updateDifficulty, 1000); | |
// Start game loop | |
requestAnimationFrame(gameLoop); | |
} | |
// Game loop | |
function gameLoop(timestamp) { | |
if (gameState.isPaused || gameState.gameOver) return; | |
// Move obstacles | |
moveObstacles(); | |
// Check collisions | |
checkCollisions(); | |
// Update beat explosions | |
updateBeatExplosions(); | |
// Continue loop | |
requestAnimationFrame(gameLoop); | |
} | |
// Spawn obstacle | |
function spawnObstacle() { | |
if (gameState.isPaused || gameState.gameOver) return; | |
// Make first few obstacles easier (only one lane at a time) | |
if (gameState.score < 30 && Math.random() > 0.3) { | |
return; | |
} | |
const lane = Math.random() > 0.5 ? 'right' : 'left'; | |
const obstacle = document.createElement('div'); | |
obstacle.className = `obstacle absolute w-1/2 h-8 ${lane === 'left' ? 'left-0' : 'right-0'} top-0 bg-gradient-to-b ${lane === 'left' ? 'from-red-500 to-pink-500' : 'from-blue-500 to-cyan-400'} rounded-b-lg`; | |
obstacle.dataset.lane = lane; | |
elements.gameArea.appendChild(obstacle); | |
gameState.obstacles.push({ | |
element: obstacle, | |
lane: lane, | |
y: 0, | |
speed: 5 * gameState.speed | |
}); | |
// Create beat explosion at top | |
createBeatExplosion(lane === 'left' ? '25%' : '75%', '10%'); | |
} | |
// Move obstacles | |
function moveObstacles() { | |
gameState.obstacles = gameState.obstacles.filter(obstacle => { | |
obstacle.y += obstacle.speed; | |
obstacle.element.style.transform = `translateY(${obstacle.y}px)`; | |
// Remove if out of screen | |
if (obstacle.y > elements.gameArea.offsetHeight) { | |
obstacle.element.remove(); | |
// If obstacle passes safely, increase combo | |
if ((gameState.playerPosition === 'left' && obstacle.lane === 'right') || | |
(gameState.playerPosition === 'right' && obstacle.lane === 'left')) { | |
gameState.combo += 1; | |
if (gameState.combo > gameState.maxCombo) { | |
gameState.maxCombo = gameState.combo; | |
} | |
updateCombo(); | |
} | |
return false; | |
} | |
return true; | |
}); | |
} | |
// Check collisions | |
function checkCollisions() { | |
const playerRect = elements.player.getBoundingClientRect(); | |
const gameAreaRect = elements.gameArea.getBoundingClientRect(); | |
const playerTop = playerRect.top - gameAreaRect.top; | |
const playerBottom = playerRect.bottom - gameAreaRect.top; | |
const playerLeft = playerRect.left - gameAreaRect.left; | |
const playerRight = playerRect.right - gameAreaRect.left; | |
for (const obstacle of gameState.obstacles) { | |
const obstacleRect = obstacle.element.getBoundingClientRect(); | |
const obstacleTop = obstacleRect.top - gameAreaRect.top; | |
const obstacleBottom = obstacleRect.bottom - gameAreaRect.top; | |
const obstacleLeft = obstacleRect.left - gameAreaRect.left; | |
const obstacleRight = obstacleRect.right - gameAreaRect.left; | |
// Check if player and obstacle overlap | |
if (playerBottom > obstacleTop && playerTop < obstacleBottom && | |
playerRight > obstacleLeft && playerLeft < obstacleRight) { | |
// Check if in same lane | |
if ((gameState.playerPosition === 'left' && obstacle.lane === 'left') || | |
(gameState.playerPosition === 'right' && obstacle.lane === 'right')) { | |
gameOver(); | |
return; | |
} else { | |
// Successful dodge - increase score and combo | |
gameState.score += Math.floor(10 * gameState.speed); | |
gameState.combo += 1; | |
if (gameState.combo > gameState.maxCombo) { | |
gameState.maxCombo = gameState.combo; | |
} | |
// Create beat explosion | |
createBeatExplosion( | |
obstacle.lane === 'left' ? '25%' : '75%', | |
`${obstacleTop + obstacleRect.height / 2}px` | |
); | |
// Remove obstacle | |
obstacle.element.remove(); | |
return false; | |
} | |
} | |
} | |
// Update UI if no collision | |
updateScore(); | |
updateCombo(); | |
} | |
// Create beat explosion | |
function createBeatExplosion(x, y) { | |
const explosion = document.createElement('div'); | |
explosion.className = 'beat-explosion'; | |
explosion.style.left = x; | |
explosion.style.top = y; | |
explosion.style.width = '100px'; | |
explosion.style.height = '100px'; | |
explosion.style.opacity = '0.8'; | |
elements.gameArea.appendChild(explosion); | |
gameState.beatExplosions.push({ | |
element: explosion, | |
size: 100, | |
opacity: 0.8 | |
}); | |
} | |
// Update beat explosions | |
function updateBeatExplosions() { | |
gameState.beatExplosions = gameState.beatExplosions.filter(explosion => { | |
explosion.size += 5; | |
explosion.opacity -= 0.02; | |
explosion.element.style.width = `${explosion.size}px`; | |
explosion.element.style.height = `${explosion.size}px`; | |
explosion.element.style.opacity = explosion.opacity; | |
// Remove if invisible | |
if (explosion.opacity <= 0) { | |
explosion.element.remove(); | |
return false; | |
} | |
return true; | |
}); | |
} | |
// Update difficulty | |
function updateDifficulty() { | |
if (gameState.isPaused || gameState.gameOver) return; | |
// Check if we should increase difficulty | |
const nextDifficulty = gameState.currentDifficulty + 1; | |
if (nextDifficulty < gameState.difficultyCurve.length && | |
gameState.score >= gameState.difficultyCurve[nextDifficulty].threshold) { | |
gameState.currentDifficulty = nextDifficulty; | |
gameState.speed = gameState.difficultyCurve[gameState.currentDifficulty].speed; | |
// Update spawn rate | |
clearInterval(gameState.obstacleInterval); | |
gameState.obstacleInterval = setInterval( | |
spawnObstacle, | |
gameState.difficultyCurve[gameState.currentDifficulty].spawnRate | |
); | |
updateSpeed(); | |
} | |
// Update progress bar (0-100% based on score to next level) | |
let progress = 0; | |
if (gameState.currentDifficulty < gameState.difficultyCurve.length - 1) { | |
const currentThreshold = gameState.difficultyCurve[gameState.currentDifficulty].threshold; | |
const nextThreshold = gameState.difficultyCurve[gameState.currentDifficulty + 1].threshold; | |
progress = ((gameState.score - currentThreshold) / (nextThreshold - currentThreshold)) * 100; | |
} else { | |
progress = 100; | |
} | |
elements.progressBar.style.width = `${Math.min(100, progress)}%`; | |
} | |
// Switch player lane | |
function switchLane() { | |
if (gameState.isPaused || gameState.gameOver || gameState.isIntro) return; | |
gameState.playerPosition = gameState.playerPosition === 'left' ? 'right' : 'left'; | |
elements.player.style.left = gameState.playerPosition === 'left' ? '25%' : '75%'; | |
// Add animation | |
elements.player.classList.remove('lane-switch-animation'); | |
void elements.player.offsetWidth; // Trigger reflow | |
elements.player.classList.add('lane-switch-animation'); | |
// Create small beat explosion at player position | |
const playerRect = elements.player.getBoundingClientRect(); | |
const gameAreaRect = elements.gameArea.getBoundingClientRect(); | |
const y = playerRect.top - gameAreaRect.top + playerRect.height / 2; | |
createBeatExplosion(gameState.playerPosition === 'left' ? '25%' : '75%', `${y}px`); | |
} | |
// Update score display | |
function updateScore() { | |
elements.scoreEl.textContent = gameState.score; | |
} | |
// Update combo display | |
function updateCombo() { | |
elements.comboEl.textContent = `${gameState.combo}x`; | |
// Add pop animation for combos over 5 | |
if (gameState.combo >= 5) { | |
elements.comboEl.classList.add('combo-pop'); | |
setTimeout(() => { | |
elements.comboEl.classList.remove('combo-pop'); | |
}, 300); | |
} | |
} | |
// Update speed display | |
function updateSpeed() { | |
elements.speedEl.textContent = `${gameState.speed.toFixed(1)}x`; | |
} | |
// Game over | |
function gameOver() { | |
gameState.isPlaying = false; | |
gameState.gameOver = true; | |
clearInterval(gameState.obstacleInterval); | |
clearInterval(gameState.speedInterval); | |
// Show game over screen | |
elements.finalScore.textContent = gameState.score; | |
elements.finalCombo.textContent = `${gameState.maxCombo}x`; | |
elements.finalDifficulty.textContent = `${gameState.speed.toFixed(1)}x`; | |
elements.gameOverScreen.style.opacity = '1'; | |
elements.gameOverScreen.style.pointerEvents = 'auto'; | |
// Create explosion at player position | |
const playerRect = elements.player.getBoundingClientRect(); | |
const gameAreaRect = elements.gameArea.getBoundingClientRect(); | |
const x = gameState.playerPosition === 'left' ? '25%' : '75%'; | |
const y = playerRect.top - gameAreaRect.top + playerRect.height / 2; | |
const bigExplosion = document.createElement('div'); | |
bigExplosion.className = 'beat-explosion'; | |
bigExplosion.style.left = x; | |
bigExplosion.style.top = `${y}px`; | |
bigExplosion.style.width = '200px'; | |
bigExplosion.style.height = '200px'; | |
bigExplosion.style.background = 'radial-gradient(circle, var(--danger), transparent 70%)'; | |
bigExplosion.style.opacity = '0.9'; | |
elements.gameArea.appendChild(bigExplosion); | |
// Make player disappear | |
elements.player.style.opacity = '0'; | |
} | |
// Pause game | |
function pauseGame() { | |
if (!gameState.isPlaying || gameState.gameOver || gameState.isIntro) return; | |
gameState.isPaused = true; | |
elements.pauseScreen.style.opacity = '1'; | |
elements.pauseScreen.style.pointerEvents = 'auto'; | |
} | |
// Resume game | |
function resumeGame() { | |
if (!gameState.isPaused) return; | |
gameState.isPaused = false; | |
elements.pauseScreen.style.opacity = '0'; | |
elements.pauseScreen.style.pointerEvents = 'none'; | |
// Restart game loop | |
requestAnimationFrame(gameLoop); | |
} | |
// Setup event listeners | |
function setupEventListeners() { | |
// Lane switch on tap/click | |
elements.gameArea.addEventListener('click', switchLane); | |
elements.gameArea.addEventListener('touchstart', (e) => { | |
e.preventDefault(); | |
switchLane(); | |
}); | |
// Start button | |
elements.startBtn.addEventListener('click', startGameWithCountdown); | |
// Pause button | |
elements.pauseBtn.addEventListener('click', pauseGame); | |
// Pause screen buttons | |
elements.resumeBtn.addEventListener('click', resumeGame); | |
elements.quitBtn.addEventListener('click', () => { | |
resetGameState(); | |
elements.introScreen.style.display = 'flex'; | |
gameState.isIntro = true; | |
}); | |
// Game over screen buttons | |
elements.restartBtn.addEventListener('click', () => { | |
resetGameState(); | |
startGameWithCountdown(); | |
}); | |
elements.menuBtn.addEventListener('click', () => { | |
resetGameState(); | |
elements.introScreen.style.display = 'flex'; | |
gameState.isIntro = true; | |
}); | |
// Pause on window blur | |
window.addEventListener('blur', pauseGame); | |
} | |
// Start the game when page loads | |
window.addEventListener('DOMContentLoaded', initGame); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LukasBe/drop-dodger-game" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |