cartridge.cafe · open source world
MIDNIGHT CIRCUIT — Pinball
Launch a steel ball through a raymarched neon pinball machine. Three balls, glowing bumpers, and a 5x chase.
THE VISION
A raymarched midnight pinball machine seen through an oblique overhead camera. Solid brass rails and articulated gold flippers rise above an etched teal playfield; three cylindrical cyan beacons pulse on impact, and a reflective steel ball catches studio lights. Preserve all pinball rules and controls.
HOW TO PLAY
A / LEFT — left flipper D / RIGHT — right flipper SPACE / ENTER — launch or play again Touch — use the flipper and launch buttons Score bumper hits across three balls. Every eight hits increases the multiplier, up to 5x. A five-second ball save protects each launch. After the third drain, launch to play again.
built by: Codex
2 visual shaders · 0 shader modules · 6 step hooks · runs on WebGPU in the browser
This source is part of the cartridge.cafe commons: readable by anyone, reusable inside other cafe worlds with lineage attribution.
— VISUAL SHADERS (WGSL) —
visual · midnight
fn mc_seg(p:vec2f,a:vec2f,b:vec2f)->f32{let v=b-a;return length(p-a-v*clamp(dot(p-a,v)/dot(v,v),0.0,1.0));}
fn mc_cap(p:vec3f,a:vec2f,b:vec2f,h:f32,r:f32)->f32{return length(vec2f(mc_seg(p.xz,a,b),p.y-h))-r;}
fn mc_map(p:vec3f)->vec2f{
var hit=vec2f(p.y,1.0);
var rail=mc_seg(p.xz,vec2f(122,110),vec2f(122,345));
rail=min(rail,mc_seg(p.xz,vec2f(122,110),vec2f(150,86)));
rail=min(rail,mc_seg(p.xz,vec2f(150,86),vec2f(362,86)));
rail=min(rail,mc_seg(p.xz,vec2f(362,86),vec2f(397,121)));
rail=min(rail,mc_seg(p.xz,vec2f(397,121),vec2f(397,454)));
rail=min(rail,mc_seg(p.xz,vec2f(365,155),vec2f(365,454)));
rail=min(rail,mc_seg(p.xz,vec2f(122,345),vec2f(175,396)));
rail=min(rail,mc_seg(p.xz,vec2f(365,345),vec2f(337,396)));
let rd=length(vec2f(rail,p.y-10.0))-4.0;if(rd<hit.x){hit=vec2f(rd,2);}
var sling=mc_seg(p.xz,vec2f(161,294),vec2f(188,341));
sling=min(sling,mc_seg(p.xz,vec2f(188,341),vec2f(150,331)));
sling=min(sling,mc_seg(p.xz,vec2f(350,294),vec2f(324,341)));
sling=min(sling,mc_seg(p.xz,vec2f(324,341),vec2f(359,331)));
let sd=length(vec2f(sling,p.y-8.0))-4.0;if(sd<hit.x){hit=vec2f(sd,3);}
for(var k=0;k<3;k++){
var b=vec2f(208,174);var r=23.0;if(k==1){b=vec2f(302,174);}if(k==2){b=vec2f(256,239);r=26.0;}
let q=vec2f(length(p.xz-b)-r,abs(p.y-12.0)-10.0);
let d=min(max(q.x,q.y),0.0)+length(max(q,vec2f(0)))-2.0;
if(d<hit.x){hit=vec2f(d,4.0+f32(k));}
}
var f=mc_cap(p,vec2f(175,396),vec2f(236,420-uni(2)*54),7,7);
f=min(f,mc_cap(p,vec2f(337,396),vec2f(276,420-uni(3)*54),7,7));if(f<hit.x){hit=vec2f(f,7);}
let ball=length(p-vec3f(uni(0),7,uni(1)))-7.0;if(ball<hit.x){hit=vec2f(ball,8);}
return hit;
}
fn mc_normal(p:vec3f)->vec3f{
let e=vec2f(.35,-.35);
return normalize(e.xyy*mc_map(p+e.xyy).x+e.yyx*mc_map(p+e.yyx).x+e.yxy*mc_map(p+e.yxy).x+e.xxx*mc_map(p+e.xxx).x);
}
fn visual_midnight(uv:vec2f,sdf:f32,color:vec4f,time:f32,params:vec4f,behind:vec4f)->vec4f{
let screen=(uv+1.0)*256.0;
let box=max(abs(screen.x-256.0)-156.0,abs(screen.y-265.0)-195.0);
var col=vec3f(.006,.012,.021)+vec3f(.02,.05,.055)*exp(-max(box,0.0)*.08);
if(box>5.0){return vec4f(col,1);}
let ro=vec3f(screen.x,72,screen.y+43.2);let ray=normalize(vec3f(0,-1,-.6));
var t=0.0;var mat=1.0;var p=ro;
for(var step=0;step<48;step++){p=ro+ray*t;let h=mc_map(p);mat=h.y;if(h.x<.16||t>86.0){break;}t+=max(.15,h.x*.85);}
let normal=mc_normal(p);let light=normalize(vec3f(-.5,1,-.45));
let diffuse=max(0.0,dot(normal,light));let spec=pow(max(0.0,dot(normal,normalize(light-ray))),48.0);
var base=vec3f(.018,.065,.077);var metal=.2;var emit=vec3f(0);
if(mat<1.5){
let line=min(abs(fract(p.x/24.0)-.5),abs(fract(p.z/24.0)-.5));base+=vec3f(.015,.04,.043)*(1.0-smoothstep(.015,.045,line));
let ring=abs(length(p.xz-vec2f(256,320))-32.0);base=mix(base,vec3f(.38,.22,.085),1.0-smoothstep(.5,1.2,ring));
for(var j=0;j<5;j++){let d=length(p.xz-vec2f(210.0+f32(j)*23.0,116));emit+=vec3f(1,.45,.1)*exp(-d*.32)*select(.12,1.4,f32(j)<uni(9));}
// Broad cyan light pools, with contact shadows beneath raised bumpers.
for(var j=0;j<3;j++){var b=vec2f(208,174);if(j==1){b=vec2f(302,174);}if(j==2){b=vec2f(256,239);}let d=length(p.xz-b);emit+=vec3f(.01,.18,.17)*exp(-d*.035);base*=smoothstep(22.0,40.0,d);}
base*=.55+.45*smoothstep(5.0,17.0,length(p.xz-vec2f(uni(0)+3.0,uni(1)+3.0)));
}else if(mat<2.5){base=vec3f(.65,.30,.07);metal=.8;emit=vec3f(.22,.065,.008);}
else if(mat<3.5){base=vec3f(.025,.45,.42);emit=vec3f(.02,.35,.31);metal=.55;}
else if(mat<6.5){
base=vec3f(.025,.16,.19);metal=.65;
let band=1.0-smoothstep(1.0,2.4,abs(p.y-17.0));emit=vec3f(.05,.8,.68)*band;
if(p.y>22.0){base=vec3f(.12,.31,.34);}
let flash=select(0.0,uni(4),abs(mat-4.0-uni(5))<.2);emit+=vec3f(.13,1,.8)*flash;
}else if(mat<7.5){base=vec3f(.85,.4,.08);metal=.75;}
else{base=vec3f(.34,.43,.48);metal=1.0;}
let refl=reflect(ray,normal);let env=vec3f(.08,.18,.24)+vec3f(.55,.7,.74)*pow(max(refl.y,0.0),6.0);
let strip=pow(max(0.0,1.0-abs(refl.x+.35)*3.0),18.0)*max(refl.y,0.0);
col=base*(.32+.8*diffuse)+spec*vec3f(1,.82,.6)*metal+env*metal*.35+strip*metal*.6+emit;
let edge=1.0-smoothstep(-2.0,3.0,box);col=mix(vec3f(.045,.04,.03),col,edge);
return vec4f(col,1);
}
visual · base_bg
fn visual_base_bg(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let g = uv.y * 0.5 + 0.5;
var col = mix(vec3f(0.075, 0.085, 0.115), vec3f(0.028, 0.033, 0.05), g);
col += vec3f(0.05, 0.06, 0.09) * (0.5 + 0.5 * sin(uv.x * 2.0 + uv.y * 3.0)) * 0.06;
col += 0.010 * (vnoise(uv * 34.0) - 0.5);
// BLANK: no horizon line (Galen, Aug 31: "mobile base has a blue line — we need
// a blank"). The old soft floor line at uv.y≈0.35 was baked into every born
// world's backdrop and read as a stray blue line; a blank base is a clean
// neutral ground the builder paints over, nothing to remove first.
return vec4f(col, 1.0);
}— STEP HOOKS (JAVASCRIPT) —
hook · player
by claude-code
const wd=sim.worldData,inp=wd.input||{},h=inp.held||{};
wd.__resets=['__pin','__pinInput','__pinKeys'];
if(!wd.__pinKeys)wd.__pinKeys={a:0,d:0,left:0,right:0,space:0,enter:0,lt:0,rt:0};
const k=wd.__pinKeys;let launch=!!inp.action;
for(const key of ['a','d','left','right','space','enter']){const n=Number(wd['key_'+key+'_n']||0);if(n>k[key]){if(key==='a'||key==='left')k.lt=.10;else if(key==='d'||key==='right')k.rt=.10;else launch=true;}k[key]=n;}
k.lt=Math.max(0,k.lt-dt);k.rt=Math.max(0,k.rt-dt);
wd.__pinInput={left:!!(h.a||h.left||wd.key_a||wd.key_left||k.lt>0),right:!!(h.d||h.right||wd.key_d||wd.key_right||k.rt>0),launch};
hook · world
by claude-code
const wd=sim.worldData;
if(!wd.__pin)wd.__pin={x:382,y:425,vx:0,vy:0,score:0,ball:1,mode:0,left:0,right:0,hits:0,mult:1,flash:0,hitId:-1,age:0,save:0,lastUi:0};
const g=wd.__pin,i=wd.__pinInput||{};
let launch=i.launch;
if(wd.__uiClick==='launch'&&wd.__uiClickT!==g.lastUi){g.lastUi=wd.__uiClickT;launch=true;}
if(launch&&g.mode===2){Object.assign(g,{score:0,ball:1,mode:0,hits:0,mult:1,x:382,y:425,vx:0,vy:0});}
if(launch&&g.mode===0){g.mode=1;g.vy=-740;g.vx=0;g.age=0;g.save=5;g.inLane=true;}
g.left+=(Number(!!i.left)-g.left)*Math.min(1,dt*25);
g.right+=(Number(!!i.right)-g.right)*Math.min(1,dt*25);
g.flash=Math.max(0,g.flash-dt*3);g.age+=dt;g.save=Math.max(0,g.save-dt);
if(g.mode!==1)return;
const n=Math.max(1,Math.ceil(Math.min(dt,0.04)*240)),s=Math.min(dt,0.04)/n;
const seg=(ax,ay,bx,by,bounce,kick=0)=>{
const dx=bx-ax,dy=by-ay,t=Math.max(0,Math.min(1,((g.x-ax)*dx+(g.y-ay)*dy)/(dx*dx+dy*dy)));
const ex=g.x-ax-t*dx,ey=g.y-ay-t*dy,d=Math.hypot(ex,ey),r=11;
if(d<r&&d>0.001){const nx=ex/d,ny=ey/d;g.x+=nx*(r-d);g.y+=ny*(r-d);const v=g.vx*nx+g.vy*ny;
if(v<0){g.vx-=(1+bounce)*v*nx;g.vy-=(1+bounce)*v*ny;if(kick){g.vy=-Math.max(400,Math.abs(g.vy)+kick);g.vx+=(256-g.x)*2.5;}}}
};
for(let z=0;z<n;z++){
g.vy+=410*s;g.vx*=Math.exp(-0.06*s);g.x+=g.vx*s;g.y+=g.vy*s;
if(g.inLane&&g.y<125){g.inLane=false;g.vx=-270;g.vy=-110;}
seg(122,110,122,345,.8);seg(122,110,150,86,.8);seg(150,86,362,86,.85);seg(362,86,397,121,.8);seg(397,121,397,454,.8);seg(365,155,365,454,.7);
seg(122,345,175,396,.85);seg(365,345,337,396,.85);
seg(161,294,188,341,1.05);seg(188,341,150,331,1.05);seg(350,294,324,341,1.05);seg(324,341,359,331,1.05);
const bumps=[[208,174,23],[302,174,23],[256,239,26]];
for(let k=0;k<3;k++){const [bx,by,br]=bumps[k],dx=g.x-bx,dy=g.y-by,d=Math.hypot(dx,dy);
if(d<br+7&&d>.01){const nx=dx/d,ny=dy/d;g.x=bx+nx*(br+7+.2);g.y=by+ny*(br+7+.2);const v=g.vx*nx+g.vy*ny;
if(v<0){g.vx-=2*v*nx;g.vy-=2*v*ny;g.vx+=nx*105;g.vy+=ny*105;g.score+=100*g.mult;g.hits++;g.hitId=k;g.flash=1;wd.__play_sound={frequency:520+k*180,duration:.10,volume:.18,type:'sine'};}}}
seg(175,396,236,420-g.left*54,.72,i.left?230:0);
seg(337,396,276,420-g.right*54,.72,i.right?230:0);
const speed=Math.hypot(g.vx,g.vy);if(speed>820){g.vx*=820/speed;g.vy*=820/speed;}
if(g.y>468){if(g.save>0){g.x=382;g.y=425;g.vx=0;g.vy=-740;g.inLane=true;}else{g.ball++;g.mode=g.ball>3?2:0;g.x=382;g.y=425;g.vx=0;g.vy=0;}break;}
}
hook · entities
everything that lives in the world — spawns, rosters, behavior
// ── ENTITIES — this node owns every non-player thing that moves or
// lives: spawn tables, rosters, per-entity behavior ticks. Give each
// KIND its own SUB-NODE rather than one blob: add_step_hook
// {"hookId":"entities:wolves"} — each independently held and healed.
hook · rules
by claude-code
const wd=sim.worldData,g=wd.__pin;if(!g)return;
g.mult=Math.min(5,1+Math.floor(g.hits/8));
wd.gpuUniforms=[g.x,g.y,g.left,g.right,g.flash,g.hitId,g.mode,g.age,g.save,g.mult];
hook · hud
by claude-code
const wd=sim.worldData,g=wd.__pin;if(!g)return;
wd.ui={rev:1,root:[
{id:'score',kind:'panel',anchor:{gx:256,gy:8},align:'tc',w:284,pad:8,children:[{kind:'text',text:'MIDNIGHT CIRCUIT',fontSize:15,color:'#ffd59a'},{kind:'text',text:String(g.score).padStart(6,'0')+' BALL '+Math.min(g.ball,3)+'/3 '+g.mult+'X',fontSize:14,color:'#78f2ed'}]},
{id:'controls',kind:'panel',anchor:{gx:256,gy:500},align:'bc',w:310,pad:5,children:[{kind:'text',text:g.mode===2?'GAME OVER - PLAY AGAIN':g.mode===0?'LAUNCH TO PLAY':g.save>0?'BALL SAVE ACTIVE':'8 BUMPER HITS = MULTIPLIER UP',fontSize:10,color:'#ffd59a'},{kind:'row',gap:8,children:[{id:'left',kind:'button',text:'A / LEFT',click:'key:a'},{id:'launch',kind:'button',text:g.mode===2?'AGAIN':'LAUNCH',click:'launch'},{id:'right',kind:'button',text:'D / RIGHT',click:'key:d'}]}]}]};
hook · net
shared state — what this world syncs when crews play together
// ── NET — this node owns SHARED STATE: when this world goes
// multiplayer (arena), what state is authoritative-server truth vs
// local cosmetic? Declare the world's mpManifest here as it grows.
// Until arena wiring lands this node idles — but it EXISTS, so the
// day the world goes shared is a hot-swap, not a rebuild.