Brick Blitz
game.js10.3 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 === " " || e.key === "Enter" || e.key === "ArrowUp") { press(); e.preventDefault(); }85 });86 addEventListener("keyup", (e) => (keys[e.key] = false));87 function toLocal(e) {88 const r = c.getBoundingClientRect();89 return ((e.clientX - r.left) / r.width) * W;90 }91 c.addEventListener("pointerdown", (e) => { pointerX = toLocal(e); press(); c.focus(); });92 addEventListener("pointermove", (e) => { if (e.pointerType === "mouse" || e.buttons) pointerX = toLocal(e); });93 94 let paused = false;95 if (window.Arcadia) { Arcadia.onPause(() => (paused = true)); Arcadia.onResume(() => (paused = false)); }96 97 function update(dt) {98 // Paddle99 const speed = 620;100 if (keys.ArrowLeft || keys.a) { paddle.x -= speed * dt; pointerX = null; }101 if (keys.ArrowRight || keys.d) { paddle.x += speed * dt; pointerX = null; }102 if (pointerX !== null) paddle.x += (pointerX - paddle.x) * Math.min(1, dt * 18);103 if (paddle.wideTimer > 0) { paddle.wideTimer -= dt; paddle.w += (160 - paddle.w) * dt * 8; }104 else paddle.w += (110 - paddle.w) * dt * 8;105 paddle.x = Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, paddle.x));106 107 for (const p of particles) { p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 400 * dt; p.life -= dt; }108 particles = particles.filter((p) => p.life > 0);109 shake = Math.max(0, shake - dt * 30);110 111 if (state === "serve") { balls[0].x = paddle.x; balls[0].y = paddle.y - 14; return; }112 if (state !== "play") return;113 114 // Balls (sub-stepped so fast balls don't tunnel through bricks)115 for (const b of balls) {116 const steps = Math.ceil((Math.hypot(b.vx, b.vy) * dt) / 6);117 for (let s = 0; s < steps; s++) {118 const sdt = dt / steps;119 b.x += b.vx * sdt; b.y += b.vy * sdt;120 if (b.x < b.r) { b.x = b.r; b.vx = Math.abs(b.vx); }121 if (b.x > W - b.r) { b.x = W - b.r; b.vx = -Math.abs(b.vx); }122 if (b.y < b.r) { b.y = b.r; b.vy = Math.abs(b.vy); }123 // Paddle: angle depends on where the ball hits124 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) {125 const hit = (b.x - paddle.x) / (paddle.w / 2);126 const sp = Math.min(Math.hypot(b.vx, b.vy) * 1.01, 900);127 const a = -Math.PI / 2 + hit * 1.05;128 b.vx = Math.cos(a) * sp; b.vy = Math.sin(a) * sp;129 b.y = paddle.y - b.r;130 }131 for (const br of bricks) {132 if (br.hp <= 0) continue;133 const nx = Math.max(br.x, Math.min(b.x, br.x + br.w));134 const ny = Math.max(br.y, Math.min(b.y, br.y + br.h));135 const dx = b.x - nx, dy = b.y - ny;136 if (dx * dx + dy * dy <= b.r * b.r) {137 if (Math.abs(dx) > Math.abs(dy)) b.vx = Math.sign(dx || -b.vx) * Math.abs(b.vx);138 else b.vy = Math.sign(dy || -b.vy) * Math.abs(b.vy);139 br.hp--;140 score += 10 * level;141 if (br.hp <= 0) {142 burst(br.x + br.w / 2, br.y + br.h / 2, br.color, 14);143 shake = 4;144 if (Math.random() < 0.12) drops.push({ x: br.x + br.w / 2, y: br.y, kind: Math.random() < 0.5 ? "wide" : "multi" });145 }146 break;147 }148 }149 }150 }151 balls = balls.filter((b) => b.y < H + 20);152 if (!balls.length) {153 lives--;154 shake = 10;155 if (lives <= 0) return gameOver();156 serve();157 }158 159 for (const d of drops) {160 d.y += 180 * dt;161 if (d.y > paddle.y - 8 && d.y < paddle.y + paddle.h && Math.abs(d.x - paddle.x) < paddle.w / 2 + 12) {162 d.taken = true;163 score += 50;164 if (d.kind === "wide") paddle.wideTimer = 12;165 else {166 const extra = [];167 for (const b of balls.slice(0, 3)) {168 for (const turn of [-0.4, 0.4]) {169 const sp = Math.hypot(b.vx, b.vy), a = Math.atan2(b.vy, b.vx) + turn;170 extra.push({ ...b, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp });171 }172 }173 balls.push(...extra);174 }175 }176 }177 drops = drops.filter((d) => !d.taken && d.y < H + 20);178 179 if (bricks.every((b) => b.hp <= 0)) {180 score += 500 * level;181 level++;182 if (window.Arcadia) Arcadia.submitScore(score);183 buildLevel();184 }185 }186 187 function gameOver() {188 state = "over";189 if (score > best()) localStorage.setItem("brickblitz.best", String(score));190 if (window.Arcadia) { Arcadia.submitScore(score); Arcadia.gameOver(); }191 }192 193 function roundRect(x, y, w, h, r) {194 ctx.beginPath();195 ctx.roundRect ? ctx.roundRect(x, y, w, h, r) : ctx.rect(x, y, w, h);196 }197 198 function draw() {199 ctx.save();200 if (shake) ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);201 const g = ctx.createLinearGradient(0, 0, 0, H);202 g.addColorStop(0, "#1b1340"); g.addColorStop(1, "#0d0a1f");203 ctx.fillStyle = g; ctx.fillRect(-20, -20, W + 40, H + 40);204 205 for (const b of bricks) {206 if (b.hp <= 0) continue;207 ctx.fillStyle = b.color;208 ctx.globalAlpha = b.hp > 1 ? 1 : 0.85;209 roundRect(b.x, b.y, b.w, b.h, 5); ctx.fill();210 if (b.hp > 1) { ctx.strokeStyle = "#fff"; ctx.lineWidth = 2; ctx.stroke(); }211 ctx.globalAlpha = 0.25; ctx.fillStyle = "#fff"; roundRect(b.x + 3, b.y + 3, b.w - 6, 5, 3); ctx.fill();212 ctx.globalAlpha = 1;213 }214 for (const d of drops) {215 ctx.fillStyle = d.kind === "wide" ? "#3ddc97" : "#ffd23f";216 roundRect(d.x - 14, d.y - 9, 28, 18, 9); ctx.fill();217 ctx.fillStyle = "#111"; ctx.font = "bold 12px system-ui"; ctx.textAlign = "center"; ctx.textBaseline = "middle";218 ctx.fillText(d.kind === "wide" ? "W" : "×3", d.x, d.y + 1);219 }220 ctx.shadowColor = "#4cc9f0"; ctx.shadowBlur = 16;221 ctx.fillStyle = "#4cc9f0"; roundRect(paddle.x - paddle.w / 2, paddle.y, paddle.w, paddle.h, 7); ctx.fill();222 ctx.shadowColor = "#fff";223 ctx.fillStyle = "#fff";224 for (const b of balls) { ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2); ctx.fill(); }225 ctx.shadowBlur = 0;226 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); }227 ctx.globalAlpha = 1;228 229 ctx.fillStyle = "#fff"; ctx.font = "bold 18px system-ui"; ctx.textBaseline = "top";230 ctx.textAlign = "left"; ctx.fillText(`SCORE ${score ?? 0}`, 16, 16);231 ctx.textAlign = "center"; ctx.fillText(`LEVEL ${level ?? 1}`, W / 2, 16);232 ctx.textAlign = "right"; ctx.fillText("♥".repeat(Math.max(0, lives ?? 3)), W - 16, 16);233 ctx.restore();234 235 const center = (title, sub, sub2) => {236 ctx.fillStyle = "rgba(8,6,20,.72)"; ctx.fillRect(0, 0, W, H);237 ctx.fillStyle = "#fff"; ctx.textAlign = "center"; ctx.textBaseline = "middle";238 ctx.font = "900 64px system-ui"; ctx.fillText(title, W / 2, H / 2 - 50);239 ctx.font = "600 22px system-ui"; ctx.fillStyle = "#c9c3ff"; ctx.fillText(sub, W / 2, H / 2 + 20);240 if (sub2) { ctx.font = "16px system-ui"; ctx.fillStyle = "#8f88c9"; ctx.fillText(sub2, W / 2, H / 2 + 56); }241 };242 if (state === "title") center("BRICK BLITZ", "Tap or press Space to start", `Move: mouse, touch or ← → · Best: ${best()}`);243 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); }244 if (state === "over") center("GAME OVER", `Score ${score} · Best ${best()}`, "Tap or press Space to play again");245 }246 247 if (!paddle) { reset(); state = "title"; }248 let last = performance.now();249 function loop(now) {250 const dt = Math.min(0.033, (now - last) / 1000);251 last = now;252 if (!paused) update(dt);253 draw();254 requestAnimationFrame(loop);255 }256 requestAnimationFrame(loop);257})();258