XTaylorSpenceX

Traffic Blitz: Hustle Mode

Sep 18th, 2025
55
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 22.60 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="utf-8" />
  5.   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  6.   <title>Traffic Blitz [Hustle Mode]</title>
  7.   <style>
  8.     :root{
  9.       --bg:#0b0f14;--fg:#e8f1ff;--muted:#93a0b8;--acc:#4ade80;--acc2:#60a5fa;--warn:#facc15;--danger:#fb7185;--panel:#0f1722;--glass:rgba(255,255,255,.06);
  10.     }
  11.     html,body{height:100%;}
  12.     *{box-sizing:border-box}
  13.     body{
  14.       margin:0; background:radial-gradient(1200px 800px at 70% -10%,#122036,transparent),
  15.       radial-gradient(1200px 800px at -20% 120%,#191f2c,transparent),var(--bg);
  16.       color:var(--fg);
  17.       font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
  18.       overflow:hidden;
  19.     }
  20.     #wrap{position:relative; width:100vw; height:100vh;}
  21.     canvas{position:absolute; inset:0; width:100%; height:100%; image-rendering: crisp-edges; image-rendering: pixelated;}
  22.     .ui{position:absolute; inset:0; pointer-events:none;}
  23.     .topbar{display:flex; gap:.75rem; align-items:center; justify-content:space-between; padding:10px 14px;}
  24.     .pill{background:var(--glass); border:1px solid rgba(255,255,255,.08); backdrop-filter: blur(6px); box-shadow:0 8px 24px rgba(0,0,0,.25) inset, 0 2px 6px rgba(0,0,0,.2);
  25.       border-radius:999px; padding:6px 12px; font-weight:700; letter-spacing:.3px;}
  26.     .btn{pointer-events:auto; cursor:pointer; user-select:none; transition:transform .08s ease, filter .15s ease;}
  27.     .btn:active{transform:translateY(1px) scale(.99)}
  28.     .corner{position:absolute; bottom:14px; left:50%; transform:translateX(-50%); display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:center}
  29.     .cta{background:linear-gradient(180deg,var(--acc2),#2563eb); border:0; color:white; padding:12px 18px; border-radius:14px; font-weight:800; letter-spacing:.4px;
  30.       box-shadow:0 10px 30px rgba(37,99,235,.35);}
  31.     .ghost{background:var(--glass); color:var(--fg); border:1px solid rgba(255,255,255,.08);}
  32.  
  33.     .center{position:absolute; inset:0; display:grid; place-items:center;}
  34.     .panel{pointer-events:auto; width:min(720px,92vw); background:linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.03)); border:1px solid rgba(255,255,255,.08);
  35.       border-radius:18px; padding:20px; box-shadow:0 24px 60px rgba(0,0,0,.45);}
  36.     .title{font-size:clamp(28px,4vw,44px); font-weight:900; letter-spacing:.5px; margin:4px 0 8px; background:linear-gradient(90deg,#93c5fd,#a7f3d0,#facc15);
  37.       -webkit-background-clip:text; background-clip:text; color:transparent;}
  38.     .subtitle{opacity:.8; margin-bottom:14px}
  39.     .row{display:flex; gap:10px; align-items:center; flex-wrap:wrap}
  40.     .kbd{font-weight:800; border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.06); padding:3px 8px; border-radius:8px;}
  41.     .list{display:grid; grid-template-columns: 1fr 1fr; gap:8px 16px;}
  42.     @media (max-width:640px){.list{grid-template-columns: 1fr}}
  43.     .fine{font-size:12px; opacity:.8}
  44.     .meter{height:10px; background:rgba(255,255,255,.08); border-radius:999px; overflow:hidden; border:1px solid rgba(255,255,255,.1)}
  45.     .meter>span{display:block; height:100%; background:linear-gradient(90deg,#22d3ee,#60a5fa,#34d399); width:0%}
  46.   </style>
  47. </head>
  48. <body>
  49.   <div id="wrap">
  50.     <canvas id="game"></canvas>
  51.     <div class="ui">
  52.       <div class="topbar">
  53.         <div class="pill" id="scorePill">Score 0</div>
  54.         <div class="pill">x<span id="mult">1.0</span> Mult</div>
  55.         <div class="pill">Level <span id="lvl">1</span></div>
  56.         <div class="pill">Best <span id="best">0</span></div>
  57.         <div class="pill" id="hustlePill" title="Hustle Meter (Space / Hold)">
  58.           Hustle
  59.           <div class="meter" style="width:120px; margin-top:6px"><span id="hustleBar"></span></div>
  60.         </div>
  61.         <button class="pill btn ghost" id="pauseBtn" aria-label="Pause (P)">⏸︎</button>
  62.       </div>
  63.  
  64.       <div class="corner">
  65.         <button class="btn cta" id="playBtn">▶ Play / Toggle Lights (Click or Tap)</button>
  66.         <button class="btn ghost" id="howBtn">How to Play</button>
  67.       </div>
  68.  
  69.       <div class="center" id="menu">
  70.         <div class="panel">
  71.           <div class="title">Traffic Blitz <span class="fine">[Hustle Mode]</span></div>
  72.           <div class="subtitle">One-button chaos: run the intersection, keep the flow, chase the combo. No accidents allowed.</div>
  73.           <div class="list" style="margin:12px 0 16px">
  74.             <div><b>Click / Tap</b> — toggle lights (N/S ↔ E/W)</div>
  75.             <div><span class="kbd">Space</span> — hold to Hustle (faster spawn, bigger <b>multiplier</b>)</div>
  76.             <div><span class="kbd">P</span> — pause / resume</div>
  77.             <div><span class="kbd">M</span> — mute</div>
  78.           </div>
  79.           <div class="row" style="margin:8px 0 16px">
  80.             <div class="pill">Objective: <b>Survive</b> & score big with clean streaks.</div>
  81.            <div class="pill">Combo builds when no one waits too long.</div>
  82.          </div>
  83.          <div class="row" style="justify-content:space-between; align-items:center">
  84.             <button class="btn cta" id="startBtn">Start Game</button>
  85.             <div class="fine">Autosaves Best Score</div>
  86.           </div>
  87.         </div>
  88.       </div>
  89.  
  90.       <div class="center" id="help" style="display:none">
  91.         <div class="panel">
  92.           <div class="title">How to Play</div>
  93.           <p>Cars stream in from four directions toward a single intersection. You control the lights: either <b>N/S is green</b> and <b>E/W is red</b>, or vice versa. Tap/click anywhere to toggle.
  94.           Let cars pass to score. If cars stack up and wait too long, you’ll lose combo juice. If they collide inside the box—it’s game over.</p>
  95.           <ul>
  96.             <li><b>Hustle Mode:</b> Hold <span class="kbd">Space</span> to temporarily spike spawn rate & speed. You’ll earn a bigger multiplier while hustling, but it drains the meter.</li>
  97.            <li><b>Level Ups:</b> Every 100 points increases pace, introduces vans & speedy cars, and shortens patience a bit.</li>
  98.            <li><b>Pro Tip:</b> Flip lights in rhythm right before cars arrive at the line. Keep both queues flowing.</li>
  99.          </ul>
  100.          <div class="row" style="justify-content:space-between; align-items:center">
  101.             <button class="btn ghost" id="backBtn">Back</button>
  102.             <button class="btn cta" id="playFromHelp">Play Now</button>
  103.           </div>
  104.         </div>
  105.       </div>
  106.  
  107.       <div class="center" id="gameover" style="display:none">
  108.         <div class="panel">
  109.           <div class="title">Crash! 💥</div>
  110.           <div style="margin:6px 0 10px">Final Score: <b id="finalScore">0</b> &middot; Best: <b id="finalBest">0</b></div>
  111.           <div style="margin:6px 0 12px">Max Multiplier: <b id="finalMult">1.0x</b> &middot; Level Reached: <b id="finalLvl">1</b></div>
  112.           <div class="row" style="justify-content:space-between; align-items:center">
  113.             <button class="btn ghost" id="retryBtn">Retry</button>
  114.             <button class="btn cta" id="menuBtn">Main Menu</button>
  115.           </div>
  116.         </div>
  117.       </div>
  118.  
  119.     </div>
  120.   </div>
  121.  
  122.   <script>
  123.     /* ==========================
  124.        Traffic Blitz [Hustle Mode]
  125.        by Pyroflame Games (single-file build)
  126.        FIX: ignore collisions for cars traveling along the same axis (vertical vs horizontal)
  127.        ========================== */
  128.     const cvs = document.getElementById('game');
  129.     const ctx = cvs.getContext('2d');
  130.  
  131.     // UI refs
  132.     const $ = (id)=>document.getElementById(id);
  133.     const scorePill=$('scorePill'), bestEl=$('best'), multEl=$('mult'), lvlEl=$('lvl');
  134.     const hustleBar=$('hustleBar');
  135.     const pauseBtn=$('pauseBtn'), playBtn=$('playBtn');
  136.     const menu=$('menu'), help=$('help'), gameover=$('gameover');
  137.     const startBtn=$('startBtn'), retryBtn=$('retryBtn'), menuBtn=$('menuBtn');
  138.     const howBtn=$('howBtn'), backBtn=$('backBtn'), playFromHelp=$('playFromHelp');
  139.     const finalScore=$('finalScore'), finalBest=$('finalBest'), finalMult=$('finalMult'), finalLvl=$('finalLvl');
  140.  
  141.     // State
  142.     const STATE={MENU:0, PLAY:1, PAUSED:2, OVER:3};
  143.     let state=STATE.MENU;
  144.  
  145.     // DPR scaling
  146.     const S = { w:0, h:0, dpr:1 };
  147.     function resize(){
  148.       S.dpr = Math.min(2, window.devicePixelRatio||1);
  149.       S.w = cvs.clientWidth; S.h = cvs.clientHeight;
  150.       cvs.width = (S.w*S.dpr)|0; cvs.height=(S.h*S.dpr)|0;
  151.       ctx.setTransform(S.dpr,0,0,S.dpr,0,0);
  152.     }
  153.     addEventListener('resize', resize, {passive:true}); resize();
  154.  
  155.     // RNG
  156.     const rand = (a,b)=>a+Math.random()*(b-a);
  157.     const rint = (a,b)=> (Math.random()*(b-a+1)|0)+a;
  158.  
  159.     // Audio (lightweight, oscillator-based)
  160.     const Audio = (function(){
  161.       const ac = new (window.AudioContext||window.webkitAudioContext||function(){return {resume:()=>{}, currentTime:0, createOscillator:()=>({}), createGain:()=>({})}})();
  162.       let muted = false; function toggle(){ muted=!muted; return muted; }
  163.       function beep(freq=880, dur=0.06, type='sine', vol=0.04){
  164.         if(muted || !ac.createOscillator) return;
  165.         const o=ac.createOscillator(), g=ac.createGain();
  166.         o.type=type; o.frequency.value=freq; g.gain.value=vol; o.connect(g); g.connect(ac.destination);
  167.         o.start(); g.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime+dur); o.stop(ac.currentTime+dur);
  168.       }
  169.       function honk(){ beep(220, .09, 'square', .05); }
  170.       function crash(){ beep(120, .25, 'sawtooth', .12); setTimeout(()=>beep(60,.35,'triangle',.1),40); }
  171.       return {beep,honk,crash,toggle, get muted(){return muted;}, context:ac};
  172.     })();
  173.  
  174.     // Game objects
  175.     let score=0, best=Number(localStorage.getItem('tb_best')||0), level=1, multiplier=1, maxMult=1;
  176.     bestEl.textContent=best;
  177.  
  178.     const world = {
  179.       lightNS:true, // true => NS green, EW red
  180.       toggle(){ this.lightNS=!this.lightNS; Audio.beep(this.lightNS?880:660, .05, 'triangle', .045); },
  181.       lanes:[ // 0=N,1=E,2=S,3=W
  182.         {dir:0, x:0, y:-1}, {dir:1, x:1, y:0}, {dir:2, x:0, y:1}, {dir:3, x:-1, y:0}
  183.       ],
  184.       spawnCooldown:0,
  185.       baseSpawn: 1.2, // seconds
  186.       hustle:0, // 0..1
  187.       hustleHeld:false,
  188.     };
  189.  
  190.     const cars=[];
  191.  
  192.     function carTemplate(dir){
  193.       const speedType = Math.random();
  194.       const base = speedType<.1? 110 : speedType<.8? 140 : 180; // vans, normal, speedy (px/s)
  195.      const len = speedType<.1? 36 : speedType<.8? 28 : 22;
  196.      const wid = 14;
  197.      const col = speedType<.1? '#eab308' : speedType<.8? '#38bdf8' : '#f472b6';
  198.      // positions relative to intersection center
  199.      const spawnDist = Math.max(S.w,S.h);
  200.      let x=0,y=0,vx=0,vy=0; const lane=dir;
  201.      if(dir===0){ y = -spawnDist; x= rint(-18,18); vy= base; }
  202.      if(dir===2){ y = spawnDist; x= rint(-18,18); vy= -base; }
  203.      if(dir===1){ x = spawnDist; y= rint(-18,18); vx= -base; }
  204.      if(dir===3){ x = -spawnDist; y= rint(-18,18); vx= base; }
  205.      return {x,y,vx,vy,w:wid,h:len,col,dir:lane, wait:0, passed:false};
  206.    }
  207.  
  208.    function spawnCar(){
  209.      const dir = rint(0,3);
  210.      cars.push(carTemplate(dir));
  211.    }
  212.  
  213.    function reset(){
  214.      score=0; multiplier=1; maxMult=1; level=1; world.lightNS=true; world.spawnCooldown=0; world.hustle=0.35; // start with a bit of meter
  215.      cars.length=0;
  216.    }
  217.  
  218.    function aabb(a,b){ return Math.abs(a.x-b.x) < (a.w+b.w)/2 && Math.abs(a.y-b.y) < (a.h+b.h)/2; }
  219.  
  220.    // Input
  221.    let mouseDown=false;
  222.    window.addEventListener('pointerdown', (e)=>{
  223.       mouseDown=true; if(state===STATE.MENU){ startGame(); return; }
  224.       if(state===STATE.PLAY){ world.toggle(); }
  225.       if(state===STATE.OVER){ startGame(); }
  226.     });
  227.     window.addEventListener('pointerup', ()=> mouseDown=false);
  228.  
  229.     window.addEventListener('keydown', (e)=>{
  230.       if(e.key===' '){ world.hustleHeld=true; e.preventDefault(); }
  231.       if(e.key==='p'||e.key==='P'){ togglePause(); }
  232.       if(e.key==='m'||e.key==='M'){ const m=Audio.toggle(); toast(m?"Muted":"Sound On"); }
  233.     });
  234.     window.addEventListener('keyup', (e)=>{ if(e.key===' '){ world.hustleHeld=false; }});
  235.  
  236.     // Buttons
  237.     startBtn.onclick=()=>startGame();
  238.     retryBtn.onclick=()=>startGame();
  239.     menuBtn.onclick=()=>showMenu();
  240.     playBtn.onclick=()=>{ if(state===STATE.PLAY){ world.toggle(); } else startGame(); };
  241.     howBtn.onclick=()=>{ help.style.display='grid'; menu.style.display='none'; };
  242.     backBtn.onclick=()=>{ help.style.display='none'; menu.style.display='grid'; };
  243.     playFromHelp.onclick=()=>{ help.style.display='none'; startGame(); };
  244.     pauseBtn.onclick=()=>togglePause();
  245.  
  246.     function togglePause(){
  247.       if(state!==STATE.PLAY && state!==STATE.PAUSED) return;
  248.       state = state===STATE.PLAY? STATE.PAUSED : STATE.PLAY;
  249.       toast(state===STATE.PAUSED? 'Paused' : 'Resumed');
  250.     }
  251.  
  252.     function showMenu(){
  253.       state=STATE.MENU; menu.style.display='grid'; help.style.display='none'; gameover.style.display='none';
  254.     }
  255.  
  256.     function startGame(){
  257.       reset();
  258.       state=STATE.PLAY; menu.style.display='none'; help.style.display='none'; gameover.style.display='none';
  259.       Audio.context.resume && Audio.context.resume();
  260.     }
  261.  
  262.     function endGame(){
  263.       state=STATE.OVER; finalScore.textContent=score; finalBest.textContent=best; finalMult.textContent=maxMult.toFixed(1)+'x'; finalLvl.textContent=level;
  264.       gameover.style.display='grid';
  265.     }
  266.  
  267.     // HUD
  268.     function updateHUD(){
  269.       scorePill.textContent = `Score ${score}`;
  270.       multEl.textContent = multiplier.toFixed(1);
  271.       lvlEl.textContent = level;
  272.       hustleBar.style.width = `${(world.hustle*100)|0}%`;
  273.       bestEl.textContent = best;
  274.     }
  275.  
  276.     // Toasts
  277.     let toastT=0, toastText='';
  278.     function toast(t){ toastText=t; toastT=1.6; }
  279.  
  280.     // Loop
  281.     let last=performance.now();
  282.     function loop(now){
  283.       requestAnimationFrame(loop);
  284.       const dt = Math.min(0.033, (now-last)/1000); last=now;
  285.       if(state===STATE.PAUSED){ draw(dt,true); return; }
  286.       if(state===STATE.PLAY){ update(dt); }
  287.       draw(dt,false);
  288.     }
  289.     requestAnimationFrame(loop);
  290.  
  291.     // Update
  292.     function update(dt){
  293.       // Hustle meter
  294.       const hustleDrain = world.hustleHeld? .45 : -.25; // drain if held, recharge if not
  295.       world.hustle = Math.max(0, Math.min(1, world.hustle + hustleDrain*dt));
  296.       const hustleBoost = 1 + world.hustle*1.2 + (world.hustleHeld? .4:0);
  297.  
  298.       // Spawning
  299.       const spawnBase = Math.max(.45, world.baseSpawn - (level-1)*0.08);
  300.       world.spawnCooldown -= dt * hustleBoost;
  301.       if(world.spawnCooldown<=0){
  302.        world.spawnCooldown = spawnBase * rand(.75,1.25);
  303.        spawnCar();
  304.      }
  305.  
  306.      // Move cars & light logic
  307.      const lightNS = world.lightNS;
  308.      const interHalf = 44; // intersection half-size
  309.      const stopLine = interHalf+20;
  310.  
  311.      for(let i=cars.length-1;i>=0;i--){
  312.         const c = cars[i];
  313.         // Decide if should stop at red
  314.         let red = false;
  315.         if(c.dir===0||c.dir===2){ // N/S
  316.           red = !lightNS;
  317.           const willEnter = Math.abs(c.y + Math.sign(c.vy)*c.h/2) < stopLine;
  318.          if(red && ((c.dir===0 && c.y < -stopLine) || (c.dir===2 && c.y > stopLine))){
  319.             // Far from line: keep moving
  320.           } else if(red && willEnter){ // apply braking
  321.            c.vstop = true;
  322.           } else { c.vstop=false; }
  323.         } else { // E/W
  324.           red = lightNS;
  325.           const willEnter = Math.abs(c.x + Math.sign(c.vx)*c.w/2) < stopLine;
  326.          if(red && ((c.dir===3 && c.x < -stopLine) || (c.dir===1 && c.x > stopLine))){
  327.           } else if(red && willEnter){ c.vstop=true; } else { c.vstop=false; }
  328.         }
  329.  
  330.         // patience / wait
  331.         if(c.vstop){ c.wait += dt; if(c.wait>.9) { if((i&7)===0) Audio.honk(); } }
  332.         else { if(c.wait>0){ c.wait=Math.max(0, c.wait - dt*1.5); } }
  333.  
  334.         // speed
  335.         const spdScale = 1 + (level-1)*.08 + (world.hustleHeld? .1:0);
  336.         const maxV = Math.hypot(c.vx,c.vy) * spdScale;
  337.         const v = c.vstop? 0 : maxV;
  338.         const nx = c.x + (c.vx? Math.sign(c.vx)*v*dt : 0);
  339.         const ny = c.y + (c.vy? Math.sign(c.vy)*v*dt : 0);
  340.         c.x=nx; c.y=ny;
  341.  
  342.         // Passed center for scoring
  343.         if(!c.passed && Math.abs(c.x)<interHalf && Math.abs(c.y)<interHalf){
  344.          c.passed=true;
  345.         }
  346.         if(c.passed && (Math.abs(c.x)>interHalf+40 || Math.abs(c.y)>interHalf+40)){
  347.          // exited intersection safely
  348.          score += Math.ceil(1*multiplier);
  349.           // combo influenced by how little they waited
  350.           multiplier = Math.min(9.9, multiplier + (c.wait<.15? .15 : c.wait<.35? .08 : .03));
  351.          maxMult = Math.max(maxMult, multiplier);
  352.          cars.splice(i,1);
  353.          if(score>0 && score%100===0){ level++; toast('Level Up!'); Audio.beep(1200,.09,'triangle',.08); }
  354.         }
  355.  
  356.         // remove if offscreen long
  357.         if(Math.abs(c.x)>Math.max(S.w,S.h)+200 || Math.abs(c.y)>Math.max(S.w,S.h)+200){ cars.splice(i,1); }
  358.       }
  359.  
  360.       // Combo decay if queues are long (someone is waiting too long)
  361.       const worstWait = cars.reduce((m,c)=>Math.max(m,c.wait),0);
  362.       const decay = worstWait>.8? .55 : worstWait>.5? .3 : .12;
  363.       multiplier = Math.max(1, multiplier - decay*dt + (world.hustleHeld? .18*dt:0));
  364.  
  365.       // Collisions inside intersection
  366.       const active = cars.filter(c=> Math.abs(c.x) < interHalf && Math.abs(c.y) < interHalf);
  367.      for(let i=0;i<active.length;i++){
  368.        for(let j=i+1;j<active.length;j++){
  369.          const A = active[i], B = active[j];
  370.          // IMPORTANT FIX:
  371.          // Ignore collisions between cars that travel along the same axis (both vertical: dir 0 or 2, or both horizontal: dir 1 or 3).
  372.          // This lets N<>S vehicles pass together and E<>W vehicles pass together without falsely colliding.
  373.           if( (A.dir % 2) === (B.dir % 2) ) continue;
  374.  
  375.           if(aabb(A, B)){
  376.             // crash
  377.             Audio.crash();
  378.             best = Math.max(best, score); localStorage.setItem('tb_best', best);
  379.             endGame(); return;
  380.           }
  381.         }
  382.       }
  383.  
  384.       updateHUD();
  385.     }
  386.  
  387.     // Draw
  388.     function draw(dt, paused){
  389.       ctx.clearRect(0,0,S.w,S.h);
  390.       ctx.save();
  391.       // transform origin to center
  392.       ctx.translate(S.w/2, S.h/2);
  393.  
  394.       // Road background
  395.       drawRoad();
  396.       drawLights();
  397.  
  398.       // Cars
  399.       for(const c of cars){ drawCar(c); }
  400.  
  401.       // Toast
  402.       if(toastT>0){
  403.         toastT -= dt; const t = Math.max(0, Math.min(1, toastT/1.6));
  404.         ctx.save(); ctx.translate(0, -S.h*0.34); ctx.globalAlpha = Math.pow(t, .9);
  405.         roundedText(toastText, 0,0, 16, '#0b1220', 'white'); ctx.restore();
  406.       }
  407.  
  408.       // Paused overlay
  409.       if(paused){
  410.         ctx.save(); ctx.globalAlpha=.9; ctx.fillStyle='rgba(11,15,20,.55)'; ctx.fillRect(-S.w,S.h,-S.w*2,-S.h*2); ctx.restore();
  411.         roundedText('Paused',0,0,22,'#0b1220','white');
  412.       }
  413.  
  414.       ctx.restore();
  415.     }
  416.  
  417.     function drawRoad(){
  418.       const w=S.w, h=S.h; const interSize=88; // full size
  419.       // asphalt
  420.       ctx.fillStyle = '#0f172a';
  421.       ctx.fillRect(-w, -interSize/2-120, w*2, interSize+240); // horizontal band
  422.       ctx.fillRect(-interSize/2-120, -h, interSize+240, h*2); // vertical band
  423.       // lane markings
  424.       ctx.strokeStyle='rgba(255,255,255,.25)'; ctx.lineWidth=2; ctx.setLineDash([10,10]);
  425.       ctx.beginPath();
  426.       ctx.moveTo(-w,0); ctx.lineTo(w,0); // center horiz
  427.       ctx.moveTo(0,-h); ctx.lineTo(0,h); // center vert
  428.       ctx.stroke(); ctx.setLineDash([]);
  429.       // intersection box
  430.       ctx.strokeStyle='rgba(250,204,21,.6)'; ctx.lineWidth=3; roundedRect(-44,-44,88,88,6,true);
  431.       // sidewalks
  432.       ctx.fillStyle='#1f2937';
  433.       roundedRect(-w,-h,w*2, h*2, 0, true);
  434.       ctx.save(); ctx.globalCompositeOperation='destination-out';
  435.       roundedRect(-w, -interSize/2-120, w*2, interSize+240, 0, true);
  436.       roundedRect(-interSize/2-120, -h, interSize+240, h*2, 0, true);
  437.       ctx.restore();
  438.       // glow
  439.       const g = ctx.createRadialGradient(0,0,10, 0,0,220); g.addColorStop(0,'rgba(99,102,241,.18)'); g.addColorStop(1,'transparent');
  440.       ctx.fillStyle=g; ctx.beginPath(); ctx.arc(0,0,220,0,Math.PI*2); ctx.fill();
  441.     }
  442.  
  443.     function drawLights(){
  444.       const green = world.lightNS? 'rgba(74,222,128,.95)':'rgba(239,68,68,.9)';
  445.       const red   = world.lightNS? 'rgba(239,68,68,.9)':'rgba(74,222,128,.95)';
  446.       // NS heads
  447.       lightPuck(0,-72, green);
  448.       lightPuck(0, 72, green);
  449.       // EW heads
  450.       lightPuck(-72,0, red);
  451.       lightPuck(72,0, red);
  452.     }
  453.  
  454.     function lightPuck(x,y,color){
  455.       ctx.save(); ctx.translate(x,y);
  456.       ctx.shadowColor=color; ctx.shadowBlur=18; ctx.fillStyle=color; roundedRect(-14,-14,28,28,8,true);
  457.       ctx.restore();
  458.     }
  459.  
  460.     function drawCar(c){
  461.       ctx.save(); ctx.translate(c.x, c.y);
  462.       ctx.fillStyle=c.col; roundedRect(-c.w/2, -c.h/2, c.w, c.h, 3, true);
  463.       // windshield
  464.       ctx.fillStyle='rgba(255,255,255,.25)'; if(c.vx){ roundedRect(-c.w/2+2, -c.h/2+2, c.w-4, 5, 2, true); } else { roundedRect(-c.w/2+2, -c.h/2+2, c.w-4, 5, 2, true); }
  465.       // taillight glow
  466.       ctx.fillStyle='rgba(248,113,113,.5)'; if(c.vx>0) roundedRect(-c.w/2, -c.h/2, 3, c.h, 1, true); if(c.vx<0) roundedRect(c.w/2-3, -c.h/2, 3, c.h, 1, true);
  467.      if(c.vy>0) roundedRect(-c.w/2, -c.h/2, c.w, 3, 1, true); if(c.vy<0) roundedRect(-c.w/2, c.h/2-3, c.w, 3, 1, true);
  468.      // impatience bar
  469.      if(c.wait>0){ ctx.fillStyle='rgba(250,204,21,.9)'; const w = Math.min(1, c.wait/1.2)* (c.w); ctx.fillRect(-c.w/2, -c.h/2-6, w, 2); }
  470.       ctx.restore();
  471.     }
  472.  
  473.     // Helpers
  474.     function roundedRect(x,y,w,h,r,fill){ ctx.beginPath(); rrPath(x,y,w,h,r); fill?ctx.fill():ctx.stroke(); }
  475.     function rrPath(x,y,w,h,r){ const rr = Math.min(r, Math.abs(w/2), Math.abs(h/2));
  476.       ctx.moveTo(x+rr,y); ctx.arcTo(x+w,y, x+w,y+h, rr); ctx.arcTo(x+w,y+h, x,y+h, rr); ctx.arcTo(x,y+h, x,y, rr); ctx.arcTo(x,y, x+w,y, rr); ctx.closePath(); }
  477.  
  478.     function roundedText(t, x,y, pad, bg='#000', fg='#fff'){
  479.       ctx.save(); ctx.font='700 18px system-ui, -apple-system, Segoe UI, Roboto, Arial'; ctx.textAlign='center'; ctx.textBaseline='middle';
  480.       const metrics=ctx.measureText(t); const tw=metrics.width; const th=28; const ww=tw+pad*2;
  481.       ctx.fillStyle=bg; ctx.globalAlpha=.9; roundedRect(x-ww/2,y-th/2,ww,th,12,true);
  482.       ctx.globalAlpha=1; ctx.fillStyle=fg; ctx.fillText(t,x,y+1);
  483.       ctx.restore();
  484.     }
  485.  
  486.     // Kick off
  487.     toast('Tap to toggle lights • Hold Space to Hustle');
  488.   </script>
  489. </body>
  490. </html>
  491.  
Advertisement
Add Comment
Please, Sign In to add comment