{"connections":[{"sourceLocalID":0,"sourcePortID":"outlet_dismissed","targetLocalID":1,"targetPortID":"inlet_input"},{"sourceLocalID":2,"sourcePortID":"outlet_output","targetLocalID":0,"targetPortID":"inlet_input"}],"hideGrid":false,"name":"Rush Hour Hopper","nodes":[{"config":{"chrome":"none","closeIsDismiss":true,"height":520,"html":"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1\">\n<title>Rush Hour Hopper</title>\n<style>\n:root{color-scheme:dark;font-family:ui-rounded,system-ui,-apple-system,sans-serif;color:#e9fce7;background:#081512}*{box-sizing:border-box}body{margin:0;overflow:hidden;user-select:none;overscroll-behavior:none}button{font:inherit;cursor:pointer;touch-action:none;color:inherit;border:1px solid #355347;background:#163227;border-radius:12px;padding:10px 16px}button:hover{background:#254b37}button:focus-visible{outline:3px solid #c8fc71;outline-offset:3px}#app{height:100vh;height:100dvh;display:flex;flex-direction:column;gap:10px;padding:16px 24px}header,footer{width:100%;max-width:1000px;margin:auto;display:flex;align-items:center;justify-content:space-between;gap:16px;flex-shrink:0}.brand{font-weight:900;letter-spacing:.14em;font-size:22px;color:#c8fc71}.tag{font-size:11px;letter-spacing:.18em;color:#81a893;margin-top:4px}.stats{display:flex;gap:24px}.stat{font-size:10px;letter-spacing:.15em;color:#91b3a3}.stat b{display:block;font-size:23px;letter-spacing:0;color:#f2ffe8;font-variant-numeric:tabular-nums}.buttons{display:flex;gap:8px}#field{flex:1;min-height:0;position:relative;display:flex;align-items:center;justify-content:center}canvas{display:block;border-radius:16px;touch-action:none}#veil{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#081512b8;border-radius:16px}#veil[hidden]{display:none}.card{max-width:440px;margin:20px;padding:30px;text-align:center;background:#10271f;border:1px solid #36533c;border-radius:22px;box-shadow:0 18px 70px #0006}.eyebrow{color:#c8fc71;font-size:11px;letter-spacing:.22em;font-weight:800}h1{font-size:42px;margin:10px 0;letter-spacing:-.04em}p{color:#b5cfc0;line-height:1.6;margin:12px 0 22px}#start{background:#c8fc71;color:#17351d;border:0;font-weight:800;padding:13px 28px}.hint{font-size:12px;color:#91b3a3;line-height:1.7}.pad{display:grid;grid-template-columns:repeat(3,42px);gap:4px}.pad button{padding:5px;height:32px;border-radius:8px}.pad [data-dir=\"up\"]{grid-column:2}.pad [data-dir=\"left\"]{grid-column:1}.pad [data-dir=\"down\"]{grid-column:2}.pad [data-dir=\"right\"]{grid-column:3}@media(max-width:650px){#app{padding:10px;gap:6px}.brand{font-size:16px}.tag{display:none}.stats{gap:10px}.stat{font-size:8px}.stat b{font-size:18px}header{gap:8px}.buttons button{padding:8px;font-size:12px}.hint{font-size:10px}h1{font-size:32px}}\n</style>\n</head>\n<body>\n<div id=\"app\">\n<header><div><div class=\"brand\">RUSH HOUR HOPPER</div><div class=\"tag\">THE LITTLE COMMUTE</div></div><div class=\"stats\" aria-live=\"polite\"><div class=\"stat\">SCORE<b id=\"score\">0</b></div><div class=\"stat\">LIVES<b id=\"lives\">3</b></div><div class=\"stat\">LEVEL<b id=\"level\">1</b></div><div class=\"stat\">BEST<b id=\"best\">0</b></div></div><div class=\"buttons\"><button id=\"pause\" aria-label=\"Pause game\">Pause</button><button id=\"exit\">Exit</button></div></header>\n<div id=\"field\"><canvas id=\"game\" width=\"624\" height=\"576\" role=\"img\" aria-label=\"Rush Hour Hopper board. Use arrow keys or WASD to reach five home pads while avoiding traffic and riding logs.\"></canvas><div id=\"veil\"><div class=\"card\"><div class=\"eyebrow\">ONE SMALL FROG. BIG AMBITIONS.</div><h1 id=\"heading\">Hop to it.</h1><p id=\"description\">Dodge traffic, ride the logs, and land in all five home pads. Water is only safe when you're on a log.</p><button id=\"start\">Let's hop</button><div class=\"hint\" style=\"margin-top:16px\">Arrows / WASD to move · P to pause · R to restart</div></div></div></div>\n<footer><div class=\"hint\">Fill all five pads to advance.<br><span id=\"status\" role=\"status\">Ready when you are.</span></div><div class=\"pad\" aria-label=\"Movement controls\"><button data-dir=\"up\" aria-label=\"Hop up\">↑</button><button data-dir=\"left\" aria-label=\"Hop left\">←</button><button data-dir=\"down\" aria-label=\"Hop down\">↓</button><button data-dir=\"right\" aria-label=\"Hop right\">→</button></div><div class=\"hint\">P · Pause<br>Esc · Exit</div></footer>\n</div>\n<script>\n'use strict';\n(() => {\nconst W=624,H=576,C=48,DT=1/120;\nconst canvas=document.getElementById('game'),ctx=canvas.getContext('2d',{alpha:false});\nconst field=document.getElementById('field'),veil=document.getElementById('veil');\nconst heading=document.getElementById('heading'),description=document.getElementById('description');\nconst startButton=document.getElementById('start'),pauseButton=document.getElementById('pause');\nconst scoreEl=document.getElementById('score'),livesEl=document.getElementById('lives'),levelEl=document.getElementById('level'),bestEl=document.getElementById('best'),statusEl=document.getElementById('status');\nconst homeX=[72,168,312,456,552],homes=[false,false,false,false,false];\nconst lanes=[\n {row:2,water:true,speed:61,len:154,count:3,color:'#aa7845'},\n {row:3,water:true,speed:-78,len:125,count:3,color:'#bd8b50'},\n {row:4,water:true,speed:94,len:166,count:3,color:'#aa7845'},\n {row:5,water:true,speed:-66,len:133,count:3,color:'#bd8b50'},\n {row:7,water:false,speed:107,len:67,count:3,color:'#f5c654'},\n {row:8,water:false,speed:-145,len:49,count:3,color:'#ee7984'},\n {row:9,water:false,speed:86,len:98,count:3,color:'#9a9bed'},\n {row:10,water:false,speed:-122,len:57,count:3,color:'#6fd1c7'}\n];\nfor(let i=0;i<lanes.length;i++){const l=lanes[i];l.cars=[];for(let j=0;j<l.count;j++)l.cars.push({x:0,prev:0});}\nlet mode='ready',score=0,lives=3,level=1,best=0,scale=1,dpr=1;\nlet x=312,row=11,prevX=x,hopX=x,hopY=552,hopAge=1,face='up';\nlet last=0,acc=0,dirty=true,held='',repeat=0,flash=0,farthest=11;\ntry{best=Number(localStorage.getItem('watchflows.rushhourhopper.best'))||0;}catch(e){}\nbestEl.textContent=best;\nconst scenery=document.createElement('canvas');scenery.width=W*2;scenery.height=H*2;\nconst bg=scenery.getContext('2d');bg.scale(2,2);\nbg.fillStyle='#203d2a';bg.fillRect(0,0,W,H);\nbg.fillStyle='#102f39';bg.fillRect(0,2*C,W,4*C);\nfor(let r=2;r<=5;r++){bg.fillStyle=r%2?'#153943':'#12343e';bg.fillRect(0,r*C,W,C);bg.fillStyle='#23505a';for(let a=15;a<W;a+=67)bg.fillRect(a+(r%2)*17,r*C+22,22,2);}\nbg.fillStyle='#222b35';bg.fillRect(0,7*C,W,4*C);\nfor(let r=8;r<=10;r++){bg.fillStyle='#51575d';for(let a=10;a<W;a+=40)bg.fillRect(a,r*C,20,2);}\nfor(const r of [0,6,11]){bg.fillStyle=r===6?'#304e31':'#28462d';bg.fillRect(0,r*C,W,C);bg.fillStyle='#456238';for(let a=8;a<W;a+=24){bg.fillRect(a,r*C+12,3,6);bg.fillRect(a+9,r*C+30,3,5);}}\nbg.fillStyle='#0e2928';bg.fillRect(0,C,W,C);\nfor(const hx of homeX){bg.fillStyle='#92b653';bg.beginPath();bg.ellipse(hx,72,21,19,0,0,Math.PI*2);bg.fill();bg.fillStyle='#bed970';bg.beginPath();bg.ellipse(hx-3,69,13,11,0,0,Math.PI*2);bg.fill();}\nbg.fillStyle='#c8d993';bg.font='bold 11px system-ui';bg.textAlign='center';bg.fillText('H O M E   S W E E T   H O M E',312,29);\nconst frog=document.createElement('canvas');frog.width=80;frog.height=80;\nconst fc=frog.getContext('2d');fc.scale(2,2);fc.translate(20,20);\nfc.fillStyle='#77bc46';for(const sx of [-1,1])for(const sy of [-1,1]){fc.beginPath();fc.ellipse(sx*12,sy*10,6,5,sx*sy*.5,0,Math.PI*2);fc.fill();}\nfc.fillStyle='#b5ed64';fc.beginPath();fc.ellipse(0,1,11,14,0,0,Math.PI*2);fc.fill();\nfor(const sx of [-1,1]){fc.fillStyle='#d5ff93';fc.beginPath();fc.arc(sx*7,-9,5,0,Math.PI*2);fc.fill();fc.fillStyle='#142e21';fc.beginPath();fc.arc(sx*7,-11,2.3,0,Math.PI*2);fc.fill();}\nfc.strokeStyle='#548e35';fc.lineWidth=1.5;fc.beginPath();fc.arc(0,-4,5,.25,Math.PI-.25);fc.stroke();\nfunction sync(){scoreEl.textContent=score;livesEl.textContent=lives;levelEl.textContent=level;if(score>best){best=score;bestEl.textContent=best;}dirty=true;}\nfunction saveBest(){try{localStorage.setItem('watchflows.rushhourhopper.best',String(best));}catch(e){}}\nfunction resetLanes(){for(let i=0;i<lanes.length;i++){const l=lanes[i],span=W+l.len;for(let j=0;j<l.count;j++){const o=l.cars[j];o.x=((j*span/l.count+i*37)%span)-l.len;o.prev=o.x;}}}\nfunction resetFrog(){x=312;row=11;prevX=x;hopX=x;hopY=552;hopAge=1;farthest=11;face='up';held='';repeat=0;}\nfunction show(title,text,label){heading.textContent=title;description.textContent=text;startButton.textContent=label;veil.hidden=false;dirty=true;}\nfunction start(){score=0;lives=3;level=1;for(let i=0;i<5;i++)homes[i]=false;resetLanes();resetFrog();flash=0;mode='playing';veil.hidden=true;pauseButton.textContent='Pause';statusEl.textContent='Head upstream. The logs are your ride.';last=0;acc=0;sync();}\nfunction togglePause(){if(mode==='playing'){mode='paused';held='';acc=0;pauseButton.textContent='Resume';show('Take a breather.','Your frog will wait right here.','Keep hopping');saveBest();}else if(mode==='paused'){mode='playing';veil.hidden=true;pauseButton.textContent='Pause';last=0;acc=0;dirty=true;}}\nfunction die(reason){lives--;flash=.23;resetFrog();statusEl.textContent=reason;sync();if(lives<=0){mode='over';saveBest();show('One more hop?','Final score: '+score+'. Your best: '+best+'.','Play again');}}\nfunction home(){let target=-1;for(let i=0;i<5;i++)if(Math.abs(x-homeX[i])<=19){target=i;break;}if(target<0){die('Aim for the center of a home pad.');return;}if(homes[target]){die('That pad already has a frog. Try an empty one.');return;}homes[target]=true;score+=100;let full=true;for(let i=0;i<5;i++)if(!homes[i])full=false;if(full){score+=500;level++;for(let i=0;i<5;i++)homes[i]=false;resetLanes();statusEl.textContent='All home! Level '+level+'. A little faster now.';}else statusEl.textContent='Safe at home. Find the next empty pad.';resetFrog();sync();saveBest();}\nfunction move(dir){if(mode!=='playing')return;let nx=x,nr=row;if(dir==='left')nx-=C;if(dir==='right')nx+=C;if(dir==='up')nr--;if(dir==='down')nr++;if(nx<17||nx>W-17||nr<1||nr>11)return;hopX=x;hopY=row*C+C/2;hopAge=0;face=dir;x=nx;row=nr;prevX=x;if(row<farthest){score+=(farthest-row)*10;farthest=row;sync();}if(row===1)home();}\nfunction tick(){const mult=Math.min(2.4,1+(level-1)*.12);prevX=x;hopAge+=DT;flash=Math.max(0,flash-DT);\nfor(let i=0;i<lanes.length;i++){const l=lanes[i],dx=l.speed*mult*DT,span=W+l.len;for(let j=0;j<l.cars.length;j++){const o=l.cars[j];o.prev=o.x;o.x+=dx;if(o.x>W){o.x-=span;o.prev-=span;}else if(o.x<-l.len){o.x+=span;o.prev+=span;}}}\nif(held){repeat-=DT;if(repeat<=0){move(held);repeat=.145;}}if(mode!=='playing')return;\nfor(let i=0;i<lanes.length;i++){const l=lanes[i];if(l.row!==row)continue;let supported=false;for(let j=0;j<l.cars.length;j++){const o=l.cars[j];if(l.water){if(x>=o.x+9&&x<=o.x+l.len-9){supported=true;break;}}else if(x+12>o.x+4&&x-12<o.x+l.len-4){die('Traffic got you. Watch for a gap.');return;}}if(l.water){if(!supported){die('Splash! Land on a log to cross the river.');return;}const dx=l.speed*mult*DT;x+=dx;hopX+=dx;if(x<14||x>W-14){die('The log carried you out of bounds.');return;}}break;}}\nfunction drawFrog(px,py,direction,size){ctx.save();ctx.translate(px,py);let angle=0;if(direction==='left')angle=-Math.PI/2;if(direction==='right')angle=Math.PI/2;if(direction==='down')angle=Math.PI;ctx.rotate(angle);ctx.drawImage(frog,-size/2,-size/2,size,size);ctx.restore();}\nfunction render(alpha){ctx.setTransform(dpr*scale,0,0,dpr*scale,0,0);ctx.drawImage(scenery,0,0,W,H);\nfor(let i=0;i<lanes.length;i++){const l=lanes[i],y=l.row*C;for(let j=0;j<l.cars.length;j++){const o=l.cars[j],px=o.prev+(o.x-o.prev)*alpha;ctx.fillStyle=l.water?'#0b2429':'#171d25';ctx.fillRect(px+3,y+12,l.len,30);ctx.fillStyle=l.color;ctx.fillRect(px,y+9,l.len,29);if(l.water){ctx.fillStyle='#d0a371';ctx.fillRect(px+5,y+12,l.len-10,3);ctx.fillStyle='#78522f';ctx.fillRect(px+8,y+31,l.len-16,2);ctx.fillRect(px+15,y+18,3,10);ctx.fillRect(px+l.len-19,y+18,3,10);}else{ctx.fillStyle='#102f40';const front=l.speed>0?px+l.len-22:px+9;ctx.fillRect(front,y+13,13,20);ctx.fillStyle='#ffffff36';ctx.fillRect(px+24,y+12,Math.max(8,l.len-49),3);ctx.fillStyle='#f9f2b1';const nose=l.speed>0?px+l.len-3:px;ctx.fillRect(nose,y+12,3,6);ctx.fillRect(nose,y+29,3,6);ctx.fillStyle='#111820';ctx.fillRect(px+10,y+6,12,4);ctx.fillRect(px+l.len-22,y+37,12,4);}}}\nfor(let i=0;i<5;i++)if(homes[i])drawFrog(homeX[i],72,'up',32);\nlet px=prevX+(x-prevX)*alpha,py=row*C+24;const t=Math.min(1,hopAge/.085);if(t<1){const eased=1-(1-t)*(1-t);px=hopX+(x-hopX)*eased;py=hopY+(py-hopY)*eased;}drawFrog(px,py,face,38+Math.sin(t*Math.PI)*3);\nif(flash>0){ctx.fillStyle='rgba(255,105,100,'+(flash*.75)+')';ctx.fillRect(0,0,W,H);}dirty=false;}\nfunction resize(){const rect=field.getBoundingClientRect();if(rect.width<=0||rect.height<=0)return;scale=Math.min(rect.width/W,rect.height/H);dpr=Math.min(window.devicePixelRatio||1,2);canvas.style.width=W*scale+'px';canvas.style.height=H*scale+'px';canvas.width=Math.max(1,Math.round(W*scale*dpr));canvas.height=Math.max(1,Math.round(H*scale*dpr));ctx.imageSmoothingEnabled=true;dirty=true;}\nfunction frame(now){if(mode==='playing'){if(!last)last=now;acc=Math.min(acc+Math.min((now-last)/1000,.066),DT*8);last=now;let steps=0;while(acc>=DT&&steps<8&&mode==='playing'){tick();acc-=DT;steps++;}render(Math.max(0,Math.min(1,acc/DT)));}else{last=0;acc=0;if(dirty)render(1);}requestAnimationFrame(frame);}\nconst keyDirections={ArrowUp:'up',ArrowDown:'down',ArrowLeft:'left',ArrowRight:'right',w:'up',s:'down',a:'left',d:'right'};\nwindow.addEventListener('keydown',e=>{const key=e.key.length===1?e.key.toLowerCase():e.key;const dir=keyDirections[key];if(dir){e.preventDefault();if(!e.repeat&&mode==='playing'){held=dir;repeat=.18;move(dir);}return;}if(e.key==='Escape'){e.preventDefault();exit();return;}if(e.target instanceof HTMLButtonElement&&(e.key==='Enter'||e.key===' '))return;if(e.repeat)return;if(key==='p'){e.preventDefault();togglePause();}else if(key==='r'){e.preventDefault();start();}else if(e.key==='Enter'||e.key===' '){e.preventDefault();if(mode==='ready'||mode==='over')start();else togglePause();}});\nwindow.addEventListener('keyup',e=>{const key=e.key.length===1?e.key.toLowerCase():e.key;if(keyDirections[key]===held)held='';});\nfor(const button of document.querySelectorAll('[data-dir]')){button.addEventListener('pointerdown',e=>{e.preventDefault();if(mode!=='playing')return;button.setPointerCapture(e.pointerId);held=button.dataset.dir;repeat=.18;move(held);});for(const event of ['pointerup','pointercancel','lostpointercapture'])button.addEventListener(event,()=>{held='';});}\nstartButton.addEventListener('click',()=>{if(mode==='paused')togglePause();else start();startButton.blur();});pauseButton.addEventListener('click',()=>{togglePause();pauseButton.blur();});\nfunction exit(){held='';if(mode==='playing')togglePause();saveBest();if(window.watchflows&&typeof window.watchflows.dismiss==='function')window.watchflows.dismiss();}\ndocument.getElementById('exit').addEventListener('click',exit);\ndocument.addEventListener('visibilitychange',()=>{if(document.hidden&&mode==='playing')togglePause();});window.addEventListener('blur',()=>{held='';if(mode==='playing')togglePause();});\nnew ResizeObserver(resize).observe(field);window.addEventListener('resize',resize);\nresetLanes();resize();requestAnimationFrame(frame);\n})();\n</script>\n</body>\n</html>","timeoutMinutes":0,"title":"Rush Hour Hopper","width":720,"windowMode":"fullscreen"},"customName":"Rush Hour Hopper","isDisabled":false,"localID":0,"typeID":"action.html_view","x":450,"y":260},{"config":{"body":"Thanks for playing.","forceDisplay":true,"title":"Rush Hour Hopper closed"},"customName":"Thanks for Playing","isDisabled":false,"localID":1,"typeID":"action.notification","x":800,"y":320},{"config":{"inputType":"text","textInput":"Play"},"customName":"Launch Game","isDisabled":false,"localID":2,"typeID":"trigger.input","x":100,"y":200},{"config":{"_height":450,"_width":460,"text":"**Rush Hour Hopper is a full-screen road and river crossing game that starts when you run this flow.**\n\n**What it does**\n- Hop through traffic and ride the logs to fill all five home pads.\n- Each level is a little faster. Traffic and water cost a life.\n- When you close the game, a notification thanks you for playing.\n\n**Set it up**\n- Nothing to connect. Run the flow to play.\n- Arrow keys or W, A, S and D move. You can also use the on-screen pad. P pauses and R restarts.\n- Press Escape or click Exit to close the game.\n\n**Make it yours**\n- Delete **Thanks for Playing** if you do not want the notification.\n- Set Window to Window on **Rush Hour Hopper** to play in a normal window instead of full screen."},"isDisabled":false,"localID":3,"typeID":"note","x":100,"y":-290}],"version":1}