Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | |
| <title>Dino Run - Bitcoin Edition</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #f7931a; | |
| --primary-light: #ffb347; | |
| --secondary: #4ade80; | |
| --dark: #1a1a2e; | |
| --darker: #0f0f1a; | |
| --light: #ffffff; | |
| --danger: #ef4444; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Fredoka One', cursive; | |
| background: var(--darker); | |
| overflow: hidden; | |
| touch-action: none; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| } | |
| #gameCanvas { | |
| display: block; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| .screen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| background: linear-gradient(135deg, var(--darker) 0%, #16213e 50%, var(--darker) 100%); | |
| z-index: 100; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.5s ease; | |
| } | |
| .screen.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .title { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(2rem, 8vw, 5rem); | |
| font-weight: 900; | |
| background: linear-gradient(180deg, var(--primary) 0%, var(--primary-light) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-shadow: 0 0 40px rgba(247, 147, 26, 0.5); | |
| margin-bottom: 1rem; | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| } | |
| .subtitle { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(0.8rem, 3vw, 1.5rem); | |
| color: var(--secondary); | |
| margin-bottom: 2rem; | |
| letter-spacing: 0.3em; | |
| } | |
| .btn { | |
| font-family: 'Fredoka One', cursive; | |
| font-size: clamp(1rem, 4vw, 1.5rem); | |
| padding: 1rem 3rem; | |
| border: none; | |
| border-radius: 50px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); | |
| color: var(--dark); | |
| box-shadow: 0 10px 30px rgba(247, 147, 26, 0.4); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-5px) scale(1.05); | |
| box-shadow: 0 20px 40px rgba(247, 147, 26, 0.6); | |
| } | |
| .btn-primary:active { | |
| transform: translateY(0) scale(0.98); | |
| } | |
| .dino-preview { | |
| width: clamp(120px, 30vw, 200px); | |
| height: clamp(120px, 30vw, 200px); | |
| margin: 2rem 0; | |
| animation: bounce 1s ease-in-out infinite; | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-20px); } | |
| } | |
| .hud { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 1rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| z-index: 50; | |
| pointer-events: none; | |
| } | |
| .hud-item { | |
| background: rgba(15, 15, 26, 0.85); | |
| backdrop-filter: blur(10px); | |
| border: 2px solid rgba(247, 147, 26, 0.3); | |
| border-radius: 15px; | |
| padding: 0.75rem 1.25rem; | |
| color: var(--light); | |
| } | |
| .hud-label { | |
| font-size: 0.7rem; | |
| color: rgba(255, 255, 255, 0.6); | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| } | |
| .hud-value { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: clamp(1rem, 3vw, 1.5rem); | |
| font-weight: 700; | |
| } | |
| .hud-value.btc { | |
| color: var(--primary); | |
| } | |
| .hud-value.distance { | |
| color: var(--secondary); | |
| } | |
| .powerup-indicator { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 2rem; | |
| font-weight: 900; | |
| color: var(--primary); | |
| text-shadow: 0 0 20px var(--primary); | |
| opacity: 0; | |
| z-index: 60; | |
| pointer-events: none; | |
| } | |
| .powerup-indicator.active { | |
| animation: powerupPopup 1s ease-out forwards; | |
| } | |
| @keyframes powerupPopup { | |
| 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } | |
| 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); } | |
| 80% { opacity: 1; transform: translate(-50%, -60%) scale(1); } | |
| 100% { opacity: 0; transform: translate(-50%, -80%) scale(0.8); } | |
| } | |
| .controls-hint { | |
| position: fixed; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 1rem; | |
| z-index: 50; | |
| opacity: 0; | |
| transition: opacity 0.5s; | |
| } | |
| .controls-hint.visible { | |
| opacity: 1; | |
| } | |
| .control-key { | |
| width: 60px; | |
| height: 60px; | |
| background: rgba(15, 15, 26, 0.8); | |
| border: 2px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 15px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| color: var(--light); | |
| } | |
| .game-over-stats { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| margin: 2rem 0; | |
| } | |
| .stat-row { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 2rem; | |
| font-size: clamp(1rem, 3vw, 1.3rem); | |
| } | |
| .stat-label { | |
| color: rgba(255, 255, 255, 0.6); | |
| } | |
| .stat-value { | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 700; | |
| } | |
| .stat-value.btc { | |
| color: var(--primary); | |
| } | |
| .high-score { | |
| color: var(--primary-light); | |
| } | |
| .paused-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(15, 15, 26, 0.9); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 90; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s; | |
| } | |
| .paused-overlay.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .paused-text { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 3rem; | |
| color: var(--light); | |
| animation: pausePulse 1.5s ease-in-out infinite; | |
| } | |
| @keyframes pausePulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| /* Built with anycoder */ | |
| .branding { | |
| position: fixed; | |
| bottom: 0.5rem; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| font-size: 0.7rem; | |
| color: rgba(255, 255, 255, 0.3); | |
| z-index: 200; | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| } | |
| .branding:hover { | |
| color: var(--primary); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- Start Screen --> | |
| <div id="startScreen" class="screen active"> | |
| <div class="title">DINO RUN</div> | |
| <div class="subtitle">BITCOIN EDITION</div> | |
| <canvas id="dinoPreview" class="dino-preview" width="200" height="200"></canvas> | |
| <button class="btn btn-primary" id="startBtn">START GAME</button> | |
| <div style="margin-top: 2rem; color: rgba(255,255,255,0.4); font-size: 0.9rem;"> | |
| Use Arrow Keys or Touch to Play | |
| </div> | |
| </div> | |
| <!-- HUD --> | |
| <div id="hud" class="hud" style="display: none;"> | |
| <div class="hud-item"> | |
| <div class="hud-label">Balance</div> | |
| <div class="hud-value btc" id="btcBalance">₿ 0.00000000</div> | |
| </div> | |
| <div class="hud-item"> | |
| <div class="hud-label">Distance</div> | |
| <div class="hud-value distance" id="distanceDisplay">0m</div> | |
| </div> | |
| <div class="hud-item"> | |
| <div class="hud-label">Speed</div> | |
| <div class="hud-value" id="speedDisplay">100%</div> | |
| </div> | |
| </div> | |
| <!-- Power-up Indicator --> | |
| <div id="powerupIndicator" class="powerup-indicator"></div> | |
| <!-- Paused Overlay --> | |
| <div id="pausedOverlay" class="paused-overlay"> | |
| <div class="paused-text">PAUSED</div> | |
| </div> | |
| <!-- Game Over Screen --> | |
| <div id="gameOverScreen" class="screen"> | |
| <div class="title" style="font-size: clamp(2rem, 6vw, 3.5rem);">GAME OVER</div> | |
| <div class="game-over-stats"> | |
| <div class="stat-row"> | |
| <span class="stat-label">Distance</span> | |
| <span class="stat-value" id="finalDistance">0m</span> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">BTC Earned</span> | |
| <span class="stat-value btc" id="finalBtc">₿ 0.00000000</span> | |
| </div> | |
| <div class="stat-row"> | |
| <span class="stat-label">High Score</span> | |
| <span class="stat-value high-score" id="highScore">0m</span> | |
| </div> | |
| </div> | |
| <button class="btn btn-primary" id="restartBtn">PLAY AGAIN</button> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="branding">Built with anycoder</a> | |
| <script> | |
| // ===================================================== | |
| // GAME CONFIGURATION | |
| // ===================================================== | |
| const CONFIG = { | |
| LANES: 3, | |
| LANE_WIDTH: 120, | |
| INITIAL_SPEED: 400, | |
| MAX_SPEED: 1200, | |
| SPEED_INCREMENT: 5, | |
| GRAVITY: 1800, | |
| JUMP_FORCE: 700, | |
| GROUND_Y: 0, | |
| // Bitcoin values | |
| BASE_COIN_VALUE: 0.00000001, | |
| BONUS_COIN_VALUE: 0.00000005, | |
| COIN_SPAWN_CHANCE: 0.7, | |
| BONUS_COIN_CHANCE: 0.1, | |
| // Power-up durations (seconds) | |
| MAGNET_DURATION: 8, | |
| SHIELD_DURATION: 5, | |
| MULTIPLIER_DURATION: 10, | |
| SPEEDBOOST_DURATION: 5, | |
| // Spawn distances | |
| MIN_OBSTACLE_DISTANCE: 300, | |
| MAX_OBSTACLE_DISTANCE: 600, | |
| // Colors | |
| COLORS: { | |
| primary: '#f7931a', | |
| primaryLight: '#ffb347', | |
| secondary: '#4ade80', | |
| danger: '#ef4444', | |
| sky: ['#1a1a2e', '#16213e', '#0f0f1a'], | |
| ground: '#2d2d44', | |
| laneMarker: '#3d3d5c' | |
| } | |
| }; | |
| // ===================================================== | |
| // GAME STATE | |
| // ===================================================== | |
| const gameState = { | |
| screen: 'start', // start, playing, paused, gameover | |
| distance: 0, | |
| btcBalance: 0, | |
| speed: CONFIG.INITIAL_SPEED, | |
| currentLane: 1, | |
| targetLane: 1, | |
| isJumping: false, | |
| isSliding: false, | |
| jumpVelocity: 0, | |
| playerY: 0, | |
| slideTimer: 0, | |
| highScore: parseInt(localStorage.getItem('dinoRunHighScore')) || 0, | |
| // Power-ups | |
| activePowerups: { | |
| magnet: 0, | |
| shield: 0, | |
| multiplier: 1, | |
| speedboost: 0 | |
| }, | |
| // Game objects | |
| obstacles: [], | |
| coins: [], | |
| powerups: [], | |
| particles: [], | |
| // Timing | |
| lastTime: 0, | |
| spawnTimer: 0, | |
| obstacleSpawnDistance: 0, | |
| // Screen shake | |
| shakeIntensity: 0, | |
| shakeDuration: 0 | |
| }; | |
| // ===================================================== | |
| // CANVAS SETUP | |
| // ===================================================== | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let canvasWidth, canvasHeight, centerX, groundY; | |
| function resizeCanvas() { | |
| canvasWidth = canvas.width = window.innerWidth; | |
| canvasHeight = canvas.height = window.innerHeight; | |
| centerX = canvasWidth / 2; | |
| groundY = canvasHeight * 0.75; | |
| CONFIG.LANE_WIDTH = Math.min(120, canvasWidth / 6); | |
| CONFIG.GROUND_Y = groundY; | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // ===================================================== | |
| // UTILITY FUNCTIONS | |
| // ===================================================== | |
| function lerp(a, b, t) { | |
| return a + (b - a) * t; | |
| } | |
| function randomRange(min, max) { | |
| return Math.random() * (max - min) + min; | |
| } | |
| function randomInt(min, max) { | |
| return Math.floor(randomRange(min, max + 1)); | |
| } | |
| function formatBTC(value) { | |
| return '₿ ' + value.toFixed(8); | |
| } | |
| function screenShake(intensity, duration) { | |
| gameState.shakeIntensity = intensity; | |
| gameState.shakeDuration = duration; | |
| } | |
| // ===================================================== | |
| // PARTICLE SYSTEM | |
| // ===================================================== | |
| class Particle { | |
| constructor(x, y, color, type = 'default') { | |
| this.x = x; | |
| this.y = y; | |
| this.color = color; | |
| this.type = type; | |
| this.life = 1; | |
| this.decay = randomRange(0.02, 0.05); | |
| this.vx = randomRange(-100, 100); | |
| this.vy = randomRange(-200, -50); | |
| this.size = randomRange(3, 8); | |
| if (type === 'coin') { | |
| this.vy = randomRange(-150, -50); | |
| this.decay = 0.03; | |
| } else if (type === 'trail') { | |
| this.vx = randomRange(-30, 30); | |
| this.vy = 0; | |
| this.decay = 0.1; | |
| this.size = randomRange(2, 5); | |
| } else if (type === 'shield') { | |
| this.decay = 0.02; | |
| this.size = randomRange(4, 10); | |
| } | |
| } | |
| update(dt) { | |
| this.x += this.vx * dt; | |
| this.y += this.vy * dt; | |
| this.vy += 400 * dt; // gravity | |
| this.life -= this.decay; | |
| return this.life > 0; | |
| } | |
| draw() { | |
| ctx.save(); | |
| ctx.globalAlpha = this.life; | |
| ctx.fillStyle = this.color; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size * this.life, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| } | |
| function createParticleBurst(x, y, color, count = 10, type = 'default') { | |
| for (let i = 0; i < count; i++) { | |
| gameState.particles.push(new Particle(x, y, color, type)); | |
| } | |
| } | |
| // ===================================================== | |
| // DINOSAUR PLAYER | |
| // ===================================================== | |
| const dinosaur = { | |
| x: 0, | |
| y: 0, | |
| width: 60, | |
| height: 80, | |
| lane: 1, | |
| targetX: 0, | |
| animFrame: 0, | |
| animTimer: 0, | |
| bounceOffset: 0, | |
| draw() { | |
| const targetX = (gameState.targetLane - 1) * CONFIG.LANE_WIDTH - CONFIG.LANE_WIDTH; | |
| this.x = lerp(this.x, targetX, 0.15); | |
| const bounce = gameState.isJumping ? 0 : Math.sin(this.animFrame * 0.3) * 3; | |
| const slideScale = gameState.isSliding ? 0.6 : 1; | |
| const jumpOffset = gameState.playerY; | |
| const drawX = centerX + this.x; | |
| const drawY = groundY - this.height / 2 - 20 - jumpOffset + bounce; | |
| // Running animation | |
| this.animTimer += 1/60; | |
| if (this.animTimer > 0.1) { | |
| this.animTimer = 0; | |
| this.animFrame++; | |
| } | |
| ctx.save(); | |
| ctx.translate(drawX, drawY); | |
| ctx.scale(slideScale, slideScale); | |
| // Shield effect | |
| if (gameState.activePowerups.shield > 0) { | |
| const shieldPulse = Math.sin(Date.now() * 0.01) * 0.2 + 0.8; | |
| ctx.save(); | |
| ctx.globalAlpha = 0.3 * shieldPulse; | |
| ctx.fillStyle = CONFIG.COLORS.secondary; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 60, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| ctx.save(); | |
| ctx.globalAlpha = shieldPulse; | |
| ctx.strokeStyle = CONFIG.COLORS.secondary; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 55, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| // Magnet effect - draw attraction line | |
| if (gameState.activePowerups.magnet > 0) { | |
| ctx.save(); | |
| ctx.strokeStyle = CONFIG.COLORS.primary; | |
| ctx.globalAlpha = 0.3; | |
| ctx.lineWidth = 2; | |
| ctx.setLineDash([5, 5]); | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, 150, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| // Shadow | |
| ctx.save(); | |
| ctx.fillStyle = 'rgba(0,0,0,0.3)'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, this.height/2 + 5 - jumpOffset, 25, 8, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| // Body | |
| ctx.fillStyle = '#22c55e'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, 0, 28, 35, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Belly | |
| ctx.fillStyle = '#86efac'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, 8, 18, 22, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Head | |
| ctx.fillStyle = '#22c55e'; | |
| ctx.beginPath(); | |
| ctx.ellipse(0, -35, 22, 20, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Snout | |
| ctx.fillStyle = '#86efac'; | |
| ctx.beginPath(); | |
| ctx.ellipse(8, -32, 12, 10, 0.2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Nostril | |
| ctx.fillStyle = '#166534'; | |
| ctx.beginPath(); | |
| ctx.arc(14, -34, 2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Eye white | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.beginPath(); | |
| ctx.ellipse(-5, -40, 10, 12, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Eye pupil | |
| ctx.fillStyle = '#1a1a2e'; | |
| ctx.beginPath(); | |
| ctx.ellipse(-3, -38, 6, 7, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Eye shine | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.beginPath(); | |
| ctx.arc(-6, -42, 3, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Headband | |
| ctx.fillStyle = '#dc2626'; | |
| ctx.fillRect(-24, -52, 48, 8); | |
| // Bitcoin symbol on headband | |
| ctx.fillStyle = CONFIG.COLORS.primary; | |
| ctx.font = 'bold 12px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('₿', 0, -45); | |
| // Legs | |
| const legOffset = gameState.isJumping ? 0 : Math.sin(this.animFrame * 0.4) * 8; | |
| const legOffset2 = gameState.isJumping ? 0 : Math.sin(this.animFrame * 0.4 + Math.PI) * 8; | |
| // Back leg | |
| ctx.fillStyle = '#16a34a'; | |
| ctx.beginPath(); | |
| ctx.ellipse(-12 + legOffset2, 30, 8, 15, -0.2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Front leg | |
| ctx.fillStyle = '#16a34a'; | |
| ctx.beginPath(); | |
| ctx.ellipse(12 + legOffset, 30, 8, 15, 0.2, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Arms | |
| const armOffset = gameState.isJumping ? 0 : Math.sin(this.animFrame * 0.4 + Math.PI/2) * 5; | |
| ctx.fillStyle = '#22c55e'; | |
| // Left arm | |
| ctx.beginPath(); | |
| ctx.ellipse(-25 + armOffset, -5, 6, 12, -0.5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Right arm | |
| ctx.beginPath(); | |
| ctx.ellipse(25 - armOffset, -5, 6, 12, 0.5, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| // Running particles | |
| if (!gameState.isJumping && Math.random() > 0.7) { | |
| gameState.particles.push(new Particle( | |
| drawX + randomRange(-10, 10), | |
| groundY - 10, | |
| 'rgba(100, 100, 150, 0.5)', | |
| 'trail' | |
| )); | |
| } | |
| } | |
| }; | |
| // ===================================================== | |
| // OBSTACLES | |
| // ===================================================== | |
| class Obstacle { | |
| constructor(lane, type) { | |
| this.lane = lane; | |
| this.type = type; | |
| this.x = (lane - 1) * CONFIG.LANE_WIDTH - CONFIG.LANE_WIDTH; | |
| this.z = 2000; // Far away | |
| this.passed = false; | |
| if (type === 'low') { | |
| this.height = 40; | |
| this.width = 50; | |
| } else if (type === 'high') { | |
| this.height = 70; | |
| this.width = 50; | |
| } else if (type === 'gap') { | |
| this.height = 20; | |
| this.width = CONFIG.LANE_WIDTH * 0.8; | |
| } else { | |
| this.height = 60; | |
| this.width = 40; | |
| } | |
| } | |
| update(dt, speed) { | |
| this.z -= speed * dt; | |
| return this.z > -100; | |
| } | |
| draw() { | |
| const perspective = 1 - (this.z / 2000); | |
| const scale = perspective * 0.8 + 0.2; | |
| const drawX = centerX + this.x * scale; | |
| const drawY = groundY - this.height * scale * 0.5; | |
| const drawWidth = this.width * scale; | |
| const drawHeight = this.height * scale; | |
| if (this.type === 'gap') { | |
| // Draw gap/hole | |
| ctx.fillStyle = '#0f0f1a'; | |
| ctx.fillRect(drawX - drawWidth/2, drawY, drawWidth, 10 * scale); | |
| return; | |
| } | |
| // Shadow | |
| ctx.fillStyle = 'rgba(0,0,0,0.3)'; | |
| ctx.beginPath(); | |
| ctx.ellipse(drawX, groundY - 5, drawWidth/2, 8 * scale, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Main body | |
| if (this.type === 'low') { | |
| // Barrier (duck under) | |
| ctx.fillStyle = '#dc2626'; | |
| ctx.fillRect(drawX - drawWidth/2, drawY - drawHeight/2, drawWidth, drawHeight); | |
| // Stripe | |
| ctx.fillStyle = '#fbbf24'; | |
| ctx.fillRect(drawX - drawWidth/2, drawY - drawHeight/4, drawWidth, drawHeight/4); | |
| // Warning symbol | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.font = `bold ${14 * scale}px Arial`; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('!', drawX, drawY + 5 * scale); | |
| } else if (this.type === 'high') { | |
| // High barrier (jump over) | |
| ctx.fillStyle = '#7c3aed'; | |
| ctx.fillRect(drawX - drawWidth/2, drawY - drawHeight, drawWidth, drawHeight); | |
| // Top cone | |
| ctx.fillStyle = '#a78bfa'; | |
| ctx.beginPath(); | |
| ctx.moveTo(drawX - drawWidth/2, drawY - drawHeight); | |
| ctx.lineTo(drawX + drawWidth/2, drawY - drawHeight); | |
| ctx.lineTo(drawX, drawY - drawHeight - 20 * scale); | |
| ctx.fill(); | |
| } else { | |
| // Regular obstacle | |
| ctx.fillStyle = '#ef4444'; | |
| ctx.beginPath(); | |
| ctx.roundRect(drawX - drawWidth/2, drawY - drawHeight, drawWidth, drawHeight, 8 * scale); | |
| ctx.fill(); | |
| // Detail | |
| ctx.fillStyle = '#fca5a5'; | |
| ctx.fillRect(drawX - drawWidth/4, drawY - drawHeight + 10*scale, drawWidth/2, 10*scale); | |
| } | |
| } | |
| } | |
| // ===================================================== | |
| // BITCOIN COINS | |
| // ===================================================== | |
| class Coin { | |
| constructor(lane, value, isBonus = false) { | |
| this.lane = lane; | |
| this.x = (lane - 1) * CONFIG.LANE_WIDTH - CONFIG.LANE_WIDTH; | |
| this.z = 2000; | |
| this.value = value; | |
| this.isBonus = isBonus; | |
| this.rotation = 0; | |
| this.collected = false; | |
| this.size = isBonus ? 25 : 20; | |
| this.glowIntensity = 0; | |
| } | |
| update(dt, speed) { | |
| this.z -= speed * dt; | |
| this.rotation += dt * 5; | |
| this.glowIntensity = (Math.sin(Date.now() * 0.005) + 1) / 2; | |
| // Magnet effect | |
| if (gameState.activePowerups.magnet > 0 && !this.collected) { | |
| const playerX = (gameState.targetLane - 1) * CONFIG.LANE_WIDTH - CONFIG.LANE_WIDTH; | |
| const dx = playerX - this.x; | |
| if (Math.abs(dx) < CONFIG.LANE_WIDTH) { | |
| this.x += dx * dt * 5; | |
| } | |
| } | |
| return this.z > -50 && !this.collected; | |
| } | |
| draw() { | |
| const perspective = 1 - (this.z / 2000); | |
| const scale = perspective * 0.8 + 0.2; | |
| const drawX = centerX + this.x * scale; | |
| const drawY = groundY - 40 * scale; | |
| // Spin effect (scale X based on rotation) | |
| const spinScale = Math.abs(Math.cos(this.rotation)); | |
| const drawWidth = this.size * scale * spinScale; | |
| const drawHeight = this.size * scale; | |
| if (drawWidth < 2) return; // Don't draw when coin is edge-on | |
| // Glow | |
| if (this.isBonus) { | |
| ctx.save(); | |
| ctx.shadowColor = CONFIG.COLORS.primary; | |
| ctx.shadowBlur = 20 + this.glowIntensity * 10; | |
| ctx.fillStyle = CONFIG.COLORS.primary; | |
| ctx.beginPath(); | |
| ctx.ellipse(drawX, drawY, drawWidth, drawHeight, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } else { | |
| ctx.save(); | |
| ctx.globalAlpha = 0.5 + this.glowIntensity * 0.3; | |
| ctx.fillStyle = CONFIG.COLORS.primary; | |
| ctx.beginPath(); | |
| ctx.ellipse(drawX, drawY, drawWidth + 5, drawHeight + 5, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| // Main coin | |
| ctx.fillStyle = this.isBonus ? '#ffd700' : CONFIG.COLORS.primary; | |
| ctx.beginPath(); | |
| ctx.ellipse(drawX, drawY, drawWidth, drawHeight, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Inner circle | |
| ctx.fillStyle = this.isBonus ? '#b8860b' : '#d97706'; | |
| ctx.beginPath(); | |
| ctx.ellipse(drawX, drawY, drawWidth * 0.75, drawHeight * 0.75, 0, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Bitcoin symbol | |
| if (drawWidth > 8) { | |
| ctx.fillStyle = this.isBonus ? '#ffd700' : '#f7931a'; | |
| ctx.font = `bold ${10 * scale * spinScale}px Arial`; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('₿', drawX, drawY); | |
| } | |
| } | |
| collect() { | |
| if (this.collected) return false; | |
| this.collected = true; | |
| const multiplier = gameState.activePowerups.multiplier; | |
| const earned = this.value * multiplier; | |
| gameState.btcBalance += earned; | |
| // Particles | |
| createParticleBurst( | |
| centerX + this.x * 0.8, | |
| groundY - 40, | |
| this.isBonus ? '#ffd700' : CONFIG.COLORS.primary, | |
| 15, | |
| 'coin' | |
| ); | |
| return true; | |
| } | |
| } | |
| // ===================================================== | |
| // POWER-UPS | |
| // ===================================================== | |
| class PowerUp { | |
| constructor(lane, type) { | |
| this.lane = lane; | |
| this.x = (lane - 1) * CONFIG.LANE_WIDTH - CONFIG.LANE_WIDTH; | |
| this.z = 2000; | |
| this.type = type; | |
| this.collected = false; | |
| this.bobOffset = 0; | |
| } | |
| update(dt, speed) { | |
| this.z -= speed * dt; | |
| this.bobOffset = Math.sin(Date.now() * 0.005) * 10; | |
| return this.z > -50 && !this.collected; | |
| } | |
| draw() { | |
| const perspective = 1 - (this.z / 2000); | |
| const scale = perspective * 0.8 + 0.2; | |
| const drawX = centerX + this.x * scale; | |
| const drawY = groundY - 50 * scale + this.bobOffset * scale; | |
| const size = 30 * scale; | |
| // Glow | |
| ctx.save(); | |
| ctx.shadowColor = this.getColor(); | |
| ctx.shadowBlur = 20; | |
| ctx.fillStyle = this.getColor(); | |
| ctx.beginPath(); | |
| ctx.arc(drawX, drawY, size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| // Inner circle | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.beginPath(); | |
| ctx.arc(drawX, drawY, size * 0.7, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Icon | |
| ctx.fillStyle = this.getColor(); | |
| ctx.font = `bold ${18 * scale}px Arial`; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(this.getIcon(), drawX, drawY); | |
| } | |
| getColor() { | |
| switch(this.type) { | |
| case 'magnet': return CONFIG.COLORS.primary; | |
| case 'shield': return CONFIG.COLORS.secondary; | |
| case 'multiplier': return '#a855f7'; | |
| case 'speedboost': return '#3b82f6'; | |
| default: return '#ffffff'; | |
| } | |
| } | |
| getIcon() { | |
| switch(this.type) { | |
| case 'magnet': return '🧲'; | |
| case 'shield': return '🛡️'; | |
| case 'multiplier': return '✖️'; | |
| case 'speedboost': return '⚡'; | |
| default: return '?'; | |
| } | |
| } | |
| collect() { | |
| if (this.collected) return; | |
| this.collected = true; | |
| // Activate power-up | |
| switch(this.type) { | |
| case 'magnet': | |
| gameState.activePowerups.magnet = CONFIG.MAGNET_DURATION; | |
| break; | |
| case 'shield': | |
| gameState.activePowerups.shield = CONFIG.SHIELD_DURATION; | |
| break; | |
| case 'multiplier': | |
| gameState.activePowerups.multiplier = 2; | |
| gameState.activePowerups.multiplier = CONFIG.MULTIPLIER_DURATION; | |
| break; | |
| case 'speedboost': | |
| gameState.activePowerups.speedboost = CONFIG.SPEEDBOOST_DURATION; | |
| break; | |
| } | |
| showPowerupText(this.getText()); | |
| createParticleBurst(centerX, groundY - 50, this.getColor(), 20, 'shield'); | |
| } | |
| getText() { | |
| switch(this.type) { | |
| case 'magnet': return 'MAGNET!'; | |
| case 'shield': return 'SHIELD!'; | |
| case 'multiplier': return '2X BTC!'; | |
| case 'speedboost': return 'SPEED BOOST!'; | |
| default: return 'POWER UP!'; | |
| } | |
| } | |
| } | |
| // ===================================================== | |
| // BACKGROUND & ENVIRONMENT | |
| // ===================================================== | |
| function drawBackground() { | |
| // Sky gradient | |
| const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight); | |
| gradient.addColorStop(0, '#0f0f1a'); | |
| gradient.addColorStop(0.5, '#1a1a2e'); | |
| gradient.addColorStop(1, '#16213e'); | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(0, 0, canvasWidth, canvasHeight); | |
| // Stars | |
| ctx.fillStyle = '#ffffff'; | |
| for (let i = 0; i < 50; i++) { | |
| const x = (i * 137.5) % canvasWidth; | |
| const y = (i * 73.3) % (canvasHeight * 0.5); | |
| const size = (i % 3) + 1; | |
| const alpha = 0.3 + Math.sin(Date.now() * 0.001 + i) * 0.2; | |
| ctx.globalAlpha = alpha; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| ctx.globalAlpha = 1; | |
| // Distant mountains (parallax layer 1) | |
| const mountainOffset = (gameState.distance * 0.1) % canvasWidth; | |
| ctx.fillStyle = '#1e1e3f'; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, groundY); | |
| for (let x = 0; x <= canvasWidth; x += 50) { | |
| const height = Math.sin((x + mountainOffset) * 0.01) * 80 + | |
| Math.sin((x + mountainOffset) * 0.02) * 40; | |
| ctx.lineTo(x, groundY - 100 - height); | |
| } | |
| ctx.lineTo(canvasWidth, groundY); | |
| ctx.fill(); | |
| // Mid mountains (parallax layer 2) | |
| const midOffset = (gameState.distance * 0.3) % canvasWidth; | |
| ctx.fillStyle = '#252547'; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, groundY); | |
| for (let x = 0; x <= canvasWidth; x += 30) { | |
| const height = Math.sin((x + midOffset) * 0.015) * 60 + | |
| Math.sin((x + midOffset) * 0.03) * 30; | |
| ctx.lineTo(x, groundY - 60 - height); | |
| } | |
| ctx.lineTo(canvasWidth, groundY); | |
| ctx.fill(); | |
| } | |
| function drawGround() { | |
| // Main ground | |
| ctx.fillStyle = CONFIG.COLORS.ground; | |
| ctx.fillRect(0, groundY, canvasWidth, canvasHeight - groundY); | |
| // Lane lines | |
| const lineOffset = (gameState.distance * 2) % CONFIG.LANE_WIDTH; | |
| ctx.strokeStyle = CONFIG.COLORS.laneMarker; | |
| ctx.lineWidth = 2; | |
| for (let i = -1; i <= 3; i++) { | |
| const x = centerX + (i - 1) * CONFIG.LANE_WIDTH - lineOffset; | |
| ctx.beginPath(); | |
| ctx.moveTo(x, groundY); | |
| ctx.lineTo(x + CONFIG.LANE_WIDTH * 0.5, canvasHeight); | |
| ctx.stroke(); | |
| } | |
| // Ground line | |
| ctx.strokeStyle = CONFIG.COLORS.primary; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, groundY); | |
| ctx.lineTo(canvasWidth, groundY); | |
| ctx.stroke(); | |
| // Speed lines | |
| if (gameState.speed > 600) { | |
| const intensity = (gameState.speed - 600) / 600; | |
| ctx.strokeStyle = `rgba(247, 147, 26, ${intensity * 0.3})`; | |
| ctx.lineWidth = 2; | |
| for (let i = 0; i < 10; i++) { | |
| const x = randomRange(0, canvasWidth); | |
| const y = randomRange(groundY, canvasHeight); | |
| const length = randomRange(20, 50) * intensity; | |
| ctx.beginPath(); | |
| ctx.moveTo(x, y); | |
| ctx.lineTo(x, y + length); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| // ===================================================== | |
| // SPAWNING SYSTEM | |
| // ===================================================== | |
| function spawnObstacle() { | |
| const lane = randomInt(0, 2); | |
| const types = ['low', 'high', 'regular']; | |
| const type = types[randomInt(0, types.length - 1)]; | |
| gameState.obstacles.push(new Obstacle(lane, type)); | |
| // Spawn coins in other lanes | |
| for (let l = 0; l < 3; l++) { | |
| if (l !== lane && Math.random() < CONFIG.COIN_SPAWN_CHANCE) { | |
| const isBonus = Math.random() < CONFIG.BONUS_COIN_CHANCE; | |
| const value = isBonus ? CONFIG.BONUS_COIN_VALUE : CONFIG.BASE_COIN_VALUE; | |
| // Spawn multiple coins in a row | |
| const coinCount = isBonus ? randomInt(3, 5) : randomInt(3, 7); | |
| for (let c = 0; c < coinCount; c++) { | |
| const coin = new Coin(l, value, isBonus); | |
| coin.z = 1800 - c * 40; | |
| gameState.coins.push(coin); | |
| } | |
| } | |
| } | |
| // Chance to spawn power-up | |
| if (Math.random() < 0.08) { | |
| const powerTypes = ['magnet', 'shield', 'multiplier', 'speedboost']; | |
| const pType = powerTypes[randomInt(0, powerTypes.length - 1)]; | |
| // Find a lane without obstacle | |
| let pLane = lane; | |
| while (pLane === lane) { | |
| pLane = randomInt(0, 2); | |
| } | |
| gameState.powerups.push(new PowerUp(pLane, pType)); | |
| } | |
| } | |
| // ===================================================== | |
| // COLLISION DETECTION | |
| // ===================================================== | |
| function checkCollisions() { | |
| const playerLane = gameState.targetLane; | |
| // Check obstacles | |
| for (const obstacle of gameState.obstacles) { | |
| if (obstacle.z < 100 && obstacle.z > -50 && | |
| obstacle.lane === playerLane && !obstacle.passed) { | |
| // Check collision based on player state | |
| let collided = false; | |
| if (obstacle.type === 'low' && !gameState.isSliding) { | |
| collided = true; | |
| } else if (obstacle.type === 'high' && !gameState.is |