This commit is contained in:
gpt-engineer-app[bot]
2026-01-22 23:38:57 +00:00
parent 224b0a5450
commit a9548741ce
7 changed files with 252 additions and 42 deletions
+129 -2
View File
@@ -24,8 +24,32 @@ interface Brick {
height: number;
alive: boolean;
color: string;
hasPowerUp?: PowerUpType;
}
type PowerUpType = 'wide' | 'multi' | 'slow' | 'life';
interface PowerUp {
x: number;
y: number;
type: PowerUpType;
active: boolean;
}
const POWER_UP_COLORS: Record<PowerUpType, string> = {
wide: 'hsl(200 80% 60%)',
multi: 'hsl(280 80% 60%)',
slow: 'hsl(45 80% 60%)',
life: 'hsl(0 80% 60%)',
};
const POWER_UP_LABELS: Record<PowerUpType, string> = {
wide: 'W',
multi: 'M',
slow: 'S',
life: '+',
};
const Breakout = () => {
const { playSound } = useSettings();
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
@@ -40,6 +64,12 @@ const Breakout = () => {
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
const [isTwoPlayer, setIsTwoPlayer] = useState(false);
// Power-up state
const [activePowerUps, setActivePowerUps] = useState<{ type: PowerUpType; endTime: number }[]>([]);
const powerUpsRef = useRef<PowerUp[]>([]);
const extraBallsRef = useRef<{ x: number; y: number; dx: number; dy: number }[]>([]);
const basePaddleWidth = useRef(90);
// P2 state
const [score2, setScore2] = useState(0);
const [lives2, setLives2] = useState(3);
@@ -91,7 +121,11 @@ const Breakout = () => {
useEffect(() => { setCanvasSize(getCanvasSize()); }, [isFullscreen, getCanvasSize, isTwoPlayer]);
const paddleWidth = isMobile ? 70 : (isTwoPlayer ? 70 : (isFullscreen ? 100 : 90));
// Calculate paddle width based on active power-ups
const hasWidePaddle = activePowerUps.some(p => p.type === 'wide' && p.endTime > Date.now());
const hasSlowBall = activePowerUps.some(p => p.type === 'slow' && p.endTime > Date.now());
basePaddleWidth.current = isMobile ? 70 : (isTwoPlayer ? 70 : (isFullscreen ? 100 : 90));
const paddleWidth = hasWidePaddle ? basePaddleWidth.current * 1.5 : basePaddleWidth.current;
const ballSize = isMobile ? 10 : (isTwoPlayer ? 10 : (isFullscreen ? 14 : 12));
useEffect(() => {
@@ -126,9 +160,23 @@ const Breakout = () => {
const bricks: Brick[] = [];
const brickWidth = (width - (BRICK_COLS + 1) * BRICK_GAP) / BRICK_COLS;
const colors = ['hsl(0 70% 50%)', 'hsl(30 70% 50%)', 'hsl(60 70% 50%)', 'hsl(120 70% 50%)', 'hsl(200 70% 50%)', 'hsl(280 70% 50%)'];
const powerUpTypes: PowerUpType[] = ['wide', 'multi', 'slow', 'life'];
for (let row = 0; row < BRICK_ROWS; row++) {
for (let col = 0; col < BRICK_COLS; col++) {
bricks.push({ x: BRICK_GAP + col * (brickWidth + BRICK_GAP), y: 60 + row * (BRICK_HEIGHT + BRICK_GAP), width: brickWidth, height: BRICK_HEIGHT, alive: true, color: colors[row % colors.length] });
// ~15% chance of having a power-up
const hasPowerUp = Math.random() < 0.15;
const powerUpType = hasPowerUp ? powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)] : undefined;
bricks.push({
x: BRICK_GAP + col * (brickWidth + BRICK_GAP),
y: 60 + row * (BRICK_HEIGHT + BRICK_GAP),
width: brickWidth,
height: BRICK_HEIGHT,
alive: true,
color: colors[row % colors.length],
hasPowerUp: powerUpType
});
}
}
return bricks;
@@ -143,6 +191,9 @@ const Breakout = () => {
const startGame = (twoPlayer: boolean) => {
setIsTwoPlayer(twoPlayer);
setScore(0); setLevel(1); setLives(3); setGameOver(false); setIsPaused(false); setGameStarted(true);
setActivePowerUps([]);
powerUpsRef.current = [];
extraBallsRef.current = [];
p1DeadRef.current = false;
p2DeadRef.current = false;
@@ -278,6 +329,17 @@ const Breakout = () => {
allDestroyed = false;
if (ball.x + ballSize / 2 >= brick.x && ball.x - ballSize / 2 <= brick.x + brick.width && ball.y + ballSize / 2 >= brick.y && ball.y - ballSize / 2 <= brick.y + brick.height) {
brick.alive = false; ball.dy = -ball.dy;
// Spawn power-up if brick had one (P1 only for simplicity)
if (brick.hasPowerUp && playerNum === 1) {
powerUpsRef.current.push({
x: brick.x + brick.width / 2,
y: brick.y + brick.height,
type: brick.hasPowerUp,
active: true
});
}
setScoreParam(prev => {
const ns = Math.min(prev + 10, MAX_SCORE);
if (ns >= MAX_SCORE && playerNum === 1) setShowGlitchCrash(true);
@@ -287,6 +349,52 @@ const Breakout = () => {
playSound('success'); break;
}
}
// Update power-ups (P1 only)
if (playerNum === 1) {
const paddleY = canvasSize.height - 30;
for (const pu of powerUpsRef.current) {
if (!pu.active) continue;
pu.y += 2; // Fall speed
// Check paddle collision
if (pu.y >= paddleY && pu.y <= paddleY + PADDLE_HEIGHT && pu.x >= paddleRefParam.current && pu.x <= paddleRefParam.current + paddleWidth) {
pu.active = false;
playSound('success');
if (pu.type === 'life') {
setLivesParam(prev => Math.min(prev + 1, 5));
} else if (pu.type === 'multi') {
// Add extra ball
const newBall = { x: ball.x, y: ball.y, dx: -ball.dx, dy: ball.dy };
extraBallsRef.current.push(newBall);
} else {
// Wide or Slow - timed power-up (8 seconds)
setActivePowerUps(prev => [...prev.filter(p => p.type !== pu.type), { type: pu.type, endTime: Date.now() + 8000 }]);
}
}
// Remove if off screen
if (pu.y > canvasSize.height) pu.active = false;
}
powerUpsRef.current = powerUpsRef.current.filter(pu => pu.active);
// Update extra balls
for (const eb of extraBallsRef.current) {
const speedMod = hasSlowBall ? 0.6 : 1;
eb.x += eb.dx * speedMod; eb.y += eb.dy * speedMod;
if (eb.x <= ballSize / 2 || eb.x >= canvasSize.width - ballSize / 2) eb.dx = -eb.dx;
if (eb.y <= ballSize / 2) eb.dy = -eb.dy;
if (eb.y + ballSize / 2 >= paddleY && eb.y - ballSize / 2 <= paddleY + PADDLE_HEIGHT && eb.x >= paddleRefParam.current && eb.x <= paddleRefParam.current + paddleWidth) {
const hitPos = (eb.x - paddleRefParam.current) / paddleWidth;
const angle = (hitPos - 0.5) * Math.PI * 0.7;
const speed = Math.sqrt(eb.dx * eb.dx + eb.dy * eb.dy);
eb.dx = Math.sin(angle) * speed; eb.dy = -Math.abs(Math.cos(angle) * speed);
eb.y = paddleY - ballSize / 2;
}
}
extraBallsRef.current = extraBallsRef.current.filter(eb => eb.y < canvasSize.height);
}
if (allDestroyed && bricksRefParam.current.length > 0 && !deadRef.current) nextLevel(playerNum);
@@ -296,6 +404,25 @@ const Breakout = () => {
if (!brick.alive) continue;
ctx.fillStyle = brick.color; ctx.fillRect(brick.x, brick.y, brick.width, brick.height);
ctx.strokeStyle = primaryColor; ctx.globalAlpha = 0.5; ctx.strokeRect(brick.x, brick.y, brick.width, brick.height); ctx.globalAlpha = 1;
// Draw power-up indicator
if (brick.hasPowerUp) {
ctx.fillStyle = POWER_UP_COLORS[brick.hasPowerUp];
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText(POWER_UP_LABELS[brick.hasPowerUp], brick.x + brick.width / 2, brick.y + brick.height / 2 + 3);
}
}
// Draw falling power-ups
if (playerNum === 1) {
for (const pu of powerUpsRef.current) {
if (!pu.active) continue;
ctx.fillStyle = POWER_UP_COLORS[pu.type];
ctx.shadowColor = POWER_UP_COLORS[pu.type]; ctx.shadowBlur = 8;
ctx.beginPath(); ctx.arc(pu.x, pu.y, 8, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center';
ctx.fillText(POWER_UP_LABELS[pu.type], pu.x, pu.y + 4); ctx.shadowBlur = 0;
}
}
ctx.fillStyle = primaryColor; ctx.shadowColor = primaryColor; ctx.shadowBlur = 10;