Showcase

Angry Birds, one level, built by Gemini 4 Argon

Hostedvia DaBenchOpenRouter
Running live in your browserNew tab
Speed
105tok/s
First token included
Written
38.5Ktokens
Time to build
6m 7s
Cost to build
$0.19
Show the code the model wrote
HTML77,058 characters
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Angry Slingshot — Physics Fort</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
<style>
  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    user-select: none;
    -webkit-user-select: none;
  }
  html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #5aa9e6;
    font-family: 'Trebuchet MS', 'Fredoka One', 'Segoe UI', Impact, sans-serif;
    touch-action: none;
  }
  canvas {
    display: block;
    width: 100vw;
    height: 100vh;
    cursor: grab;
  }
  canvas:active {
    cursor: grabbing;
  }

  /* HUD */
  #hud {
    position: fixed;
    top: 16px;
    left: 18px;
    right: 18px;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    pointer-events: none;
    z-index: 10;
  }
  .hud-group {
    display: flex;
    align-items: center;
    gap: 12px;
    pointer-events: auto;
  }
  .btn-icon {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    border: 3px solid #4a2810;
    background: linear-gradient(180deg, #ffcf4b 0%, #f59f00 100%);
    box-shadow: 0 4px 0 #4a2810, inset 0 2px 0 rgba(255,255,255,0.65);
    color: #4a2810;
    font-size: 22px;
    font-weight: 900;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 0.1s ease;
  }
  .btn-icon:hover {
    transform: scale(1.06);
  }
  .btn-icon:active {
    transform: translateY(2px) scale(0.97);
  }
  .score-plaque {
    background: linear-gradient(180deg, #9c5a25 0%, #6e3b14 100%);
    border: 3px solid #3c1e06;
    border-radius: 14px;
    padding: 8px 18px;
    box-shadow: 0 5px 0 #3c1e06, inset 0 2px 0 rgba(255,220,160,0.4);
    color: #fff8e7;
    text-align: right;
    min-width: 155px;
  }
  .score-label {
    font-size: 11px;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: #ffd97d;
    font-weight: 800;
  }
  .score-value {
    font-size: 26px;
    font-weight: 900;
    color: #ffffff;
    text-shadow: 0 2px 0 #3c1e06;
    line-height: 1.05;
  }

  /* Demo Banner */
  #demo-banner {
    position: fixed;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(35, 22, 10, 0.82);
    border: 2.5px solid #ffd452;
    color: #fff6d6;
    padding: 8px 20px;
    border-radius: 999px;
    font-size: 15px;
    font-weight: 800;
    letter-spacing: 0.4px;
    box-shadow: 0 4px 14px rgba(0,0,0,0.3);
    pointer-events: none;
    z-index: 11;
    transition: opacity 0.3s ease, transform 0.3s ease;
    white-space: nowrap;
  }
  #demo-banner.hidden {
    opacity: 0;
    transform: translateX(-50%) translateY(-20px);
  }
  #demo-banner span {
    color: #ffd452;
  }

  /* End-of-Level Modal Overlay */
  #overlay {
    position: fixed;
    inset: 0;
    background: rgba(15, 24, 38, 0.58);
    backdrop-filter: blur(4px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 30;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.35s ease;
  }
  #overlay.visible {
    opacity: 1;
    pointer-events: auto;
  }
  .modal-card {
    background: linear-gradient(180deg, #fff7df 0%, #f4dfb4 100%);
    border: 5px solid #4a2810;
    border-radius: 28px;
    padding: 30px 42px 32px;
    width: min(440px, 90vw);
    text-align: center;
    box-shadow: 0 12px 0 #4a2810, 0 24px 48px rgba(0,0,0,0.45);
    transform: scale(0.85);
    transition: transform 0.35s cubic-bezier(0.22, 1.35, 0.5, 1);
  }
  #overlay.visible .modal-card {
    transform: scale(1);
  }
  .modal-title {
    font-size: 38px;
    font-weight: 900;
    color: #fff;
    -webkit-text-stroke: 2px #4a2810;
    text-shadow: 0 4px 0 #4a2810;
    margin-bottom: 14px;
    letter-spacing: 0.5px;
  }
  .modal-title.win {
    color: #ffcf33;
  }
  .modal-title.lose {
    color: #ff6b57;
  }
  .stars-row {
    display: flex;
    justify-content: center;
    gap: 14px;
    margin: 14px 0 18px;
    height: 76px;
    align-items: center;
  }
  .star-svg {
    width: 66px;
    height: 66px;
    filter: drop-shadow(0 4px 0 #4a2810);
    transform: scale(0);
    transition: transform 0.45s cubic-bezier(0.25, 1.6, 0.5, 1);
  }
  .star-svg.show {
    transform: scale(1);
  }
  .star-svg:nth-child(2).show {
    transform: scale(1.18) translateY(-6px);
  }
  .modal-score-box {
    background: rgba(74, 40, 16, 0.1);
    border: 2px dashed rgba(74, 40, 16, 0.3);
    border-radius: 16px;
    padding: 12px 20px;
    margin-bottom: 24px;
  }
  .modal-score-label {
    font-size: 13px;
    font-weight: 800;
    text-transform: uppercase;
    color: #7c4d25;
    letter-spacing: 1px;
  }
  .modal-score-num {
    font-size: 34px;
    font-weight: 900;
    color: #4a2810;
  }
  .btn-restart {
    background: linear-gradient(180deg, #68d33b 0%, #3da617 100%);
    border: 4px solid #23570b;
    border-radius: 999px;
    padding: 13px 38px;
    font-size: 22px;
    font-weight: 900;
    color: #ffffff;
    text-shadow: 0 2px 0 #23570b;
    box-shadow: 0 6px 0 #23570b, inset 0 3px 0 rgba(255,255,255,0.45);
    cursor: pointer;
    transition: transform 0.1s ease;
  }
  .btn-restart:hover {
    transform: scale(1.05);
  }
  .btn-restart:active {
    transform: translateY(3px) scale(0.98);
    box-shadow: 0 3px 0 #23570b;
  }
</style>
</head>
<body>

<div id="hud">
  <div class="hud-group">
    <button class="btn-icon" id="hud-reset-btn" title="Restart Level">&#x21bb;</button>
  </div>
  <div class="score-plaque">
    <div class="score-label">Score</div>
    <div class="score-value" id="hud-score">0</div>
  </div>
</div>

<div id="demo-banner">
  <span>AUTOPLAY DEMO</span> &mdash; Grab the bird in the slingshot to play!
</div>

<div id="overlay">
  <div class="modal-card">
    <div class="modal-title win" id="modal-title">Level cleared</div>
    <div class="stars-row" id="modal-stars">
      <svg class="star-svg" id="star-1" viewBox="0 0 64 64">
        <polygon points="32,4 40.5,21.5 60,24.2 45.8,37.8 49.3,57 32,47.8 14.7,57 18.2,37.8 4,24.2 23.5,21.5" stroke="#4a2810" stroke-width="4" stroke-linejoin="round"/>
      </svg>
      <svg class="star-svg" id="star-2" viewBox="0 0 64 64">
        <polygon points="32,4 40.5,21.5 60,24.2 45.8,37.8 49.3,57 32,47.8 14.7,57 18.2,37.8 4,24.2 23.5,21.5" stroke="#4a2810" stroke-width="4" stroke-linejoin="round"/>
      </svg>
      <svg class="star-svg" id="star-3" viewBox="0 0 64 64">
        <polygon points="32,4 40.5,21.5 60,24.2 45.8,37.8 49.3,57 32,47.8 14.7,57 18.2,37.8 4,24.2 23.5,21.5" stroke="#4a2810" stroke-width="4" stroke-linejoin="round"/>
      </svg>
    </div>
    <div class="modal-score-box">
      <div class="modal-score-label">Final Score</div>
      <div class="modal-score-num" id="modal-score">0</div>
    </div>
    <button class="btn-restart" id="modal-restart-btn">Restart</button>
  </div>
</div>

<canvas id="game"></canvas>

<script>
(() => {
  const { Engine, World, Bodies, Body, Vector, Events, Sleeping, Composite } = Matter;

  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const hudScoreEl = document.getElementById('hud-score');
  const hudResetBtn = document.getElementById('hud-reset-btn');
  const demoBannerEl = document.getElementById('demo-banner');
  const overlayEl = document.getElementById('overlay');
  const modalTitleEl = document.getElementById('modal-title');
  const modalScoreEl = document.getElementById('modal-score');
  const modalRestartBtn = document.getElementById('modal-restart-btn');
  const starEls = [
    document.getElementById('star-1'),
    document.getElementById('star-2'),
    document.getElementById('star-3')
  ];

  // =========================================================================
  // SOUND SYNTHESIS (Web Audio API - No external files)
  // =========================================================================
  let audioCtx = null;
  function ensureAudio() {
    if (!audioCtx) {
      const AC = window.AudioContext || window.webkitAudioContext;
      if (AC) audioCtx = new AC();
    }
    if (audioCtx && audioCtx.state === 'suspended') {
      audioCtx.resume().catch(() => {});
    }
  }

  function playSound(type, intensity = 1) {
    if (!audioCtx || audioCtx.state !== 'running') return;
    const now = audioCtx.currentTime;
    try {
      if (type === 'launch') {
        // Slingshot twang + bird battle cry
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(140, now);
        osc.frequency.exponentialRampToValueAtTime(520, now + 0.14);
        osc.frequency.exponentialRampToValueAtTime(280, now + 0.32);
        gain.gain.setValueAtTime(0.22, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.34);
        osc.connect(gain).connect(audioCtx.destination);
        osc.start(now);
        osc.stop(now + 0.35);
      } else if (type === 'wood') {
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = 'square';
        osc.frequency.setValueAtTime(160 + Math.random() * 50, now);
        osc.frequency.exponentialRampToValueAtTime(55, now + 0.09);
        gain.gain.setValueAtTime(Math.min(0.24, 0.08 * intensity), now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
        osc.connect(gain).connect(audioCtx.destination);
        osc.start(now);
        osc.stop(now + 0.11);
      } else if (type === 'stone') {
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = 'sine';
        osc.frequency.setValueAtTime(110 + Math.random() * 30, now);
        osc.frequency.exponentialRampToValueAtTime(42, now + 0.12);
        gain.gain.setValueAtTime(Math.min(0.28, 0.1 * intensity), now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.13);
        osc.connect(gain).connect(audioCtx.destination);
        osc.start(now);
        osc.stop(now + 0.14);
      } else if (type === 'pop') {
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = 'sine';
        osc.frequency.setValueAtTime(260, now);
        osc.frequency.exponentialRampToValueAtTime(680, now + 0.08);
        osc.frequency.exponentialRampToValueAtTime(180, now + 0.18);
        gain.gain.setValueAtTime(0.28, now);
        gain.gain.exponentialRampToValueAtTime(0.001, now + 0.19);
        osc.connect(gain).connect(audioCtx.destination);
        osc.start(now);
        osc.stop(now + 0.2);
      } else if (type === 'win') {
        [523.25, 659.25, 783.99, 1046.5].forEach((freq, idx) => {
          const osc = audioCtx.createOscillator();
          const gain = audioCtx.createGain();
          osc.type = 'triangle';
          osc.frequency.setValueAtTime(freq, now + idx * 0.11);
          gain.gain.setValueAtTime(0.18, now + idx * 0.11);
          gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.11 + 0.35);
          osc.connect(gain).connect(audioCtx.destination);
          osc.start(now + idx * 0.11);
          osc.stop(now + idx * 0.11 + 0.36);
        });
      } else if (type === 'lose') {
        [320, 290, 260, 210].forEach((freq, idx) => {
          const osc = audioCtx.createOscillator();
          const gain = audioCtx.createGain();
          osc.type = 'sawtooth';
          osc.frequency.setValueAtTime(freq, now + idx * 0.16);
          gain.gain.setValueAtTime(0.12, now + idx * 0.16);
          gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.16 + 0.28);
          osc.connect(gain).connect(audioCtx.destination);
          osc.start(now + idx * 0.16);
          osc.stop(now + idx * 0.16 + 0.3);
        });
      }
    } catch (e) {}
  }

  // =========================================================================
  // WORLD CONSTANTS & GEOMETRY
  // =========================================================================
  const GROUND_Y = 600;
  const SLING_X = 285;
  const SLING_Y = GROUND_Y - 128; // 472 (fork pouch center)
  const MAX_PULL = 132;
  const MIN_PULL = 18;
  const LAUNCH_POWER = 0.228; // maps pull vector to Matter.js velocity
  const BIRD_RADIUS = 22;
  const FIXED_DT = 1000 / 60;

  // Camera state in virtual world space
  // Default view covers x = 55 .. 1445 (width 1390), height ~ 740
  const camera = {
    x: 750,
    y: 355,
    viewWidth: 1410,
    targetX: 750,
    targetY: 355,
    targetViewWidth: 1410,
    scale: 1,
    offsetX: 0,
    offsetY: 0
  };

  let dpr = 1;
  let screenW = window.innerWidth;
  let screenH = window.innerHeight;

  function resizeCanvas() {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    screenW = window.innerWidth;
    screenH = window.innerHeight;
    canvas.width = Math.floor(screenW * dpr);
    canvas.height = Math.floor(screenH * dpr);
  }
  window.addEventListener('resize', resizeCanvas);
  resizeCanvas();

  function updateCameraTransform() {
    camera.x += (camera.targetX - camera.x) * 0.08;
    camera.y += (camera.targetY - camera.y) * 0.08;
    camera.viewWidth += (camera.targetViewWidth - camera.viewWidth) * 0.08;

    const scaleX = screenW / camera.viewWidth;
    const scaleY = screenH / 730;
    camera.scale = Math.min(scaleX, scaleY);
    camera.offsetX = screenW * 0.5 - camera.x * camera.scale;
    camera.offsetY = screenH * 0.54 - camera.y * camera.scale;

    // Clamp so ground is always nicely near the bottom of the screen
    const groundScreenY = GROUND_Y * camera.scale + camera.offsetY;
    const minGroundScreenY = screenH * 0.80;
    const maxGroundScreenY = screenH * 0.88;
    if (groundScreenY < minGroundScreenY) {
      camera.offsetY += (minGroundScreenY - groundScreenY);
    } else if (groundScreenY > maxGroundScreenY) {
      camera.offsetY += (maxGroundScreenY - groundScreenY);
    }
  }

  function screenToWorld(sx, sy) {
    return {
      x: (sx - camera.offsetX) / camera.scale,
      y: (sy - camera.offsetY) / camera.scale
    };
  }

  // =========================================================================
  // DECORATIVE BACKGROUND ELEMENTS
  // =========================================================================
  const clouds = [
    { x: 180, y: 110, s: 1.1, speed: 0.14 },
    { x: 520, y: 75,  s: 0.85, speed: 0.1 },
    { x: 920, y: 125, s: 1.25, speed: 0.16 },
    { x: 1340, y: 85, s: 0.95, speed: 0.12 },
    { x: 1700, y: 115, s: 1.05, speed: 0.15 }
  ];

  const grassTufts = [];
  for (let gx = -200; gx < 1900; gx += 26) {
    grassTufts.push({
      x: gx + (Math.sin(gx * 0.7) * 8),
      h: 9 + (Math.abs(Math.cos(gx * 1.3)) * 8),
      lean: Math.sin(gx * 0.3) * 4
    });
  }

  // =========================================================================
  // GAME & PHYSICS STATE
  // =========================================================================
  let engine = null;
  let world = null;
  let groundBody = null;

  let fortBlocks = [];
  let pigs = [];
  let activeBird = null;
  let launchedBirds = [];
  let waitingBirds = []; // { x, y, hopPhase, hopOffset, targetX }

  // Slingshot interaction state
  let slingState = 'aiming'; // 'aiming' | 'flying' | 'settling' | 'gameover'
  let isDragging = false;
  let pullPos = { x: SLING_X, y: SLING_Y };
  let fortAwake = false;
  let initialFortPose = new Map();

  // Score & progression
  let score = 0;
  let displayedScore = 0;
  let birdsRemaining = 3; // includes the one currently in the slingshot
  let settleTimer = 0;
  let flightTimer = 0;
  let birdHasHit = false;

  // Particles & trails
  let flightTrail = [];
  let particles = [];
  let floatingScores = [];

  // Demo mode state
  let isDemo = true;
  let demoTimer = 0;
  let demoShotIndex = 0;
  const demoShots = [
    // Shot 1: Hits the lower-left/center junction for a dramatic collapse
    { dx: -116, dy: 52 },
    // Shot 2: High arc into the upper center/right tower
    { dx: -124, dy: 66 },
    // Shot 3: Direct strike at the right bay
    { dx: -128, dy: 58 }
  ];

  // =========================================================================
  // FORT & LEVEL INITIALIZATION
  // =========================================================================
  function createWoodGrainSeed(w, h) {
    const lines = [];
    const count = Math.max(2, Math.floor(Math.min(w, h) / 7));
    for (let i = 0; i < count; i++) {
      lines.push({
        offset: ((i + 0.5) / count - 0.5) * 0.78,
        wave: (Math.random() - 0.5) * 3
      });
    }
    return {
      lines,
      knot: Math.random() > 0.35 ? {
        u: (Math.random() - 0.5) * 0.5,
        v: (Math.random() - 0.5) * 0.4,
        r: 2.5 + Math.random() * 2.5
      } : null
    };
  }

  function addBlock(x, y, w, h, material) {
    const isStone = material === 'stone';
    const maxHp = isStone ? 165 : 58;
    const body = Bodies.rectangle(x, y, w, h, {
      density: isStone ? 0.0042 : 0.0018,
      friction: isStone ? 0.78 : 0.68,
      frictionStatic: isStone ? 0.92 : 0.85,
      restitution: isStone ? 0.06 : 0.10,
      frictionAir: 0.006,
      slop: 0.01
    });
    body.gameType = 'block';
    body.material = material;
    body.w = w;
    body.h = h;
    body.hp = maxHp;
    body.maxHp = maxHp;
    body.woodData = !isStone ? createWoodGrainSeed(w, h) : null;
    body.preVel = { x: 0, y: 0 };

    Sleeping.set(body, true);
    initialFortPose.set(body.id, { x, y, angle: 0 });
    fortBlocks.push(body);
    World.add(world, body);
    return body;
  }

  function addPig(x, y, r, role = 'minion') {
    const maxHp = role === 'boss' ? 34 : 24;
    const body = Bodies.circle(x, y, r, {
      density: 0.0016,
      friction: 0.55,
      frictionStatic: 0.75,
      restitution: 0.22,
      frictionAir: 0.006,
      slop: 0.01
    });
    body.gameType = 'pig';
    body.r = r;
    body.role = role;
    body.hp = maxHp;
    body.maxHp = maxHp;
    body.blinkTimer = Math.random() * 180;
    body.scaredTimer = 0;
    body.preVel = { x: 0, y: 0 };

    Sleeping.set(body, true);
    initialFortPose.set(body.id, { x, y, angle: 0 });
    pigs.push(body);
    World.add(world, body);
    return body;
  }

  function buildFort() {
    fortBlocks = [];
    pigs = [];
    initialFortPose.clear();
    fortAwake = false;

    // Exact flush vertical stacking starting from GROUND_Y = 600:
    // 1. Stone foundation plinths (y: 600 -> 564, h = 36, center = 582)
    addBlock(975,  582, 132, 36, 'stone');
    addBlock(1120, 582, 150, 36, 'stone');
    addBlock(1265, 582, 132, 36, 'stone');

    // 2. First floor vertical pillars (y: 564 -> 444, h = 120, center = 504)
    addBlock(926,  504, 22, 120, 'wood');
    addBlock(1024, 504, 22, 120, 'wood');

    addBlock(1062, 504, 24, 120, 'stone');
    addBlock(1120, 504, 22, 120, 'wood'); // central column supporting upper tower
    addBlock(1178, 504, 24, 120, 'stone');

    addBlock(1216, 504, 22, 120, 'wood');
    addBlock(1314, 504, 22, 120, 'wood');

    // 3. First floor ceiling planks (y: 444 -> 422, h = 22, center = 433)
    addBlock(975,  433, 132, 22, 'wood');
    addBlock(1120, 433, 148, 22, 'wood');
    addBlock(1265, 433, 132, 22, 'wood');

    // 4. Second floor side weights & braces (resting on y = 422)
    // Left roof blocks (y: 422 -> 382, h = 40, center = 402)
    addBlock(952,  402, 38, 40, 'stone');
    addBlock(1002, 402, 28, 40, 'wood');
    // Left pediment plank (y: 382 -> 364, h = 18, center = 373)
    addBlock(977,  373, 94, 18, 'wood');

    // Right roof blocks (y: 422 -> 382, h = 40, center = 402)
    addBlock(1238, 402, 28, 40, 'wood');
    addBlock(1288, 402, 38, 40, 'stone');
    // Right pediment plank (y: 382 -> 364, h = 18, center = 373)
    addBlock(1263, 373, 94, 18, 'wood');

    // 5. Center Tower Second Floor (y: 422 -> 312, h = 110, center = 367)
    addBlock(1066, 367, 22, 110, 'wood');
    addBlock(1174, 367, 22, 110, 'wood');

    // 6. Center Tower Roof Plank (y: 312 -> 290, h = 22, center = 301)
    addBlock(1120, 301, 144, 22, 'wood');

    // 7. Heavy Stone Cap Blocks on Center Roof (y: 290 -> 252, h = 38, center = 271)
    addBlock(1082, 271, 38, 38, 'stone');
    addBlock(1158, 271, 38, 38, 'stone');

    // 8. Top Finial Wood Beam (y: 252 -> 232, h = 20, center = 242)
    addBlock(1120, 242, 122, 20, 'wood');

    // 9. Three Pigs Inside the Fort!
    // Pig 1: Left Bay (sits on y = 564, r = 24 -> center y = 540)
    addPig(975, 540, 24, 'minion');
    // Pig 2: Right Bay (sits on y = 564, r = 24 -> center y = 540)
    addPig(1265, 540, 24, 'minion');
    // Pig 3: Boss Pig in Center Second-Floor Throne Room (sits on y = 422, r = 29 -> center y = 393)
    addPig(1120, 393, 29, 'boss');
  }

  function wakeFort() {
    if (fortAwake) return;
    fortAwake = true;
    for (const b of fortBlocks) {
      Sleeping.set(b, false);
    }
    for (const p of pigs) {
      Sleeping.set(p, false);
    }
  }

  function enforceFortStillnessBeforeHit() {
    if (fortAwake) return;
    for (const b of fortBlocks) {
      const pose = initialFortPose.get(b.id);
      if (pose) {
        Body.setPosition(b, { x: pose.x, y: pose.y });
        Body.setAngle(b, pose.angle);
        Body.setVelocity(b, { x: 0, y: 0 });
        Body.setAngularVelocity(b, 0);
        Sleeping.set(b, true);
      }
    }
    for (const p of pigs) {
      const pose = initialFortPose.get(p.id);
      if (pose) {
        Body.setPosition(p, { x: pose.x, y: pose.y });
        Body.setAngle(p, pose.angle);
        Body.setVelocity(p, { x: 0, y: 0 });
        Body.setAngularVelocity(p, 0);
        Sleeping.set(p, true);
      }
    }

    // Check if the flying bird is about to touch any fort block or pig
    if (activeBird && slingState === 'flying') {
      const bx = activeBird.position.x + activeBird.velocity.x;
      const by = activeBird.position.y + activeBird.velocity.y;
      const br = BIRD_RADIUS + 8;

      for (const b of fortBlocks) {
        const dx = Math.abs(bx - b.position.x);
        const dy = Math.abs(by - b.position.y);
        if (dx <= b.w * 0.5 + br && dy <= b.h * 0.5 + br) {
          wakeFort();
          return;
        }
      }
      for (const p of pigs) {
        const dist = Math.hypot(bx - p.position.x, by - p.position.y);
        if (dist <= p.r + br) {
          wakeFort();
          return;
        }
      }
    }
  }

  // =========================================================================
  // LEVEL RESET & SETUP
  // =========================================================================
  function initLevel() {
    overlayEl.classList.remove('visible');
    starEls.forEach(el => el.classList.remove('show'));

    engine = Engine.create({
      enableSleeping: true,
      positionIterations: 10,
      velocityIterations: 8
    });
    world = engine.world;
    engine.gravity.y = 1;
    engine.gravity.scale = 0.001;

    // Ground & invisible left barrier
    groundBody = Bodies.rectangle(800, GROUND_Y + 60, 4000, 120, {
      isStatic: true,
      friction: 0.9,
      restitution: 0.1
    });
    groundBody.gameType = 'ground';
    World.add(world, groundBody);

    buildFort();

    score = 0;
    displayedScore = 0;
    hudScoreEl.textContent = '0';

    birdsRemaining = 3;
    launchedBirds = [];
    activeBird = null;
    pullPos = { x: SLING_X, y: SLING_Y };
    isDragging = false;
    slingState = 'aiming';
    settleTimer = 0;
    flightTimer = 0;
    birdHasHit = false;
    flightTrail = [];
    particles = [];
    floatingScores = [];
    demoTimer = 0;

    // Two waiting birds on the ground behind the slingshot
    waitingBirds = [
      { x: 185, y: GROUND_Y - BIRD_RADIUS, phase: 0, jumping: false, jumpT: 0, startX: 185, startY: GROUND_Y - BIRD_RADIUS },
      { x: 125, y: GROUND_Y - BIRD_RADIUS, phase: 2.1, jumping: false, jumpT: 0, startX: 125, startY: GROUND_Y - BIRD_RADIUS }
    ];

    camera.targetX = 750;
    camera.targetY = 355;
    camera.targetViewWidth = 1410;

    setupCollisionHandling();
  }

  // =========================================================================
  // COLLISION & DAMAGE HANDLING
  // =========================================================================
  function setupCollisionHandling() {
    Events.on(engine, 'beforeUpdate', () => {
      enforceFortStillnessBeforeHit();

      const allDynamic = [...fortBlocks, ...pigs, ...launchedBirds];
      for (const b of allDynamic) {
        b.preVel = { x: b.velocity.x, y: b.velocity.y };
      }
    });

    Events.on(engine, 'collisionStart', (event) => {
      const pairs = event.pairs;
      const blocksToRemove = new Set();
      const pigsToPop = new Set();

      for (const pair of pairs) {
        const A = pair.bodyA;
        const B = pair.bodyB;

        const involvesBird = (A.gameType === 'bird' || B.gameType === 'bird');
        const involvesFort = (A.gameType === 'block' || B.gameType === 'block' || A.gameType === 'pig' || B.gameType === 'pig');

        if (involvesBird && involvesFort && !fortAwake) {
          wakeFort();
        }

        if (involvesBird) {
          const bird = A.gameType === 'bird' ? A : B;
          if (!bird.hasImpacted) {
            bird.hasImpacted = true;
            bird.frictionAir = 0.008;
            birdHasHit = true;
            spawnFeathers(bird.position.x, bird.position.y, 7);
          }
        }

        const vA = A.preVel || A.velocity || { x: 0, y: 0 };
        const vB = B.preVel || B.velocity || { x: 0, y: 0 };
        const relVx = vA.x - vB.x;
        const relVy = vA.y - vB.y;
        const relSpeed = Math.hypot(relVx, relVy);

        if (relSpeed < 1.35) continue;

        const contact = (pair.activeContacts && pair.activeContacts[0] && pair.activeContacts[0].vertex)
          ? pair.activeContacts[0].vertex
          : { x: (A.position.x + B.position.x) * 0.5, y: (A.position.y + B.position.y) * 0.5 };

        if (relSpeed > 2.2) {
          spawnDust(contact.x, contact.y, Math.min(6, Math.floor(relSpeed)));
        }

        // Apply damage to blocks & pigs
        applyImpactDamage(A, B, relSpeed, contact, blocksToRemove, pigsToPop);
        applyImpactDamage(B, A, relSpeed, contact, blocksToRemove, pigsToPop);
      }

      // Process destroyed blocks
      for (const block of blocksToRemove) {
        destroyBlock(block);
      }
      // Process popped pigs
      for (const pig of pigsToPop) {
        popPig(pig);
      }
    });
  }

  function applyImpactDamage(victim, hitter, relSpeed, contact, blocksToRemove, pigsToPop) {
    if (!victim || (victim.gameType !== 'block' && victim.gameType !== 'pig')) return;

    // Calculate mass factor of the hitter
    let massFactor = 1.0;
    if (hitter.isStatic) {
      massFactor = 1.35; // hitting the ground hard
    } else if (hitter.gameType === 'bird') {
      massFactor = 2.8;  // birds pack a strong punch
    } else if (hitter.material === 'stone') {
      massFactor = 2.2;  // heavy falling stone crushes wood & pigs
    } else if (hitter.material === 'wood') {
      massFactor = 1.15;
    }

    // Scare nearby pigs
    for (const p of pigs) {
      if (Math.hypot(p.position.x - contact.x, p.position.y - contact.y) < 180) {
        p.scaredTimer = 70;
      }
    }

    if (victim.gameType === 'block') {
      const minThreshold = victim.material === 'stone' ? 2.4 : 1.7;
      if (relSpeed < minThreshold) return;

      const dmg = (relSpeed - minThreshold + 0.5) * 9.5 * massFactor;
      victim.hp -= dmg;

      playSound(victim.material, relSpeed * 0.3);

      if (dmg > 10) {
        spawnSplinters(contact.x, contact.y, victim.material, Math.min(5, Math.ceil(dmg / 12)));
      }

      if (victim.hp <= 0) {
        blocksToRemove.add(victim);
        // If the bird smashed through a wooden block, preserve a portion of the bird's pre-impact momentum!
        if (hitter.gameType === 'bird' && victim.material === 'wood' && hitter.preVel) {
          Body.setVelocity(hitter, {
            x: hitter.preVel.x * 0.68,
            y: hitter.preVel.y * 0.68
          });
        }
      }
    } else if (victim.gameType === 'pig') {
      const minThreshold = 1.5;
      if (relSpeed < minThreshold) return;

      const dmg = (relSpeed - minThreshold + 0.6) * 8.5 * massFactor;
      victim.hp -= dmg;
      victim.scaredTimer = 90;

      if (victim.hp <= 0) {
        pigsToPop.add(victim);
      }
    }
  }

  function destroyBlock(block) {
    const idx = fortBlocks.indexOf(block);
    if (idx === -1) return;
    fortBlocks.splice(idx, 1);
    World.remove(world, block);

    const pts = block.material === 'stone' ? 800 : 500;
    addScore(pts, block.position.x, block.position.y, block.material === 'stone' ? '#d9e5f0' : '#ffcf56');
    spawnSplinters(block.position.x, block.position.y, block.material, 11);
    spawnDust(block.position.x, block.position.y, 7);
    playSound(block.material, 1.5);
  }

  function popPig(pig) {
    const idx = pigs.indexOf(pig);
    if (idx === -1) return;
    pigs.splice(idx, 1);
    World.remove(world, pig);

    addScore(5000, pig.position.x, pig.position.y - 10, '#80ff52', true);
    spawnPigPopParticles(pig.position.x, pig.position.y, pig.r);
    playSound('pop', 1.5);
  }

  function addScore(amount, x, y, color = '#ffdd44', big = false) {
    score += amount;
    floatingScores.push({
      text: `+${amount}`,
      x,
      y,
      vy: -1.6,
      life: 1.0,
      color,
      big
    });
  }

  // =========================================================================
  // PARTICLE SYSTEM (Feathers, Dust, Splinters, Pig Pops)
  // =========================================================================
  function spawnFeathers(x, y, count = 6) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const spd = 1.5 + Math.random() * 4.5;
      particles.push({
        type: 'feather',
        x,
        y,
        vx: Math.cos(angle) * spd,
        vy: Math.sin(angle) * spd - 1.5,
        rot: Math.random() * Math.PI * 2,
        vRot: (Math.random() - 0.5) * 0.16,
        size: 7 + Math.random() * 5,
        color: Math.random() > 0.3 ? '#e52521' : '#b31217',
        life: 1.0,
        decay: 0.014 + Math.random() * 0.01
      });
    }
  }

  function spawnDust(x, y, count = 5) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const spd = 0.6 + Math.random() * 2.6;
      particles.push({
        type: 'dust',
        x: x + (Math.random() - 0.5) * 14,
        y: y + (Math.random() - 0.5) * 14,
        vx: Math.cos(angle) * spd,
        vy: Math.sin(angle) * spd - 0.5,
        r: 7 + Math.random() * 10,
        color: 'rgba(235, 224, 202, 0.7)',
        life: 1.0,
        decay: 0.028 + Math.random() * 0.018
      });
    }
  }

  function spawnSplinters(x, y, material, count = 8) {
    const isStone = material === 'stone';
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const spd = 2 + Math.random() * 6.5;
      particles.push({
        type: 'shard',
        x: x + (Math.random() - 0.5) * 18,
        y: y + (Math.random() - 0.5) * 18,
        vx: Math.cos(angle) * spd,
        vy: Math.sin(angle) * spd - 2.0,
        w: isStone ? (6 + Math.random() * 7) : (10 + Math.random() * 12),
        h: isStone ? (6 + Math.random() * 6) : (3.5 + Math.random() * 3.5),
        rot: Math.random() * Math.PI * 2,
        vRot: (Math.random() - 0.5) * 0.28,
        color: isStone
          ? (Math.random() > 0.5 ? '#8d99a6' : '#6c7682')
          : (Math.random() > 0.5 ? '#d88b38' : '#a8621c'),
        life: 1.0,
        decay: 0.02 + Math.random() * 0.015
      });
    }
  }

  function spawnPigPopParticles(x, y, r) {
    // Big cartoon cloud ring
    for (let i = 0; i < 14; i++) {
      const angle = (i / 14) * Math.PI * 2;
      const spd = 2.2 + Math.random() * 3.2;
      particles.push({
        type: 'dust',
        x: x + Math.cos(angle) * (r * 0.4),
        y: y + Math.sin(angle) * (r * 0.4),
        vx: Math.cos(angle) * spd,
        vy: Math.sin(angle) * spd,
        r: 12 + Math.random() * 14,
        color: i % 3 === 0 ? 'rgba(165, 242, 110, 0.85)' : 'rgba(255, 255, 255, 0.9)',
        life: 1.0,
        decay: 0.025 + Math.random() * 0.015
      });
    }
    // Little stars
    for (let i = 0; i < 8; i++) {
      const angle = Math.random() * Math.PI * 2;
      const spd = 3 + Math.random() * 5;
      particles.push({
        type: 'star',
        x,
        y,
        vx: Math.cos(angle) * spd,
        vy: Math.sin(angle) * spd - 1.5,
        rot: Math.random() * Math.PI * 2,
        vRot: (Math.random() - 0.5) * 0.2,
        size: 7 + Math.random() * 5,
        life: 1.0,
        decay: 0.022
      });
    }
  }

  // =========================================================================
  // LAUNCH & TRAJECTORY PREDICTION
  // =========================================================================
  function getLaunchVelocity(pullX, pullY) {
    const dx = SLING_X - pullX;
    const dy = SLING_Y - pullY;
    return {
      x: dx * LAUNCH_POWER,
      y: dy * LAUNCH_POWER
    };
  }

  // Exact step-by-step Verlet simulation matching Matter.js Body.update
  function computeTrajectoryPoints(pullX, pullY, numSteps = 75) {
    const v0 = getLaunchVelocity(pullX, pullY);
    const dtSq = FIXED_DT * FIXED_DT;
    const gravAccY = engine.gravity.y * engine.gravity.scale * dtSq;
    const airFactor = 1 - 0.0012; // matches bird frictionAir in free flight

    let x = pullX;
    let y = pullY;
    let vx = v0.x;
    let vy = v0.y;

    const pts = [];
    for (let step = 1; step <= numSteps; step++) {
      vx = vx * airFactor;
      vy = vy * airFactor + gravAccY;
      x += vx;
      y += vy;
      if (step % 3 === 0) {
        pts.push({ x, y });
      }
      if (y > GROUND_Y) break;
    }
    return pts;
  }

  function fireBird(pullX, pullY) {
    const v0 = getLaunchVelocity(pullX, pullY);
    const bird = Bodies.circle(pullX, pullY, BIRD_RADIUS, {
      density: 0.0046,
      friction: 0.5,
      restitution: 0.38,
      frictionAir: 0.0012,
      slop: 0.01
    });
    bird.gameType = 'bird';
    bird.hasImpacted = false;
    bird.preVel = { x: v0.x, y: v0.y };

    World.add(world, bird);
    Body.setVelocity(bird, v0);

    activeBird = bird;
    launchedBirds.push(bird);
    birdsRemaining--;
    slingState = 'flying';
    flightTimer = 0;
    settleTimer = 0;
    birdHasHit = false;
    flightTrail = [{ x: pullX, y: pullY, r: 4.5 }];

    spawnFeathers(pullX, pullY, 5);
    playSound('launch');
  }

  // =========================================================================
  // PLAYER INPUT (GRAB SLINGSHOT & TAKE OVER FROM DEMO)
  // =========================================================================
  function clampPull(wx, wy) {
    let dx = wx - SLING_X;
    let dy = wy - SLING_Y;
    const dist = Math.hypot(dx, dy);
    if (dist > MAX_PULL) {
      dx = (dx / dist) * MAX_PULL;
      dy = (dy / dist) * MAX_PULL;
    }
    // Prevent dragging the bird into the ground below GROUND_Y - BIRD_RADIUS
    const clampedY = Math.min(SLING_Y + dy, GROUND_Y - BIRD_RADIUS - 4);
    return {
      x: SLING_X + dx,
      y: clampedY
    };
  }

  function enterPlayerModeAndGrab(worldPt) {
    ensureAudio();
    if (isDemo) {
      isDemo = false;
      demoBannerEl.classList.add('hidden');
      // If the demo already launched a bird or modified the level, reset cleanly!
      if (slingState !== 'aiming' || birdsRemaining < 3 || fortAwake) {
        initLevel();
        // Snap camera immediately for responsive player control
        camera.x = camera.targetX;
        camera.y = camera.targetY;
        camera.viewWidth = camera.targetViewWidth;
        updateCameraTransform();
      }
    }
    if (slingState === 'aiming' && birdsRemaining > 0) {
      isDragging = true;
      pullPos = clampPull(worldPt.x, worldPt.y);
    }
  }

  function onPointerDown(e) {
    if (overlayEl.classList.contains('visible')) return;
    ensureAudio();

    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;
    const worldPt = screenToWorld(clientX, clientY);

    // Check if the player grabbed near the slingshot / bird (generous radius in world or screen left)
    const distToPouch = Math.hypot(worldPt.x - pullPos.x, worldPt.y - pullPos.y);
    const distToSling = Math.hypot(worldPt.x - SLING_X, worldPt.y - SLING_Y);
    const isLeftSideGrab = isDemo && clientX < screenW * 0.42;

    if (distToPouch < 85 || distToSling < 110 || isLeftSideGrab) {
      enterPlayerModeAndGrab(worldPt);
    }
  }

  function onPointerMove(e) {
    if (!isDragging) return;
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;
    const worldPt = screenToWorld(clientX, clientY);
    pullPos = clampPull(worldPt.x, worldPt.y);
  }

  function onPointerUp() {
    if (!isDragging) return;
    isDragging = false;
    const pullDist = Math.hypot(pullPos.x - SLING_X, pullPos.y - SLING_Y);
    if (pullDist >= MIN_PULL && slingState === 'aiming') {
      fireBird(pullPos.x, pullPos.y);
      pullPos = { x: SLING_X, y: SLING_Y };
    } else {
      pullPos = { x: SLING_X, y: SLING_Y };
    }
  }

  canvas.addEventListener('mousedown', onPointerDown);
  window.addEventListener('mousemove', onPointerMove);
  window.addEventListener('mouseup', onPointerUp);

  canvas.addEventListener('touchstart', (e) => { e.preventDefault(); onPointerDown(e); }, { passive: false });
  window.addEventListener('touchmove', (e) => { if (isDragging) e.preventDefault(); onPointerMove(e); }, { passive: false });
  window.addEventListener('touchend', onPointerUp);

  hudResetBtn.addEventListener('click', () => {
    ensureAudio();
    isDemo = false;
    demoBannerEl.classList.add('hidden');
    initLevel();
  });

  modalRestartBtn.addEventListener('click', () => {
    ensureAudio();
    isDemo = false;
    demoBannerEl.classList.add('hidden');
    initLevel();
  });

  // =========================================================================
  // END-OF-LEVEL MODAL ("Level cleared" / "Try again")
  // =========================================================================
  function showEndScreen(cleared) {
    slingState = 'gameover';

    if (isDemo) {
      // In demo mode, pause briefly to let the viewer admire the collapse, then loop!
      setTimeout(() => {
        if (isDemo) {
          demoShotIndex = 0;
          initLevel();
        }
      }, 1700);
      return;
    }

    if (cleared) {
      playSound('win');
      modalTitleEl.textContent = 'Level cleared';
      modalTitleEl.className = 'modal-title win';

      // Star calculation:
      // 3 stars: >= 28,000 (e.g. cleared with 1-2 birds + good fort destruction)
      // 2 stars: >= 20,000
      // 1 star: cleared all 3 pigs
      let stars = 1;
      if (score >= 28000 || birdsRemaining >= 2) stars = 3;
      else if (score >= 20000 || birdsRemaining >= 1) stars = 2;

      starEls.forEach((star, i) => {
        const poly = star.querySelector('polygon');
        if (i < stars) {
          poly.setAttribute('fill', '#ffd426');
          setTimeout(() => star.classList.add('show'), 220 + i * 240);
        } else {
          poly.setAttribute('fill', '#b8ab93');
          setTimeout(() => star.classList.add('show'), 220 + i * 240);
        }
      });
    } else {
      playSound('lose');
      modalTitleEl.textContent = 'Try again';
      modalTitleEl.className = 'modal-title lose';
      starEls.forEach((star) => {
        const poly = star.querySelector('polygon');
        poly.setAttribute('fill', '#b8ab93');
        star.classList.add('show');
      });
    }

    modalScoreEl.textContent = score.toLocaleString();
    overlayEl.classList.add('visible');
  }

  // =========================================================================
  // GAME UPDATE LOOP
  // =========================================================================
  function isWorldCalm() {
    for (const b of fortBlocks) {
      if (!b.isSleeping && (Math.hypot(b.velocity.x, b.velocity.y) > 0.35 || Math.abs(b.angularVelocity) > 0.02)) {
        return false;
      }
    }
    for (const p of pigs) {
      if (!p.isSleeping && Math.hypot(p.velocity.x, p.velocity.y) > 0.35) {
        return false;
      }
    }
    if (activeBird) {
      const bv = Math.hypot(activeBird.velocity.x, activeBird.velocity.y);
      if (bv > 0.65 && activeBird.position.x < 1550 && activeBird.position.x > -100) {
        return false;
      }
    }
    return true;
  }

  function advanceToNextTurnOrEnd() {
    // Did we clear all three pigs?
    if (pigs.length === 0) {
      // Award unused bird bonuses!
      if (birdsRemaining > 0) {
        for (let i = 0; i < waitingBirds.length; i++) {
          const wb = waitingBirds[i];
          addScore(10000, wb.x, wb.y - 25, '#ffd93b', true);
        }
        // Also if a bird was still in the slingshot (unlikely here, but safe)
        const extraInSling = birdsRemaining - waitingBirds.length;
        for (let i = 0; i < extraInSling; i++) {
          addScore(10000, SLING_X, SLING_Y - 30, '#ffd93b', true);
        }
        birdsRemaining = 0;
      }
      showEndScreen(true);
      return;
    }

    // Pigs still remain: do we have birds left?
    if (birdsRemaining > 0) {
      // Animate next waiting bird hopping into the slingshot
      if (waitingBirds.length > 0) {
        const nextBird = waitingBirds.shift();
        nextBird.jumping = true;
        nextBird.jumpT = 0;
        // Shift remaining waiting bird forward
        if (waitingBirds.length > 0) {
          waitingBirds[0].startX = waitingBirds[0].x;
        }
      }
      slingState = 'aiming';
      pullPos = { x: SLING_X, y: SLING_Y };
      demoTimer = 0;
      camera.targetX = 750;
      camera.targetY = 355;
      camera.targetViewWidth = 1410;
    } else {
      // Out of birds and pigs still alive!
      showEndScreen(false);
    }
  }

  function updateDemoAI() {
    if (!isDemo || slingState !== 'aiming' || isDragging) return;
    demoTimer++;

    const waitFrames = 45;
    const pullFrames = 75;
    const holdFrames = 22;

    const shot = demoShots[demoShotIndex % demoShots.length];

    if (demoTimer < waitFrames) {
      pullPos = { x: SLING_X, y: SLING_Y };
    } else if (demoTimer < waitFrames + pullFrames) {
      const t = (demoTimer - waitFrames) / pullFrames;
      // Smooth ease-in-out
      const ease = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
      pullPos = clampPull(SLING_X + shot.dx * ease, SLING_Y + shot.dy * ease);
    } else if (demoTimer < waitFrames + pullFrames + holdFrames) {
      pullPos = clampPull(SLING_X + shot.dx, SLING_Y + shot.dy);
    } else {
      demoShotIndex++;
      fireBird(pullPos.x, pullPos.y);
      pullPos = { x: SLING_X, y: SLING_Y };
    }
  }

  function updateGame() {
    // Step physics engine at fixed 60Hz delta
    Engine.update(engine, FIXED_DT);

    // Pop any pigs or remove blocks that fell off the world edge
    for (let i = pigs.length - 1; i >= 0; i--) {
      const p = pigs[i];
      if (p.position.x > 1650 || p.position.x < -100 || p.position.y > GROUND_Y + 40) {
        popPig(p);
      }
    }

    // Update clouds
    for (const c of clouds) {
      c.x += c.speed;
      if (c.x > 1950) c.x = -250;
    }

    // Animate HUD score counter smoothly
    if (displayedScore < score) {
      const diff = score - displayedScore;
      displayedScore += Math.max(25, Math.ceil(diff * 0.15));
      if (displayedScore > score) displayedScore = score;
      hudScoreEl.textContent = displayedScore.toLocaleString();
    }

    // Update waiting birds idle bobbing
    for (const wb of waitingBirds) {
      wb.phase += 0.06;
      if (wb.x < 185) {
        wb.x += (185 - wb.x) * 0.08;
      }
    }

    // Update Demo AI if active
    updateDemoAI();

    // Update Flying Bird, Trail & Camera Tracking
    if (slingState === 'flying' && activeBird) {
      flightTimer++;

      // Record dotted trail while the bird is before/near first impact
      if (!birdHasHit && flightTimer % 3 === 0 && flightTrail.length < 45) {
        flightTrail.push({
          x: activeBird.position.x,
          y: activeBird.position.y,
          r: (flightTrail.length % 2 === 0) ? 4.5 : 3.2
        });
      }

      // Camera smoothly follows the bird into the fort!
      const bx = Math.max(350, Math.min(1140, activeBird.position.x + 160));
      camera.targetX = bx;
      camera.targetY = 365;
      camera.targetViewWidth = 1120;

      // Check if round should settle
      const allPigsGone = pigs.length === 0;
      const calm = isWorldCalm();

      if (allPigsGone) {
        settleTimer++;
        if (settleTimer > 75 || (calm && settleTimer > 40)) {
          advanceToNextTurnOrEnd();
        }
      } else if (flightTimer > 55 && (calm || flightTimer > 310)) {
        settleTimer++;
        if (settleTimer > 42 || flightTimer > 320) {
          advanceToNextTurnOrEnd();
        }
      } else {
        settleTimer = 0;
      }
    } else if (slingState === 'aiming') {
      camera.targetX = 750;
      camera.targetY = 355;
      camera.targetViewWidth = 1410;
    }

    // Update particles
    for (let i = particles.length - 1; i >= 0; i--) {
      const pt = particles[i];
      pt.x += pt.vx;
      pt.y += pt.vy;
      pt.life -= pt.decay;

      if (pt.type === 'feather') {
        pt.vy += 0.06;
        pt.vx *= 0.96;
        pt.vy *= 0.96;
        pt.rot += pt.vRot + Math.sin(pt.life * 12) * 0.03;
      } else if (pt.type === 'shard') {
        pt.vy += 0.25;
        pt.rot += pt.vRot;
        if (pt.y > GROUND_Y - 3) {
          pt.y = GROUND_Y - 3;
          pt.vy *= -0.35;
          pt.vx *= 0.7;
        }
      } else if (pt.type === 'dust') {
        pt.r += 0.35;
        pt.vx *= 0.94;
        pt.vy *= 0.94;
      } else if (pt.type === 'star') {
        pt.vy += 0.14;
        pt.rot += pt.vRot;
      }

      if (pt.life <= 0) {
        particles.splice(i, 1);
      }
    }

    // Update floating score popups
    for (let i = floatingScores.length - 1; i >= 0; i--) {
      const fs = floatingScores[i];
      fs.y += fs.vy;
      fs.vy *= 0.96;
      fs.life -= 0.018;
      if (fs.life <= 0) {
        floatingScores.splice(i, 1);
      }
    }

    updateCameraTransform();
  }

  // =========================================================================
  // CARTOON RENDERING (100% Drawn in Code)
  // =========================================================================
  function drawSkyAndParallax() {
    // 1. Sky gradient (screen space)
    const skyGrad = ctx.createLinearGradient(0, 0, 0, screenH);
    skyGrad.addColorStop(0, '#3e92eb');
    skyGrad.addColorStop(0.58, '#8ed2ff');
    skyGrad.addColorStop(0.88, '#dcf3ff');
    skyGrad.addColorStop(1, '#f7fcff');
    ctx.fillStyle = skyGrad;
    ctx.fillRect(0, 0, screenW, screenH);

    // Save and enter world coordinates with parallax factors
    ctx.save();
    ctx.translate(camera.offsetX, camera.offsetY);
    ctx.scale(camera.scale, camera.scale);

    // 2. Fluffy Cartoon Clouds (parallax 0.25)
    const pCloud = (camera.x - 750) * 0.72;
    ctx.save();
    ctx.translate(pCloud, 0);
    for (const c of clouds) {
      drawCartoonCloud(c.x, c.y, c.s);
    }
    ctx.restore();

    // 3. Distant Mountain Range / Mesas (parallax 0.55)
    const pMount = (camera.x - 750) * 0.52;
    ctx.save();
    ctx.translate(pMount, 0);
    ctx.fillStyle = '#9ac7e3';
    ctx.beginPath();
    ctx.moveTo(-600, GROUND_Y);
    ctx.lineTo(-420, 340);
    ctx.lineTo(-180, 460);
    ctx.lineTo(140, 290);
    ctx.lineTo(430, 470);
    ctx.lineTo(760, 310);
    ctx.lineTo(1090, 450);
    ctx.lineTo(1410, 275);
    ctx.lineTo(1780, 440);
    ctx.lineTo(2100, GROUND_Y);
    ctx.closePath();
    ctx.fill();

    // Snow/highlight caps on peaks
    ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
    for (const pk of [{ x: 140, y: 290 }, { x: 760, y: 310 }, { x: 1410, y: 275 }]) {
      ctx.beginPath();
      ctx.moveTo(pk.x, pk.y);
      ctx.lineTo(pk.x - 52, pk.y + 52);
      ctx.lineTo(pk.x - 18, pk.y + 42);
      ctx.lineTo(pk.x + 8, pk.y + 56);
      ctx.lineTo(pk.x + 54, pk.y + 50);
      ctx.closePath();
      ctx.fill();
    }
    ctx.restore();

    // 4. Midground Rolling Hills (parallax 0.3)
    const pHill1 = (camera.x - 750) * 0.32;
    ctx.save();
    ctx.translate(pHill1, 0);
    ctx.fillStyle = '#78be68';
    ctx.strokeStyle = '#559446';
    ctx.lineWidth = 4;
    ctx.beginPath();
    ctx.moveTo(-600, GROUND_Y);
    ctx.bezierCurveTo(-250, 430, 80, 420, 420, 520);
    ctx.bezierCurveTo(720, 410, 1080, 400, 1460, 515);
    ctx.bezierCurveTo(1720, 440, 1950, 450, 2200, GROUND_Y);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // Distant pine/cypress trees on hills
    for (const tx of [-120, 25, 680, 890, 930, 1580]) {
      drawDistantTree(tx, 475);
    }
    ctx.restore();

    // 5. Foreground Lush Hills (parallax 0.14)
    const pHill2 = (camera.x - 750) * 0.14;
    ctx.save();
    ctx.translate(pHill2, 0);
    ctx.fillStyle = '#5da942';
    ctx.strokeStyle = '#3f7d29';
    ctx.lineWidth = 4;
    ctx.beginPath();
    ctx.moveTo(-600, GROUND_Y);
    ctx.bezierCurveTo(-150, 495, 220, 470, 590, 560);
    ctx.bezierCurveTo(860, 480, 1220, 475, 1620, 555);
    ctx.bezierCurveTo(1820, 505, 2000, 515, 2200, GROUND_Y);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
    ctx.restore();

    ctx.restore();
  }

  function drawCartoonCloud(x, y, s) {
    ctx.save();
    ctx.translate(x, y);
    ctx.scale(s, s);

    // Soft bottom shadow
    ctx.fillStyle = '#d3ecfa';
    ctx.beginPath();
    ctx.roundRect(-78, -6, 156, 32, 16);
    ctx.fill();

    // Main white puffs
    ctx.fillStyle = '#ffffff';
    ctx.beginPath();
    ctx.arc(-44, 0, 26, 0, Math.PI * 2);
    ctx.arc(-10, -14, 36, 0, Math.PI * 2);
    ctx.arc(32, -6, 29, 0, Math.PI * 2);
    ctx.arc(56, 6, 20, 0, Math.PI * 2);
    ctx.roundRect(-70, -4, 140, 26, 13);
    ctx.fill();
    ctx.restore();
  }

  function drawDistantTree(x, baseY) {
    ctx.save();
    ctx.translate(x, baseY);
    ctx.fillStyle = '#488537';
    ctx.beginPath();
    ctx.moveTo(0, -52);
    ctx.lineTo(-18, 0);
    ctx.lineTo(18, 0);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  }

  function drawGround() {
    // Deep earth fill
    const soilGrad = ctx.createLinearGradient(0, GROUND_Y, 0, GROUND_Y + 320);
    soilGrad.addColorStop(0, '#7c4722');
    soilGrad.addColorStop(0.25, '#5e3214');
    soilGrad.addColorStop(1, '#3b1d09');
    ctx.fillStyle = soilGrad;
    ctx.fillRect(-1000, GROUND_Y, 3800, 500);

    // Decorative soil stones
    ctx.fillStyle = '#683918';
    for (let sx = -200; sx < 1900; sx += 110) {
      ctx.beginPath();
      ctx.ellipse(sx, GROUND_Y + 42 + (sx % 3) * 14, 18, 9, 0.2, 0, Math.PI * 2);
      ctx.fill();
    }

    // Lush green grass turf band
    ctx.fillStyle = '#65c434';
    ctx.fillRect(-1000, GROUND_Y - 4, 3800, 18);

    // Top bright grass highlight
    ctx.fillStyle = '#8be04e';
    ctx.fillRect(-1000, GROUND_Y - 4, 3800, 6);

    // Dark cartoon outline along ground surface
    ctx.strokeStyle = '#2d5714';
    ctx.lineWidth = 3.5;
    ctx.beginPath();
    ctx.moveTo(-1000, GROUND_Y - 4);
    ctx.lineTo(2800, GROUND_Y - 4);
    ctx.stroke();

    // Grass blades
    ctx.strokeStyle = '#4ea623';
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    for (const g of grassTufts) {
      ctx.beginPath();
      ctx.moveTo(g.x, GROUND_Y - 3);
      ctx.quadraticCurveTo(g.x + g.lean * 0.5, GROUND_Y - 3 - g.h * 0.6, g.x + g.lean, GROUND_Y - 3 - g.h);
      ctx.stroke();
    }
  }

  // =========================================================================
  // SLINGSHOT RENDERING (Split Back & Front Layers)
  // =========================================================================
  const BACK_FORK = { x: SLING_X + 14, y: SLING_Y - 4 };
  const FRONT_FORK = { x: SLING_X - 13, y: SLING_Y + 3 };

  function getActivePouchPosition() {
    if (slingState === 'aiming' && birdsRemaining > 0) {
      return { x: pullPos.x, y: pullPos.y };
    }
    return { x: SLING_X, y: SLING_Y };
  }

  function drawSlingshotBack() {
    // Wooden pedestal shadow on ground
    ctx.fillStyle = 'rgba(0, 0, 0, 0.22)';
    ctx.beginPath();
    ctx.ellipse(SLING_X, GROUND_Y + 2, 38, 8, 0, 0, Math.PI * 2);
    ctx.fill();

    // Back wooden branch of the Y-fork
    ctx.save();
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';

    ctx.strokeStyle = '#3b1d08';
    ctx.lineWidth = 22;
    ctx.beginPath();
    ctx.moveTo(SLING_X + 2, SLING_Y + 42);
    ctx.quadraticCurveTo(SLING_X + 18, SLING_Y + 18, BACK_FORK.x, BACK_FORK.y);
    ctx.stroke();

    ctx.strokeStyle = '#7d441b';
    ctx.lineWidth = 15;
    ctx.beginPath();
    ctx.moveTo(SLING_X + 2, SLING_Y + 42);
    ctx.quadraticCurveTo(SLING_X + 18, SLING_Y + 18, BACK_FORK.x, BACK_FORK.y);
    ctx.stroke();

    // Back elastic band
    if (slingState === 'aiming' && birdsRemaining > 0) {
      const pouch = getActivePouchPosition();
      const angle = Math.atan2(pouch.y - SLING_Y, pouch.x - SLING_X);
      const backAttachX = pouch.x - Math.cos(angle) * (BIRD_RADIUS * 0.7);
      const backAttachY = pouch.y - Math.sin(angle) * (BIRD_RADIUS * 0.7);

      ctx.strokeStyle = '#2d1507';
      ctx.lineWidth = 11;
      ctx.beginPath();
      ctx.moveTo(BACK_FORK.x, BACK_FORK.y);
      ctx.lineTo(backAttachX, backAttachY);
      ctx.stroke();

      ctx.strokeStyle = '#572911';
      ctx.lineWidth = 6.5;
      ctx.beginPath();
      ctx.moveTo(BACK_FORK.x, BACK_FORK.y);
      ctx.lineTo(backAttachX, backAttachY);
      ctx.stroke();
    }
    ctx.restore();
  }

  function drawSlingshotFront() {
    ctx.save();
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';

    // Main trunk + Front branch outline
    ctx.strokeStyle = '#3b1d08';
    ctx.lineWidth = 25;
    ctx.beginPath();
    ctx.moveTo(SLING_X, GROUND_Y - 4);
    ctx.lineTo(SLING_X, SLING_Y + 42);
    ctx.quadraticCurveTo(SLING_X - 16, SLING_Y + 20, FRONT_FORK.x, FRONT_FORK.y);
    ctx.stroke();

    // Main trunk + Front branch wood fill
    ctx.strokeStyle = '#a35d28';
    ctx.lineWidth = 17;
    ctx.beginPath();
    ctx.moveTo(SLING_X, GROUND_Y - 4);
    ctx.lineTo(SLING_X, SLING_Y + 42);
    ctx.quadraticCurveTo(SLING_X - 16, SLING_Y + 20, FRONT_FORK.x, FRONT_FORK.y);
    ctx.stroke();

    // Warm rim highlight on front fork
    ctx.strokeStyle = '#cc8143';
    ctx.lineWidth = 5;
    ctx.beginPath();
    ctx.moveTo(SLING_X - 4, GROUND_Y - 10);
    ctx.lineTo(SLING_X - 4, SLING_Y + 42);
    ctx.quadraticCurveTo(SLING_X - 18, SLING_Y + 22, FRONT_FORK.x - 3, FRONT_FORK.y);
    ctx.stroke();

    // Front elastic band + leather pouch when aiming
    if (slingState === 'aiming' && birdsRemaining > 0) {
      const pouch = getActivePouchPosition();
      const dx = pouch.x - SLING_X;
      const dy = pouch.y - SLING_Y;
      const dist = Math.hypot(dx, dy);
      const angle = dist > 2 ? Math.atan2(dy, dx) : Math.PI;
      const attachX = pouch.x + Math.cos(angle) * (BIRD_RADIUS * 0.78);
      const attachY = pouch.y + Math.sin(angle) * (BIRD_RADIUS * 0.78);

      // Leather pouch cradling the back of the bird
      ctx.save();
      ctx.translate(attachX, attachY);
      ctx.rotate(angle);
      ctx.fillStyle = '#47220d';
      ctx.strokeStyle = '#241004';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.roundRect(-8, -13, 15, 26, 6);
      ctx.fill();
      ctx.stroke();
      ctx.restore();

      // Front rubber band
      ctx.strokeStyle = '#2d1507';
      ctx.lineWidth = 11.5;
      ctx.beginPath();
      ctx.moveTo(FRONT_FORK.x, FRONT_FORK.y);
      ctx.lineTo(attachX, attachY);
      ctx.stroke();

      ctx.strokeStyle = '#6c3417';
      ctx.lineWidth = 7;
      ctx.beginPath();
      ctx.moveTo(FRONT_FORK.x, FRONT_FORK.y);
      ctx.lineTo(attachX, attachY);
      ctx.stroke();
    } else {
      // Relaxed band hanging between forks after launch
      ctx.strokeStyle = '#47220d';
      ctx.lineWidth = 6.5;
      ctx.beginPath();
      ctx.moveTo(BACK_FORK.x, BACK_FORK.y);
      ctx.quadraticCurveTo(SLING_X, SLING_Y + 28, FRONT_FORK.x, FRONT_FORK.y);
      ctx.stroke();
    }

    // Band tie wraps around the fork tips
    for (const fk of [FRONT_FORK, BACK_FORK]) {
      ctx.fillStyle = '#4a2410';
      ctx.strokeStyle = '#241004';
      ctx.lineWidth = 2.5;
      ctx.beginPath();
      ctx.roundRect(fk.x - 10, fk.y - 4, 20, 9, 4);
      ctx.fill();
      ctx.stroke();
    }

    ctx.restore();
  }

  // =========================================================================
  // TRAJECTORY PREVIEW ARC & FLIGHT TRAIL
  // =========================================================================
  function drawTrajectoryAndTrail() {
    // 1. Persistent trail of the last launched bird
    for (let i = 0; i < flightTrail.length; i++) {
      const pt = flightTrail[i];
      ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
      ctx.strokeStyle = 'rgba(60, 40, 20, 0.55)';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, pt.r, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();
    }

    // 2. Dotted aiming trajectory arc when dragging the bird back
    if (slingState === 'aiming' && birdsRemaining > 0) {
      const pullDist = Math.hypot(pullPos.x - SLING_X, pullPos.y - SLING_Y);
      if (pullDist >= MIN_PULL) {
        const arcPts = computeTrajectoryPoints(pullPos.x, pullPos.y, 75);
        for (let i = 0; i < arcPts.length; i++) {
          const p = arcPts[i];
          const fade = Math.max(0.15, 1 - i / arcPts.length);
          const r = (i % 2 === 0 ? 5.2 : 3.6) * (0.65 + 0.35 * fade);
          ctx.save();
          ctx.globalAlpha = fade;
          ctx.fillStyle = '#ffffff';
          ctx.strokeStyle = '#3c2410';
          ctx.lineWidth = 2.2;
          ctx.beginPath();
          ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
          ctx.fill();
          ctx.stroke();
          ctx.restore();
        }
      }
    }
  }

  // =========================================================================
  // CARTOON RED BIRD WITH ANGRY EYEBROWS
  // =========================================================================
  function drawCartoonBird(x, y, angle = 0, screaming = false) {
    const r = BIRD_RADIUS;
    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(angle);

    // Three black tail feathers sticking out the back
    ctx.fillStyle = '#1f1a17';
    ctx.strokeStyle = '#1f1a17';
    ctx.lineWidth = 2;
    for (const ta of [-0.35, 0, 0.35]) {
      ctx.save();
      ctx.rotate(ta);
      ctx.beginPath();
      ctx.moveTo(-r + 3, 0);
      ctx.lineTo(-r - 12, -3.5);
      ctx.lineTo(-r - 11, 3.5);
      ctx.closePath();
      ctx.fill();
      ctx.restore();
    }

    // Two top head crest feathers
    ctx.fillStyle = '#e52521';
    ctx.strokeStyle = '#40110d';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.ellipse(-2, -r - 5, 8, 5, -0.45, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    ctx.beginPath();
    ctx.ellipse(-9, -r - 2, 6.5, 4, -0.8, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Main round red body
    ctx.fillStyle = '#e52521';
    ctx.strokeStyle = '#40110d';
    ctx.lineWidth = 3.5;
    ctx.beginPath();
    ctx.arc(0, 0, r, 0, Math.PI * 2);
    ctx.fill();

    // Darker red lower shading
    ctx.save();
    ctx.beginPath();
    ctx.arc(0, 0, r - 1.5, 0, Math.PI * 2);
    ctx.clip();

    ctx.fillStyle = '#bd1714';
    ctx.beginPath();
    ctx.arc(-4, 5, r, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = '#e52521';
    ctx.beginPath();
    ctx.arc(3, -3, r * 0.92, 0, Math.PI * 2);
    ctx.fill();

    // Cream belly patch at bottom
    ctx.fillStyle = '#f5e2c8';
    ctx.beginPath();
    ctx.ellipse(2, r * 0.78, r * 0.68, r * 0.44, 0, 0, Math.PI * 2);
    ctx.fill();

    // Cute darker red cheek spots
    ctx.fillStyle = '#b31217';
    ctx.beginPath();
    ctx.ellipse(-6, 3, 3.5, 2.5, 0, 0, Math.PI * 2);
    ctx.ellipse(-12, 1, 2.5, 2, 0, 0, Math.PI * 2);
    ctx.fill();

    // Glossy top-left highlight
    ctx.fillStyle = 'rgba(255, 190, 180, 0.45)';
    ctx.beginPath();
    ctx.ellipse(-6, -10, 7, 4, -0.5, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();

    // Outer body outline on top
    ctx.strokeStyle = '#40110d';
    ctx.lineWidth = 3.5;
    ctx.beginPath();
    ctx.arc(0, 0, r, 0, Math.PI * 2);
    ctx.stroke();

    // Eyes (two side-by-side white ovals facing right)
    ctx.fillStyle = '#ffffff';
    ctx.strokeStyle = '#2b120e';
    ctx.lineWidth = 2.5;
    // Left/back eye
    ctx.beginPath();
    ctx.ellipse(4, -2, 5.2, 5.8, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
    // Right/front eye
    ctx.beginPath();
    ctx.ellipse(12.5, -2, 5.0, 5.6, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Pupils glaring forward
    ctx.fillStyle = '#140c0a';
    ctx.beginPath();
    ctx.arc(5.6, -2, 2.1, 0, Math.PI * 2);
    ctx.arc(13.8, -2, 2.1, 0, Math.PI * 2);
    ctx.fill();

    // Eye catchlights
    ctx.fillStyle = '#ffffff';
    ctx.beginPath();
    ctx.arc(4.8, -3.2, 0.9, 0, Math.PI * 2);
    ctx.arc(13.0, -3.2, 0.9, 0, Math.PI * 2);
    ctx.fill();

    // Thick, sharply angled V-shaped ANGRY EYEBROWS
    ctx.fillStyle = '#1a110f';
    ctx.beginPath();
    ctx.moveTo(-3, -10);
    ctx.lineTo(9, -4.5);
    ctx.lineTo(19, -9.5);
    ctx.lineTo(17.5, -13.5);
    ctx.lineTo(9, -8.5);
    ctx.lineTo(-1, -14.2);
    ctx.closePath();
    ctx.fill();

    // Golden-yellow beak
    ctx.lineJoin = 'round';
    if (screaming) {
      // Open battle-cry beak
      ctx.fillStyle = '#ff9d00';
      ctx.strokeStyle = '#3b1c06';
      ctx.lineWidth = 2.5;
      ctx.beginPath();
      ctx.moveTo(8, 3);
      ctx.lineTo(22, 0);
      ctx.lineTo(9, 8);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();

      ctx.fillStyle = '#ffbe2e';
      ctx.beginPath();
      ctx.moveTo(7, 2);
      ctx.lineTo(24, -1);
      ctx.lineTo(10, -5);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
    } else {
      // Lower beak
      ctx.fillStyle = '#e88b00';
      ctx.strokeStyle = '#3b1c06';
      ctx.lineWidth = 2.4;
      ctx.beginPath();
      ctx.moveTo(7, 3.5);
      ctx.lineTo(20, 4.5);
      ctx.lineTo(10, 9.5);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();

      // Upper sharp beak
      ctx.fillStyle = '#ffbe26';
      ctx.beginPath();
      ctx.moveTo(7, 3.5);
      ctx.lineTo(23, 2.2);
      ctx.lineTo(10, -3.8);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();
    }

    ctx.restore();
  }

  // =========================================================================
  // GREEN PIGS WITH EXPRESSIONS & BOSS CROWN
  // =========================================================================
  function drawCartoonPig(pig) {
    const { x, y } = pig.position;
    const r = pig.r;
    const isBoss = pig.role === 'boss';
    const damaged = pig.hp < pig.maxHp * 0.65;
    const scared = pig.scaredTimer > 0;
    if (pig.scaredTimer > 0) pig.scaredTimer--;
    pig.blinkTimer = (pig.blinkTimer + 1) % 220;
    const blinking = pig.blinkTimer > 210 && !scared;

    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(pig.angle);

    // Round Pig Ears
    ctx.fillStyle = '#6ec926';
    ctx.strokeStyle = '#224a08';
    ctx.lineWidth = 3;
    for (const side of [-1, 1]) {
      ctx.save();
      ctx.rotate(side * 0.65);
      ctx.beginPath();
      ctx.ellipse(0, -r - 2, 6, 7.5, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = '#488f12';
      ctx.beginPath();
      ctx.ellipse(0, -r - 2, 3, 4, 0, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = '#6ec926';
      ctx.restore();
    }

    // Main green pig body
    const bodyGrad = ctx.createRadialGradient(-r * 0.25, -r * 0.3, r * 0.1, 0, 0, r);
    bodyGrad.addColorStop(0, '#9cf04a');
    bodyGrad.addColorStop(0.7, '#6ec926');
    bodyGrad.addColorStop(1, '#52a314');
    ctx.fillStyle = bodyGrad;
    ctx.strokeStyle = '#224a08';
    ctx.lineWidth = 3.5;
    ctx.beginPath();
    ctx.arc(0, 0, r, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Bruised eye patch if damaged
    if (damaged) {
      ctx.fillStyle = 'rgba(110, 55, 145, 0.55)';
      ctx.beginPath();
      ctx.arc(-r * 0.44, -r * 0.08, r * 0.32, 0, Math.PI * 2);
      ctx.fill();
    }

    // Wide-set Eyes
    for (const side of [-1, 1]) {
      const ex = side * r * 0.46;
      const ey = -r * 0.12;
      const er = r * 0.23;

      ctx.fillStyle = '#ffffff';
      ctx.strokeStyle = '#224a08';
      ctx.lineWidth = 2.4;
      ctx.beginPath();
      ctx.arc(ex, ey, er, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();

      if (blinking) {
        ctx.fillStyle = '#6ec926';
        ctx.beginPath();
        ctx.arc(ex, ey, er - 1, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = '#224a08';
        ctx.beginPath();
        ctx.moveTo(ex - er, ey);
        ctx.lineTo(ex + er, ey);
        ctx.stroke();
      } else {
        // Pupil
        const pr = scared ? er * 0.32 : er * 0.44;
        const lookX = side * -1.2;
        ctx.fillStyle = '#152905';
        ctx.beginPath();
        ctx.arc(ex + lookX, ey, pr, 0, Math.PI * 2);
        ctx.fill();

        // Half-eyelid if bruised
        if (damaged && side === -1) {
          ctx.fillStyle = '#638035';
          ctx.beginPath();
          ctx.arc(ex, ey, er, Math.PI, Math.PI * 2);
          ctx.fill();
          ctx.stroke();
        }
      }
    }

    // Mouth / Expression under snout
    ctx.strokeStyle = '#224a08';
    ctx.lineWidth = 2.4;
    if (scared) {
      ctx.fillStyle = '#33120c';
      ctx.beginPath();
      ctx.arc(0, r * 0.48, r * 0.22, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();
    } else {
      ctx.beginPath();
      ctx.arc(0, r * 0.32, r * 0.26, 0.25, Math.PI - 0.25);
      ctx.stroke();
    }

    // Boss Pig Moustache
    if (isBoss) {
      ctx.fillStyle = '#994b16';
      ctx.strokeStyle = '#3b1a04';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.ellipse(-r * 0.28, r * 0.36, r * 0.34, r * 0.14, -0.35, 0, Math.PI * 2);
      ctx.ellipse(r * 0.28, r * 0.36, r * 0.34, r * 0.14, 0.35, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();
    }

    // Large Green Oval Snout in Center
    ctx.fillStyle = '#8de838';
    ctx.strokeStyle = '#224a08';
    ctx.lineWidth = 2.8;
    ctx.beginPath();
    ctx.ellipse(0, 0, r * 0.38, r * 0.29, 0, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Two dark nostrils
    ctx.fillStyle = '#1b4005';
    ctx.beginPath();
    ctx.ellipse(-r * 0.14, 0, r * 0.08, r * 0.12, -0.15, 0, Math.PI * 2);
    ctx.ellipse(r * 0.14, 0, r * 0.08, r * 0.12, 0.15, 0, Math.PI * 2);
    ctx.fill();

    // Band-aid on forehead if damaged
    if (damaged) {
      ctx.save();
      ctx.translate(r * 0.25, -r * 0.55);
      ctx.rotate(0.4);
      ctx.fillStyle = '#f5d1a6';
      ctx.strokeStyle = '#5c3b1c';
      ctx.lineWidth = 1.8;
      ctx.beginPath();
      ctx.roundRect(-9, -4, 18, 8, 3);
      ctx.fill();
      ctx.stroke();
      ctx.restore();
    }

    // Golden Crown on Boss Pig!
    if (isBoss) {
      ctx.save();
      ctx.translate(0, -r + 2);
      ctx.fillStyle = '#ffcf26';
      ctx.strokeStyle = '#4a2b05';
      ctx.lineWidth = 2.6;
      ctx.lineJoin = 'round';
      ctx.beginPath();
      ctx.moveTo(-15, 0);
      ctx.lineTo(-18, -16);
      ctx.lineTo(-8, -8);
      ctx.lineTo(0, -20);
      ctx.lineTo(8, -8);
      ctx.lineTo(18, -16);
      ctx.lineTo(15, 0);
      ctx.closePath();
      ctx.fill();
      ctx.stroke();

      // Red crown jewel
      ctx.fillStyle = '#e52521';
      ctx.beginPath();
      ctx.arc(0, -7, 3.2, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }

    ctx.restore();
  }

  // =========================================================================
  // WOOD PLANKS (WITH WOOD GRAIN) & STONE BLOCKS (WITH CRACKS)
  // =========================================================================
  function drawFortBlock(block) {
    const { x, y } = block.position;
    const { w, h, material, hp, maxHp } = block;
    const hw = w * 0.5;
    const hh = h * 0.5;
    const dmgRatio = 1 - Math.max(0, hp / maxHp);

    ctx.save();
    ctx.translate(x, y);
    ctx.rotate(block.angle);

    if (material === 'wood') {
      // Base wood fill
      const woodGrad = ctx.createLinearGradient(-hw, -hh, hw, hh);
      woodGrad.addColorStop(0, '#e39a44');
      woodGrad.addColorStop(0.5, '#cf802b');
      woodGrad.addColorStop(1, '#b3681b');
      ctx.fillStyle = woodGrad;
      ctx.strokeStyle = '#4a2608';
      ctx.lineWidth = 3;

      ctx.beginPath();
      ctx.roundRect(-hw, -hh, w, h, 3);
      ctx.fill();

      // Top/Left warm bevel highlight
      ctx.fillStyle = 'rgba(255, 224, 166, 0.35)';
      ctx.fillRect(-hw + 2, -hh + 2, w - 4, 4);

      // Wood grain lines along the longer axis
      ctx.strokeStyle = 'rgba(92, 46, 10, 0.42)';
      ctx.lineWidth = 1.6;
      const horizontal = w >= h;
      if (block.woodData) {
        for (const ln of block.woodData.lines) {
          ctx.beginPath();
          if (horizontal) {
            const ly = ln.offset * h;
            ctx.moveTo(-hw + 4, ly);
            ctx.quadraticCurveTo(0, ly + ln.wave, hw - 4, ly);
          } else {
            const lx = ln.offset * w;
            ctx.moveTo(lx, -hh + 4);
            ctx.quadraticCurveTo(lx + ln.wave, 0, lx, hh - 4);
          }
          ctx.stroke();
        }
        if (block.woodData.knot) {
          const k = block.woodData.knot;
          ctx.beginPath();
          ctx.ellipse(k.u * w, k.v * h, k.r * (horizontal ? 1.5 : 0.9), k.r * (horizontal ? 0.9 : 1.5), 0, 0, Math.PI * 2);
          ctx.stroke();
        }
      }

      // Corner nail heads on planks
      ctx.fillStyle = '#5c3110';
      const inset = 5.5;
      if (w >= 45 || h >= 45) {
        for (const sx of [-1, 1]) {
          for (const sy of [-1, 1]) {
            ctx.beginPath();
            ctx.arc(sx * (hw - inset), sy * (hh - inset), 1.8, 0, Math.PI * 2);
            ctx.fill();
          }
        }
      }

      // Outer border
      ctx.strokeStyle = '#4a2608';
      ctx.lineWidth = 3;
      ctx.strokeRect(-hw, -hh, w, h);

    } else {
      // Heavy Stone Block
      const stoneGrad = ctx.createLinearGradient(-hw, -hh, hw, hh);
      stoneGrad.addColorStop(0, '#a6b2bf');
      stoneGrad.addColorStop(0.55, '#8693a1');
      stoneGrad.addColorStop(1, '#687482');
      ctx.fillStyle = stoneGrad;

      ctx.beginPath();
      ctx.roundRect(-hw, -hh, w, h, 4);
      ctx.fill();

      // Chiseled inner bevel
      ctx.fillStyle = 'rgba(235, 245, 255, 0.35)';
      ctx.fillRect(-hw + 3, -hh + 3, w - 6, 4.5);
      ctx.fillStyle = 'rgba(30, 38, 48, 0.28)';
      ctx.fillRect(-hw + 3, hh - 7.5, w - 6, 4.5);

      // Subtle stone speckles
      ctx.fillStyle = 'rgba(50, 60, 72, 0.32)';
      ctx.beginPath();
      ctx.arc(-hw * 0.4, -hh * 0.2, 2.2, 0, Math.PI * 2);
      ctx.arc(hw * 0.35, hh * 0.25, 2.6, 0, Math.PI * 2);
      ctx.arc(hw * 0.1, -hh * 0.3, 1.8, 0, Math.PI * 2);
      ctx.fill();

      // Stone border
      ctx.strokeStyle = '#2d3540';
      ctx.lineWidth = 3.2;
      ctx.strokeRect(-hw, -hh, w, h);
    }

    // Dynamic damage cracks when hit
    if (dmgRatio > 0.22) {
      ctx.strokeStyle = material === 'stone' ? '#1f252d' : '#361904';
      ctx.lineWidth = 2.2;
      ctx.beginPath();
      ctx.moveTo(-hw * 0.3, -hh);
      ctx.lineTo(-hw * 0.05, -hh * 0.2);
      ctx.lineTo(-hw * 0.25, hh * 0.3);
      if (dmgRatio > 0.55) {
        ctx.moveTo(hw * 0.25, hh);
        ctx.lineTo(hw * 0.05, 0);
        ctx.lineTo(hw * 0.35, -hh * 0.4);
      }
      ctx.stroke();
    }

    ctx.restore();
  }

  // =========================================================================
  // PARTICLES & FLOATING SCORE POPUPS RENDERING
  // =========================================================================
  function drawParticlesAndScores() {
    for (const pt of particles) {
      ctx.save();
      ctx.globalAlpha = Math.max(0, Math.min(1, pt.life));
      ctx.translate(pt.x, pt.y);

      if (pt.type === 'dust') {
        ctx.fillStyle = pt.color;
        ctx.beginPath();
        ctx.arc(0, 0, pt.r, 0, Math.PI * 2);
        ctx.fill();
      } else if (pt.type === 'shard') {
        ctx.rotate(pt.rot);
        ctx.fillStyle = pt.color;
        ctx.strokeStyle = '#2b1808';
        ctx.lineWidth = 1.6;
        ctx.fillRect(-pt.w * 0.5, -pt.h * 0.5, pt.w, pt.h);
        ctx.strokeRect(-pt.w * 0.5, -pt.h * 0.5, pt.w, pt.h);
      } else if (pt.type === 'feather') {
        ctx.rotate(pt.rot);
        ctx.fillStyle = pt.color;
        ctx.strokeStyle = '#3b0e0b';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.ellipse(0, 0, pt.size, pt.size * 0.42, 0, 0, Math.PI * 2);
        ctx.fill();
        ctx.stroke();
      } else if (pt.type === 'star') {
        ctx.rotate(pt.rot);
        ctx.fillStyle = '#ffdf38';
        ctx.strokeStyle = '#4a2810';
        ctx.lineWidth = 1.8;
        ctx.beginPath();
        for (let i = 0; i < 5; i++) {
          const a1 = (i * 2 * Math.PI) / 5 - Math.PI / 2;
          const a2 = a1 + Math.PI / 5;
          ctx.lineTo(Math.cos(a1) * pt.size, Math.sin(a1) * pt.size);
          ctx.lineTo(Math.cos(a2) * (pt.size * 0.45), Math.sin(a2) * (pt.size * 0.45));
        }
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
      }

      ctx.restore();
    }

    // Floating score popups
    for (const fs of floatingScores) {
      ctx.save();
      const scale = fs.life > 0.8 ? 1 + (1 - fs.life) * 1.4 : 1;
      ctx.globalAlpha = Math.max(0, Math.min(1, fs.life * 1.4));
      ctx.translate(fs.x, fs.y);
      ctx.scale(scale, scale);
      ctx.font = `900 ${fs.big ? 32 : 22}px "Trebuchet MS", Impact, sans-serif`;
      ctx.textAlign = 'center';
      ctx.lineJoin = 'round';
      ctx.strokeStyle = '#2d1606';
      ctx.lineWidth = fs.big ? 6 : 4.5;
      ctx.strokeText(fs.text, 0, 0);
      ctx.fillStyle = fs.color;
      ctx.fillText(fs.text, 0, 0);
      ctx.restore();
    }
  }

  // =========================================================================
  // MAIN RENDER LOOP
  // =========================================================================
  function render() {
    ctx.save();
    ctx.scale(dpr, dpr);

    // 1. Sky & Parallax Hills
    drawSkyAndParallax();

    // 2. Enter World Coordinate Space
    ctx.save();
    ctx.translate(camera.offsetX, camera.offsetY);
    ctx.scale(camera.scale, camera.scale);

    // 3. Ground & Grass
    drawGround();

    // 4. Trajectory Arc & Flight Trail (behind slingshot & fort)
    drawTrajectoryAndTrail();

    // 5. Slingshot Back Fork & Back Elastic Band
    drawSlingshotBack();

    // 6. Waiting Birds on the Ground (left of slingshot)
    for (const wb of waitingBirds) {
      const bobY = wb.y - Math.abs(Math.sin(wb.phase)) * 4;
      // Ground shadow
      ctx.fillStyle = 'rgba(0,0,0,0.2)';
      ctx.beginPath();
      ctx.ellipse(wb.x, GROUND_Y + 1, 18, 5, 0, 0, Math.PI * 2);
      ctx.fill();
      drawCartoonBird(wb.x, bobY, 0, false);
    }

    // 7. Bird in Slingshot Pouch (when aiming)
    if (slingState === 'aiming' && birdsRemaining > 0) {
      const pullDist = Math.hypot(pullPos.x - SLING_X, pullPos.y - SLING_Y);
      const aimAngle = pullDist > 8 ? Math.atan2(SLING_Y - pullPos.y, SLING_X - pullPos.x) : 0;
      drawCartoonBird(pullPos.x, pullPos.y, aimAngle, pullDist > 60);
    }

    // 8. Launched Birds in Flight / Tumbled on Ground
    for (const b of launchedBirds) {
      const spd = Math.hypot(b.velocity.x, b.velocity.y);
      const screaming = !b.hasImpacted && spd > 4;
      const angle = !b.hasImpacted && spd > 2
        ? Math.atan2(b.velocity.y, b.velocity.x)
        : b.angle;
      drawCartoonBird(b.position.x, b.position.y, angle, screaming);
    }

    // 9. Slingshot Front Elastic Band, Pouch & Front Fork
    drawSlingshotFront();

    // 10. Fort Blocks (Wood Planks & Stone Blocks)
    for (const block of fortBlocks) {
      drawFortBlock(block);
    }

    // 11. Green Pigs Inside the Fort
    for (const pig of pigs) {
      drawCartoonPig(pig);
    }

    // 12. Particles & Score Pops
    drawParticlesAndScores();

    ctx.restore(); // exit world space
    ctx.restore(); // exit dpr scale
  }

  function frameLoop() {
    updateGame();
    render();
    requestAnimationFrame(frameLoop);
  }

  // Boot the level and start the loop!
  initLevel();
  updateCameraTransform();
  requestAnimationFrame(frameLoop);
})();
</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.