Unblocked — Bloons

.stat span color: #ffd966; font-size: 1.6rem; font-weight: 800; margin-right: 6px;

// ---------- GAME DIMENSIONS ---------- const W = 900, H = 540; // path definition (winding road) const waypoints = [ x: 80, y: 120, // start x: 220, y: 120, x: 220, y: 280, x: 500, y: 280, x: 500, y: 420, x: 720, y: 420, x: 720, y: 180, x: 820, y: 180 // end zone ]; // helper: interpolate path length (for bloon movement) function getPathLength() let len = 0; for(let i=0; i<waypoints.length-1; i++) const p1 = waypoints[i]; const p2 = waypoints[i+1]; len += Math.hypot(p2.x-p1.x, p2.y-p1.y); return len; const TOTAL_PATH_LEN = getPathLength(); // get position from t (0..1) function getPathPosition(t) if(t <= 0) return ...waypoints[0]; if(t >= 1) return ...waypoints[waypoints.length-1]; let total = TOTAL_PATH_LEN; let distNeeded = t * total; let accumulated = 0; for(let i=0; i<waypoints.length-1; i++) const p1 = waypoints[i]; const p2 = waypoints[i+1]; const segLen = Math.hypot(p2.x-p1.x, p2.y-p1.y); if(distNeeded <= accumulated + segLen) const localT = (distNeeded - accumulated) / segLen; const x = p1.x + (p2.x-p1.x)*localT; const y = p1.y + (p2.y-p1.y)*localT; return x, y; accumulated += segLen; return ...waypoints[waypoints.length-1]; // ----- GAME STATE ----- let lives = 100; let cash = 350; let totalPopped = 0; // score let wave = 1; let waveInProgress = false; let waveTimer = 0; // delay before spawning next bloon (frames) let bloonsToSpawn = 0; let spawnDelayFrames = 0; // dynamic arrays let bloons = []; let towers = []; // projectile array let projectiles = []; // frame counter for wave logic let frameCounter = 0; // bloon types (stats) const BLOON_TYPES = red: health: 1, speed: 1.8, reward: 20, color: "#E34234", radius: 12, value: 1 , blue: health: 2, speed: 1.5, reward: 35, color: "#4C9AFF", radius: 13, value: 2 , green: health: 3, speed: 1.2, reward: 55, color: "#5FAD41", radius: 14, value: 3 , ; // tower stats const TOWER_PRICE = 150; const TOWER_RANGE = 78; const TOWER_COOLDOWN_MAX = 28; // frames between attacks const TOWER_DAMAGE = 1; // ----- helper functions ----- function addCash(amount) cash += amount; updateUI(); function removeCash(amount) if(cash >= amount) cash -= amount; updateUI(); return true; return false; function loseLife(amount=1) lives -= amount; if(lives <= 0) lives = 0; gameOver(); updateUI(); function updateUI() document.getElementById('livesDisplay').innerText = lives; document.getElementById('cashDisplay').innerText = Math.floor(cash); document.getElementById('scoreDisplay').innerText = totalPopped; function gameOver() waveInProgress = false; bloons = []; projectiles = []; document.getElementById('waveStatus').innerHTML = `💀 GAME OVER 💀 · press RESTART`; // spawn a specific bloon type at start (t=0) function spawnBloon(typeKey) const def = BLOON_TYPES[typeKey]; if(!def) return; bloons.push( type: typeKey, health: def.health, maxHealth: def.health, t: 0.0, speed: def.speed / 100, // per frame advance (~0.018 to 0.012) reward: def.reward, radius: def.radius, color: def.color, value: def.value ); // start wave: determine composition based on wave number function startWave() if(lives <= 0) return; waveInProgress = true; bloonsToSpawn = 0; // wave difficulty: more bloons + stronger types let redCount = Math.min(4 + Math.floor(wave * 0.8), 18); let blueCount = Math.min(1 + Math.floor(wave / 2), 8); let greenCount = Math.min(0 + Math.floor(wave / 4), 6); if(wave >= 3) blueCount = Math.min(2 + Math.floor(wave/2.5), 10); if(wave >= 5) greenCount = Math.min(1 + Math.floor(wave/3.5), 8); let spawnQueue = []; for(let i=0; i<redCount; i++) spawnQueue.push('red'); for(let i=0; i<blueCount; i++) spawnQueue.push('blue'); for(let i=0; i<greenCount; i++) spawnQueue.push('green'); // shuffle for variety for(let i=spawnQueue.length-1; i>0; i--) const j = Math.floor(Math.random()*(i+1)); [spawnQueue[i], spawnQueue[j]] = [spawnQueue[j], spawnQueue[i]]; bloonsToSpawn = spawnQueue.length; // store pending spawns window._waveSpawnQueue = spawnQueue; spawnDelayFrames = 12; // initial delay before first bloon document.getElementById('waveStatus').innerHTML = `🌊 WAVE $wave · incoming! 🎈`; // update wave spawning logic function updateWaveSpawning() if(!waveInProgress) return; if(lives <= 0) return; if(bloonsToSpawn <= 0 && bloons.length === 0 && projectiles.filter(p=>!p.hit).length===0) // wave cleared waveInProgress = false; const waveBonus = 100 + wave * 15; addCash(waveBonus); totalPopped += 15; // bonus for wave clear wave++; updateUI(); document.getElementById('waveStatus').innerHTML = `🏆 WAVE $wave-1 CLEARED! Next wave in 2s 🏆`; // auto start next wave after 2 seconds (60 frames) setTimeout(() => if(lives > 0 && !waveInProgress && bloons.length === 0) startWave(); , 1800); return; if(bloonsToSpawn > 0 && spawnDelayFrames <= 0) if(window._waveSpawnQueue && window._waveSpawnQueue.length) const nextType = window._waveSpawnQueue.shift(); spawnBloon(nextType); bloonsToSpawn--; // dynamic delay between spawns (faster waves = less delay) let delay = Math.max(14, 28 - Math.floor(wave/2)); if(delay < 8) delay = 8; spawnDelayFrames = delay; else bloonsToSpawn = 0; else if(spawnDelayFrames > 0) spawnDelayFrames--; // update bloon movement & endpoint damage function updateBloons() for(let i=0; i<bloons.length; i++) const b = bloons[i]; let newT = b.t + b.speed; if(newT >= 1.0) // reached end -> damage player loseLife(1); bloons.splice(i,1); i--; continue; b.t = newT; // tower attacks function updateTowersAndProjectiles() // tower cooldown update for(let t of towers) if(t.cooldown > 0) t.cooldown--; // attack: find target for(let t of towers) if(t.cooldown > 0) continue; let closest = null; let minDist = TOWER_RANGE + 1; for(let b of bloons) const pos = getPathPosition(b.t); const dx = pos.x - t.x; const dy = pos.y - t.y; const dist = Math.hypot(dx,dy); if(dist < minDist) minDist = dist; closest = b; if(closest) t.cooldown = TOWER_COOLDOWN_MAX; // create projectile that will hit this bloon after flight time const targetBloon = closest; const startPos = x: t.x, y: t.y; const targetPos = getPathPosition(targetBloon.t); projectiles.push( x: startPos.x, y: startPos.y, target: targetBloon, damage: TOWER_DAMAGE, active: true, speed: 7.2, reached: false ); // update projectiles for(let i=0; i<projectiles.length; i++) p.target.health <= 0 // tower placement on canvas (click) function tryPlaceTower(mouseX, mouseY) if(lives <= 0) return false; // check not overlapping existing tower (min 32px) for(let t of towers) const dx = t.x - mouseX; const dy = t.y - mouseY; if(Math.hypot(dx,dy) < 28) return false; // avoid placing on the path (simple check: sample 6 points along track near click) for(let tVal = 0; tVal <= 1.0; tVal+=0.05) const pt = getPathPosition(tVal); if(Math.hypot(pt.x - mouseX, pt.y - mouseY) < 19) return false; // check if enough cash if(cash >= TOWER_PRICE) removeCash(TOWER_PRICE); towers.push( x: mouseX, y: mouseY, cooldown: 0, range: TOWER_RANGE ); return true; return false; // reset full game function resetGame() lives = 100; cash = 350; totalPopped = 0; wave = 1; bloons = []; towers = []; projectiles = []; waveInProgress = false; window._waveSpawnQueue = []; bloonsToSpawn = 0; spawnDelayFrames = 0; updateUI(); // start wave after short delay setTimeout(() => if(lives > 0 && !waveInProgress && bloons.length === 0) startWave(); , 100); document.getElementById('waveStatus').innerHTML = `✨ RESTARTED · Wave 1 ✨`; // ---------- DRAW EVERYTHING (unblocked style, vivid) ---------- function draw() ctx.clearRect(0,0,W,H); // background grassy ctx.fillStyle = "#297a4d"; ctx.fillRect(0,0,W,H); // draw path (dirt track) ctx.beginPath(); for(let i=0; i<waypoints.length; i++) if(i===0) ctx.moveTo(waypoints[i].x, waypoints[i].y); else ctx.lineTo(waypoints[i].x, waypoints[i].y); ctx.lineWidth = 32; ctx.strokeStyle = "#bc9a6c"; ctx.shadowBlur = 0; ctx.stroke(); ctx.lineWidth = 6; ctx.strokeStyle = "#e7cfa1"; ctx.setLineDash([12, 18]); ctx.stroke(); ctx.setLineDash([]); // draw path border ctx.lineWidth = 3; ctx.strokeStyle = "#5e3a1c"; ctx.stroke(); // start / end icons ctx.font = "bold 18monospace"; ctx.fillStyle = "#FFE484"; ctx.shadowBlur = 0; ctx.fillText("🏁", waypoints[0].x-16, waypoints[0].y-8); ctx.fillStyle = "#ff8866"; ctx.fillText("🏁", waypoints[waypoints.length-1].x+4, waypoints[waypoints.length-1].y-8); // draw towers (monkey faces!) for(let t of towers) ctx.beginPath(); ctx.arc(t.x, t.y, 18, 0, Math.PI*2); ctx.fillStyle = "#c99e6f"; ctx.fill(); ctx.beginPath(); ctx.arc(t.x-6, t.y-4, 4, 0, Math.PI*2); ctx.arc(t.x+6, t.y-4, 4, 0, Math.PI*2); ctx.fillStyle = "#2f221b"; ctx.fill(); ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(t.x-7, t.y-6, 1.5, 0, Math.PI*2); ctx.arc(t.x+5, t.y-6, 1.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = "#a55828"; ctx.beginPath(); ctx.ellipse(t.x, t.y+4, 6, 4, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = "#654321"; ctx.font = "bold 20px monospace"; ctx.fillText("🐒", t.x-11, t.y+8); // range indicator (semi) ctx.beginPath(); ctx.arc(t.x, t.y, TOWER_RANGE, 0, Math.PI*2); ctx.globalAlpha = 0.1; ctx.fillStyle = "#fcdb6a"; ctx.fill(); ctx.globalAlpha = 1; // bloons: cute balloons with pop animation feel for(let b of bloons) const pos = getPathPosition(b.t); const hpPercent = b.health / b.maxHealth; ctx.beginPath(); ctx.ellipse(pos.x, pos.y, b.radius, b.radius*1.1, 0, 0, Math.PI*2); ctx.fillStyle = b.color; ctx.fill(); ctx.beginPath(); ctx.moveTo(pos.x-4, pos.y+ b.radius-2); ctx.lineTo(pos.x, pos.y+ b.radius+6); ctx.lineTo(pos.x+4, pos.y+ b.radius-2); ctx.fillStyle = "#ad8b4c"; ctx.fill(); ctx.fillStyle = "white"; ctx.font = `bold $Math.floor(b.radius-2)px monospace`; ctx.fillText(b.health, pos.x-5, pos.y+5); // stress lines ctx.beginPath(); ctx.strokeStyle = "#fff3cf"; ctx.lineWidth = 1.5; for(let s=0; s<3; s++) ctx.moveTo(pos.x-9 + s*3, pos.y-6); ctx.lineTo(pos.x-4 + s*2, pos.y-12); ctx.stroke(); // projectiles (darts) for(let p of projectiles) ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x-6, p.y-4); ctx.lineTo(p.x-2, p.y); ctx.fillStyle = "#d4af37"; ctx.fill(); ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI*2); ctx.fillStyle = "#ffb347"; ctx.fill(); // placement preview if hovering (optional) // UI text overlay: range etc. ctx.font = "bold 14px 'Segoe UI'"; ctx.fillStyle = "#f9eec1"; ctx.shadowBlur = 2; ctx.fillText("✦ Click on grass to build tower ✦", 30, 40); ctx.font = "italic 12px monospace"; ctx.fillStyle = "#cbf5c9"; ctx.fillText("Bloons follow the dirt path", W-180, H-18); // main game loop let lastTimestamp = 0; function gameLoop() if(lives > 0) updateWaveSpawning(); updateBloons(); updateTowersAndProjectiles(); draw(); requestAnimationFrame(gameLoop); // ---- EVENT HANDLERS ---- function handleCanvasClick(e) const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; let mouseX = (e.clientX - rect.left) * scaleX; let mouseY = (e.clientY - rect.top) * scaleY; mouseX = Math.min(W-15, Math.max(15, mouseX)); mouseY = Math.min(H-15, Math.max(15, mouseY)); tryPlaceTower(mouseX, mouseY); updateUI(); // buy tower button logic document.getElementById('buyTowerBtn').addEventListener('click', () => // manual placement mode is via canvas click, but button just informs if(lives <= 0) return; alert("💰 Click anywhere on the grassy track area to build a DART MONKEY! (150 gold)"); ); document.getElementById('resetGameBtn').addEventListener('click', () => resetGame(); ); canvas.addEventListener('click', handleCanvasClick); // initialize game resetGame(); // also sets wave start after short timeout // start loop gameLoop(); )(); </script> </body> </html>

