Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Bitcoin Dino Runner</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| overflow: hidden; | |
| font-family: 'Arial', sans-serif; | |
| background: #121212; | |
| touch-action: manipulation; | |
| } | |
| #gameContainer { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| #gameCanvas { | |
| display: block; | |
| background: linear-gradient(to bottom, #87CEEB, #E0F7FA); | |
| } | |
| #uiContainer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| } | |
| #hud { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| color: white; | |
| font-size: 18px; | |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); | |
| } | |
| #btcBalance { | |
| font-family: 'Courier New', monospace; | |
| font-weight: bold; | |
| color: #FFD700; | |
| } | |
| #startScreen, #gameOverScreen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| z-index: 10; | |
| } | |
| #gameOverScreen { | |
| display: none; | |
| } | |
| .title { | |
| font-size: 48px; | |
| margin-bottom: 30px; | |
| color: #FFD700; | |
| text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.8); | |
| } | |
| .subtitle { | |
| font-size: 24px; | |
| margin-bottom: 40px; | |
| text-align: center; | |
| } | |
| .btn { | |
| padding: 15px 30px; | |
| font-size: 20px; | |
| background: linear-gradient(to right, #FFD700, #FFA500); | |
| border: none; | |
| border-radius: 30px; | |
| color: #121212; | |
| cursor: pointer; | |
| pointer-events: auto; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); | |
| } | |
| .btn:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); | |
| } | |
| .btn:active { | |
| transform: translateY(1px); | |
| } | |
| #controlsInfo { | |
| margin-top: 30px; | |
| font-size: 16px; | |
| opacity: 0.8; | |
| } | |
| #header { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| color: white; | |
| font-size: 12px; | |
| z-index: 20; | |
| } | |
| #header a { | |
| color: #FFD700; | |
| text-decoration: none; | |
| } | |
| @media (max-width: 768px) { | |
| .title { | |
| font-size: 36px; | |
| } | |
| .subtitle { | |
| font-size: 18px; | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| font-size: 18px; | |
| } | |
| #hud { | |
| font-size: 16px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="gameContainer"> | |
| <canvas id="gameCanvas"></canvas> | |
| <div id="uiContainer"> | |
| <div id="hud"> | |
| <div>Distance: <span id="distance">0</span>m</div> | |
| <div>BTC: <span id="btcBalance">0.00000000</span> ₿</div> | |
| <div>Speed: <span id="speed">1</span>x</div> | |
| </div> | |
| <div id="startScreen"> | |
| <h1 class="title">Bitcoin Dino Runner</h1> | |
| <p class="subtitle">Collect BTC while avoiding obstacles!</p> | |
| <button class="btn" id="startBtn">Start Game</button> | |
| <div id="controlsInfo"> | |
| <p>Controls: Arrow Keys or Swipe</p> | |
| <p>↑ Jump | ↓ Slide | ← → Move</p> | |
| </div> | |
| </div> | |
| <div id="gameOverScreen"> | |
| <h1 class="title">Game Over</h1> | |
| <p class="subtitle">You collected <span id="finalBtc">0.00000000</span> BTC!</p> | |
| <button class="btn" id="restartBtn">Play Again</button> | |
| </div> | |
| </div> | |
| <div id="header"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </div> | |
| </div> | |
| <script> | |
| // Game Constants | |
| const LANES = 3; | |
| const LANE_WIDTH = 100; | |
| const PLAYER_WIDTH = 60; | |
| const PLAYER_HEIGHT = 80; | |
| const JUMP_HEIGHT = 120; | |
| const JUMP_DURATION = 800; // ms | |
| const SLIDE_DURATION = 1000; // ms | |
| const BASE_SPEED = 5; | |
| const SPEED_INCREASE = 0.001; | |
| const OBSTACLE_SPAWN_RATE = 120; // frames | |
| const COIN_SPAWN_RATE = 30; // frames | |
| const POWERUP_SPAWN_RATE = 500; // frames | |
| const COIN_VALUE = 0.00000001; | |
| const BIG_COIN_VALUE = 0.00000005; | |
| // Game Variables | |
| let canvas, ctx; | |
| let gameWidth, gameHeight; | |
| let gameActive = false; | |
| let gameOver = false; | |
| let distance = 0; | |
| let btcBalance = 0; | |
| let gameSpeed = 1; | |
| let currentLane = 1; // 0: left, 1: middle, 2: right | |
| let playerState = 'running'; // running, jumping, sliding | |
| let jumpStartTime = 0; | |
| let slideStartTime = 0; | |
| let lastObstacleSpawn = 0; | |
| let lastCoinSpawn = 0; | |
| let lastPowerupSpawn = 0; | |
| let obstacles = []; | |
| let coins = []; | |
| let powerups = []; | |
| let particles = []; | |
| let backgroundOffset = 0; | |
| let midgroundOffset = 0; | |
| let foregroundOffset = 0; | |
| let animationFrameId; | |
| let lastTime = 0; | |
| let combo = 0; | |
| let comboTimeout = null; | |
| let powerupActive = null; | |
| let powerupEndTime = 0; | |
| let shakeOffset = { x: 0, y: 0 }; | |
| let magnetRadius = 0; | |
| // DOM Elements | |
| const distanceElement = document.getElementById('distance'); | |
| const btcBalanceElement = document.getElementById('btcBalance'); | |
| const speedElement = document.getElementById('speed'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const finalBtcElement = document.getElementById('finalBtc'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const restartBtn = document.getElementById('restartBtn'); | |
| // Initialize game | |
| function init() { | |
| canvas = document.getElementById('gameCanvas'); | |
| ctx = canvas.getContext('2d'); | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // Event listeners | |
| startBtn.addEventListener('click', startGame); | |
| restartBtn.addEventListener('click', restartGame); | |
| // Keyboard controls | |
| document.addEventListener('keydown', handleKeyDown); | |
| // Touch controls | |
| canvas.addEventListener('touchstart', handleTouchStart); | |
| canvas.addEventListener('touchmove', handleTouchMove); | |
| // Preload assets (in a real game, you'd want to preload images) | |
| drawStartScreen(); | |
| } | |
| function resizeCanvas() { | |
| gameWidth = window.innerWidth; | |
| gameHeight = window.innerHeight; | |
| canvas.width = gameWidth; | |
| canvas.height = gameHeight; | |
| // Redraw if needed | |
| if (!gameActive) { | |
| drawStartScreen(); | |
| } | |
| } | |
| function drawStartScreen() { | |
| // Draw animated dinosaur on start screen | |
| ctx.clearRect(0, 0, gameWidth, gameHeight); | |
| // Draw background | |
| drawBackground(0); | |
| // Draw dinosaur | |
| const dinoX = gameWidth / 2; | |
| const dinoY = gameHeight / 2 + 50; | |
| const headBob = Math.sin(Date.now() / 200) * 5; | |
| drawDino(dinoX, dinoY + headBob, 1.5, 'running'); | |
| } | |
| function startGame() { | |
| // Reset game state | |
| gameActive = true; | |
| gameOver = false; | |
| distance = 0; | |
| btcBalance = 0; | |
| gameSpeed = 1; | |
| currentLane = 1; | |
| playerState = 'running'; | |
| obstacles = []; | |
| coins = []; | |
| powerups = []; | |
| particles = []; | |
| combo = 0; | |
| powerupActive = null; | |
| // Update UI | |
| startScreen.style.display = 'none'; | |
| gameOverScreen.style.display = 'none'; | |
| updateHUD(); | |
| // Start game loop | |
| lastTime = performance.now(); | |
| gameLoop(lastTime); | |
| } | |
| function restartGame() { | |
| gameOverScreen.style.display = 'none'; | |
| startGame(); | |
| } | |
| function endGame() { | |
| gameActive = false; | |
| gameOver = true; | |
| finalBtcElement.textContent = btcBalance.toFixed(8); | |
| gameOverScreen.style.display = 'flex'; | |
| cancelAnimationFrame(animationFrameId); | |
| } | |
| function gameLoop(timestamp) { | |
| if (!gameActive) return; | |
| const deltaTime = timestamp - lastTime; | |
| lastTime = timestamp; | |
| // Update game state | |
| update(deltaTime); | |
| // Render game | |
| render(); | |
| // Continue loop | |
| animationFrameId = requestAnimationFrame(gameLoop); | |
| } | |
| function update(deltaTime) { | |
| // Increase distance and speed | |
| distance += gameSpeed; | |
| gameSpeed = BASE_SPEED + (distance / 10000) * SPEED_INCREASE; | |
| // Update player state | |
| updatePlayer(deltaTime); | |
| // Spawn obstacles and coins | |
| spawnObjects(); | |
| // Update objects | |
| updateObstacles(); | |
| updateCoins(); | |
| updatePowerups(); | |
| updateParticles(); | |
| // Check collisions | |
| checkCollisions(); | |
| // Update powerups | |
| updateActivePowerup(timestamp); | |
| // Update combo | |
| if (combo > 0 && comboTimeout && timestamp > comboTimeout) { | |
| combo = 0; | |
| comboTimeout = null; | |
| } | |
| // Update HUD | |
| updateHUD(); | |
| // Screen shake at high speeds | |
| if (gameSpeed > 10) { | |
| shakeOffset.x = (Math.random() - 0.5) * 2 * (gameSpeed - 10) / 2; | |
| shakeOffset.y = (Math.random() - 0.5) * 2 * (gameSpeed - 10) / 2; | |
| } else { | |
| shakeOffset.x = 0; | |
| shakeOffset.y = 0; | |
| } | |
| } | |
| function updatePlayer(deltaTime) { | |
| // Handle jump animation | |
| if (playerState === 'jumping') { | |
| const jumpProgress = (performance.now() - jumpStartTime) / JUMP_DURATION; | |
| if (jumpProgress >= 1) { | |
| playerState = 'running'; | |
| } | |
| } | |
| // Handle slide animation | |
| if (playerState === 'sliding') { | |
| const slideProgress = (performance.now() - slideStartTime) / SLIDE_DURATION; | |
| if (slideProgress >= 1) { | |
| playerState = 'running'; | |
| } | |
| } | |
| } | |
| function spawnObjects() { | |
| // Spawn obstacles | |
| if (distance - lastObstacleSpawn > OBSTACLE_SPAWN_RATE / gameSpeed) { | |
| const lane = Math.floor(Math.random() * LANES); | |
| const type = Math.random() > 0.3 ? 'barrier' : 'gap'; | |
| obstacles.push({ | |
| x: getLaneX(lane), | |
| y: -100, | |
| width: LANE_WIDTH - 20, | |
| height: type === 'gap' ? 0 : 60, | |
| type: type, | |
| lane: lane | |
| }); | |
| lastObstacleSpawn = distance; | |
| } | |
| // Spawn coins | |
| if (distance - lastCoinSpawn > COIN_SPAWN_RATE / gameSpeed) { | |
| const lane = Math.floor(Math.random() * LANES); | |
| const isBig = Math.random() > 0.9; | |
| coins.push({ | |
| x: getLaneX(lane), | |
| y: -50, | |
| radius: isBig ? 25 : 20, | |
| value: isBig ? BIG_COIN_VALUE : COIN_VALUE, | |
| isBig: isBig, | |
| rotation: 0, | |
| rotationSpeed: Math.random() * 0.1 + 0.05 | |
| }); | |
| lastCoinSpawn = distance; | |
| } | |
| // Spawn powerups | |
| if (distance - lastPowerupSpawn > POWERUP_SPAWN_RATE / gameSpeed && !powerupActive) { | |
| const lane = Math.floor(Math.random() * LANES); | |
| const types = ['magnet', 'multiplier', 'shield', 'speed']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| powerups.push({ | |
| x: getLaneX(lane), | |
| y: -50, | |
| radius: 25, | |
| type: type, | |
| rotation: 0 | |
| }); | |
| lastPowerupSpawn = distance; | |
| } | |
| } | |
| function updateObstacles() { | |
| for (let i = obstacles.length - 1; i >= 0; i--) { | |
| obstacles[i].y += gameSpeed; | |
| // Remove off-screen obstacles | |
| if (obstacles[i].y > gameHeight) { | |
| obstacles.splice(i, 1); | |
| } | |
| } | |
| } | |
| function updateCoins() { | |
| for (let i = coins.length - 1; i >= 0; i--) { | |
| coins[i].y += gameSpeed; | |
| coins[i].rotation += coins[i].rotationSpeed; | |
| // Coin magnet effect | |
| if (powerupActive === 'magnet' && magnetRadius > 0) { | |
| const dx = coins[i].x - getLaneX(currentLane); | |
| const dy = coins[i].y - (gameHeight - 150); | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < magnetRadius) { | |
| const pullStrength = (magnetRadius - distance) / magnetRadius * 10; | |
| coins[i].x -= dx * 0.05 * pullStrength; | |
| coins[i].y -= dy * 0.05 * pullStrength; | |
| } | |
| } | |
| // Remove off-screen coins | |
| if (coins[i].y > gameHeight + 50) { | |
| coins.splice(i, 1); | |
| } | |
| } | |
| // Update magnet radius | |
| if (powerupActive === 'magnet') { | |
| magnetRadius = 200 + Math.sin(Date.now() / 200) * 20; | |
| } else { | |
| magnetRadius = 0; | |
| } | |
| } | |
| function updatePowerups() { | |
| for (let i = powerups.length - 1; i >= 0; i--) { | |
| powerups[i].y += gameSpeed; | |
| powerups[i].rotation += 0.05; | |
| // Remove off-screen powerups | |
| if (powerups[i].y > gameHeight + 50) { | |
| powerups.splice(i, 1); | |
| } | |
| } | |
| } | |
| function updateParticles() { | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| particles[i].x += particles[i].vx; | |
| particles[i].y += particles[i].vy; | |
| particles[i].life--; | |
| if (particles[i].life <= 0) { | |
| particles.splice(i, 1); | |
| } | |
| } | |
| } | |
| function updateActivePowerup(timestamp) { | |
| if (powerupActive && timestamp > powerupEndTime) { | |
| powerupActive = null; | |
| powerupEndTime = 0; | |
| } | |
| } | |
| function checkCollisions() { | |
| const playerX = getLaneX(currentLane); | |
| const playerY = gameHeight - 150; | |
| const playerHeight = playerState === 'sliding' ? PLAYER_HEIGHT / 2 : PLAYER_HEIGHT; | |
| const playerBottom = playerY + playerHeight; | |
| // Check coin collisions | |
| for (let i = coins.length - 1; i >= 0; i--) { | |
| const dx = coins[i].x - playerX; | |
| const dy = coins[i].y - playerY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < coins[i].radius + PLAYER_WIDTH / 2) { | |
| // Collect coin | |
| let value = coins[i].value; | |
| // Apply multiplier if active | |
| if (powerupActive === 'multiplier') { | |
| value *= 2; | |
| } | |
| btcBalance += value; | |
| coins.splice(i, 1); | |
| // Add combo | |
| combo++; | |
| if (comboTimeout) clearTimeout(comboTimeout); | |
| comboTimeout = timestamp + 2000; // 2 second combo window | |
| // Create particles | |
| createParticles(coins[i].x, coins[i].y, coins[i].isBig ? 'gold' : 'yellow', 10); | |
| } | |
| } | |
| // Check powerup collisions | |
| for (let i = powerups.length - 1; i >= 0; i--) { | |
| const dx = powerups[i].x - playerX; | |
| const dy = powerups[i].y - playerY; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < powerups[i].radius + PLAYER_WIDTH / 2) { | |
| // Activate powerup | |
| powerupActive = powerups[i].type; | |
| powerupEndTime = performance.now() + 10000; // 10 seconds | |
| powerups.splice(i, 1); | |
| // Create particles | |
| createParticles(powerups[i].x, powerups[i].y, 'blue', 15); | |
| } | |
| } | |
| // Check obstacle collisions | |
| if (powerupActive === 'shield') return; // Shield protects from collisions | |
| for (let i = 0; i < obstacles.length; i++) { | |
| const obstacle = obstacles[i]; | |
| // Check if in same lane | |
| if (obstacle.lane !== currentLane) continue; | |
| // Check vertical position | |
| if (obstacle.y + obstacle.height < playerY) continue; | |
| if (obstacle.y > playerBottom) continue; | |
| // For gaps, check if player is jumping | |
| if (obstacle.type === 'gap' && playerState !== 'jumping') { | |
| endGame(); | |
| return; | |
| } | |
| // For barriers, check if player is sliding | |
| if (obstacle.type === 'barrier') { | |
| if (playerState !== 'sliding' || obstacle.y > playerY + PLAYER_HEIGHT / 2) { | |
| endGame(); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| function createParticles(x, y, color, count) { | |
| for (let i = 0; i < count; i++) { | |
| particles.push({ | |
| x: x, | |
| y: y, | |
| vx: (Math.random() - 0.5) * 4, | |
| vy: (Math.random() - 0.5) * 4, | |
| radius: Math.random() * 3 + 2, | |
| color: color, | |
| life: Math.random() * 30 + 30 | |
| }); | |
| } | |
| } | |
| function updateHUD() { | |
| distanceElement.textContent = Math.floor(distance / 10); | |
| btcBalanceElement.textContent = btcBalance.toFixed(8); | |
| speedElement.textContent = gameSpeed.toFixed(2); | |
| } | |
| function render() { | |
| // Clear canvas with shake offset | |
| ctx.save(); | |
| ctx.translate(shakeOffset.x, shakeOffset.y); | |
| ctx.clearRect(0, 0, gameWidth, gameHeight); | |
| // Draw background layers with parallax | |
| drawBackground(backgroundOffset); | |
| // Draw game objects | |
| drawObstacles(); | |
| drawPowerups(); | |
| drawCoins(); | |
| drawParticles(); | |
| // Draw player | |
| const playerX = getLaneX(currentLane); | |
| const playerY = gameHeight - 150; | |
| drawDino(playerX, playerY, 1, playerState); | |
| // Draw powerup indicator | |
| if (powerupActive) { | |
| drawPowerupIndicator(); | |
| } | |
| // Draw combo | |
| if (combo > 1) { | |
| drawCombo(); | |
| } | |
| ctx.restore(); | |
| } | |
| function drawBackground(offset) { | |
| // Sky gradient | |
| const skyGradient = ctx.createLinearGradient(0, 0, 0, gameHeight); | |
| skyGradient.addColorStop(0, '#87CEEB'); | |
| skyGradient.addColorStop(1, '#E0F7FA'); | |
| ctx.fillStyle = skyGradient; | |
| ctx.fillRect(0, 0, gameWidth, gameHeight); | |
| // Distant mountains | |
| ctx.fillStyle = '#5D4037'; | |
| for (let i = 0; i < 5; i++) { | |
| const x = (i * 400 - offset * 0.2) % (gameWidth + 400) - 200; | |
| ctx.beginPath(); | |
| ctx.moveTo(x, gameHeight - 200); | |
| ctx.lineTo(x + 200, gameHeight - 300); | |
| ctx.lineTo(x + 400, gameHeight - 200); | |
| ctx.fill(); | |
| } | |
| // Ground | |
| ctx.fillStyle = '#8BC34A'; | |
| ctx.fillRect(0, gameHeight - 100, gameWidth, 100); | |
| // Lane markers | |
| ctx.strokeStyle = '#FFFFFF'; | |
| ctx.lineWidth = 2; | |
| for (let lane = 1; lane < LANES; lane++) { | |
| const x = getLaneX(lane) - LANE_WIDTH / 2; | |
| ctx.setLineDash([20, 20]); | |
| ctx.beginPath(); | |
| ctx.moveTo(x, gameHeight - 100); | |
| ctx.lineTo(x, gameHeight); | |
| ctx.stroke(); | |
| } | |
| ctx.setLineDash([]); | |
| // Update background offsets | |
| backgroundOffset += gameSpeed * 0.2; | |
| midgroundOffset += gameSpeed * 0.5; | |
| foregroundOffset += gameSpeed; | |
| } | |
| function drawDino(x, y, scale, state) { | |
| ctx.save(); | |
| ctx.translate(x, y); | |
| ctx.scale(scale, scale); | |
| // Body color | |
| ctx.fillStyle = '#4CAF50'; | |
| // Calculate animation offsets | |
| let bodyYOffset = 0; | |
| let headBob = 0; | |
| let legOffset = 0; | |
| if (state === 'running') { | |
| headBob = Math.sin(Date.now() / 200) * 5; | |
| legOffset = Math.sin(Date.now() / 100) * 10; | |
| } else if (state === 'jumping') { | |
| const jumpProgress = (performance.now() - jumpStartTime) / JUMP_DURATION; | |
| const jumpHeight = JUMP_HEIGHT * Math.sin(jumpProgress * Math.PI); | |
| bodyYOffset = -jumpHeight; | |
| } else if (state === 'sliding') { | |
| bodyYOffset = PLAYER_HEIGHT / 4; | |
| } | |
| // Body | |
| ctx.beginPath(); | |
| ctx.ellipse(0, bodyYOffset + 20, 30, 40, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Head | |
| ctx.beginPath(); | |
| ctx.ellipse(0, bodyYOffset - 30 + headBob, 25, 20, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Eyes | |
| ctx.fillStyle = 'white'; | |
| ctx.beginPath(); | |
| ctx.arc(-10, bodyYOffset - 35 + headBob, 5, 0, Math.PI * 2); | |
| ctx.arc(10, bodyYOffset - 35 + headBob, 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'black'; | |
| ctx.beginPath(); | |
| ctx.arc(-10, bodyYOffset - 35 + headBob, 2, 0, Math.PI * 2); | |
| ctx.arc(10, bodyYOffset - 35 + headBob, 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Headband | |
| ctx.strokeStyle = 'red'; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| ctx.arc(0, bodyYOffset - 30 + headBob, 20, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| // Bitcoin symbol on headband | |
| ctx.fillStyle = 'gold'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('₿', 0, bodyYOffset - 30 + headBob); | |
| // Legs | |
| ctx.fillStyle = '#4CAF50'; | |
| if (state === 'sliding') { | |
| // Sliding pose | |
| ctx.beginPath(); | |
| ctx.ellipse(-20, bodyYOffset + 50, 10, 5, Math.PI/4, 0, Math.PI * 2); | |
| ctx.ellipse(20, bodyYOffset + 50, 10, 5, -Math.PI/4, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } else { | |
| // Running/jumping pose | |
| ctx.beginPath(); | |
| ctx.ellipse(-15 + legOffset, bodyYOffset + 50, 10, 5, 0, 0, Math.PI * 2); | |
| ctx.ellipse(15 - legOffset, bodyYOffset + 50, 10, 5, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| // Tail | |
| ctx.beginPath(); | |
| ctx.moveTo(30, bodyYOffset + 10); | |
| ctx.quadraticCurveTo(50, bodyYOffset, 60, bodyYOffset + 20); | |
| ctx.lineWidth = 8; | |
| ctx.strokeStyle = '#4CAF50'; | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| function drawObstacles() { | |
| obstacles.forEach(obstacle => { | |
| if (obstacle.type === 'barrier') { | |
| // Draw barrier | |
| ctx.fillStyle = '#795548'; | |
| ctx.fillRect( | |
| obstacle.x - obstacle.width / 2, | |
| obstacle.y, | |
| obstacle.width, | |
| obstacle.height | |
| ); | |
| // Add some details | |
| ctx.fillStyle = '#5D4037'; | |
| ctx.fillRect( | |
| obstacle.x - obstacle.width / 2 + 5, | |
| obstacle.y + 5, | |
| obstacle.width - 10, | |
| obstacle.height - 10 | |
| ); | |
| } else if (obstacle.type === 'gap') { | |
| // Draw gap indicators | |
| ctx.strokeStyle = '#FF5722'; | |
| ctx.lineWidth = 3; | |
| ctx.setLineDash([10, 5]); | |
| ctx.beginPath(); | |
| ctx.moveTo(obstacle.x - LANE_WIDTH / 2 + 10, obstacle.y + 20); | |
| ctx.lineTo(obstacle.x + LANE_WIDTH / 2 - 10, obstacle.y + 20); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| } | |
| }); | |
| } | |
| function drawCoins() { | |
| coins.forEach(coin => { | |
| ctx.save(); | |
| ctx.translate(coin.x, coin.y); | |
| ctx.rotate(coin.rotation); | |
| // Coin gradient | |
| const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, coin.radius); | |
| gradient.addColorStop(0, coin.isBig ? '#FFD700' : '#FFEB3B'); | |
| gradient.addColorStop(0.7, coin.isBig ? '#FFC107' : '#FFD600'); | |
| gradient.addColorStop(1, coin.isBig ? '#FFA000' : '#FFC107'); | |
| ctx.fillStyle = gradient; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, coin.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Bitcoin symbol | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; | |
| ctx.font = `bold ${coin.radius}px Arial`; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('₿', 0, 0); | |
| // Glow effect | |
| if (Math.sin(Date.now() / 200) > 0.8) { | |
| ctx.shadowColor = 'gold'; | |
| ctx.shadowBlur = 15; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, coin.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| }); | |
| } | |
| function drawPowerups() { | |
| powerups.forEach(powerup => { | |
| ctx.save(); | |
| ctx.translate(powerup.x, powerup.y); | |
| ctx.rotate(powerup.rotation); | |
| // Draw different powerups | |
| switch (powerup.type) { | |
| case 'magnet': | |
| // Magnet powerup | |
| ctx.fillStyle = '#2196F3'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, powerup.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 20px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('M', 0, 0); | |
| break; | |
| case 'multiplier': | |
| // Multiplier powerup | |
| ctx.fillStyle = '#FF9800'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, powerup.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 20px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('2×', 0, 0); | |
| break; | |
| case 'shield': | |
| // Shield powerup | |
| ctx.fillStyle = '#4CAF50'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, powerup.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 20px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('S', 0, 0); | |
| break; | |
| case 'speed': | |
| // Speed powerup | |
| ctx.fillStyle = '#F44336'; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, powerup.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 20px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('⚡', 0, 0); | |
| break; | |
| } | |
| // Glow effect | |
| ctx.shadowColor = 'rgba(255, 255, 255, 0.7)'; | |
| ctx.shadowBlur = 10; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, powerup.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| }); | |
| } | |
| function drawParticles() { | |
| particles.forEach(particle => { | |
| ctx.globalAlpha = particle.life / 60; | |
| ctx.fillStyle = particle.color; | |
| ctx.beginPath(); | |
| ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| }); | |
| ctx.globalAlpha = 1; | |
| } | |
| function drawPowerupIndicator() { | |
| const x = gameWidth - 50; | |
| const y = 50; | |
| const radius = 20; | |
| const timeLeft = (powerupEndTime - performance.now()) / 1000; | |
| const angle = (timeLeft / 10) * Math.PI * 2; | |
| // Draw background circle | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, radius + 5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw timer arc | |
| ctx.strokeStyle = 'white'; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, radius + 2, -Math.PI/2, -Math.PI/2 + angle); | |
| ctx.stroke(); | |
| // Draw powerup icon | |
| switch (powerupActive) { | |
| case 'magnet': | |
| ctx.fillStyle = '#2196F3'; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('M', x, y); | |
| break; | |
| case 'multiplier': | |
| ctx.fillStyle = '#FF9800'; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('2×', x, y); | |
| break; | |
| case 'shield': | |
| ctx.fillStyle = '#4CAF50'; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('S', x, y); | |
| break; | |
| case 'speed': | |
| ctx.fillStyle = '#F44336'; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 16px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('⚡', x, y); | |
| break; | |
| } | |
| } | |
| function drawCombo() { | |
| const x = gameWidth / 2; | |
| const y = 100; | |
| const scale = 1 + combo * 0.05; | |
| ctx.save(); | |
| ctx.translate(x, y); | |
| ctx.scale(scale, scale); | |
| ctx.fillStyle = 'rgba(255, 215, 0, 0.8)'; | |
| ctx.font = 'bold 24px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(`${combo}× COMBO!`, 0, 0); | |
| ctx.restore(); | |
| } | |
| function getLaneX(lane) { | |
| const centerX = gameWidth / 2; | |
| const laneOffset = (lane - 1) * LANE_WIDTH; | |
| return centerX + laneOffset; | |
| } | |
| function handleKeyDown(e) { | |
| if (!gameActive) return; | |
| switch (e.key) { | |
| case 'ArrowLeft': | |
| moveLeft(); | |
| break; | |
| case 'ArrowRight': | |
| moveRight(); | |
| break; | |
| case 'ArrowUp': | |
| jump(); | |
| break; | |
| case 'ArrowDown': | |
| slide(); | |
| break; | |
| case ' ': | |
| jump(); | |
| break; | |
| case 'Escape': | |
| // Pause game | |
| break; | |
| } | |
| } | |
| function handleTouchStart(e) { | |
| if (!gameActive) return; | |
| e.preventDefault(); | |
| const touchX = e.touches[0].clientX; | |
| const touchY = e.touches[0].clientY; | |
| // Check if touch is in left or right half of screen | |
| if (touchX < gameWidth / 2) { | |
| moveLeft(); | |
| } else { | |
| moveRight(); | |
| } | |
| // Jump if touch is in upper half, slide if in lower half | |
| if (touchY < gameHeight / 2) { | |
| jump(); | |
| } else { | |
| slide(); | |
| } | |
| } | |
| function handleTouchMove(e) { | |
| e.preventDefault(); | |
| } | |
| function moveLeft() { | |
| if (playerState === 'running' || playerState === 'sliding') { | |
| currentLane = Math.max(0, currentLane - 1); | |
| } | |
| } | |
| function moveRight() { | |
| if (playerState === 'running' || playerState === 'sliding') { | |
| currentLane = Math.min(LANES - 1, currentLane + 1); | |
| } | |
| } | |
| function jump() { | |
| if (playerState === 'running') { | |
| playerState = 'jumping'; | |
| jumpStartTime = performance.now(); | |
| } | |
| } | |
| function slide() { | |
| if (playerState === 'running') { | |
| playerState = 'sliding'; | |
| slideStartTime = performance.now(); | |
| } | |
| } | |
| // Start the game | |
| window.onload = init; | |
| </script> | |
| </body> | |
| </html> |