# Frogger AI Prompt and results

> Source: <https://gist.github.com/ivanfioravanti/6042a6b02a7a95648b797fd6ec1c0760>
> Published: 2026-06-14 09:36:08+00:00

|
<!doctype html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf-8" /> |
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" /> |
|
<title>HOME FRONT — a frog's commute through the modern home</title> |
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,700;12..96,800&family=Sora:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
<style> |
|
:root{ |
|
--ink:#20232b; |
|
--cream:#efe7d6; |
|
--cream-2:#e4d9c2; |
|
--paper:#f6f0e2; |
|
--stage:#16181d; |
|
--stage-2:#23262e; |
|
--bezel:#1b1d22; |
|
--coral:#ff5a3c; |
|
--coral-d:#d8442b; |
|
--gold:#f3b13d; |
|
--green:#7ed957; |
|
--green-d:#4ea632; |
|
--blue:#7fc6ff; |
|
--muted:#8a8576; |
|
--shadow:0 18px 40px -18px rgba(0,0,0,.65); |
|
} |
|
*{box-sizing:border-box} |
|
html,body{height:100%} |
|
body{ |
|
margin:0; |
|
font-family:"Sora",system-ui,sans-serif; |
|
color:var(--cream); |
|
background: |
|
radial-gradient(120% 80% at 18% -10%, #2a2e38 0%, rgba(42,46,56,0) 55%), |
|
radial-gradient(120% 90% at 110% 110%, #2c1d18 0%, rgba(44,29,24,0) 50%), |
|
linear-gradient(160deg,#15171c 0%, #1d2027 60%, #14161b 100%); |
|
min-height:100dvh; |
|
display:flex; |
|
align-items:center; |
|
justify-content:center; |
|
padding:18px; |
|
overflow:hidden; |
|
} |
|
/* grain */ |
|
body::before{ |
|
content:"";position:fixed;inset:0;pointer-events:none;opacity:.05;mix-blend-mode:overlay; |
|
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.6'/></svg>"); |
|
} |
|
|
|
.cabinet{ |
|
position:relative; |
|
width:min(94vw, 560px); |
|
background:linear-gradient(180deg,var(--cream) 0%, var(--cream-2) 100%); |
|
border-radius:26px; |
|
padding:14px 14px 12px; |
|
box-shadow: |
|
0 1px 0 rgba(255,255,255,.7) inset, |
|
0 -6px 0 rgba(0,0,0,.06) inset, |
|
var(--shadow); |
|
border:1px solid rgba(0,0,0,.18); |
|
} |
|
/* brass corner screws */ |
|
.cabinet::before,.cabinet::after, |
|
.screw-bl,.screw-br{ |
|
content:"";position:absolute;width:11px;height:11px;border-radius:50%; |
|
background:radial-gradient(circle at 35% 30%, #f4d488, #b8862f 60%, #6e4d18); |
|
box-shadow:0 1px 1px rgba(0,0,0,.35); |
|
} |
|
.cabinet::before{top:9px;left:9px} |
|
.cabinet::after{top:9px;right:9px} |
|
.screw-bl{bottom:9px;left:9px} |
|
.screw-br{bottom:9px;right:9px} |
|
|
|
.topbar{ |
|
display:flex;align-items:center;justify-content:space-between;gap:10px; |
|
padding:4px 22px 10px; |
|
} |
|
.brand{display:flex;flex-direction:column;line-height:1} |
|
.brand h1{ |
|
font-family:"Bricolage Grotesque",sans-serif; |
|
font-weight:800;font-size:clamp(20px,4.6vw,26px); |
|
letter-spacing:.02em;margin:0;color:var(--ink); |
|
display:flex;align-items:center;gap:7px; |
|
} |
|
.brand h1 .dot{ |
|
width:11px;height:11px;border-radius:50%; |
|
background:var(--coral);box-shadow:0 0 0 3px rgba(255,90,60,.18); |
|
} |
|
.brand .sub{ |
|
font-size:10.5px;color:var(--muted);margin-top:5px;font-weight:500; |
|
letter-spacing:.06em;text-transform:uppercase; |
|
} |
|
.stats{display:flex;gap:7px;align-items:stretch} |
|
.chip{ |
|
background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:11px; |
|
padding:5px 9px 6px;min-width:54px;text-align:center; |
|
box-shadow:0 1px 0 rgba(255,255,255,.8) inset, 0 2px 6px -3px rgba(0,0,0,.3); |
|
} |
|
.chip .k{display:block;font-size:8.5px;letter-spacing:.14em;color:var(--muted);font-weight:600;text-transform:uppercase} |
|
.chip .v{display:block;font-family:"Bricolage Grotesque",sans-serif;font-weight:800;font-size:17px;color:var(--ink);font-variant-numeric:tabular-nums;line-height:1.1} |
|
.chip.coral .v{color:var(--coral-d)} |
|
|
|
.lives{display:flex;gap:3px;align-items:center;justify-content:center;padding-top:2px} |
|
.lives .frog{width:16px;height:16px;display:block} |
|
|
|
.timerbar{ |
|
height:9px;border-radius:6px;margin:0 4px 8px; |
|
background:rgba(0,0,0,.16);overflow:hidden; |
|
box-shadow:0 1px 2px rgba(0,0,0,.2) inset; |
|
} |
|
.timerbar > i{ |
|
display:block;height:100%;width:100%; |
|
background:linear-gradient(90deg,var(--gold),var(--coral)); |
|
border-radius:6px;transition:width .12s linear, background .3s; |
|
} |
|
.timerbar.low > i{background:linear-gradient(90deg,var(--coral),#ff2d2d);animation:pulse .5s ease-in-out infinite} |
|
@keyframes pulse{50%{opacity:.55}} |
|
|
|
.screen-wrap{ |
|
position:relative; |
|
aspect-ratio:1/1; |
|
border-radius:16px; |
|
background:var(--bezel); |
|
padding:7px; |
|
box-shadow:0 0 0 1px rgba(0,0,0,.2), 0 10px 24px -14px rgba(0,0,0,.8); |
|
} |
|
canvas{ |
|
display:block;width:100%;height:100%; |
|
border-radius:11px; |
|
background:#0e0f12; |
|
image-rendering:auto; |
|
touch-action:none; |
|
} |
|
.vignette{ |
|
pointer-events:none;position:absolute;inset:7px;border-radius:11px; |
|
box-shadow:0 0 0 1px rgba(0,0,0,.25), 0 0 40px 6px rgba(0,0,0,.45) inset; |
|
} |
|
|
|
/* overlays */ |
|
.overlay{ |
|
position:absolute;inset:7px;border-radius:11px; |
|
display:flex;align-items:center;justify-content:center; |
|
background:rgba(14,15,18,.74);backdrop-filter:blur(3px); |
|
z-index:5;text-align:center;padding:22px; |
|
animation:fade .25s ease; |
|
} |
|
.overlay.hide{display:none} |
|
@keyframes fade{from{opacity:0}to{opacity:1}} |
|
.card{max-width:88%;color:var(--paper)} |
|
.card .eyebrow{font-size:10px;letter-spacing:.32em;text-transform:uppercase;color:var(--gold);font-weight:600} |
|
.card h2{ |
|
font-family:"Bricolage Grotesque",sans-serif;font-weight:800; |
|
font-size:clamp(28px,7vw,42px);margin:8px 0 4px;color:#fff;letter-spacing:.01em; |
|
} |
|
.card p{margin:8px auto;max-width:330px;font-size:13.5px;line-height:1.55;color:#d6d2c4;font-weight:400} |
|
.card .legend{ |
|
display:grid;grid-template-columns:1fr 1fr;gap:6px 14px;margin:14px auto;max-width:320px; |
|
font-size:11.5px;color:#c7c2b3;text-align:left; |
|
} |
|
.card .legend b{color:#fff;font-weight:600} |
|
.card .scoreline{font-family:"Bricolage Grotesque",sans-serif;font-size:20px;color:var(--green);font-weight:700;margin-top:6px} |
|
.card .scoreline small{color:#9b968a;font-weight:500;font-size:12px;font-family:"Sora"} |
|
.btn{ |
|
appearance:none;border:none;cursor:pointer; |
|
font-family:"Bricolage Grotesque",sans-serif;font-weight:800; |
|
font-size:16px;letter-spacing:.04em;text-transform:uppercase; |
|
color:#fff;background:linear-gradient(180deg,var(--coral),var(--coral-d)); |
|
padding:13px 26px;border-radius:13px;margin-top:8px; |
|
box-shadow:0 6px 0 #a8331f, 0 10px 18px -8px rgba(255,90,60,.7); |
|
transition:transform .06s, box-shadow .06s; |
|
} |
|
.btn:active{transform:translateY(4px);box-shadow:0 2px 0 #a8331f, 0 6px 12px -8px rgba(255,90,60,.7)} |
|
|
|
.footbar{ |
|
display:flex;align-items:center;justify-content:space-between;gap:10px; |
|
padding:10px 6px 2px;color:var(--muted);font-size:10.5px;font-weight:500; |
|
} |
|
.footbar .keys{display:flex;gap:6px;align-items:center;flex-wrap:wrap} |
|
.kbd{ |
|
display:inline-flex;align-items:center;justify-content:center; |
|
min-width:20px;height:20px;padding:0 5px;border-radius:5px; |
|
background:#fff;color:var(--ink);border:1px solid rgba(0,0,0,.16); |
|
font-family:"Bricolage Grotesque";font-weight:700;font-size:11px; |
|
box-shadow:0 1px 0 rgba(0,0,0,.12); |
|
} |
|
.iconbtn{ |
|
appearance:none;border:1px solid rgba(0,0,0,.16);background:#fff;color:var(--ink); |
|
border-radius:9px;padding:6px 9px;font-size:12px;font-weight:600;cursor:pointer; |
|
display:inline-flex;align-items:center;gap:5px; |
|
} |
|
.iconbtn:active{transform:translateY(1px)} |
|
|
|
/* on-screen dpad */ |
|
.dpad{ |
|
position:absolute;left:10px;bottom:10px;z-index:4; |
|
display:grid;grid-template-columns:repeat(3,40px);grid-template-rows:repeat(3,40px); |
|
gap:4px;opacity:.92; |
|
} |
|
.dpad button{ |
|
border:none;border-radius:10px;cursor:pointer; |
|
background:rgba(255,255,255,.12);color:#fff;backdrop-filter:blur(4px); |
|
border:1px solid rgba(255,255,255,.18); |
|
font-size:18px;display:flex;align-items:center;justify-content:center; |
|
-webkit-tap-highlight-color:transparent;user-select:none; |
|
} |
|
.dpad button:active{background:rgba(255,90,60,.55)} |
|
.dpad .up{grid-column:2;grid-row:1} |
|
.dpad .left{grid-column:1;grid-row:2} |
|
.dpad .right{grid-column:3;grid-row:2} |
|
.dpad .down{grid-column:2;grid-row:3} |
|
@media (hover:hover) and (pointer:fine){ |
|
.dpad{display:none} |
|
} |
|
@media (max-width:430px){ |
|
.stats{gap:5px}.chip{min-width:46px;padding:4px 7px} |
|
.chip .v{font-size:15px} |
|
.brand .sub{display:none} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<main class="cabinet" role="application" aria-label="Home Front game"> |
|
<span class="screw-bl"></span><span class="screw-br"></span> |
|
|
|
<div class="topbar"> |
|
<div class="brand"> |
|
<h1><span class="dot"></span>HOME FRONT</h1> |
|
<span class="sub">a frog's commute through the modern home</span> |
|
</div> |
|
<div class="stats"> |
|
<div class="chip"><span class="k">Score</span><span class="v" id="score">0</span></div> |
|
<div class="chip coral"><span class="k">Hi</span><span class="v" id="hi">0</span></div> |
|
<div class="chip"><span class="k">Lvl</span><span class="v" id="level">1</span></div> |
|
</div> |
|
</div> |
|
|
|
<div class="timerbar" id="timerbar"><i id="timerfill"></i></div> |
|
|
|
<div class="screen-wrap"> |
|
<canvas id="game" width="572" height="572"></canvas> |
|
<div class="vignette"></div> |
|
|
|
<div class="dpad" id="dpad" aria-hidden="true"> |
|
<button class="up" data-dir="up" aria-label="up">▲</button> |
|
<button class="left" data-dir="left" aria-label="left">◀</button> |
|
<button class="right" data-dir="right" aria-label="right">▶</button> |
|
<button class="down" data-dir="down" aria-label="down">▼</button> |
|
</div> |
|
|
|
<!-- START --> |
|
<div class="overlay" id="startScreen"> |
|
<div class="card"> |
|
<div class="eyebrow">Arcade · Cozy Mode</div> |
|
<h2>HOME FRONT</h2> |
|
<p>You're a frog trying to get home across the living room, the driveway, and the office desk. Hop lane by lane — and don't let the everyday objects touch you.</p> |
|
<div class="legend"> |
|
<div><b>▲ ▼ ◀ ▶ / WASD</b><br>hop one tile</div> |
|
<div><b>Reach the nests</b><br>4 cozy alcoves up top</div> |
|
<div><b>Dodge</b><br>Tesla, Ferrari, Lambo…</div> |
|
<div><b>Avoid</b><br>phones, laptops, monitors</div> |
|
</div> |
|
<button class="btn" id="startBtn">Play</button> |
|
</div> |
|
</div> |
|
|
|
<!-- PAUSE --> |
|
<div class="overlay hide" id="pauseScreen"> |
|
<div class="card"> |
|
<div class="eyebrow">Paused</div> |
|
<h2>Take a Breath</h2> |
|
<p>Press <span class="kbd">P</span> or tap to resume.</p> |
|
<button class="btn" id="resumeBtn">Resume</button> |
|
</div> |
|
</div> |
|
|
|
<!-- GAME OVER --> |
|
<div class="overlay hide" id="overScreen"> |
|
<div class="card"> |
|
<div class="eyebrow" id="overEyebrow">Squashed</div> |
|
<h2 id="overTitle">Game Over</h2> |
|
<div class="scoreline" id="overScore">0 <small>pts</small></div> |
|
<p id="overMsg">The household claimed another hopper.</p> |
|
<button class="btn" id="againBtn">Play Again</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="footbar"> |
|
<div class="keys"> |
|
<span class="kbd">←</span><span class="kbd">↑</span><span class="kbd">↓</span><span class="kbd">→</span> |
|
<span style="opacity:.7">move</span> |
|
<span style="margin-left:8px" class="kbd">P</span><span style="opacity:.7">pause</span> |
|
<span style="margin-left:8px" class="kbd">M</span><span style="opacity:.7">mute</span> |
|
</div> |
|
<button class="iconbtn" id="muteBtn" aria-label="toggle sound"> |
|
<span id="muteIcon">🔊</span><span>Sound</span> |
|
</button> |
|
</div> |
|
</main> |
|
|
|
<script> |
|
(function(){ |
|
"use strict"; |
|
/* ============================================================ HOME FRONT |
|
A cozy, top-down Frogger-style game. Frog crosses a modern home: |
|
a desk (gadgets), a grass median, and a driveway (toy cars). |
|
Single file, vanilla canvas. No external assets. |
|
========================================================================= */ |
|
|
|
/* ---------- Config ---------- */ |
|
const COLS=13, ROWS=13, TILE=44; |
|
const W=COLS*TILE, H=ROWS*TILE; |
|
const HOP_DUR=0.13; |
|
const START_COL=6, START_ROW=ROWS-1; |
|
const GOAL_SLOTS=[[1,2],[4,5],[7,8],[10,11]]; // column ranges in goal row |
|
const BASE_TIME=40, MIN_TIME=22; |
|
const HI_KEY="homefront.hiscore.v1"; |
|
|
|
/* ---------- DOM ---------- */ |
|
const canvas=document.getElementById("game"); |
|
const ctx=canvas.getContext("2d"); |
|
const elScore=document.getElementById("score"); |
|
const elHi=document.getElementById("hi"); |
|
const elLevel=document.getElementById("level"); |
|
const elTimerBar=document.getElementById("timerbar"); |
|
const elTimerFill=document.getElementById("timerfill"); |
|
const startScreen=document.getElementById("startScreen"); |
|
const pauseScreen=document.getElementById("pauseScreen"); |
|
const overScreen=document.getElementById("overScreen"); |
|
|
|
/* ---------- Canvas / DPR ---------- */ |
|
let DPR=1, bgCanvas=document.createElement("canvas"), bgctx=bgCanvas.getContext("2d"); |
|
function resize(){ |
|
DPR=Math.min(window.devicePixelRatio||1,2); |
|
canvas.width=W*DPR; canvas.height=H*DPR; |
|
ctx.setTransform(DPR,0,0,DPR,0,0); |
|
ctx.imageSmoothingEnabled=true; |
|
buildBackground(); |
|
} |
|
window.addEventListener("resize",resize); |
|
|
|
/* ---------- Helpers ---------- */ |
|
const clamp=(v,a,b)=>v<a?a:v>b?b:v; |
|
const lerp=(a,b,t)=>a+(b-a)*t; |
|
const rand=(a,b)=>a+Math.random()*(b-a); |
|
const pick=arr=>arr[(Math.random()*arr.length)|0]; |
|
function rr(x,y,w,h,r){ // rounded rect path |
|
r=Math.min(r,w/2,h/2); |
|
ctx.beginPath(); |
|
ctx.moveTo(x+r,y); |
|
ctx.arcTo(x+w,y,x+w,y+h,r); |
|
ctx.arcTo(x+w,y+h,x,y+h,r); |
|
ctx.arcTo(x,y+h,x,y,r); |
|
ctx.arcTo(x,y,x+w,y,r); |
|
ctx.closePath(); |
|
} |
|
function bg_rr(x,y,w,h,r){ |
|
r=Math.min(r,w/2,h/2); |
|
bgctx.beginPath(); |
|
bgctx.moveTo(x+r,y); |
|
bgctx.arcTo(x+w,y,x+w,y+h,r); |
|
bgctx.arcTo(x+w,y+h,x,y+h,r); |
|
bgctx.arcTo(x,y+h,x,y,r); |
|
bgctx.arcTo(x,y,x+w,y,r); |
|
bgctx.closePath(); |
|
} |
|
const colToX=c=>c*TILE+TILE/2; |
|
const rowToY=r=>r*TILE+TILE/2; |
|
|
|
/* ---------- Audio (Web Audio synth) ---------- */ |
|
let actx=null, master=null, muted=false; |
|
function ensureAudio(){ |
|
if(actx) return; |
|
try{ |
|
actx=new (window.AudioContext||window.webkitAudioContext)(); |
|
master=actx.createGain(); master.gain.value=0.5; master.connect(actx.destination); |
|
}catch(e){actx=null;} |
|
} |
|
function blip(freq,dur,type,vol,slideTo){ |
|
if(!actx||muted) return; |
|
const t=actx.currentTime; |
|
const o=actx.createOscillator(), g=actx.createGain(); |
|
o.type=type||"square"; o.frequency.setValueAtTime(freq,t); |
|
if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo,t+dur); |
|
g.gain.setValueAtTime(0.0001,t); |
|
g.gain.exponentialRampToValueAtTime(vol||0.25,t+0.012); |
|
g.gain.exponentialRampToValueAtTime(0.0001,t+dur); |
|
o.connect(g); g.connect(master); o.start(t); o.stop(t+dur+0.02); |
|
} |
|
function noise(dur,vol){ |
|
if(!actx||muted) return; |
|
const t=actx.currentTime; |
|
const n=Math.floor(actx.sampleRate*dur); |
|
const buf=actx.createBuffer(1,n,actx.sampleRate); |
|
const d=buf.getChannelData(0); |
|
for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*(1-i/n); |
|
const s=actx.createBufferSource(); s.buffer=buf; |
|
const g=actx.createGain(); g.gain.value=vol||0.3; |
|
const f=actx.createBiquadFilter(); f.type="lowpass"; f.frequency.value=900; |
|
s.connect(f); f.connect(g); g.connect(master); s.start(t); |
|
} |
|
const sfx={ |
|
hop(){blip(520,0.07,"triangle",0.18,640);}, |
|
goal(){blip(523,0.09,"square",0.2);setTimeout(()=>blip(659,0.09,"square",0.2),70);setTimeout(()=>blip(784,0.14,"square",0.22),140);}, |
|
die(){noise(0.35,0.35);blip(300,0.4,"sawtooth",0.18,70);}, |
|
wall(){blip(140,0.18,"square",0.2,90);noise(0.12,0.18);}, |
|
level(){[523,659,784,1046].forEach((f,i)=>setTimeout(()=>blip(f,0.16,"triangle",0.22),i*90));}, |
|
over(){[392,330,262].forEach((f,i)=>setTimeout(()=>blip(f,0.3,"sawtooth",0.2),i*150));} |
|
}; |
|
|
|
/* ---------- Variant tables ---------- */ |
|
const CARS=[ |
|
{name:"tesla", w:1.70, body:"#e9edf2", roof:"#cfd6dd", glass:"#a9dcff", stripe:"#dfe4ea", dark:"#9aa0a8"}, |
|
{name:"ferrari", w:1.60, body:"#e8392b", roof:"#b22619", glass:"#bfe9ff", stripe:"#ffe9c2", dark:"#7a1a12"}, |
|
{name:"lambo", w:1.66, body:"#f4c542", roof:"#d6a72f", glass:"#cfeaff", stripe:"#1b1b1b", dark:"#9c7a1f"}, |
|
{name:"cybertruck",w:1.95, body:"#b7bcc4", roof:"#9aa0a8", glass:"#8fc0db", stripe:"#8a8f97", dark:"#6c7178"}, |
|
{name:"porsche", w:1.60, body:"#27344f", roof:"#19212f", glass:"#bfe9ff", stripe:"#3c4d6b", dark:"#10151f"} |
|
]; |
|
const GADGETS=[ |
|
{name:"iphone", w:0.82, body:"#15171c", screen:"#1d2740", tile:"#3a4a6b"}, |
|
{name:"tablet", w:1.04, body:"#1b1e25", screen:"#243049", tile:"#3c4d72"}, |
|
{name:"laptop", w:1.36, body:"#cbd0d8", base:"#aab0ba", screen:"#27314a"}, |
|
{name:"monitor",w:1.26, body:"#14161b", screen:"#243552", stand:"#3b4049"} |
|
]; |
|
|
|
/* ---------- Lane plan ---------- */ |
|
const LANE_PLAN=[ |
|
// row, dir, speed, count, possible variants, kind |
|
{row:1, dir:-1, speed:52, count:3, kinds:["iphone","tablet"], kind:"gadget"}, |
|
{row:2, dir: 1, speed:80, count:2, kinds:["laptop"], kind:"gadget"}, |
|
{row:3, dir:-1, speed:44, count:3, kinds:["monitor","tablet"], kind:"gadget"}, |
|
{row:4, dir: 1, speed:94, count:3, kinds:["iphone"], kind:"gadget"}, |
|
{row:5, dir:-1, speed:62, count:2, kinds:["tablet","laptop"], kind:"gadget"}, |
|
{row:7, dir: 1, speed:70, count:3, kinds:["tesla","porsche"], kind:"car"}, |
|
{row:8, dir:-1, speed:98, count:2, kinds:["ferrari"], kind:"car"}, |
|
{row:9, dir: 1, speed:58, count:2, kinds:["cybertruck"], kind:"car"}, |
|
{row:10,dir:-1, speed:114, count:3, kinds:["lambo","ferrari"], kind:"car"}, |
|
{row:11,dir: 1, speed:50, count:3, kinds:["tesla","porsche"], kind:"car"} |
|
]; |
|
|
|
function buildLanes(){ |
|
return LANE_PLAN.map(p=>{ |
|
const tbl = p.kind==="car"?CARS:GADGETS; |
|
const items=[]; |
|
let maxW=0; |
|
for(let i=0;i<p.count;i++){ |
|
const v=tbl.find(x=>x.name===pick(p.kinds))||tbl[0]; |
|
const wpx=v.w*TILE; |
|
items.push({variant:v, w:wpx}); |
|
if(wpx>maxW)maxW=wpx; |
|
} |
|
const pad=rand(30,140); |
|
const cycleLen=W+maxW+pad; |
|
const spacing=cycleLen/p.count; |
|
items.forEach((it,i)=>{ it.pos0 = i*spacing - it.w/2; }); |
|
return {row:p.row, dir:p.dir, speed:p.speed, kind:p.kind, items, cycleLen, phase:rand(0,cycleLen)}; |
|
}); |
|
} |
|
|
|
/* ---------- Game state ---------- */ |
|
const game={ |
|
state:"boot", // boot | play | dead | levelclear | gameover |
|
paused:false, |
|
score:0, hi:0, level:1, lives:3, |
|
timer:BASE_TIME, maxTime:BASE_TIME, speedMul:1, |
|
goals:[false,false,false,false], |
|
deadTimer:0, lcTimer:0, |
|
toast:null, toastLife:0, |
|
flash:0 |
|
}; |
|
const frog={ |
|
col:START_COL, row:START_ROW, fx:colToX(START_COL), fy:rowToY(START_ROW), |
|
fromX:0, fromY:0, toX:0, toY:0, hopT:0, hopping:false, facing:0, alive:true, |
|
minRow:START_ROW, hopPulse:0 |
|
}; |
|
let lanes=[]; let particles=[]; |
|
|
|
/* ---------- Flow ---------- */ |
|
function startGame(){ |
|
ensureAudio(); |
|
game.score=0; game.level=1; game.lives=3; game.speedMul=1; |
|
game.maxTime=BASE_TIME; game.goals=[false,false,false,false]; |
|
game.paused=false; |
|
lanes=buildLanes(); |
|
respawn(); |
|
hideOverlay(startScreen); hideOverlay(pauseScreen); hideOverlay(overScreen); |
|
game.state="play"; |
|
updateHUD(); |
|
} |
|
function respawn(){ |
|
frog.col=START_COL; frog.row=START_ROW; |
|
frog.fx=colToX(START_COL); frog.fy=rowToY(START_ROW); |
|
frog.hopping=false; frog.alive=true; frog.facing=0; frog.minRow=START_ROW; |
|
game.timer=game.maxTime; |
|
} |
|
function loseLife(){ |
|
game.lives--; |
|
updateHUD(); |
|
if(game.lives<=0){ gameOver(); } |
|
else { respawn(); game.state="play"; } |
|
} |
|
function reachGoal(slot){ |
|
game.goals[slot]=true; |
|
const tb=Math.floor(game.timer)*10; |
|
addScore(200+tb); |
|
spawnParticles(colToX((GOAL_SLOTS[slot][0]+GOAL_SLOTS[slot][1])/2), rowToY(0)+TILE*0.4, 22, "goal"); |
|
sfx.goal(); |
|
if(game.goals.every(Boolean)){ |
|
addScore(1000); |
|
game.state="levelclear"; game.lcTimer=1.9; |
|
showToast("LEVEL "+game.level+" CLEAR","+1000 bonus"); |
|
sfx.level(); |
|
}else{ |
|
respawn(); |
|
} |
|
updateHUD(); |
|
} |
|
function nextLevel(){ |
|
game.level++; |
|
game.speedMul=1+(game.level-1)*0.15; |
|
game.maxTime=Math.max(MIN_TIME,BASE_TIME-(game.level-1)*2); |
|
game.goals=[false,false,false,false]; |
|
lanes=buildLanes(); |
|
respawn(); |
|
game.state="play"; |
|
showToast("LEVEL "+game.level,"faster traffic"); |
|
updateHUD(); |
|
} |
|
function killFrog(reason){ |
|
if(!frog.alive) return; |
|
frog.alive=false; |
|
game.state="dead"; game.deadTimer=0.95; |
|
game.flash=0.6; |
|
spawnParticles(frog.fx,frog.fy,18,reason==="time"?"drown":"splat"); |
|
if(reason==="wall") sfx.wall(); else sfx.die(); |
|
} |
|
function gameOver(){ |
|
game.state="gameover"; |
|
if(game.score>game.hi){ game.hi=game.score; try{localStorage.setItem(HI_KEY,String(game.hi));}catch(e){} } |
|
document.getElementById("overScore").innerHTML = game.score+" <small>pts</small>"; |
|
document.getElementById("overTitle").textContent = game.score===0?"Try Again":"Game Over"; |
|
document.getElementById("overMsg").textContent = |
|
game.score>=1500?"A legend of the living room.":game.score>=600?"Respectable hopping.":"The household claimed another hopper."; |
|
document.getElementById("overEyebrow").textContent = game.lives<=0?"Out of lives":"Time's up"; |
|
showOverlay(overScreen); |
|
updateHUD(); |
|
sfx.over(); |
|
} |
|
function addScore(n){ game.score+=n; updateHUD(); } |
|
|
|
/* ---------- HUD ---------- */ |
|
function updateHUD(){ |
|
elScore.textContent=game.score; |
|
elHi.textContent=Math.max(game.hi,game.score); |
|
elLevel.textContent=game.level; |
|
elTimerFill.style.width=clamp(game.timer/game.maxTime,0,1)*100+"%"; |
|
elTimerBar.classList.toggle("low", game.timer/game.maxTime<0.25); |
|
} |
|
function showToast(text,sub){ game.toast={text,sub}; game.toastLife=2.0; } |
|
function showOverlay(el){ el.classList.remove("hide"); } |
|
function hideOverlay(el){ el.classList.add("hide"); } |
|
|
|
/* ---------- Input ---------- */ |
|
const DIR={ up:{dc:0,dr:-1,a:0}, down:{dc:0,dr:1,a:Math.PI}, left:{dc:-1,dr:0,a:-Math.PI/2}, right:{dc:1,dr:0,a:Math.PI/2} }; |
|
function tryMove(name){ |
|
if(game.state==="boot"){ startGame(); return; } |
|
if(game.state==="gameover"){ return; } |
|
if(game.paused){ togglePause(); return; } |
|
if(game.state!=="play"||frog.hopping||!frog.alive) return; |
|
const d=DIR[name]; if(!d) return; |
|
const nc=clamp(frog.col+d.dc,0,COLS-1); |
|
const nr=clamp(frog.row+d.dr,0,ROWS-1); |
|
if(nc===frog.col&&nr===frog.row) return; // edge, no-op |
|
frog.facing=d.a; |
|
frog.fromX=frog.fx; frog.fromY=frog.fy; |
|
frog.col=nc; frog.row=nr; |
|
frog.toX=colToX(nc); frog.toY=rowToY(nr); |
|
frog.hopping=true; frog.hopT=0; |
|
if(nr<frog.minRow){ game.score+=(frog.minRow-nr)*10; frog.minRow=nr; updateHUD(); } |
|
sfx.hop(); |
|
} |
|
function togglePause(){ |
|
if(game.state!=="play") return; |
|
game.paused=!game.paused; |
|
if(game.paused) showOverlay(pauseScreen); else hideOverlay(pauseScreen); |
|
} |
|
window.addEventListener("keydown",e=>{ |
|
const k=e.key; |
|
if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(k)) e.preventDefault(); |
|
if(k==="ArrowUp"||k==="w"||k==="W") tryMove("up"); |
|
else if(k==="ArrowDown"||k==="s"||k==="S") tryMove("down"); |
|
else if(k==="ArrowLeft"||k==="a"||k==="A") tryMove("left"); |
|
else if(k==="ArrowRight"||k==="d"||k==="D") tryMove("right"); |
|
else if(k==="p"||k==="P"||k==="Escape"){ togglePause(); } |
|
else if(k==="m"||k==="M"){ toggleMute(); } |
|
else if(k==="Enter"){ if(game.state==="boot") startGame(); else if(game.state==="gameover") startGame(); } |
|
},{passive:false}); |
|
|
|
/* on-screen dpad + swipe */ |
|
document.querySelectorAll("#dpad button").forEach(b=>{ |
|
const fire=ev=>{ev.preventDefault(); ensureAudio(); tryMove(b.dataset.dir);}; |
|
b.addEventListener("touchstart",fire,{passive:false}); |
|
b.addEventListener("mousedown",fire); |
|
}); |
|
let tsx=0,tsy=0,tracking=false; |
|
canvas.addEventListener("touchstart",e=>{ ensureAudio(); if(e.touches.length){tsx=e.touches[0].clientX;tsy=e.touches[0].clientY;tracking=true;} },{passive:true}); |
|
canvas.addEventListener("touchend",e=>{ |
|
if(!tracking) return; tracking=false; |
|
const t=e.changedTouches[0]; const dx=t.clientX-tsx, dy=t.clientY-tsy; |
|
if(Math.abs(dx)<22&&Math.abs(dy)<22){ // tap |
|
if(game.state==="boot") startGame(); |
|
return; |
|
} |
|
if(Math.abs(dx)>Math.abs(dy)) tryMove(dx>0?"right":"left"); |
|
else tryMove(dy>0?"down":"up"); |
|
},{passive:true}); |
|
|
|
/* buttons */ |
|
document.getElementById("startBtn").addEventListener("click",()=>{ensureAudio();startGame();}); |
|
document.getElementById("againBtn").addEventListener("click",()=>{ensureAudio();startGame();}); |
|
document.getElementById("resumeBtn").addEventListener("click",togglePause); |
|
const muteBtn=document.getElementById("muteBtn"), muteIcon=document.getElementById("muteIcon"); |
|
function toggleMute(){ |
|
muted=!muted; |
|
muteIcon.textContent = muted?"🔇":"🔊"; |
|
muteBtn.style.opacity = muted?".6":"1"; |
|
} |
|
muteBtn.addEventListener("click",toggleMute); |
|
|
|
/* pause when tab hidden */ |
|
document.addEventListener("visibilitychange",()=>{ |
|
if(document.hidden && game.state==="play" && !game.paused){ game.paused=true; showOverlay(pauseScreen); } |
|
}); |
|
|
|
/* ---------- Particles ---------- */ |
|
function spawnParticles(x,y,n,type){ |
|
const palettes={ |
|
splat:["#7ed957","#4ea632","#a7e76a","#e8ffce"], |
|
drown:["#7fc6ff","#3f7fd6","#bfe6ff","#e8f5ff"], |
|
goal:["#ff5a3c","#f3b13d","#7ed957","#ffffff","#ff8a5c"] |
|
}; |
|
const pal=palettes[type]||palettes.splat; |
|
for(let i=0;i<n;i++){ |
|
const a=Math.random()*Math.PI*2, sp=rand(40,180); |
|
particles.push({ |
|
x,y, vx:Math.cos(a)*sp, vy:Math.sin(a)*sp - (type==="goal"?60:0), |
|
life:rand(0.5,1.0), max:1.0, size:rand(2.5,5.5), |
|
color:pick(pal), spin:rand(-6,6), rot:rand(0,6.28), |
|
shape: type==="goal"? (Math.random()<0.5?"star":"rect") : "leaf" |
|
}); |
|
} |
|
} |
|
|
|
/* ---------- Update ---------- */ |
|
function update(dt){ |
|
// toast & flash & particles always tick (even overlays) for liveliness |
|
if(game.toastLife>0) game.toastLife-=dt; |
|
if(game.flash>0) game.flash=Math.max(0,game.flash-dt*1.6); |
|
updateParticles(dt); |
|
|
|
if(game.paused) return; |
|
|
|
if(game.state==="play"){ |
|
updateFrog(dt); |
|
updateLanes(dt); |
|
checkCollisions(); |
|
game.timer-=dt; |
|
if(game.timer<=0){ game.timer=0; killFrog("time"); } |
|
updateHUD(); |
|
} else if(game.state==="dead"){ |
|
updateLanes(dt); |
|
game.deadTimer-=dt; |
|
if(game.deadTimer<=0) loseLife(); |
|
} else if(game.state==="levelclear"){ |
|
updateLanes(dt); |
|
game.lcTimer-=dt; |
|
if(game.lcTimer<=0) nextLevel(); |
|
} |
|
} |
|
function updateFrog(dt){ |
|
frog.hopPulse+=dt; |
|
if(!frog.hopping) return; |
|
frog.hopT+=dt/HOP_DUR; |
|
if(frog.hopT>=1){ |
|
frog.hopT=1; frog.hopping=false; |
|
frog.fx=frog.toX; frog.fy=frog.toY; |
|
if(frog.row===0) handleGoalRow(); |
|
return; |
|
} |
|
const e=1-Math.pow(1-frog.hopT,2.2); // ease-out |
|
frog.fx=lerp(frog.fromX,frog.toX,e); |
|
frog.fy=lerp(frog.fromY,frog.toY,e); |
|
} |
|
function handleGoalRow(){ |
|
const col=frog.col; |
|
const slot=GOAL_SLOTS.findIndex(r=>col>=r[0]&&col<=r[1]); |
|
if(slot===-1){ killFrog("wall"); return; } |
|
if(game.goals[slot]){ killFrog("wall"); return; } |
|
reachGoal(slot); |
|
} |
|
function updateLanes(dt){ |
|
for(const lane of lanes){ |
|
lane.phase=(lane.phase + lane.dir*lane.speed*game.speedMul*dt)%lane.cycleLen; |
|
for(const it of lane.items){ |
|
let x=(it.pos0+lane.phase)%lane.cycleLen; |
|
if(x<0) x+=lane.cycleLen; |
|
it.curX=x; |
|
} |
|
} |
|
} |
|
function checkCollisions(){ |
|
if(!frog.alive) return; |
|
const hw=TILE*0.28, hh=TILE*0.30; |
|
for(const lane of lanes){ |
|
const top=lane.row*TILE, bot=top+TILE; |
|
if(bot<=frog.fy-hh||top>=frog.fy+hh) continue; |
|
for(const it of lane.items){ |
|
for(const off of [0,-lane.cycleLen,lane.cycleLen]){ |
|
const sx=it.curX+off; |
|
if(sx>frog.fx+hw||sx+it.w<frog.fx-hw) continue; |
|
killFrog("hit"); return; |
|
} |
|
} |
|
} |
|
} |
|
function updateParticles(dt){ |
|
for(let i=particles.length-1;i>=0;i--){ |
|
const p=particles[i]; |
|
p.life-=dt; |
|
if(p.life<=0){ particles.splice(i,1); continue; } |
|
p.x+=p.vx*dt; p.y+=p.vy*dt; |
|
p.vx*=0.92; p.vy=p.vy*0.92+ (p.shape==="leaf"?120:40)*dt; |
|
p.rot+=p.spin*dt; |
|
} |
|
} |
|
|
|
/* ============================================================ RENDER |
|
========================================================================= */ |
|
function render(){ |
|
ctx.clearRect(0,0,W,H); |
|
ctx.drawImage(bgCanvas,0,0,W,H); |
|
drawGoalRow(); |
|
drawLanes(); |
|
if(frog.alive && (game.state==="play"||game.state==="boot")) drawFrog(ctx,frog.fx,frog.fy,1,frog.facing,frog.hopping?frog.hopT:0,frog.hopPulse); |
|
drawParticles(); |
|
if(game.flash>0){ ctx.fillStyle="rgba(255,90,60,"+game.flash*0.5+")"; ctx.fillRect(0,0,W,H); } |
|
if(game.toastLife>0&&game.toast) drawToast(); |
|
} |
|
|
|
/* ---------- Background prerender ---------- */ |
|
function buildBackground(){ |
|
bgCanvas.width=W*DPR; bgCanvas.height=H*DPR; |
|
bgctx.setTransform(DPR,0,0,DPR,0,0); |
|
const g=bgctx; |
|
g.clearRect(0,0,W,H); |
|
for(let r=0;r<ROWS;r++){ |
|
const y=r*TILE; |
|
if(r===0) paintGoalWall(g,y); |
|
else if(r>=1&&r<=5) paintDesk(g,y,r); |
|
else if(r===6) paintGrass(g,y); |
|
else if(r>=7&&r<=11) paintAsphalt(g,y,r); |
|
else if(r===12) paintStartFloor(g,y); |
|
// separators |
|
g.strokeStyle="rgba(0,0,0,0.18)"; g.lineWidth=1; |
|
g.beginPath(); g.moveTo(0,y+TILE-0.5); g.lineTo(W,y+TILE-0.5); g.stroke(); |
|
} |
|
// outer frame |
|
g.strokeStyle="rgba(0,0,0,0.35)"; g.lineWidth=2; g.strokeRect(1,1,W-2,H-2); |
|
} |
|
function paintGoalWall(g,y){ |
|
g.fillStyle="#3a2f26"; g.fillRect(0,y,W,TILE); |
|
// wood grain |
|
g.strokeStyle="rgba(0,0,0,0.18)"; g.lineWidth=1; |
|
for(let i=0;i<6;i++){ g.beginPath(); g.moveTo(0,y+6+i*7); g.bezierCurveTo(W*0.3,y+8+i*7,W*0.6,y+4+i*7,W,y+7+i*7); g.stroke(); } |
|
// walls between slots (cols 0,3,6,9,12) |
|
g.fillStyle="#2a211b"; |
|
for(const c of [0,3,6,9,12]){ |
|
g.fillRect(c*TILE,y,TILE,TILE); |
|
g.fillStyle="rgba(255,255,255,0.04)"; g.fillRect(c*TILE,y,TILE,3); |
|
g.fillStyle="#2a211b"; |
|
} |
|
} |
|
function paintDesk(g,y,r){ |
|
// birch desk surface |
|
g.fillStyle="#e7d3a8"; g.fillRect(0,y,W,TILE); |
|
g.fillStyle="rgba(255,255,255,0.18)"; g.fillRect(0,y,W,TILE*0.5); |
|
g.strokeStyle="rgba(150,120,70,0.22)"; g.lineWidth=1; |
|
for(let i=0;i<3;i++){ const yy=y+8+i*12; g.beginPath(); g.moveTo(0,yy); g.bezierCurveTo(W*0.3,yy+2,W*0.6,yy-2,W,yy+1); g.stroke(); } |
|
// a coffee stain on one lane for character |
|
if(r===3){ |
|
g.save(); g.globalAlpha=0.10; g.fillStyle="#5a3a1a"; |
|
g.beginPath(); g.arc(W*0.78,y+TILE*0.5,TILE*0.42,0,6.29); g.fill(); |
|
g.beginPath(); g.arc(W*0.78,y+TILE*0.5,TILE*0.30,0,6.29); g.fillStyle="#e7d3a8"; g.fill(); |
|
g.restore(); |
|
} |
|
// direction chevrons |
|
const dir=LANE_PLAN.find(p=>p.row===r)?.dir||1; |
|
drawChevrons(g,y,dir,"rgba(120,90,40,0.25)"); |
|
} |
|
function paintGrass(g,y){ |
|
g.fillStyle="#7fb15a"; g.fillRect(0,y,W,TILE); |
|
g.fillStyle="#6ba247"; |
|
for(let i=0;i<26;i++){ |
|
const x=(i/26)*W + ((i*53)%18); |
|
g.fillRect(x,y+ ( (i*7)%(TILE-8) )+4, 2, 6+ (i%3)*2); |
|
} |
|
// little flowers |
|
const fc=["#f3b13d","#ff5a3c","#ffffff"]; |
|
for(let i=0;i<8;i++){ |
|
const x=rand(8,W-8), yy=y+rand(8,TILE-8); |
|
g.fillStyle=fc[i%3]; |
|
for(let a=0;a<5;a++){ const ang=a/5*6.28; g.beginPath(); g.arc(x+Math.cos(ang)*2.4,yy+Math.sin(ang)*2.4,1.7,0,6.29); g.fill(); } |
|
g.fillStyle="#fff3c2"; g.beginPath(); g.arc(x,yy,1.3,0,6.29); g.fill(); |
|
} |
|
} |
|
function paintAsphalt(g,y,r){ |
|
g.fillStyle="#26282d"; g.fillRect(0,y,W,TILE); |
|
// speckle |
|
g.fillStyle="rgba(255,255,255,0.03)"; |
|
for(let i=0;i<40;i++){ g.fillRect(rand(0,W),y+rand(0,TILE),1,1); } |
|
g.fillStyle="rgba(0,0,0,0.25)"; |
|
for(let i=0;i<14;i++){ g.fillRect(rand(0,W),y+rand(0,TILE),rand(2,5),2); } |
|
// dashed lane line at top edge |
|
if(r>7){ |
|
g.strokeStyle="rgba(244,237,220,0.55)"; g.lineWidth=2; g.setLineDash([12,12]); |
|
g.beginPath(); g.moveTo(0,y); g.lineTo(W,y); g.stroke(); g.setLineDash([]); |
|
} |
|
const dir=LANE_PLAN.find(p=>p.row===r)?.dir||1; |
|
drawChevrons(g,y,dir,"rgba(244,237,220,0.10)"); |
|
} |
|
function paintStartFloor(g,y){ |
|
// warm wood planks |
|
g.fillStyle="#c98a5a"; g.fillRect(0,y,W,TILE); |
|
g.fillStyle="#b87642"; |
|
for(let c=0;c<COLS;c++){ |
|
if(c%2===0){ g.fillRect(c*TILE,y,1,TILE); } |
|
} |
|
g.strokeStyle="rgba(0,0,0,0.18)"; g.lineWidth=1; |
|
for(let c=0;c<=COLS;c++){ g.beginPath(); g.moveTo(c*TILE,y); g.lineTo(c*TILE,y+TILE); g.stroke(); } |
|
// a little rug under start |
|
g.save(); |
|
g.translate(W/2,y+TILE*0.5); |
|
g.fillStyle="#b3422b"; bg_rr(-TILE*1.6,-TILE*0.34,TILE*3.2,TILE*0.68,10); g.fill(); |
|
g.strokeStyle="rgba(255,255,255,0.18)"; g.lineWidth=2; g.setLineDash([5,5]); |
|
bg_rr(-TILE*1.45,-TILE*0.28,TILE*2.9,TILE*0.56,8); g.stroke(); g.setLineDash([]); |
|
g.restore(); |
|
} |
|
function drawChevrons(g,y,dir,color){ |
|
g.save(); g.globalAlpha=1; g.fillStyle=color; |
|
const n=4; |
|
for(let i=0;i<n;i++){ |
|
const x=(i+0.5)*(W/n); |
|
const cx=x, cy=y+TILE/2; |
|
g.beginPath(); |
|
if(dir>0){ g.moveTo(cx-6,cy-5); g.lineTo(cx+2,cy); g.lineTo(cx-6,cy+5); } |
|
else { g.moveTo(cx+6,cy-5); g.lineTo(cx-2,cy); g.lineTo(cx+6,cy+5); } |
|
g.closePath(); g.fill(); |
|
} |
|
g.restore(); |
|
} |
|
|
|
/* ---------- Goal row (dynamic) ---------- */ |
|
function drawGoalRow(){ |
|
const y=0; |
|
for(let s=0;s<GOAL_SLOTS.length;s++){ |
|
const [c0,c1]=GOAL_SLOTS[s]; |
|
const x=c0*TILE, w=(c1-c0+1)*TILE; |
|
const cx=x+w/2, cy=y+TILE/2; |
|
// alcove |
|
ctx.save(); |
|
ctx.fillStyle="#efe2c4"; rr(x+3,y+3,w-6,TILE-6,8); ctx.fill(); |
|
// nest cushion |
|
ctx.fillStyle = game.goals[s] ? "#9be36b" : "#d9c79a"; |
|
rr(x+8,y+8,w-16,TILE-16,12); ctx.fill(); |
|
if(game.goals[s]){ |
|
// sleeping frog |
|
drawFrog(ctx,cx,cy+2,0.78,0,0,performance.now()/600,true); |
|
}else{ |
|
// pulsing invite glow |
|
const p=0.5+0.5*Math.sin(performance.now()/420 + s); |
|
ctx.globalAlpha=0.25+0.25*p; |
|
ctx.fillStyle="#fff7a8"; rr(x+12,y+12,w-24,TILE-24,10); ctx.fill(); |
|
ctx.globalAlpha=1; |
|
} |
|
ctx.restore(); |
|
} |
|
} |
|
|
|
/* ---------- Lanes ---------- */ |
|
function drawLanes(){ |
|
for(const lane of lanes){ |
|
const cy=rowToY(lane.row); |
|
for(const it of lane.items){ |
|
for(const off of [0,-lane.cycleLen,lane.cycleLen]){ |
|
const sx=it.curX+off; |
|
if(sx>W+4||sx+it.w<-4) continue; |
|
if(lane.kind==="car") drawCar(ctx,sx,cy-laneH(it)/2,it.w,laneH(it),it.variant,lane.dir); |
|
else drawGadget(ctx,sx,cy-laneH(it)/2,it.w,laneH(it),it.variant,lane.dir); |
|
} |
|
} |
|
} |
|
} |
|
function laneH(it){ return TILE*0.80; } |
|
|
|
/* ---------- Sprite: FROG ---------- */ |
|
function drawFrog(c,cx,cy,scale,angle,hopT,pulse,asleep){ |
|
const lift = hopT>0 ? Math.sin(hopT*Math.PI) : 0; // 0..1 airborne amount |
|
const stretchY = 1 + 0.22*lift, stretchX = 1 - 0.10*lift; |
|
const pop = 1 + 0.10*lift; |
|
const bob = asleep ? Math.sin(pulse)*1.2 : 0; |
|
c.save(); |
|
c.translate(cx,cy+bob); |
|
// shadow |
|
c.save(); |
|
c.scale(1,0.5); |
|
c.fillStyle="rgba(0,0,0,"+(0.32*(1-0.45*lift))+")"; |
|
c.beginPath(); c.arc(0, TILE*0.5, TILE*0.34*(1-0.18*lift)*scale, 0, 6.29); c.fill(); |
|
c.restore(); |
|
|
|
c.rotate(angle); |
|
c.scale(stretchX*pop*scale, stretchY*pop*scale); |
|
const u=TILE; // unit |
|
// hind legs |
|
c.fillStyle="#5fb33a"; |
|
ellipse(c,-0.30*u,0.22*u,0.16*u,0.12*u,0.4); |
|
ellipse(c, 0.30*u,0.22*u,0.16*u,0.12*u,-0.4); |
|
// body |
|
c.fillStyle="#7ed957"; |
|
bodyPath(c,0,0,0.34*u,0.30*u); c.fill(); |
|
// belly highlight |
|
c.fillStyle="rgba(255,255,255,0.18)"; |
|
ellipse(c,0,0.04*u,0.20*u,0.14*u,0); |
|
// back spots |
|
c.fillStyle="#57b635"; |
|
ellipse(c,-0.12*u,-0.02*u,0.05*u,0.04*u,0); |
|
ellipse(c, 0.12*u,-0.04*u,0.05*u,0.04*u,0); |
|
ellipse(c,0,0.10*u,0.04*u,0.03*u,0); |
|
// front feet |
|
c.fillStyle="#5fb33a"; |
|
ellipse(c,-0.20*u,-0.20*u,0.09*u,0.06*u,0.5); |
|
ellipse(c, 0.20*u,-0.20*u,0.09*u,0.06*u,-0.5); |
|
// eyes (head is toward -y => up) |
|
const ey=-0.18*u, ex=0.16*u; |
|
// eye humps |
|
c.fillStyle="#7ed957"; |
|
ellipse(c,-ex,ey,0.11*u,0.10*u,0); |
|
ellipse(c, ex,ey,0.11*u,0.10*u,0); |
|
// whites |
|
c.fillStyle="#ffffff"; |
|
ellipse(c,-ex,ey-0.01*u,0.075*u,0.075*u,0); |
|
ellipse(c, ex,ey-0.01*u,0.075*u,0.075*u,0); |
|
// pupils |
|
c.fillStyle="#15240c"; |
|
const py = asleep ? ey : ey-0.015*u; |
|
ellipse(c,-ex,py,0.035*u,asleep?0.012*u:0.04*u,0); |
|
ellipse(c, ex,py,0.035*u,asleep?0.012*u:0.04*u,0); |
|
// eye shine |
|
c.fillStyle="rgba(255,255,255,0.9)"; |
|
ellipse(c,-ex+0.02*u,py-0.02*u,0.014*u,0.014*u,0); |
|
ellipse(c, ex+0.02*u,py-0.02*u,0.014*u,0.014*u,0); |
|
c.restore(); |
|
} |
|
function ellipse(c,x,y,rx,ry,rot){ c.save(); c.translate(x,y); c.rotate(rot||0); c.beginPath(); c.ellipse(0,0,rx,ry,0,0,6.2832); c.fill(); c.restore(); } |
|
function bodyPath(c,x,y,rx,ry){ |
|
// rounded blob, slightly pointed at top (head) |
|
c.beginPath(); |
|
c.moveTo(x, y-ry); |
|
c.bezierCurveTo(x+rx,y-ry, x+rx,y+ry*0.9, x+rx*0.5,y+ry); |
|
c.bezierCurveTo(x+rx*0.2,y+ry*1.05, x-rx*0.2,y+ry*1.05, x-rx*0.5,y+ry); |
|
c.bezierCurveTo(x-rx,y+ry*0.9, x-rx,y-ry, x,y-ry); |
|
c.closePath(); |
|
} |
|
|
|
/* ---------- Sprite: CAR ---------- */ |
|
function drawCar(c,x,y,w,h,v,dir){ |
|
c.save(); |
|
// shadow |
|
c.fillStyle="rgba(0,0,0,0.28)"; |
|
rr(x+2,y+h*0.16,w-2,h*0.86,h*0.32); c.fill(); |
|
// body |
|
const flip = dir<0; |
|
c.save(); |
|
if(flip){ c.translate(x+w/2,0); c.scale(-1,1); c.translate(-(x+w/2),0); } |
|
// chassis |
|
c.fillStyle=v.body; rr(x,y,w,h,h*0.34); c.fill(); |
|
// lower skirt |
|
c.fillStyle=v.dark; rr(x+w*0.04,y+h*0.62,w*0.92,h*0.3,h*0.2); c.fill(); |
|
// cabin/roof |
|
c.fillStyle=v.roof; rr(x+w*0.20,y+h*0.16,w*0.5,h*0.5,h*0.22); c.fill(); |
|
// windshield (front, toward +x = right since not flipped here in local) |
|
c.fillStyle=v.glass; |
|
c.beginPath(); |
|
c.moveTo(x+w*0.66,y+h*0.20); c.lineTo(x+w*0.78,y+h*0.20); c.lineTo(x+w*0.74,y+h*0.80); c.lineTo(x+w*0.64,y+h*0.80); c.closePath(); c.fill(); |
|
// rear window |
|
c.beginPath(); |
|
c.moveTo(x+w*0.22,y+h*0.20); c.lineTo(x+w*0.34,y+h*0.20); c.lineTo(x+w*0.34,y+h*0.80); c.lineTo(x+w*0.24,y+h*0.80); c.closePath(); c.fill(); |
|
// stripe |
|
c.fillStyle=v.stripe; c.fillRect(x+w*0.06,y+h*0.47,w*0.88,h*0.06); |
|
// headlights (front = right side locally) |
|
c.fillStyle="#fff6c8"; |
|
c.fillRect(x+w*0.93,y+h*0.18,w*0.05,h*0.12); |
|
c.fillRect(x+w*0.93,y+h*0.70,w*0.05,h*0.12); |
|
// taillights (left) |
|
c.fillStyle="#ff4d4d"; |
|
c.fillRect(x+w*0.02,y+h*0.18,w*0.04,h*0.12); |
|
c.fillRect(x+w*0.02,y+h*0.70,w*0.04,h*0.12); |
|
// wheels |
|
c.fillStyle="#15171c"; |
|
c.fillRect(x+w*0.10,y+h*0.04,w*0.18,h*0.10); |
|
c.fillRect(x+w*0.10,y+h*0.86,w*0.18,h*0.10); |
|
c.fillRect(x+w*0.72,y+h*0.04,w*0.18,h*0.10); |
|
c.fillRect(x+w*0.72,y+h*0.86,w*0.18,h*0.10); |
|
// cybertruck angularity |
|
if(v.name==="cybertruck"){ |
|
c.strokeStyle="rgba(0,0,0,0.25)"; c.lineWidth=1.5; |
|
c.beginPath(); c.moveTo(x+w*0.5,y+h*0.05); c.lineTo(x+w*0.96,y+h*0.5); c.lineTo(x+w*0.5,y+h*0.95); c.lineTo(x+w*0.04,y+h*0.5); c.closePath(); c.stroke(); |
|
} |
|
// tesla screen hint |
|
if(v.name==="tesla"){ c.fillStyle="#0c1218"; rr(x+w*0.42,y+h*0.30,w*0.10,h*0.20,2); c.fill(); } |
|
c.restore(); |
|
c.restore(); |
|
} |
|
|
|
/* ---------- Sprite: GADGET ---------- */ |
|
function drawGadget(c,x,y,w,h,v,dir){ |
|
c.save(); |
|
// shadow |
|
c.fillStyle="rgba(0,0,0,0.22)"; |
|
rr(x+2,y+h*0.1,w,h*0.9,h*0.18); c.fill(); |
|
if(v.name==="laptop"){ |
|
// base |
|
c.fillStyle=v.base; rr(x,y+h*0.42,w,h*0.58,h*0.14); c.fill(); |
|
// trackpad |
|
c.fillStyle="rgba(0,0,0,0.10)"; rr(x+w*0.30,y+h*0.66,w*0.40,h*0.26,h*0.06); c.fill(); |
|
// keyboard dots |
|
c.fillStyle="rgba(0,0,0,0.22)"; |
|
const cols=8,rowsK=3,kw=w/cols*0.6; |
|
for(let r=0;r<rowsK;r++) for(let cc=0;cc<cols;cc++){ |
|
c.fillRect(x+w*0.10+cc*(w*0.8/cols), y+h*0.46+r*h*0.06, kw, h*0.035); |
|
} |
|
// screen (lid) — hinged at top edge |
|
c.fillStyle=v.body; rr(x+w*0.06,y,w*0.88,h*0.44,h*0.1); c.fill(); |
|
c.fillStyle=v.screen; rr(x+w*0.10,y+h*0.05,w*0.80,h*0.34,h*0.06); c.fill(); |
|
// desktop bar |
|
c.fillStyle="#5a7bbf"; c.fillRect(x+w*0.10,y+h*0.05,w*0.80,h*0.06); |
|
c.fillStyle="rgba(255,255,255,0.25)"; |
|
for(let i=0;i<4;i++) c.fillRect(x+w*0.14+i*w*0.18,y+h*0.16,w*0.10,h*0.10); |
|
} else if(v.name==="monitor"){ |
|
// screen |
|
c.fillStyle=v.body; rr(x+w*0.04,y,w*0.92,h*0.74,h*0.1); c.fill(); |
|
c.fillStyle=v.screen; rr(x+w*0.10,y+h*0.07,w*0.80,h*0.6,h*0.04); c.fill(); |
|
// content |
|
c.fillStyle="#5a7bbf"; c.fillRect(x+w*0.10,y+h*0.07,w*0.80,h*0.07); |
|
c.fillStyle="rgba(255,255,255,0.22)"; |
|
c.fillRect(x+w*0.14,y+h*0.20,w*0.30,h*0.18); |
|
c.fillRect(x+w*0.48,y+h*0.20,w*0.38,h*0.08); |
|
c.fillRect(x+w*0.48,y+h*0.32,w*0.38,h*0.06); |
|
// stand neck + foot |
|
c.fillStyle=v.stand; |
|
c.fillRect(x+w*0.46,y+h*0.70,w*0.08,h*0.14); |
|
rr(x+w*0.32,y+h*0.82,w*0.36,h*0.12,h*0.05); c.fill(); |
|
} else { |
|
// phone / tablet |
|
const rad = v.name==="tablet" ? h*0.1 : h*0.18; |
|
c.fillStyle=v.body; rr(x,y,w,h,rad); c.fill(); |
|
c.fillStyle=v.screen; rr(x+w*0.12,y+h*0.07,w*0.76,h*0.86,rad*0.6); c.fill(); |
|
// notch (phone) |
|
if(v.name==="iphone"){ |
|
c.fillStyle=v.body; rr(x+w*0.38,y+h*0.03,w*0.24,h*0.06,h*0.03); c.fill(); |
|
} |
|
// app grid |
|
c.fillStyle=v.tile; |
|
const cols = v.name==="tablet"?5:4; |
|
const rowsG=6, pad=w*0.04, aw=(w*0.76-pad*(cols+1))/cols, ah=(h*0.86-pad*(rowsG+1))/rowsG; |
|
for(let r=0;r<rowsG;r++) for(let cc=0;cc<cols;cc++){ |
|
const ax=x+w*0.12+pad+cc*(aw+pad), ay=y+h*0.07+pad+r*(ah+pad); |
|
c.fillStyle = ["#ff5a3c","#f3b13d","#7ed957","#7fc6ff","#c98a5a"][(r*cols+cc)%5]; |
|
rr(ax,ay,aw,ah,Math.min(aw,ah)*0.25); c.fill(); |
|
} |
|
// reflection |
|
c.fillStyle="rgba(255,255,255,0.12)"; |
|
c.beginPath(); c.moveTo(x+w*0.12,y+h*0.07); c.lineTo(x+w*0.12+w*0.3,y+h*0.07); c.lineTo(x+w*0.12,y+h*0.07+h*0.4); c.closePath(); c.fill(); |
|
} |
|
c.restore(); |
|
} |
|
|
|
/* ---------- Particles ---------- */ |
|
function drawParticles(){ |
|
for(const p of particles){ |
|
const a=clamp(p.life/p.max,0,1); |
|
ctx.globalAlpha=a; |
|
ctx.fillStyle=p.color; |
|
if(p.shape==="star"){ |
|
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot); |
|
ctx.beginPath(); |
|
for(let i=0;i<5;i++){ |
|
const ang=i/5*6.28 - 1.57; |
|
ctx.lineTo(Math.cos(ang)*p.size, Math.sin(ang)*p.size); |
|
const ang2=ang+0.63; |
|
ctx.lineTo(Math.cos(ang2)*p.size*0.45, Math.sin(ang2)*p.size*0.45); |
|
} |
|
ctx.closePath(); ctx.fill(); ctx.restore(); |
|
} else { |
|
ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rot); |
|
ctx.fillStyle=p.color; ellipse(ctx,0,0,p.size*0.6,p.size*1.4,0); |
|
ctx.restore(); |
|
} |
|
} |
|
ctx.globalAlpha=1; |
|
} |
|
|
|
/* ---------- Toast ---------- */ |
|
function drawToast(){ |
|
const t=game.toast; if(!t) return; |
|
const a = clamp(game.toastLife,0,1) * (game.toastLife>1.7?(2-game.toastLife)/0.3:1); |
|
ctx.save(); |
|
ctx.globalAlpha=clamp(a,0,1); |
|
ctx.translate(W/2,H*0.42); |
|
const w=TILE*7.2, h=TILE*2.0; |
|
ctx.fillStyle="rgba(14,15,18,0.9)"; |
|
rr(-w/2,-h/2,w,h,16); ctx.fill(); |
|
ctx.strokeStyle="#f3b13d"; ctx.lineWidth=2; rr(-w/2,-h/2,w,h,16); ctx.stroke(); |
|
ctx.fillStyle="#fff"; ctx.textAlign="center"; ctx.textBaseline="middle"; |
|
ctx.font="800 "+Math.floor(TILE*0.5)+"px 'Bricolage Grotesque',sans-serif"; |
|
ctx.fillText(t.text,0,-TILE*0.18); |
|
ctx.font="500 "+Math.floor(TILE*0.3)+"px 'Sora',sans-serif"; |
|
ctx.fillStyle="#f3b13d"; ctx.fillText(t.sub,0,TILE*0.32); |
|
ctx.restore(); |
|
} |
|
|
|
/* ---------- Loop ---------- */ |
|
let last=0; |
|
function frame(ts){ |
|
if(!last) last=ts; |
|
let dt=(ts-last)/1000; last=ts; |
|
if(dt>0.05) dt=0.05; |
|
update(dt); |
|
render(); |
|
requestAnimationFrame(frame); |
|
} |
|
|
|
/* ---------- Init ---------- */ |
|
function init(){ |
|
try{ game.hi=parseInt(localStorage.getItem(HI_KEY)||"0",10)||0; }catch(e){ game.hi=0; } |
|
resize(); |
|
updateHUD(); |
|
// idle ambient: show a frog on the start rug behind the overlay |
|
requestAnimationFrame(frame); |
|
} |
|
init(); |
|
})(); |
|
</script> |
|
</body> |
|
</html> |
