Skip to content

Instantly share code, notes, and snippets.

@ivanfioravanti
Last active August 10, 2026 13:47
Show Gist options
  • Select an option

  • Save ivanfioravanti/6042a6b02a7a95648b797fd6ec1c0760 to your computer and use it in GitHub Desktop.

Select an option

Save ivanfioravanti/6042a6b02a7a95648b797fd6ec1c0760 to your computer and use it in GitHub Desktop.
Frogger AI Prompt and results

Build a Frogger style arcade game in a single self-contained HTML file with inline CSS and JS. But Frog lives in a modern home, and must avoid contact with day to day objects like iPhone, computer, monitor, mini cars like tesla, Ferrari, etc. create sprites and game mechanic. If you feel html is not enough use a game engine of your choice.

<!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&nbsp;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>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Modern Frogger — Smart Home Crossing</title>
<style>
:root {
--font-display: "Avenir Next Condensed", "Futura", "Trebuchet MS", system-ui, sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
--amber: #ffd166;
--frog: #7ee787;
--ink: #f3ead9;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
min-height: 100dvh;
display: flex; align-items: center; justify-content: center;
background:
radial-gradient(1100px 700px at 50% -10%, #33261a 0%, #1a1410 55%, #0d0b08 100%);
color: var(--ink);
font-family: var(--font-display);
overflow: hidden;
touch-action: none;
-webkit-user-select: none; user-select: none;
}
#app {
display: flex; flex-direction: column; align-items: center; gap: 10px;
width: min(94vw, calc((100dvh - 170px) * 0.9286), 660px);
}
header { text-align: center; line-height: 1.05; }
header h1 {
font-size: clamp(26px, 5vw, 40px);
font-weight: 800; letter-spacing: 0.14em;
background: linear-gradient(100deg, var(--amber) 15%, #ff9f43 45%, var(--frog) 85%);
-webkit-background-clip: text; background-clip: text; color: transparent;
text-shadow: 0 0 26px rgba(255, 209, 102, 0.18);
}
header p {
font-size: clamp(10px, 1.8vw, 13px);
letter-spacing: 0.32em; text-transform: uppercase; color: #9c8b74; margin-top: 4px;
}
#stage {
position: relative; width: 100%;
background: #211812;
border: 1px solid #43321f;
border-radius: 18px;
padding: 12px;
box-shadow:
0 30px 60px rgba(0,0,0,0.55),
0 0 0 6px rgba(0,0,0,0.18),
0 0 44px rgba(255, 159, 67, 0.07);
}
#hud {
display: flex; justify-content: space-between; align-items: center;
font-family: var(--font-mono); font-size: clamp(11px, 2vw, 14px);
padding: 2px 6px 10px; letter-spacing: 0.06em; color: #d8c9ae;
}
#hud .grp { display: flex; gap: 1.2em; align-items: center; }
#hud b { color: var(--amber); font-weight: 700; }
#hudFrogs { letter-spacing: 0.1em; font-size: 1.05em; }
#timeWrap {
height: 7px; margin: 0 6px 10px; border-radius: 99px;
background: rgba(0,0,0,0.45); overflow: hidden;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.6);
}
#timeBar {
height: 100%; width: 100%; border-radius: 99px;
background: linear-gradient(90deg, #57cc5f, #ffd166 70%, #ff6b5e);
transition: width 0.1s linear;
}
#cvHolder { position: relative; border-radius: 10px; overflow: hidden; }
canvas { display: block; width: 100%; height: auto; background: #c9a06b; }
.scan {
position: absolute; inset: 0; pointer-events: none;
background: repeating-linear-gradient(0deg, rgba(0,0,0,0.10) 0 1px, transparent 1px 3px);
opacity: 0.55; mix-blend-mode: multiply;
}
.glare {
position: absolute; inset: 0; pointer-events: none;
background: radial-gradient(120% 70% at 18% -8%, rgba(255,255,255,0.10), transparent 55%);
}
#overlay {
position: absolute; inset: 0; z-index: 5;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: rgba(13, 11, 8, 0.86); backdrop-filter: blur(3px);
text-align: center; padding: 24px; gap: 10px;
}
#overlay.hidden { display: none; }
#overlay h1 {
font-size: clamp(30px, 6vw, 52px); font-weight: 800; letter-spacing: 0.12em;
background: linear-gradient(100deg, var(--amber), #ff9f43 50%, var(--frog));
-webkit-background-clip: text; background-clip: text; color: transparent;
}
#overlay p { font-size: clamp(13px, 2.4vw, 17px); color: #d8c9ae; max-width: 42ch; line-height: 1.5; }
#overlay .key { color: var(--amber); font-weight: 700; font-family: var(--font-mono); }
#overlay .prompt {
margin-top: 14px; font-family: var(--font-mono); font-size: clamp(12px, 2vw, 15px);
color: var(--frog); letter-spacing: 0.2em;
animation: blink 1.1s steps(2, start) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
footer {
font-family: var(--font-mono); font-size: clamp(10px, 1.7vw, 12px);
color: #7d6f5b; letter-spacing: 0.08em; text-align: center;
}
footer b { color: #b3a28a; }
#dpad {
position: fixed; right: 16px; bottom: 16px; z-index: 20;
display: none; grid-template-columns: repeat(3, 56px); grid-template-rows: repeat(2, 56px);
gap: 8px;
}
#dpad button {
border: 1px solid rgba(255,255,255,0.22); border-radius: 14px;
background: rgba(255,255,255,0.10); color: #fff; font-size: 22px;
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
touch-action: none;
}
#dpad button:active { background: rgba(255,255,255,0.28); }
#dpad .up { grid-column: 2; grid-row: 1; }
#dpad .lf { grid-column: 1; grid-row: 2; }
#dpad .dn { grid-column: 2; grid-row: 2; }
#dpad .rt { grid-column: 3; grid-row: 2; }
@media (pointer: coarse) { #dpad { display: grid; } }
</style>
</head>
<body>
<div id="app">
<header>
<h1>MODERN FROGGER</h1>
<p>one frog · one smart home · zero chill</p>
</header>
<div id="stage">
<div id="hud">
<div class="grp"><span>SCORE <b id="hudScore">0</b></span><span>HI <b id="hudHi">0</b></span></div>
<div class="grp"><span>LV <b id="hudLevel">1</b></span><span id="hudFrogs">🐸🐸🐸</span></div>
</div>
<div id="timeWrap"><div id="timeBar"></div></div>
<div id="cvHolder">
<canvas id="game"></canvas>
<div class="scan"></div>
<div class="glare"></div>
</div>
<div id="overlay"></div>
</div>
<footer><b>←→↑↓ / WASD</b> hop &nbsp;·&nbsp; <b>P</b> pause &nbsp;·&nbsp; <b>M</b> mute &nbsp;·&nbsp; dodge the gadgets, ride the clutter, reach an outlet</footer>
</div>
<div id="dpad">
<button class="up" data-d="up" aria-label="up"></button>
<button class="lf" data-d="left" aria-label="left"></button>
<button class="dn" data-d="down" aria-label="down"></button>
<button class="rt" data-d="right" aria-label="right"></button>
</div>
<script>
"use strict";
/* ============================================================
MODERN FROGGER — a Frogger-style arcade game set in a smart home.
Dodge Roombas, mini Teslas & Ferraris, sliding iPhones, laptops
and monitor carts; ride magazines, iPads, slippers, drones and
cushions across the freshly-mopped wet floor; reach the outlets.
============================================================ */
// ---------- Grid & canvas ----------
const COLS = 13, ROWS = 14, CELL = 48;
const W = COLS * CELL, H = ROWS * CELL;
const DOCK_COLS = [0, 3, 6, 9, 12]; // outlets along the top wall
const RIDE_ROWS = [1, 2, 3, 4, 5]; // wet-floor zone (ride the clutter)
const cv = document.getElementById("game");
const g = cv.getContext("2d");
const dpr = Math.min(2, window.devicePixelRatio || 1);
cv.width = W * dpr; cv.height = H * dpr;
g.setTransform(dpr, 0, 0, dpr, 0, 0);
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const lerp = (a, b, t) => a + (b - a) * t;
const cellCX = c => c * CELL + CELL / 2;
const cellCY = r => r * CELL + CELL / 2;
// rounded-rect path helper (avoids relying on ctx.roundRect support)
function rr(ctx, x, y, w, h, r) {
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();
}
// ---------- Audio (tiny WebAudio synth) ----------
let actx = null, muted = false;
function audio() {
if (!actx) { try { actx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {} }
if (actx && actx.state === "suspended") actx.resume();
return actx;
}
function tone(f0, f1, dur, type = "square", vol = 0.12, delay = 0) {
if (muted) return;
const a = audio(); if (!a) return;
const t = a.currentTime + delay;
const o = a.createOscillator(), gn = a.createGain();
o.type = type;
o.frequency.setValueAtTime(f0, t);
o.frequency.exponentialRampToValueAtTime(Math.max(1, f1), t + dur);
gn.gain.setValueAtTime(vol, t);
gn.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(gn); gn.connect(a.destination);
o.start(t); o.stop(t + dur + 0.02);
}
const sfx = {
hop: () => tone(420, 640, 0.07, "triangle", 0.10),
bump: () => tone(140, 90, 0.07, "square", 0.08),
zap: () => { tone(320, 40, 0.45, "sawtooth", 0.16); tone(180, 30, 0.4, "square", 0.08, 0.03); },
splash:() => tone(260, 55, 0.5, "sine", 0.18),
time: () => tone(880, 110, 0.5, "triangle", 0.12),
dock: () => { tone(523, 523, 0.09, "triangle", 0.12); tone(659, 659, 0.09, "triangle", 0.12, 0.09); tone(880, 880, 0.14, "triangle", 0.12, 0.18); },
level: () => { [523, 659, 784, 1046].forEach((f, i) => tone(f, f, 0.12, "square", 0.10, i * 0.11)); },
over: () => { [392, 330, 262, 196].forEach((f, i) => tone(f, f * 0.9, 0.16, "sawtooth", 0.10, i * 0.15)); },
};
// ============================================================
// SPRITES — everything is drawn top-down, centered on origin.
// Vehicles face +x (right); callers flip for leftward lanes.
// ============================================================
function ellipse(ctx, x, y, rx, ry, rot = 0) {
ctx.beginPath(); ctx.ellipse(x, y, rx, ry, rot, 0, Math.PI * 2); ctx.fill();
}
function drawFrog(ctx, x, y, angle, stretch = 1, scale = 1) {
ctx.save();
ctx.translate(x, y); ctx.rotate(angle); ctx.scale(scale, scale * stretch);
// legs
ctx.fillStyle = "#2f8f3e";
ellipse(ctx, -13, 9, 5, 8, -0.5); ellipse(ctx, 13, 9, 5, 8, 0.5);
ellipse(ctx, -12, -8, 4, 6, -0.4); ellipse(ctx, 12, -8, 4, 6, 0.4);
// body
ctx.fillStyle = "#54c24a";
ellipse(ctx, 0, 0, 15, 17);
ctx.strokeStyle = "#1e5c2a"; ctx.lineWidth = 2;
ctx.beginPath(); ctx.ellipse(0, 0, 15, 17, 0, 0, Math.PI * 2); ctx.stroke();
// belly
ctx.fillStyle = "#b8e986";
ellipse(ctx, 0, 5, 9, 10);
// eyes
ctx.fillStyle = "#fff";
ellipse(ctx, -7.5, -13, 5, 5); ellipse(ctx, 7.5, -13, 5, 5);
ctx.fillStyle = "#141414";
ellipse(ctx, -7.5, -14.5, 2.3, 2.3); ellipse(ctx, 7.5, -14.5, 2.3, 2.3);
// blush + smile
ctx.fillStyle = "rgba(255,150,150,0.5)";
ellipse(ctx, -10, -6, 2.4, 1.5); ellipse(ctx, 10, -6, 2.4, 1.5);
ctx.strokeStyle = "#1e5c2a"; ctx.lineWidth = 1.6; ctx.beginPath();
ctx.arc(0, -8, 6, Math.PI * 0.25, Math.PI * 0.75); ctx.stroke();
ctx.restore();
}
function carBase(ctx, w, h, bodyFn) {
// wheels (shared by both cars), then delegate body paint
ctx.fillStyle = "#17191d";
const wx = w * 0.30, wy = h / 2 - 1;
rr(ctx, -wx - 7, -wy - 4, 14, 6, 3); ctx.fill();
rr(ctx, -wx - 7, wy - 2, 14, 6, 3); ctx.fill();
rr(ctx, wx - 7, -wy - 4, 14, 6, 3); ctx.fill();
rr(ctx, wx - 7, wy - 2, 14, 6, 3); ctx.fill();
bodyFn();
}
function drawTesla(ctx, x, y, w, h, flip) {
ctx.save(); ctx.translate(x, y); ctx.scale(flip ? -1 : 1, 1);
carBase(ctx, w, h, () => {
ctx.fillStyle = "#e9eaee";
rr(ctx, -w / 2, -h / 2 + 3, w, h - 6, 14); ctx.fill();
ctx.strokeStyle = "#b9bdc7"; ctx.lineWidth = 1.5;
rr(ctx, -w / 2, -h / 2 + 3, w, h - 6, 14); ctx.stroke();
// glass roof / cabin
const grd = ctx.createLinearGradient(0, -h / 2, 0, h / 2);
grd.addColorStop(0, "#28323f"); grd.addColorStop(0.5, "#3d4c5c"); grd.addColorStop(1, "#28323f");
ctx.fillStyle = grd;
rr(ctx, -w * 0.22, -h / 2 + 7, w * 0.44, h - 14, 8); ctx.fill();
// light bar (rear) + headlights (front)
ctx.fillStyle = "#ff3b30"; rr(ctx, -w / 2 + 1, -h / 2 + 8, 3, h - 16, 1.5); ctx.fill();
ctx.fillStyle = "#fff7c2";
ellipse(ctx, w / 2 - 3, -h / 2 + 9, 2.6, 2); ellipse(ctx, w / 2 - 3, h / 2 - 9, 2.6, 2);
});
ctx.restore();
}
function drawFerrari(ctx, x, y, w, h, flip) {
ctx.save(); ctx.translate(x, y); ctx.scale(flip ? -1 : 1, 1);
carBase(ctx, w, h, () => {
ctx.fillStyle = "#d90429";
rr(ctx, -w / 2, -h / 2 + 3, w, h - 6, 12); ctx.fill();
ctx.strokeStyle = "#8f0320"; ctx.lineWidth = 1.5;
rr(ctx, -w / 2, -h / 2 + 3, w, h - 6, 12); ctx.stroke();
// roof stripe + cabin
ctx.fillStyle = "#141414";
rr(ctx, -w * 0.26, -4, w * 0.5, 8, 4); ctx.fill();
ctx.fillStyle = "#20262e";
rr(ctx, -w * 0.2, -h / 2 + 7, w * 0.34, h - 14, 7); ctx.fill();
// spoiler at rear
ctx.fillStyle = "#141414";
rr(ctx, -w / 2 - 4, -h / 2 + 5, 5, h - 10, 2); ctx.fill();
// headlights + shield
ctx.fillStyle = "#ffe066";
ellipse(ctx, w / 2 - 3, -h / 2 + 8, 2.6, 2); ellipse(ctx, w / 2 - 3, h / 2 - 8, 2.6, 2);
ctx.fillStyle = "#ffd60a"; ellipse(ctx, w * 0.1, -h / 2 + 5.5, 2.2, 2.2);
});
ctx.restore();
}
function drawIPhone(ctx, x, y, w, h, t) {
ctx.save(); ctx.translate(x, y);
ctx.fillStyle = "#15161c";
rr(ctx, -w / 2, -h / 2, w, h, 9); ctx.fill();
ctx.strokeStyle = "#3a3d47"; ctx.lineWidth = 1.5;
rr(ctx, -w / 2, -h / 2, w, h, 9); ctx.stroke();
// glowing screen
const grd = ctx.createLinearGradient(-w / 2, -h / 2, w / 2, h / 2);
grd.addColorStop(0, "#0b3b66"); grd.addColorStop(0.55, "#0e5aa0"); grd.addColorStop(1, "#0a2c4e");
ctx.fillStyle = grd;
rr(ctx, -w / 2 + 4, -h / 2 + 4, w - 8, h - 8, 6); ctx.fill();
// notification cards
ctx.fillStyle = "rgba(255,255,255,0.82)";
rr(ctx, -w / 2 + 10, -7, w - 26, 6, 3); ctx.fill();
rr(ctx, -w / 2 + 10, 3, w - 34, 6, 3); ctx.fill();
// side button glints + home bar
ctx.fillStyle = "#e8e8ec"; rr(ctx, w / 2 - 12, -2, 7, 3, 1.5); ctx.fill();
// moving glare
const gx = ((t * 60) % (w + 40)) - w / 2 - 20;
ctx.fillStyle = "rgba(255,255,255,0.14)";
ctx.beginPath();
ctx.moveTo(gx, -h / 2 + 4); ctx.lineTo(gx + 12, -h / 2 + 4);
ctx.lineTo(gx + 2, h / 2 - 4); ctx.lineTo(gx - 10, h / 2 - 4);
ctx.closePath(); ctx.fill();
ctx.restore();
}
function drawLaptop(ctx, x, y, w, h) {
ctx.save(); ctx.translate(x, y);
// screen half (top)
ctx.fillStyle = "#1b1d22";
rr(ctx, -w / 2, -h / 2, w, h / 2, 4); ctx.fill();
const grd = ctx.createLinearGradient(0, -h / 2, 0, 0);
grd.addColorStop(0, "#123c33"); grd.addColorStop(1, "#1d6f5c");
ctx.fillStyle = grd;
rr(ctx, -w / 2 + 3, -h / 2 + 3, w - 6, h / 2 - 6, 3); ctx.fill();
// code lines on screen
const cols = ["#7ee787", "#ffd166", "#79c0ff"];
for (let i = 0; i < 3; i++) {
ctx.fillStyle = cols[i]; ctx.globalAlpha = 0.85;
rr(ctx, -w / 2 + 7 + (i % 2) * 5, -h / 2 + 6 + i * 5, 18 + i * 7, 2.4, 1); ctx.fill();
ctx.globalAlpha = 1;
}
// keyboard half (bottom)
ctx.fillStyle = "#c6cbd4";
rr(ctx, -w / 2, 0, w, h / 2, 4); ctx.fill();
ctx.strokeStyle = "#9aa1ac"; ctx.lineWidth = 1;
rr(ctx, -w / 2, 0, w, h / 2, 4); ctx.stroke();
ctx.fillStyle = "#8d939e";
for (let r = 0; r < 2; r++) for (let c = 0; c < 8; c++)
{ rr(ctx, -w / 2 + 5 + c * (w - 10) / 8, 4 + r * 5, (w - 10) / 8 - 2, 3.4, 1); ctx.fill(); }
ctx.fillStyle = "#aab0ba"; rr(ctx, -7, h / 2 - 6, 14, 4, 2); ctx.fill();
ctx.restore();
}
function drawMonitorCart(ctx, x, y, w, h) {
ctx.save(); ctx.translate(x, y);
// cart base + casters
ctx.fillStyle = "#5c6470";
rr(ctx, -w / 2 + 6, h / 2 - 12, w - 12, 9, 4); ctx.fill();
ctx.fillStyle = "#2b2f36";
const cx0 = -w / 2 + 10, cx1 = w / 2 - 10;
ellipse(ctx, cx0, h / 2 - 2, 2.6, 2.6); ellipse(ctx, cx1, h / 2 - 2, 2.6, 2.6);
// stand neck
ctx.fillStyle = "#454b54"; rr(ctx, -3, 0, 6, h / 2 - 8, 2); ctx.fill();
// monitor
ctx.fillStyle = "#101216";
rr(ctx, -w / 2, -h / 2, w, h - 14, 4); ctx.fill();
const grd = ctx.createLinearGradient(-w / 2, -h / 2, w / 2, 0);
grd.addColorStop(0, "#1b2a4a"); grd.addColorStop(1, "#274b8f");
ctx.fillStyle = grd;
rr(ctx, -w / 2 + 3, -h / 2 + 3, w - 6, h - 20, 3); ctx.fill();
// chart on screen
ctx.strokeStyle = "#7ee787"; ctx.lineWidth = 2; ctx.beginPath();
ctx.moveTo(-w / 2 + 8, -6);
ctx.lineTo(-w / 2 + 18, -12); ctx.lineTo(-w / 2 + 28, -8); ctx.lineTo(-w / 2 + 40, -16);
ctx.stroke();
ctx.fillStyle = "#ffd166"; ellipse(ctx, -w / 2 + 40, -16, 2, 2);
ctx.restore();
}
function drawRoomba(ctx, x, y, t) {
ctx.save(); ctx.translate(x, y);
ctx.fillStyle = "#2b2f36";
ellipse(ctx, 0, 0, 16, 16);
ctx.strokeStyle = "#454b54"; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.ellipse(0, 0, 16, 16, 0, 0, Math.PI * 2); ctx.stroke();
ctx.fillStyle = "#3a404a"; ellipse(ctx, 0, 0, 10.5, 10.5);
ctx.fillStyle = "#22262d"; ellipse(ctx, 0, 0, 6, 6);
// blinking LED
const on = Math.sin(t * 5) > -0.2;
ctx.fillStyle = on ? "#57cc5f" : "#2a5c33";
ellipse(ctx, 0, -3, 2.2, 2.2);
// bumper arc + spinning side brush
ctx.strokeStyle = "#565e6a"; ctx.lineWidth = 3; ctx.beginPath();
ctx.arc(0, 0, 14, -Math.PI * 0.35, Math.PI * 0.35); ctx.stroke();
ctx.strokeStyle = "#8a929e"; ctx.lineWidth = 1.6;
const a = t * 9;
for (let i = 0; i < 3; i++) {
ctx.beginPath();
ctx.moveTo(14 * Math.cos(a + i * 2.09), 14 * Math.sin(a + i * 2.09));
ctx.lineTo(19 * Math.cos(a + i * 2.09), 19 * Math.sin(a + i * 2.09));
ctx.stroke();
}
ctx.restore();
}
// ---------- rideable clutter (wet-floor zone) ----------
function drawMagazine(ctx, x, y, w, h, rot, hue) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
ctx.fillStyle = "#f7f4ee";
rr(ctx, -w / 2, -h / 2, w, h, 3); ctx.fill();
ctx.strokeStyle = "#cfc9bd"; ctx.lineWidth = 1.2;
rr(ctx, -w / 2, -h / 2, w, h, 3); ctx.stroke();
ctx.fillStyle = hue; rr(ctx, -w / 2, -h / 2, w, 9, 3); ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.9)";
ctx.font = "bold 7px monospace";
ctx.textAlign = "left"; ctx.textBaseline = "middle";
ctx.fillText("HOME·TECH", -w / 2 + 4, -h / 2 + 5);
ctx.fillStyle = "#b9b2a4";
rr(ctx, -w / 2 + 5, -h / 2 + 14, w * 0.45, 3, 1); ctx.fill();
rr(ctx, -w / 2 + 5, -h / 2 + 20, w * 0.35, 3, 1); ctx.fill();
ctx.fillStyle = hue + "55";
rr(ctx, w / 2 - w * 0.32, -h / 2 + 13, w * 0.26, h - 20, 2); ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.16)";
ctx.beginPath(); ctx.moveTo(-w / 2, h / 2); ctx.lineTo(w / 4, -h / 2);
ctx.lineTo(w / 2, -h / 2); ctx.lineTo(-w / 4, h / 2); ctx.closePath(); ctx.fill();
ctx.restore();
}
function drawIPad(ctx, x, y, w, h, rot) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
ctx.fillStyle = "#14161c";
rr(ctx, -w / 2, -h / 2, w, h, 7); ctx.fill();
ctx.strokeStyle = "#3a3d47"; ctx.lineWidth = 1.5;
rr(ctx, -w / 2, -h / 2, w, h, 7); ctx.stroke();
const grd = ctx.createLinearGradient(-w / 2, 0, w / 2, 0);
grd.addColorStop(0, "#0c3350"); grd.addColorStop(0.5, "#1577c2"); grd.addColorStop(1, "#0c3350");
ctx.fillStyle = grd;
rr(ctx, -w / 2 + 5, -h / 2 + 5, w - 10, h - 10, 4); ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.75)";
rr(ctx, -8, h / 2 - 8, 16, 2.4, 1.2); ctx.fill();
ctx.fillStyle = "rgba(255,255,255,0.15)";
ctx.beginPath(); ctx.moveTo(-w / 4, -h / 2 + 5); ctx.lineTo(w / 8, -h / 2 + 5);
ctx.lineTo(-w / 8, h / 2 - 5); ctx.lineTo(-w / 2 + 12, h / 2 - 5); ctx.closePath(); ctx.fill();
ctx.restore();
}
const SLIPPER_COLORS = ["#e07a5f", "#81b29a", "#f2cc8f", "#9d8ce0"];
function drawSlippers(ctx, x, y, w, h, rot, ci) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
const col = SLIPPER_COLORS[ci % SLIPPER_COLORS.length];
for (const s of [-1, 1]) {
ctx.save(); ctx.translate(s * w * 0.22, 0); ctx.rotate(s * 0.12);
ctx.fillStyle = col;
ellipse(ctx, 0, 0, w * 0.17, h * 0.44);
ctx.strokeStyle = "rgba(255,255,255,0.55)"; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.ellipse(0, 0, w * 0.17, h * 0.44, 0, 0, Math.PI * 2); ctx.stroke();
ctx.fillStyle = "rgba(0,0,0,0.28)";
ellipse(ctx, 0, -h * 0.16, w * 0.10, h * 0.16);
ctx.restore();
}
ctx.restore();
}
function drawDrone(ctx, x, y, w, h, t) {
ctx.save(); ctx.translate(x, y);
// arms
ctx.strokeStyle = "#4b5260"; ctx.lineWidth = 4; ctx.beginPath();
ctx.moveTo(-w * 0.32, -h * 0.30); ctx.lineTo(w * 0.32, h * 0.30);
ctx.moveTo(w * 0.32, -h * 0.30); ctx.lineTo(-w * 0.32, h * 0.30);
ctx.stroke();
// rotors
for (const [rx, ry] of [[-w * 0.36, -h * 0.34], [w * 0.36, -h * 0.34], [-w * 0.36, h * 0.34], [w * 0.36, h * 0.34]]) {
ctx.fillStyle = "rgba(220,226,235,0.30)";
ellipse(ctx, rx, ry, 9, 9);
ctx.strokeStyle = "rgba(140,148,160,0.9)"; ctx.lineWidth = 1.5;
const a = t * 22 + rx;
ctx.beginPath();
ctx.moveTo(rx - 8 * Math.cos(a), ry - 8 * Math.sin(a));
ctx.lineTo(rx + 8 * Math.cos(a), ry + 8 * Math.sin(a));
ctx.stroke();
}
// body
ctx.fillStyle = "#2f3542";
rr(ctx, -w * 0.16, -h * 0.30, w * 0.32, h * 0.60, 5); ctx.fill();
ctx.strokeStyle = "#57606f"; ctx.lineWidth = 1.4;
rr(ctx, -w * 0.16, -h * 0.30, w * 0.32, h * 0.60, 5); ctx.stroke();
ctx.fillStyle = Math.sin(t * 6) > 0 ? "#ffd166" : "#7a6224";
ellipse(ctx, w * 0.10, 0, 2, 2);
ctx.restore();
}
const CUSHION_COLORS = ["#e63946", "#f4a261", "#2a9d8f", "#e9c46a"];
function drawCushion(ctx, x, y, w, h, rot, ci) {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
const col = CUSHION_COLORS[ci % CUSHION_COLORS.length];
ctx.fillStyle = col;
rr(ctx, -w / 2, -h / 2, w, h, 10); ctx.fill();
ctx.strokeStyle = "rgba(255,255,255,0.45)"; ctx.lineWidth = 2;
rr(ctx, -w / 2 + 4, -h / 2 + 4, w - 8, h - 8, 7); ctx.stroke();
ctx.strokeStyle = "rgba(0,0,0,0.18)"; ctx.lineWidth = 1.5;
rr(ctx, -w / 2, -h / 2, w, h, 10); ctx.stroke();
// tuft
ctx.fillStyle = "rgba(0,0,0,0.25)"; ellipse(ctx, 0, 0, 2.6, 2.6);
ctx.strokeStyle = "rgba(0,0,0,0.15)"; ctx.beginPath();
ctx.moveTo(-8, 0); ctx.lineTo(-3, 0); ctx.moveTo(3, 0); ctx.lineTo(8, 0);
ctx.moveTo(0, -6); ctx.lineTo(0, -3); ctx.moveTo(0, 3); ctx.lineTo(0, 6);
ctx.stroke();
ctx.restore();
}
// ============================================================
// SCENERY
// ============================================================
function drawFloor(ctx) {
ctx.fillStyle = "#c9a06b";
ctx.fillRect(0, 0, W, H);
for (let r = 0; r < ROWS; r++) {
if (r % 2) { ctx.fillStyle = "rgba(0,0,0,0.045)"; ctx.fillRect(0, r * CELL, W, CELL); }
ctx.strokeStyle = "rgba(90,60,30,0.16)"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, r * CELL + 0.5); ctx.lineTo(W, r * CELL + 0.5); ctx.stroke();
const off = (r % 2) * CELL;
for (let x = off; x < W; x += CELL * 2) {
ctx.beginPath(); ctx.moveTo(x + 0.5, r * CELL); ctx.lineTo(x + 0.5, r * CELL + CELL); ctx.stroke();
}
}
}
function drawRugRow(ctx, row) {
const y = row * CELL;
ctx.fillStyle = "#c1633c";
ctx.fillRect(0, y, W, CELL);
ctx.strokeStyle = "#e8c37a"; ctx.lineWidth = 2;
ctx.setLineDash([10, 7]);
ctx.strokeRect(6, y + 6, W - 12, CELL - 12);
ctx.setLineDash([]);
ctx.fillStyle = "rgba(0,0,0,0.10)";
ctx.fillRect(0, y, W, 3); ctx.fillRect(0, y + CELL - 3, W, 3);
}
function drawWetZone(ctx, t) {
const y0 = CELL, h = CELL * 5;
const grd = ctx.createLinearGradient(0, y0, 0, y0 + h);
grd.addColorStop(0, "rgba(126,178,205,0.62)");
grd.addColorStop(0.5, "rgba(109,163,193,0.55)");
grd.addColorStop(1, "rgba(126,178,205,0.62)");
ctx.fillStyle = grd; ctx.fillRect(0, y0, W, h);
// faint wave lines
ctx.strokeStyle = "rgba(255,255,255,0.12)"; ctx.lineWidth = 1.2;
for (let i = 0; i < 4; i++) {
const yy = y0 + 14 + i * 30 + Math.sin(t * 1.4 + i) * 3;
ctx.beginPath();
for (let x = 0; x <= W; x += 24) ctx.lineTo(x, yy + Math.sin(x / 46 + t * 2 + i) * 2.5);
ctx.stroke();
}
// moving mop-shine streaks
ctx.save(); ctx.beginPath(); ctx.rect(0, y0, W, h); ctx.clip();
for (let i = 0; i < 3; i++) {
const sx = ((t * 36 + i * 300) % (W + 240)) - 120;
ctx.fillStyle = "rgba(255,255,255,0.09)";
ctx.beginPath();
ctx.moveTo(sx, y0); ctx.lineTo(sx + 46, y0);
ctx.lineTo(sx + 10, y0 + h); ctx.lineTo(sx - 36, y0 + h);
ctx.closePath(); ctx.fill();
}
ctx.restore();
// caution sign on the rug edge
ctx.save(); ctx.translate(26, CELL * 6 + CELL / 2); ctx.rotate(Math.PI / 4);
ctx.fillStyle = "#ffd60a"; ctx.fillRect(-10, -10, 20, 20);
ctx.strokeStyle = "#8a6d00"; ctx.lineWidth = 2; ctx.strokeRect(-10, -10, 20, 20);
ctx.rotate(-Math.PI / 4);
ctx.fillStyle = "#141414"; ctx.font = "bold 15px sans-serif";
ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("!", 0, 1);
ctx.restore();
}
function drawWallRow(ctx) {
const grd = ctx.createLinearGradient(0, 0, 0, CELL);
grd.addColorStop(0, "#efe7da"); grd.addColorStop(1, "#e2d7c4");
ctx.fillStyle = grd; ctx.fillRect(0, 0, W, CELL);
// baseboard
ctx.fillStyle = "#fbf7f0"; ctx.fillRect(0, CELL - 9, W, 9);
ctx.fillStyle = "rgba(0,0,0,0.18)"; ctx.fillRect(0, CELL - 10, W, 2);
// framed photos between outlets
ctx.textAlign = "center"; ctx.textBaseline = "middle";
for (const bx of [2, 5, 8, 11].map(c => cellCX(c) - CELL / 2)) {
ctx.fillStyle = "#8b5e34"; rr(ctx, bx - 13, 7, 26, 20, 3); ctx.fill();
ctx.fillStyle = "#a8d5e5"; rr(ctx, bx - 10, 10, 20, 14, 2); ctx.fill();
ctx.fillStyle = "#7ec850"; ellipse(ctx, bx - 4, 20, 6, 4);
ctx.fillStyle = "#ffd166"; ellipse(ctx, bx + 4, 15, 3, 3);
}
// outlets (docks)
for (let i = 0; i < DOCK_COLS.length; i++) {
const x = cellCX(DOCK_COLS[i]);
if (docks[i]) {
const glow = ctx.createRadialGradient(x, CELL - 18, 2, x, CELL - 18, 26);
glow.addColorStop(0, "rgba(126,231,135,0.55)"); glow.addColorStop(1, "rgba(126,231,135,0)");
ctx.fillStyle = glow; ctx.fillRect(x - 26, CELL - 44, 52, 44);
}
ctx.fillStyle = "#fdfdfb"; rr(ctx, x - 13, 8, 26, 22, 4); ctx.fill();
ctx.strokeStyle = "#c9c2b4"; ctx.lineWidth = 1.4; rr(ctx, x - 13, 8, 26, 22, 4); ctx.stroke();
ctx.fillStyle = "#3a3a3a";
rr(ctx, x - 6.5, 13, 3, 8, 1.5); ctx.fill();
rr(ctx, x + 3.5, 13, 3, 8, 1.5); ctx.fill();
ctx.fillStyle = "#9a938a"; ellipse(ctx, x, 24.5, 1.4, 1.4);
if (docks[i]) drawFrog(ctx, x, CELL - 16, Math.PI, 1, 0.72);
}
}
// ============================================================
// LANES
// ============================================================
const PAD = 130;
const HAZARD_DEFS = [
{ row: 7, type: "roomba", speed: 52, count: 4, wCells: 1.0 },
{ row: 8, type: "tesla", speed: -88, count: 3, wCells: 1.7 },
{ row: 9, type: "iphone", speed: 112, count: 4, wCells: 1.0 },
{ row: 10, type: "ferrari", speed: -145, count: 3, wCells: 1.8 },
{ row: 11, type: "laptop", speed: 72, count: 3, wCells: 1.3 },
{ row: 12, type: "monitor", speed: -62, count: 3, wCells: 1.4 },
];
const RIDE_DEFS = [
{ row: 1, type: "magazine", speed: 55, count: 3, wCells: 2.2 },
{ row: 2, type: "ipad", speed: -78, count: 3, wCells: 1.9 },
{ row: 3, type: "slippers", speed: 98, count: 4, wCells: 1.6 },
{ row: 4, type: "drone", speed: -125, count: 3, wCells: 2.0 },
{ row: 5, type: "cushion", speed: 68, count: 4, wCells: 1.8 },
];
function buildLane(def, ride, level) {
const w = def.wCells * CELL;
const span = W + PAD * 2;
const D = span / def.count;
const speed = def.speed * (1 + (level - 1) * 0.15);
const items = [];
for (let i = 0; i < def.count; i++) {
items.push({
x: -PAD + i * D, w,
rot: ((i * 53 + def.row * 29) % 9 - 4) * Math.PI / 180,
ci: i,
});
}
return { row: def.row, type: def.type, speed, ride, items, cycle: D * def.count };
}
const buildLanes = level =>
HAZARD_DEFS.map(d => buildLane(d, false, level))
.concat(RIDE_DEFS.map(d => buildLane(d, true, level)));
// ============================================================
// STATE
// ============================================================
let hi = 0;
try { hi = parseInt(localStorage.getItem("modernFroggerHi") || "0", 10) || 0; } catch (e) {}
let state = "title"; // title | playing | dying | levelup | gameover | paused
let score = 0, lives = 3, level = 1;
let timeMax = 45, timeLeft = timeMax;
let docks = [false, false, false, false, false];
let lanes = buildLanes(1);
let particles = [];
let maxRow = ROWS - 1;
let deathT = 0, deathCause = null, levelT = 0;
let bumpT = 0, bumpDx = 0, bumpDy = 0;
let queued = null;
let time = 0;
const DIRS = {
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 },
};
const frog = { col: 6, row: ROWS - 1, px: cellCX(6), py: cellCY(ROWS - 1),
fromX: 0, fromY: 0, toX: 0, toY: 0, hopT: 1, angle: 0, dir: "up" };
const HOP_DUR = 0.11;
function resetFrog() {
frog.col = 6; frog.row = ROWS - 1;
frog.px = cellCX(6); frog.py = cellCY(ROWS - 1);
frog.hopT = 1; frog.angle = 0; frog.dir = "up";
maxRow = ROWS - 1; queued = null;
}
function startGame() {
score = 0; lives = 3; level = 1;
timeMax = 45; timeLeft = timeMax;
docks = [false, false, false, false, false];
lanes = buildLanes(1);
particles = [];
resetFrog();
state = "playing";
setOverlay(null);
updateHUD();
}
function spawnBurst(x, y, colors, n = 14, power = 190) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const v = power * (0.4 + Math.random() * 0.8);
particles.push({
x, y,
vx: Math.cos(a) * v, vy: Math.sin(a) * v - 70,
life: 0.55 + Math.random() * 0.3, maxLife: 0.85,
r: 2 + Math.random() * 3,
color: colors[i % colors.length],
});
}
}
function die(cause) {
if (state !== "playing") return;
state = "dying"; deathT = 0; deathCause = cause; queued = null;
if (cause === "splash") { spawnBurst(frog.px, frog.py, ["#7ec8e3", "#bfe6f5", "#4b96b9"], 18); sfx.splash(); }
else if (cause === "time") { spawnBurst(frog.px, frog.py, ["#ffffff", "#ffd166"], 12); sfx.time(); }
else { spawnBurst(frog.px, frog.py, ["#ffd166", "#ff9f43", "#fff3c2"], 16); sfx.zap(); }
}
function resolveLanding() {
if (frog.row === 0) {
const di = DOCK_COLS.indexOf(frog.col);
if (di >= 0 && !docks[di]) {
docks[di] = true;
score += 100 + Math.round(timeLeft) * 2;
sfx.dock();
spawnBurst(frog.px, frog.py, ["#7ee787", "#ffd166", "#ffffff"], 18, 150);
if (docks.every(Boolean)) {
score += 500;
state = "levelup"; levelT = 0; sfx.level();
} else {
resetFrog(); timeLeft = timeMax;
}
updateHUD();
return;
}
}
if (frog.row < maxRow) { score += 10; maxRow = frog.row; updateHUD(); }
}
function bump(d) {
bumpT = 0.16; bumpDx = d.dc; bumpDy = d.dr;
sfx.bump();
}
function tryHop(dirName) {
if (state !== "playing") return;
if (frog.hopT < 1) { queued = dirName; return; }
const d = DIRS[dirName];
frog.dir = dirName; frog.angle = d.a;
const nc = frog.col + d.dc, nr = frog.row + d.dr;
if (nc < 0 || nc >= COLS || nr < 0 || nr >= ROWS) { bump(d); return; }
if (nr === 0) {
const di = DOCK_COLS.indexOf(nc);
if (di === -1 || docks[di]) { bump(d); return; }
}
frog.fromX = frog.px; frog.fromY = frog.py;
frog.toX = cellCX(nc); frog.toY = cellCY(nr);
frog.col = nc; frog.row = nr;
frog.hopT = 0;
sfx.hop();
}
// ============================================================
// UPDATE
// ============================================================
const pxToCol = px => clamp(Math.round((px - CELL / 2) / CELL), 0, COLS - 1);
const isRideRow = r => RIDE_ROWS.indexOf(r) !== -1;
const isHazardRow = r => r >= 7 && r <= 12;
function update(dt) {
time += dt;
if (state !== "paused") {
for (const lane of lanes) {
for (const it of lane.items) {
it.x += lane.speed * dt;
if (lane.speed > 0 && it.x > W + 2) it.x -= lane.cycle;
else if (lane.speed < 0 && it.x + it.w < -2) it.x += lane.cycle;
}
}
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt; p.y += p.vy * dt; p.vy += 340 * dt; p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
if (bumpT > 0) bumpT = Math.max(0, bumpT - dt);
if (state === "dying") {
deathT += dt;
if (deathT > 0.85) {
lives--;
updateHUD();
if (lives <= 0) {
state = "gameover";
if (score > hi) { hi = score; try { localStorage.setItem("modernFroggerHi", String(hi)); } catch (e) {} }
sfx.over();
updateHUD();
setOverlay("gameover");
} else {
resetFrog(); timeLeft = timeMax; state = "playing";
}
}
return;
}
if (state === "levelup") {
levelT += dt;
if (levelT > 1.5) {
level++;
timeMax = Math.max(30, 45 - (level - 1) * 2);
timeLeft = timeMax;
docks = [false, false, false, false, false];
lanes = buildLanes(level);
resetFrog();
state = "playing";
updateHUD();
}
return;
}
if (state !== "playing") return;
timeLeft -= dt;
if (timeLeft <= 0) { timeLeft = 0; die("time"); return; }
if (frog.hopT < 1) {
frog.hopT = Math.min(1, frog.hopT + dt / HOP_DUR);
const e = frog.hopT * (2 - frog.hopT);
frog.px = lerp(frog.fromX, frog.toX, e);
frog.py = lerp(frog.fromY, frog.toY, e);
if (frog.hopT >= 1) {
frog.px = frog.toX; frog.py = frog.toY;
resolveLanding();
if (state === "playing" && queued) { const q = queued; queued = null; tryHop(q); }
}
return;
}
if (isHazardRow(frog.row)) {
const fx0 = frog.px - 13, fx1 = frog.px + 13;
const fy0 = frog.py - 12, fy1 = frog.py + 12;
for (const lane of lanes) {
if (lane.ride || lane.row !== frog.row) continue;
for (const it of lane.items) {
const y0 = cellCY(lane.row) - 15, y1 = cellCY(lane.row) + 15;
if (fx1 > it.x + 5 && fx0 < it.x + it.w - 5 && fy1 > y0 && fy0 < y1) { die("zap"); return; }
}
}
}
if (isRideRow(frog.row)) {
let on = null, lane0 = null;
for (const lane of lanes) {
if (!lane.ride || lane.row !== frog.row) continue;
for (const it of lane.items) {
if (frog.px > it.x - 8 && frog.px < it.x + it.w + 8) { on = it; lane0 = lane; break; }
}
if (on) break;
}
if (!on) { die("splash"); return; }
frog.px += lane0.speed * dt;
frog.col = pxToCol(frog.px);
if (frog.px < -8 || frog.px > W + 8) { die("off"); return; }
}
}
// ============================================================
// INPUT
// ============================================================
const KEYMAP = {
ArrowUp: "up", KeyW: "up", ArrowDown: "down", KeyS: "down",
ArrowLeft: "left", KeyA: "left", ArrowRight: "right", KeyD: "right",
};
window.addEventListener("keydown", e => {
audio();
const dir = KEYMAP[e.code];
if (dir) e.preventDefault();
if (e.code === "KeyM") { muted = !muted; return; }
if (state === "title" || state === "gameover") {
if (dir || e.code === "Enter" || e.code === "Space") { e.preventDefault(); startGame(); }
return;
}
if (state === "paused") {
if (e.code === "KeyP" || e.code === "Enter") state = "playing";
return;
}
if (e.code === "KeyP" && state === "playing") { state = "paused"; return; }
if (dir && !e.repeat) tryHop(dir);
else if (dir && e.repeat) queued = dir;
});
function pointerHop(dir) {
audio();
if (state === "title" || state === "gameover") { startGame(); return; }
if (state === "paused") { state = "playing"; return; }
tryHop(dir);
}
document.querySelectorAll("#dpad button").forEach(btn => {
btn.addEventListener("pointerdown", e => { e.preventDefault(); pointerHop(btn.dataset.d); });
});
let touchStart = null;
cv.addEventListener("touchstart", e => {
e.preventDefault(); audio();
touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}, { passive: false });
cv.addEventListener("touchend", e => {
e.preventDefault();
if (!touchStart) return;
const dx = e.changedTouches[0].clientX - touchStart.x;
const dy = e.changedTouches[0].clientY - touchStart.y;
touchStart = null;
if (Math.abs(dx) < 20 && Math.abs(dy) < 20) {
if (state === "title" || state === "gameover") startGame();
return;
}
pointerHop(Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? "right" : "left") : (dy > 0 ? "down" : "up"));
}, { passive: false });
document.addEventListener("visibilitychange", () => {
if (document.hidden && state === "playing") state = "paused";
});
// ============================================================
// RENDER
// ============================================================
const MAG_HUES = ["#e63946", "#457b9d", "#f4a261", "#2a9d8f"];
function drawItem(lane, it) {
const cx = it.x + it.w / 2;
const cy = cellCY(lane.row);
g.fillStyle = "rgba(0,0,0,0.16)";
ellipse(g, cx, cy + 13, it.w * 0.40, 5.5);
switch (lane.type) {
case "roomba": drawRoomba(g, cx, cy, time + it.ci * 1.7); break;
case "tesla": drawTesla(g, cx, cy, it.w, 34, lane.speed < 0); break;
case "ferrari": drawFerrari(g, cx, cy, it.w, 32, lane.speed < 0); break;
case "iphone": drawIPhone(g, cx, cy, it.w, 34, time + it.ci); break;
case "laptop": drawLaptop(g, cx, cy, it.w, 36); break;
case "monitor": drawMonitorCart(g, cx, cy, it.w, 38); break;
case "magazine": drawMagazine(g, cx, cy, it.w, 34, it.rot, MAG_HUES[it.ci % MAG_HUES.length]); break;
case "ipad": drawIPad(g, cx, cy, it.w, 34, it.rot); break;
case "slippers": drawSlippers(g, cx, cy, it.w, 34, it.rot, it.ci); break;
case "drone": drawDrone(g, cx, cy + Math.sin(time * 3 + it.ci * 2.1) * 3, it.w, 36, time + it.ci); break;
case "cushion": drawCushion(g, cx, cy, it.w, 35, it.rot, it.ci); break;
}
}
function banner(title, sub, prog) {
const a = Math.sin(Math.PI * clamp(prog, 0, 1));
g.fillStyle = `rgba(13,11,8,${0.55 * a})`;
g.fillRect(0, 0, W, H);
g.save();
g.globalAlpha = a;
g.textAlign = "center"; g.textBaseline = "middle";
g.fillStyle = "#ffd166";
g.font = "800 34px 'Avenir Next Condensed', Futura, sans-serif";
g.fillText(title, W / 2, H / 2 - 14);
g.fillStyle = "#7ee787";
g.font = "bold 17px ui-monospace, Menlo, monospace";
g.fillText(sub, W / 2, H / 2 + 22);
g.restore();
}
function render() {
drawFloor(g);
drawRugRow(g, 6);
drawRugRow(g, ROWS - 1);
drawWetZone(g, time);
drawWallRow(g);
for (const lane of lanes)
for (const it of lane.items)
if (it.x + it.w > -40 && it.x < W + 40) drawItem(lane, it);
// frog
const showFrog = state !== "dying" || deathT < 0.12;
if (showFrog) {
let ox = 0, oy = 0;
if (bumpT > 0) {
const off = Math.sin(Math.PI * (1 - bumpT / 0.16)) * 8;
ox = bumpDx * off; oy = bumpDy * off;
}
const squash = frog.hopT < 1 ? 1 + 0.28 * Math.sin(Math.PI * frog.hopT) : 1;
g.fillStyle = "rgba(0,0,0,0.20)";
ellipse(g, frog.px + ox, frog.py + oy + 14, 13, 4.5);
drawFrog(g, frog.px + ox, frog.py + oy, frog.angle, squash, 1);
}
for (const p of particles) {
g.globalAlpha = clamp(p.life / p.maxLife, 0, 1);
g.fillStyle = p.color;
ellipse(g, p.x, p.y, p.r, p.r);
}
g.globalAlpha = 1;
if (state === "levelup") banner(`LEVEL ${level} CLEARED!`, "+500 BONUS", levelT / 1.5);
if (state === "paused") banner("PAUSED", "press P to resume", 0.5);
// vignette
const vg = g.createRadialGradient(W / 2, H / 2, H * 0.38, W / 2, H / 2, H * 0.75);
vg.addColorStop(0, "rgba(20,10,0,0)"); vg.addColorStop(1, "rgba(20,10,0,0.30)");
g.fillStyle = vg; g.fillRect(0, 0, W, H);
}
// ============================================================
// HUD & OVERLAY
// ============================================================
const el = id => document.getElementById(id);
function updateHUD() {
el("hudScore").textContent = score;
el("hudHi").textContent = hi;
el("hudLevel").textContent = level;
el("hudFrogs").textContent = lives > 0 ? "🐸".repeat(lives) : "—";
}
function setOverlay(kind) {
const ov = el("overlay");
if (!kind) { ov.classList.add("hidden"); return; }
ov.classList.remove("hidden");
if (kind === "title") {
ov.innerHTML = `
<h1>MODERN FROGGER</h1>
<p>Your smart home is out to get you. Hop across the floor — dodge
<span class="key">Roombas</span>, <span class="key">mini Teslas</span>,
<span class="key">Ferraris</span>, <span class="key">iPhones</span>,
<span class="key">laptops</span> &amp; <span class="key">monitor carts</span> —
then ride drifting <span class="key">magazines</span>, <span class="key">iPads</span>,
<span class="key">slippers</span>, <span class="key">drones</span> &amp;
<span class="key">cushions</span> across the freshly-mopped wet zone.
Reach all five wall <span class="key">outlets</span> to clear the level.</p>
<p><span class="key">←→↑↓ / WASD</span> hop · <span class="key">P</span> pause · <span class="key">M</span> mute</p>
<p class="prompt">PRESS ENTER OR TAP TO START</p>`;
} else {
ov.innerHTML = `
<h1>GAME OVER</h1>
<p>The gadgets got you. Final score: <span class="key">${score}</span>
&nbsp;·&nbsp; best: <span class="key">${hi}</span></p>
<p class="prompt">PRESS ENTER OR TAP TO RETRY</p>`;
}
}
// ============================================================
// MAIN LOOP
// ============================================================
let last = performance.now();
function frame(now) {
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
update(dt);
render();
el("timeBar").style.width = (clamp(timeLeft / timeMax, 0, 1) * 100) + "%";
requestAnimationFrame(frame);
}
setOverlay("title");
updateHUD();
requestAnimationFrame(frame);
</script>
</body>
</html>
@HP2000C

HP2000C commented Aug 10, 2026

Copy link
Copy Markdown

Good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment