Kod · Mega Bubble Phonics
CC BY 4.0 · sila beri kredit @joe · v1
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.
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>