diff --git a/public/js/games/breakout.js b/public/js/games/breakout.js new file mode 100644 index 0000000..dcd7473 --- /dev/null +++ b/public/js/games/breakout.js @@ -0,0 +1,301 @@ +/* ============================================================ + 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'); +})(); \ No newline at end of file