BE AGGRESSIVE — Beat the Stopwatch

* { box-sizing: border-box; }

body {

margin: 0;

background: #101221;

color: #fff4ce;

font-family: “Courier New”, monospace;

display: flex;

flex-direction: column;

align-items: center;

padding: 18px 12px;

}

h1 {

margin: 0 0 5px;

color: #ffce45;

letter-spacing: 4px;

text-shadow: 3px 3px #ba3658;

font-size: clamp(24px, 5vw, 40px);

}

.subtitle {

color: #aebbd5;

font-size: 12px;

text-align: center;

margin-bottom: 14px;

}

.game {

width: min(100%, 1024px);

position: relative;

border: 4px solid #425273;

background: #151b2c;

box-shadow: 8px 8px 0 #080c16;

}

canvas {

display: block;

width: 100%;

height: auto;

image-rendering: pixelated;

image-rendering: crisp-edges;

touch-action: none;

}

.overlay {

position: absolute;

inset: 0;

background: #0d1324e8;

display: flex;

align-items: center;

justify-content: center;

padding: 16px;

text-align: center;

}

.overlay[hidden] { display: none; }

.panel { max-width: 590px; }

.panel h2 {

color: #ffce45;

font-size: clamp(19px, 4vw, 32px);

margin: 0 0 12px;

}

.panel p {

line-height: 1.5;

font-size: clamp(11px, 2vw, 16px);

}

button {

font: bold 14px “Courier New”, monospace;

background: #ffce45;

border: 0;

border-bottom: 4px solid #a86c27;

color: #171a28;

padding: 12px 18px;

cursor: pointer;

touch-action: none;

}

