← back to Games Agentabrams

games/breakout/index.html

241 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Abrams Break</title>
<style>
  :root { --bg:#0a0e14; --ink:#e5e7eb; }
  * { box-sizing:border-box; margin:0; padding:0; }
  html,body { height:100%; }
  body {
    background:radial-gradient(1200px 600px at 50% -10%, #16202e, var(--bg));
    color:var(--ink); font:15px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    min-height:100%; gap:10px; padding:12px; overscroll-behavior:none; touch-action:none;
  }
  h1 { font-size:14px; letter-spacing:.28em; text-transform:uppercase; color:#93c5fd; font-weight:600; }
  .hud { display:flex; gap:18px; font-variant-numeric:tabular-nums; font-size:13px; color:#9aa4b2; }
  .hud b { color:var(--ink); font-weight:600; }
  #wrap { position:relative; }
  canvas { background:#0d131c; border-radius:12px; box-shadow:0 10px 40px rgba(0,0,0,.5); display:block; }
  .overlay {
    position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:10px; text-align:center; background:rgba(8,12,18,.72); border-radius:12px; backdrop-filter:blur(2px);
  }
  .overlay.hide { display:none; }
  .big { font-size:22px; font-weight:700; }
  .sub { font-size:13px; color:#9aa4b2; max-width:300px; }
  .tag { font-size:11px; color:#93c5fd; letter-spacing:.2em; text-transform:uppercase; }
  button.play { margin-top:4px; padding:10px 22px; border:0; border-radius:999px; cursor:pointer; background:linear-gradient(180deg,#60a5fa,#3b82f6); color:#fff; font-weight:700; font-size:14px; }
  .btns { position:absolute; top:8px; right:10px; display:flex; gap:6px; }
  .btns button { width:30px; height:30px; border-radius:8px; border:1px solid #2a3441; background:#141b25; color:#cbd5e1; cursor:pointer; font-size:14px; }
  .hint { font-size:11px; color:#5b6673; }
</style>
</head>
<body>
  <h1>Abrams Break</h1>
  <div class="hud"><span>Score <b id="score">0</b></span><span>Lives <b id="lives">3</b></span><span>Level <b id="lvl">1</b></span><span>Best <b id="best">0</b></span></div>
  <div id="wrap">
    <canvas id="c" width="440" height="480"></canvas>
    <div class="btns">
      <button id="mute" title="Mute (M)">🔊</button>
      <button id="pauseBtn" title="Pause (P)">⏸</button>
    </div>
    <div class="overlay" id="ov">
      <div class="tag" id="ovTag">Ready</div>
      <div class="big" id="ovTitle">Abrams Break</div>
      <div class="sub" id="ovSub">Clear every brick. Move the paddle with mouse / arrows, or drag on touch. Don't drop the ball.</div>
      <button class="play" id="playBtn">Play</button>
      <div class="hint">P pause · M mute · Space launch</div>
    </div>
  </div>

<script>
(function () {
  var cv = document.getElementById('c'), ctx = cv.getContext('2d');
  var W = cv.width, H = cv.height;
  var scoreEl = document.getElementById('score'), livesEl = document.getElementById('lives'),
      lvlEl = document.getElementById('lvl'), bestEl = document.getElementById('best');
  var ov = document.getElementById('ov'), ovTag = document.getElementById('ovTag'), ovTitle = document.getElementById('ovTitle'), ovSub = document.getElementById('ovSub');
  var playBtn = document.getElementById('playBtn'), pauseBtn = document.getElementById('pauseBtn'), muteBtn = document.getElementById('mute');

  var BEST_KEY = 'abrams-break-best';
  var best = parseInt(localStorage.getItem(BEST_KEY) || '0', 10) || 0; bestEl.textContent = best;

  var COLS = 10, PAD = 6, TOP = 46, BRICK_H = 20;
  var brickW = (W - PAD * (COLS + 1)) / COLS;
  var COLORS = ['#f472b6', '#fb923c', '#facc15', '#4ade80', '#38bdf8', '#a78bfa'];

  var paddle, ball, bricks, score, lives, level, running, paused, launched, muted = false;
  var last = 0;

  var actx = null;
  function beep(freq, dur, type, vol) {
    if (muted) return;
    try {
      if (!actx) actx = new (window.AudioContext || window.webkitAudioContext)();
      var o = actx.createOscillator(), g = actx.createGain();
      o.type = type || 'square'; o.frequency.value = freq; g.gain.value = vol || 0.05;
      o.connect(g); g.connect(actx.destination);
      var t = actx.currentTime; o.start(t);
      g.gain.exponentialRampToValueAtTime(0.0001, t + (dur || 0.07)); o.stop(t + (dur || 0.07));
    } catch (e) {}
  }

  function buildBricks(lv) {
    bricks = [];
    var rows = Math.min(4 + lv, 7);
    for (var r = 0; r < rows; r++) for (var c = 0; c < COLS; c++) {
      bricks.push({
        x: PAD + c * (brickW + PAD), y: TOP + r * (BRICK_H + PAD),
        w: brickW, h: BRICK_H, color: COLORS[r % COLORS.length],
        hits: r < 1 ? 2 : 1                 // top row tougher
      });
    }
  }
  function resetBallPaddle() {
    paddle = { w: 84, h: 12, x: W / 2 - 42, y: H - 28, speed: 7 };
    var sp = 4.2 + level * 0.4;
    ball = { x: W / 2, y: paddle.y - 8, r: 7, vx: sp * (Math.random() < 0.5 ? -1 : 1) * 0.7, vy: -sp };
    launched = false;
  }
  function newGame() {
    score = 0; lives = 3; level = 1; buildBricks(level); resetBallPaddle(); updateHud();
  }
  function updateHud() { scoreEl.textContent = score; livesEl.textContent = lives; lvlEl.textContent = level; }

  function launch() { if (!launched) { launched = true; beep(520, 0.06, 'square', 0.05); } }

  function tick() {
    if (!launched) { ball.x = paddle.x + paddle.w / 2; ball.y = paddle.y - ball.r - 1; return; }
    ball.x += ball.vx; ball.y += ball.vy;
    // walls
    if (ball.x - ball.r < 0) { ball.x = ball.r; ball.vx *= -1; beep(300, 0.04, 'square', 0.04); }
    if (ball.x + ball.r > W) { ball.x = W - ball.r; ball.vx *= -1; beep(300, 0.04, 'square', 0.04); }
    if (ball.y - ball.r < 0) { ball.y = ball.r; ball.vy *= -1; beep(300, 0.04, 'square', 0.04); }
    // paddle
    if (ball.vy > 0 && ball.y + ball.r >= paddle.y && ball.y + ball.r <= paddle.y + paddle.h + 8 &&
        ball.x >= paddle.x - ball.r && ball.x <= paddle.x + paddle.w + ball.r) {
      var hit = (ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2); // -1..1
      var speed = Math.min(9, Math.hypot(ball.vx, ball.vy) + 0.06);
      var ang = hit * 1.05;                 // steer by contact point
      ball.vx = speed * Math.sin(ang); ball.vy = -Math.abs(speed * Math.cos(ang));
      ball.y = paddle.y - ball.r - 1; beep(440, 0.05, 'square', 0.05);
    }
    // bricks
    for (var i = 0; i < bricks.length; i++) {
      var b = bricks[i];
      if (ball.x + ball.r > b.x && ball.x - ball.r < b.x + b.w && ball.y + ball.r > b.y && ball.y - ball.r < b.y + b.h) {
        // decide bounce axis by shallowest overlap
        var overL = ball.x + ball.r - b.x, overR = b.x + b.w - (ball.x - ball.r);
        var overT = ball.y + ball.r - b.y, overB = b.y + b.h - (ball.y - ball.r);
        var minX = Math.min(overL, overR), minY = Math.min(overT, overB);
        if (minX < minY) ball.vx *= -1; else ball.vy *= -1;
        b.hits--; score += 10; updateHud(); beep(660, 0.05, 'square', 0.05);
        if (b.hits <= 0) { bricks.splice(i, 1); score += 5; updateHud(); }
        break;
      }
    }
    // lost ball
    if (ball.y - ball.r > H) {
      lives--; updateHud(); beep(160, 0.25, 'sawtooth', 0.07);
      if (lives <= 0) { gameOver(); return; }
      resetBallPaddle();
    }
    // level clear
    if (bricks.length === 0) {
      level++; score += 100; updateHud(); buildBricks(level); resetBallPaddle();
      beep(880, 0.12, 'triangle', 0.07);
      overlay('Level ' + level, 'Cleared!', 'Nice. Space or click to launch the next wave.', 'Continue');
      running = false;
    }
  }

  function draw() {
    ctx.clearRect(0, 0, W, H);
    for (var i = 0; i < bricks.length; i++) {
      var b = bricks[i]; ctx.fillStyle = b.color; ctx.globalAlpha = b.hits > 1 ? 1 : 0.82;
      round(b.x, b.y, b.w, b.h, 4); ctx.fill(); ctx.globalAlpha = 1;
    }
    ctx.fillStyle = '#e5e7eb'; round(paddle.x, paddle.y, paddle.w, paddle.h, 6); ctx.fill();
    ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.r, 0, 7); ctx.fillStyle = '#a7f3d0'; ctx.fill();
  }
  function round(x, y, w, h, r) {
    ctx.beginPath(); ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath();
  }

  function loop(ts) {
    requestAnimationFrame(loop);
    if (!running || paused) { last = ts; return; }
    if (!last) last = ts;
    var steps = Math.min(3, Math.round((ts - last) / 16.67)) || 1; last = ts;
    for (var s = 0; s < steps; s++) if (running) tick();
    draw();
  }

  function overlay(tag, title, sub, btn) { ovTag.textContent = tag; ovTitle.textContent = title; ovSub.textContent = sub; playBtn.textContent = btn; ov.classList.remove('hide'); }
  function gameOver() {
    running = false;
    if (score > best) { best = score; localStorage.setItem(BEST_KEY, best); bestEl.textContent = best; }
    overlay(score >= best && score > 0 ? 'New Best!' : 'Game Over', 'Score ' + score, 'Best ' + best + '. Play again?', 'Play again');
  }

  function start() {
    // If overlay was a level-clear pause, just resume; otherwise fresh game.
    if (bricks && bricks.length > 0 && lives > 0 && !paused && running === false && launched === false && score > 0) {
      running = true; ov.classList.add('hide'); last = 0; return;
    }
    newGame(); running = true; paused = false; last = 0; ov.classList.add('hide');
    if (actx && actx.state === 'suspended') actx.resume();
  }
  function togglePause() {
    if (!running && !paused) return;
    paused = !paused; pauseBtn.textContent = paused ? '▶' : '⏸';
    if (paused) overlay('Paused', 'Paused', 'Press P to resume.', 'Resume');
    else { ov.classList.add('hide'); last = 0; }
  }

  // input
  function movePaddleTo(px) { paddle.x = Math.max(0, Math.min(W - paddle.w, px - paddle.w / 2)); }
  cv.addEventListener('mousemove', function (e) { if (!paddle) return; var r = cv.getBoundingClientRect(); movePaddleTo((e.clientX - r.left) * (W / r.width)); });
  cv.addEventListener('click', function () { if (!running && !paused) start(); else launch(); });
  cv.addEventListener('touchmove', function (e) { if (!paddle) return; var r = cv.getBoundingClientRect(); movePaddleTo((e.touches[0].clientX - r.left) * (W / r.width)); e.preventDefault(); }, { passive: false });
  cv.addEventListener('touchstart', function () { if (!running && !paused) start(); else launch(); }, { passive: true });

  var leftDown = false, rightDown = false;
  window.addEventListener('keydown', function (e) {
    var k = e.key.toLowerCase();
    if (k === 'arrowleft' || k === 'a') { leftDown = true; e.preventDefault(); }
    else if (k === 'arrowright' || k === 'd') { rightDown = true; e.preventDefault(); }
    else if (k === ' ') { if (!running && !paused) start(); else launch(); e.preventDefault(); }
    else if (k === 'p' || k === 'escape') togglePause();
    else if (k === 'm') toggleMute();
  });
  window.addEventListener('keyup', function (e) {
    var k = e.key.toLowerCase();
    if (k === 'arrowleft' || k === 'a') leftDown = false;
    if (k === 'arrowright' || k === 'd') rightDown = false;
  });
  // keyboard paddle drift
  setInterval(function () {
    if (!paddle || !running || paused) return;
    if (leftDown) movePaddleTo(paddle.x + paddle.w / 2 - paddle.speed);
    if (rightDown) movePaddleTo(paddle.x + paddle.w / 2 + paddle.speed);
  }, 16);

  playBtn.addEventListener('click', function () { if (paused) togglePause(); else start(); });
  pauseBtn.addEventListener('click', togglePause);
  function toggleMute() { muted = !muted; muteBtn.textContent = muted ? '🔈' : '🔊'; }
  muteBtn.addEventListener('click', toggleMute);
  window.addEventListener('blur', function () { if (running && !paused) togglePause(); });

  newGame(); draw();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>