Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>PaperDash Express</title> | |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
| body { | |
| font-family: 'Press Start 2P', cursive; | |
| overflow: hidden; | |
| touch-action: none; | |
| background-color: #0f172a; | |
| color: #f8fafc; | |
| } | |
| .game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .player { | |
| position: absolute; | |
| transition: transform 0.1s ease-out; | |
| z-index: 10; | |
| } | |
| .obstacle { | |
| position: absolute; | |
| z-index: 5; | |
| } | |
| .paper { | |
| position: absolute; | |
| z-index: 8; | |
| transition: transform 0.5s linear, opacity 0.5s ease-out; | |
| } | |
| .house { | |
| position: absolute; | |
| z-index: 3; | |
| } | |
| .parallax-bg { | |
| position: absolute; | |
| width: 300%; | |
| height: 100%; | |
| background-repeat: repeat-x; | |
| z-index: 1; | |
| } | |
| .score-popup { | |
| position: absolute; | |
| animation: floatUp 1s forwards; | |
| opacity: 0; | |
| z-index: 20; | |
| } | |
| @keyframes floatUp { | |
| 0% { transform: translateY(0); opacity: 1; } | |
| 100% { transform: translateY(-100px); opacity: 0; } | |
| } | |
| .neon-text { | |
| text-shadow: 0 0 10px #3b82f6, 0 0 20px #3b82f6, 0 0 30px #3b82f6; | |
| } | |
| .menu-btn { | |
| transition: all 0.2s ease; | |
| } | |
| .menu-btn:hover { | |
| transform: scale(1.05); | |
| text-shadow: 0 0 10px #3b82f6; | |
| } | |
| </style> | |
| </head> | |
| <body class="select-none"> | |
| <div id="main-menu" class="fixed inset-0 flex flex-col items-center justify-center bg-gradient-to-b from-blue-900 to-gray-900 z-50 transition-opacity duration-500"> | |
| <div class="text-center mb-12"> | |
| <h1 class="text-5xl md:text-6xl lg:text-7xl mb-4 neon-text">PAPERDASH</h1> | |
| <p class="text-xl text-blue-300">Express Delivery Challenge</p> | |
| </div> | |
| <div class="w-full max-w-md space-y-4 px-4"> | |
| <button id="start-btn" class="menu-btn w-full py-4 bg-blue-600 hover:bg-blue-500 rounded-lg text-xl font-bold transition-all"> | |
| START DELIVERY | |
| </button> | |
| <button id="settings-btn" class="menu-btn w-full py-4 bg-gray-700 hover:bg-gray-600 rounded-lg text-lg flex items-center justify-center gap-2"> | |
| <i data-feather="settings"></i> OPTIONS | |
| </button> | |
| <button id="about-btn" class="menu-btn w-full py-4 bg-gray-700 hover:bg-gray-600 rounded-lg text-lg flex items-center justify-center gap-2"> | |
| <i data-feather="info"></i> ABOUT | |
| </button> | |
| </div> | |
| </div> | |
| <div id="difficulty-menu" class="fixed inset-0 flex flex-col items-center justify-center bg-gray-900 bg-opacity-90 z-40 hidden"> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-3xl mb-2 neon-text">SELECT DIFFICULTY</h2> | |
| <p class="text-blue-300">Choose your delivery challenge</p> | |
| </div> | |
| <div class="w-full max-w-md space-y-3 px-4"> | |
| <button data-difficulty="easy" class="difficulty-btn w-full py-3 bg-green-600 hover:bg-green-500 rounded-lg text-lg font-bold transition-all"> | |
| EASY - Suburban Route | |
| </button> | |
| <button data-difficulty="normal" class="difficulty-btn w-full py-3 bg-yellow-600 hover:bg-yellow-500 rounded-lg text-lg font-bold transition-all"> | |
| NORMAL - City Streets | |
| </button> | |
| <button data-difficulty="hard" class="difficulty-btn w-full py-3 bg-orange-600 hover:bg-orange-500 rounded-lg text-lg font-bold transition-all"> | |
| HARD - Downtown Rush | |
| </button> | |
| <button data-difficulty="extreme" class="difficulty-btn w-full py-3 bg-red-600 hover:bg-red-500 rounded-lg text-lg font-bold transition-all"> | |
| EXTREME - Neon Challenge | |
| </button> | |
| <button id="back-btn" class="menu-btn w-full py-3 bg-gray-700 hover:bg-gray-600 rounded-lg mt-4 flex items-center justify-center gap-2"> | |
| <i data-feather="arrow-left"></i> BACK | |
| </button> | |
| </div> | |
| </div> | |
| <div id="game-container" class="game-container hidden"> | |
| <div class="parallax-bg bg-gray-900" id="bg1"></div> | |
| <div class="parallax-bg bg-gray-800" id="bg2"></div> | |
| <div class="parallax-bg bg-gray-700" id="bg3"></div> | |
| <div id="player" class="player"> | |
| <img src="http://static.photos/technology/200x200/1" alt="Player" class="w-16 h-16 object-contain"> | |
| </div> | |
| <div id="hud" class="absolute top-0 left-0 right-0 p-4 flex justify-between items-center z-20"> | |
| <div class="flex items-center gap-2"> | |
| <span id="score" class="text-xl font-bold">0</span> | |
| <span class="text-blue-400">PTS</span> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <div class="flex items-center gap-1"> | |
| <i data-feather="clock" class="text-yellow-400"></i> | |
| <span id="timer" class="text-xl font-bold">60</span> | |
| </div> | |
| <div class="flex items-center gap-1"> | |
| <i data-feather="heart" class="text-red-400"></i> | |
| <span id="lives" class="text-xl font-bold">3</span> | |
| </div> | |
| <div class="flex items-center gap-1"> | |
| <i data-feather="mail" class="text-green-400"></i> | |
| <span id="papers" class="text-xl font-bold">30</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="controls" class="absolute bottom-4 left-0 right-0 flex justify-around items-center px-4 z-20"> | |
| <button id="left-btn" class="control-btn bg-blue-600 bg-opacity-70 rounded-full w-16 h-16 flex items-center justify-center"> | |
| <i data-feather="arrow-left" class="text-2xl"></i> | |
| </button> | |
| <button id="jump-btn" class="control-btn bg-green-600 bg-opacity-70 rounded-full w-16 h-16 flex items-center justify-center"> | |
| <i data-feather="arrow-up" class="text-2xl"></i> | |
| </button> | |
| <button id="throw-btn" class="control-btn bg-red-600 bg-opacity-70 rounded-full w-16 h-16 flex items-center justify-center"> | |
| <i data-feather="arrow-right" class="text-2xl"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="game-over" class="fixed inset-0 flex flex-col items-center justify-center bg-black bg-opacity-80 z-30 hidden"> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-4xl mb-4 text-red-500 neon-text">DELIVERY FAILED!</h2> | |
| <p class="text-xl mb-2">Your final score:</p> | |
| <p id="final-score" class="text-5xl font-bold text-blue-400">0</p> | |
| </div> | |
| <div class="w-full max-w-md space-y-4 px-4"> | |
| <button id="retry-btn" class="menu-btn w-full py-4 bg-blue-600 hover:bg-blue-500 rounded-lg text-xl font-bold"> | |
| RETRY DELIVERY | |
| </button> | |
| <button id="menu-btn" class="menu-btn w-full py-4 bg-gray-700 hover:bg-gray-600 rounded-lg text-lg flex items-center justify-center gap-2"> | |
| <i data-feather="home"></i> MAIN MENU | |
| </button> | |
| </div> | |
| </div> | |
| <div id="level-complete" class="fixed inset-0 flex flex-col items-center justify-center bg-black bg-opacity-80 z-30 hidden"> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-4xl mb-4 text-green-500 neon-text">DELIVERY COMPLETE!</h2> | |
| <p class="text-xl mb-2">You scored:</p> | |
| <p id="level-score" class="text-5xl font-bold text-blue-400">0</p> | |
| <p id="bonus-text" class="text-yellow-400 mt-2">+500 Time Bonus!</p> | |
| </div> | |
| <div class="w-full max-w-md space-y-4 px-4"> | |
| <button id="next-level-btn" class="menu-btn w-full py-4 bg-green-600 hover:bg-green-500 rounded-lg text-xl font-bold"> | |
| NEXT DELIVERY | |
| </button> | |
| <button id="level-menu-btn" class="menu-btn w-full py-4 bg-gray-700 hover:bg-gray-600 rounded-lg text-lg flex items-center justify-center gap-2"> | |
| <i data-feather="home"></i> MAIN MENU | |
| </button> | |
| </div> | |
| </div> | |
| <script> | |
| feather.replace(); | |
| // Game state | |
| const gameState = { | |
| score: 0, | |
| lives: 3, | |
| papers: 30, | |
| time: 60, | |
| difficulty: 'normal', | |
| level: 1, | |
| isGameOver: false, | |
| isPaused: false, | |
| player: { | |
| x: 0, | |
| y: 0, | |
| lane: 1, // 0: left, 1: middle, 2: right | |
| isJumping: false | |
| }, | |
| obstacles: [], | |
| houses: [], | |
| papersThrown: [], | |
| combo: 0, | |
| bgPos: 0 | |
| }; | |
| // DOM elements | |
| const elements = { | |
| mainMenu: document.getElementById('main-menu'), | |
| difficultyMenu: document.getElementById('difficulty-menu'), | |
| gameContainer: document.getElementById('game-container'), | |
| gameOver: document.getElementById('game-over'), | |
| levelComplete: document.getElementById('level-complete'), | |
| player: document.getElementById('player'), | |
| score: document.getElementById('score'), | |
| timer: document.getElementById('timer'), | |
| lives: document.getElementById('lives'), | |
| papers: document.getElementById('papers'), | |
| finalScore: document.getElementById('final-score'), | |
| levelScore: document.getElementById('level-score'), | |
| bonusText: document.getElementById('bonus-text'), | |
| bg1: document.getElementById('bg1'), | |
| bg2: document.getElementById('bg2'), | |
| bg3: document.getElementById('bg3'), | |
| startBtn: document.getElementById('start-btn'), | |
| settingsBtn: document.getElementById('settings-btn'), | |
| aboutBtn: document.getElementById('about-btn'), | |
| backBtn: document.getElementById('back-btn'), | |
| retryBtn: document.getElementById('retry-btn'), | |
| menuBtn: document.getElementById('menu-btn'), | |
| nextLevelBtn: document.getElementById('next-level-btn'), | |
| levelMenuBtn: document.getElementById('level-menu-btn'), | |
| leftBtn: document.getElementById('left-btn'), | |
| jumpBtn: document.getElementById('jump-btn'), | |
| throwBtn: document.getElementById('throw-btn') | |
| }; | |
| // Button event listeners | |
| elements.startBtn.addEventListener('click', () => { | |
| elements.mainMenu.classList.add('hidden'); | |
| elements.difficultyMenu.classList.remove('hidden'); | |
| }); | |
| elements.backBtn.addEventListener('click', () => { | |
| elements.difficultyMenu.classList.add('hidden'); | |
| elements.mainMenu.classList.remove('hidden'); | |
| }); | |
| document.querySelectorAll('.difficulty-btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| gameState.difficulty = this.dataset.difficulty; | |
| startGame(); | |
| }); | |
| }); | |
| elements.retryBtn.addEventListener('click', () => { | |
| elements.gameOver.classList.add('hidden'); | |
| startGame(); | |
| }); | |
| elements.menuBtn.addEventListener('click', () => { | |
| elements.gameOver.classList.add('hidden'); | |
| elements.mainMenu.classList.remove('hidden'); | |
| }); | |
| elements.nextLevelBtn.addEventListener('click', () => { | |
| gameState.level++; | |
| elements.levelComplete.classList.add('hidden'); | |
| startGame(); | |
| }); | |
| elements.levelMenuBtn.addEventListener('click', () => { | |
| elements.levelComplete.classList.add('hidden'); | |
| elements.mainMenu.classList.remove('hidden'); | |
| }); | |
| // Control event listeners | |
| elements.leftBtn.addEventListener('click', () => movePlayer(-1)); | |
| elements.jumpBtn.addEventListener('click', jump); | |
| elements.throwBtn.addEventListener('click', throwPaper); | |
| // Touch events for swipe controls | |
| let touchStartX = 0; | |
| let touchStartY = 0; | |
| document.addEventListener('touchstart', (e) => { | |
| touchStartX = e.changedTouches[0].screenX; | |
| touchStartY = e.changedTouches[0].screenY; | |
| }); | |
| document.addEventListener('touchend', (e) => { | |
| const touchEndX = e.changedTouches[0].screenX; | |
| const touchEndY = e.changedTouches[0].screenY; | |
| const diffX = touchEndX - touchStartX; | |
| const diffY = touchEndY - touchStartY; | |
| // Horizontal swipe | |
| if (Math.abs(diffX) > Math.abs(diffY)) { | |
| if (diffX > 50) { // Right swipe | |
| throwPaper(); | |
| } else if (diffX < -50) { // Left swipe | |
| movePlayer(-1); | |
| } | |
| } else { // Vertical swipe | |
| if (diffY < -50) { // Up swipe | |
| jump(); | |
| } | |
| } | |
| }); | |
| // Keyboard controls | |
| document.addEventListener('keydown', (e) => { | |
| if (!elements.gameContainer.classList.contains('hidden')) { | |
| switch(e.key) { | |
| case 'ArrowLeft': | |
| movePlayer(-1); | |
| break; | |
| case 'ArrowRight': | |
| movePlayer(1); | |
| break; | |
| case 'ArrowUp': | |
| jump(); | |
| break; | |
| case ' ': | |
| case 'ArrowDown': | |
| throwPaper(); | |
| break; | |
| } | |
| } | |
| }); | |
| // Device tilt controls | |
| if (window.DeviceOrientationEvent) { | |
| window.addEventListener('deviceorientation', (e) => { | |
| if (elements.gameContainer.classList.contains('hidden') || gameState.isPaused) return; | |
| const gamma = e.gamma; // Left/right tilt | |
| if (gamma > 15 && gameState.player.lane < 2) { | |
| movePlayer(1); | |
| } else if (gamma < -15 && gameState.player.lane > 0) { | |
| movePlayer(-1); | |
| } | |
| }); | |
| } | |
| // Game functions | |
| function startGame() { | |
| // Reset game state | |
| gameState.score = 0; | |
| gameState.lives = 3; | |
| gameState.papers = 30; | |
| gameState.time = getTimeForDifficulty(); | |
| gameState.isGameOver = false; | |
| gameState.isPaused = false; | |
| gameState.player = { | |
| x: 0, | |
| y: 0, | |
| lane: 1, | |
| isJumping: false | |
| }; | |
| gameState.obstacles = []; | |
| gameState.houses = []; | |
| gameState.papersThrown = []; | |
| gameState.combo = 0; | |
| gameState.bgPos = 0; | |
| // Update UI | |
| elements.score.textContent = gameState.score; | |
| elements.lives.textContent = gameState.lives; | |
| elements.papers.textContent = gameState.papers; | |
| elements.timer.textContent = gameState.time; | |
| // Show game container | |
| elements.mainMenu.classList.add('hidden'); | |
| elements.difficultyMenu.classList.add('hidden'); | |
| elements.gameContainer.classList.remove('hidden'); | |
| // Set up game area | |
| setupGameArea(); | |
| // Start game loop | |
| gameLoop(); | |
| // Start timer | |
| startTimer(); | |
| } | |
| function getTimeForDifficulty() { | |
| switch(gameState.difficulty) { | |
| case 'easy': return 90; | |
| case 'normal': return 60; | |
| case 'hard': return 45; | |
| case 'extreme': return 30; | |
| default: return 60; | |
| } | |
| } | |
| function setupGameArea() { | |
| const containerWidth = elements.gameContainer.clientWidth; | |
| const containerHeight = elements.gameContainer.clientHeight; | |
| // Set up lanes | |
| const laneWidth = containerWidth / 3; | |
| gameState.player.x = laneWidth * 1.5; | |
| gameState.player.y = containerHeight * 0.7; | |
| // Position player | |
| elements.player.style.left = `${gameState.player.x - 32}px`; | |
| elements.player.style.top = `${gameState.player.y - 32}px`; | |
| // Generate initial houses | |
| generateHouses(containerWidth, containerHeight); | |
| } | |
| function generateHouses(width, height) { | |
| // Clear existing houses | |
| gameState.houses = []; | |
| document.querySelectorAll('.house').forEach(el => el.remove()); | |
| // Generate new houses based on difficulty | |
| const houseCount = getHouseCountForDifficulty(); | |
| for (let i = 0; i < houseCount; i++) { | |
| const house = { | |
| id: i, | |
| x: width + (i * getHouseSpacingForDifficulty()), | |
| lane: Math.floor(Math.random() * 3), | |
| delivered: false | |
| }; | |
| gameState.houses.push(house); | |
| const houseEl = document.createElement('div'); | |
| houseEl.className = 'house'; | |
| houseEl.dataset.id = i; | |
| houseEl.style.width = '64px'; | |
| houseEl.style.height = '64px'; | |
| houseEl.style.left = `${house.x}px`; | |
| houseEl.style.top = `${height * 0.6}px`; | |
| // Different house styles based on lane | |
| const colors = ['#ef4444', '#3b82f6', '#10b981']; | |
| houseEl.style.backgroundColor = colors[house.lane]; | |
| // Mailbox indicator | |
| const mailbox = document.createElement('div'); | |
| mailbox.style.width = '16px'; | |
| mailbox.style.height = '16px'; | |
| mailbox.style.backgroundColor = '#f59e0b'; | |
| mailbox.style.position = 'absolute'; | |
| mailbox.style.bottom = '-8px'; | |
| mailbox.style.left = '24px'; | |
| mailbox.style.borderRadius = '4px'; | |
| houseEl.appendChild(mailbox); | |
| elements.gameContainer.appendChild(houseEl); | |
| } | |
| } | |
| function getHouseCountForDifficulty() { | |
| switch(gameState.difficulty) { | |
| case 'easy': return 15; | |
| case 'normal': return 20; | |
| case 'hard': return 25; | |
| case 'extreme': return 30; | |
| default: return 20; | |
| } | |
| } | |
| function getHouseSpacingForDifficulty() { | |
| switch(gameState.difficulty) { | |
| case 'easy': return 300; | |
| case 'normal': return 250; | |
| case 'hard': return 200; | |
| case 'extreme': return 150; | |
| default: return 250; | |
| } | |
| } | |
| function movePlayer(direction) { | |
| if (gameState.isPaused) return; | |
| const containerWidth = elements.gameContainer.clientWidth; | |
| const laneWidth = containerWidth / 3; | |
| gameState.player.lane = Math.max(0, Math.min(2, gameState.player.lane + direction)); | |
| gameState.player.x = laneWidth * (gameState.player.lane + 0.5); | |
| elements.player.style.left = `${gameState.player.x - 32}px`; | |
| } | |
| function jump() { | |
| if (gameState.isPaused || gameState.player.isJumping) return; | |
| gameState.player.isJumping = true; | |
| const jumpHeight = 50; | |
| const startY = gameState.player.y; | |
| // Jump up | |
| const jumpUp = setInterval(() => { | |
| gameState.player.y -= 2; | |
| elements.player.style.top = `${gameState.player.y - 32}px`; | |
| if (gameState.player.y <= startY - jumpHeight) { | |
| clearInterval(jumpUp); | |
| // Fall down | |
| const fallDown = setInterval(() => { | |
| gameState.player.y += 2; | |
| elements.player.style.top = `${gameState.player.y - 32}px`; | |
| if (gameState.player.y >= startY) { | |
| gameState.player.y = startY; | |
| elements.player.style.top = `${gameState.player.y - 32}px`; | |
| gameState.player.isJumping = false; | |
| clearInterval(fallDown); | |
| } | |
| }, 16); | |
| } | |
| }, 16); | |
| } | |
| function throwPaper() { | |
| if (gameState.isPaused || gameState.papers <= 0) return; | |
| gameState.papers--; | |
| elements.papers.textContent = gameState.papers; | |
| const paperEl = document.createElement('div'); | |
| paperEl.className = 'paper'; | |
| paperEl.style.width = '24px'; | |
| paperEl.style.height = '24px'; | |
| paperEl.style.left = `${gameState.player.x - 12}px`; | |
| paperEl.style.top = `${gameState.player.y - 12}px`; | |
| paperEl.style.backgroundColor = '#ffffff'; | |
| paperEl.style.borderRadius = '2px'; | |
| elements.gameContainer.appendChild(paperEl); | |
| const paper = { | |
| id: Date.now(), | |
| x: gameState.player.x, | |
| y: gameState.player.y, | |
| element: paperEl | |
| }; | |
| gameState.papersThrown.push(paper); | |
| // Animate paper throw | |
| const throwDistance = 200; | |
| let distance = 0; | |
| const throwInterval = setInterval(() => { | |
| distance += 8; | |
| paper.x += 8; | |
| paperEl.style.left = `${paper.x - 12}px`; | |
| // Parabolic trajectory | |
| const height = Math.sin(distance / throwDistance * Math.PI) * 50; | |
| paperEl.style.top = `${gameState.player.y - 12 - height}px`; | |
| // Check for collisions with houses | |
| checkPaperCollision(paper); | |
| if (distance >= throwDistance) { | |
| clearInterval(throwInterval); | |
| paperEl.remove(); | |
| gameState.papersThrown = gameState.papersThrown.filter(p => p.id !== paper.id); | |
| } | |
| }, 16); | |
| // Play sound | |
| playSound('throw'); | |
| } | |
| function checkPaperCollision(paper) { | |
| const paperRect = { | |
| x: paper.x - 12, | |
| y: paper.y - 12, | |
| width: 24, | |
| height: 24 | |
| }; | |
| for (const house of gameState.houses) { | |
| if (house.delivered) continue; | |
| const houseEl = document.querySelector(`.house[data-id="${house.id}"]`); | |
| if (!houseEl) continue; | |
| const houseRect = { | |
| x: parseInt(houseEl.style.left), | |
| y: parseInt(houseEl.style.top), | |
| width: 64, | |
| height: 64 | |
| }; | |
| // Simple collision detection | |
| if (paperRect.x < houseRect.x + houseRect.width && | |
| paperRect.x + paperRect.width > houseRect.x && | |
| paperRect.y < houseRect.y + houseRect.height && | |
| paperRect.y + paperRect.height > houseRect.y) { | |
| // Check if it's the correct lane | |
| if (gameState.player.lane === house.lane) { | |
| // Successful delivery | |
| house.delivered = true; | |
| houseEl.style.opacity = '0.5'; | |
| // Increment combo | |
| gameState.combo++; | |
| // Calculate points | |
| const basePoints = 100; | |
| const comboBonus = Math.min(5, gameState.combo) * 20; | |
| const points = basePoints + comboBonus; | |
| gameState.score += points; | |
| elements.score.textContent = gameState.score; | |
| // Show score popup | |
| showScorePopup(points, houseRect.x + 32, houseRect.y); | |
| // Play sound | |
| playSound('ding'); | |
| // Check if all houses are delivered | |
| if (gameState.houses.every(h => h.delivered)) { | |
| levelComplete(); | |
| } | |
| } else { | |
| // Wrong lane - penalty | |
| gameState.combo = 0; | |
| gameState.score -= 50; | |
| elements.score.textContent = Math.max(0, gameState.score); | |
| // Show penalty popup | |
| showScorePopup(-50, houseRect.x + 32, houseRect.y); | |
| // Play sound | |
| playSound('error'); | |
| } | |
| // Remove paper | |
| paper.element.remove(); | |
| gameState.papersThrown = gameState.papersThrown.filter(p => p.id !== paper.id); | |
| break; | |
| } | |
| } | |
| } | |
| function showScorePopup(points, x, y) { | |
| const popup = document.createElement('div'); | |
| popup.className = 'score-popup'; | |
| popup.textContent = points > 0 ? `+${points}` : points; | |
| popup.style.left = `${x - 20}px`; | |
| popup.style.top = `${y - 20}px`; | |
| popup.style.color = points > 0 ? '#10b981' : '#ef4444'; | |
| popup.style.fontSize = '24px'; | |
| popup.style.fontWeight = 'bold'; | |
| elements.gameContainer.appendChild(popup); | |
| // Remove after animation | |
| setTimeout(() => { | |
| popup.remove(); | |
| }, 1000); | |
| } | |
| function generateObstacle() { | |
| const containerWidth = elements.gameContainer.clientWidth; | |
| const containerHeight = elements.gameContainer.clientHeight; | |
| const obstacle = { | |
| id: Date.now(), | |
| x: containerWidth, | |
| lane: Math.floor(Math.random() * 3), | |
| type: Math.random() > 0.5 ? 'cone' : 'dog' | |
| }; | |
| gameState.obstacles.push(obstacle); | |
| const obstacleEl = document.createElement('div'); | |
| obstacleEl.className = 'obstacle'; | |
| obstacleEl.dataset.id = obstacle.id; | |
| obstacleEl.style.width = '32px'; | |
| obstacleEl.style.height = '32px'; | |
| obstacleEl.style.left = `${obstacle.x}px`; | |
| obstacleEl.style.top = `${containerHeight * 0.7}px`; | |
| if (obstacle.type === 'cone') { | |
| obstacleEl.style.backgroundColor = '#f59e0b'; | |
| obstacleEl.style.borderRadius = '4px'; | |
| } else { | |
| obstacleEl.style.backgroundColor = '#ef4444'; | |
| obstacleEl.style.borderRadius = '50%'; | |
| } | |
| elements.gameContainer.appendChild(obstacleEl); | |
| } | |
| function checkPlayerCollision() { | |
| const playerRect = { | |
| x: gameState.player.x - 32, | |
| y: gameState.player.y - 32, | |
| width: 64, | |
| height: 64 | |
| }; | |
| for (const obstacle of gameState.obstacles) { | |
| const obstacleEl = document.querySelector(`.obstacle[data-id="${obstacle.id}"]`); | |
| if (!obstacleEl) continue; | |
| const obstacleRect = { | |
| x: parseInt(obstacleEl.style.left), | |
| y: parseInt(obstacleEl.style.top), | |
| width: 32, | |
| height: 32 | |
| }; | |
| // Simple collision detection | |
| if (playerRect.x < obstacleRect.x + obstacleRect.width && | |
| playerRect.x + playerRect.width > obstacleRect.x && | |
| playerRect.y < obstacleRect.y + obstacleRect.height && | |
| playerRect.y + playerRect.height > obstacleRect.y) { | |
| // Handle collision | |
| gameState.lives--; | |
| elements.lives.textContent = gameState.lives; | |
| // Show hit effect | |
| elements.player.style.filter = 'brightness(2)'; | |
| setTimeout(() => { | |
| elements.player.style.filter = 'none'; | |
| }, 200); | |
| // Remove obstacle | |
| obstacleEl.remove(); | |
| gameState.obstacles = gameState.obstacles.filter(o => o.id !== obstacle.id); | |
| // Reset combo | |
| gameState.combo = 0; | |
| // Play sound | |
| playSound('crash'); | |
| // Check if game over | |
| if (gameState.lives <= 0) { | |
| gameOver(); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| function startTimer() { | |
| gameState.timerInterval = setInterval(() => { | |
| gameState.time--; | |
| elements.timer.textContent = gameState.time; | |
| if (gameState.time <= 10) { | |
| elements.timer.style.color = '#ef4444'; | |
| } | |
| if (gameState.time <= 0) { | |
| gameOver(); | |
| } | |
| }, 1000); | |
| } | |
| function gameOver() { | |
| clearInterval(gameState.timerInterval); | |
| gameState.isGameOver = true; | |
| gameState.isPaused = true; | |
| // Show game over screen | |
| elements.finalScore.textContent = gameState.score; | |
| elements.gameOver.classList.remove('hidden'); | |
| // Play sound | |
| playSound('gameover'); | |
| } | |
| function levelComplete() { | |
| clearInterval(gameState.timerInterval); | |
| gameState.isPaused = true; | |
| // Calculate time bonus | |
| const timeBonus = gameState.time * 10; | |
| gameState.score += timeBonus; | |
| // Show level complete screen | |
| elements.levelScore.textContent = gameState.score; | |
| elements.bonusText.textContent = `+${timeBonus} Time Bonus!`; | |
| elements.levelComplete.classList.remove('hidden'); | |
| // Play sound | |
| playSound('win'); | |
| } | |
| function playSound(type) { | |
| // In a real implementation, you would use Howler.js or similar | |
| // This is a placeholder for sound effects | |
| console.log(`Play sound: ${type}`); | |
| } | |
| function gameLoop() { | |
| if (gameState.isPaused) return; | |
| const containerWidth = elements.gameContainer.clientWidth; | |
| const containerHeight = elements.gameContainer.clientHeight; | |
| // Move background (parallax effect) | |
| gameState.bgPos -= getScrollSpeedForDifficulty(); | |
| elements.bg1.style.backgroundPositionX = `${gameState.bgPos * 0.2}px`; | |
| elements.bg2.style.backgroundPositionX = `${gameState.bgPos * 0.5}px`; | |
| elements.bg3.style.backgroundPositionX = `${gameState.bgPos}px`; | |
| // Move houses | |
| for (const house of gameState.houses) { | |
| const houseEl = document.querySelector(`.house[data-id="${house.id}"]`); | |
| if (houseEl) { | |
| house.x -= getScrollSpeedForDifficulty(); | |
| houseEl.style.left = `${house.x}px`; | |
| // Remove if off screen | |
| if (house.x < -64) { | |
| houseEl.remove(); | |
| gameState.houses = gameState.houses.filter(h => h.id !== house.id); | |
| } | |
| } | |
| } | |
| // Move obstacles | |
| for (const obstacle of gameState.obstacles) { | |
| const obstacleEl = document.querySelector(`.obstacle[data-id="${obstacle.id}"]`); | |
| if (obstacleEl) { | |
| obstacle.x -= getScrollSpeedForDifficulty() * 1.2; | |
| obstacleEl.style.left = `${obstacle.x}px`; | |
| // Remove if off screen | |
| if (obstacle.x < -32) { | |
| obstacleEl.remove(); | |
| gameState.obstacles = gameState.obstacles.filter(o => o.id !== obstacle.id); | |
| } | |
| } | |
| } | |
| // Check for collisions | |
| checkPlayerCollision(); | |
| // Randomly generate obstacles | |
| if (Math.random() < getObstacleFrequencyForDifficulty()) { | |
| generateObstacle(); | |
| } | |
| // Continue game loop | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function getScrollSpeedForDifficulty() { | |
| switch(gameState.difficulty) { | |
| case 'easy': return 2; | |
| case 'normal': return 3; | |
| case 'hard': return 4; | |
| case 'extreme': return 5; | |
| default: return 3; | |
| } | |
| } | |
| function getObstacleFrequencyForDifficulty() { | |
| switch(gameState.difficulty) { | |
| case 'easy': return 0.005; | |
| case 'normal': return 0.01; | |
| case 'hard': return 0.015; | |
| case 'extreme': return 0.02; | |
| default: return 0.01; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |