Kod · Mega Bubble Phonics

CC BY 4.0 · sila beri kredit @joe · v1

← Butiran permainan

Ambil, olah semula, ajar dengannya

Salin kod di bawah ke dalam ChatGPT, Claude, Gemini, Grok — mana-mana alat yang anda suka — ubah untuk kelas anda, kemudian terbitkan olahan semula anda di sini dengan kredit.

⬇ Muat turun HTML ⬇ Muat turun ZIP
Pemula gesaan suntingan AI
Sunting permainan bilik darjah ini untuk papan putih interaktif. Kekalkan sasaran sentuhan yang besar dan permainan seluruh kelas. Penjejakan, log masuk, pengumpulan data peribadi dan panggilan API jauh adalah dilarang. Kekalkan struktur HTML tunggalnya. Kekalkan matlamat pendidikan. Pencipta asal: @joe di ClassBoard Games (CC BY 4.0).
mega-bubble-phonics.html · 26.5 KB
<!DOCTYPE html>
<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>Mega Bubble Phonics</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&display=swap');

        body {
            margin: 0;
            overflow: hidden;
            background-color: #87CEEB; /* Soft Sky Blue */
            font-family: 'Fredoka', sans-serif;
            touch-action: none; /* Prevent pull-to-refresh on mobile */
            user-select: none;
        }

        #gameCanvas {
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
        }

        /* UI Overlay */
        #ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 10;
            pointer-events: none; /* Let clicks pass through to canvas */
        }

        .pointer-events-auto {
            pointer-events: auto;
        }

        .hud-text {
            text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.2), 0 0 10px rgba(255, 255, 255, 0.8);
        }

        /* Cute Modal Styles */
        .cute-panel {
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(10px);
            border: 4px solid #FF9AA2; /* Pastel Pink */
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2), inset 0 0 20px rgba(255, 154, 162, 0.2);
            border-radius: 2rem;
        }

        .cute-button {
            transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
            border-bottom-width: 6px;
        }
        
        .cute-button:active {
            transform: translateY(4px);
            border-bottom-width: 2px;
        }

        /* Animations */
        @keyframes bounce-in {
            0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
            80% { transform: translate(-50%, -50%) scale(1.05); opacity: 1; }
            100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
        }

        .modal-animate {
            animation: bounce-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
        }

        .shake {
            animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
        }

        @keyframes shake {
            10%, 90% { transform: translate3d(-2px, 0, 0); }
            20%, 80% { transform: translate3d(4px, 0, 0); }
            30%, 50%, 70% { transform: translate3d(-6px, 0, 0); }
            40%, 60% { transform: translate3d(6px, 0, 0); }
        }

        @keyframes float-up {
            0% { transform: translateY(0px); }
            50% { transform: translateY(-15px); }
            100% { transform: translateY(0px); }
        }

        .floating {
            animation: float-up 3s ease-in-out infinite;
        }
    </style>
