Created
December 29, 2025 08:38
-
-
Save igorcosta/b9f5d53d30391cba36d0dbf18a953e0e to your computer and use it in GitHub Desktop.
Simple Arrow game with balloons
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Balloon Popper</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| background: linear-gradient(to bottom, #87CEEB, #E0F6FF); | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| height: 100vh; | |
| user-select: none; | |
| } | |
| #ui-layer { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 10; | |
| display: flex; | |
| gap: 20px; | |
| color: #333; | |
| font-weight: bold; | |
| font-size: 1.2rem; | |
| text-shadow: 1px 1px 2px white; | |
| } | |
| .stat-box { | |
| background: rgba(255, 255, 255, 0.7); | |
| padding: 8px 15px; | |
| border-radius: 10px; | |
| border: 2px solid #fff; | |
| } | |
| #game-area { | |
| position: relative; | |
| width: 800px; | |
| height: 600px; | |
| background: rgba(255, 255, 255, 0.2); | |
| margin-top: 20px; | |
| border: 2px solid #fff; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| box-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
| } | |
| /* Elements */ | |
| .archer { | |
| position: absolute; | |
| bottom: 50px; | |
| left: 50px; | |
| width: 60px; | |
| height: 100px; | |
| background: #333; | |
| border-radius: 10px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| } | |
| .archer::before { | |
| content: '🏹'; | |
| font-size: 40px; | |
| position: absolute; | |
| top: -20px; | |
| left: 10px; | |
| transform: rotate(-45deg); | |
| } | |
| .balloon { | |
| position: absolute; | |
| width: 40px; | |
| height: 50px; | |
| border-radius: 50% 50% 50% 50% / 40% 40% 60% 60%; | |
| z-index: 2; | |
| transition: transform 0.1s; | |
| } | |
| .balloon::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: -15px; | |
| left: 19px; | |
| width: 2px; | |
| height: 15px; | |
| background: rgba(0,0,0,0.2); | |
| } | |
| /* Colors for balloons */ | |
| .color-0 { background: radial-gradient(circle at 30% 30%, #ff4d4d, #b30000); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); } | |
| .color-1 { background: radial-gradient(circle at 30% 30%, #4dff4d, #00b300); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); } | |
| .color-2 { background: radial-gradient(circle at 30% 30%, #4d4dff, #0000b3); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); } | |
| .color-3 { background: radial-gradient(circle at 30% 30%, #ffff4d, #b3b300); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); } | |
| .color-4 { background: radial-gradient(circle at 30% 30%, #ff4dff, #b300b3); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); } | |
| .color-5 { background: radial-gradient(circle at 30% 30%, #4dffff, #00b3b3); box-shadow: inset -5px -5px 10px rgba(0,0,0,0.2); } | |
| .arrow { | |
| position: absolute; | |
| width: 40px; | |
| height: 6px; | |
| background: #5d4037; | |
| border-radius: 3px; | |
| z-index: 3; | |
| transform-origin: center; | |
| } | |
| .arrow::after { | |
| content: ''; | |
| position: absolute; | |
| right: -10px; | |
| top: -3px; | |
| width: 0; | |
| height: 0; | |
| border-left: 10px solid #5d4037; | |
| border-top: 6px solid transparent; | |
| border-bottom: 6px solid transparent; | |
| } | |
| .arrow::before { | |
| content: ''; | |
| position: absolute; | |
| left: -5px; | |
| top: 0; | |
| width: 6px; | |
| height: 6px; | |
| background: #8d6e63; | |
| border-radius: 50%; | |
| } | |
| .pop-effect { | |
| position: absolute; | |
| width: 60px; | |
| height: 60px; | |
| background: rgba(255, 255, 255, 0.8); | |
| border-radius: 50%; | |
| animation: pop 0.3s ease-out forwards; | |
| pointer-events: none; | |
| z-index: 5; | |
| } | |
| @keyframes pop { | |
| 0% { transform: scale(0.5); opacity: 1; } | |
| 100% { transform: scale(2); opacity: 0; } | |
| } | |
| #game-over { | |
| position: absolute; | |
| top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| display: none; /* hidden by default */ | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 20; | |
| } | |
| #game-over h1 { font-size: 3rem; margin-bottom: 10px; } | |
| #game-over p { font-size: 1.5rem; margin-bottom: 20px; } | |
| button { | |
| padding: 15px 30px; | |
| font-size: 1.2rem; | |
| background: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| font-weight: bold; | |
| } | |
| button:hover { background: #45a049; } | |
| .instruction { | |
| margin-top: 10px; | |
| color: white; | |
| font-weight: bold; | |
| text-shadow: 1px 1px 2px black; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="instruction">Click anywhere in the game area to Shoot!</div> | |
| <div id="ui-layer"> | |
| <div class="stat-box">Popped: <span id="score">0</span> / 100</div> | |
| <div class="stat-box">Arrows: <span id="arrows-left">100</span></div> | |
| <div class="stat-box">Balloons: <span id="balloons-left">100</span></div> | |
| </div> | |
| <div id="game-area"> | |
| <!-- The Archer --> | |
| <div class="archer"></div> | |
| <!-- Game Over Screen --> | |
| <div id="game-over"> | |
| <h1 id="result-title">Game Over</h1> | |
| <p id="result-score">Score: 0</p> | |
| <button onclick="resetGame()">Play Again</button> | |
| </div> | |
| </div> | |
| <script> | |
| // Configuration | |
| const CONFIG = { | |
| maxArrows: 100, | |
| maxBalloons: 100, | |
| balloonSpeedMin: 1.5, | |
| balloonSpeedMax: 3.5, | |
| arrowSpeed: 12, | |
| spawnRate: 800, // ms between balloons | |
| colors: ['color-0', 'color-1', 'color-2', 'color-3', 'color-4', 'color-5'] | |
| }; | |
| // State | |
| let gameState = { | |
| playing: false, | |
| score: 0, | |
| arrowsLeft: 0, | |
| balloonsLeft: 0, | |
| balloonsOnScreen: [], | |
| arrowsOnScreen: [], | |
| lastSpawn: 0, | |
| animId: null | |
| }; | |
| // DOM Elements | |
| const gameArea = document.getElementById('game-area'); | |
| const scoreEl = document.getElementById('score'); | |
| const arrowsEl = document.getElementById('arrows-left'); | |
| const balloonsEl = document.getElementById('balloons-left'); | |
| const gameOverScreen = document.getElementById('game-over'); | |
| const resultTitle = document.getElementById('result-title'); | |
| const resultScore = document.getElementById('result-score'); | |
| // --- Input Handling --- | |
| // Use mousedown for responsiveness (vs click which waits for mouse up) | |
| gameArea.addEventListener('mousedown', (e) => { | |
| // Only shoot if playing and not clicking UI buttons | |
| if (gameState.playing && e.target.tagName !== 'BUTTON') { | |
| shootArrow(e); | |
| } | |
| }); | |
| // --- Game Logic --- | |
| function resetGame() { | |
| // Clear any existing game loop | |
| if (gameState.animId) cancelAnimationFrame(gameState.animId); | |
| // Reset State | |
| gameState = { | |
| playing: true, | |
| score: 0, | |
| arrowsLeft: CONFIG.maxArrows, | |
| balloonsLeft: CONFIG.maxBalloons, | |
| balloonsOnScreen: [], | |
| arrowsOnScreen: [], | |
| lastSpawn: performance.now(), | |
| animId: null | |
| }; | |
| // Clear DOM | |
| const elements = document.querySelectorAll('.balloon, .arrow, .pop-effect'); | |
| elements.forEach(el => el.remove()); | |
| // Update UI | |
| updateUI(); | |
| gameOverScreen.style.display = 'none'; | |
| // Start Loop | |
| requestAnimationFrame(gameLoop); | |
| } | |
| function shootArrow(e) { | |
| if (gameState.arrowsLeft <= 0) return; // No arrows left | |
| gameState.arrowsLeft--; | |
| updateUI(); | |
| // Calculate spawn position (Archer position) | |
| const startX = 110; // Left of archer + offset | |
| const startY = 460; // Middle height of archer area | |
| // Calculate angle towards click | |
| const rect = gameArea.getBoundingClientRect(); | |
| const clickX = e.clientX - rect.left; | |
| const clickY = e.clientY - rect.top; | |
| const angle = Math.atan2(clickY - startY, clickX - startX); | |
| const arrow = { | |
| el: document.createElement('div'), | |
| x: startX, | |
| y: startY, | |
| vx: Math.cos(angle) * CONFIG.arrowSpeed, | |
| vy: Math.sin(angle) * CONFIG.arrowSpeed, | |
| width: 40, | |
| height: 6 | |
| }; | |
| arrow.el.classList.add('arrow'); | |
| // Visual rotation based on angle | |
| arrow.el.style.transform = `rotate(${angle}rad)`; | |
| gameArea.appendChild(arrow.el); | |
| gameState.arrowsOnScreen.push(arrow); | |
| } | |
| function spawnBalloon(timestamp) { | |
| if (gameState.balloonsLeft <= 0) return; | |
| if (timestamp - gameState.lastSpawn > CONFIG.spawnRate) { | |
| gameState.balloonsLeft--; | |
| updateUI(); | |
| const balloon = { | |
| el: document.createElement('div'), | |
| x: 650, // Start right side | |
| y: Math.random() * (500 - 50) + 20, // Random Y | |
| width: 40, | |
| height: 50, | |
| speed: Math.random() * (CONFIG.balloonSpeedMax - CONFIG.balloonSpeedMin) + CONFIG.balloonSpeedMin, | |
| color: CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)] | |
| }; | |
| balloon.el.classList.add('balloon', balloon.color); | |
| gameArea.appendChild(balloon.el); | |
| gameState.balloonsOnScreen.push(balloon); | |
| gameState.lastSpawn = timestamp; | |
| } | |
| } | |
| function updateEntities() { | |
| // Update Arrows | |
| for (let i = gameState.arrowsOnScreen.length - 1; i >= 0; i--) { | |
| const arrow = gameState.arrowsOnScreen[i]; | |
| arrow.x += arrow.vx; | |
| arrow.y += arrow.vy; | |
| // Render | |
| arrow.el.style.left = arrow.x + 'px'; | |
| arrow.el.style.top = arrow.y + 'px'; | |
| // Check bounds (remove if off screen) | |
| if (arrow.x > 820 || arrow.x < -50 || arrow.y > 620 || arrow.y < -50) { | |
| arrow.el.remove(); | |
| gameState.arrowsOnScreen.splice(i, 1); | |
| } | |
| } | |
| // Update Balloons | |
| for (let i = gameState.balloonsOnScreen.length - 1; i >= 0; i--) { | |
| const balloon = gameState.balloonsOnScreen[i]; | |
| balloon.x -= balloon.speed; // Move left | |
| // Render | |
| balloon.el.style.left = balloon.x + 'px'; | |
| balloon.el.style.top = balloon.y + 'px'; | |
| // Check bounds (missed) | |
| if (balloon.x < -50) { | |
| balloon.el.remove(); | |
| gameState.balloonsOnScreen.splice(i, 1); | |
| } | |
| } | |
| } | |
| function checkCollisions() { | |
| // Optimization: Iterate over arrows (usually fewer than balloons potentially) | |
| for (let i = gameState.arrowsOnScreen.length - 1; i >= 0; i--) { | |
| const arrow = gameState.arrowsOnScreen[i]; | |
| // Simple Circle/Rect collision logic | |
| // Treat Arrow as a small point for hit detection accuracy or a small rect | |
| const arrowRect = { | |
| x: arrow.x + 20, // tip | |
| y: arrow.y, | |
| w: 10, | |
| h: 6 | |
| }; | |
| for (let j = gameState.balloonsOnScreen.length - 1; j >= 0; j--) { | |
| const balloon = gameState.balloonsOnScreen[j]; | |
| // Check AABB overlap | |
| // Balloon dimensions | |
| const bRect = { | |
| x: balloon.x, | |
| y: balloon.y, | |
| w: balloon.width, | |
| h: balloon.height | |
| }; | |
| // Hit detection | |
| if (arrowRect.x < bRect.x + bRect.w && | |
| arrowRect.x + arrowRect.w > bRect.x && | |
| arrowRect.y < bRect.y + bRect.h && | |
| arrowRect.y + arrowRect.h > bRect.y) { | |
| // POP! | |
| createPopEffect(balloon.x, balloon.y); | |
| // Remove balloon | |
| balloon.el.remove(); | |
| gameState.balloonsOnScreen.splice(j, 1); | |
| // Remove Arrow | |
| arrow.el.remove(); | |
| gameState.arrowsOnScreen.splice(i, 1); | |
| // Update Score | |
| gameState.score++; | |
| updateUI(); | |
| // Stop checking this specific arrow | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| function createPopEffect(x, y) { | |
| const effect = document.createElement('div'); | |
| effect.classList.add('pop-effect'); | |
| effect.style.left = x + 'px'; | |
| effect.style.top = y + 'px'; | |
| gameArea.appendChild(effect); | |
| // Remove after animation | |
| setTimeout(() => effect.remove(), 300); | |
| } | |
| function updateUI() { | |
| scoreEl.textContent = gameState.score; | |
| arrowsEl.textContent = gameState.arrowsLeft; | |
| balloonsEl.textContent = gameState.balloonsLeft; | |
| } | |
| function checkEndCondition() { | |
| // Conditions to end game | |
| // 1. Balloons are all popped (Victory) | |
| // 2. Arrows are 0 AND no balloons on screen (Defeat) | |
| const noBalloonsLeft = gameState.balloonsLeft === 0 && gameState.balloonsOnScreen.length === 0; | |
| const outOfArrows = gameState.arrowsLeft === 0 && gameState.arrowsOnScreen.length === 0; | |
| if (noBalloonsLeft) { | |
| endGame(true); | |
| return true; | |
| } | |
| if (outOfArrows) { | |
| endGame(false); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function endGame(victory) { | |
| gameState.playing = false; | |
| cancelAnimationFrame(gameState.animId); | |
| resultTitle.textContent = victory ? "VICTORY!" : "GAME OVER"; | |
| resultTitle.style.color = victory ? "#4CAF50" : "#ff4d4d"; | |
| resultScore.textContent = `Score: ${gameState.score} / ${CONFIG.maxBalloons}`; | |
| gameOverScreen.style.display = 'flex'; | |
| } | |
| // --- Main Loop --- | |
| function gameLoop(timestamp) { | |
| if (!gameState.playing) return; | |
| // 1. Spawn | |
| spawnBalloon(timestamp); | |
| // 2. Update positions | |
| updateEntities(); | |
| // 3. Check physics (collisions) | |
| checkCollisions(); | |
| // 4. Check End | |
| if (checkEndCondition()) return; | |
| // 5. Continue | |
| gameState.animId = requestAnimationFrame(gameLoop); | |
| } | |
| // Initialize on load | |
| // We wait for the user to click "Play Again" which calls resetGame, | |
| // but we can show the over screen initially to prompt start. | |
| window.onload = () => { | |
| // Initialize with 0 stats visually | |
| updateUI(); | |
| // Start immediately | |
| resetGame(); | |
| }; | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment