Showcase

Angry Birds, one level, built by GPT-6 Luna

Hostedvia Codex CLIOpenRouter
Running live in your browserNew tab
Show the code the model wrote
HTML42,835 characters
<!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" />
  <meta name="theme-color" content="#d3efff" />
  <title>Featherweight — Woodland Siege</title>
  <style>
    :root {
      color-scheme: light;
      --ink: #263c42;
      --cream: #fff9eb;
      --orange: #f0783d;
      --green: #4f8c61;
      --glass: rgba(255, 251, 239, .82);
      --line: rgba(49, 72, 70, .12);
    }
    * { box-sizing: border-box; }
    html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #b8e3f3; }
    body { font-family: ui-rounded, "Avenir Next", Avenir, "Segoe UI", sans-serif; color: var(--ink); user-select: none; -webkit-user-select: none; }
    canvas { display: block; width: 100vw; height: 100vh; touch-action: none; cursor: crosshair; }
    .hud { position: fixed; inset: 0; pointer-events: none; padding: 22px 28px; display: flex; flex-direction: column; justify-content: space-between; }
    .topbar { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
    .brand, .score-card, .chapter, .bottom-card, .result-card { background: var(--glass); border: 1px solid rgba(255,255,255,.8); box-shadow: 0 10px 30px rgba(43, 77, 77, .12), inset 0 1px rgba(255,255,255,.9); backdrop-filter: blur(12px); }
    .brand { padding: 12px 16px 13px; border-radius: 18px; min-width: 214px; }
    .brandline { display: flex; align-items: center; gap: 9px; color: #608174; font-size: 10px; font-weight: 900; letter-spacing: .17em; text-transform: uppercase; }
    .brandmark { display: grid; place-items: center; width: 24px; height: 24px; color: white; background: #ee7650; border-radius: 9px 9px 9px 3px; transform: rotate(-7deg); font-size: 14px; box-shadow: inset 0 -3px rgba(120,49,41,.16); }
    h1 { font-size: 20px; line-height: 1; margin: 8px 0 0; letter-spacing: -.035em; font-weight: 950; color: #294148; }
    .topright { display: flex; align-items: flex-start; gap: 10px; }
    .score-card { display: flex; align-items: center; gap: 16px; border-radius: 17px; padding: 9px 15px; min-height: 56px; }
    .stat { min-width: 52px; }
    .stat-label { color: #78908b; text-transform: uppercase; letter-spacing: .13em; font-size: 9px; font-weight: 900; display: block; margin-bottom: 2px; }
    .stat-value { font-size: 18px; font-weight: 950; line-height: 1; color: #314c50; }
    .birds { display: flex; gap: 5px; align-items: center; height: 18px; }
    .bird-dot { width: 15px; height: 15px; display: inline-block; border-radius: 50%; background: #e6644f; border: 2px solid #fff1d4; box-shadow: 0 2px 0 rgba(118,54,42,.25); position: relative; }
    .bird-dot::after { content: ""; position: absolute; width: 5px; height: 4px; right: -2px; top: 2px; background: #f2b946; clip-path: polygon(0 0,100% 50%,0 100%); }
    .bird-dot.used { background: #bbc5bd; box-shadow: none; opacity: .48; }
    .chapter { color: #668177; border-radius: 999px; padding: 10px 14px; text-transform: uppercase; font-weight: 900; letter-spacing: .1em; font-size: 10px; white-space: nowrap; }
    .icon-button { pointer-events: auto; display: grid; place-items: center; width: 44px; height: 44px; border: 1px solid rgba(255,255,255,.85); border-radius: 15px; background: var(--glass); color: #476464; cursor: pointer; box-shadow: 0 8px 22px rgba(43,77,77,.11); transition: transform .15s, background .15s; }
    .icon-button:hover { transform: translateY(-2px); background: #fffdf4; }
    .icon-button:active { transform: translateY(1px); }
    .icon-button svg { width: 19px; height: 19px; }
    .bottomrow { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; }
    .bottom-card { border-radius: 15px; padding: 10px 14px; display: flex; gap: 10px; align-items: center; color: #607871; font-size: 11px; font-weight: 850; letter-spacing: .04em; }
    .gesture { width: 22px; height: 22px; flex: 0 0 22px; border: 1.5px solid #9cb8a7; border-radius: 8px; display: grid; place-items: center; color: #618d70; font-size: 13px; font-weight: 900; transform: rotate(-18deg); }
    .auto-pill { color: #557b69; }
    .auto-dot { width: 7px; height: 7px; background: #ee9258; border-radius: 50%; box-shadow: 0 0 0 4px rgba(238,146,88,.14); }
    .result { display: none; position: fixed; inset: 0; z-index: 4; align-items: center; justify-content: center; background: rgba(31,56,58,.17); backdrop-filter: blur(3px); pointer-events: auto; }
    .result.show { display: flex; animation: appear .25s ease-out; }
    @keyframes appear { from { opacity: 0; } to { opacity: 1; } }
    .result-card { width: min(390px, calc(100vw - 36px)); text-align: center; border-radius: 25px; padding: 28px 30px 25px; background: rgba(255,251,239,.94); box-shadow: 0 24px 70px rgba(32,57,55,.22), inset 0 1px rgba(255,255,255,.95); }
    .result-kicker { color: #719080; text-transform: uppercase; letter-spacing: .19em; font-size: 10px; font-weight: 950; }
    .result h2 { margin: 8px 0 4px; font-size: 34px; letter-spacing: -.055em; line-height: 1; font-weight: 1000; color: #29464a; }
    .result-copy { color: #708782; margin: 8px 0 13px; font-size: 13px; font-weight: 650; }
    .stars { height: 42px; color: #eab64b; font-size: 31px; letter-spacing: 7px; text-shadow: 0 2px #fff0ba; }
    .stars .dim { color: #cbd4c8; text-shadow: none; }
    .final-score { color: #385459; font-size: 14px; font-weight: 950; margin-bottom: 18px; }
    .restart { border: 0; border-radius: 13px; background: #e9724b; color: white; padding: 13px 22px; font: inherit; font-weight: 950; letter-spacing: .025em; cursor: pointer; box-shadow: inset 0 -3px rgba(121,53,39,.2), 0 7px 15px rgba(233,114,75,.2); transition: transform .15s, filter .15s; }
    .restart:hover { transform: translateY(-2px); filter: brightness(1.04); }
    @media (max-width: 700px) {
      .hud { padding: 13px; }
      .brand { min-width: 0; padding: 9px 11px 10px; border-radius: 15px; }
      .brandline { font-size: 8px; gap: 6px; }
      .brandmark { width: 20px; height: 20px; font-size: 11px; }
      h1 { font-size: 16px; }
      .topbar { gap: 8px; }
      .topright { gap: 6px; align-items: flex-start; }
      .score-card { padding: 8px 9px; min-height: 49px; gap: 9px; border-radius: 14px; }
      .stat { min-width: 42px; }
      .stat-label { font-size: 7px; letter-spacing: .1em; }
      .stat-value { font-size: 15px; }
      .bird-dot { width: 12px; height: 12px; }
      .chapter { display: none; }
      .icon-button { width: 39px; height: 39px; border-radius: 13px; }
      .bottomrow { align-items: flex-end; }
      .bottom-card { font-size: 9px; padding: 9px 10px; gap: 7px; }
      .auto-pill { max-width: 145px; }
    }
    @media (max-width: 420px) { .brandline span:last-child { display: none; } .score-card { gap: 6px; } .stat { min-width: 37px; } }
  </style>
</head>
<body>
  <canvas id="game" aria-label="A slingshot game. Drag the red bird back and release to launch at the pig fort."></canvas>
  <div class="hud">
    <div class="topbar">
      <div class="brand">
        <div class="brandline"><span class="brandmark">✦</span><span>Featherweight arcade</span></div>
        <h1>Woodland Siege</h1>
      </div>
      <div class="topright">
        <div class="chapter">Chapter 01&nbsp; · &nbsp;Hilltop fort</div>
        <div class="score-card">
          <div class="stat"><span class="stat-label">Score</span><span class="stat-value" id="score">00000</span></div>
          <div class="stat"><span class="stat-label">Birds left</span><div class="birds" id="birds"><i class="bird-dot"></i><i class="bird-dot"></i><i class="bird-dot"></i></div></div>
        </div>
        <button class="icon-button" id="restartTop" aria-label="Restart level" title="Restart level">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7v5h-5"/><path d="M19 12a7.5 7.5 0 1 1-2.2-5.3L20 9"/></svg>
        </button>
      </div>
    </div>
    <div class="bottomrow">
      <div class="bottom-card"><span class="gesture">↶</span><span>DRAG BACK TO AIM &nbsp;·&nbsp; RELEASE TO FLY</span></div>
      <div class="bottom-card auto-pill" id="modePill"><span class="auto-dot"></span><span id="modeText">AUTO PILOT &nbsp;·&nbsp; GRAB THE SLINGSHOT</span></div>
    </div>
  </div>
  <div class="result" id="result" role="dialog" aria-modal="true" aria-labelledby="resultTitle">
    <div class="result-card">
      <div class="result-kicker" id="resultKicker">The flock has landed</div>
      <h2 id="resultTitle">Level cleared!</h2>
      <p class="result-copy" id="resultCopy">A fine bit of flying.</p>
      <div class="stars" id="stars">★★★</div>
      <div class="final-score" id="finalScore">0 points</div>
      <button class="restart" id="restartResult">Fly again</button>
    </div>
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script>
  <script>
  (() => {
    const canvas = document.getElementById('game');
    const ctx = canvas.getContext('2d');
    const $score = document.getElementById('score');
    const $birds = document.getElementById('birds');
    const $modeText = document.getElementById('modeText');
    const $modePill = document.getElementById('modePill');
    const $result = document.getElementById('result');
    const $resultTitle = document.getElementById('resultTitle');
    const $resultKicker = document.getElementById('resultKicker');
    const $resultCopy = document.getElementById('resultCopy');
    const $stars = document.getElementById('stars');
    const $finalScore = document.getElementById('finalScore');
    const M = window.Matter;
    if (!M) {
      document.getElementById('modeText').textContent = 'PHYSICS COULD NOT LOAD · REFRESH TO TRY AGAIN';
      return;
    }
    const { Engine, Bodies, Body, Composite, Events, Sleeping, Vector } = M;
    const engine = Engine.create({ enableSleeping: true });
    engine.gravity.y = 1;
    engine.gravity.scale = 0.001;
    engine.positionIterations = 8;
    engine.velocityIterations = 6;
    engine.constraintIterations = 2;
    const STEP = 1000 / 60;
    const GRAVITY_STEP = engine.gravity.y * engine.gravity.scale * STEP * STEP;
    const TAU = Math.PI * 2;
    let W = 0, H = 0, DPR = 1, S = 1, groundY = 0, worldWidth = 0;
    let sling = null, readyBird = null, blocks = [], pigs = [], shots = [], particles = [], floats = [], debris = [];
    let score = 0, birdsUsed = 0, cameraX = 0, autoMode = true, autoTimer = 1.3, autoAim = 0, dragging = false, pointerId = null;
    let pointer = { x: 0, y: 0 }, gameResult = null, resultAt = 0, lastShotAt = 0, aimState = false, levelReady = false;
    let lastFrame = 0, accumulator = 0, now = 0, autoCycle = 0, shake = 0, nextBirdWait = 0;
    const rand = (a, b) => a + Math.random() * (b - a);
    const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
    const ease = (a, b, t) => a + (b - a) * (1 - Math.exp(-t));
    const alivePigs = () => pigs.filter(p => !p.isPopped).length;

    function resize() {
      W = window.innerWidth; H = window.innerHeight;
      DPR = Math.min(2, window.devicePixelRatio || 1);
      canvas.width = Math.round(W * DPR); canvas.height = Math.round(H * DPR);
      ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
      S = clamp(Math.min(W / 1280, H / 800), .55, 1.2);
      groundY = H * .835;
      worldWidth = W * 1.55;
      makeLevel();
    }
    function makeLevel() {
      Composite.clear(engine.world, false);
      engine.pairs = M.Pairs.create();
      blocks = []; pigs = []; shots = []; particles = []; floats = []; debris = [];
      score = 0; birdsUsed = 0; cameraX = 0; gameResult = null; aimState = false; dragging = false; pointerId = null;
      autoTimer = autoMode ? 1.35 : Infinity; autoAim = 0; autoCycle = 0; nextBirdWait = 0; lastShotAt = 0; shake = 0;
      $result.classList.remove('show');
      const floor = Bodies.rectangle(worldWidth / 2, groundY + 80 * S, worldWidth + 500, 160 * S, { isStatic: true, friction: .88, label: 'ground' });
      floor.gameKind = 'ground'; Composite.add(engine.world, floor);
      sling = { x: W * .155, y: groundY - 132 * S, baseY: groundY };
      const cx = Math.max(W * .735, sling.x + 420 * S);
      buildFort(cx);
      readyBird = makeBird(sling.x, sling.y);
      updateHud();
      levelReady = true;
    }
    function addRect(x, y, w, h, kind, angle = 0, options = {}) {
      const stone = kind === 'stone';
      const body = Bodies.rectangle(x, y, w, h, {
        angle, density: stone ? .0046 : .00125, friction: stone ? .82 : .72,
        frictionStatic: .95, frictionAir: .008, restitution: stone ? .035 : .075,
        chamfer: undefined, sleepThreshold: 50,
        label: kind
      });
      body.gameKind = kind; body.gameW = w; body.gameH = h;
      body.materialName = kind; body.breakStrength = options.breakStrength || 9.8;
      body.paintSeed = Math.random() * 1000;
      blocks.push(body); Composite.add(engine.world, body); Sleeping.set(body, true);
      return body;
    }
    function buildFort(cx) {
      const u = S;
      const col = (x,y,w,h,k,ang=0) => addRect(x,y,w*u,h*u,k,ang);
      const g = groundY;
      // A stout stone plinth and two timber pillars make a stable, quiet start.
      col(cx - 109*u, g - 20*u, 84, 40, 'stone');
      col(cx + 109*u, g - 20*u, 84, 40, 'stone');
      col(cx - 108*u, g - 112*u, 25, 144, 'wood');
      col(cx + 108*u, g - 112*u, 25, 144, 'wood');
      col(cx, g - 190*u, 242, 22, 'wood');
      // Two compact rooms with pigs on their floors.
      col(cx - 82*u, g - 244*u, 22, 88, 'wood');
      col(cx + 82*u, g - 244*u, 22, 88, 'wood');
      col(cx, g - 200*u, 176, 18, 'wood');
      col(cx, g - 291*u, 206, 23, 'stone');
      col(cx - 99*u, g - 334*u, 21, 73, 'wood');
      col(cx + 99*u, g - 334*u, 21, 73, 'wood');
      col(cx, g - 376*u, 246, 22, 'wood');
      // Corner braces and weighted roof stones give the collapse some variety.
      col(cx - 132*u, g - 119*u, 18, 64, 'wood', -.23);
      col(cx + 132*u, g - 119*u, 18, 64, 'wood', .23);
      col(cx - 62*u, g - 352*u, 52, 37, 'stone');
      col(cx + 62*u, g - 352*u, 52, 37, 'stone');
      const pigSpec = [
        [cx - 43*u, g - 222*u, 21*u], [cx + 43*u, g - 222*u, 21*u], [cx, g - 326*u, 20*u]
      ];
      for (const [x,y,r] of pigSpec) {
        const pig = Bodies.circle(x, y, r, { density: .0014, friction: .8, frictionAir: .012, restitution: .015, label: 'pig', sleepThreshold: 50 });
        pig.gameKind = 'pig'; pig.gameR = r; pig.isPopped = false; pig.blinkSeed = Math.random() * 10;
        pigs.push(pig); Composite.add(engine.world, pig); Sleeping.set(pig, true);
      }
    }
    function makeBird(x, y) {
      const r = 25 * S;
      const body = Bodies.circle(x, y, r, { density: .0028, friction: .68, frictionAir: .0028, restitution: .31, label: 'bird', sleepThreshold: 45 });
      body.gameKind = 'bird'; body.gameR = r; body.isReady = true; body.isLaunched = false; body.flightAge = 0; body.flightState = 'ready';
      Composite.add(engine.world, body); Sleeping.set(body, true);
      return body;
    }
    function bodyPosition(body) { return body ? { x: body.position.x, y: body.position.y } : sling; }

    function aimPositionFromPointer(p) {
      if (!readyBird) return;
      let dx = p.x - sling.x, dy = p.y - sling.y;
      const maxPull = 123 * S;
      const mag = Math.hypot(dx, dy);
      if (mag > maxPull) { dx *= maxPull / mag; dy *= maxPull / mag; }
      dx = Math.min(dx, 13 * S);
      Body.setPosition(readyBird, { x: sling.x + dx, y: sling.y + dy });
      Body.setVelocity(readyBird, { x: 0, y: 0 }); Body.setAngularVelocity(readyBird, 0);
      Sleeping.set(readyBird, true);
      aimState = true;
    }
    function launch(body) {
      if (!body || gameResult) return;
      const dx = sling.x - body.position.x, dy = sling.y - body.position.y;
      if (Math.hypot(dx, dy) < 9 * S) return;
      const power = .205;
      Body.setVelocity(body, { x: dx * power, y: dy * power });
      Body.setAngularVelocity(body, rand(-.09, .09));
      Sleeping.set(body, false);
      body.isReady = false; body.isLaunched = true; body.flightState = 'flying'; body.flightAge = 0; body.lastEffectAt = 0;
      shots.push(body); readyBird = null; birdsUsed++; lastShotAt = now; nextBirdWait = .62;
      aimState = false; dragging = false; autoAim = 0; autoTimer = autoMode ? 1.05 : Infinity;
      updateHud();
      addFloat(body.position.x, body.position.y - 35*S, 'FLY!', '#fff4c8', 1.1);
      addParticles(body.position.x, body.position.y, 'feather', 5, '#e96d4e');
    }
    function makeNextBird() {
      if (readyBird || birdsUsed >= 3 || gameResult) return;
      readyBird = makeBird(sling.x, sling.y); autoTimer = autoMode ? 1.05 : Infinity; autoAim = 0; aimState = false; updateHud();
    }
    function autoPull() {
      const tweak = autoCycle % 3;
      const backX = (100 + tweak * 4) * S;
      const down = [42, 52, 36][tweak] * S;
      aimPositionFromPointer({ x: sling.x - backX, y: sling.y + down });
    }

    Events.on(engine, 'collisionStart', event => {
      for (const pair of event.pairs) {
        const a = pair.bodyA, b = pair.bodyB;
        const ka = a.gameKind, kb = b.gameKind;
        if (!ka || !kb || ka === 'ground' || kb === 'ground') continue;
        const rel = Vector.sub(a.velocity, b.velocity);
        const impact = Vector.magnitude(rel);
        const contact = pair.collision && pair.collision.supports && pair.collision.supports.length ? pair.collision.supports[0] : { x: (a.position.x+b.position.x)/2, y: (a.position.y+b.position.y)/2 };
        if (ka === 'wood' && impact > a.breakStrength && (kb === 'bird' || impact > 12.1)) breakWood(a, contact, impact);
        if (kb === 'wood' && impact > b.breakStrength && (ka === 'bird' || impact > 12.1)) breakWood(b, contact, impact);
        checkPigImpact(a,b,impact,contact);
        if ((ka === 'bird' || kb === 'bird') && impact > 3 && now - Math.max(a.lastEffectAt||0,b.lastEffectAt||0) > 360) {
          const bird = ka === 'bird' ? a : b;
          if (bird.isLaunched) {
            bird.lastEffectAt = now;
            shake = Math.min(8*S, impact*.25*S);
            addParticles(contact.x, contact.y, impact > 9 ? 'dust' : 'spark', impact > 9 ? 9 : 5, impact > 9 ? '#e9c692' : '#fff0a6');
            if (impact > 5) addFloat(contact.x, contact.y-14*S, `+${Math.round(impact*8)}`, '#fff5d5', .85);
          }
        }
      }
    });
    Events.on(engine, 'collisionActive', event => {
      for (const pair of event.pairs) {
        const a=pair.bodyA,b=pair.bodyB;
        if(a.gameKind!=='pig' && b.gameKind!=='pig') continue;
        const impact=Vector.magnitude(Vector.sub(a.velocity,b.velocity));
        const contact=pair.collision && pair.collision.supports && pair.collision.supports.length ? pair.collision.supports[0] : {x:(a.position.x+b.position.x)/2,y:(a.position.y+b.position.y)/2};
        checkPigImpact(a,b,impact,contact);
      }
    });
    function checkPigImpact(a,b,impact,contact) {
      const pig=a.gameKind==='pig'?a:(b.gameKind==='pig'?b:null);
      if(!pig || pig.isPopped) return;
      const other=pig===a?b:a;
      if(other.gameKind==='bird' && impact>4.7) { popPig(pig,contact); return; }
      if(!['wood','stone','shard'].includes(other.gameKind)) return;
      if(impact>8.1 || (other.gameKind==='stone' && impact>5.6)) { popPig(pig,contact); return; }
      const above=other.position.y < pig.position.y - pig.gameR*.18;
      if(above) {
        pig.crushSince ??= now;
        pig.lastCrushContact=now;
        if(now-pig.crushSince>260) popPig(pig,contact);
      }
    }
    function breakWood(body, at, impact) {
      if (!body || body.isBroken) return;
      body.isBroken = true;
      const idx = blocks.indexOf(body); if (idx >= 0) blocks.splice(idx, 1);
      Composite.remove(engine.world, body);
      score += 45; updateHud();
      addParticles(at.x, at.y, 'wood', 15, '#b97643');
      addFloat(at.x, at.y-20*S, '+45', '#fff2c9', 1);
      if (debris.length < 12) {
        const count = impact > 15 ? 3 : 2;
        for (let i=0;i<count;i++) {
          const shard = Bodies.rectangle(at.x+rand(-8,8)*S, at.y+rand(-7,7)*S, rand(19,31)*S, rand(7,11)*S, { density:.0009, friction:.55, frictionAir:.018, restitution:.2, label:'shard' });
          shard.gameKind='shard'; shard.gameW=26*S; shard.gameH=9*S; shard.paintSeed=Math.random()*1000; shard.ttl=2500;
          Composite.add(engine.world, shard); Body.setVelocity(shard,{x:rand(-8,8),y:rand(-9,2)}); Body.setAngularVelocity(shard,rand(-.18,.18)); debris.push(shard);
        }
      }
    }
    function popPig(pig, at) {
      if (!pig || pig.isPopped) return;
      pig.isPopped = true; Composite.remove(engine.world, pig);
      score += 500; updateHud();
      addParticles(at.x, at.y, 'pig', 20, '#72bd60');
      addParticles(at.x, at.y, 'feather', 7, '#f6d483');
      addFloat(at.x, at.y-25*S, 'OINK!  +500', '#fff2b9', 1.25);
      shake = 13*S;
      if (alivePigs() === 0) endGame(true);
    }
    function updateHud() {
      $score.textContent = String(score).padStart(5, '0');
      const remaining = Math.max(0, 3 - birdsUsed);
      [...$birds.children].forEach((el, i) => el.classList.toggle('used', i >= remaining));
      $modeText.textContent = autoMode ? 'AUTO PILOT  ·  GRAB THE SLINGSHOT' : 'YOUR TURN  ·  PULL BACK TO AIM';
      $modePill.style.opacity = autoMode ? '1' : '.9';
    }
    function endGame(won) {
      if (gameResult) return;
      gameResult = won ? 'win' : 'lose'; resultAt = now;
      $resultTitle.textContent = won ? 'Level cleared!' : 'Try again';
      $resultKicker.textContent = won ? 'The fort has fallen' : 'The pigs are still in there';
      $resultCopy.textContent = won ? 'A fine bit of flying.' : 'Take another run at that wobbly fort.';
      const stars = won ? (birdsUsed <= 1 ? 3 : birdsUsed === 2 ? 2 : 1) : 0;
      $stars.innerHTML = won ? `${'★'.repeat(stars)}<span class="dim">${'★'.repeat(3-stars)}</span>` : '<span class="dim">★★★</span>';
      $finalScore.textContent = `${score.toLocaleString()} points  ·  ${birdsUsed} bird${birdsUsed === 1 ? '' : 's'} used`;
      $result.classList.add('show');
      updateHud();
    }

    function addParticles(x,y,type,count,color) {
      for(let i=0;i<count;i++) {
        const feather = type==='feather';
        particles.push({ x:x+rand(-8,8)*S,y:y+rand(-8,8)*S,vx:rand(-4,4)*S,vy:rand(-6,2)*S,life:rand(.45,1.05),max:1,size:(feather?rand(5,12):rand(2,7))*S,rot:rand(0,TAU),spin:rand(-.14,.14),type,color });
      }
    }
    function addFloat(x,y,text,color='#fff5d7',life=1) { floats.push({x,y,text,color,life,max:life}); }

    function roundedRect(c,x,y,w,h,r) {
      r=Math.min(r,w/2,h/2); c.beginPath(); c.moveTo(x+r,y); c.arcTo(x+w,y,x+w,y+h,r); c.arcTo(x+w,y+h,x,y+h,r); c.arcTo(x,y+h,x,y,r); c.arcTo(x,y,x+w,y,r); c.closePath();
    }
    function hill(c, offset, y, amp, fill, phase) {
      c.beginPath(); c.moveTo(-10,H+4); c.lineTo(-10,y);
      for(let x=-10;x<=W+20;x+=W/7) c.quadraticCurveTo(x+W/14,y-amp*Math.sin((x+offset)/W*TAU+phase),x+W/7,y+amp*.3*Math.cos((x+offset)/W*TAU+phase));
      c.lineTo(W+20,H+4); c.closePath(); c.fillStyle=fill; c.fill();
    }
    function drawBackground() {
      const sky=ctx.createLinearGradient(0,0,0,H); sky.addColorStop(0,'#a9def6'); sky.addColorStop(.55,'#d6f0ee'); sky.addColorStop(1,'#fff0ce');
      ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);
      const sunX=W*.79-cameraX*.12, sunY=H*.18, sr=48*S;
      const halo=ctx.createRadialGradient(sunX,sunY,5,sunX,sunY,sr*2.3);halo.addColorStop(0,'rgba(255,244,192,.68)');halo.addColorStop(1,'rgba(255,244,192,0)');ctx.fillStyle=halo;ctx.fillRect(sunX-sr*2.3,sunY-sr*2.3,sr*4.6,sr*4.6);
      ctx.beginPath();ctx.arc(sunX,sunY,sr*.52,0,TAU);ctx.fillStyle='#fff3c5';ctx.fill();
      // Soft little cloud banks drift with the distant ridge.
      for(let i=0;i<4;i++) {
        const x=((i*W*.36-cameraX*.18+W*2)%(W*1.45))-W*.1, y=H*(.15+(i%3)*.085), q=16*S;
        ctx.fillStyle='rgba(255,255,255,.36)';ctx.beginPath();ctx.ellipse(x,y,q*2.1,q*.55,0,0,TAU);ctx.ellipse(x-q*.65,y-q*.3,q*.76,q*.8,0,0,TAU);ctx.ellipse(x+q*.5,y-q*.2,q*.98,q*.92,0,0,TAU);ctx.fill();
      }
      hill(ctx,cameraX*.12,H*.62,H*.075,'#a9d6bd',.4);
      hill(ctx,cameraX*.27,H*.70,H*.10,'#83c29d',1.7);
      hill(ctx,cameraX*.42,H*.77,H*.07,'#65ae83',3.1);
      // Distant pines are simple rounded silhouettes.
      ctx.fillStyle='rgba(69,142,104,.42)';
      for(let i=0;i<14;i++) {
        const x=((i*W*.115-cameraX*.31+W*3)%(W*1.4))-W*.15, y=H*(.71+(i%4)*.025), ht=(32+(i%3)*10)*S;
        ctx.beginPath();ctx.moveTo(x,y-ht);ctx.lineTo(x-ht*.36,y);ctx.lineTo(x+ht*.36,y);ctx.closePath();ctx.fill();
      }
    }
    function drawGround() {
      ctx.save();ctx.translate(-cameraX,0);
      const x0=-cameraX-100, width=worldWidth+300;
      ctx.fillStyle='#c9945c';ctx.fillRect(x0,groundY,width,H-groundY+100);
      const soil=ctx.createLinearGradient(0,groundY,0,H);soil.addColorStop(0,'#c89761');soil.addColorStop(1,'#a66f48');ctx.fillStyle=soil;ctx.fillRect(x0,groundY,width,H-groundY+100);
      ctx.beginPath();ctx.moveTo(x0,groundY+2*S);ctx.lineTo(x0,groundY-8*S);
      for(let x=x0;x<x0+width;x+=44*S) ctx.quadraticCurveTo(x+17*S,groundY-13*S+Math.sin(x*.017)*2*S,x+44*S,groundY-6*S+Math.sin(x*.012)*2*S);
      ctx.lineTo(x0+width,groundY+6*S);ctx.lineTo(x0,groundY+6*S);ctx.closePath();
      const grass=ctx.createLinearGradient(0,groundY-10*S,0,groundY+8*S);grass.addColorStop(0,'#5aab75');grass.addColorStop(.45,'#70bd79');grass.addColorStop(1,'#499363');ctx.fillStyle=grass;ctx.fill();
      // Subtle earth strata, pebbles and small grass tufts.
      for(let i=0;i<24;i++) {
        const x=((i*91*S+47*S)%(worldWidth+150))-50*S;
        ctx.fillStyle=i%2?'rgba(120,75,48,.12)':'rgba(250,212,151,.23)';
        ctx.beginPath();ctx.ellipse(x,groundY+(30+(i%5)*25)*S,rand(7,14)*S,rand(2,4)*S,0,0,TAU);ctx.fill();
      }
      for(let i=0;i<38;i++) {
        const x=((i*67*S+19*S)%(worldWidth+100))-30*S, yy=groundY-7*S;
        ctx.strokeStyle=i%2?'#478e61':'#79bf75';ctx.lineWidth=2*S;ctx.beginPath();ctx.moveTo(x,yy+2*S);ctx.quadraticCurveTo(x-3*S,yy-5*S,x-4*S,yy-8*S);ctx.moveTo(x,yy+2*S);ctx.quadraticCurveTo(x+2*S,yy-5*S,x+5*S,yy-7*S);ctx.stroke();
      }
      ctx.restore();
    }
    function drawSling(back) {
      const x=sling.x,y=sling.y, base=sling.baseY;
      ctx.save();ctx.translate(-cameraX,0);
      if(back) {
        ctx.lineCap='round';ctx.lineJoin='round';
        // Back fork and the elastic straps.
        ctx.strokeStyle='#75402d';ctx.lineWidth=13*S;ctx.beginPath();ctx.moveTo(x+2*S,base+4*S);ctx.quadraticCurveTo(x-8*S,base-64*S,x-14*S,y-3*S);ctx.stroke();
        ctx.strokeStyle='#b87743';ctx.lineWidth=8*S;ctx.beginPath();ctx.moveTo(x+1*S,base-2*S);ctx.quadraticCurveTo(x-7*S,base-73*S,x-14*S,y-3*S);ctx.stroke();
        ctx.strokeStyle='#dcb37a';ctx.lineWidth=4*S;ctx.beginPath();ctx.moveTo(x-14*S,y-5*S);ctx.lineTo(x-8*S,y-5*S);ctx.moveTo(x-14*S,y-5*S);ctx.lineTo(x-8*S,y+3*S);ctx.stroke();
        if(readyBird && (aimState || autoAim>0 || autoMode)) {
          const p=bodyPosition(readyBird);ctx.strokeStyle='#654637';ctx.lineWidth=4*S;ctx.beginPath();ctx.moveTo(x-12*S,y-3*S);ctx.lineTo(p.x,p.y);ctx.moveTo(x-8*S,y+3*S);ctx.lineTo(p.x,p.y);ctx.stroke();
        }
      } else {
        // Front fork crosses the pocket and gives the sling a hand-cut wooden look.
        ctx.strokeStyle='#75402d';ctx.lineWidth=14*S;ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();ctx.moveTo(x-8*S,base+3*S);ctx.quadraticCurveTo(x+4*S,base-63*S,x+15*S,y-2*S);ctx.stroke();
        ctx.strokeStyle='#c3834b';ctx.lineWidth=8*S;ctx.beginPath();ctx.moveTo(x-8*S,base-4*S);ctx.quadraticCurveTo(x+4*S,base-65*S,x+15*S,y-2*S);ctx.stroke();
        ctx.strokeStyle='#e0b576';ctx.lineWidth=3*S;ctx.beginPath();ctx.moveTo(x+10*S,y-9*S);ctx.lineTo(x+16*S,y-1*S);ctx.moveTo(x+10*S,y-8*S);ctx.lineTo(x+4*S,y-5*S);ctx.stroke();
      }
      ctx.restore();
    }
    function drawTrajectory() {
      if(!readyBird || gameResult || !(aimState || autoAim>0)) return;
      const p=readyBird.position, vx=(sling.x-p.x)*.205, vy=(sling.y-p.y)*.205;
      ctx.save();ctx.translate(-cameraX,0);
      for(let i=1;i<=16;i++) {
        const t=i*3.05, x=p.x+vx*t, y=p.y+vy*t+.5*GRAVITY_STEP*t*t;
        if(y>groundY-3*S) break;
        const r=(4.3-i*.16)*S, alpha=.76-i*.034;
        ctx.beginPath();ctx.arc(x,y,Math.max(1.2*S,r),0,TAU);ctx.fillStyle=`rgba(255,247,218,${alpha})`;ctx.fill();
        ctx.beginPath();ctx.arc(x,y,Math.max(.7*S,r*.38),0,TAU);ctx.fillStyle='rgba(237,126,77,.88)';ctx.fill();
      }
      ctx.restore();
    }
    function drawWood(body) {
      const w=body.gameW,h=body.gameH,seed=body.paintSeed||0;
      ctx.save();ctx.translate(body.position.x,body.position.y);ctx.rotate(body.angle);
      // A soft offset shadow grounds each plank without making it look like UI art.
      ctx.fillStyle='rgba(54,60,44,.16)';roundedRect(ctx,-w/2+2*S,-h/2+4*S,w,h,Math.min(5*S,h*.28));ctx.fill();
      const g=ctx.createLinearGradient(0,-h/2,0,h/2);g.addColorStop(0,'#dca66a');g.addColorStop(.34,'#c78a51');g.addColorStop(1,'#aa6d3c');
      roundedRect(ctx,-w/2,-h/2,w,h,Math.min(5*S,h*.28));ctx.fillStyle=g;ctx.fill();ctx.strokeStyle='#87552f';ctx.lineWidth=1.5*S;ctx.stroke();
      ctx.save();roundedRect(ctx,-w/2+2*S,-h/2+2*S,w-4*S,h-4*S,Math.min(4*S,h*.26));ctx.clip();
      const horizontal=w>=h;
      for(let i=0;i<3;i++) {
        ctx.beginPath();ctx.strokeStyle=i===1?'rgba(255,213,146,.45)':'rgba(107,61,32,.2)';ctx.lineWidth=(i===1?1.7:1)*S;
        if(horizontal) { const yy=-h*.25+i*h*.25;ctx.moveTo(-w/2+5*S,yy);ctx.bezierCurveTo(-w*.2,yy+Math.sin(seed+i)*2*S,w*.18,yy-2*S,w/2-5*S,yy+Math.cos(seed+i)*2*S); }
        else { const xx=-w*.25+i*w*.25;ctx.moveTo(xx,-h/2+4*S);ctx.bezierCurveTo(xx+Math.sin(seed+i)*2*S,-h*.2,xx-2*S,h*.2,xx+Math.cos(seed+i)*2*S,h/2-4*S); }
        ctx.stroke();
      }
      for(const side of [-1,1]) { ctx.beginPath();ctx.arc(side*(w/2-5*S),0,1.6*S,0,TAU);ctx.fillStyle='#68482f';ctx.fill();ctx.beginPath();ctx.arc(side*(w/2-5*S)-.4*S,-.4*S,.55*S,0,TAU);ctx.fillStyle='#e6bd83';ctx.fill(); }
      ctx.restore();ctx.restore();
    }
    function drawStone(body) {
      const w=body.gameW,h=body.gameH;
      ctx.save();ctx.translate(body.position.x,body.position.y);ctx.rotate(body.angle);
      ctx.fillStyle='rgba(43,58,54,.15)';roundedRect(ctx,-w/2+2*S,-h/2+4*S,w,h,5*S);ctx.fill();
      const g=ctx.createLinearGradient(0,-h/2,0,h/2);g.addColorStop(0,'#a8b5aa');g.addColorStop(.42,'#889b91');g.addColorStop(1,'#6f847d');
      roundedRect(ctx,-w/2,-h/2,w,h,5*S);ctx.fillStyle=g;ctx.fill();ctx.strokeStyle='#63786f';ctx.lineWidth=1.6*S;ctx.stroke();
      ctx.strokeStyle='rgba(235,242,219,.5)';ctx.lineWidth=1.2*S;ctx.beginPath();ctx.moveTo(-w*.32,-h*.22);ctx.lineTo(w*.24,-h*.22);ctx.moveTo(-w*.38,-h*.12);ctx.lineTo(-w*.28,-h*.04);ctx.lineTo(-w*.34,h*.07);ctx.moveTo(w*.29,h*.05);ctx.lineTo(w*.19,h*.14);ctx.lineTo(w*.28,h*.23);ctx.stroke();
      ctx.fillStyle='rgba(255,255,255,.24)';roundedRect(ctx,-w/2+4*S,-h/2+3*S,w-8*S,2*S,1*S);ctx.fill();
      ctx.restore();
    }
    function drawPig(body) {
      const r=body.gameR, x=body.position.x,y=body.position.y;
      ctx.save();ctx.translate(x,y);ctx.rotate(body.angle*.25);
      // ears
      ctx.fillStyle='#62ae55';ctx.beginPath();ctx.ellipse(-r*.62,-r*.68,r*.34,r*.43,-.38,0,TAU);ctx.ellipse(r*.62,-r*.68,r*.34,r*.43,.38,0,TAU);ctx.fill();
      ctx.fillStyle='#92d675';ctx.beginPath();ctx.ellipse(-r*.62,-r*.68,r*.17,r*.25,-.38,0,TAU);ctx.ellipse(r*.62,-r*.68,r*.17,r*.25,.38,0,TAU);ctx.fill();
      const g=ctx.createRadialGradient(-r*.35,-r*.42,r*.08,0,0,r*1.25);g.addColorStop(0,'#9ade72');g.addColorStop(.62,'#71c35b');g.addColorStop(1,'#4a9b4e');ctx.beginPath();ctx.arc(0,0,r,0,TAU);ctx.fillStyle=g;ctx.fill();ctx.strokeStyle='#418749';ctx.lineWidth=1.7*S;ctx.stroke();
      // cheek blush
      ctx.fillStyle='rgba(242,155,123,.38)';ctx.beginPath();ctx.ellipse(-r*.62,r*.2,r*.2,r*.12,0,0,TAU);ctx.ellipse(r*.62,r*.2,r*.2,r*.12,0,0,TAU);ctx.fill();
      // eyes with tiny worried brows
      for(const side of [-1,1]) {
        const ex=side*r*.34, ey=-r*.18;
        ctx.fillStyle='#fff7dc';ctx.beginPath();ctx.ellipse(ex,ey,r*.22,r*.28,side*.12,0,TAU);ctx.fill();
        ctx.fillStyle='#344449';ctx.beginPath();ctx.arc(ex+side*r*.055,ey+r*.02,r*.095,0,TAU);ctx.fill();ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(ex+side*r*.075,ey-r*.015,r*.035,0,TAU);ctx.fill();
        ctx.strokeStyle='#365842';ctx.lineWidth=2.5*S;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(ex-r*.17,ey-r*.35);ctx.lineTo(ex+r*.12,ey-r*.43);ctx.stroke();
      }
      // snout
      const sg=ctx.createLinearGradient(0,r*.05,0,r*.65);sg.addColorStop(0,'#b4ea80');sg.addColorStop(1,'#80cc65');ctx.fillStyle=sg;ctx.beginPath();ctx.ellipse(0,r*.28,r*.44,r*.31,0,0,TAU);ctx.fill();ctx.strokeStyle='rgba(66,143,69,.5)';ctx.lineWidth=1.2*S;ctx.stroke();
      ctx.fillStyle='#47894e';ctx.beginPath();ctx.ellipse(-r*.16,r*.29,r*.055,r*.105,-.1,0,TAU);ctx.ellipse(r*.16,r*.29,r*.055,r*.105,.1,0,TAU);ctx.fill();
      ctx.restore();
    }
    function drawBird(body) {
      const r=body.gameR,x=body.position.x,y=body.position.y;
      ctx.save();ctx.translate(x,y);ctx.rotate(body.angle*.4);
      // tail feathers
      ctx.fillStyle='#c64a3e';ctx.beginPath();ctx.ellipse(-r*.84,-r*.07,r*.35,r*.16,-.55,0,TAU);ctx.ellipse(-r*.9,r*.17,r*.32,r*.14,-.14,0,TAU);ctx.fill();
      const g=ctx.createRadialGradient(-r*.35,-r*.45,r*.04,0,0,r*1.35);g.addColorStop(0,'#fa8860');g.addColorStop(.52,'#e65d4b');g.addColorStop(1,'#bc3f3e');ctx.beginPath();ctx.arc(0,0,r,0,TAU);ctx.fillStyle=g;ctx.fill();ctx.strokeStyle='#a8443c';ctx.lineWidth=1.7*S;ctx.stroke();
      // crown tufts
      ctx.fillStyle='#c44940';ctx.beginPath();ctx.ellipse(-r*.18,-r*.91,r*.14,r*.35,-.4,0,TAU);ctx.ellipse(r*.1,-r*.94,r*.13,r*.32,.35,0,TAU);ctx.fill();
      for(const side of [-1,1]) {
        const ex=side*r*.25, ey=-r*.2;
        ctx.fillStyle='#fff8e8';ctx.beginPath();ctx.ellipse(ex,ey,r*.22,r*.28,0,0,TAU);ctx.fill();
        ctx.fillStyle='#253e45';ctx.beginPath();ctx.arc(ex+r*.045,ey+r*.04,r*.09,0,TAU);ctx.fill();ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(ex+r*.066,ey+r*.005,r*.032,0,TAU);ctx.fill();
      }
      ctx.strokeStyle='#5c3433';ctx.lineWidth=4*S;ctx.lineCap='round';ctx.beginPath();ctx.moveTo(-r*.51,-r*.5);ctx.lineTo(-r*.07,-r*.38);ctx.moveTo(r*.07,-r*.39);ctx.lineTo(r*.51,-r*.51);ctx.stroke();
      const bg=ctx.createLinearGradient(0,-r*.02,0,r*.5);bg.addColorStop(0,'#ffd76a');bg.addColorStop(1,'#e99737');ctx.fillStyle=bg;ctx.beginPath();ctx.moveTo(r*.28,-r*.02);ctx.lineTo(r*1.13,r*.12);ctx.lineTo(r*.28,r*.31);ctx.closePath();ctx.fill();ctx.strokeStyle='#a9662d';ctx.lineWidth=1*S;ctx.stroke();
      ctx.restore();
    }
    function drawBody(body) {
      if(body.gameKind==='wood' || body.gameKind==='shard') drawWood(body);
      else if(body.gameKind==='stone') drawStone(body);
      else if(body.gameKind==='pig') drawPig(body);
      else if(body.gameKind==='bird') drawBird(body);
    }
    function drawQueue() {
      if(birdsUsed===0 || gameResult) return;
      ctx.save();ctx.translate(-cameraX,0);
      const n=Math.max(0,3-birdsUsed-(readyBird?1:0));
      if(n>0) {
        ctx.fillStyle='rgba(48,82,67,.54)';ctx.font=`900 ${9*S}px ui-rounded, sans-serif`;ctx.textAlign='left';ctx.fillText('ON DECK',sling.x+39*S,groundY-56*S);
        for(let i=0;i<n;i++) {
          const bx=sling.x+55*S+i*34*S, by=groundY-21*S, rr=12*S;
          ctx.beginPath();ctx.arc(bx,by,rr,0,TAU);ctx.fillStyle='#df624c';ctx.fill();ctx.strokeStyle='#a8463e';ctx.lineWidth=1.2*S;ctx.stroke();
          ctx.fillStyle='#fff4dd';ctx.beginPath();ctx.arc(bx-3*S,by-2*S,2.2*S,0,TAU);ctx.arc(bx+3*S,by-2*S,2.2*S,0,TAU);ctx.fill();ctx.fillStyle='#263b41';ctx.beginPath();ctx.arc(bx-2*S,by-2*S,1*S,0,TAU);ctx.arc(bx+4*S,by-2*S,1*S,0,TAU);ctx.fill();
        }
      }
      ctx.restore();
    }
    function drawParticles(dt) {
      for(let i=particles.length-1;i>=0;i--) {
        const p=particles[i];p.life-=dt/1000;if(p.life<=0){particles.splice(i,1);continue;}
        p.x+=p.vx*dt/16.67;p.y+=p.vy*dt/16.67;p.vy+=.22*S*dt/16.67;p.vx*=.992;p.rot+=p.spin*dt/16.67;
        const alpha=clamp(p.life/p.max,0,1);
        ctx.save();ctx.globalAlpha=alpha;ctx.translate(p.x,p.y);ctx.rotate(p.rot);
        if(p.type==='feather') {ctx.fillStyle=p.color;ctx.beginPath();ctx.ellipse(0,0,p.size*.68,p.size*.2,0,0,TAU);ctx.fill();ctx.strokeStyle='rgba(255,255,255,.5)';ctx.lineWidth=.6*S;ctx.beginPath();ctx.moveTo(-p.size*.5,0);ctx.lineTo(p.size*.5,0);ctx.stroke();}
        else if(p.type==='dust') {ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(0,0,p.size*(1.1-alpha*.3),0,TAU);ctx.fill();}
        else if(p.type==='pig') {ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(0,0,p.size*(.7+alpha*.25),0,TAU);ctx.fill();}
        else if(p.type==='wood') {ctx.fillStyle=p.color;roundedRect(ctx,-p.size*.8,-p.size*.24,p.size*1.6,p.size*.48,p.size*.12);ctx.fill();}
        else {ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(0,0,p.size*.52,0,TAU);ctx.fill();}
        ctx.restore();
      }
    }
    function drawFloats(dt) {
      for(let i=floats.length-1;i>=0;i--) {
        const f=floats[i];f.life-=dt/1000;f.y-=.44*S*dt/16.67;if(f.life<=0){floats.splice(i,1);continue;}
        ctx.save();ctx.globalAlpha=clamp(f.life/f.max,0,1);ctx.translate(f.x,f.y);ctx.font=`1000 ${16*S}px ui-rounded, sans-serif`;ctx.textAlign='center';ctx.lineJoin='round';ctx.strokeStyle='rgba(63,66,48,.55)';ctx.lineWidth=3*S;ctx.strokeText(f.text,0,0);ctx.fillStyle=f.color;ctx.fillText(f.text,0,0);ctx.restore();
      }
    }
    function draw(dt) {
      ctx.setTransform(DPR,0,0,DPR,0,0);ctx.clearRect(0,0,W,H);
      drawBackground();drawGround();
      ctx.save();
      if(shake>0) { ctx.translate(rand(-shake,shake),rand(-shake*.42,shake*.42)); shake*=.86; if(shake<.2)shake=0; }
      drawSling(true);
      ctx.save();drawTrajectory();ctx.translate(-cameraX,0);
      const all=Composite.allBodies(engine.world).filter(b=>b.gameKind && b.gameKind!=='ground' && !b.isPopped);
      all.sort((a,b)=>a.position.y-b.position.y);
      for(const body of all) drawBody(body);
      drawParticles(dt);drawFloats(dt);
      ctx.restore();
      drawSling(false);drawQueue();ctx.restore();
    }

    function step(dt) {
      now += dt;
      accumulator = Math.min(accumulator + dt, STEP*5);
      while(accumulator >= STEP) { Engine.update(engine,STEP); accumulator-=STEP; }
      const current = readyBird;
      if(autoMode && !gameResult && current && !dragging) {
        if(autoAim > 0) {
          autoAim -= dt;
          if(autoAim <= 0) launch(current);
        } else {
          autoTimer -= dt/1000;
          if(autoTimer <= 0) { autoPull(); autoAim = .72; autoCycle++; }
        }
      }
      for(const shot of shots) {
        if(shot.flightState!=='flying') continue;
        shot.flightAge += dt;
        if(shot.position.y > groundY+145*S || shot.position.x > worldWidth+100 || shot.position.x < -100) {
          Composite.remove(engine.world,shot);shot.flightState='settled';shot.wasRemoved=true;
        } else if((shot.speed < .55 && shot.flightAge > 850) || shot.isSleeping || shot.flightAge > 4400) {
          shot.flightState='settled';
          if(shot.flightAge > 4400) { Sleeping.set(shot,true); Body.setVelocity(shot,{x:0,y:0}); }
          nextBirdWait=.65;
        }
      }
      for(const pig of pigs) if(!pig.isPopped && pig.position.y > groundY+22*S) popPig(pig,pig.position);
      for(const pig of pigs) if(!pig.isPopped && pig.crushSince && now-(pig.lastCrushContact||0)>130) pig.crushSince=0;
      for(let i=debris.length-1;i>=0;i--) {
        debris[i].ttl-=dt;if(debris[i].ttl<=0 || debris[i].position.y>groundY+100*S) { Composite.remove(engine.world,debris[i]);debris.splice(i,1); }
      }
      if(!gameResult && !readyBird && birdsUsed<3 && shots.every(s=>s.flightState==='settled' || s.wasRemoved)) {
        nextBirdWait-=dt/1000;
        if(nextBirdWait<=0) makeNextBird();
      }
      if(!gameResult && birdsUsed>=3 && shots.every(s=>s.flightState==='settled' || s.wasRemoved) && now-lastShotAt>1900) endGame(false);
      const liveShot=shots.find(s=>s.flightState==='flying');
      const maxCam=Math.max(0, Math.min(W*.22, (sling.x+W*.82)-W*.6));
      const targetCam=liveShot ? clamp(liveShot.position.x-W*.5,0,maxCam) : 0;
      cameraX=ease(cameraX,targetCam,.055);
      if(gameResult && autoMode && now-resultAt>2600) { autoCycle=0; makeLevel(); }
    }
    function frame(t) {
      if(!lastFrame)lastFrame=t;const dt=Math.min(45,t-lastFrame);lastFrame=t;
      if(levelReady)step(dt);draw(dt);requestAnimationFrame(frame);
    }

    function localPoint(ev) {
      const rect=canvas.getBoundingClientRect();return { x:(ev.clientX-rect.left)*W/rect.width+cameraX, y:(ev.clientY-rect.top)*H/rect.height };
    }
    canvas.addEventListener('pointerdown', ev => {
      if(gameResult) { if(autoMode) { autoMode=false;updateHud(); } return; }
      const p=localPoint(ev);pointer=p;
      const bird=readyBird;
      const onBird=bird && Math.hypot(p.x-bird.position.x,p.y-bird.position.y) <= bird.gameR*1.8;
      const nearSling=Math.hypot(p.x-sling.x,p.y-sling.y)<126*S;
      if(!onBird && !nearSling) return;
      autoMode=false;autoAim=0;autoTimer=Infinity;updateHud();
      if(!onBird || !bird) return;
      dragging=true;pointerId=ev.pointerId;canvas.setPointerCapture(pointerId);aimPositionFromPointer(p);
      canvas.style.cursor='grabbing';ev.preventDefault();
    });
    canvas.addEventListener('pointermove', ev => {
      if(!dragging || ev.pointerId!==pointerId)return;
      pointer=localPoint(ev);aimPositionFromPointer(pointer);ev.preventDefault();
    });
    function releasePointer(ev) {
      if(!dragging || (ev && ev.pointerId!==pointerId))return;
      if(readyBird) launch(readyBird);
      dragging=false;pointerId=null;canvas.style.cursor='crosshair';
    }
    canvas.addEventListener('pointerup',releasePointer);
    canvas.addEventListener('pointercancel',releasePointer);
    canvas.addEventListener('lostpointercapture', () => { if(dragging)releasePointer(); });
    function restart(manual=true) { autoMode=!manual; makeLevel(); updateHud(); }
    document.getElementById('restartTop').addEventListener('click',()=>restart(true));
    document.getElementById('restartResult').addEventListener('click',()=>restart(true));
    window.addEventListener('resize',resize);
    resize();requestAnimationFrame(frame);
  })();
  </script>
</body>
</html>

Same prompt, other models

2 more
Compare side by side

One email when a new model lands on the board or in the showcase. Unsubscribe anytime.