</head>
<body>

    <!-- Game Canvas -->
    <canvas id="gameCanvas"></canvas>

    <!-- UI Layer -->
    <div id="ui-layer" class="flex flex-col justify-between p-4 md:p-6">
        
        <!-- HUD -->
        <div class="flex justify-between items-start w-full pointer-events-none">
            <div class="bg-white/80 border-4 border-yellow-400 p-3 rounded-2xl shadow-lg">
                <div class="text-sm font-bold text-yellow-500 uppercase tracking-widest text-center">Stars</div>
                <div class="text-4xl font-black text-orange-500 hud-text text-center" id="scoreDisplay">0</div>
            </div>
            
            <div class="flex gap-2 bg-white/80 border-4 border-red-300 p-3 rounded-2xl shadow-lg" id="livesContainer">
                <!-- Hearts injected via JS -->
            </div>
        </div>

        <!-- Question Modal (Hidden by default) -->
        <div id="questionModal" class="hidden absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-11/12 max-w-sm cute-panel p-6 pointer-events-auto">
            <div class="text-center mb-4">
                <h2 class="text-2xl font-bold text-pink-500 mb-2">What's in the bubble?</h2>
                <div class="text-8xl my-4 drop-shadow-md" id="questionEmoji">
                    <!-- Emoji goes here -->
                </div>
            </div>

            <div class="grid grid-cols-1 gap-4" id="optionsGrid">
                <!-- Options injected via JS -->
            </div>
        </div>

        <!-- Start Screen -->
        <div id="startScreen" class="absolute inset-0 bg-sky-300/90 backdrop-blur-sm z-50 flex flex-col items-center justify-center pointer-events-auto">
            <div class="text-center px-6 max-w-lg floating">
                <div class="text-8xl mb-4 drop-shadow-lg">🫧✨</div>
                <h1 class="text-5xl md:text-7xl font-black text-white mb-2 hud-text" style="color: #FF9AA2; -webkit-text-stroke: 2px white;">MEGA BUBBLE<br>PHONICS</h1>
                <div class="bg-yellow-400 text-yellow-900 font-bold px-4 py-1 rounded-full inline-block mb-4 border-2 border-white shadow-sm">50 Words Edition!</div>
                <p class="text-white text-xl md:text-2xl mb-8 font-bold drop-shadow-md">Pop the cute bubbles before they float into the rainbow portal!</p>
                
                <button onclick="startGame()" class="cute-button bg-green-400 hover:bg-green-300 text-white border-green-600 font-bold py-5 px-12 rounded-full shadow-lg text-2xl uppercase tracking-wider w-full md:w-auto">
                    Play Now! 🚀
                </button>
            </div>
        </div>

        <!-- Game Over Screen -->
        <div id="gameOverScreen" class="hidden absolute inset-0 bg-blue-900/90 backdrop-blur-sm z-50 flex flex-col items-center justify-center pointer-events-auto">
            <div class="text-center px-6 floating">
                <div class="text-7xl mb-4">😴✨</div>
                <h2 class="text-5xl md:text-6xl font-black text-yellow-300 mb-4 hud-text">Sleepy Time!</h2>
                <div class="bg-white/20 p-6 rounded-3xl mb-8 border-4 border-white/30">
                    <p class="text-white text-2xl mb-2 font-bold">You collected</p>
                    <p class="text-6xl font-black text-yellow-400 drop-shadow-lg mb-2"><span id="finalScore">0</span></p>
                    <p class="text-white text-2xl font-bold">Stars! ⭐</p>
                </div>
                <button onclick="resetGame()" class="cute-button bg-pink-400 hover:bg-pink-300 text-white border-pink-600 font-bold py-4 px-10 rounded-full shadow-lg text-2xl uppercase tracking-wider">
                    Play Again! 🎈
                </button>
            </div>
        </div>

    </div>

    <script>
        /**
         * CVC DATA REPOSITORY - 50 CVC Words!
         * Includes Short A, E, I, O, U
         */
        const CVC_DATA = [
            // Short A (10 words)
            { word: "cat", emoji: "🐱" }, { word: "bat", emoji: "🦇" }, { word: "map", emoji: "🗺️" }, 
            { word: "dad", emoji: "👨" }, { word: "pan", emoji: "🍳" },
            { word: "hat", emoji: "🎩" }, { word: "mad", emoji: "😠" }, { word: "cap", emoji: "🧢" }, 
            { word: "rat", emoji: "🐀" }, { word: "jam", emoji: "🍓" },
            
            // Short E (10 words)
            { word: "bed", emoji: "🛏️" }, { word: "red", emoji: "🔴" }, { word: "pen", emoji: "🖊️" }, 
            { word: "ten", emoji: "🔟" }, { word: "net", emoji: "🥅" },
            { word: "leg", emoji: "🦵" }, { word: "web", emoji: "🕸️" }, { word: "wet", emoji: "💦" }, 
            { word: "hen", emoji: "🐔" }, { word: "peg", emoji: "📍" },
            
            // Short I (10 words)
            { word: "pig", emoji: "🐷" }, { word: "sit", emoji: "🪑" }, { word: "big", emoji: "🐘" }, 
            { word: "lip", emoji: "👄" }, { word: "win", emoji: "🏆" },
            { word: "fin", emoji: "🦈" }, { word: "hit", emoji: "🎯" }, { word: "zip", emoji: "🤐" }, 
            { word: "kid", emoji: "🧒" }, { word: "dip", emoji: "🥣" },
            
            // Short O (10 words)
            { word: "dog", emoji: "🐶" }, { word: "hot", emoji: "♨️" }, { word: "pot", emoji: "🍲" }, 
            { word: "box", emoji: "📦" }, { word: "top", emoji: "🪀" },
            { word: "fox", emoji: "🦊" }, { word: "log", emoji: "🪵" }, { word: "mop", emoji: "🧹" }, 
            { word: "hop", emoji: "🐇" }, { word: "cot", emoji: "🏕️" },
            
            // Short U (10 words)
            { word: "sun", emoji: "☀️" }, { word: "cup", emoji: "🍵" }, { word: "bug", emoji: "🐛" }, 
            { word: "run", emoji: "🏃" }, { word: "nut", emoji: "🥜" },
            { word: "bus", emoji: "🚌" }, { word: "tub", emoji: "🛁" }, { word: "mud", emoji: "🤎" }, 
            { word: "rug", emoji: "🧶" }, { word: "gum", emoji: "🍬" }
        ];

        // Pastel Colors for Bubbles and Buttons
        const CUTE_COLORS = [
            '#FFB7B2', '#FFDAC1', '#E2F0CB', '#B5EAD7', '#C7CEEA', '#FDFD96'
        ];

        const BUTTON_COLORS = [
            { bg: 'bg-blue-400', border: 'border-blue-600', hover: 'hover:bg-blue-300' },
            { bg: 'bg-pink-400', border: 'border-pink-600', hover: 'hover:bg-pink-300' },
            { bg: 'bg-purple-400', border: 'border-purple-600', hover: 'hover:bg-purple-300' },
            { bg: 'bg-yellow-400', border: 'border-yellow-600', hover: 'hover:bg-yellow-300' },
            { bg: 'bg-orange-400', border: 'border-orange-600', hover: 'hover:bg-orange-300' }
        ];

        /**
         * GAME ENGINE
         */
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        
        let gameState = 'START'; // START, PLAYING, MODAL, GAMEOVER
        let score = 0;
        let lives = 3;
        
        let particles = []; // Background magical dust
        let confettis = []; // Confetti for correct answers
        let bubbles = [];   // The CVC bubbles
        let portalAngle = 0;
        
        let spawnRate = 220; // Slightly slower for kids
        let spawnTimer = 0;
        let gameFrame = 0;
        let activeBubble = null;
        
        let width, height, centerX, centerY;

        function resize() {
            width = canvas.width = window.innerWidth;
            height = canvas.height = window.innerHeight;
            centerX = width / 2;
            centerY = height / 2;
        }

        window.addEventListener('resize', resize);
        resize();

        // --- CLASSES ---

        class Sparkle {
            constructor() {
                this.reset();
                // Randomize initial position so screen isn't empty on load
                this.x = Math.random() * width;
                this.y = Math.random() * height;
            }

            reset() {
                this.x = Math.random() * width;
                this.y = height + Math.random() * 100; // Start below screen
                this.size = Math.random() * 4 + 2;
                this.speedY = Math.random() * -1 - 0.5; // Float up
                this.speedX = (Math.random() - 0.5) * 0.5;
                this.opacity = Math.random() * 0.5 + 0.2;
                this.wobble = Math.random() * Math.PI * 2;
            }

            update() {
                this.y += this.speedY;
                this.x += this.speedX + Math.sin(this.wobble) * 0.5;
                this.wobble += 0.05;

                if (this.y < -10) {
                    this.reset();
                }
            }

            draw() {
                ctx.fillStyle = `rgba(255, 255, 255, ${this.opacity})`;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                ctx.fill();
            }
        }

        class Confetti {
            constructor(x, y) {
                this.x = x;
                this.y = y;
                this.size = Math.random() * 10 + 5;
                this.color = CUTE_COLORS[Math.floor(Math.random() * CUTE_COLORS.length)];
                
                const angle = Math.random() * Math.PI * 2;
                const velocity = Math.random() * 15 + 5;
                this.vx = Math.cos(angle) * velocity;
                this.vy = Math.sin(angle) * velocity - 5; // Extra upward pop
                
                this.gravity = 0.5;
                this.drag = 0.95;
                this.rotation = Math.random() * 360;
                this.rotationSpeed = (Math.random() - 0.5) * 20;
                this.life = 1.0;
                this.decay = Math.random() * 0.02 + 0.01;
            }

            update() {
                this.vx *= this.drag;
                this.vy *= this.drag;
                this.vy += this.gravity;
                this.x += this.vx;
                this.y += this.vy;
                this.rotation += this.rotationSpeed;
                this.life -= this.decay;
            }

            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.rotate(this.rotation * Math.PI / 180);
                ctx.globalAlpha = Math.max(0, this.life);
                ctx.fillStyle = this.color;
                ctx.fillRect(-this.size/2, -this.size/2, this.size, this.size);
                ctx.restore();
            }
        }

        class Bubble {
            constructor() {
                // Spawn off-screen
                const edge = Math.floor(Math.random() * 4);
                if (edge === 0) { this.x = Math.random() * width; this.y = -60; } // Top
                else if (edge === 1) { this.x = width + 60; this.y = Math.random() * height; } // Right
                else if (edge === 2) { this.x = Math.random() * width; this.y = height + 60; } // Bottom
                else { this.x = -60; this.y = Math.random() * height; } // Left

                this.speed = 0.3 + (score * 0.01); // Very slow start for kids
                if (this.speed > 1.5) this.speed = 1.5; // Cap speed lower

                this.radius = 45; // Big friendly bubbles
                this.wobble = Math.random() * Math.PI * 2;
                
                // Pick a random CVC word out of the 50
                this.cvcData = CVC_DATA[Math.floor(Math.random() * CVC_DATA.length)];
                this.color = CUTE_COLORS[Math.floor(Math.random() * CUTE_COLORS.length)];
            }

            update() {
                if (gameState !== 'PLAYING') return false;

                // Move towards center portal
                const dx = centerX - this.x;
                const dy = centerY - this.y;
                const dist = Math.sqrt(dx*dx + dy*dy);

                this.x += (dx / dist) * this.speed;
                this.y += (dy / dist) * this.speed;
                
                // Add playful floating wobble
                this.y += Math.sin(this.wobble) * 0.5;
                this.wobble += 0.05;

                // Collision with portal (Missed it!)
                if (dist < 50) {
                    loseLife();
                    return true; // Mark for deletion
                }
                return false;
            }

            draw() {
                ctx.save();
                ctx.translate(this.x, this.y);
                
                // Bubble Body
                ctx.beginPath();
                ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
                
                // Gradient for 3D bubble effect
                const grad = ctx.createRadialGradient(-10, -10, 5, 0, 0, this.radius);
                grad.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
                grad.addColorStop(0.3, this.color);
                grad.addColorStop(1, 'rgba(255, 255, 255, 0.4)');
                
                ctx.fillStyle = grad;
                ctx.fill();
                
                ctx.lineWidth = 3;
                ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
                ctx.stroke();

                // Bubble Shine
                ctx.beginPath();
                ctx.ellipse(-15, -15, 10, 5, Math.PI / 4, 0, Math.PI * 2);
                ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
                ctx.fill();

                // Emoji
                ctx.font = '40px Arial';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                // Slight shadow for emoji
                ctx.shadowColor = 'rgba(0,0,0,0.2)';
                ctx.shadowBlur = 5;
                ctx.shadowOffsetX = 2;
                ctx.shadowOffsetY = 2;
                ctx.fillText(this.cvcData.emoji, 0, 5); // offset y slightly for visual center

                ctx.restore();
            }
        }

        // --- GAME LOGIC ---

        function init() {
            // Create background sparkles
            for (let i = 0; i < 50; i++) {
                particles.push(new Sparkle());
            }
            updateLivesUI();
            animate();
        }

        function startGame() {
            document.getElementById('startScreen').classList.add('hidden');
            document.getElementById('gameOverScreen').classList.add('hidden');
            resetVars();
            gameState = 'PLAYING';
        }

        function resetVars() {
            score = 0;
            lives = 3;
            spawnRate = 220;
            bubbles = [];
            confettis = [];
            activeBubble = null;
            document.getElementById('scoreDisplay').innerText = score;
            updateLivesUI();
        }

        function loseLife() {
            lives--;
            updateLivesUI();
            
            // Screen Shake Effect
            document.body.classList.add('shake');
            setTimeout(() => document.body.classList.remove('shake'), 500);

            // Pop sound fallback (visual)
            ctx.fillStyle = 'rgba(255, 100, 100, 0.3)';
            ctx.fillRect(0, 0, width, height);

            if (lives <= 0) {
                gameOver();
            }
        }

        function updateLivesUI() {
            const container = document.getElementById('livesContainer');
            container.innerHTML = '';
            for(let i=0; i<3; i++) {
                const heart = document.createElement('div');
                heart.className = `text-3xl transition-all duration-300 ${i < lives ? 'scale-100 opacity-100' : 'scale-75 opacity-30 grayscale'}`;
                heart.innerText = '❤️';
                container.appendChild(heart);
            }
        }

        function gameOver() {
            gameState = 'GAMEOVER';
            document.getElementById('finalScore').innerText = score;
            document.getElementById('gameOverScreen').classList.remove('hidden');
            document.getElementById('questionModal').classList.add('hidden');
        }

        function resetGame() {
            startGame();
        }

        // --- INTERACTION ---

        function handleInput(x, y) {
            if (gameState !== 'PLAYING') return;

            // Check if clicked on a bubble
            for (let i = bubbles.length - 1; i >= 0; i--) { // Check newest top-most first
                const b = bubbles[i];
                const dx = x - b.x;
                const dy = y - b.y;
                const dist = Math.sqrt(dx*dx + dy*dy);

                if (dist < b.radius + 15) { // +15 forgiveness for chubby little fingers
                    activateRescue(b, i);
                    break;
                }
            }
        }

        canvas.addEventListener('mousedown', (e) => handleInput(e.clientX, e.clientY));
        canvas.addEventListener('touchstart', (e) => {
            handleInput(e.touches[0].clientX, e.touches[0].clientY);
        }, {passive: false});

        function activateRescue(bubble, index) {
            gameState = 'MODAL'; // Pauses the game loop updates
            activeBubble = { data: bubble, index: index };
            
            const modal = document.getElementById('questionModal');
            const qEmoji = document.getElementById('questionEmoji');
            const grid = document.getElementById('optionsGrid');

            // Set Emoji
            qEmoji.innerText = bubble.cvcData.emoji;

            // Generate Options
            grid.innerHTML = '';
            const correctWord = bubble.cvcData.word;
            let options = [correctWord];
            
            // Get 2 random wrong answers for 3 total options (from the giant pool of 50!)
            const pool = CVC_DATA.filter(opt => opt.word !== correctWord);
            while (options.length < 3) {
                const randomOpt = pool[Math.floor(Math.random() * pool.length)].word;
                if (!options.includes(randomOpt)) options.push(randomOpt);
            }

            // Shuffle
            options.sort(() => Math.random() - 0.5);

            // Create big cute buttons
            options.forEach((opt, idx) => {
                const btn = document.createElement('button');
                const btnTheme = BUTTON_COLORS[idx % BUTTON_COLORS.length];
                
                btn.className = `cute-button w-full ${btnTheme.bg} ${btnTheme.border} ${btnTheme.hover} text-white font-bold py-4 px-6 rounded-2xl text-3xl tracking-wide text-center uppercase shadow-md`;
                btn.innerText = opt;
                btn.onclick = () => checkAnswer(opt, correctWord);
                grid.appendChild(btn);
            });

            // Trigger bounce animation
            modal.classList.remove('hidden');
            modal.classList.remove('modal-animate');
            void modal.offsetWidth; // Trigger reflow
            modal.classList.add('modal-animate');
        }

        function checkAnswer(selected, correct) {
            const modal = document.getElementById('questionModal');
            
            if (selected === correct) {
                // YAY! CORRECT!
                score += 1; // 1 star per correct
                document.getElementById('scoreDisplay').innerText = score;
                
                if (activeBubble) {
                    // Confetti explosion at bubble location
                    spawnConfetti(activeBubble.data.x, activeBubble.data.y);
                    // Remove bubble
                    bubbles.splice(activeBubble.index, 1);
                }

                // Slightly increase difficulty over time
                if (spawnRate > 80 && score % 3 === 0) spawnRate -= 10;
            } else {
                // OOPS! WRONG!
                // Push bubble closer to the center portal as a slight penalty
                if (activeBubble && bubbles[activeBubble.index]) {
                    const b = bubbles[activeBubble.index];
                    const dx = centerX - b.x;
                    const dy = centerY - b.y;
                    b.x += dx * 0.4; 
                    b.y += dy * 0.4;
                    
                    // Show a little red flash
                    ctx.fillStyle = 'rgba(255, 0, 0, 0.4)';
                    ctx.fillRect(0, 0, width, height);
                }
            }

            modal.classList.add('hidden');
            gameState = 'PLAYING';
            activeBubble = null;
        }

        function spawnConfetti(x, y) {
            for(let i=0; i<40; i++) {
                confettis.push(new Confetti(x, y));
            }
        }

        function drawPortal() {
            portalAngle += 0.02;
            ctx.save();
            ctx.translate(centerX, centerY);
            ctx.rotate(portalAngle);

            // Draw a spinning rainbow star/portal
            const numSpokes = 8;
            for(let i=0; i<numSpokes; i++) {
                ctx.rotate((Math.PI * 2) / numSpokes);
                ctx.beginPath();
                ctx.moveTo(0, 0);
                ctx.lineTo(30, 80);
                ctx.lineTo(-30, 80);
                ctx.fillStyle = `hsla(${(i * 45 + gameFrame) % 360}, 80%, 70%, 0.6)`;
                ctx.fill();
            }

            // Center glowing circle
            ctx.beginPath();
            ctx.arc(0, 0, 40, 0, Math.PI * 2);
            ctx.fillStyle = '#fff';
            ctx.shadowColor = '#FFB7B2';
            ctx.shadowBlur = 20;
            ctx.fill();
            
            ctx.restore();
        }

        // --- MAIN LOOP ---

        function animate() {
            // Soft sky blue background wipe
            ctx.fillStyle = '#87CEEB';
            ctx.fillRect(0, 0, width, height);

            // Draw Background Sparkles
            particles.forEach(p => {
                p.update();
                p.draw();
            });

            // Draw Rainbow Portal at center
            drawPortal();

            // Draw and Update Confetti
            for (let i = confettis.length - 1; i >= 0; i--) {
                confettis[i].update();
                confettis[i].draw();
                if (confettis[i].life <= 0 || confettis[i].y > height + 50) {
                    confettis.splice(i, 1);
                }
            }

            // Handle Bubbles
            if (gameState === 'PLAYING') {
                gameFrame++;
                
                // Spawn Logic
                if (gameFrame % spawnRate === 0) {
                    bubbles.push(new Bubble());
                }

                // Update & Draw Bubbles
                for (let i = bubbles.length - 1; i >= 0; i--) {
                    let shouldRemove = bubbles[i].update();
                    if (shouldRemove) {
                        bubbles.splice(i, 1);
                        // Edge case safeguard
                        if (activeBubble && activeBubble.index === i) {
                            document.getElementById('questionModal').classList.add('hidden');
                            activeBubble = null;
                        }
                    } else {
                        bubbles[i].draw();
                    }
                }
            } else if (gameState === 'MODAL') {
                // Just draw them frozen if modal is open
                bubbles.forEach(b => b.draw());
            }

            requestAnimationFrame(animate);
        }

        // Boot
        init();

    </script>
</body>
</html>