|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | +<meta charset="UTF-8" /> |
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| 6 | +<title>CatchMyCapybara</title> |
| 7 | +<style> |
| 8 | + :root{ |
| 9 | + --bg:#f3efe7; |
| 10 | + --ink:#1f1d1a; |
| 11 | + --grass:#cfe8b4; |
| 12 | + --puddle:#a8d8ff; |
| 13 | + --capy:#8a5a44; |
| 14 | + --capy-dark:#6f4636; |
| 15 | + --accent:#ff7a59; |
| 16 | + } |
| 17 | + *{box-sizing:border-box} |
| 18 | + html,body{height:100%} |
| 19 | + body{ |
| 20 | + margin:0; font-family:system-ui,Segoe UI,Arial,sans-serif; color:var(--ink); |
| 21 | + background:radial-gradient(1200px 800px at 20% 20%,#fff 0%,#f9f7f2 60%,#efeae0 100%); |
| 22 | + display:flex; flex-direction:column; align-items:center; gap:14px; padding:16px; |
| 23 | + } |
| 24 | + |
| 25 | + h1{margin:6px 0 2px; font-weight:800; letter-spacing:.2px} |
| 26 | + .hud{ |
| 27 | + display:flex; flex-wrap:wrap; gap:10px; align-items:center; justify-content:center |
| 28 | + } |
| 29 | + .pill{ |
| 30 | + background:#fff; border:1px solid #e8e1d5; padding:8px 12px; border-radius:999px; |
| 31 | + box-shadow:0 3px 10px rgba(0,0,0,.06) |
| 32 | + } |
| 33 | + .btn{ |
| 34 | + border:none; padding:10px 16px; border-radius:12px; font-weight:700; cursor:pointer; |
| 35 | + background:var(--ink); color:#fff; box-shadow:0 6px 14px rgba(0,0,0,.2); transition:.15s; |
| 36 | + } |
| 37 | + .btn:hover{transform:translateY(-1px)} |
| 38 | + .btn:active{transform:translateY(0)} |
| 39 | + .board-wrap{ |
| 40 | + position:relative; width:min(92vw,800px); aspect-ratio:16/9; max-height:68vh; |
| 41 | + border-radius:20px; border:2px solid #e8e1d5; overflow:hidden; |
| 42 | + background: |
| 43 | + radial-gradient(200px 120px at 15% 75%, #d6f1b9 0%, #cbe7ad 60%, #c2e09f 100%), |
| 44 | + radial-gradient(240px 160px at 80% 25%, #d6f1b9 0%, #cbe7ad 60%, #c2e09f 100%), |
| 45 | + var(--grass); |
| 46 | + box-shadow:0 12px 28px rgba(0,0,0,.15), inset 0 0 0 1px rgba(255,255,255,.5); |
| 47 | + touch-action:manipulation; |
| 48 | + } |
| 49 | + |
| 50 | + /* Puddles (safe-ish zones where capybara slows down) */ |
| 51 | + .puddle{ |
| 52 | + position:absolute; width:160px; height:110px; background:var(--puddle); |
| 53 | + opacity:.7; border-radius:50% 48% 54% 46% / 56% 44% 56% 44%; |
| 54 | + filter:blur(.4px); box-shadow:inset 0 0 12px rgba(0,0,0,.12); |
| 55 | + } |
| 56 | + |
| 57 | + /* Carrot powerup */ |
| 58 | + .carrot{ |
| 59 | + position:absolute; width:28px; height:28px; transform:translate(-50%,-50%) rotate(-12deg); |
| 60 | + } |
| 61 | + .carrot:before, .carrot:after{ |
| 62 | + content:""; position:absolute; border-radius:6px; |
| 63 | + } |
| 64 | + .carrot:before{ /* body */ |
| 65 | + left:8px; top:8px; width:14px; height:18px; background:linear-gradient(#ffa35e,#ff7a2d); |
| 66 | + border:1px solid rgba(0,0,0,.1); |
| 67 | + } |
| 68 | + .carrot:after{ /* leaves */ |
| 69 | + left:5px; top:-2px; width:20px; height:10px; background:linear-gradient(#6bd58c,#3fbf6c); |
| 70 | + border-radius:10px 10px 2px 2px; transform:rotate(8deg); |
| 71 | + } |
| 72 | + |
| 73 | + /* Capybara sprite (CSS art) */ |
| 74 | + .capy{ |
| 75 | + position:absolute; width:70px; height:46px; transform:translate(-50%,-50%); |
| 76 | + filter:drop-shadow(0 6px 6px rgba(0,0,0,.25)); |
| 77 | + } |
| 78 | + .capy .body{ |
| 79 | + position:absolute; inset:0; background:linear-gradient(180deg,var(--capy),var(--capy-dark)); |
| 80 | + border-radius:18px 24px 22px 22px; |
| 81 | + } |
| 82 | + .capy .head{ |
| 83 | + position:absolute; right:-18px; top:8px; width:36px; height:26px; |
| 84 | + background:linear-gradient(180deg,var(--capy),var(--capy-dark)); |
| 85 | + border-radius:18px; transform:rotate(4deg); |
| 86 | + } |
| 87 | + .capy .ear{ |
| 88 | + position:absolute; right:10px; top:-6px; width:14px; height:10px; |
| 89 | + background:var(--capy-dark); border-radius:10px 10px 2px 2px; |
| 90 | + } |
| 91 | + .capy .eye{ |
| 92 | + position:absolute; right:10px; top:12px; width:6px; height:6px; background:#111; border-radius:50%; |
| 93 | + } |
| 94 | + .capy .nose{ |
| 95 | + position:absolute; right:-2px; top:18px; width:6px; height:6px; background:#2a2220; border-radius:50%; |
| 96 | + } |
| 97 | + .capy .foot{ |
| 98 | + position:absolute; bottom:-4px; width:12px; height:6px; background:#3a2b24; border-radius:6px; |
| 99 | + } |
| 100 | + .capy .f1{left:10px} .capy .f2{left:28px} .capy .f3{left:48px} |
| 101 | + |
| 102 | + .toast{ |
| 103 | + position:absolute; left:50%; top:10px; transform:translateX(-50%); |
| 104 | + background:#111; color:#fff; padding:8px 12px; border-radius:999px; |
| 105 | + font-weight:700; opacity:0; transition:.25s; pointer-events:none; |
| 106 | + } |
| 107 | + .toast.show{opacity:1; transform:translateX(-50%) translateY(6px)} |
| 108 | + .muted{opacity:.5} |
| 109 | +</style> |
| 110 | +</head> |
| 111 | +<body> |
| 112 | + <h1>CatchMyCapybara</h1> |
| 113 | + <div class="hud"> |
| 114 | + <div class="pill">⏱️ Time: <span id="time">30</span>s</div> |
| 115 | + <div class="pill">⭐ Score: <span id="score">0</span></div> |
| 116 | + <div class="pill">🏆 Best: <span id="best">0</span></div> |
| 117 | + <button id="startBtn" class="btn">Start</button> |
| 118 | + <button id="pauseBtn" class="btn" disabled>Pause</button> |
| 119 | + </div> |
| 120 | + |
| 121 | + <div id="board" class="board-wrap" aria-label="Play area"> |
| 122 | + <!-- Puddles --> |
| 123 | + <div class="puddle" style="left:18%; top:68%"></div> |
| 124 | + <div class="puddle" style="left:76%; top:34%"></div> |
| 125 | + <div class="puddle" style="left:50%; top:82%"></div> |
| 126 | + |
| 127 | + <!-- Capybara --> |
| 128 | + <div id="capy" class="capy" role="button" aria-label="Capybara" tabindex="0"> |
| 129 | + <div class="body"></div> |
| 130 | + <div class="head"></div> |
| 131 | + <div class="ear"></div> |
| 132 | + <div class="eye"></div> |
| 133 | + <div class="nose"></div> |
| 134 | + <div class="foot f1"></div> |
| 135 | + <div class="foot f2"></div> |
| 136 | + <div class="foot f3"></div> |
| 137 | + </div> |
| 138 | + |
| 139 | + <!-- Carrot powerup (spawned by JS) --> |
| 140 | + </div> |
| 141 | + |
| 142 | + <div id="toast" class="toast">Nice catch! +1</div> |
| 143 | + |
| 144 | +<script> |
| 145 | +(() => { |
| 146 | + const board = document.getElementById('board'); |
| 147 | + const capy = document.getElementById('capy'); |
| 148 | + const timeEl= document.getElementById('time'); |
| 149 | + const scoreEl=document.getElementById('score'); |
| 150 | + const bestEl =document.getElementById('best'); |
| 151 | + const startBtn=document.getElementById('startBtn'); |
| 152 | + const pauseBtn=document.getElementById('pauseBtn'); |
| 153 | + const toast=document.getElementById('toast'); |
| 154 | + |
| 155 | + // State |
| 156 | + let running=false, paused=false, timer=30, score=0, speed=220; // px/sec base |
| 157 | + let vx=0, vy=0; // current vector |
| 158 | + let lastT=0, rafId=null, dodgeCooldown=0, slowMoUntil=0; |
| 159 | + const puddles=[...board.querySelectorAll('.puddle')].map(p=>rectOf(p)); |
| 160 | + let carrotEl=null, nextCarrotAt=0; |
| 161 | + |
| 162 | + // Persist best |
| 163 | + const BEST_KEY='catchmycapybara_best'; |
| 164 | + bestEl.textContent = localStorage.getItem(BEST_KEY) || 0; |
| 165 | + |
| 166 | + function rectOf(el){ |
| 167 | + const r=el.getBoundingClientRect(); |
| 168 | + const br=board.getBoundingClientRect(); |
| 169 | + return {x:r.left-br.left, y:r.top-br.top, w:r.width, h:r.height}; |
| 170 | + } |
| 171 | + function capyPos(){ |
| 172 | + return rectOf(capy); |
| 173 | + } |
| 174 | + function inPuddle(x,y){ |
| 175 | + return puddles.some(p => x>p.x && x<p.x+p.w && y>p.y && y<p.y+p.h); |
| 176 | + } |
| 177 | + function clamp(v,min,max){ return Math.max(min, Math.min(max, v)); } |
| 178 | + function rnd(a,b){ return Math.random()*(b-a)+a; } |
| 179 | + |
| 180 | + function setCapy(x,y){ |
| 181 | + const b=rectOf(board); |
| 182 | + const cx = clamp(x, 40, b.w-40); |
| 183 | + const cy = clamp(y, 30, b.h-30); |
| 184 | + capy.style.left = cx+'px'; |
| 185 | + capy.style.top = cy+'px'; |
| 186 | + } |
| 187 | + |
| 188 | + function randomizeVector(){ |
| 189 | + const angle = rnd(0, Math.PI*2); |
| 190 | + vx = Math.cos(angle); |
| 191 | + vy = Math.sin(angle); |
| 192 | + } |
| 193 | + |
| 194 | + function toastMsg(msg){ |
| 195 | + toast.textContent = msg; |
| 196 | + toast.classList.add('show'); |
| 197 | + clearTimeout(toast._t); |
| 198 | + toast._t = setTimeout(()=>toast.classList.remove('show'), 800); |
| 199 | + } |
| 200 | + |
| 201 | + function reset(){ |
| 202 | + running=false; paused=false; |
| 203 | + timer=30; score=0; speed=220; vx=0; vy=0; lastT=0; dodgeCooldown=0; slowMoUntil=0; |
| 204 | + timeEl.textContent=timer; scoreEl.textContent=score; |
| 205 | + startBtn.disabled=false; pauseBtn.disabled=true; pauseBtn.textContent='Pause'; |
| 206 | + // center capy |
| 207 | + setCapy(board.clientWidth*0.5, board.clientHeight*0.5); |
| 208 | + // remove carrot |
| 209 | + if(carrotEl){ carrotEl.remove(); carrotEl=null; } |
| 210 | + nextCarrotAt = performance.now() + 2500; |
| 211 | + } |
| 212 | + |
| 213 | + function start(){ |
| 214 | + if(running){ return; } |
| 215 | + running=true; paused=false; startBtn.disabled=true; pauseBtn.disabled=false; |
| 216 | + randomizeVector(); |
| 217 | + lastT=performance.now(); |
| 218 | + rafId = requestAnimationFrame(loop); |
| 219 | + } |
| 220 | + |
| 221 | + function pauseToggle(){ |
| 222 | + if(!running) return; |
| 223 | + paused=!paused; |
| 224 | + pauseBtn.textContent = paused ? 'Resume' : 'Pause'; |
| 225 | + if(!paused){ |
| 226 | + lastT=performance.now(); |
| 227 | + rafId=requestAnimationFrame(loop); |
| 228 | + }else{ |
| 229 | + cancelAnimationFrame(rafId); |
| 230 | + } |
| 231 | + } |
| 232 | + |
| 233 | + function spawnCarrot(){ |
| 234 | + if(carrotEl) return; |
| 235 | + carrotEl = document.createElement('div'); |
| 236 | + carrotEl.className='carrot'; |
| 237 | + const x=rnd(40, board.clientWidth-40); |
| 238 | + const y=rnd(40, board.clientHeight-40); |
| 239 | + carrotEl.style.left=x+'px'; |
| 240 | + carrotEl.style.top =y+'px'; |
| 241 | + board.appendChild(carrotEl); |
| 242 | + } |
| 243 | + |
| 244 | + function collectCarrotIfHit(px,py){ |
| 245 | + if(!carrotEl) return; |
| 246 | + const r = rectOf(carrotEl); |
| 247 | + const dx = px - r.x, dy = py - r.y; |
| 248 | + if(Math.abs(dx)<22 && Math.abs(dy)<22){ |
| 249 | + // Slow motion 3s |
| 250 | + slowMoUntil = performance.now()+3000; |
| 251 | + carrotEl.remove(); carrotEl=null; |
| 252 | + toastMsg('🥕 Slow-mo!'); |
| 253 | + } |
| 254 | + } |
| 255 | + |
| 256 | + function onCatch(){ |
| 257 | + if(!running || paused) return; |
| 258 | + score++; scoreEl.textContent=score; |
| 259 | + speed = Math.min(520, speed + 14); // ramps difficulty |
| 260 | + toastMsg('Nice catch! +1'); |
| 261 | + // hop to a new random spot |
| 262 | + setCapy(rnd(40, board.clientWidth-40), rnd(30, board.clientHeight-30)); |
| 263 | + } |
| 264 | + |
| 265 | + // Evasive movement near pointer/touch |
| 266 | + function nudgeAwayFrom(px,py){ |
| 267 | + const c = capyPos(); |
| 268 | + const cx = c.x + c.w/2, cy=c.y + c.h/2; |
| 269 | + const dx = cx - px, dy = cy - py; |
| 270 | + const dist = Math.hypot(dx,dy); |
| 271 | + if(dist<140){ |
| 272 | + vx = (dx/dist) || vx; |
| 273 | + vy = (dy/dist) || vy; |
| 274 | + dodgeCooldown = 220; // ms resist randomization |
| 275 | + } |
| 276 | + } |
| 277 | + |
| 278 | + // Pointer handling |
| 279 | + let lastPointer={x:0,y:0}; |
| 280 | + board.addEventListener('mousemove', e=>{ |
| 281 | + const br=board.getBoundingClientRect(); |
| 282 | + lastPointer={x:e.clientX-br.left, y:e.clientY-br.top}; |
| 283 | + nudgeAwayFrom(lastPointer.x,lastPointer.y); |
| 284 | + }); |
| 285 | + board.addEventListener('touchmove', e=>{ |
| 286 | + const t=e.touches[0]; if(!t) return; |
| 287 | + const br=board.getBoundingClientRect(); |
| 288 | + lastPointer={x:t.clientX-br.left, y:t.clientY-br.top}; |
| 289 | + nudgeAwayFrom(lastPointer.x,lastPointer.y); |
| 290 | + }, {passive:true}); |
| 291 | + |
| 292 | + // Catch interactions |
| 293 | + capy.addEventListener('click', onCatch); |
| 294 | + capy.addEventListener('touchstart', (e)=>{ e.preventDefault(); onCatch(); }, {passive:false}); |
| 295 | + capy.addEventListener('keydown', (e)=>{ if(e.key==='Enter' || e.key===' ') onCatch(); }); |
| 296 | + |
| 297 | + startBtn.addEventListener('click', start); |
| 298 | + pauseBtn.addEventListener('click', pauseToggle); |
| 299 | + |
| 300 | + function loop(t){ |
| 301 | + if(!running || paused) return; |
| 302 | + |
| 303 | + const dt = (t - lastT) / 1000; // seconds |
| 304 | + lastT = t; |
| 305 | + |
| 306 | + // Timer |
| 307 | + timer -= dt; |
| 308 | + if(timer<=0){ |
| 309 | + timer=0; running=false; |
| 310 | + timeEl.textContent='0'; |
| 311 | + cancelAnimationFrame(rafId); |
| 312 | + // Best |
| 313 | + const best = Math.max(Number(localStorage.getItem(BEST_KEY)||0), score); |
| 314 | + localStorage.setItem(BEST_KEY, best); |
| 315 | + bestEl.textContent=best; |
| 316 | + toastMsg(`Time! Final: ${score}`); |
| 317 | + startBtn.disabled=false; pauseBtn.disabled=true; pauseBtn.textContent='Pause'; |
| 318 | + return; |
| 319 | + } |
| 320 | + timeEl.textContent = Math.ceil(timer); |
| 321 | + |
| 322 | + // Randomize vector occasionally (unless dodging) |
| 323 | + if(dodgeCooldown>0){ dodgeCooldown -= (t - lastT); } |
| 324 | + else if(Math.random()<0.015){ randomizeVector(); } |
| 325 | + |
| 326 | + // Slow-mo factor |
| 327 | + const slowFactor = (t<slowMoUntil) ? 0.45 : 1.0; |
| 328 | + (t<slowMoUntil) ? capy.classList.add('muted') : capy.classList.remove('muted'); |
| 329 | + |
| 330 | + // Prefer puddles: if not inside any, bias vector toward nearest puddle |
| 331 | + const c = capyPos(); |
| 332 | + const cx = c.x + c.w/2, cy=c.y + c.h/2; |
| 333 | + if(!inPuddle(cx,cy) && puddles.length){ |
| 334 | + let best=null, bd=1e9; |
| 335 | + for(const p of puddles){ |
| 336 | + const px = clamp(cx, p.x, p.x+p.w); |
| 337 | + const py = clamp(cy, p.y, p.y+p.h); |
| 338 | + const d = Math.hypot(px-cx, py-cy); |
| 339 | + if(d<bd){ bd=d; best={x:px,y:py}; } |
| 340 | + } |
| 341 | + if(best && bd>1){ |
| 342 | + vx += (best.x - cx)/bd * 0.015; |
| 343 | + vy += (best.y - cy)/bd * 0.015; |
| 344 | + const mag=Math.hypot(vx,vy)||1; vx/=mag; vy/=mag; |
| 345 | + } |
| 346 | + }else{ |
| 347 | + // When in puddle, capy goes slower (nice for player) |
| 348 | + } |
| 349 | + |
| 350 | + // Move |
| 351 | + const base = speed * slowFactor * (inPuddle(cx,cy)? 0.55 : 1); |
| 352 | + let nx = cx + vx * base * dt; |
| 353 | + let ny = cy + vy * base * dt; |
| 354 | + |
| 355 | + // Bounce on edges |
| 356 | + const b=rectOf(board); |
| 357 | + if(nx<40 || nx>b.w-40){ vx*=-1; nx=clamp(nx,40,b.w-40); } |
| 358 | + if(ny<30 || ny>b.h-30){ vy*=-1; ny=clamp(ny,30,b.h-30); } |
| 359 | + |
| 360 | + setCapy(nx, ny); |
| 361 | + |
| 362 | + // Spawn carrot every ~2.5–5s |
| 363 | + if(t > nextCarrotAt){ |
| 364 | + spawnCarrot(); |
| 365 | + nextCarrotAt = t + (2500 + Math.random()*2500); |
| 366 | + } |
| 367 | + // Collect carrot if capy overlaps |
| 368 | + collectCarrotIfHit(nx,ny); |
| 369 | + |
| 370 | + rafId = requestAnimationFrame(loop); |
| 371 | + } |
| 372 | + |
| 373 | + // Initialize |
| 374 | + window.addEventListener('resize', () => setCapy(board.clientWidth*0.5, board.clientHeight*0.5)); |
| 375 | + reset(); |
| 376 | +})(); |
| 377 | +</script> |
| 378 | +</body> |
| 379 | +</html> |
0 commit comments