breakout-game / index.html
LukasBe's picture
Add 2 files
4b64993 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cosmic Laser Breakout 3D</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
<script src="https://kit.fontawesome.com/5a4d8a29cf.js" crossorigin="anonymous"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: radial-gradient(ellipse at center, #000000 0%, #16004b 100%);
color: #fff;
font-family: 'Orbitron', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
perspective: 1200px;
}
#gameWrapper {
position: relative;
width: 1000px;
height: 700px;
transform-style: preserve-3d;
transform: rotateX(10deg) rotateY(0deg) rotateZ(0deg);
perspective: 1500px;
filter: drop-shadow(0 0 50px rgba(107, 0, 255, 0.7));
}
#gameContainer {
position: relative;
width: 100%;
height: 100%;
border: 4px solid transparent;
border-image: linear-gradient(45deg, #6b00ff, #00dcff, #ff00aa);
border-image-slice: 1;
box-shadow:
0 0 60px rgba(107, 0, 255, 0.8),
inset 0 0 40px rgba(107, 0, 255, 0.6),
inset 0 0 80px rgba(0, 220, 255, 0.4),
inset 0 0 120px rgba(255, 0, 170, 0.2);
overflow: hidden;
background:
radial-gradient(circle at 50% 0%, rgba(107, 0, 255, 0.2) 0%, transparent 70%),
radial-gradient(circle at 50% 100%, rgba(0, 220, 255, 0.2) 0%, transparent 70%),
linear-gradient(0deg, rgba(0, 0, 30, 0.9) 0%, rgba(10, 0, 80, 0.7) 100%);
transform-style: preserve-3d;
backdrop-filter: blur(2px);
}
#paddle {
position: absolute;
width: 140px;
height: 20px;
background: linear-gradient(90deg, #00dcff, #6b00ff);
border-radius: 12px;
box-shadow:
0 0 40px rgba(0, 220, 255, 0.8),
0 0 80px rgba(107, 0, 255, 0.4),
inset 0 5px 10px rgba(255, 255, 255, 0.4);
bottom: 40px;
left: 430px;
z-index: 10;
transition: transform 0.1s cubic-bezier(0.17, 0.67, 0.83, 0.67);
transform-origin: center bottom;
transform: translateZ(20px);
}
#paddle::before {
content: '';
position: absolute;
top: -5px;
left: 0;
right: 0;
height: 5px;
background: linear-gradient(90deg, #00dcff, #6b00ff);
border-radius: 5px;
filter: blur(2px);
opacity: 0.8;
}
#paddle::after {
content: '';
position: absolute;
bottom: -10px;
left: 10%;
right: 10%;
height: 5px;
background: rgba(0, 220, 255, 0.3);
border-radius: 50%;
filter: blur(5px);
transform: translateZ(-5px);
}
#ball {
position: absolute;
width: 18px;
height: 18px;
background: radial-gradient(circle at 30% 30%, #fff, #00dcff);
border-radius: 50%;
box-shadow:
0 0 15px #fff,
0 0 30px rgba(255, 255, 255, 0.8),
0 0 60px rgba(255, 255, 255, 0.4),
inset 0 0 5px rgba(255, 255, 255, 0.8);
z-index: 20;
transform-style: preserve-3d;
filter: drop-shadow(0 0 5px #fff);
}
.brick {
position: absolute;
width: 92px;
height: 32px;
border-radius: 4px;
transform: translateZ(20px);
opacity: 1;
transition: all 0.3s ease-out;
perspective: 500px;
border-bottom: 4px solid rgba(0,0,0,0.4);
overflow: hidden;
will-change: transform, opacity;
}
.brick::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(to bottom, rgba(255,255,255,0.4) 0%, transparent 100%);
border-radius: 4px 4px 0 0;
transform-origin: top;
transform: rotateX(60deg);
}
.brick::after {
content: '';
position: absolute;
bottom: -2px;
left: 5%;
right: 5%;
height: 3px;
background: rgba(255,255,255,0.7);
border-radius: 50%;
filter: blur(2px);
transform: translateZ(-5px);
}
#scoreDisplay, #livesDisplay {
position: absolute;
top: 20px;
font-size: 24px;
font-weight: 700;
letter-spacing: 2px;
text-shadow:
0 0 10px #6b00ff,
0 0 20px rgba(107, 0, 255, 0.8);
color: #fff;
z-index: 10;
transform: translateZ(30px);
font-family: 'Orbitron', sans-serif;
}
#scoreDisplay { left: 20px; }
#livesDisplay { right: 20px; }
#comboCounter {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) translateZ(30px) scale(0);
font-size: 48px;
font-weight: 900;
letter-spacing: 4px;
color: #ff00aa;
text-shadow:
0 0 20px #ff00aa,
0 0 40px rgba(255, 0, 170, 0.8);
z-index: 10;
opacity: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
#gameOver {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) translateZ(50px);
font-size: 80px;
font-weight: 900;
color: #ff00aa;
text-shadow:
0 0 20px #ff00aa,
0 0 40px rgba(255, 0, 170, 0.9),
0 0 80px rgba(255, 0, 170, 0.7);
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease-out;
letter-spacing: 5px;
z-index: 100;
transform-style: preserve-3d;
text-transform: uppercase;
text-align: center;
}
#startMessage {
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, -50%) translateZ(30px);
font-size: 24px;
color: #00dcff;
text-shadow:
0 0 10px #6b00ff,
0 0 20px rgba(107, 0, 255, 0.8);
text-align: center;
opacity: 1;
transition: opacity 0.3s ease-out;
letter-spacing: 2px;
z-index: 50;
}
#startBtn {
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, 0) translateZ(30px);
padding: 12px 30px;
background: linear-gradient(135deg, #ff00aa 0%, #6b00ff 100%);
border: none;
border-radius: 30px;
color: white;
font-weight: bold;
font-size: 18px;
letter-spacing: 2px;
cursor: pointer;
opacity: 1;
transition: all 0.3s ease-out;
box-shadow:
0 0 10px #ff00aa,
0 0 30px rgba(255, 0, 170, 0.8);
z-index: 50;
text-transform: uppercase;
font-family: 'Orbitron', sans-serif;
}
#startBtn:hover {
background: linear-gradient(135deg, #ff00aa 0%, #00dcff 100%);
box-shadow:
0 0 15px #ff00aa,
0 0 50px rgba(255, 0, 170, 0.9);
}
.particle {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
pointer-events: none;
z-index: 5;
will-change: transform, opacity;
}
.explosion {
position: absolute;
border-radius: 50%;
pointer-events: none;
z-index: 20;
transform-style: preserve-3d;
will-change: transform, opacity;
}
.bonus {
position: absolute;
width: 24px;
height: 24px;
border-radius: 50%;
z-index: 15;
font-family: 'Orbitron', sans-serif;
font-weight: bold;
text-align: center;
line-height: 24px;
font-size: 14px;
box-shadow:
0 0 10px currentColor,
0 0 20px rgba(255,255,255,0.4);
animation: float 1s ease-in-out infinite alternate;
}
@keyframes float {
0% { transform: translateY(0px); }
100% { transform: translateY(-3px); }
}
.bonus.multiball {
background: radial-gradient(circle at 30% 30%, #00dcff, #6b00ff);
color: white;
}
.bonus.extralife {
background: radial-gradient(circle at 30% 30%, #ff0000, #ff00aa);
color: white;
}
.bonus.widepaddle {
background: radial-gradient(circle at 30% 30%, #ffff00, #ffaa00);
color: black;
}
.bonus.slowmo {
background: radial-gradient(circle at 30% 30%, #00ff00, #007700);
color: white;
}
.bonus.points {
background: radial-gradient(circle at 30% 30%, #ffffff, #aaaaaa);
color: black;
}
.powerup-active {
animation: powerupGlow 0.5s ease-in-out infinite alternate;
}
@keyframes powerupGlow {
0% { box-shadow: 0 0 10px #ff00aa; }
100% { box-shadow: 0 0 40px #ff00aa; }
}
.trail {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
pointer-events: none;
z-index: 19;
filter: blur(1px);
transform: scale(0);
opacity: 0;
will-change: transform, opacity;
}
.ball-pulse {
position: absolute;
border-radius: 50%;
border: 2px solid rgba(0, 220, 255, 0.8);
pointer-events: none;
z-index: 18;
transform: scale(0);
opacity: 1;
will-change: transform, opacity;
}
.star {
position: absolute;
width: 6px;
height: 6px;
background: white;
clip-path: polygon(
50% 0%,
61% 35%,
98% 35%,
68% 57%,
79% 91%,
50% 70%,
21% 91%,
32% 57%,
2% 35%,
39% 35%
);
pointer-events: none;
z-index: 3;
will-change: transform, opacity;
filter: drop-shadow(0 0 5px currentColor);
}
</style>
</head>
<body>
<div id="gameWrapper">
<div id="gameContainer">
<div id="paddle"></div>
<div id="ball"></div>
<div id="scoreDisplay">SCORE: 0</div>
<div id="livesDisplay">LIVES: 3</div>
<div id="comboCounter">COMBO x3!</div>
<div id="gameOver">GAME OVER</div>
<div id="startMessage">COSMIC LASER BREAKOUT</div>
<button id="startBtn">START GAME</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Game elements
const gameContainer = document.getElementById('gameContainer');
const paddle = document.getElementById('paddle');
const ball = document.getElementById('ball');
const scoreDisplay = document.getElementById('scoreDisplay');
const livesDisplay = document.getElementById('livesDisplay');
const comboCounter = document.getElementById('comboCounter');
const gameOver = document.getElementById('gameOver');
const startMessage = document.getElementById('startMessage');
const startBtn = document.getElementById('startBtn');
// Game dimensions
const gameWidth = gameContainer.offsetWidth;
const gameHeight = gameContainer.offsetHeight;
const paddleWidth = paddle.offsetWidth;
const paddleHeight = paddle.offsetHeight;
const ballSize = ball.offsetWidth;
// Game state
let paddleX = (gameWidth - paddleWidth) / 2;
let ballX = gameWidth / 2 - ballSize / 2;
let ballY = gameHeight - paddleHeight - 40 - ballSize;
let ballSpeedX = 0;
let ballSpeedY = 0;
let score = 0;
let lives = 3;
let combo = 0;
let gameStarted = false;
let gameActive = false;
let bricks = [];
let brickCount = 0;
let activePowerups = [];
let balls = [{ x: ballX, y: ballY, speedX: ballSpeedX, speedY: ballSpeedY, element: ball }];
let trailInterval;
let starParticles = [];
let originalPaddleWidth = paddleWidth;
// Initialize game elements
function initGame() {
// Position paddle and ball
paddleX = (gameWidth - originalPaddleWidth) / 2;
paddle.style.width = originalPaddleWidth + 'px';
paddle.style.left = paddleX + 'px';
ballX = paddleX + originalPaddleWidth / 2 - ballSize / 2;
ballY = gameHeight - paddleHeight - 40 - ballSize;
ball.style.left = ballX + 'px';
ball.style.top = ballY + 'px';
ballSpeedX = 0;
ballSpeedY = 0;
// Create bricks
createBricks();
// Clear active powerups
activePowerups.forEach(powerup => {
clearTimeout(powerup.timer);
});
activePowerups = [];
// Remove any extra balls
balls.slice(1).forEach(b => {
if (gameContainer.contains(b.element)) {
gameContainer.removeChild(b.element);
}
});
balls = [{ x: ballX, y: ballY, speedX: ballSpeedX, speedY: ballSpeedY, element: ball }];
// Update displays
scoreDisplay.textContent = `SCORE: ${score}`;
livesDisplay.textContent = `LIVES: ${lives}`;
// Hide game over if visible
gameOver.style.opacity = '0';
gameActive = true;
}
// Create brick layout
function createBricks() {
// Clear existing bricks
bricks.forEach(brick => {
if (gameContainer.contains(brick)) {
gameContainer.removeChild(brick);
}
});
bricks = [];
brickCount = 0;
const rows = 5;
const cols = 10;
const brickWidth = 92;
const brickHeight = 32;
const brickPadding = 4;
const offsetTop = 60;
const offsetLeft = (gameWidth - (cols * (brickWidth + brickPadding))) / 2;
// Colors for different brick rows
const colors = [
'rgba(255, 50, 50, 0.8)',
'rgba(255, 153, 51, 0.8)',
'rgba(247, 219, 96, 0.8)',
'rgba(80, 200, 120, 0.8)',
'rgba(100, 175, 255, 0.8)'
];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const brick = document.createElement('div');
brick.className = 'brick';
brick.style.left = offsetLeft + c * (brickWidth + brickPadding) + 'px';
brick.style.top = offsetTop + r * (brickHeight + brickPadding) + 'px';
brick.style.backgroundColor = colors[r];
brick.hits = 1; // Number of hits to break (1 for standard bricks)
brick.scoreValue = (rows - r) * 10; // More points for higher rows
// 15% chance to have a powerup brick
if (Math.random() < 0.15) {
brick.hasPowerup = true;
brick.powerupType = ['multiball', 'extralife', 'widepaddle', 'slowmo', 'points'][Math.floor(Math.random() * 5)];
}
gameContainer.appendChild(brick);
bricks.push(brick);
brickCount++;
}
}
}
// Launch the ball
function launchBall() {
if (!gameActive || gameStarted) return;
gameStarted = true;
startMessage.style.opacity = '0';
startBtn.style.opacity = '0';
startBtn.style.pointerEvents = 'none';
// Random initial angle
const angle = (Math.random() * Math.PI / 2) + Math.PI / 4; // Between 45 and 135 degrees
const speed = 4;
ballSpeedX = Math.cos(angle) * speed;
ballSpeedY = -Math.sin(angle) * speed;
balls[0].speedX = ballSpeedX;
balls[0].speedY = ballSpeedY;
// Start ball trail
if (!trailInterval) {
trailInterval = setInterval(createBallTrail, 50);
}
// Create stars background
createStars();
}
// Create stars background
function createStars() {
// Clear existing stars
starParticles.forEach(star => {
if (gameContainer.contains(star)) {
gameContainer.removeChild(star);
}
});
starParticles = [];
// Create 100 stars
for (let i = 0; i < 100; i++) {
const star = document.createElement('div');
star.className = 'star';
star.style.left = Math.random() * gameWidth + 'px';
star.style.top = Math.random() * gameHeight + 'px';
// Random size and color
const size = 2 + Math.random() * 5;
const hue = Math.random() * 360;
const opacity = 0.2 + Math.random() * 0.8;
star.style.width = size + 'px';
star.style.height = size + 'px';
star.style.color = `hsla(${hue}, 100%, 80%, ${opacity})`;
// Random animation for twinkling
star.style.animation = `float ${2 + Math.random() * 3}s ease-in-out infinite ${Math.random() * 3}s alternate`;
gameContainer.appendChild(star);
starParticles.push(star);
}
}
// Create ball trail effect
function createBallTrail() {
if (!gameStarted || !gameActive) return;
balls.forEach(ballData => {
const trail = document.createElement('div');
trail.className = 'trail';
trail.style.left = (ballData.x + ballSize/2) + 'px';
trail.style.top = (ballData.y + ballSize/2) + 'px';
trail.style.background = `radial-gradient(circle, rgba(0,220,255,0.8) 0%, rgba(107,0,255,0.6) 100%)`;
trail.style.transform = 'scale(0.5)';
gameContainer.appendChild(trail);
// Animate trail
setTimeout(() => {
trail.style.transform = 'scale(1.5)';
trail.style.opacity = '0.6';
setTimeout(() => {
if (gameContainer.contains(trail)) {
gameContainer.removeChild(trail);
}
}, 200);
}, 10);
});
}
// Create a pulse effect when ball hits something
function createBallPulse(x, y) {
const pulse = document.createElement('div');
pulse.className = 'ball-pulse';
pulse.style.left = x + 'px';
pulse.style.top = y + 'px';
pulse.style.width = ballSize + 'px';
pulse.style.height = ballSize + 'px';
pulse.style.borderColor = `hsl(${Math.random() * 360}, 100%, 70%)`;
gameContainer.appendChild(pulse);
// Animate pulse
let scale = 1;
const animate = () => {
scale += 0.2;
pulse.style.transform = `scale(${scale})`;
pulse.style.opacity = 1 - scale * 0.1;
if (scale < 5) {
requestAnimationFrame(animate);
} else {
if (gameContainer.contains(pulse)) {
gameContainer.removeChild(pulse);
}
}
};
requestAnimationFrame(animate);
}
// Reset ball after losing a life
function resetBall() {
gameStarted = false;
ballX = paddleX + paddleWidth / 2 - ballSize / 2;
ballY = gameHeight - paddleHeight - 40 - ballSize;
ball.style.left = ballX + 'px';
ball.style.top = ballY + 'px';
ballSpeedX = 0;
ballSpeedY = 0;
// Update main ball position in array
balls[0].x = ballX;
balls[0].y = ballY;
balls[0].speedX = 0;
balls[0].speedY = 0;
startMessage.style.opacity = '1';
startMessage.textContent = 'GET READY';
startBtn.style.opacity = '1';
startBtn.style.pointerEvents = 'auto';
startBtn.textContent = 'CONTINUE';
// Clear trail interval since ball is not moving
clearInterval(trailInterval);
trailInterval = null;
}
// Game over
function endGame() {
gameActive = false;
gameOver.style.opacity = '1';
startBtn.style.opacity = '1';
startBtn.style.pointerEvents = 'auto';
startBtn.textContent = 'PLAY AGAIN';
// Clear trail interval
clearInterval(trailInterval);
trailInterval = null;
}
// Create a bonus/powerup that falls from brick
function createBonus(x, y, type) {
const bonus = document.createElement('div');
bonus.className = `bonus ${type}`;
bonus.style.left = x + 'px';
bonus.style.top = y + 'px';
// Set symbol based on type
switch(type) {
case 'multiball':
bonus.textContent = 'M';
break;
case 'extralife':
bonus.textContent = '+';
break;
case 'widepaddle':
bonus.textContent = 'W';
break;
case 'slowmo':
bonus.textContent = 'S';
break;
case 'points':
bonus.textContent = 'P';
break;
}
gameContainer.appendChild(bonus);
// Animate falling
let speed = 2;
let spin = (Math.random() - 0.5) * 5;
let rotation = 0;
const fall = () => {
y += speed;
rotation += spin;
bonus.style.top = y + 'px';
bonus.style.transform = `rotate(${rotation}deg)`;
// Check if caught by paddle
if (y + 24 >= gameHeight - paddleHeight - 40 &&
y + 24 <= gameHeight - 40 &&
x + 24 >= paddleX &&
x <= paddleX + paddleWidth) {
// Apply powerup
activatePowerup(type);
// Remove bonus
gameContainer.removeChild(bonus);
return;
}
// Check if fell off screen
if (y < gameHeight) {
requestAnimationFrame(fall);
} else {
gameContainer.removeChild(bonus);
}
};
requestAnimationFrame(fall);
}
// Activate a powerup
function activatePowerup(type) {
// Create powerup activation effect
createPowerupActivationEffect(paddleX + paddleWidth/2, gameHeight - paddleHeight - 20, type);
switch(type) {
case 'multiball':
// Create 2 extra balls
for (let i = 0; i < 2; i++) {
const newBall = document.createElement('div');
newBall.className = 'ball';
newBall.style.width = ballSize + 'px';
newBall.style.height = ballSize + 'px';
newBall.style.left = balls[0].x + 'px';
newBall.style.top = balls[0].y + 'px';
gameContainer.appendChild(newBall);
// Random angle for new balls
const angle = Math.random() * Math.PI/2 - Math.PI/4;
const speed = 5;
balls.push({
x: balls[0].x,
y: balls[0].y,
speedX: Math.cos(angle) * speed,
speedY: -Math.sin(angle) * speed,
element: newBall
});
}
// Show message
showPowerupMessage("MULTI BALL!");
break;
case 'extralife':
// Add extra life
lives++;
livesDisplay.textContent = `LIVES: ${lives}`;
// Show message
showPowerupMessage("EXTRA LIFE!");
break;
case 'widepaddle':
// Double paddle width
paddle.style.width = (originalPaddleWidth * 2) + 'px';
paddleX = Math.max(0, Math.min(paddleX, gameWidth - originalPaddleWidth * 2));
paddle.style.left = paddleX + 'px';
// Set timeout to return to normal
const timer = setTimeout(() => {
paddle.style.width = originalPaddleWidth + 'px';
paddleX = Math.max(0, Math.min(paddleX, gameWidth - originalPaddleWidth));
paddle.style.left = paddleX + 'px';
// Remove from active powerups
const index = activePowerups.findIndex(p => p.type === 'widepaddle');
if (index >= 0) {
activePowerups.splice(index, 1);
}
}, 10000); // 10 seconds
activePowerups.push({ type: 'widepaddle', timer });
// Show message
showPowerupMessage("WIDE PADDLE!");
break;
case 'slowmo':
// Slow down all balls for 8 seconds
balls.forEach(b => {
b.speedX *= 0.5;
b.speedY *= 0.5;
});
const slowmoTimer = setTimeout(() => {
// Return to normal speed
balls.forEach(b => {
b.speedX *= 2;
b.speedY *= 2;
});
// Remove from active powerups
const index = activePowerups.findIndex(p => p.type === 'slowmo');
if (index >= 0) {
activePowerups.splice(index, 1);
}
}, 8000); // 8 seconds
activePowerups.push({ type: 'slowmo', timer: slowmoTimer });
// Show message
showPowerupMessage("SLOW MOTION!");
break;
case 'points':
// Add bonus points
score += 500;
scoreDisplay.textContent = `SCORE: ${score}`;
// Show message
showPowerupMessage("500 POINTS!");
break;
}
}
// Show powerup activation message
function showPowerupMessage(text) {
const message = document.createElement('div');
message.textContent = text;
message.style.position = 'absolute';
message.style.top = '20%';
message.style.left = '50%';
message.style.transform = 'translate(-50%, 0) translateZ(40px)';
message.style.fontSize = '32px';
message.style.fontWeight = '900';
message.style.color = '#00dcff';
message.style.textShadow = '0 0 10px #6b00ff, 0 0 20px rgba(107, 0, 255, 0.8)';
message.style.opacity = '1';
message.style.transition = 'all 0.5s ease-out';
message.style.zIndex = '100';
gameContainer.appendChild(message);
// Animate
setTimeout(() => {
message.style.opacity = '0';
message.style.transform = 'translate(-50%, -20px) translateZ(40px)';
setTimeout(() => {
if (gameContainer.contains(message)) {
gameContainer.removeChild(message);
}
}, 500);
}, 1000);
}
// Create powerup activation effect
function createPowerupActivationEffect(x, y, type) {
// Create a circular burst effect
const particles = 20;
const colors = {
'multiball': '#6b00ff',
'extralife': '#ff00aa',
'widepaddle': '#ffff00',
'slowmo': '#00ff00',
'points': '#ffffff'
};
for (let i = 0; i < particles; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = x + 'px';
particle.style.top = y + 'px';
particle.style.background = colors[type];
particle.style.boxShadow = `0 0 10px ${colors[type]}`;
// Random angle and speed
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 5;
const size = 4 + Math.random() * 12;
// Set random size
particle.style.width = size + 'px';
particle.style.height = size + 'px';
gameContainer.appendChild(particle);
let posX = 0;
let posY = 0;
let opacity = 1;
const animate = () => {
posX += Math.cos(angle) * speed;
posY += Math.sin(angle) * speed;
opacity -= 0.02;
particle.style.transform = `translate(${posX}px, ${posY}px)`;
particle.style.opacity = opacity;
if (opacity > 0) {
requestAnimationFrame(animate);
} else {
if (gameContainer.contains(particle)) {
gameContainer.removeChild(particle);
}
}
};
requestAnimationFrame(animate);
}
}
// Check collision between ball and brick
function checkBrickCollision(ballData) {
for (let i = 0; i < bricks.length; i++) {
const brick = bricks[i];
if (!brick || !gameContainer.contains(brick)) continue;
if (ballData.x + ballSize > parseInt(brick.style.left) &&
ballData.x < parseInt(brick.style.left) + brick.offsetWidth &&
ballData.y + ballSize > parseInt(brick.style.top) &&
ballData.y < parseInt(brick.style.top) + brick.offsetHeight) {
// Hit the brick
brick.hits--;
// Add to combo
combo++;
if (combo > 1) {
showCombo(combo);
}
// Play hit effect
createBrickHitEffect(brick);
if (brick.hits <= 0) {
// Break the brick
score += brick.scoreValue * combo; // Multiply by combo
scoreDisplay.textContent = `SCORE: ${score}`;
brickCount--;
// Check if brick had a powerup
if (brick.hasPowerup) {
createBonus(
parseInt(brick.style.left) + brick.offsetWidth / 2 - 12,
parseInt(brick.style.top) + brick.offsetHeight,
brick.powerupType
);
}
// Create explosion effect
createExplosion(
parseInt(brick.style.left) + brick.offsetWidth / 2,
parseInt(brick.style.top) + brick.offsetHeight / 2,
brick.style.backgroundColor
);
gameContainer.removeChild(brick);
bricks[i] = null;
} else {
// Just a hit, change color to show damage
brick.style.opacity = '0.6';
}
// Check if all bricks are cleared
if (brickCount <= 0) {
// Level complete!
setTimeout(() => {
createBricks();
resetBall();
gameStarted = false;
startMessage.textContent = 'LEVEL COMPLETE!';
}, 1000);
}
// Change ball direction based on where it hit the brick
const ballCenterX = ballData.x + ballSize / 2;
const ballCenterY = ballData.y + ballSize / 2;
const brickCenterX = parseInt(brick.style.left) + brick.offsetWidth / 2;
const brickCenterY = parseInt(brick.style.top) + brick.offsetHeight / 2;
// Check which side was hit
const overlapX = Math.min(
ballData.x + ballSize - parseInt(brick.style.left),
parseInt(brick.style.left) + brick.offsetWidth - ballData.x
);
const overlapY = Math.min(
ballData.y + ballSize - parseInt(brick.style.top),
parseInt(brick.style.top) + brick.offsetHeight - ballData.y
);
if (overlapX < overlapY) {
ballData.speedX = -ballData.speedX;
} else {
ballData.speedY = -ballData.speedY;
}
return true;
}
}
return false;
}
// Show combo counter
function showCombo(comboValue) {
comboCounter.textContent = `COMBO x${comboValue}!`;
comboCounter.style.opacity = '1';
comboCounter.style.transform = `translate(-50%, -50%) translateZ(30px) scale(1.2)`;
setTimeout(() => {
comboCounter.style.opacity = '0';
comboCounter.style.transform = `translate(-50%, -50%) translateZ(30px) scale(0.8)`;
}, 800);
}
// Create explosion effect when brick is destroyed
function createExplosion(x, y, color) {
const explosion = document.createElement('div');
explosion.className = 'explosion';
explosion.style.left = x + 'px';
explosion.style.top = y + 'px';
explosion.style.background = `radial-gradient(circle, ${color} 0%, transparent 70%)`;
explosion.style.width = '1px';
explosion.style.height = '1px';
explosion.style.opacity = '1';
gameContainer.appendChild(explosion);
// Animate explosion
let size = 1;
const growthRate = 40;
const fadeRate = 0.95;
const expand = () => {
size += growthRate;
const currentOpacity = parseFloat(explosion.style.opacity);
explosion.style.width = size + 'px';
explosion.style.height = size + 'px';
explosion.style.marginLeft = -size/2 + 'px';
explosion.style.marginTop = -size/2 + 'px';
explosion.style.opacity = currentOpacity * fadeRate;
if (currentOpacity > 0.1) {
requestAnimationFrame(expand);
} else {
gameContainer.removeChild(explosion);
}
};
requestAnimationFrame(expand);
// Create particles
createParticles(x, y, color);
}
// Create particles when brick is hit
function createParticles(x, y, color) {
const particleCount = 25;
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = x + 'px';
particle.style.top = y + 'px';
particle.style.background = color;
particle.style.boxShadow = `0 0 10px ${color}`;
// Random angle and speed
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 3;
const size = 2 + Math.random() * 8;
// Set random size
particle.style.width = size + 'px';
particle.style.height = size + 'px';
gameContainer.appendChild(particle);
let posX = 0;
let posY = 0;
let opacity = 1;
const animate = () => {
posX += Math.cos(angle) * speed;
posY += Math.sin(angle) * speed * 1.5; // More vertical movement
opacity -= 0.02;
// Add gravity effect
posY += 0.1;
particle.style.transform = `translate(${posX}px, ${posY}px)`;
particle.style.opacity = opacity;
if (opacity > 0) {
requestAnimationFrame(animate);
} else {
gameContainer.removeChild(particle);
}
};
requestAnimationFrame(animate);
}
}
// Create hit effect for brick
function createBrickHitEffect(brick) {
const hitEffect = document.createElement('div');
hitEffect.className = 'particle';
hitEffect.style.left = parseInt(brick.style.left) + brick.offsetWidth/2 + 'px';
hitEffect.style.top = parseInt(brick.style.top) + brick.offsetHeight/2 + 'px';
hitEffect.style.background = brick.style.backgroundColor;
hitEffect.style.boxShadow = `0 0 15px ${brick.style.backgroundColor}`;
hitEffect.style.width = '30px';
hitEffect.style.height = '30px';
hitEffect.style.opacity = '0.8';
hitEffect.style.transition = 'all 0.3s ease-out';
gameContainer.appendChild(hitEffect);
// Animate
setTimeout(() => {
hitEffect.style.width = '0';
hitEffect.style.height = '0';
hitEffect.style.opacity = '0';
setTimeout(() => {
if (gameContainer.contains(hitEffect)) {
gameContainer.removeChild(hitEffect);
}
}, 300);
}, 50);
}
// Mouse movement handler
function mouseMoveHandler(e) {
if (!gameActive) return;
const relativeX = e.clientX - gameContainer.getBoundingClientRect().left;
paddleX = Math.max(0, Math.min(relativeX - paddleWidth / 2, gameWidth - paddleWidth));
paddle.style.left = paddleX + 'px';
if (!gameStarted) {
// Main ball follows paddle before launch
ballX = paddleX + paddleWidth / 2 - ballSize / 2;
ball.style.left = ballX + 'px';
// Update main ball position in array
balls[0].x = ballX;
}
}
// Keyboard controls
function keyDownHandler(e) {
if (!gameActive) return;
if (e.key === ' ') {
if (!gameStarted) {
launchBall();
}
}
}
// Game loop
function update() {
if (!gameActive) {
requestAnimationFrame(update);
return;
}
// Move all balls if game has started
if (gameStarted) {
for (let i = balls.length - 1; i >= 0; i--) {
const ballData = balls[i];
ballData.x += ballData.speedX;
ballData.y += ballData.speedY;
// Apply ball position
ballData.element.style.left = ballData.x + 'px';
ballData.element.style.top = ballData.y + 'px';
// Wall collision (left/right)
if (ballData.x <= 0 || ballData.x + ballSize >= gameWidth) {
ballData.speedX = -ballData.speedX;
ballData.x = Math.max(0, Math.min(ballData.x, gameWidth - ballSize));
// Create wall hit effect
createWallHitEffect(ballData.x, ballData.y);
createBallPulse(ballData.x, ballData.y);
}
// Ceiling collision
if (ballData.y <= 0) {
ballData.speedY = -ballData.speedY;
ballData.y = Math.max(0, ballData.y);
// Create ceiling hit effect
createWallHitEffect(ballData.x, ballData.y);
createBallPulse(ballData.x, ballData.y);
}
// Paddle collision
if (ballData.y + ballSize >= gameHeight - paddleHeight - 40 &&
ballData.y + ballSize <= gameHeight - 40 &&
ballData.x + ballSize >= paddleX &&
ballData.x <= paddleX + paddleWidth) {
// Calculate relative position of hit on paddle (from -1 to 1)
const hitPos = ((ballData.x + ballSize/2) - (paddleX + paddleWidth/2)) / (paddleWidth/2);
// Change ball direction based on where it hits the paddle
ballData.speedX = hitPos * 5; // Max 5 pixels/frame horizontally
ballData.speedY = -Math.abs(ballData.speedY); // Ensure it goes up
// Create paddle hit effect
createPaddleHitEffect();
createBallPulse(ballData.x, ballData.y);
}
// Bottom collision (check only for main ball)
if (i === 0 && ballData.y + ballSize >= gameHeight) {
// Lose a life
lives--;
livesDisplay.textContent = `LIVES: ${lives}`;
// Reset or end game
if (lives > 0) {
resetBall();
} else {
endGame();
}
// Reset combo
combo = 0;
}
// Brick collision
if (!checkBrickCollision(ballData)) {
// No brick hit this frame - reset combo (for main ball only)
if (i === 0) {
combo = 0;
}
}
// Check for extra balls going off screen
if (i > 0 && ballData.y + ballSize >= gameHeight) {
// Remove extra ball
gameContainer.removeChild(ballData.element);
balls.splice(i, 1);
}
}
}
requestAnimationFrame(update);
}
// Create wall hit effect
function createWallHitEffect(x, y) {
const particles = 8;
for (let i = 0; i < particles; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = x + ballSize/2 + 'px';
particle.style.top = y + ballSize/2 + 'px';
particle.style.background = '#00dcff';
particle.style.boxShadow = '0 0 10px #00dcff';
// Random angle (away from wall)
const angle = x <= 0 ? Math.random() * Math.PI/2 + Math.PI/4 :
x + ballSize >= gameWidth ? Math.random() * Math.PI/2 + Math.PI*3/4 :
y <= 0 ? Math.random() * Math.PI/2 + Math.PI*3/2 :
Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 3;
const size = 2 + Math.random() * 6;
particle.style.width = size + 'px';
particle.style.height = size + 'px';
gameContainer.appendChild(particle);
let posX = 0;
let posY = 0;
let opacity = 1;
const animate = () => {
posX += Math.cos(angle) * speed;
posY += Math.sin(angle) * speed;
opacity -= 0.03;
particle.style.transform = `translate(${posX}px, ${posY}px)`;
particle.style.opacity = opacity;
if (opacity > 0) {
requestAnimationFrame(animate);
} else {
if (gameContainer.contains(particle)) {
gameContainer.removeChild(particle);
}
}
};
requestAnimationFrame(animate);
}
}
// Create paddle hit effect
function createPaddleHitEffect() {
const particles = 12;
const x = paddleX + paddleWidth/2;
const y = gameHeight - paddleHeight - 40;
for (let i = 0; i < particles; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = x + 'px';
particle.style.top = y + 'px';
particle.style.background = '#6b00ff';
particle.style.boxShadow = '0 0 10px #6b00ff';
// Angle going up
const angle = Math.random() * Math.PI/2 - Math.PI/4; // Between -45 and +45 degrees up
const speed = 1 + Math.random() * 4;
const size = 2 + Math.random() * 8;
particle.style.width = size + 'px';
particle.style.height = size + 'px';
gameContainer.appendChild(particle);
let posX = 0;
let posY = 0;
let opacity = 1;
const animate = () => {
posX += Math.cos(angle) * speed;
posY += Math.sin(angle) * speed;
opacity -= 0.01;
particle.style.transform = `translate(${posX}px, ${posY}px)`;
particle.style.opacity = opacity;
if (opacity > 0) {
requestAnimationFrame(animate);
} else {
if (gameContainer.contains(particle)) {
gameContainer.removeChild(particle);
}
}
};
requestAnimationFrame(animate);
}
}
// Start game button
startBtn.addEventListener('click', () => {
if (!gameActive) {
// Reset game state
score = 0;
lives = 3;
combo = 0;
gameStarted = false;
initGame();
} else if (!gameStarted) {
launchBall();
}
});
// Event listeners
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('keydown', keyDownHandler);
document.addEventListener('click', launchBall);
// Start the game loop
update();
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
</html>