Skip to content

Commit 03bcfb1

Browse files
committed
Added CatchMyCapybara
1 parent 2c9648a commit 03bcfb1

File tree

1 file changed

+379
-0
lines changed

1 file changed

+379
-0
lines changed
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
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

Comments
 (0)