<!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>