Initial commit
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user