.tower-zone display: flex; gap: 12px; background: #00000055; padding: 5px 18px; border-radius: 40px; align-items: center; unblocked bloons

.tower-btn background: #f5bc70; border: none; font-family: monospace; font-weight: bold; font-size: 1.2rem; padding: 8px 20px; border-radius: 36px; cursor: pointer; box-shadow: 0 4px 0 #7a4a1a; transition: 0.07s linear; color: #2c1a0a; display: flex; align-items: center; gap: 8px;

body background: linear-gradient(145deg, #0a2f2a 0%, #0a1f1a 100%); min-height: 100vh; display: flex; justify-content: center; align-items: center; font-family: 'Segoe UI', 'Courier New', 'Press Start 2P', system-ui, monospace; margin: 0; padding: 20px; ctx.font = "bold 14px 'Segoe UI'"

.stat color: #f9e7b3; text-shadow: 0 2px 0 #3a2a1a; font-size: 1.3rem; letter-spacing: 1px;

/* UI panel */ .info-panel display: flex; justify-content: space-between; align-items: baseline; gap: 20px; margin-top: 18px; margin-bottom: 8px; flex-wrap: wrap; background: #0c2b22cc; backdrop-filter: blur(4px); padding: 12px 24px; border-radius: 60px; ctx.fillStyle = "#f9eec1"

<script> (function() // ---------- CANVAS ---------- const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d');