Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Whacky Wheels - Emoji Edition</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| body { | |
| overflow: hidden; | |
| touch-action: none; | |
| background-color: #1a202c; | |
| } | |
| canvas { | |
| display: block; | |
| margin: 0 auto; | |
| background-color: #2d3748; | |
| } | |
| .emoji-button { | |
| font-size: 2rem; | |
| transition: all 0.2s; | |
| } | |
| .emoji-button:hover { | |
| transform: scale(1.2); | |
| } | |
| .emoji-button:active { | |
| transform: scale(0.9); | |
| } | |
| .track-tile { | |
| width: 40px; | |
| height: 40px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .track-tile-road { background-color: #4a5568; } | |
| .track-tile-grass { background-color: #48bb78; } | |
| .track-tile-oil { background-color: #805ad5; } | |
| .track-tile-power { background-color: #f6e05e; } | |
| .track-tile-start { background-color: #f56565; } | |
| .track-tile-finish { background-color: #4299e1; } | |
| </style> | |
| </head> | |
| <body class="font-sans text-white"> | |
| <div id="game-container" class="relative w-full h-screen"> | |
| <!-- Splash Screen --> | |
| <div id="splash-screen" class="absolute inset-0 flex flex-col items-center justify-center bg-gray-900 bg-opacity-90 z-10"> | |
| <h1 class="text-6xl font-bold mb-8">๐ฎ Whacky Wheels</h1> | |
| <p class="text-2xl mb-12">Emoji Edition</p> | |
| <div class="flex space-x-8 mb-12"> | |
| <button id="player-frog" class="emoji-button bg-gray-800 p-4 rounded-full hover:bg-green-600 transition">๐ธ</button> | |
| <button id="player-turtle" class="emoji-button bg-gray-800 p-4 rounded-full hover:bg-blue-600 transition">๐ข</button> | |
| <button id="player-cat" class="emoji-button bg-gray-800 p-4 rounded-full hover:bg-yellow-600 transition">๐ฑ</button> | |
| <button id="player-dog" class="emoji-button bg-gray-800 p-4 rounded-full hover:bg-red-600 transition">๐ถ</button> | |
| </div> | |
| <button id="start-game" class="px-8 py-4 bg-green-600 text-2xl font-bold rounded-lg hover:bg-green-700 transition transform hover:scale-105"> | |
| ๐ START RACE ๐ | |
| </button> | |
| <div class="mt-12 text-gray-400"> | |
| <p>Controls: โโโโ to move, SPACE to fire, Z to drop mines</p> | |
| <p>Press R to restart race</p> | |
| </div> | |
| </div> | |
| <!-- Countdown Screen --> | |
| <div id="countdown-screen" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70 z-20 hidden"> | |
| <div id="countdown-text" class="text-9xl font-bold">3</div> | |
| </div> | |
| <!-- Game Canvas --> | |
| <canvas id="game-canvas" class="absolute inset-0 w-full h-full"></canvas> | |
| <!-- HUD --> | |
| <div id="game-hud" class="absolute top-0 left-0 right-0 p-4 flex justify-between items-start z-10 hidden"> | |
| <div class="bg-gray-900 bg-opacity-70 p-3 rounded-lg"> | |
| <div class="text-xl">Lap: <span id="lap-counter">1</span>/3</div> | |
| <div class="text-xl">Speed: <span id="speed-counter">0</span></div> | |
| </div> | |
| <div class="bg-gray-900 bg-opacity-70 p-3 rounded-lg flex items-center"> | |
| <div id="weapon-display" class="text-3xl mr-3">๐</div> | |
| <div id="weapon-count" class="text-xl">x3</div> | |
| </div> | |
| <div class="bg-gray-900 bg-opacity-70 p-3 rounded-lg"> | |
| <div class="text-xl">Position: <span id="position-counter">1st</span></div> | |
| <div class="text-xl">Time: <span id="time-counter">0:00</span></div> | |
| </div> | |
| </div> | |
| <!-- Mobile Controls --> | |
| <div id="mobile-controls" class="absolute bottom-0 left-0 right-0 p-4 grid grid-cols-3 gap-4 hidden"> | |
| <div></div> | |
| <button id="mobile-up" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">โ</button> | |
| <div></div> | |
| <button id="mobile-left" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">โ</button> | |
| <button id="mobile-fire" class="bg-red-800 bg-opacity-70 p-4 rounded-full text-3xl">๐ฅ</button> | |
| <button id="mobile-right" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">โ</button> | |
| <button id="mobile-down" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">โ</button> | |
| <button id="mobile-mine" class="bg-gray-800 bg-opacity-70 p-4 rounded-full text-3xl">๐ฆ</button> | |
| <div></div> | |
| </div> | |
| <!-- Results Screen --> | |
| <div id="results-screen" class="absolute inset-0 flex flex-col items-center justify-center bg-gray-900 bg-opacity-90 z-30 hidden"> | |
| <h2 class="text-5xl font-bold mb-8">Race Complete!</h2> | |
| <div id="result-position" class="text-8xl mb-8">1st ๐</div> | |
| <div class="text-2xl mb-4">Time: <span id="result-time">1:23.45</span></div> | |
| <div class="text-2xl mb-12">Best Lap: <span id="result-best-lap">0:27.89</span></div> | |
| <div class="flex space-x-4"> | |
| <button id="play-again" class="px-6 py-3 bg-blue-600 text-xl font-bold rounded-lg hover:bg-blue-700 transition"> | |
| Race Again | |
| </button> | |
| <button id="back-to-menu" class="px-6 py-3 bg-gray-600 text-xl font-bold rounded-lg hover:bg-gray-700 transition"> | |
| Main Menu | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game Constants | |
| const TILE_SIZE = 40; | |
| const KART_SIZE = 30; | |
| const PROJECTILE_SIZE = 20; | |
| const MINE_SIZE = 25; | |
| const EXPLOSION_SIZE = 50; | |
| const MAX_LAPS = 3; | |
| // Track Types | |
| const TRACK_TYPES = { | |
| 'G': { name: 'grass', friction: 0.2, color: '#48bb78' }, | |
| 'R': { name: 'road', friction: 0.05, color: '#4a5568' }, | |
| 'O': { name: 'oil', friction: 0.3, color: '#805ad5' }, | |
| 'P': { name: 'power', friction: 0.05, color: '#f6e05e' }, | |
| 'S': { name: 'start', friction: 0.05, color: '#f56565' }, | |
| 'F': { name: 'finish', friction: 0.05, color: '#4299e1' } | |
| }; | |
| // Game State | |
| let gameState = { | |
| screen: 'splash', | |
| playerEmoji: '๐ธ', | |
| playerName: 'Frog Racer', | |
| track: [], | |
| karts: [], | |
| projectiles: [], | |
| mines: [], | |
| explosions: [], | |
| lap: 1, | |
| position: 1, | |
| raceTime: 0, | |
| lapTimes: [], | |
| bestLapTime: Infinity, | |
| gameTime: 0, | |
| lastTime: 0, | |
| keys: {}, | |
| isMobile: false | |
| }; | |
| // DOM Elements | |
| const elements = { | |
| splashScreen: document.getElementById('splash-screen'), | |
| countdownScreen: document.getElementById('countdown-screen'), | |
| countdownText: document.getElementById('countdown-text'), | |
| gameCanvas: document.getElementById('game-canvas'), | |
| gameHud: document.getElementById('game-hud'), | |
| mobileControls: document.getElementById('mobile-controls'), | |
| resultsScreen: document.getElementById('results-screen'), | |
| lapCounter: document.getElementById('lap-counter'), | |
| speedCounter: document.getElementById('speed-counter'), | |
| positionCounter: document.getElementById('position-counter'), | |
| timeCounter: document.getElementById('time-counter'), | |
| weaponDisplay: document.getElementById('weapon-display'), | |
| weaponCount: document.getElementById('weapon-count'), | |
| resultPosition: document.getElementById('result-position'), | |
| resultTime: document.getElementById('result-time'), | |
| resultBestLap: document.getElementById('result-best-lap') | |
| }; | |
| // Canvas Setup | |
| const canvas = elements.gameCanvas; | |
| const ctx = canvas.getContext('2d'); | |
| function resizeCanvas() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); | |
| // Event Listeners | |
| document.getElementById('start-game').addEventListener('click', startGame); | |
| document.getElementById('play-again').addEventListener('click', startGame); | |
| document.getElementById('back-to-menu').addEventListener('click', backToMenu); | |
| // Player selection | |
| document.querySelectorAll('[id^="player-"]').forEach(button => { | |
| button.addEventListener('click', function() { | |
| gameState.playerEmoji = this.textContent; | |
| gameState.playerName = this.id.split('-')[1].charAt(0).toUpperCase() + this.id.split('-')[1].slice(1) + ' Racer'; | |
| // Update active button | |
| document.querySelectorAll('[id^="player-"]').forEach(btn => { | |
| btn.classList.remove('ring-4', 'ring-white'); | |
| }); | |
| this.classList.add('ring-4', 'ring-white'); | |
| }); | |
| }); | |
| // Keyboard Controls | |
| window.addEventListener('keydown', (e) => { | |
| gameState.keys[e.key] = true; | |
| // Restart race | |
| if (e.key === 'r' && gameState.screen === 'racing') { | |
| startGame(); | |
| } | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| gameState.keys[e.key] = false; | |
| }); | |
| // Mobile Controls | |
| function setupMobileControls() { | |
| gameState.isMobile = true; | |
| elements.mobileControls.classList.remove('hidden'); | |
| // Control buttons | |
| const mobileControls = { | |
| up: false, | |
| down: false, | |
| left: false, | |
| right: false, | |
| fire: false, | |
| mine: false | |
| }; | |
| // Press events | |
| document.getElementById('mobile-up').addEventListener('touchstart', () => mobileControls.up = true); | |
| document.getElementById('mobile-up').addEventListener('touchend', () => mobileControls.up = false); | |
| document.getElementById('mobile-down').addEventListener('touchstart', () => mobileControls.down = true); | |
| document.getElementById('mobile-down').addEventListener('touchend', () => mobileControls.down = false); | |
| document.getElementById('mobile-left').addEventListener('touchstart', () => mobileControls.left = true); | |
| document.getElementById('mobile-left').addEventListener('touchend', () => mobileControls.left = false); | |
| document.getElementById('mobile-right').addEventListener('touchstart', () => mobileControls.right = true); | |
| document.getElementById('mobile-right').addEventListener('touchend', () => mobileControls.right = false); | |
| document.getElementById('mobile-fire').addEventListener('touchstart', () => mobileControls.fire = true); | |
| document.getElementById('mobile-fire').addEventListener('touchend', () => mobileControls.fire = false); | |
| document.getElementById('mobile-mine').addEventListener('touchstart', () => mobileControls.mine = true); | |
| document.getElementById('mobile-mine').addEventListener('touchend', () => mobileControls.mine = false); | |
| // Map mobile controls to keyboard state | |
| setInterval(() => { | |
| gameState.keys['ArrowUp'] = mobileControls.up; | |
| gameState.keys['ArrowDown'] = mobileControls.down; | |
| gameState.keys['ArrowLeft'] = mobileControls.left; | |
| gameState.keys['ArrowRight'] = mobileControls.right; | |
| gameState.keys[' '] = mobileControls.fire; | |
| gameState.keys['z'] = mobileControls.mine; | |
| }, 16); | |
| } | |
| // Check if mobile | |
| if ('ontouchstart' in window || navigator.maxTouchPoints) { | |
| setupMobileControls(); | |
| } | |
| // Game Functions | |
| function generateTrack() { | |
| // Simple procedural track generation | |
| const width = 20; | |
| const height = 15; | |
| const track = Array(height).fill().map(() => Array(width).fill('G')); | |
| // Create a looping road | |
| const roadPath = [ | |
| {x: 3, y: 3}, {x: 16, y: 3}, {x: 16, y: 11}, {x: 3, y: 11}, {x: 3, y: 3} | |
| ]; | |
| // Draw the road | |
| for (let i = 0; i < roadPath.length - 1; i++) { | |
| const from = roadPath[i]; | |
| const to = roadPath[i+1]; | |
| // Horizontal road | |
| if (from.y === to.y) { | |
| const start = Math.min(from.x, to.x); | |
| const end = Math.max(from.x, to.x); | |
| for (let x = start; x <= end; x++) { | |
| track[from.y][x] = 'R'; | |
| } | |
| } | |
| // Vertical road | |
| else if (from.x === to.x) { | |
| const start = Math.min(from.y, to.y); | |
| const end = Math.max(from.y, to.y); | |
| for (let y = start; y <= end; y++) { | |
| track[y][from.x] = 'R'; | |
| } | |
| } | |
| } | |
| // Add start and finish | |
| track[3][3] = 'S'; | |
| track[3][4] = 'F'; | |
| // Add some obstacles and power-ups | |
| track[5][5] = 'O'; | |
| track[7][10] = 'P'; | |
| track[9][14] = 'O'; | |
| track[11][7] = 'P'; | |
| return track; | |
| } | |
| function createKart(emoji, x, y, isPlayer = false) { | |
| return { | |
| emoji, | |
| x, | |
| y, | |
| angle: 0, | |
| speed: 0, | |
| maxSpeed: isPlayer ? 5 : 4, | |
| acceleration: isPlayer ? 0.1 : 0.08, | |
| turnSpeed: 0.05, | |
| weapon: '๐', | |
| weaponCount: 3, | |
| isPlayer, | |
| lap: 1, | |
| checkPoint: 0, | |
| waypoints: [ | |
| {x: 4, y: 3}, {x: 15, y: 3}, | |
| {x: 15, y: 10}, {x: 4, y: 10}, | |
| {x: 4, y: 3} | |
| ], | |
| nextWaypoint: 1 | |
| }; | |
| } | |
| function startGame() { | |
| // Reset game state | |
| gameState.track = generateTrack(); | |
| gameState.karts = []; | |
| gameState.projectiles = []; | |
| gameState.mines = []; | |
| gameState.explosions = []; | |
| gameState.lap = 1; | |
| gameState.position = 1; | |
| gameState.raceTime = 0; | |
| gameState.lapTimes = []; | |
| gameState.bestLapTime = Infinity; | |
| gameState.gameTime = 0; | |
| gameState.lastTime = 0; | |
| // Create player kart | |
| const startPos = findStartPosition(); | |
| gameState.karts.push(createKart(gameState.playerEmoji, startPos.x, startPos.y, true)); | |
| // Create AI karts | |
| gameState.karts.push(createKart('๐ข', startPos.x - 40, startPos.y - 40)); | |
| gameState.karts.push(createKart('๐ฑ', startPos.x + 40, startPos.y)); | |
| gameState.karts.push(createKart('๐ถ', startPos.x, startPos.y + 40)); | |
| // Show countdown | |
| gameState.screen = 'countdown'; | |
| elements.splashScreen.classList.add('hidden'); | |
| elements.countdownScreen.classList.remove('hidden'); | |
| elements.gameHud.classList.add('hidden'); | |
| elements.resultsScreen.classList.add('hidden'); | |
| let count = 3; | |
| elements.countdownText.textContent = count; | |
| const countdownInterval = setInterval(() => { | |
| count--; | |
| if (count > 0) { | |
| elements.countdownText.textContent = count; | |
| } else if (count === 0) { | |
| elements.countdownText.textContent = '๐'; | |
| } else { | |
| clearInterval(countdownInterval); | |
| startRace(); | |
| } | |
| }, 1000); | |
| } | |
| function startRace() { | |
| gameState.screen = 'racing'; | |
| elements.countdownScreen.classList.add('hidden'); | |
| elements.gameHud.classList.remove('hidden'); | |
| // Start game loop | |
| gameState.lastTime = performance.now(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function findStartPosition() { | |
| for (let y = 0; y < gameState.track.length; y++) { | |
| for (let x = 0; x < gameState.track[y].length; x++) { | |
| if (gameState.track[y][x] === 'S') { | |
| return { | |
| x: x * TILE_SIZE + TILE_SIZE/2, | |
| y: y * TILE_SIZE + TILE_SIZE/2 | |
| }; | |
| } | |
| } | |
| } | |
| return {x: 100, y: 100}; // Fallback | |
| } | |
| function backToMenu() { | |
| gameState.screen = 'splash'; | |
| elements.splashScreen.classList.remove('hidden'); | |
| elements.resultsScreen.classList.add('hidden'); | |
| } | |
| function gameLoop(timestamp) { | |
| if (gameState.screen !== 'racing') return; | |
| // Calculate delta time | |
| const deltaTime = (timestamp - gameState.lastTime) / 1000; | |
| gameState.lastTime = timestamp; | |
| gameState.gameTime += deltaTime; | |
| gameState.raceTime += deltaTime; | |
| // Update game state | |
| updateKarts(deltaTime); | |
| updateProjectiles(deltaTime); | |
| updateMines(deltaTime); | |
| updateExplosions(deltaTime); | |
| updateAI(deltaTime); | |
| checkCollisions(); | |
| checkLapCompletion(); | |
| updatePosition(); | |
| // Update HUD | |
| updateHUD(); | |
| // Draw everything | |
| drawGame(); | |
| // Continue loop | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function updateKarts(deltaTime) { | |
| gameState.karts.forEach(kart => { | |
| // Apply friction based on current tile | |
| const tileX = Math.floor(kart.x / TILE_SIZE); | |
| const tileY = Math.floor(kart.y / TILE_SIZE); | |
| let tileType = 'G'; | |
| if (tileY >= 0 && tileY < gameState.track.length && | |
| tileX >= 0 && tileX < gameState.track[tileY].length) { | |
| tileType = gameState.track[tileY][tileX]; | |
| } | |
| const friction = TRACK_TYPES[tileType]?.friction || 0.2; | |
| // Player controls | |
| if (kart.isPlayer) { | |
| // Acceleration | |
| if (gameState.keys['ArrowUp'] || gameState.keys['w']) { | |
| kart.speed += kart.acceleration * deltaTime * 60; | |
| } | |
| // Brake/reverse | |
| else if (gameState.keys['ArrowDown'] || gameState.keys['s']) { | |
| kart.speed -= kart.acceleration * deltaTime * 60; | |
| } | |
| // Natural deceleration | |
| else { | |
| if (kart.speed > 0) { | |
| kart.speed = Math.max(0, kart.speed - friction * deltaTime * 20); | |
| } else if (kart.speed < 0) { | |
| kart.speed = Math.min(0, kart.speed + friction * deltaTime * 20); | |
| } | |
| } | |
| // Turning | |
| if (kart.speed !== 0) { | |
| const turnDirection = kart.speed > 0 ? 1 : -1; | |
| if (gameState.keys['ArrowLeft'] || gameState.keys['a']) { | |
| kart.angle -= kart.turnSpeed * turnDirection * deltaTime * 60; | |
| } | |
| if (gameState.keys['ArrowRight'] || gameState.keys['d']) { | |
| kart.angle += kart.turnSpeed * turnDirection * deltaTime * 60; | |
| } | |
| } | |
| // Fire weapon | |
| if (gameState.keys[' '] && kart.weaponCount > 0) { | |
| fireWeapon(kart); | |
| gameState.keys[' '] = false; // Prevent rapid fire | |
| } | |
| // Drop mine | |
| if (gameState.keys['z'] && kart.weaponCount > 0) { | |
| dropMine(kart); | |
| gameState.keys['z'] = false; | |
| } | |
| } | |
| // Clamp speed | |
| kart.speed = Math.max(-kart.maxSpeed * 0.5, Math.min(kart.speed, kart.maxSpeed)); | |
| // Apply movement | |
| kart.x += Math.cos(kart.angle) * kart.speed * deltaTime * 60; | |
| kart.y += Math.sin(kart.angle) * kart.speed * deltaTime * 60; | |
| // Track boundaries | |
| const trackWidth = gameState.track[0].length * TILE_SIZE; | |
| const trackHeight = gameState.track.length * TILE_SIZE; | |
| if (kart.x < 0) kart.x = 0; | |
| if (kart.x > trackWidth) kart.x = trackWidth; | |
| if (kart.y < 0) kart.y = 0; | |
| if (kart.y > trackHeight) kart.y = trackHeight; | |
| // Check if kart is on oil | |
| if (tileType === 'O') { | |
| // Random sliding effect | |
| kart.angle += (Math.random() - 0.5) * 0.2; | |
| } | |
| }); | |
| } | |
| function fireWeapon(kart) { | |
| if (kart.weaponCount <= 0) return; | |
| const projectile = { | |
| emoji: kart.weapon, | |
| x: kart.x + Math.cos(kart.angle) * KART_SIZE, | |
| y: kart.y + Math.sin(kart.angle) * KART_SIZE, | |
| angle: kart.angle, | |
| speed: 8, | |
| owner: kart.isPlayer ? 'player' : 'ai', | |
| lifetime: 100 | |
| }; | |
| gameState.projectiles.push(projectile); | |
| kart.weaponCount--; | |
| // Play sound (optional) | |
| if (typeof AudioContext !== 'undefined') { | |
| const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| const oscillator = audioCtx.createOscillator(); | |
| const gainNode = audioCtx.createGain(); | |
| oscillator.type = 'square'; | |
| oscillator.frequency.setValueAtTime(800, audioCtx.currentTime); | |
| oscillator.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.2); | |
| gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime); | |
| gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2); | |
| oscillator.connect(gainNode); | |
| gainNode.connect(audioCtx.destination); | |
| oscillator.start(); | |
| oscillator.stop(audioCtx.currentTime + 0.2); | |
| } | |
| } | |
| function dropMine(kart) { | |
| if (kart.weaponCount <= 0) return; | |
| const mine = { | |
| emoji: '๐ฆ', | |
| x: kart.x, | |
| y: kart.y, | |
| size: MINE_SIZE, | |
| lifetime: 300, | |
| owner: kart.isPlayer ? 'player' : 'ai' | |
| }; | |
| gameState.mines.push(mine); | |
| kart.weaponCount--; | |
| } | |
| function updateProjectiles(deltaTime) { | |
| for (let i = gameState.projectiles.length - 1; i >= 0; i--) { | |
| const proj = gameState.projectiles[i]; | |
| // Update position | |
| proj.x += Math.cos(proj.angle) * proj.speed * deltaTime * 60; | |
| proj.y += Math.sin(proj.angle) * proj.speed * deltaTime * 60; | |
| // Decrease lifetime | |
| proj.lifetime--; | |
| // Remove if expired or out of bounds | |
| if (proj.lifetime <= 0 || | |
| proj.x < 0 || proj.x > canvas.width || | |
| proj.y < 0 || proj.y > canvas.height) { | |
| gameState.projectiles.splice(i, 1); | |
| } | |
| } | |
| } | |
| function updateMines(deltaTime) { | |
| for (let i = gameState.mines.length - 1; i >= 0; i--) { | |
| const mine = gameState.mines[i]; | |
| // Decrease lifetime | |
| mine.lifetime--; | |
| // Remove if expired | |
| if (mine.lifetime <= 0) { | |
| gameState.mines.splice(i, 1); | |
| } | |
| } | |
| } | |
| function updateExplosions(deltaTime) { | |
| for (let i = gameState.explosions.length - 1; i >= 0; i--) { | |
| const explosion = gameState.explosions[i]; | |
| // Update size and opacity | |
| explosion.size += 2; | |
| explosion.opacity -= 0.02; | |
| // Remove if faded out | |
| if (explosion.opacity <= 0) { | |
| gameState.explosions.splice(i, 1); | |
| } | |
| } | |
| } | |
| function updateAI(deltaTime) { | |
| gameState.karts.forEach(kart => { | |
| if (kart.isPlayer) return; | |
| // Simple AI that follows waypoints | |
| const target = kart.waypoints[kart.nextWaypoint]; | |
| const targetX = target.x * TILE_SIZE + TILE_SIZE/2; | |
| const targetY = target.y * TILE_SIZE + TILE_SIZE/2; | |
| // Calculate angle to target | |
| const dx = targetX - kart.x; | |
| const dy = targetY - kart.y; | |
| const targetAngle = Math.atan2(dy, dx); | |
| // Adjust angle gradually | |
| let angleDiff = targetAngle - kart.angle; | |
| // Normalize angle difference | |
| while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI; | |
| while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI; | |
| // Turn towards target | |
| if (angleDiff > 0.1) { | |
| kart.angle += kart.turnSpeed * deltaTime * 60; | |
| } else if (angleDiff < -0.1) { | |
| kart.angle -= kart.turnSpeed * deltaTime * 60; | |
| } | |
| // Accelerate | |
| kart.speed = Math.min(kart.maxSpeed, kart.speed + kart.acceleration * deltaTime * 60); | |
| // Check if reached waypoint | |
| const distance = Math.sqrt(dx*dx + dy*dy); | |
| if (distance < 30) { | |
| kart.nextWaypoint = (kart.nextWaypoint + 1) % kart.waypoints.length; | |
| } | |
| // Random weapon firing | |
| if (Math.random() < 0.01 && kart.weaponCount > 0) { | |
| if (Math.random() < 0.7) { | |
| fireWeapon(kart); | |
| } else { | |
| dropMine(kart); | |
| } | |
| } | |
| }); | |
| } | |
| function checkCollisions() { | |
| // Projectiles vs Karts | |
| for (let i = gameState.projectiles.length - 1; i >= 0; i--) { | |
| const proj = gameState.projectiles[i]; | |
| for (let j = 0; j < gameState.karts.length; j++) { | |
| const kart = gameState.karts[j]; | |
| // Don't hit owner | |
| if ((proj.owner === 'player' && kart.isPlayer) || | |
| (proj.owner === 'ai' && !kart.isPlayer)) { | |
| continue; | |
| } | |
| // Check collision | |
| const dx = proj.x - kart.x; | |
| const dy = proj.y - kart.y; | |
| const distance = Math.sqrt(dx*dx + dy*dy); | |
| if (distance < KART_SIZE) { | |
| // Hit effect | |
| kart.speed *= 0.5; // Slow down | |
| // Create explosion | |
| gameState.explosions.push({ | |
| emoji: '๐ฅ', | |
| x: proj.x, | |
| y: proj.y, | |
| size: 20, | |
| opacity: 1.0 | |
| }); | |
| // Remove projectile | |
| gameState.projectiles.splice(i, 1); | |
| break; | |
| } | |
| } | |
| } | |
| // Mines vs Karts | |
| for (let i = gameState.mines.length - 1; i >= 0; i--) { | |
| const mine = gameState.mines[i]; | |
| for (let j = 0; j < gameState.karts.length; j++) { | |
| const kart = gameState.karts[j]; | |
| // Don't hit owner | |
| if ((mine.owner === 'player' && kart.isPlayer) || | |
| (mine.owner === 'ai' && !kart.isPlayer)) { | |
| continue; | |
| } | |
| // Check collision | |
| const dx = mine.x - kart.x; | |
| const dy = mine.y - kart.y; | |
| const distance = Math.sqrt(dx*dx + dy*dy); | |
| if (distance < KART_SIZE + mine.size/2) { | |
| // Hit effect | |
| kart.speed *= 0.3; // Big slow down | |
| // Create explosion | |
| gameState.explosions.push({ | |
| emoji: '๐ฅ', | |
| x: mine.x, | |
| y: mine.y, | |
| size: 40, | |
| opacity: 1.0 | |
| }); | |
| // Remove mine | |
| gameState.mines.splice(i, 1); | |
| break; | |
| } | |
| } | |
| } | |
| // Karts vs Karts | |
| for (let i = 0; i < gameState.karts.length; i++) { | |
| for (let j = i + 1; j < gameState.karts.length; j++) { | |
| const kart1 = gameState.karts[i]; | |
| const kart2 = gameState.karts[j]; | |
| const dx = kart1.x - kart2.x; | |
| const dy = kart1.y - kart2.y; | |
| const distance = Math.sqrt(dx*dx + dy*dy); | |
| if (distance < KART_SIZE * 1.5) { | |
| // Push karts apart | |
| const pushForce = 0.5; | |
| const angle = Math.atan2(dy, dx); | |
| kart1.x += Math.cos(angle) * pushForce; | |
| kart1.y += Math.sin(angle) * pushForce; | |
| kart2.x -= Math.cos(angle) * pushForce; | |
| kart2.y -= Math.sin(angle) * pushForce; | |
| // Slow down both karts | |
| kart1.speed *= 0.8; | |
| kart2.speed *= 0.8; | |
| } | |
| } | |
| } | |
| } | |
| function checkLapCompletion() { | |
| const finishLine = findFinishPosition(); | |
| const finishRect = { | |
| x: finishLine.x - TILE_SIZE/2, | |
| y: finishLine.y - TILE_SIZE/2, | |
| width: TILE_SIZE, | |
| height: TILE_SIZE | |
| }; | |
| gameState.karts.forEach(kart => { | |
| // Check if kart is crossing finish line | |
| if (kart.x > finishRect.x && kart.x < finishRect.x + finishRect.width && | |
| kart.y > finishRect.y && kart.y < finishRect.y + finishRect.height) { | |
| // Check direction (prevent multiple lap counts when sitting on line) | |
| const angleToFinish = Math.atan2(finishLine.y - kart.y, finishLine.x - kart.x); | |
| const angleDiff = Math.abs(normalizeAngle(kart.angle - angleToFinish)); | |
| if (angleDiff < Math.PI/2) { | |
| kart.lap++; | |
| kart.checkPoint = 0; | |
| if (kart.isPlayer) { | |
| gameState.lapTimes.push(gameState.raceTime); | |
| if (kart.lap <= MAX_LAPS) { | |
| gameState.lap = kart.lap; | |
| // Check if race finished | |
| if (kart.lap > MAX_LAPS) { | |
| finishRace(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function findFinishPosition() { | |
| for (let y = 0; y < gameState.track.length; y++) { | |
| for (let x = 0; x < gameState.track[y].length; x++) { | |
| if (gameState.track[y][x] === 'F') { | |
| return { | |
| x: x * TILE_SIZE + TILE_SIZE/2, | |
| y: y * TILE_SIZE + TILE_SIZE/2 | |
| }; | |
| } | |
| } | |
| } | |
| return {x: 100, y: 100}; // Fallback | |
| } | |
| function normalizeAngle(angle) { | |
| while (angle > Math.PI) angle -= 2 * Math.PI; | |
| while (angle < -Math.PI) angle += 2 * Math.PI; | |
| return angle; | |
| } | |
| function updatePosition() { | |
| // Simple position tracking based on lap progress | |
| if (gameState.lap <= MAX_LAPS) { | |
| const playerKart = gameState.karts.find(k => k.isPlayer); | |
| // Sort karts by lap progress | |
| gameState.karts.sort((a, b) => { | |
| if (a.lap !== b.lap) return b.lap - a.lap; | |
| return b.checkPoint - a.checkPoint; | |
| }); | |
| // Find player position | |
| for (let i = 0; i < gameState.karts.length; i++) { | |
| if (gameState.karts[i].isPlayer) { | |
| gameState.position = i + 1; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| function updateHUD() { | |
| const playerKart = gameState.karts.find(k => k.isPlayer); | |
| // Update lap counter | |
| elements.lapCounter.textContent = Math.min(gameState.lap, MAX_LAPS); | |
| // Update speed | |
| elements.speedCounter.textContent = Math.abs(Math.round(playerKart.speed * 10)); | |
| // Update position | |
| const positions = ['1st', '2nd', '3rd', '4th']; | |
| elements.positionCounter.textContent = positions[gameState.position - 1] || `${gameState.position}th`; | |
| // Update time | |
| const minutes = Math.floor(gameState.raceTime / 60); | |
| const seconds = Math.floor(gameState.raceTime % 60); | |
| const milliseconds = Math.floor((gameState.raceTime % 1) * 100); | |
| elements.timeCounter.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`; | |
| // Update weapon | |
| elements.weaponDisplay.textContent = playerKart.weapon; | |
| elements.weaponCount.textContent = `x${playerKart.weaponCount}`; | |
| } | |
| function finishRace() { | |
| gameState.screen = 'results'; | |
| elements.gameHud.classList.add('hidden'); | |
| elements.resultsScreen.classList.remove('hidden'); | |
| // Calculate best lap | |
| if (gameState.lapTimes.length > 0) { | |
| gameState.bestLapTime = Math.min(...gameState.lapTimes); | |
| } | |
| // Display results | |
| const positions = ['1st ๐', '2nd ๐ฅ', '3rd ๐ฅ', '4th']; | |
| elements.resultPosition.textContent = positions[gameState.position - 1] || `${gameState.position}th`; | |
| const totalMinutes = Math.floor(gameState.raceTime / 60); | |
| const totalSeconds = Math.floor(gameState.raceTime % 60); | |
| const totalMilliseconds = Math.floor((gameState.raceTime % 1) * 100); | |
| elements.resultTime.textContent = `${totalMinutes}:${totalSeconds.toString().padStart(2, '0')}.${totalMilliseconds.toString().padStart(2, '0')}`; | |
| const bestMinutes = Math.floor(gameState.bestLapTime / 60); | |
| const bestSeconds = Math.floor(gameState.bestLapTime % 60); | |
| const bestMilliseconds = Math.floor((gameState.bestLapTime % 1) * 100); | |
| elements.resultBestLap.textContent = `${bestMinutes}:${bestSeconds.toString().padStart(2, '0')}.${bestMilliseconds.toString().padStart(2, '0')}`; | |
| // Save progress | |
| try { | |
| const saveData = JSON.parse(localStorage.getItem('whackySave')) || { cupsCompleted: 0, totalPoints: 0 }; | |
| if (gameState.position === 1) { | |
| saveData.cupsCompleted = Math.max(saveData.cupsCompleted, 1); | |
| saveData.totalPoints += 10; | |
| } | |
| localStorage.setItem('whackySave', JSON.stringify(saveData)); | |
| } catch (e) { | |
| console.error('Failed to save game progress', e); | |
| } | |
| } | |
| function drawGame() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Calculate camera position (follow player) | |
| const playerKart = gameState.karts.find(k => k.isPlayer); | |
| const cameraX = playerKart ? playerKart.x - canvas.width/2 : 0; | |
| const cameraY = playerKart ? playerKart.y - canvas.height/2 : 0; | |
| // Draw track | |
| drawTrack(cameraX, cameraY); | |
| // Draw mines | |
| drawMines(cameraX, cameraY); | |
| // Draw projectiles | |
| drawProjectiles(cameraX, cameraY); | |
| // Draw explosions | |
| drawExplosions(cameraX, cameraY); | |
| // Draw karts | |
| drawKarts(cameraX, cameraY); | |
| // Draw minimap (if player exists) | |
| if (playerKart) { | |
| drawMinimap(); | |
| } | |
| } | |
| function drawTrack(cameraX, cameraY) { | |
| const startX = Math.max(0, Math.floor(cameraX / TILE_SIZE) - 1); | |
| const startY = Math.max(0, Math.floor(cameraY / TILE_SIZE) - 1); | |
| const endX = Math.min(gameState.track[0].length, Math.ceil((cameraX + canvas.width) / TILE_SIZE) + 1); | |
| const endY = Math.min(gameState.track.length, Math.ceil((cameraY + canvas.height) / TILE_SIZE) + 1); | |
| for (let y = startY; y < endY; y++) { | |
| for (let x = startX; x < endX; x++) { | |
| const tileType = gameState.track[y][x]; | |
| const tileInfo = TRACK_TYPES[tileType] || TRACK_TYPES['G']; | |
| // Draw tile background | |
| ctx.fillStyle = tileInfo.color; | |
| ctx.fillRect( | |
| x * TILE_SIZE - cameraX, | |
| y * TILE_SIZE - cameraY, | |
| TILE_SIZE, | |
| TILE_SIZE | |
| ); | |
| // Draw tile decorations | |
| if (tileType === 'P') { | |
| drawEmoji( | |
| 'โญ', | |
| x * TILE_SIZE + TILE_SIZE/2 - cameraX, | |
| y * TILE_SIZE + TILE_SIZE/2 - cameraY, | |
| TILE_SIZE * 0.8 | |
| ); | |
| } else if (tileType === 'S') { | |
| drawEmoji( | |
| '๐ฆ', | |
| x * TILE_SIZE + TILE_SIZE/2 - cameraX, | |
| y * TILE_SIZE + TILE_SIZE/2 - cameraY, | |
| TILE_SIZE * 0.8 | |
| ); | |
| } else if (tileType === 'F') { | |
| drawEmoji( | |
| '๐', | |
| x * TILE_SIZE + TILE_SIZE/2 - cameraX, | |
| y * TILE_SIZE + TILE_SIZE/2 - cameraY, | |
| TILE_SIZE * 0.8 | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| function drawKarts(cameraX, cameraY) { | |
| gameState.karts.forEach(kart => { | |
| // Draw kart body | |
| ctx.save(); | |
| ctx.translate(kart.x - cameraX, kart.y - cameraY); | |
| ctx.rotate(kart.angle); | |
| // Body | |
| ctx.fillStyle = kart.isPlayer ? '#e53e3e' : '#3182ce'; | |
| ctx.fillRect(-KART_SIZE/2, -KART_SIZE/2, KART_SIZE, KART_SIZE); | |
| // Details | |
| ctx.fillStyle = '#000000'; | |
| ctx.fillRect(-KART_SIZE/2 + 5, -KART_SIZE/2 + 5, KART_SIZE - 10, KART_SIZE - 10); | |
| // Restore transform | |
| ctx.restore(); | |
| // Draw emoji on top | |
| drawEmoji( | |
| kart.emoji, | |
| kart.x - cameraX, | |
| kart.y - cameraY, | |
| KART_SIZE * 1.2 | |
| ); | |
| // Draw name for AI | |
| if (!kart.isPlayer) { | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.font = '12px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText( | |
| kart.emoji + ' AI', | |
| kart.x - cameraX, | |
| kart.y - cameraY + KART_SIZE + 15 | |
| ); | |
| } | |
| }); | |
| } | |
| function drawProjectiles(cameraX, cameraY) { | |
| gameState.projectiles.forEach(proj => { | |
| drawEmoji( | |
| proj.emoji, | |
| proj.x - cameraX, | |
| proj.y - cameraY, | |
| PROJECTILE_SIZE | |
| ); | |
| }); | |
| } | |
| function drawMines(cameraX, cameraY) { | |
| gameState.mines.forEach(mine => { | |
| drawEmoji( | |
| mine.emoji, | |
| mine.x - cameraX, | |
| mine.y - cameraY, | |
| mine.size | |
| ); | |
| }); | |
| } | |
| function drawExplosions(cameraX, cameraY) { | |
| gameState.explosions.forEach(explosion => { | |
| ctx.save(); | |
| ctx.globalAlpha = explosion.opacity; | |
| drawEmoji( | |
| explosion.emoji, | |
| explosion.x - cameraX, | |
| explosion.y - cameraY, | |
| explosion.size | |
| ); | |
| ctx.restore(); | |
| }); | |
| } | |
| function drawEmoji(emoji, x, y, size) { | |
| ctx.font = `${size}px Arial`; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(emoji, x, y); | |
| } | |
| function drawMinimap() { | |
| const miniMapSize = 150; | |
| const miniMapX = canvas.width - miniMapSize - 20; | |
| const miniMapY = 20; | |
| const scale = miniMapSize / (Math.max(gameState.track[0].length, gameState.track.length) * TILE_SIZE); | |
| // Draw background | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; | |
| ctx.fillRect(miniMapX, miniMapY, miniMapSize, miniMapSize); | |
| // Draw track | |
| for (let y = 0; y < gameState.track.length; y++) { | |
| for (let x = 0; x < gameState.track[y].length; x++) { | |
| const tileType = gameState.track[y][x]; | |
| const tileInfo = TRACK_TYPES[tileType] || TRACK_TYPES['G']; | |
| ctx.fillStyle = tileInfo.color; | |
| ctx.fillRect( | |
| miniMapX + x * TILE_SIZE * scale, | |
| miniMapY + y * TILE_SIZE * scale, | |
| Math.ceil(TILE_SIZE * scale), | |
| Math.ceil(TILE_SIZE * scale) | |
| ); | |
| } | |
| } | |
| // Draw karts | |
| gameState.karts.forEach(kart => { | |
| ctx.fillStyle = kart.isPlayer ? '#ff0000' : '#0000ff'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| miniMapX + kart.x * scale, | |
| miniMapY + kart.y * scale, | |
| 3, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| }); | |
| // Draw border | |
| ctx.strokeStyle = '#ffffff'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(miniMapX, miniMapY, miniMapSize, miniMapSize); | |
| } | |
| // Initialize game | |
| document.getElementById('player-frog').click(); | |
| </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/whacky-wheels-2d" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |