game.js10.4 KB
1// Brick Blitz — an Arcadia seed game. Fork it and make it better!2(() => {3 const W = 800, H = 600;4 const c = document.getElementById("c");5 const ctx = c.getContext("2d");6 let scale = 1;7 function fit() {8 scale = Math.min(innerWidth / W, innerHeight / H);9 const dpr = window.devicePixelRatio || 1;10 c.style.width = W * scale + "px";11 c.style.height = H * scale + "px";12 c.width = Math.round(W * scale * dpr);13 c.height = Math.round(H * scale * dpr);14 ctx.setTransform(scale * dpr, 0, 0, scale * dpr, 0, 0);15 }16 addEventListener("resize", fit);17 fit();18 19 const COLORS = ["#ff4d6d", "#ff9f1c", "#ffd23f", "#3ddc97", "#4cc9f0", "#9d4edd"];20 const best = () => Number(localStorage.getItem("brickblitz.best") || 0);21 22 let state = "title"; // title | play | serve | over | won23 let paddle, balls, bricks, drops, particles, score, lives, level, keys = {}, pointerX = null, shake = 0;24 25 function reset() {26 score = 0; lives = 3; level = 1;27 buildLevel();28 }29 30 function buildLevel() {31 paddle = { x: W / 2, y: H - 40, w: 110, h: 14, wideTimer: 0 };32 balls = [];33 drops = [];34 particles = [];35 bricks = [];36 const rows = Math.min(4 + level, 9);37 const cols = 11;38 const bw = 64, bh = 22, gap = 6;39 const ox = (W - (cols * bw + (cols - 1) * gap)) / 2;40 for (let r = 0; r < rows; r++) {41 for (let col = 0; col < cols; col++) {42 // Patterns get more interesting each level.43 if (level % 3 === 2 && (r + col) % 4 === 0) continue;44 if (level % 3 === 0 && Math.abs(col - 5) + r < 2) continue;45 const hp = r < level - 1 && r < 3 ? 2 : 1;46 bricks.push({ x: ox + col * (bw + gap), y: 70 + r * (bh + gap), w: bw, h: bh, hp, color: COLORS[r % COLORS.length] });47 }48 }49 serve();50 }51 52 function serve() {53 state = "serve";54 balls = [{ x: paddle.x, y: paddle.y - 12, vx: 0, vy: 0, r: 8, stuck: true }];55 }56 57 function launch() {58 for (const b of balls) {59 if (b.stuck) {60 const speed = 380 + level * 25;61 const a = -Math.PI / 2 + (Math.random() - 0.5) * 0.6;62 b.vx = Math.cos(a) * speed;63 b.vy = Math.sin(a) * speed;64 b.stuck = false;65 }66 }67 state = "play";68 }69 70 function burst(x, y, color, n = 10) {71 for (let i = 0; i < n; i++) {72 const a = Math.random() * Math.PI * 2, s = 60 + Math.random() * 220;73 particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life: 0.6 + Math.random() * 0.4, color });74 }75 }76 77 function press() {78 if (state === "title" || state === "over" || state === "won") { reset(); return; }79 if (state === "serve") launch();80 }81 82 addEventListener("keydown", (e) => {83 keys[e.key] = true;84 if (e.key === "p" || e.key === "Escape") { paused = !paused; return; }85 if (e.key === " " || e.key === "Enter" || e.key === "ArrowUp") { press(); e.preventDefault(); }86 });87 addEventListener("keyup", (e) => (keys[e.key] = false));88 function toLocal(e) {89 const r = c.getBoundingClientRect();90 return ((e.clientX - r.left) / r.width) * W;91 }92 c.addEventListener("pointerdown", (e) => { pointerX = toLocal(e); press(); c.focus(); });93 addEventListener("pointermove", (e) => { if (e.pointerType === "mouse" || e.buttons) pointerX = toLocal(e); });94 95 let paused = false;96 if (window.Arcadia) { Arcadia.onPause(() => (paused = true)); Arcadia.onResume(() => (paused = false)); }97 98 function update(dt) {99 // Paddle100 const speed = 620;101 if (keys.ArrowLeft || keys.a) { paddle.x -= speed * dt; pointerX = null; }102 if (keys.ArrowRight || keys.d) { paddle.x += speed * dt; pointerX = null; }103 if (pointerX !== null) paddle.x += (pointerX - paddle.x) * Math.min(1, dt * 18);104 if (paddle.wideTimer > 0) { paddle.wideTimer -= dt; paddle.w += (160 - paddle.w) * dt * 8; }105 else paddle.w += (110 - paddle.w) * dt * 8;106 paddle.x = Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, paddle.x));107 108 for (const p of particles) { p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 400 * dt; p.life -= dt; }109 particles = particles.filter((p) => p.life > 0);110 shake = Math.max(0, shake - dt * 30);111 112 if (state === "serve") { balls[0].x = paddle.x; balls[0].y = paddle.y - 14; return; }113 if (state !== "play") return;114 115 // Balls (sub-stepped so fast balls don't tunnel through bricks)116 for (const b of balls) {117 const steps = Math.ceil((Math.hypot(b.vx, b.vy) * dt) / 6);118 for (let s = 0; s < steps; s++) {119 const sdt = dt / steps;120 b.x += b.vx * sdt; b.y += b.vy * sdt;121 if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx); }122 if (b.x > W - b.r) { b.x = W - b.r; b.vx = -Math.abs(b.vx); }123 if (b.y < b.r) { b.y = b.r; b.vy = Math.abs(b.vy); }124 // Paddle: angle depends on where the ball hits125 if (b.vy > 0 && b.y + b.r >= paddle.y && b.y + b.r <= paddle.y + paddle.h + 10 && Math.abs(b.x - paddle.x) <= paddle.w / 2 + b.r) {126 const hit = (b.x - paddle.x) / (paddle.w / 2);127 const sp = Math.min(Math.hypot(b.vx, b.vy) * 1.01, 900);128 const a = -Math.PI / 2 + hit * 1.05;129 b.vx = Math.cos(a) * sp; b.vy = Math.sin(a) * sp;130 b.y = paddle.y - b.r;131 }132 for (const br of bricks) {133 if (br.hp <= 0) continue;134 const nx = Math.max(br.x, Math.min(b.x, br.x + br.w));135 const ny = Math.max(br.y, Math.min(b.y, br.y + br.h));136 const dx = b.x - nx, dy = b.y - ny;137 if (dx * dx + dy * dy <= b.r * b.r) {138 if (Math.abs(dx) > Math.abs(dy)) b.vx = Math.sign(dx || -b.vx) * Math.abs(b.vx);139 else b.vy = Math.sign(dy || -b.vy) * Math.abs(b.vy);140 br.hp--;141 score += 10 * level;142 if (br.hp <= 0) {143 burst(br.x + br.w / 2, br.y + br.h / 2, br.color, 14);144 shake = 4;145 if (Math.random() < 0.12) drops.push({ x: br.x + br.w / 2, y: br.y, kind: Math.random() < 0.5 ? "wide" : "multi" });146 }147 break;148 }149 }150 }151 }152 balls = balls.filter((b) => b.y < H + 20);153 if (!balls.length) {154 lives--;155 shake = 10;156 if (lives <= 0) return gameOver();157 serve();158 }159 160 for (const d of drops) {161 d.y += 180 * dt;162 if (d.y > paddle.y - 8 && d.y < paddle.y + paddle.h && Math.abs(d.x - paddle.x) < paddle.w / 2 + 12) {163 d.taken = true;164 score += 50;165 if (d.kind === "wide") paddle.wideTimer = 12;166 else {167 const extra = [];168 for (const b of balls.slice(0, 3)) {169 for (const turn of [-0.4, 0.4]) {170 const sp = Math.hypot(b.vx, b.vy), a = Math.atan2(b.vy, b.vx) + turn;171 extra.push({ ...b, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp });172 }173 }174 balls.push(...extra);175 }176 }177 }178 drops = drops.filter((d) => !d.taken && d.y < H + 20);179 180 if (bricks.every((b) => b.hp <= 0)) {181 score += 500 * level;182 level++;183 if (window.Arcadia) Arcadia.submitScore(score);184 buildLevel();185 }186 }187 188 function gameOver() {189 state = "over";190 if (score > best()) localStorage.setItem("brickblitz.best", String(score));191 if (window.Arcadia) { Arcadia.submitScore(score); Arcadia.gameOver(); }192 }193 194 function roundRect(x, y, w, h, r) {195 ctx.beginPath();196 ctx.roundRect ? ctx.roundRect(x, y, w, h, r) : ctx.rect(x, y, w, h);197 }198 199 function draw() {200 ctx.save();201 if (shake) ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);202 const g = ctx.createLinearGradient(0, 0, 0, H);203 g.addColorStop(0, "#1b1340"); g.addColorStop(1, "#0d0a1f");204 ctx.fillStyle = g; ctx.fillRect(-20, -20, W + 40, H + 40);205 206 for (const b of bricks) {207 if (b.hp <= 0) continue;208 ctx.fillStyle = b.color;209 ctx.globalAlpha = b.hp > 1 ? 1 : 0.85;210 roundRect(b.x, b.y, b.w, b.h, 5); ctx.fill();211 if (b.hp > 1) { ctx.strokeStyle = "#fff"; ctx.lineWidth = 2; ctx.stroke(); }212 ctx.globalAlpha = 0.25; ctx.fillStyle = "#fff"; roundRect(b.x + 3, b.y + 3, b.w - 6, 5, 3); ctx.fill();213 ctx.globalAlpha = 1;214 }215 for (const d of drops) {216 ctx.fillStyle = d.kind === "wide" ? "#3ddc97" : "#ffd23f";217 roundRect(d.x - 14, d.y - 9, 28, 18, 9); ctx.fill();218 ctx.fillStyle = "#111"; ctx.font = "bold 12px system-ui"; ctx.textAlign = "center"; ctx.textBaseline = "middle";219 ctx.fillText(d.kind === "wide" ? "W" : "×3", d.x, d.y + 1);220 }221 ctx.shadowColor = "#4cc9f0"; ctx.shadowBlur = 16;222 ctx.fillStyle = "#4cc9f0"; roundRect(paddle.x - paddle.w / 2, paddle.y, paddle.w, paddle.h, 7); ctx.fill();223 ctx.shadowColor = "#fff";224 ctx.fillStyle = "#fff";225 for (const b of balls) { ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill(); }226 ctx.shadowBlur = 0;227 for (const p of particles) { ctx.globalAlpha = Math.max(0, p.life); ctx.fillStyle = p.color; ctx.fillRect(p.x - 2, p.y - 2, 4, 4); }228 ctx.globalAlpha = 1;229 230 ctx.fillStyle = "#fff"; ctx.font = "bold 18px system-ui"; ctx.textBaseline = "top";231 ctx.textAlign = "left"; ctx.fillText(`SCORE ${score ?? 0}`, 16, 16);232 ctx.textAlign = "center"; ctx.fillText(`LEVEL ${level ?? 1}`, W / 2, 16);233 ctx.textAlign = "right"; ctx.fillText("♥".repeat(Math.max(0, lives ?? 3)), W - 16, 16);234 ctx.restore();235 236 const center = (title, sub, sub2) => {237 ctx.fillStyle = "rgba(8,6,20,.72)"; ctx.fillRect(0, 0, W, H);238 ctx.fillStyle = "#fff"; ctx.textAlign = "center"; ctx.textBaseline = "middle";239 ctx.font = "900 64px system-ui"; ctx.fillText(title, W / 2, H / 2 - 50);240 ctx.font = "600 22px system-ui"; ctx.fillStyle = "#c9c3ff"; ctx.fillText(sub, W / 2, H / 2 + 20);241 if (sub2) { ctx.font = "16px system-ui"; ctx.fillStyle = "#8f88c9"; ctx.fillText(sub2, W / 2, H / 2 + 56); }242 };243 if (state === "title") center("BRICK BLITZ", "Tap or press Space to start", `Move: mouse, touch or ← → · Best: ${best()}`);244 if (state === "serve") { ctx.fillStyle = "#c9c3ff"; ctx.font = "600 18px system-ui"; ctx.textAlign = "center"; ctx.fillText("Tap / Space to launch", W / 2, H - 90); }245 if (paused && state !== "title") center("PAUSED", "Press P to resume");246 if (state === "over") center("GAME OVER", `Score ${score} · Best ${best()}`, "Tap or press Space to play again");247 }248 249 if (!paddle) { reset(); state = "title"; }250 let last = performance.now();251 function loop(now) {252 const dt = Math.min(0.033, (now - last) / 1000);253 last = now;254 if (!paused) update(dt);255 draw();256 requestAnimationFrame(loop);257 }258 requestAnimationFrame(loop);259})();260