Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Infinity Evolution Dino</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| :root { | |
| --bg-color: #f7f7f7; | |
| --text-color: #535353; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| transition: background-color 2s, color 2s; | |
| font-family: 'Courier New', Courier, monospace; | |
| touch-action: none; | |
| user-select: none; | |
| width: 100vw; | |
| height: 100vh; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 380px; | |
| background-color: transparent; | |
| border-bottom: 2px solid var(--text-color); | |
| overflow: hidden; | |
| margin-top: 20px; | |
| } | |
| canvas { display: block; } | |
| #ui-layer { | |
| position: absolute; | |
| top: 10px; | |
| left: 20px; | |
| right: 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| font-weight: bold; | |
| pointer-events: none; | |
| z-index: 5; | |
| } | |
| #overlay { | |
| position: absolute; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| display: flex; flex-direction: column; justify-content: center; align-items: center; | |
| background: rgba(255, 255, 255, 0.8); | |
| z-index: 20; | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| background: #535353; | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 18px; | |
| } | |
| .evolution-popup { | |
| position: absolute; | |
| top: 40%; left: 50%; transform: translate(-50%, -50%); | |
| background: #fff; border: 2px solid #535353; padding: 15px 25px; | |
| border-radius: 10px; display: none; pointer-events: none; | |
| z-index: 30; | |
| animation: fadeUp 2s forwards; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
| } | |
| @keyframes fadeUp { | |
| 0% { opacity: 1; transform: translate(-50%, -50%); } | |
| 100% { opacity: 0; transform: translate(-50%, -200%); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <div id="ui-layer"> | |
| <div>STAGE: <span id="evo-level">Baby</span></div> | |
| <div>HI <span id="high-score">000000</span> | <span id="score">000000</span></div> | |
| </div> | |
| <div id="overlay"> | |
| <h1 id="main-title" class="text-3xl font-bold mb-2">INFINITY EVO</h1> | |
| <p id="sub-title" class="mb-4 text-sm text-gray-500 text-center px-4">Tap anywhere to Jump<br>Swipe/Pull Down to Duck</p> | |
| <button id="start-btn" class="btn">BEGIN EVOLUTION</button> | |
| </div> | |
| <div id="evo-msg" class="evolution-popup font-bold text-center"> | |
| <div class="text-green-600 text-xl">EVOLVED!</div> | |
| <div id="evo-name-popup" class="text-gray-700"></div> | |
| </div> | |
| <canvas id="gameCanvas"></canvas> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const scoreEl = document.getElementById('score'); | |
| const highScoreEl = document.getElementById('high-score'); | |
| const evoLevelEl = document.getElementById('evo-level'); | |
| const overlay = document.getElementById('overlay'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const evoMsg = document.getElementById('evo-msg'); | |
| const evoNamePopup = document.getElementById('evo-name-popup'); | |
| // Balanced Constants | |
| const GRAVITY = 0.5; | |
| const JUMP_FORCE = -11; | |
| const GROUND_Y_OFFSET = 40; | |
| const INITIAL_SPEED = 5; | |
| const SPEED_INCREMENT = 0.0003; | |
| let isPlaying = false; | |
| let score = 0; | |
| let highScore = 0; | |
| let speed = INITIAL_SPEED; | |
| let animationFrameId; | |
| let isNight = false; | |
| // EXTENDED EVOLUTIONS | |
| const evolutions = [ | |
| { threshold: 0, name: "Baby", color: "#535353", size: 0.8 }, | |
| { threshold: 500, name: "Teen", color: "#3498db", size: 1.0 }, | |
| { threshold: 2000, name: "Hunter", color: "#e67e22", size: 1.1 }, | |
| { threshold: 5000, name: "Titan", color: "#c0392b", size: 1.2 }, | |
| { threshold: 10000, name: "Ancient", color: "#27ae60", size: 1.3 }, | |
| { threshold: 25000, name: "Celestial", color: "#9b59b6", size: 1.4 }, | |
| { threshold: 50000, name: "Godzilla", color: "#1abc9c", size: 1.5 }, | |
| { threshold: 100000, name: "Eternal", color: "#f1c40f", size: 1.6 } | |
| ]; | |
| const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| function playSfx(f, t, d) { | |
| try { | |
| if (audioCtx.state === 'suspended') audioCtx.resume(); | |
| const o = audioCtx.createOscillator(); | |
| const g = audioCtx.createGain(); | |
| o.type = t; o.frequency.value = f; | |
| g.gain.setValueAtTime(0.05, audioCtx.currentTime); | |
| g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + d); | |
| o.connect(g); g.connect(audioCtx.destination); | |
| o.start(); o.stop(audioCtx.currentTime + d); | |
| } catch(e) {} | |
| } | |
| class Dino { | |
| constructor() { | |
| this.baseW = 44; | |
| this.baseH = 47; | |
| this.x = 60; | |
| this.y = 0; | |
| this.dy = 0; | |
| this.isJumping = false; | |
| this.isDucking = false; | |
| this.tick = 0; | |
| this.currentEvo = evolutions[0]; | |
| } | |
| update(currentScore) { | |
| // Check for new evolution | |
| const nextEvo = [...evolutions].reverse().find(e => currentScore >= e.threshold); | |
| if (nextEvo && nextEvo.name !== this.currentEvo.name) { | |
| this.currentEvo = nextEvo; | |
| evoLevelEl.innerText = this.currentEvo.name; | |
| evoNamePopup.innerText = this.currentEvo.name + " Stage"; | |
| evoMsg.style.display = 'block'; | |
| setTimeout(() => { evoMsg.style.display = 'none'; }, 2000); | |
| playSfx(800, 'sine', 0.3); | |
| } | |
| this.width = this.baseW * this.currentEvo.size; | |
| this.height = this.baseH * this.currentEvo.size; | |
| if (this.isJumping) { | |
| this.y += this.dy; | |
| this.dy += GRAVITY; | |
| const floor = canvas.height - this.height - GROUND_Y_OFFSET; | |
| if (this.y > floor) { | |
| this.y = floor; | |
| this.dy = 0; | |
| this.isJumping = false; | |
| } | |
| } else { | |
| const drawH = this.isDucking ? this.height * 0.6 : this.height; | |
| this.y = canvas.height - drawH - GROUND_Y_OFFSET; | |
| } | |
| } | |
| draw() { | |
| const drawH = this.isDucking ? this.height * 0.6 : this.height; | |
| ctx.fillStyle = this.currentEvo.color; | |
| // Draw Body | |
| ctx.beginPath(); | |
| ctx.roundRect(this.x, this.y, this.width, drawH, 6); | |
| ctx.fill(); | |
| // Eye | |
| ctx.fillStyle = isNight ? '#000' : '#fff'; | |
| ctx.fillRect(this.x + this.width - 12, this.y + 10, 5, 5); | |
| // Legs | |
| if (!this.isJumping) { | |
| this.tick++; | |
| const legToggle = Math.floor(this.tick / 6) % 2; | |
| ctx.fillStyle = this.currentEvo.color; | |
| if (legToggle) { | |
| ctx.fillRect(this.x + 5, this.y + drawH, 12, 6); | |
| } else { | |
| ctx.fillRect(this.x + this.width - 17, this.y + drawH, 12, 6); | |
| } | |
| } | |
| } | |
| jump() { | |
| if (!this.isJumping && !this.isDucking) { | |
| this.isJumping = true; | |
| this.dy = JUMP_FORCE; | |
| playSfx(400, 'square', 0.1); | |
| } | |
| } | |
| duck(state) { | |
| if (!this.isJumping) { | |
| this.isDucking = state; | |
| } | |
| } | |
| } | |
| class Obstacle { | |
| constructor() { | |
| this.type = (score > 1000 && Math.random() < 0.25) ? 'bird' : 'cactus'; | |
| this.width = this.type === 'cactus' ? 20 + Math.random() * 30 : 45; | |
| this.height = this.type === 'cactus' ? 30 + Math.random() * 40 : 25; | |
| this.x = canvas.width; | |
| // High birds require ducking, low cacti require jumping | |
| this.y = this.type === 'cactus' | |
| ? canvas.height - this.height - GROUND_Y_OFFSET | |
| : canvas.height - GROUND_Y_OFFSET - 85; | |
| } | |
| update() { | |
| this.x -= speed; | |
| ctx.fillStyle = isNight ? '#ecf0f1' : '#535353'; | |
| ctx.beginPath(); | |
| ctx.roundRect(this.x, this.y, this.width, this.height, 4); | |
| ctx.fill(); | |
| } | |
| } | |
| let dino; | |
| let obstacles = []; | |
| let nextObstacleTimer = 0; | |
| function drawEnvironment() { | |
| // Day/Night Cycle Logic | |
| const cyclePoints = 2000; | |
| const cycleProgress = (score % cyclePoints) / cyclePoints; | |
| isNight = cycleProgress > 0.5; | |
| if (isNight) { | |
| document.body.style.backgroundColor = "#2c3e50"; | |
| document.body.style.color = "#ecf0f1"; | |
| overlay.style.background = "rgba(0,0,0,0.8)"; | |
| } else { | |
| document.body.style.backgroundColor = "#f7f7f7"; | |
| document.body.style.color = "#535353"; | |
| overlay.style.background = "rgba(255,255,255,0.8)"; | |
| } | |
| // Draw Sun or Moon | |
| const centerX = canvas.width * 0.8; | |
| const centerY = 80; | |
| ctx.beginPath(); | |
| if (!isNight) { | |
| // Sun | |
| ctx.fillStyle = "#f1c40f"; | |
| ctx.arc(centerX, centerY, 30, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } else { | |
| // Moon | |
| ctx.fillStyle = "#f1c40f"; | |
| ctx.arc(centerX, centerY, 25, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Moon shadow | |
| ctx.fillStyle = isNight ? "#2c3e50" : "#f7f7f7"; | |
| ctx.beginPath(); | |
| ctx.arc(centerX + 10, centerY - 5, 25, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| // Ground | |
| ctx.strokeStyle = isNight ? "#555" : "#ccc"; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, canvas.height - GROUND_Y_OFFSET); | |
| ctx.lineTo(canvas.width, canvas.height - GROUND_Y_OFFSET); | |
| ctx.stroke(); | |
| } | |
| function init() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = 380; | |
| dino = new Dino(); | |
| obstacles = []; | |
| score = 0; | |
| speed = INITIAL_SPEED; | |
| nextObstacleTimer = 60; | |
| } | |
| function gameOver() { | |
| isPlaying = false; | |
| cancelAnimationFrame(animationFrameId); | |
| playSfx(100, 'sawtooth', 0.5); | |
| overlay.style.display = 'flex'; | |
| document.getElementById('main-title').innerText = "EXTINCT!"; | |
| document.getElementById('sub-title').innerText = `Survived as ${dino.currentEvo.name}\nScore: ${Math.floor(score)}`; | |
| startBtn.innerText = "EVOLVE AGAIN"; | |
| } | |
| function update() { | |
| if (!isPlaying) return; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| drawEnvironment(); | |
| dino.update(score); | |
| dino.draw(); | |
| if (nextObstacleTimer <= 0) { | |
| obstacles.push(new Obstacle()); | |
| nextObstacleTimer = 70 + Math.random() * 100 / (speed / 5); | |
| } | |
| nextObstacleTimer--; | |
| for (let i = obstacles.length - 1; i >= 0; i--) { | |
| const obs = obstacles[i]; | |
| obs.update(); | |
| // Collision | |
| const dH = dino.isDucking ? dino.height * 0.6 : dino.height; | |
| const pad = 8; | |
| if (dino.x + pad < obs.x + obs.width && | |
| dino.x + dino.width - pad > obs.x && | |
| dino.y + pad < obs.y + obs.height && | |
| dino.y + dH - pad > obs.y) { | |
| gameOver(); | |
| return; | |
| } | |
| if (obs.x + obs.width < 0) obstacles.splice(i, 1); | |
| } | |
| score += 0.2; | |
| scoreEl.innerText = Math.floor(score).toString().padStart(6, '0'); | |
| if (score > highScore) { | |
| highScore = Math.floor(score); | |
| highScoreEl.innerText = highScore.toString().padStart(6, '0'); | |
| } | |
| speed += SPEED_INCREMENT; | |
| animationFrameId = requestAnimationFrame(update); | |
| } | |
| function startGame() { | |
| if (audioCtx.state === 'suspended') audioCtx.resume(); | |
| init(); | |
| isPlaying = true; | |
| overlay.style.display = 'none'; | |
| update(); | |
| } | |
| // CONTROL LOGIC | |
| const triggerJump = () => { | |
| if (!isPlaying) startGame(); | |
| else dino.jump(); | |
| }; | |
| // Touch Interaction | |
| let touchStartY = 0; | |
| let isDuckingByTouch = false; | |
| document.body.addEventListener('touchstart', (e) => { | |
| touchStartY = e.touches[0].clientY; | |
| // Always treat a tap as a jump first | |
| triggerJump(); | |
| }, { passive: false }); | |
| document.body.addEventListener('touchmove', (e) => { | |
| const currentY = e.touches[0].clientY; | |
| const diffY = currentY - touchStartY; | |
| // Swiping down to duck | |
| if (diffY > 30) { | |
| dino.duck(true); | |
| isDuckingByTouch = true; | |
| } else if (diffY < -10) { | |
| // Ignore small movements or upward swipes | |
| dino.duck(false); | |
| isDuckingByTouch = false; | |
| } | |
| e.preventDefault(); | |
| }, { passive: false }); | |
| document.body.addEventListener('touchend', () => { | |
| if (isDuckingByTouch) { | |
| dino.duck(false); | |
| isDuckingByTouch = false; | |
| } | |
| }); | |
| // Mouse/Keys | |
| window.addEventListener('keydown', (e) => { | |
| if (e.code === 'Space' || e.code === 'ArrowUp') { e.preventDefault(); triggerJump(); } | |
| if (e.code === 'ArrowDown') { e.preventDefault(); dino.duck(true); } | |
| }); | |
| window.addEventListener('keyup', (e) => { if (e.code === 'ArrowDown') dino.duck(false); }); | |
| window.addEventListener('mousedown', (e) => { | |
| // Only handle left clicks as jumps if not a touch-enabled browser (prevents double triggers) | |
| if (e.button === 0 && !('ontouchstart' in window)) { | |
| triggerJump(); | |
| } | |
| }); | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = 380; | |
| }); | |
| init(); | |
| // Ensure start button works even if listeners fail | |
| startBtn.onclick = startGame; | |
| </script> | |
| </body> | |
| </html> |