<!doctype html>

<html lang="en">

<head>

  <meta charset="utf-8" />

  <meta name="viewport" content="width=device-width,initial-scale=1" />

  <title>Rapid Targets, Animal Run</title>

  <style>

    :root{

      --bg0:#0b0d12;

      --bg1:#0f1320;

      --panel:#171a21;

      --panel2:#121520;

      --line:#2a2f3b;

      --text:#e8e8e8;

      --muted:#a9b3c9;

      --accent:#ffdf6e;

      --good:#9eff7a;

      --bad:#ff6b6b;

      --glow: 0 10px 26px rgba(0,0,0,0.35);

      font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;

    }

    body{ margin:0; background:var(--bg0); color:var(--text); }

    .wrap{

      max-width:1100px;

      margin:0 auto;

      padding:18px;

      display:grid;

      grid-template-columns:360px 1fr;

      gap:16px;

    }

    .card{

      background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.00)), var(--panel);

      border:1px solid var(--line);

      border-radius:16px;

      padding:14px;

      box-shadow:var(--glow);

      position:relative;

      overflow:hidden;

    }

    .card:before{

      content:"";

      position:absolute;

      inset:-80px -120px auto auto;

      width:260px;

      height:260px;

      background:radial-gradient(circle at 30% 30%, rgba(255,223,110,0.10), rgba(255,223,110,0));

      transform:rotate(18deg);

      pointer-events:none;

    }

    h1{ font-size:18px; margin:0 0 10px 0; letter-spacing:0.2px; }

    .note{ font-size:12px; color:var(--muted); line-height:1.35; margin:0 0 12px 0; }

    .hud{ display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0 12px 0; }

    .pill{

      background:var(--panel2);

      border:1px solid var(--line);

      border-radius:999px;

      padding:8px 10px;

      font-size:12px;

      color:#d7dcf0;

      text-align:center;

      font-variant-numeric:tabular-nums;

      box-shadow: 0 6px 18px rgba(0,0,0,0.20);

    }

    .btnRow{ display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:10px; }

    button{

      width:100%;

      padding:11px 12px;

      border-radius:12px;

      border:1px solid var(--line);

      background:linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.00)), #24304a;

      color:#fff;

      font-weight:700;

      cursor:pointer;

      box-shadow: 0 8px 18px rgba(0,0,0,0.25);

    }

    button:disabled{ opacity:0.55; cursor:not-allowed; }

    .msg{ font-size:12px; color:#cbd2e1; margin-top:10px; min-height:18px; }

    .legend{

      margin-top:12px;

      font-size:12px;

      color:#cbd2e1;

    }

    .legendRow{

      display:grid;

      grid-template-columns: 1fr 1fr;

      gap:10px;

      margin-top:10px;

    }

    .chip{

      background:var(--panel2);

      border:1px solid var(--line);

      border-radius:14px;

      padding:10px;

      box-shadow: 0 6px 18px rgba(0,0,0,0.20);

    }

    .chipTop{

      display:flex;

      align-items:center;

      justify-content:space-between;

      gap:10px;

      margin-bottom:6px;

    }

    .chipIcon{

      width:32px;

      height:32px;

      border-radius:10px;

      display:grid;

      place-items:center;

      font-size:18px;

      background:rgba(255,255,255,0.05);

      border:1px solid rgba(255,255,255,0.08);

    }

    .chipTitle{ font-weight:750; letter-spacing:0.2px; }

    .chipDesc{ font-size:11px; color:var(--muted); line-height:1.3; }

    .leader{ margin-top:12px; font-size:12px; color:#cbd2e1; }

    .leader ol{ margin:8px 0 0 18px; padding:0; }

    .leader li{ margin:4px 0; font-variant-numeric:tabular-nums; }


    canvas{

      width:100%;

      height:560px;

      display:block;

      background:

        radial-gradient(1200px 560px at 50% 60%, var(--bg1) 0%, var(--bg0) 55%, #06070a 100%);

      border-radius:16px;

      border:1px solid var(--line);

      cursor: crosshair;

      user-select:none;

      -webkit-user-select:none;

      touch-action: manipulation;

      box-shadow:var(--glow);

    }

    .small{ font-size:11px; color:var(--muted); margin-top:8px; }


    @media (max-width:980px){

      .wrap{ grid-template-columns:1fr; }

      canvas{ height:520px; }

    }

  </style>

</head>

<body>

  <div class="wrap">

    <div class="card">

      <h1>Rapid Targets: Game</h1>

      <p class="note">

        Shoot animals to score. <br>Do not click the 'don't shoot' signs. <br>Missing an animal ends the run after the short warm up.

    

      </p>


      <div class="hud">

        <div class="pill" id="scorePill">Score: 0</div>

        <div class="pill" id="comboPill">Combo: 0</div>

        <div class="pill" id="levelPill">Difficulty: 1.0</div>

        <div class="pill" id="bestPill">Best: 0</div>

      </div>


      <div class="btnRow">

        <button id="startBtn">Start</button>

        <button id="resetBtn">Reset leaderboard</button>

      </div>


      <div class="msg" id="msg"></div>


      <div class="legend">

        <strong>Targets</strong>

        <div class="legendRow">

          <div class="chip">

            <div class="chipTop">

              <div style="display:flex;align-items:center;gap:10px;">

                <div class="chipIcon">🐀</div>

                <div>

                  <div class="chipTitle">Rat</div>

                  <div class="chipDesc">Small, quick, highest value</div>

                </div>

              </div>

              <div class="chipDesc">Fast</div>

            </div>

          </div>

          <div class="chip">

            <div class="chipTop">

              <div style="display:flex;align-items:center;gap:10px;">

                <div class="chipIcon">🦊</div>

                <div>

                  <div class="chipTitle">Fox</div>

                  <div class="chipDesc">Medium size, medium value</div>

                </div>

              </div>

              <div class="chipDesc">Balanced</div>

            </div>

          </div>

          <div class="chip">

            <div class="chipTop">

              <div style="display:flex;align-items:center;gap:10px;">

                <div class="chipIcon">🦌</div>

                <div>

                  <div class="chipTitle">Deer</div>

                  <div class="chipDesc">Large, steady, fewer points</div>

                </div>

              </div>

              <div class="chipDesc">Slow</div>

            </div>

          </div>

          <div class="chip">

            <div class="chipTop">

              <div style="display:flex;align-items:center;gap:10px;">

                <div class="chipIcon">🛑</div>

                <div>

                  <div class="chipTitle">Don't shoot!</div>

                  <div class="chipDesc">Clicking ends the run</div>

                </div>

              </div>

              <div class="chipDesc">Avoid</div>

            </div>

          </div>

        </div>

      </div>


      <div class="leader">

        <strong>Local top scores</strong>

        <div class="small">Stored in your browser. Swap to your server leaderboard when ready.</div>

        <ol id="lb"></ol>

      </div>

    </div>


    <div>

      <canvas id="cv" width="900" height="560"></canvas>

      <div class="small">Tip, early game starts calm. After warm up, missing an animal ends the run.</div>

    </div>

  </div>


<script>

(() => {

  const cv = document.getElementById("cv");

  const ctx = cv.getContext("2d");


  const scorePill = document.getElementById("scorePill");

  const comboPill = document.getElementById("comboPill");

  const levelPill = document.getElementById("levelPill");

  const bestPill = document.getElementById("bestPill");

  const msg = document.getElementById("msg");


  const startBtn = document.getElementById("startBtn");

  const resetBtn = document.getElementById("resetBtn");

  const lbEl = document.getElementById("lb");


  const W = cv.width, H = cv.height;

  const clamp = (v,a,b) => Math.max(a, Math.min(b, v));

  const rnd = (a,b) => a + Math.random() * (b-a);


  const STORAGE_KEY = "rapidTargetsAnimalsTop10V1";


  const state = {

    running: false,

    score: 0,

    combo: 0,

    difficulty: 1.0,

    lastTs: performance.now(),

    targets: [],

    spawnTimer: 0,

    spawnInterval: 1.25,

    maxOnScreen: 1,

    runStartTs: 0,

    warmupSeconds: 5.0,

    lastClickFx: null,

    particles: [],

    shake: 0

  };


  const TYPES = {

    rat:  { emoji:"🐀", baseR:[18, 30], life:[1.0, 1.6], speed:[55, 135], points: 160, weight: 0.30 },

    fox:  { emoji:"🦊", baseR:[24, 38], life:[1.2, 1.9], speed:[35, 95],  points: 120, weight: 0.42 },

    deer: { emoji:"🦌", baseR:[30, 46], life:[1.4, 2.3], speed:[15, 60],  points: 90,  weight: 0.28 },

    noshoot:{ emoji:"🛑", baseR:[26, 40], life:[1.1, 2.0], speed:[10, 70], points: 0,   weight: 0.00 }

  };


  function getTopScores(){

    const raw = localStorage.getItem(STORAGE_KEY);

    return raw ? JSON.parse(raw) : [];

  }

  function saveTopScores(list){

    localStorage.setItem(STORAGE_KEY, JSON.stringify(list.slice(0, 10)));

  }

  function addScoreToBoard(score){

    const list = getTopScores();

    list.push({ score, at: new Date().toISOString() });

    list.sort((a,b) => b.score - a.score);

    saveTopScores(list);

  }

  function renderBoard(){

    const list = getTopScores();

    lbEl.innerHTML = "";

    if(!list.length){

      const li = document.createElement("li");

      li.textContent = "No scores yet.";

      lbEl.appendChild(li);

      bestPill.textContent = "Best: 0";

      return;

    }

    for(const s of list){

      const li = document.createElement("li");

      const d = new Date(s.at);

      li.textContent = `${s.score.toLocaleString()}  ,  ${d.toLocaleDateString()}`;

      lbEl.appendChild(li);

    }

    bestPill.textContent = `Best: ${(list[0]?.score ?? 0).toLocaleString()}`;

  }


  function difficultyFromScore(score){

    return 1 + Math.log10(1 + score / 950) * 1.0;

  }


  function progressionTuning(){

    if(state.score < 700) state.maxOnScreen = 1;

    else if(state.score < 3200) state.maxOnScreen = 2;

    else state.maxOnScreen = 3;


    const minInterval = 0.38;

    const maxInterval = 1.35;

    state.spawnInterval = clamp(maxInterval / (0.95 + (state.difficulty - 1) * 0.55), minInterval, maxInterval);

  }


  function cryptoRandomId(){

    const a = new Uint32Array(2);

    crypto.getRandomValues(a);

    return (a[0].toString(16) + a[1].toString(16));

  }


  function pickType(){

    // No shoot chance increases slowly, stays low

    const noShootChance = clamp(0.06 + (state.difficulty - 1) * 0.05, 0.06, 0.16);

    if(Math.random() < noShootChance) return "noshoot";


    const r = Math.random();

    let acc = 0;

    const pool = ["rat", "fox", "deer"];

    for(const k of pool){

      acc += TYPES[k].weight;

      if(r <= acc) return k;

    }

    return "fox";

  }


  function spawnTarget(){

    const type = pickType();

    const def = TYPES[type];


    const baseR = rnd(def.baseR[0], def.baseR[1]);

    const r = clamp(baseR / (0.96 + (state.difficulty - 1) * 0.32), 16, 52);


    const baseLife = rnd(def.life[0], def.life[1]);

    const life = clamp(baseLife / (1.00 + (state.difficulty - 1) * 0.42), 0.60, 2.8);


    const x = rnd(r + 14, W - r - 14);

    const y = rnd(r + 14, H - r - 14);


    const speedBase = rnd(def.speed[0], def.speed[1]);

    const speed = speedBase * (0.25 + state.difficulty * 0.45);

    const ang = rnd(0, Math.PI * 2);


    state.targets.push({

      id: cryptoRandomId(),

      type,

      emoji: def.emoji,

      x, y, r,

      vx: Math.cos(ang) * speed,

      vy: Math.sin(ang) * speed,

      life,

      born: performance.now(),

      removed: false

    });

  }


  function resetRun(){

    state.running = false;

    state.score = 0;

    state.combo = 0;

    state.difficulty = 1.0;

    state.targets = [];

    state.spawnTimer = 0;

    state.spawnInterval = 1.25;

    state.maxOnScreen = 1;

    state.lastClickFx = null;

    state.particles = [];

    state.runStartTs = 0;

    state.shake = 0;


    scorePill.textContent = "Score: 0";

    comboPill.textContent = "Combo: 0";

    levelPill.textContent = "Difficulty: 1.0";

    msg.textContent = "Press Start.";


    startBtn.textContent = "Start";

    startBtn.disabled = false;

  }


  function endRun(reason){

    if(!state.running) return;

    state.running = false;


    addScoreToBoard(state.score);

    renderBoard();


    msg.textContent = `${reason} Final score: ${state.score.toLocaleString()}.`;

    startBtn.textContent = "Play again";

    startBtn.disabled = false;

  }


  function startRun(){

    resetRun();

    state.running = true;

    state.lastTs = performance.now();

    state.runStartTs = performance.now();

    msg.textContent = "Go.";

    startBtn.disabled = true;


    spawnTarget();

  }


  function addParticles(x, y, ok){

    const n = ok ? 14 : 10;

    for(let i = 0; i < n; i++){

      const a = rnd(0, Math.PI * 2);

      const s = rnd(60, ok ? 220 : 160);

      state.particles.push({

        x, y,

        vx: Math.cos(a) * s,

        vy: Math.sin(a) * s,

        life: rnd(0.22, 0.55),

        born: performance.now(),

        ok

      });

    }

  }


  function update(dt){

    const now = performance.now();


    // Particles

    state.particles = state.particles.filter(p => (now - p.born) / 1000 < p.life);

    for(const p of state.particles){

      p.x += p.vx * dt;

      p.y += p.vy * dt;

      p.vx *= (1 - 2.8 * dt);

      p.vy *= (1 - 2.8 * dt);

    }


    // Shake decay

    state.shake = Math.max(0, state.shake - dt * 9);


    if(!state.running) return;


    state.difficulty = difficultyFromScore(state.score);

    progressionTuning();

    levelPill.textContent = `Difficulty: ${state.difficulty.toFixed(1)}`;


    state.spawnTimer += dt;


    const onScreen = state.targets.filter(t => !t.removed).length;

    while(state.spawnTimer >= state.spawnInterval && onScreen < state.maxOnScreen){

      state.spawnTimer -= state.spawnInterval;

      spawnTarget();

    }


    const secondsIn = (now - state.runStartTs) / 1000;

    const inWarmup = secondsIn < state.warmupSeconds;


    for(const t of state.targets){

      if(t.removed) continue;


      t.x += t.vx * dt;

      t.y += t.vy * dt;


      if(t.x < t.r){ t.x = t.r; t.vx *= -1; }

      if(t.x > W - t.r){ t.x = W - t.r; t.vx *= -1; }

      if(t.y < t.r){ t.y = t.r; t.vy *= -1; }

      if(t.y > H - t.r){ t.y = H - t.r; t.vy *= -1; }


      const age = (now - t.born) / 1000;

      if(age > t.life){

        if(t.type !== "noshoot"){

          if(inWarmup){

            state.combo = 0;

            comboPill.textContent = "Combo: 0";

            msg.textContent = "Too slow, combo reset.";

            t.removed = true;

          } else {

            endRun("Missed an animal.");

            return;

          }

        } else {

          t.removed = true;

        }

      }

    }


    state.targets = state.targets.filter(t => !t.removed);

  }


  function drawBackground(){

    // subtle range style: faint grid and horizon band

    ctx.save();

    ctx.globalAlpha = 0.12;

    ctx.strokeStyle = "#a8b5d6";

    ctx.lineWidth = 1;


    const step = 56;

    for(let x = 0; x < W; x += step){

      ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();

    }

    for(let y = 0; y < H; y += step){

      ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();

    }

    ctx.restore();


    ctx.save();

    ctx.globalAlpha = 0.08;

    ctx.fillStyle = "#cbd2e1";

    ctx.fillRect(0, H * 0.62, W, 2);

    ctx.restore();

  }


  function drawTarget(t){

    const now = performance.now();

    const age = (now - t.born) / 1000;

    const life01 = clamp(1 - age / t.life, 0, 1);


    // Gentle size pulse

    const rr = t.r * (0.92 + 0.08 * life01);


    // Outer ring glow

    ctx.save();

    ctx.globalAlpha = 0.95;


    const isNoShoot = t.type === "noshoot";

    ctx.strokeStyle = isNoShoot ? "#ff6b6b" : "#ffdf6e";

    ctx.lineWidth = 3;


    ctx.beginPath();

    ctx.arc(t.x, t.y, rr, 0, Math.PI * 2);

    ctx.stroke();


    // Soft fill

    ctx.globalAlpha = isNoShoot ? 0.18 : 0.16;

    ctx.fillStyle = isNoShoot ? "#ff6b6b" : "#ffdf6e";

    ctx.beginPath();

    ctx.arc(t.x, t.y, rr * 0.72, 0, Math.PI * 2);

    ctx.fill();


    // Inner ring

    ctx.globalAlpha = isNoShoot ? 0.55 : 0.45;

    ctx.strokeStyle = isNoShoot ? "#ff6b6b" : "#ffdf6e";

    ctx.lineWidth = 2;

    ctx.beginPath();

    ctx.arc(t.x, t.y, rr * 0.46, 0, Math.PI * 2);

    ctx.stroke();


    // Emoji icon

    ctx.globalAlpha = 0.95;

    ctx.font = `${Math.round(rr * 1.25)}px system-ui, Apple Color Emoji, Segoe UI Emoji`;

    ctx.textAlign = "center";

    ctx.textBaseline = "middle";

    ctx.fillStyle = "#ffffff";

    ctx.fillText(t.emoji, t.x, t.y + 1);


    // Lifetime arc

    ctx.globalAlpha = 0.35;

    ctx.strokeStyle = "#cbd2e1";

    ctx.lineWidth = 2;

    ctx.beginPath();

    ctx.arc(t.x, t.y, rr + 9, -Math.PI/2, -Math.PI/2 + Math.PI*2*life01);

    ctx.stroke();


    ctx.restore();

  }


  function drawParticles(){

    const now = performance.now();

    for(const p of state.particles){

      const a = (now - p.born) / 1000;

      const life01 = clamp(1 - a / p.life, 0, 1);

      ctx.save();

      ctx.globalAlpha = 0.65 * life01;

      ctx.fillStyle = p.ok ? "#9eff7a" : "#ff6b6b";

      ctx.beginPath();

      ctx.arc(p.x, p.y, 2 + (1 - life01) * 2.5, 0, Math.PI*2);

      ctx.fill();

      ctx.restore();

    }

  }


  function draw(){

    ctx.clearRect(0, 0, W, H);


    // Camera shake

    const sh = state.shake;

    const sx = sh ? rnd(-sh, sh) * 6 : 0;

    const sy = sh ? rnd(-sh, sh) * 6 : 0;


    ctx.save();

    ctx.translate(sx, sy);


    // Vignette

    const cx = W/2, cy = H/2;

    const g = ctx.createRadialGradient(cx, cy, 40, cx, cy, Math.max(W,H)*0.65);

    g.addColorStop(0, "rgba(0,0,0,0.00)");

    g.addColorStop(1, "rgba(0,0,0,0.62)");

    ctx.fillStyle = g;

    ctx.fillRect(0,0,W,H);


    drawBackground();


    // Targets

    for(const t of state.targets) drawTarget(t);


    // Particles and click ring

    drawParticles();


    if(state.lastClickFx){

      const fx = state.lastClickFx;

      const age = (performance.now() - fx.at) / 1000;

      if(age < 0.26){

        const p = age / 0.26;

        ctx.save();

        ctx.globalAlpha = 0.55 * (1 - p);

        ctx.strokeStyle = fx.ok ? "#9eff7a" : "#ff6b6b";

        ctx.lineWidth = 2;

        ctx.beginPath();

        ctx.arc(fx.x, fx.y, 8 + p*28, 0, Math.PI*2);

        ctx.stroke();

        ctx.restore();

      } else {

        state.lastClickFx = null;

      }

    }


    // Reticle centre marker

    ctx.save();

    ctx.globalAlpha = 0.65;

    ctx.strokeStyle = "#e8e8e8";

    ctx.lineWidth = 2;

    ctx.beginPath();

    ctx.moveTo(W/2 - 18, H/2); ctx.lineTo(W/2 - 6, H/2);

    ctx.moveTo(W/2 + 6, H/2); ctx.lineTo(W/2 + 18, H/2);

    ctx.moveTo(W/2, H/2 - 18); ctx.lineTo(W/2, H/2 - 6);

    ctx.moveTo(W/2, H/2 + 6); ctx.lineTo(W/2, H/2 + 18);

    ctx.stroke();

    ctx.restore();


    // Overlay prompt when idle

    if(!state.running){

      ctx.save();

      ctx.globalAlpha = 0.78;

      ctx.fillStyle = "#0b0d12";

      ctx.fillRect(0,0,W,H);

      ctx.globalAlpha = 1;

      ctx.fillStyle = "#e8e8e8";

      ctx.font = "750 30px system-ui, -apple-system, Segoe UI, Roboto, Arial";

      ctx.fillText("Animal Run", 28, 58);

      ctx.font = "14px system-ui, -apple-system, Segoe UI, Roboto, Arial";

      ctx.fillStyle = "#cbd2e1";

      ctx.fillText("Shoot the animals, avoid the 'don't shoot' sign.", 28, 86);

      ctx.fillText("Press Start to play.", 28, 110);

      ctx.restore();

    }


    ctx.restore();

  }


  function scoreFor(type, r){

    const def = TYPES[type];

    const sizeFactor = clamp(44 / r, 0.9, 3.2);

    const diffFactor = clamp(state.difficulty, 1.0, 4.2);

    const comboFactor = 1 + Math.min(state.combo, 20) * 0.05;

    return Math.round(def.points * sizeFactor * diffFactor * comboFactor);

  }


  function handleClick(ev){

    const rect = cv.getBoundingClientRect();

    const x = (ev.clientX - rect.left) * (W / rect.width);

    const y = (ev.clientY - rect.top) * (H / rect.height);


    if(!state.running) return;


    let hit = null;

    for(let i = state.targets.length - 1; i >= 0; i--){

      const t = state.targets[i];

      const dx = x - t.x;

      const dy = y - t.y;

      if(dx*dx + dy*dy <= t.r*t.r){ hit = t; break; }

    }


    if(!hit){

      state.combo = 0;

      comboPill.textContent = "Combo: 0";

      state.lastClickFx = { x, y, ok:false, at: performance.now() };

      addParticles(x, y, false);

      msg.textContent = "Miss click, combo reset.";

      return;

    }


    if(hit.type === "noshoot"){

      state.lastClickFx = { x, y, ok:false, at: performance.now() };

      addParticles(x, y, false);

      state.shake = 1.0;

      endRun("No shoot hit.");

      return;

    }


    hit.removed = true;

    state.combo += 1;


    const add = scoreFor(hit.type, hit.r);

    state.score += add;


    state.lastClickFx = { x, y, ok:true, at: performance.now() };

    addParticles(x, y, true);

    state.shake = 0.35;


    scorePill.textContent = `Score: ${state.score.toLocaleString()}`;

    comboPill.textContent = `Combo: ${state.combo.toLocaleString()}`;

    msg.textContent = `+${add} points.`;

  }


  function tick(ts){

    const dt = Math.min((ts - state.lastTs) / 1000, 0.05);

    state.lastTs = ts;


    update(dt);

    draw();


    requestAnimationFrame(tick);

  }


  startBtn.addEventListener("click", startRun);

  resetBtn.addEventListener("click", () => {

    localStorage.removeItem(STORAGE_KEY);

    renderBoard();

    msg.textContent = "Leaderboard reset.";

  });


  cv.addEventListener("click", handleClick);

  cv.addEventListener("touchstart", (e) => {

    if(!e.changedTouches || !e.changedTouches.length) return;

    const t = e.changedTouches[0];

    handleClick({ clientX: t.clientX, clientY: t.clientY });

  }, { passive:true });


  renderBoard();

  resetRun();

  requestAnimationFrame(tick);

})();

</script>

</body>

</html>