Files

301 lines
11 KiB
JavaScript

/* ============================================================
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');
})();