427 lines
12 KiB
HTML
427 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>Brick Breaker</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body {
|
|
width: 100%; height: 100%; overflow: hidden;
|
|
background: #1a1a2e; touch-action: none;
|
|
}
|
|
canvas { display: block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<canvas id="game"></canvas>
|
|
<script>
|
|
// Audio Context
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
let audioCtx = null;
|
|
let musicEnabled = true;
|
|
let sfxEnabled = true;
|
|
let musicPlaying = false;
|
|
|
|
function initAudio() {
|
|
if (!audioCtx) audioCtx = new AudioContext();
|
|
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
}
|
|
|
|
// Catchy melody - Super Mario style bouncy tune
|
|
const melodyNotes = [
|
|
{n: 659, d: 0.15}, {n: 659, d: 0.15}, {n: 0, d: 0.15}, {n: 659, d: 0.15},
|
|
{n: 0, d: 0.15}, {n: 523, d: 0.15}, {n: 659, d: 0.3}, {n: 784, d: 0.3},
|
|
{n: 0, d: 0.3}, {n: 392, d: 0.3}, {n: 0, d: 0.3},
|
|
{n: 523, d: 0.2}, {n: 0, d: 0.1}, {n: 392, d: 0.2}, {n: 0, d: 0.1}, {n: 330, d: 0.2},
|
|
{n: 0, d: 0.1}, {n: 440, d: 0.2}, {n: 494, d: 0.2}, {n: 466, d: 0.1}, {n: 440, d: 0.2},
|
|
{n: 392, d: 0.2}, {n: 659, d: 0.2}, {n: 784, d: 0.2}, {n: 880, d: 0.2},
|
|
{n: 698, d: 0.15}, {n: 784, d: 0.15}, {n: 0, d: 0.15}, {n: 659, d: 0.2},
|
|
{n: 523, d: 0.15}, {n: 587, d: 0.15}, {n: 494, d: 0.2}, {n: 0, d: 0.2}
|
|
];
|
|
|
|
const bassNotes = [
|
|
{n: 131, d: 0.3}, {n: 165, d: 0.3}, {n: 196, d: 0.3}, {n: 165, d: 0.3},
|
|
{n: 131, d: 0.3}, {n: 165, d: 0.3}, {n: 196, d: 0.3}, {n: 247, d: 0.3},
|
|
{n: 175, d: 0.3}, {n: 220, d: 0.3}, {n: 262, d: 0.3}, {n: 220, d: 0.3},
|
|
{n: 196, d: 0.3}, {n: 247, d: 0.3}, {n: 294, d: 0.3}, {n: 247, d: 0.3}
|
|
];
|
|
|
|
let melodyIndex = 0, bassIndex = 0;
|
|
let nextMelodyTime = 0, nextBassTime = 0;
|
|
|
|
function playTone(freq, duration, type = 'square', volume = 0.15) {
|
|
if (!audioCtx || !musicEnabled || freq === 0) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.type = type;
|
|
osc.frequency.value = freq;
|
|
gain.gain.setValueAtTime(volume, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration * 0.9);
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + duration);
|
|
}
|
|
|
|
function playSfx(type) {
|
|
if (!audioCtx || !sfxEnabled) return;
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
|
|
if (type === 'hit') {
|
|
osc.frequency.setValueAtTime(520, audioCtx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(320, audioCtx.currentTime + 0.1);
|
|
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
|
|
osc.type = 'square';
|
|
} else if (type === 'break') {
|
|
osc.frequency.setValueAtTime(800, audioCtx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(200, audioCtx.currentTime + 0.15);
|
|
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
|
|
osc.type = 'sawtooth';
|
|
} else if (type === 'die') {
|
|
osc.frequency.setValueAtTime(400, audioCtx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.4);
|
|
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4);
|
|
osc.type = 'sawtooth';
|
|
} else if (type === 'win') {
|
|
osc.frequency.setValueAtTime(523, audioCtx.currentTime);
|
|
osc.frequency.setValueAtTime(659, audioCtx.currentTime + 0.1);
|
|
osc.frequency.setValueAtTime(784, audioCtx.currentTime + 0.2);
|
|
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
|
|
gain.gain.setValueAtTime(0.2, audioCtx.currentTime + 0.2);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.4);
|
|
osc.type = 'square';
|
|
}
|
|
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + 0.5);
|
|
}
|
|
|
|
function updateMusic() {
|
|
if (!audioCtx || !musicEnabled || !musicPlaying) return;
|
|
const now = audioCtx.currentTime;
|
|
|
|
if (now >= nextMelodyTime) {
|
|
const note = melodyNotes[melodyIndex];
|
|
playTone(note.n, note.d, 'square', 0.12);
|
|
nextMelodyTime = now + note.d;
|
|
melodyIndex = (melodyIndex + 1) % melodyNotes.length;
|
|
}
|
|
|
|
if (now >= nextBassTime) {
|
|
const note = bassNotes[bassIndex];
|
|
playTone(note.n, note.d, 'triangle', 0.2);
|
|
nextBassTime = now + note.d;
|
|
bassIndex = (bassIndex + 1) % bassNotes.length;
|
|
}
|
|
}
|
|
|
|
function startMusic() {
|
|
if (!musicPlaying && musicEnabled) {
|
|
initAudio();
|
|
musicPlaying = true;
|
|
nextMelodyTime = audioCtx.currentTime;
|
|
nextBassTime = audioCtx.currentTime;
|
|
}
|
|
}
|
|
|
|
// Game setup
|
|
const canvas = document.getElementById('game');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
let W, H, safeTop, safeBottom, gameH, gameTop;
|
|
|
|
function resize() {
|
|
W = window.innerWidth;
|
|
H = window.innerHeight;
|
|
canvas.width = W;
|
|
canvas.height = H;
|
|
safeTop = H * 0.1;
|
|
safeBottom = H * 0.9;
|
|
gameH = safeBottom - safeTop;
|
|
gameTop = safeTop;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
// Game state
|
|
let gameRunning = false;
|
|
let gameOver = false;
|
|
let score = 0;
|
|
let lives = 5;
|
|
let level = 1;
|
|
let ballSpeed = 3;
|
|
|
|
// Paddle
|
|
const paddle = { w: 120, h: 15 };
|
|
paddle.x = W / 2 - paddle.w / 2;
|
|
paddle.y = safeBottom - 50;
|
|
|
|
// Ball
|
|
const ball = { r: 10, x: W / 2, y: safeBottom - 100, dx: 0, dy: 0 };
|
|
|
|
// Bricks
|
|
let bricks = [];
|
|
const brickRows = 5;
|
|
const brickCols = 8;
|
|
const brickH = 25;
|
|
const brickPadding = 6;
|
|
|
|
const colors = ['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3', '#54a0ff'];
|
|
|
|
function initBricks() {
|
|
bricks = [];
|
|
const brickW = (W - brickPadding * (brickCols + 1)) / brickCols;
|
|
for (let r = 0; r < brickRows; r++) {
|
|
for (let c = 0; c < brickCols; c++) {
|
|
bricks.push({
|
|
x: c * (brickW + brickPadding) + brickPadding,
|
|
y: gameTop + 40 + r * (brickH + brickPadding),
|
|
w: brickW,
|
|
h: brickH,
|
|
color: colors[r],
|
|
alive: true
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetBall() {
|
|
ball.x = paddle.x + paddle.w / 2;
|
|
ball.y = paddle.y - ball.r - 5;
|
|
const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI / 3;
|
|
ball.dx = Math.cos(angle) * ballSpeed;
|
|
ball.dy = Math.sin(angle) * ballSpeed;
|
|
}
|
|
|
|
function init() {
|
|
score = 0;
|
|
lives = 5;
|
|
level = 1;
|
|
ballSpeed = 3; // Start very slow
|
|
paddle.x = W / 2 - paddle.w / 2;
|
|
paddle.y = safeBottom - 50;
|
|
initBricks();
|
|
resetBall();
|
|
gameOver = false;
|
|
gameRunning = true;
|
|
startMusic();
|
|
}
|
|
|
|
// Controls
|
|
let targetX = W / 2;
|
|
let keys = { left: false, right: false };
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = true;
|
|
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = true;
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
if (gameOver) init();
|
|
}
|
|
initAudio();
|
|
});
|
|
|
|
document.addEventListener('keyup', e => {
|
|
if (e.key === 'ArrowLeft' || e.key === 'a') keys.left = false;
|
|
if (e.key === 'ArrowRight' || e.key === 'd') keys.right = false;
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', e => {
|
|
targetX = e.clientX;
|
|
});
|
|
|
|
canvas.addEventListener('click', e => {
|
|
initAudio();
|
|
if (gameOver) init();
|
|
});
|
|
|
|
canvas.addEventListener('touchstart', e => {
|
|
e.preventDefault();
|
|
initAudio();
|
|
if (gameOver) init();
|
|
targetX = e.touches[0].clientX;
|
|
});
|
|
|
|
canvas.addEventListener('touchmove', e => {
|
|
e.preventDefault();
|
|
targetX = e.touches[0].clientX;
|
|
});
|
|
|
|
function update() {
|
|
if (!gameRunning || gameOver) return;
|
|
|
|
// Paddle movement
|
|
if (keys.left) paddle.x -= 10;
|
|
if (keys.right) paddle.x += 10;
|
|
|
|
// Mouse/touch follow
|
|
const diff = targetX - (paddle.x + paddle.w / 2);
|
|
paddle.x += diff * 0.15;
|
|
|
|
// Bounds
|
|
if (paddle.x < 0) paddle.x = 0;
|
|
if (paddle.x + paddle.w > W) paddle.x = W - paddle.w;
|
|
|
|
// Ball movement
|
|
ball.x += ball.dx;
|
|
ball.y += ball.dy;
|
|
|
|
// Wall collision
|
|
if (ball.x - ball.r < 0 || ball.x + ball.r > W) {
|
|
ball.dx = -ball.dx;
|
|
ball.x = Math.max(ball.r, Math.min(W - ball.r, ball.x));
|
|
}
|
|
if (ball.y - ball.r < gameTop) {
|
|
ball.dy = -ball.dy;
|
|
ball.y = gameTop + ball.r;
|
|
}
|
|
|
|
// Paddle collision (generous hitbox)
|
|
if (ball.dy > 0 &&
|
|
ball.y + ball.r >= paddle.y - 5 &&
|
|
ball.y < paddle.y + paddle.h &&
|
|
ball.x >= paddle.x - 15 &&
|
|
ball.x <= paddle.x + paddle.w + 15) {
|
|
ball.dy = -Math.abs(ball.dy);
|
|
const hitPos = (ball.x - paddle.x) / paddle.w;
|
|
ball.dx = (hitPos - 0.5) * ballSpeed * 2;
|
|
ball.y = paddle.y - ball.r - 1;
|
|
playSfx('hit');
|
|
}
|
|
|
|
// Brick collision
|
|
for (let brick of bricks) {
|
|
if (!brick.alive) continue;
|
|
if (ball.x + ball.r > brick.x - 5 &&
|
|
ball.x - ball.r < brick.x + brick.w + 5 &&
|
|
ball.y + ball.r > brick.y - 5 &&
|
|
ball.y - ball.r < brick.y + brick.h + 5) {
|
|
brick.alive = false;
|
|
ball.dy = -ball.dy;
|
|
score += 10;
|
|
playSfx('break');
|
|
|
|
// Check level complete
|
|
if (bricks.every(b => !b.alive)) {
|
|
level++;
|
|
ballSpeed = Math.min(8, 3 + level * 0.5); // Gradual speed increase
|
|
initBricks();
|
|
resetBall();
|
|
playSfx('win');
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Ball lost
|
|
if (ball.y > H) {
|
|
lives--;
|
|
playSfx('die');
|
|
if (lives <= 0) {
|
|
gameOver = true;
|
|
gameRunning = false;
|
|
} else {
|
|
resetBall();
|
|
}
|
|
}
|
|
|
|
updateMusic();
|
|
}
|
|
|
|
function draw() {
|
|
// Background
|
|
ctx.fillStyle = '#1a1a2e';
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
// Safe zone indicators (subtle)
|
|
ctx.fillStyle = '#16213e';
|
|
ctx.fillRect(0, 0, W, safeTop);
|
|
ctx.fillRect(0, safeBottom, W, H - safeBottom);
|
|
|
|
// Score and lives
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 20px Arial';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(`Score: ${score}`, 15, 30);
|
|
ctx.textAlign = 'right';
|
|
ctx.fillText(`Lives: ${'❤️'.repeat(lives)}`, W - 15, 30);
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText(`Level ${level}`, W / 2, 30);
|
|
|
|
// Bricks
|
|
for (let brick of bricks) {
|
|
if (!brick.alive) continue;
|
|
ctx.fillStyle = brick.color;
|
|
ctx.beginPath();
|
|
ctx.roundRect(brick.x, brick.y, brick.w, brick.h, 5);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Paddle
|
|
ctx.fillStyle = '#00ffff';
|
|
ctx.shadowColor = '#00ffff';
|
|
ctx.shadowBlur = 15;
|
|
ctx.beginPath();
|
|
ctx.roundRect(paddle.x, paddle.y, paddle.w, paddle.h, 8);
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
|
|
// Ball
|
|
ctx.fillStyle = '#fff';
|
|
ctx.shadowColor = '#fff';
|
|
ctx.shadowBlur = 10;
|
|
ctx.beginPath();
|
|
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.shadowBlur = 0;
|
|
|
|
// Game over screen
|
|
if (gameOver) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.8)';
|
|
ctx.fillRect(0, 0, W, H);
|
|
ctx.fillStyle = '#fff';
|
|
ctx.font = 'bold 36px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('GAME OVER', W / 2, H / 2 - 40);
|
|
ctx.font = '24px Arial';
|
|
ctx.fillText(`Final Score: ${score}`, W / 2, H / 2 + 10);
|
|
|
|
// Play again button
|
|
ctx.fillStyle = '#00ffff';
|
|
ctx.beginPath();
|
|
ctx.roundRect(W / 2 - 100, H / 2 + 40, 200, 50, 10);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#000';
|
|
ctx.font = 'bold 20px Arial';
|
|
ctx.fillText('TAP TO PLAY AGAIN', W / 2, H / 2 + 72);
|
|
}
|
|
}
|
|
|
|
function gameLoop() {
|
|
update();
|
|
draw();
|
|
requestAnimationFrame(gameLoop);
|
|
}
|
|
|
|
// Window interface
|
|
window.gameScore = 0;
|
|
Object.defineProperty(window, 'gameScore', { get: () => score });
|
|
|
|
window.pauseGame = () => { gameRunning = false; musicPlaying = false; };
|
|
window.resumeGame = () => { if (!gameOver) { gameRunning = true; startMusic(); } };
|
|
window.setMusicEnabled = (v) => { musicEnabled = v; if (!v) musicPlaying = false; };
|
|
window.setSfxEnabled = (v) => { sfxEnabled = v; };
|
|
|
|
// Start
|
|
init();
|
|
gameLoop();
|
|
</script>
|
|
</body>
|
|
</html>
|