Add Neon Breaker game (breakout with powerups + pointer-lock mouse capture)
This commit is contained in:
@@ -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');
|
||||
})();
|
||||
Reference in New Issue
Block a user