button:hover { background: #ffe294; }

button:active { transform: translateY(2px); }

.instructions {

max-width: 950px;

line-height: 1.6;

font-size: 13px;

text-align: center;

margin: 16px 0 9px;

}

.instructions b { color: #ffce45; }

.controls {

display: flex;

align-items: center;

gap: 14px;

flex-wrap: wrap;

justify-content: center;

}

.dpad {

display: grid;

grid-template-columns: repeat(3, 46px);

grid-template-rows: repeat(3, 40px);

gap: 4px;

}

.dpad button { padding: 4px; }

.up { grid-column: 2; grid-row: 1; }

.left { grid-column: 1; grid-row: 2; }

.right { grid-column: 3; grid-row: 2; }

.down { grid-column: 2; grid-row: 3; }

.actions { display: flex; gap: 8px; flex-wrap: wrap; }

.actions button { font-size: 12px; padding: 13px 11px; }

.note { color: #8b9bb9; font-size: 11px; margin-top: 12px; }

BE AGGRESSIVE

SANTA FELITA HIGH • AN ISOMETRIC ARCADE ROAD MOVIE

BEAT THE STOPWATCH

Coach Ellen has one deadline.

Honey has a competition to win.

Merv has a bus full of trouble.

Follow the gold checkpoints out of Santa Felita.

Then help Charlie and Noelle catch the moving bus.

WASD / Arrow keys: drive along the city’s isometric axes.

W = ↗, D = ↘, S = ↙, A = ↖.

Release to brake. Shift: match bus speed during the chase.

E: begin / time the vault.

P: pause. R: restart.

A fictional arcade adaptation of Jeff Carpenter’s screenplay.

Original procedural graphics; no external assets.

“use strict”;

const canvas = document.getElementById(“game”);

const g = canvas.getContext(“2d”);

g.imageSmoothingEnabled = false;

const overlay = document.getElementById(“overlay”);

const heading = document.getElementById(“heading”);

const description = document.getElementById(“description”);

const startButton = document.getElementById(“start”);

const W = 640, H = 400;

const GRID = 180;

const LAST = 1080;

const ROAD = 72;

const keys = Object.create(null);

const P = {

grass: “#496553”,

grassDark: “#3a554b”,

road: “#33394d”,

sidewalk: “#87909b”,

yellow: “#ffce45”,

pink: “#ff658a”,

cyan: “#77d7dc”,

white: “#fff5cf”,

ink: “#172035”,

red: “#f45c69”,

green: “#83dc86”

};

let active = false, paused = false, state = “bus”;

let clock = 48, elapsed = 0, score = 0;

let collisions = 0, reds = 0, pickups = 0;

let poise = 3, checkpoint = 0, invulnerable = 0;

let bus, bike, player;

let traffic = [], buildings = [], tapes = [];

let camera = {x: 0, y: 0};

let message = “”, messageTimer = 0;

let dock = 0, timing = null, jumping = null;

let noelleAboard = false;

let particles = [], signalVisits = {};

let flash = 0, shake = 0;

let randomSeed = 42;

let audio = null, sound = false;

const route = [

{x:180, y:180},

{x:360, y:180},

{x:360, y:360},

{x:540, y:360},

{x:720, y:360},

{x:720, y:540}

];

const chaseRoute = [

{x:720, y:540},

{x:900, y:540},

{x:1080,y:540},

{x:1080,y:720},

{x:1080,y:900},

{x:900, y:900},

{x:720, y:900},

{x:540, y:900},

{x:360, y:900},

{x:180, y:900},

{x:180, y:720},

{x:180, y:540},

{x:360, y:540},

{x:540, y:540}

];

let chaseTarget = 1;

function rand() {

randomSeed = (randomSeed * 1664525 + 1013904223) >>> 0;

return randomSeed / 4294967296;

}

function clamp(v,a,b) { return Math.max(a,Math.min(b,v)); }

function dist(a,b) { return Math.hypot(a.x-b.x,a.y-b.y); }

function mix(a,b,t) { return a+(b-a)*t; }

function wrapAngle(a) {

while(a > Math.PI) a -= Math.PI*2;

while(a < -Math.PI) a += Math.PI*2;

return a;

}

function project(x,y,z=0) {

return {

x: Math.round(320+(x-y-camera.x+camera.y)*0.78),

y: Math.round(186+(x+y-camera.x-camera.y)*0.39-z)

};

}

function poly(points,color) {

g.fillStyle=color;

g.beginPath();

g.moveTo(points[0].x,points[0].y);

for(let i=1;imaxWidth && line) {

text(line,x,y,color,11);

line=word+” “; y+=lineHeight;

} else line=test;

}

text(line,x,y,color,11);

}

function say(s,duration=4) {

message=s;

messageTimer=duration;

}

function beep(freq=440,duration=.08,type=”square”) {

if(!sound) return;

try {

if(!audio) audio=new (window.AudioContext||window.webkitAudioContext)();

audio.resume();

let o=audio.createOscillator(), gain=audio.createGain();

o.type=type; o.frequency.value=freq;

gain.gain.setValueAtTime(.035,audio.currentTime);

gain.gain.exponentialRampToValueAtTime(.001,audio.currentTime+duration);

o.connect(gain); gain.connect(audio.destination);

o.start(); o.stop(audio.currentTime+duration);

} catch(e) {}

}

function burst(x,y,color,n=12) {

for(let i=0;i<n;i++) particles.push({

x,y,z:8, vx:(rand()-.5)*90,vy:(rand()-.5)*90,

vz:25+rand()*45,life:.5+rand()*.5,color

});

}

function makeVehicle(x,y,angle,type,color) {

return {x,y,angle,type,color,vx:0,vy:0,speed:0};

}

function reset() {

randomSeed=42;

keysClear();

state="bus"; clock=48; elapsed=0; score=0;

collisions=0; reds=0; pickups=0; poise=3;

checkpoint=0; invulnerable=0; dock=0;

timing=null; jumping=null; noelleAboard=false;

particles=[]; signalVisits={}; chaseTarget=1;

flash=0; shake=0; paused=false;

bus=makeVehicle(0,180,0,"bus",P.yellow);

bike=makeVehicle(0,180,0,"bike","#272335");

player=bus;

camera={x:bus.x,y:bus.y};

traffic=[]; buildings=[]; tapes=[];

const colors=["#7894a1","#a48285","#768394","#9c8d78","#719180"];

for(let x=0;x<6;x++) for(let y=0;y<6;y++) {

let school=x===0&&y===0;

buildings.push({

x:x*GRID+54, y:y*GRID+54,

w:school?96:67+rand()*27,

d:school?96:68+rand()*26,

h:school?27:20+rand()*35,

color:school?"#cc9978":colors[Math.floor(rand()*colors.length)],

school,

sign: school?"SANTA FELITA HIGH":

(x===2&&y===1?"ARGENT RADIO":

(x===4&&y===3?"BURGER STOP":

(x===1&&y===4?"AUTO WASH":"")))

});

}

for(let i=0;i.5?1:-1;

let lane=Math.floor(rand()*7)*GRID;

let pos=rand()*LAST;

let car=makeVehicle(

horizontal?pos:lane-direction*13,

horizontal?lane+direction*13:pos,

horizontal?(direction===1?0:Math.PI):

(direction===1?Math.PI/2:-Math.PI/2),

“car”,

[“#dd7778″,”#77b8c8″,”#b4c98b”,”#a799c9″,”#e7bb78″][i%5]

);

car.horizontal=horizontal;

car.direction=direction;

car.cruise=38+rand()*28;

if(dist(car,bus)tapes.push({x,y,taken:false}));

say(“HONEY: Coach’s stopwatch is running! Gold checkpoints, Merv. No scenic route!”,6);

}

function keysClear() {

for(const k of Object.keys(keys)) keys[k]=false;

}

function begin() {

reset();

active=true;

overlay.hidden=true;

beep(220,.12);

}

function end(win) {

active=false;

overlay.hidden=false;

heading.textContent=win?”VICTORY! SANTA FELITA!”:”THE CLOCK WON THIS ROUND”;

if(win) {

score+=Math.ceil(Math.max(clock,0))*25;

description.innerHTML=

`Noelle is aboard. Charlie sticks the landing.

Honey catches the boombox. Merv never stops.

SCORE ${score}

${reds} red-light runs · ${pickups} cassette pickups ·

${collisions} traffic bumps

“You’ve got to work as a team.”

Next stop: the Las Vegas competition.`;

beep(880,.25);

} else {

description.innerHTML=

`Honey: “We trained all year for this!”

${state===”bus”?

“Follow every gold checkpoint. Cut clean corners and collect cassettes for extra time.”:

“Catch the bus, hold Shift to match speed, and stay directly behind the rear doors.”}

Score: ${score} · Press R or try again.`;

beep(110,.3,”sawtooth”);

}

startButton.textContent=”ONE MORE RUN”;

}

function startChase() {

state=”chase”;

clock=60;

score+=500;

bus.x=720; bus.y=540; bus.angle=0;

bus.speed=76; bus.vx=76; bus.vy=0;

bike.x=540; bike.y=540; bike.angle=0;

bike.vx=0; bike.vy=0; bike.speed=0;

player=bike; invulnerable=2; dock=0;

flash=.35;

say(“CHARLIE: Typical. Missed the bus. NOELLE: I saved the permission form! Let’s catch them!”,7);

beep(660,.2);

}

function isRoad(x,y) {

let dx=Math.abs(x-Math.round(x/GRID)*GRID);

let dy=Math.abs(y-Math.round(y/GRID)*GRID);

return Math.min(dx,dy)<ROAD/2-4;

}

function horizontalGreen(ix,iy) {

return (elapsed+(ix+iy)*1.37)%105) {

let desired=Math.atan2(player.vy,player.vx);

player.angle+=wrapAngle(desired-player.angle)*Math.min(1,dt*11);

}

}

function moveBus(dt) {

let remaining=76*dt;

while(remaining>0) {

let target=chaseRoute[chaseTarget];

let dx=target.x-bus.x,dy=target.y-bus.y;

let d=Math.hypot(dx,dy);

if(d20&&awayLAST+140)c.x=-140;

if(c.xLAST+140)c.y=-140;

if(c.y<-140)c.y=LAST+140;

if(!timing&&!jumping&&invulnerable<=0) {

let dx=player.x-c.x,dy=player.y-c.y;

let forward=Math.cos(player.angle)*dx+Math.sin(player.angle)*dy;

let side=-Math.sin(player.angle)*dx+Math.cos(player.angle)*dy;

if(Math.abs(forward)<(state==="bus"?35:24)&&Math.abs(side)0) {

poise–;

say(“HONEY: Brace formation! The squad absorbs the bump. Keep moving!”,3);

} else {

clock-=3;

say(“MERV: A little paint exchange! Three seconds lost.”,3);

}

player.vx*=.28;player.vy*=.28;

beep(90,.12,”sawtooth”);

}

}

}

}

function runSignals() {

if(player.speed14||Math.abs(player.y-iy*GRID)>14)return;

let id=ix+”,”+iy;

if(signalVisits[id]!==undefined && elapsed-signalVisits[id]Math.abs(player.vy);

let green=horizontal?horizontalGreen(ix,iy):!horizontalGreen(ix,iy);

if(!green) {

reds++;score+=75;

burst(player.x,player.y,P.pink,5);

if(messageTimer<1) say("HONEY: Red means we're behind schedule! +75",2.5);

beep(520,.045);

}

}

function collectTapes() {

for(const t of tapes) if(!t.taken&&dist(t,player)29&&along<76&&side.78&&relative<34;

dock=clamp(dock+(good?dt:-dt*2),0,.8);

// Soft separation: the motorcycle cannot disappear inside the bus.

if(dist(bus,bike)=.8&&messageTimer.37&&v=.8) {

timing={time:0,value:0};

say(“HONEY: Catch the beat! Press E when the marker enters the GREEN zone.”,4);

beep(440,.07);

} else {

say(“Get directly behind the bus. Hold SHIFT and its direction to match speed.”,3);

}

}

function update(dt) {

if(!active||paused)return;

elapsed+=dt;

clock-=dt;

messageTimer=Math.max(0,messageTimer-dt);

invulnerable=Math.max(0,invulnerable-dt);

flash=Math.max(0,flash-dt);

shake=Math.max(0,shake-dt);

let worldDt=timing?dt*.22:dt;

if(state!==”bus”) moveBus(worldDt);

if(timing||jumping) {

let rear=rearPosition(46);

bike.x=rear.x;bike.y=rear.y;bike.angle=bus.angle;

bike.vx=bus.vx;bike.vy=bus.vy;bike.speed=76;

} else movePlayer(worldDt);

updateTraffic(worldDt);

if(!timing&&!jumping) {

runSignals();

collectTapes();

}

if(state===”bus”) {

if(dist(bus,route[checkpoint])=route.length) startChase();

else {

const lines=[

“IRIS: Route confirmed. Keep following the gold diamonds!”,

“HONEY: It’s all about poise. Looking good under pressure!”,

“MERV: Behind the eight ball! Under the gun!”,

“SHAUNA-RAE: Did anyone remember the birthday present?”,

“HONEY: The blue suitcase is packed. Where are Charlie and Noelle?”

];

say(lines[(checkpoint-1)%lines.length],4);

}

}

} else if(!timing&&!jumping) {

docking(dt);

}

if(timing) {

timing.time+=dt;

timing.value=(Math.sin(timing.time*5.3-Math.PI/2)+1)/2;

if(timing.time>6) {

timing=null;dock=0;clock-=5;

say(“HONEY: The moment passed! Line it up again.”,3);

}

}

if(jumping) {

jumping.time+=dt;

if(jumping.time>.95) {

if(jumping.who===”noelle”) {

noelleAboard=true;

jumping=null;dock=0;

burst(bus.x,bus.y,P.pink,24);

say(“NOELLE IS ABOARD! Honey catches the boombox. Charlie, don’t get left behind!”,6);

clock+=8;

} else {

jumping=null;

burst(bus.x,bus.y,P.yellow,45);

end(true);

}

}

}

for(const p of particles) {

p.life-=dt;p.x+=p.vx*dt;p.y+=p.vy*dt;

p.z+=p.vz*dt;p.vz-=100*dt;

}

particles=particles.filter(p=>p.life>0);

let aim={x:player.x,y:player.y};

if(state!==”bus”&&dist(bus,bike)<280) {

aim.x=mix(bike.x,bus.x,.35);

aim.y=mix(bike.y,bus.y,.35);

}

camera.x=mix(camera.x,aim.x,1-Math.exp(-dt*5));

camera.y=mix(camera.y,aim.y,1-Math.exp(-dt*5));

if(clock<=0&&active) end(false);

}

/* ———- PIXEL-ART RENDERER ———- */

function box(x,y,w,d,h,color) {

let a=project(x,y),b=project(x+w,y),c=project(x+w,y+d),dd=project(x,y+d);

let at=project(x,y,h),bt=project(x+w,y,h);

let ct=project(x+w,y+d,h),dt=project(x,y+d,h);

poly([dd,c,ct,dt],"#445468");

poly([b,c,ct,bt],color);

poly([at,bt,ct,dt],color);

poly([at,bt,ct,dt],"#ffffff18");

g.strokeStyle="#273044";g.lineWidth=1;

g.beginPath();g.moveTo(at.x,at.y);g.lineTo(bt.x,bt.y);

g.lineTo(ct.x,ct.y);g.lineTo(dt.x,dt.y);g.closePath();g.stroke();

}

function drawBuilding(b) {

let center=project(b.x+b.w/2,b.y+b.d/2,b.h);

if(center.xW+180||center.yH+140)return;

rectWorld(b.x-4,b.y-4,b.w+13,b.d+13,”#233b3b”);

box(b.x,b.y,b.w,b.d,b.h,b.color);

rectWorld(b.x+9,b.y+9,b.w-18,b.d-18,

b.school?”#cfb091″:”#697b88″,b.h+1);

for(let i=12;i<b.w-8;i+=20) {

let p=project(b.x+i,b.y+b.d+.5,b.h-11);

g.fillStyle="#182f46";g.fillRect(p.x-3,p.y-3,7,7);

g.fillStyle="#b6dbe0";g.fillRect(p.x-2,p.y-2,3,3);

}

if(b.school) {

let p=project(b.x+b.w/2,b.y+b.d,b.h-4);

g.fillStyle=P.ink;g.fillRect(p.x-57,p.y-9,114,13);

text("SANTA FELITA HIGH",p.x-53,p.y+1,P.yellow,9);

let flag=project(b.x+5,b.y+b.d+11);

g.fillStyle=P.white;g.fillRect(flag.x,flag.y-42,2,42);

g.fillStyle=P.pink;g.fillRect(flag.x+2,flag.y-42,24,11);

text("SF",flag.x+6,flag.y-33,P.white,9);

} else if(b.sign) {

let p=project(b.x+b.w/2,b.y+b.d,b.h-3);

let len=b.sign.length*6+8;

g.fillStyle=P.ink;g.fillRect(p.x-len/2,p.y-10,len,13);

text(b.sign,p.x-len/2+4,p.y,P.cyan,9);

}

}

function drawGround() {

g.fillStyle=P.grass;g.fillRect(0,0,W,H);

for(let i=0;i<7;i++) {

rectWorld(-130,i*GRID-ROAD/2-6,LAST+260,ROAD+12,P.sidewalk);

rectWorld(i*GRID-ROAD/2-6,-130,ROAD+12,LAST+260,P.sidewalk);

}

for(let i=0;i<7;i++) {

rectWorld(-130,i*GRID-ROAD/2,LAST+260,ROAD,P.road);

rectWorld(i*GRID-ROAD/2,-130,ROAD,LAST+260,P.road);

}

for(let i=0;i<7;i++) for(let j=-2;j<37;j++) {

let pos=j*34;

if(Math.abs(pos-Math.round(pos/GRID)*GRID)<44)continue;

rectWorld(pos,i*GRID-1,14,2,"#b2a988");

rectWorld(i*GRID-1,pos,2,14,"#b2a988");

}

for(let ix=0;ix<7;ix++) for(let iy=0;iy<7;iy++) {

let x=ix*GRID,y=iy*GRID,p=project(x,y);

if(p.xW+120||p.yH+100)continue;

for(let s=-22;s<=22;s+=11) {

rectWorld(x-38,y+s,6,6,"#a8afa9");

rectWorld(x+s,y-38,6,6,"#a8afa9");

}

let hg=horizontalGreen(ix,iy);

rectWorld(x-27,y-27,3,17,hg?P.green:P.red);

rectWorld(x-27,y-27,17,3,hg?P.red:P.green);

let pole=project(x+38,y-38);

g.fillStyle="#152235";g.fillRect(pole.x,pole.y-20,2,20);

g.fillRect(pole.x-4,pole.y-25,10,10);

g.fillStyle=hg?P.green:P.red;

g.fillRect(pole.x-1,pole.y-22,4,4);

}

// School departure markings and roadside banner.

rectWorld(-29,152,58,7,P.yellow);

let sign=project(-30,110,18);

text("VEGAS OR BUST!",sign.x-38,sign.y,P.yellow,9);

for(const t of tapes) if(!t.taken) {

let p=project(t.x,t.y,8+Math.sin(elapsed*5)*3);

g.fillStyle="#17283d";g.fillRect(p.x-9,p.y-6,18,12);

g.fillStyle=P.cyan;g.fillRect(p.x-7,p.y-4,14,7);

g.fillStyle=P.ink;

g.fillRect(p.x-5,p.y-2,3,3);

g.fillRect(p.x+2,p.y-2,3,3);

text("+3",p.x-7,p.y-11,P.white,8);

}

if(state==="bus"&&checkpoint<route.length) {

let t=route[checkpoint],p=project(t.x,t.y);

let pulse=24+Math.sin(elapsed*6)*3;

poly([

{x:p.x,y:p.y-pulse/2},{x:p.x+pulse,y:p.y},

{x:p.x,y:p.y+pulse/2},{x:p.x-pulse,y:p.y}

],"#ffce4555");

g.strokeStyle=P.yellow;g.lineWidth=2;

g.beginPath();g.moveTo(p.x,p.y-pulse/2);g.lineTo(p.x+pulse,p.y);

g.lineTo(p.x,p.y+pulse/2);g.lineTo(p.x-pulse,p.y);

g.closePath();g.stroke();

text(String(checkpoint+1),p.x-4,p.y-23,P.yellow,14);

text(checkpoint===5?"OUTBOUND":"CHECKPOINT",p.x-31,p.y-37,P.white,8);

}

}

function local(v,forward,side,z=0) {

let co=Math.cos(v.angle),si=Math.sin(v.angle);

return project(v.x+co*forward-si*side,v.y+si*forward+co*side,z);

}

function rider(x,y,z,color,hair="#f5c277") {

let p=project(x,y,z);

g.fillStyle="#151c2b";g.fillRect(p.x-4,p.y-7,9,14);

g.fillStyle=color;g.fillRect(p.x-3,p.y-4,7,8);

g.fillStyle="#f4c1a1";g.fillRect(p.x-3,p.y-11,6,6);

g.fillStyle=hair;g.fillRect(p.x-3,p.y-13,6,4);

g.fillStyle="#fff4cf";g.fillRect(p.x-3,p.y+2,7,2);

}

function drawVehicle(v) {

let p=project(v.x,v.y);

if(p.xW+110||p.yH+90)return;

if(v===player&&invulnerable>0&&Math.floor(elapsed*14)%2===0)

g.globalAlpha=.55;

if(v.type===”bike”) {

poly([local(v,21,0),local(v,0,11),local(v,-21,0),local(v,0,-11)],”#16242a88″);

for(const f of [-13,13]) {

let w=local(v,f,0,3);

g.fillStyle=”#101524″;g.fillRect(w.x-5,w.y-4,10,8);

g.fillStyle=”#97a9b8″;g.fillRect(w.x-2,w.y-2,4,4);

}

poly([local(v,15,-4,6),local(v,15,4,6),

local(v,-14,4,6),local(v,-14,-4,6)],”#51405c”);

let skull=local(v,11,0,8);

g.fillStyle=P.white;g.fillRect(skull.x-2,skull.y-2,4,3);

if(!(jumping&&jumping.who===”charlie”)) {

rider(v.x+Math.cos(v.angle)*4,v.y+Math.sin(v.angle)*4,9,”#272537″,”#342638″);

}

if(!noelleAboard&&!(jumping&&jumping.who===”noelle”)) {

rider(v.x-Math.cos(v.angle)*10,v.y-Math.sin(v.angle)*10,10,P.cyan);

}

g.globalAlpha=1;

return;

}

let len=v.type===”bus”?60:31;

let width=v.type===”bus”?25:18;

let height=v.type===”bus”?20:11;

let corners=[

local(v,-len/2,-width/2),local(v,len/2,-width/2),

local(v,len/2,width/2),local(v,-len/2,width/2)

];

let tops=[

local(v,-len/2,-width/2,height),local(v,len/2,-width/2,height),

local(v,len/2,width/2,height),local(v,-len/2,width/2,height)

];

for(let i=0;i<4;i++) {

let j=(i+1)%4;

poly([corners[i],corners[j],tops[j],tops[i]],v.color);

poly([corners[i],corners[j],tops[j],tops[i]],i%2?"#00000022":"#00000044");

}

poly(tops,v.color);

poly([local(v,len/2-10,-width/2+2,height+1),

local(v,len/2-3,-width/2+2,height+1),

local(v,len/2-3,width/2-2,height+1),

local(v,len/2-10,width/2-2,height+1)],"#4b8599");

for(const side of [-1,1]) {

for(const f of [-len*.3,len*.3]) {

let wheel=local(v,f,side*(width/2+1),3);

g.fillStyle="#121c2c";g.fillRect(wheel.x-4,wheel.y-3,8,6);

}

if(v.type==="bus") {

for(let f=-21;f.2||timing||jumping?”#112133″:”#b98236″;

g.fillRect(door.x-6,door.y-6,12,13);

if(dock>.2||timing||jumping) {

g.fillStyle=P.pink;g.fillRect(door.x-5,door.y-3,3,5);

g.fillStyle=P.cyan;g.fillRect(door.x+3,door.y-3,3,5);

}

}

}

g.globalAlpha=1;

}

function drawJump() {

if(!jumping)return;

let t=clamp(jumping.time/.95,0,1);

let from=rearPosition(46),to=rearPosition(22);

let x=mix(from.x,to.x,t),y=mix(from.y,to.y,t);

rider(x,y,14+Math.sin(t*Math.PI)*38,

jumping.who===”noelle”?P.cyan:”#272537″,

jumping.who===”noelle”?”#f5c277″:”#342638″);

}

function minimap() {

const x=535,y=13,size=92,pad=6,scale=(size-pad*2)/LAST;

g.fillStyle=”#0d192cee”;g.fillRect(x-4,y-4,size+8,size+8);

for(let i=0;i<7;i++) {

g.fillStyle="#576478";

g.fillRect(x+pad+i*GRID*scale,y+pad,1,size-pad*2);

g.fillRect(x+pad,y+pad+i*GRID*scale,size-pad*2,1);

}

function dot(o,color,s=4) {

g.fillStyle=color;

g.fillRect(x+pad+o.x*scale-s/2,y+pad+o.y*scale-s/2,s,s);

}

if(state==="bus") {

g.strokeStyle="#ffce4580";

g.beginPath();

g.moveTo(x+pad+bus.x*scale,y+pad+bus.y*scale);

for(let i=checkpoint;i<route.length;i++)

g.lineTo(x+pad+route[i].x*scale,y+pad+route[i].y*scale);

g.stroke();

if(checkpoint<route.length)dot(route[checkpoint],P.white,6);

}

dot(bus,P.yellow,5);

if(state!=="bus")dot(bike,P.pink,4);

text("CITY GRID",x+13,y+size+12,P.white,8);

}

function targetArrow() {

let target=state==="bus"?route[checkpoint]:bus;

if(!target)return;

let p=project(target.x,target.y,28);

let off=p.xW-35||p.y305;

if(!off)return;

let dx=p.x-320,dy=p.y-195;

let a=Math.atan2(dy,dx);

let px=clamp(p.x,30,610),py=clamp(p.y,122,303);

poly([

{x:px+Math.cos(a)*10,y:py+Math.sin(a)*10},

{x:px+Math.cos(a+2.45)*9,y:py+Math.sin(a+2.45)*9},

{x:px+Math.cos(a-2.45)*9,y:py+Math.sin(a-2.45)*9}

],P.yellow);

text(state===”bus”?”ROUTE”:”BUS”,px-12,py-14,P.yellow,9);

}

function hud() {

g.fillStyle=”#101a2dee”;g.fillRect(10,10,239,71);

text(state===”bus”?”ACT 1 / BEAT THE STOPWATCH”:

noelleAboard?”ACT 2 / CHARLIE’S LAST LEAP”:”ACT 2 / CATCH THAT BUS”,

19,26,P.yellow,10);

text(“TIME”,19,42,P.white,9);

text(Math.max(0,clock).toFixed(1),19,67,clock=.8?”READY! PRESS E TO VAULT”:

“REAR DOORS: ALIGN + MATCH SPEED”,175,293,dock>=.8?P.green:P.white,10);

g.fillStyle=”#3a465c”;g.fillRect(176,301,287,6);

g.fillStyle=dock>=.8?P.green:P.cyan;

g.fillRect(176,301,287*(dock/.8),6);

}

if(timing) {

g.fillStyle=”#101a2df5″;g.fillRect(140,252,360,66);

text(“PRESS E ON THE GREEN BEAT!”,178,268,P.yellow,12);

g.fillStyle=”#ae4c68″;g.fillRect(160,283,320,15);

g.fillStyle=P.green;g.fillRect(160+320*.37,283,320*.26,15);

g.fillStyle=P.white;g.fillRect(158+timing.value*320,278,4,25);

text(noelleAboard?”CHARLIE’S LEAP”:”NOELLE’S STAR-FLYER VAULT”,220,311,P.cyan,9);

}

g.fillStyle=”#101a2df0″;g.fillRect(10,332,620,57);

let fallback=state===”bus”?

“Follow the gold diamonds. Cassettes add time. The squad can absorb three traffic bumps.”:

noelleAboard?

“Noelle is safe. Now line Charlie up behind the bus for one last jump!”:

“Charlie and Noelle are on the motorcycle. Follow the yellow bus on the minimap.”;

wrapped(messageTimer>0?message:fallback,21,350,596,14);

if(paused) {

g.fillStyle=”#0b1429aa”;g.fillRect(0,0,W,H);

text(“PAUSED”,260,185,P.yellow,28);

text(“P / PAUSE TO RESUME”,242,209,P.white,12);

}

}

function draw() {

g.save();

if(shake>0) g.translate(Math.sin(elapsed*96)*3,Math.cos(elapsed*79)*2);

drawGround();

let objects=buildings.map(b=>({

depth:b.x+b.y+(b.w+b.d)*.5,draw:()=>drawBuilding(b)

}));

for(const c of traffic) objects.push({

depth:c.x+c.y,draw:()=>drawVehicle(c)

});

objects.push({depth:bus.x+bus.y,draw:()=>drawVehicle(bus)});

if(state!==”bus”) objects.push({

depth:bike.x+bike.y,draw:()=>drawVehicle(bike)

});

objects.sort((a,b)=>a.depth-b.depth);

objects.forEach(o=>o.draw());

drawJump();

for(const p of particles) {

let s=project(p.x,p.y,Math.max(1,p.z));

g.fillStyle=p.color;g.fillRect(s.x,s.y,3,3);

}

g.restore();

if(flash>0) {

g.fillStyle=`rgba(255,245,220,${flash})`;

g.fillRect(0,0,W,H);

}

hud();

}

/* ———- INPUT / LOOP ———- */

function togglePause() {

if(!active)return;

paused=!paused;

keysClear();

}

window.addEventListener(“keydown”,e=>{

let k=e.key.toLowerCase();

if([“arrowup”,”arrowdown”,”arrowleft”,”arrowright”,” “,”shift”].includes(k))

e.preventDefault();

keys[k]=true;

if(e.repeat)return;

if(k===”e”)vaultAction();

if(k===”p”)togglePause();

if(k===”r”)begin();

if(k===”enter”&&!active)begin();

});

window.addEventListener(“keyup”,e=>keys[e.key.toLowerCase()]=false);

window.addEventListener(“blur”,()=>{

keysClear();

if(active)paused=true;

});

for(const b of document.querySelectorAll(“[data-key]”)) {

let k=b.dataset.key;

b.addEventListener(“pointerdown”,e=>{

e.preventDefault();b.setPointerCapture(e.pointerId);keys[k]=true;

});

b.addEventListener(“pointerup”,()=>keys[k]=false);

b.addEventListener(“pointercancel”,()=>keys[k]=false);

b.addEventListener(“lostpointercapture”,()=>keys[k]=false);

}

document.getElementById(“vaultButton”).onclick=vaultAction;

document.getElementById(“pauseButton”).onclick=togglePause;

document.getElementById(“soundButton”).onclick=()=>{

sound=!sound;

document.getElementById(“soundButton”).textContent=”SOUND: “+(sound?”ON”:”OFF”);

if(sound)beep(660,.1);

};

startButton.onclick=begin;

reset();

let last=performance.now();

function frame(now) {

let dt=Math.min((now-last)/1000,.04);

last=now;

update(dt);

draw();

requestAnimationFrame(frame);

}

requestAnimationFrame(frame);

video game