/* ============================================================ NEON BREAKER — Breakout clone with power-ups ============================================================ */ (function () { 'use strict'; const { Game, STATE } = window.DSP; const S = window.DSP_SPRITES; const W = 800, H = 600; const COLS = 12, ROWS = 8, BW = 56, BH = 18, GAP = 4, TOP = 60; const BRICK_COLORS = ['#ff2d95', '#ff7de9', '#9d7dff', '#7df9ff', '#7dff5e', '#ffe14d', '#ff9d2d', '#ff4d6d']; class NeonBreaker extends Game { constructor(canvas) { super({ name: 'breakout', title: 'NEON BREAKER', width: W, height: H, canvas, loadSteps: ['CALIBRATING PADDLE', 'CHARGING BALL', 'STACKING BRICKS', 'SYNCING GRID'] }); this.paddle = { x: W / 2, w: 90 }; this.balls = []; this.bricks = []; this.powerups = []; this.stars = Array.from({ length: 60 }, () => ({ x: Math.random() * W, y: Math.random() * H, s: Math.random() < 0.2 ? 2 : 1 })); this.lasers = []; this.laserCd = 0; this.sticky = false; this.wideTimer = 0; this.multiTimer = 0; this.laserTimer = 0; this.levelLayout = 0; this.mouseCaptured = false; this._bindMouse(); } _bindMouse() { const canvas = this.canvas; canvas.addEventListener('mousemove', (e) => { if (this.state !== STATE.PLAYING || !this.mouseCaptured) return; this._mdx = (this._mdx || 0) + (e.movementX || 0); }); canvas.addEventListener('mousedown', (e) => { if (this.state === STATE.PLAYING) { if (!this.mouseCaptured) { if (canvas.requestPointerLock) { canvas.requestPointerLock(); } else { this.mouseCaptured = true; } return; } this._fire(); } else if (this.state === STATE.TITLE) { if (window.DSP_AUDIO) window.DSP_AUDIO.sfx('start'); this.start(); } }); document.addEventListener('pointerlockchange', () => { this.mouseCaptured = (document.pointerLockElement === canvas); }); } _fire() { for (const b of this.balls) { if (b.stuck) { b.stuck = false; const a = -Math.PI / 2 + (Math.random() - 0.5) * 0.6; b.vx = Math.cos(a) * 5; b.vy = Math.sin(a) * 5; window.DSP_AUDIO.sfx('shoot'); } } } onMouseCaptureRelease() { if (document.pointerLockElement === this.canvas) document.exitPointerLock(); else if (this.mouseCaptured) this.mouseCaptured = false; } _applyMouseDelta() { if (this.mouseCaptured && this._mdx) { this.paddle.x += this._mdx * 1.1; this._mdx = 0; this.paddle.x = Math.max(this.paddle.w / 2, Math.min(W - this.paddle.w / 2, this.paddle.x)); } } _drawCrosshair() { if (!this.mouseCaptured) return; const ctx = this.ctx; const p = this.paddle; const y = 26 + 26; ctx.strokeStyle = '#ff7de9'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(p.x - 8, y); ctx.lineTo(p.x - 2, y); ctx.moveTo(p.x + 2, y); ctx.lineTo(p.x + 8, y); ctx.moveTo(p.x, y - 8); ctx.lineTo(p.x, y - 2); ctx.moveTo(p.x, y + 2); ctx.lineTo(p.x, y + 8); ctx.stroke(); } reset() { this.powerups = []; this.lasers = []; this.sticky = false; this.wideTimer = 0; this.multiTimer = 0; this.laserTimer = 0; this.paddle.w = 90; this.buildLevel(); this.launchBall(); } buildLevel() { this.bricks = []; const layout = this.levelLayout % 3; for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { let hp = 0; if (layout === 0) hp = 1 + (r < 2 ? 1 : 0); else if (layout === 1) hp = (c + r) % 2 === 0 ? 1 : (r < 3 ? 2 : 0); else hp = (c % 3 === 0 || r % 3 === 0) ? 2 : 1; if (hp > 0) { this.bricks.push({ x: (W - (COLS * (BW + GAP))) / 2 + c * (BW + GAP), y: TOP + r * (BH + GAP), w: BW, h: BH, hp, maxHp: hp, color: BRICK_COLORS[r % BRICK_COLORS.length], }); } } } } launchBall() { this.balls = [{ x: this.paddle.x, y: H - 40, vx: 0, vy: 0, stuck: true, r: 6 }]; } update() { const p = this.paddle; const left = this.input.down('ArrowLeft', 'KeyA'); const right = this.input.down('ArrowRight', 'KeyD'); if (left || right) this.mouseActive = false; if (left) p.x -= 7; if (right) p.x += 7; this._applyMouseDelta(); p.x = Math.max(p.w / 2, Math.min(W - p.w / 2, p.x)); if (this.wideTimer > 0 && --this.wideTimer === 0) p.w = 90; if (this.multiTimer > 0) this.multiTimer--; if (this.laserTimer > 0) this.laserTimer--; if (this.laserCd > 0) this.laserCd--; for (const b of this.balls) { if (b.stuck) { b.x = p.x; b.y = H - 40; if (this.input.wasPressed('Space')) { b.stuck = false; const a = -Math.PI / 2 + (Math.random() - 0.5) * 0.6; b.vx = Math.cos(a) * 5; b.vy = Math.sin(a) * 5; window.DSP_AUDIO.sfx('shoot'); } } } if (this.laserTimer > 0 && this.input.wasPressed('Space') && this.laserCd === 0) { this.lasers.push({ x: p.x - p.w / 2 + 6, y: H - 52, vy: -9 }, { x: p.x + p.w / 2 - 6, y: H - 52, vy: -9 }); this.laserCd = 15; window.DSP_AUDIO.sfx('laser'); } for (let i = this.lasers.length - 1; i >= 0; i--) { const l = this.lasers[i]; l.y += l.vy; if (l.y < 0) { this.lasers.splice(i, 1); continue; } for (let j = this.bricks.length - 1; j >= 0; j--) { const br = this.bricks[j]; if (l.x > br.x && l.x < br.x + br.w && l.y > br.y && l.y < br.y + br.h) { this.hitBrick(j, l.x, l.y); this.lasers.splice(i, 1); break; } } } for (let i = this.balls.length - 1; i >= 0; i--) { const b = this.balls[i]; if (b.stuck) continue; b.x += b.vx; b.y += b.vy; if (b.x < b.r) { b.x = b.r; b.vx *= -1; window.DSP_AUDIO.sfx('wall'); } if (b.x > W - b.r) { b.x = W - b.r; b.vx *= -1; window.DSP_AUDIO.sfx('wall'); } if (b.y < b.r) { b.y = b.r; b.vy *= -1; window.DSP_AUDIO.sfx('wall'); } if (b.vy > 0 && b.y + b.r > H - 48 && b.y < H - 30 && Math.abs(b.x - p.x) < p.w / 2 + b.r) { const rel = (b.x - p.x) / (p.w / 2); const a = -Math.PI / 2 + rel * 1.1; const sp = Math.min(9, Math.hypot(b.vx, b.vy) + 0.15); b.vx = Math.cos(a) * sp; b.vy = Math.sin(a) * sp; b.y = H - 48 - b.r; window.DSP_AUDIO.sfx('hit'); if (this.sticky) { b.stuck = true; } } if (b.y > H + 20) { this.balls.splice(i, 1); if (this.balls.length === 0) { this.lives--; window.DSP_AUDIO.sfx('die'); if (this.lives <= 0) this.gameOver(); else this.launchBall(); } continue; } for (let j = this.bricks.length - 1; j >= 0; j--) { const br = this.bricks[j]; if (b.x + b.r < br.x || b.x - b.r > br.x + br.w || b.y + b.r < br.y || b.y - b.r > br.y + br.h) continue; const ox = Math.min(b.x + b.r - br.x, br.x + br.w - (b.x - b.r)); const oy = Math.min(b.y + b.r - br.y, br.y + br.h - (b.y - b.r)); if (ox < oy) b.vx *= -1; else b.vy *= -1; this.hitBrick(j, b.x, b.y); break; } } for (let i = this.powerups.length - 1; i >= 0; i--) { const pu = this.powerups[i]; pu.y += 2; if (pu.y > H + 20) { this.powerups.splice(i, 1); continue; } if (pu.y > H - 52 && pu.y < H - 30 && Math.abs(pu.x - p.x) < p.w / 2 + 10) { this.powerups.splice(i, 1); window.DSP_AUDIO.sfx('powerup'); this.applyPowerup(pu.type); } } if (this.bricks.length === 0) { this.level++; this.levelLayout++; window.DSP_AUDIO.sfx('levelup'); this.buildLevel(); this.launchBall(); } } applyPowerup(type) { if (type === 'wide') { this.paddle.w = 150; this.wideTimer = 600; } else if (type === 'multi') { const src = this.balls.find(b => !b.stuck) || this.balls[0]; if (src) { for (let k = -1; k <= 1; k += 2) { this.balls.push({ x: src.x, y: src.y, vx: src.vx + k * 1.5, vy: src.vy, stuck: false, r: 6 }); } } this.multiTimer = 999999; } else if (type === 'sticky') { this.sticky = true; } else if (type === 'laser') { this.laserTimer = 600; } else if (type === 'life') { this.lives = Math.min(6, this.lives + 1); } } hitBrick(j, hx, hy) { const br = this.bricks[j]; br.hp--; if (br.hp <= 0) { this.bricks.splice(j, 1); this.addScore(10 * br.maxHp + this.level * 2); window.DSP_AUDIO.sfx('brick'); this.particles.burst(hx, hy, br.color, 8, 2, 20); if (Math.random() < 0.18) { const types = ['wide', 'multi', 'sticky', 'laser', 'life']; this.powerups.push({ x: br.x + br.w / 2, y: br.y + br.h / 2, type: types[Math.floor(Math.random() * types.length)] }); } } else { window.DSP_AUDIO.sfx('hit'); } } render() { const ctx = this.ctx; ctx.fillStyle = '#05010d'; ctx.fillRect(0, 0, W, H); for (const s of this.stars) { ctx.fillStyle = s.s > 1 ? '#7df9ff' : '#3a2f5b'; ctx.fillRect(s.x, s.y, s.s, s.s); } for (const br of this.bricks) { ctx.fillStyle = br.color; ctx.globalAlpha = 0.4 + 0.6 * (br.hp / br.maxHp); ctx.fillRect(br.x, br.y, br.w, br.h); ctx.globalAlpha = 1; ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.strokeRect(br.x + 0.5, br.y + 0.5, br.w - 1, br.h - 1); } const p = this.paddle; ctx.fillStyle = this.laserTimer > 0 ? '#ffe14d' : '#7df9ff'; ctx.fillRect(p.x - p.w / 2, H - 48, p.w, 10); ctx.fillStyle = '#ffffff'; ctx.fillRect(p.x - p.w / 2, H - 48, p.w, 3); ctx.fillStyle = '#ffe14d'; for (const l of this.lasers) ctx.fillRect(l.x - 1.5, l.y - 8, 3, 10); for (const b of this.balls) { ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill(); } for (const pu of this.powerups) { const colors = { wide: '#7df9ff', multi: '#ff7de9', sticky: '#7dff5e', laser: '#ffe14d', life: '#ff2d95' }; ctx.fillStyle = colors[pu.type]; ctx.fillRect(pu.x - 8, pu.y - 8, 16, 16); ctx.fillStyle = '#05010d'; ctx.font = '10px "Press Start 2P", monospace'; ctx.textAlign = 'center'; ctx.fillText(pu.type[0].toUpperCase(), pu.x, pu.y + 3); } this._drawCrosshair(); } } window.DSP_ACTIVE_GAME = new NeonBreaker(document.getElementById('game')); window.DSP_AUDIO.playMusic('breakout'); })();