cartridge.cafe · open source world

NOCTURNE DISTRICT

HOW TO PLAY

NOCTURNE DISTRICT — the city as a pinball table CLICK & HOLD — draw the ball to your hand · it grabs when it arrives DRAG & RELEASE — a real throw W / S — raise and lower the ball Red interceptors run the street at you. Smash them with a FAST ball: dull-red drifters weave · crimson dodgers juke your throws · blazing hunters burn for the line A slow held ball in traffic gets RAMMED out of your hand. Three hearts. Every ship that slips past costs one; three gone resets the night. Clear ten ships to end a wave — a heart comes back, and the street gets meaner.

1 visual shader · 0 shader modules · 2 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 · nc_city


const NC_CELL: f32 = 9.0;

struct NCHit { t: f32, kind: i32, n: vec3f, cell: vec2f };

fn nc_mode() -> f32 { return uni(15); }   // 0 endless · 1 district

fn nc_isStreet(cell: vec2f) -> bool {
  if (nc_mode() > 0.5) {                  // district: finite table, open beyond
    if (cell.x < -5.5 || cell.x > 4.5 || cell.y < -5.5 || cell.y > 4.5) { return true; }
  }
  if (cell.x == 0.0) { return true; }
  if (abs(cell.y - 5.0 * round(cell.y / 5.0)) < 0.5) { return true; }
  return false;
}

fn nc_dims(cell: vec2f) -> vec3f {
  let r = hash22(cell);
  let h  = 6.0 + r.x * 40.0;
  let bw = NC_CELL * 0.5 * (0.72 + r.y * 0.20);
  let bd = NC_CELL * 0.5 * (0.72 + fract(r.y * 7.31 + r.x) * 0.20);
  return vec3f(h, bw, bd);
}

fn nc_ball() -> vec3f { return vec3f(uni(1), uni(2), uni(3)); }

fn nc_sphereT(ro: vec3f, rd: vec3f, c: vec3f, r: f32) -> f32 {
  let oc = ro - c;
  let b = dot(oc, rd);
  let cc = dot(oc, oc) - r * r;
  let h = b * b - cc;
  if (h < 0.0) { return -1.0; }
  return -b - sqrt(h);
}

fn nc_traceCity(ro: vec3f, rd: vec3f, maxd: f32, maxCells: i32) -> NCHit {
  var best: NCHit;
  best.t = maxd; best.kind = -1; best.n = vec3f(0.0, 1.0, 0.0); best.cell = vec2f(0.0, 0.0);

  if (rd.y < -1.0e-4) {
    let tg = -ro.y / rd.y;
    if (tg > 0.0 && tg < best.t) { best.t = tg; best.kind = 0; best.n = vec3f(0.0, 1.0, 0.0); }
  }

  var cell = floor(vec2f(ro.x, ro.z) / NC_CELL);
  let stepDir = sign(vec2f(rd.x, rd.z));
  let cellMin = cell * NC_CELL;

  var tMaxX = 1.0e9;  var tMaxZ = 1.0e9;
  var tDeltaX = 1.0e9; var tDeltaZ = 1.0e9;
  if (abs(rd.x) > 1.0e-6) {
    let bx = select(cellMin.x, cellMin.x + NC_CELL, rd.x > 0.0);
    tMaxX = (bx - ro.x) / rd.x;   tDeltaX = NC_CELL / abs(rd.x);
  }
  if (abs(rd.z) > 1.0e-6) {
    let bz = select(cellMin.y, cellMin.y + NC_CELL, rd.z > 0.0);
    tMaxZ = (bz - ro.z) / rd.z;   tDeltaZ = NC_CELL / abs(rd.z);
  }

  var tEnter = 0.0;
  for (var k = 0; k < maxCells; k = k + 1) {
    if (!nc_isStreet(cell)) {
      let dm = nc_dims(cell);
      let c = vec3f((cell.x + 0.5) * NC_CELL, dm.x * 0.5, (cell.y + 0.5) * NC_CELL);
      let half = vec3f(dm.y, dm.x * 0.5, dm.z);
      let m = 1.0 / rd;
      let nrm = m * (ro - c);
      let kk = abs(m) * half;
      let t1 = -nrm - kk;
      let t2 = -nrm + kk;
      let tN = max(max(t1.x, t1.y), t1.z);
      let tF = min(min(t2.x, t2.y), t2.z);
      if (tF >= tN && tF > 0.0) {
        let th = select(tN, 0.001, tN < 0.0);
        if (th < best.t) {
          best.t = th; best.kind = 1; best.cell = cell;
          best.n = -sign(rd) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz);
        }
      }
    }
    if (tMaxX < tMaxZ) { tEnter = tMaxX; tMaxX = tMaxX + tDeltaX; cell.x = cell.x + stepDir.x; }
    else              { tEnter = tMaxZ; tMaxZ = tMaxZ + tDeltaZ; cell.y = cell.y + stepDir.y; }
    if (tEnter > best.t || tEnter > maxd) { break; }
  }
  return best;
}

fn nc_sky(rd: vec3f) -> vec3f {
  let y = max(rd.y, 0.0);
  var c = mix(vec3f(0.050, 0.060, 0.110), vec3f(0.008, 0.012, 0.035), pow(y, 0.5));
  c += vec3f(0.28, 0.13, 0.05) * exp(-max(rd.y, 0.0) * 5.5);
  let md = normalize(vec3f(-0.45, 0.42, 1.0));
  let m = max(dot(rd, md), 0.0);
  c += vec3f(0.90, 0.92, 0.98) * pow(m, 3000.0) * 3.0;
  c += vec3f(0.30, 0.34, 0.44) * pow(m, 30.0) * 0.35;
  if (rd.y > 0.05) {
    let sp = rd.xz / (rd.y + 0.2);
    let stars = hash21(floor(sp * 40.0));
    c += vec3f(step(0.995, stars)) * 0.8 * smoothstep(0.05, 0.30, rd.y);
  }
  return c;
}

fn nc_facade(p: vec3f, n: vec3f, cell: vec2f) -> vec3f {
  let baseCol = vec3f(0.018, 0.022, 0.035);
  if (abs(n.y) > 0.55) { return vec3f(0.012, 0.016, 0.026); }
  var u = p.z;
  if (abs(n.z) > abs(n.x)) { u = p.x; }
  let WU = 1.05;  let WV = 1.35;
  let cu = floor(u / WU);
  let cv = floor((p.y - 0.6) / WV);
  let fu = fract(u / WU);
  let fv = fract((p.y - 0.6) / WV);
  let inWin = step(0.18, fu) * step(fu, 0.82) * step(0.20, fv) * step(fv, 0.86) * step(0.6, p.y);
  let r = hash21(cell * 7.0 + vec2f(cu * 1.3, cv * 2.1));
  let lit = step(0.48, r);
  let cyan = step(0.86, hash21(cell + vec2f(cu, cv) * 0.7));
  let wc = mix(vec3f(1.70, 1.20, 0.62), vec3f(0.45, 1.50, 1.80), cyan);
  return baseCol + wc * lit * inWin * 2.4;
}

fn nc_ballBody(flash: f32) -> vec3f {
  return mix(vec3f(3.0, 1.7, 0.8), vec3f(7.0, 5.5, 4.0), flash);
}

fn nc_ballGlow(ro: vec3f, rd: vec3f, ball: vec3f, sceneT: f32, flash: f32) -> vec3f {
  let oc = ball - ro;
  let tca = dot(oc, rd);
  if (tca < 0.0) { return vec3f(0.0); }
  let d2x = dot(oc, oc) - tca * tca;
  // occluded: a faint pulse bleeds through the towers — the toy is never lost
  if (tca > sceneT + 2.0) { return vec3f(1.3, 0.55, 0.28) * exp(-d2x * 0.030) * (0.12 + flash * 0.3); }
  let d2 = dot(oc, oc) - tca * tca;
  let trail = ball - normalize(vec3f(uni(5), uni(6), uni(7)) + vec3f(1.0e-5)) * 2.4;
  let oc2 = trail - ro;
  let tca2 = dot(oc2, rd);
  var g = exp(-d2 * 0.10) * (0.8 + flash * 1.6);
  if (tca2 > 0.0 && tca2 < sceneT + 2.0) {
    g += exp(-(dot(oc2, oc2) - tca2 * tca2) * 0.16) * 0.30;
  }
  return vec3f(1.7, 1.0, 0.5) * g;
}

fn nc_reflColor(ro: vec3f, rd: vec3f, ball: vec3f, flash: f32) -> array<vec3f, 2> {
  let hb = nc_traceCity(ro, rd, 110.0, 20);
  var sceneT = hb.t;
  var c: vec3f;
  let tc = nc_sphereT(ro, rd, ball, 1.3);
  if (tc > 0.0 && tc < sceneT) {
    c = nc_ballBody(flash);
    sceneT = tc;
  } else if (hb.kind == 1) {
    let p = ro + rd * hb.t;
    c = mix(nc_facade(p, hb.n, hb.cell), nc_sky(rd), 1.0 - exp(-hb.t * 0.02));
  } else if (hb.kind == 0) {
    c = vec3f(0.02, 0.022, 0.03);
  } else {
    c = nc_sky(rd);
  }
  return array<vec3f, 2>(c, nc_ballGlow(ro, rd, ball, sceneT, flash));
}

fn nc_rotY(p: vec3f, a: f32) -> vec3f {
  let cc = cos(a); let ss = sin(a);
  return vec3f(cc * p.x + ss * p.z, p.y, -ss * p.x + cc * p.z);
}
fn nc_rotZ(p: vec3f, a: f32) -> vec3f {
  let cc = cos(a); let ss = sin(a);
  return vec3f(cc * p.x - ss * p.y, ss * p.x + cc * p.y, p.z);
}
fn nc_shipSdf(q: vec3f) -> f32 {
  // hull: a blade — sharp nose forward (-z), raked tail
  let hull = (abs(q.x) * 1.5 + abs(q.y) * 2.6 + abs(q.z + 0.15) * 0.42) - 0.62;
  var wq = q; wq.x = abs(wq.x);
  let wing = max(max(wq.x - 1.5, abs(wq.y + 0.06 + wq.x * 0.10) - 0.045),
                 abs(wq.z - 0.45 - wq.x * 0.85) - max(0.5 - wq.x * 0.28, 0.05));
  let fin = max(max(abs(q.x) - 0.05, abs(q.y - 0.35) - 0.34 + (q.z - 0.9) * 0.3), abs(q.z - 0.85) - 0.42);
  return min(hull, min(wing, fin));
}
fn nc_shipHit(ro: vec3f, rd: vec3f, e: vec3f, yaw: f32, bank: f32, tmax: f32) -> f32 {
  let oc = e - ro;
  let tca = dot(oc, rd);
  if (tca < 0.0 || tca > tmax + 3.0) { return -1.0; }
  let d2 = dot(oc, oc) - tca * tca;
  let R = 2.1;
  if (d2 > R * R) { return -1.0; }
  let thc = sqrt(R * R - d2);
  var tt = max(tca - thc, 0.0);
  let tEnd = tca + thc;
  for (var st = 0; st < 22; st++) {
    if (tt > tEnd || tt > tmax) { break; }
    let q = nc_rotZ(nc_rotY(ro + rd * tt - e, yaw), bank);
    let dd = nc_shipSdf(q);
    if (dd < 0.02) { return tt; }
    tt += max(dd * 0.85, 0.015);
  }
  return -1.0;
}
fn visual_nc_city(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
  let p = vec2f(uv.x, -uv.y);
  let t = time;
  let district = nc_mode() > 0.5;

  var camZ = uni(0);
  if (camZ < 0.001) { camZ = t * 5.0; }

  var ro: vec3f;
  var look: vec3f;
  if (district) {                          // street level, dollying down the path
    ro = vec3f(4.5, 5.6, uni(26));         // the hook publishes the dolly (uni 26)
    look = normalize(vec3f(0.0, -0.10, 1.0));   // hand math mirrors this exactly
  } else {                                 // the drive: low, forward, swaying
    ro = vec3f(4.5 + sin(t * 0.08) * 1.2, 5.2 + sin(t * 0.20) * 0.30, camZ);
    look = normalize(vec3f(sin(t * 0.05) * 0.12, -0.12, 1.0));
  }
  let rt = normalize(cross(vec3f(0.0, 1.0, 0.0), look));
  let up = cross(look, rt);
  var rd = normalize(rt * p.x + up * (p.y * 0.72) + look * 1.5);
  rd = normalize(rd + vec3f(1.0e-4));

  var ball = nc_ball();
  if (ball.y < 0.01) { ball = vec3f(4.5, 2.0, camZ + 20.0); }
  let flash = uni(4);
  let horizon = nc_sky(vec3f(rd.x, max(rd.y, 0.02), rd.z));
  var col = nc_sky(rd);

  let hit = nc_traceCity(ro, rd, 220.0, 44);
  var hitT = hit.t;
  var kind = hit.kind;

  let tc = nc_sphereT(ro, rd, ball, 1.3);
  if (tc > 0.0 && tc < hitT) { hitT = tc; kind = 2; }

  // interceptors — ships, not dots (pos uni 16.. · pose uni 28..)
  var eHit = -1;
  if (district) {
    for (var ei = 0; ei < 3; ei++) {
      let e = vec3f(uni(16 + ei * 3), uni(17 + ei * 3), uni(18 + ei * 3));
      if (e.y < 0.01) { continue; }
      let te = nc_shipHit(ro, rd, e, uni(28 + ei * 3), uni(29 + ei * 3), hitT);
      if (te > 0.0 && te < hitT) { hitT = te; kind = 4; eHit = ei; }
    }
  }

  if (kind == 4) {
    let e = vec3f(uni(16 + eHit * 3), uni(17 + eHit * 3), uni(18 + eHit * 3));
    let cf = uni(30 + eHit * 3);
    let cls = i32(floor(cf));
    let flare = fract(cf);
    let q = nc_rotZ(nc_rotY(ro + rd * hitT - e, uni(28 + eHit * 3)), uni(29 + eHit * 3));
    var trim = vec3f(2.0, 0.32, 0.16);
    if (cls == 1) { trim = vec3f(2.6, 0.12, 0.10); }
    if (cls == 2) { trim = vec3f(3.2, 0.55, 0.28); }
    let e2 = vec2f(0.03, 0.0);
    let nn = normalize(vec3f(
      nc_shipSdf(q + e2.xyy) - nc_shipSdf(q - e2.xyy),
      nc_shipSdf(q + e2.yxy) - nc_shipSdf(q - e2.yxy),
      nc_shipSdf(q + e2.yyx) - nc_shipSdf(q - e2.yyx)));
    let rim = pow(1.0 - abs(dot(nn, rd)), 2.0);
    var sc = vec3f(0.015, 0.02, 0.03) + trim * rim * 0.65;
    sc += trim * exp(-length(q - vec3f(0.0, 0.0, 0.95)) * 3.2) * (1.8 + flare * 4.0);
    sc += trim * smoothstep(0.06, 0.0, abs(abs(q.x) - 1.05)) * smoothstep(0.5, 0.1, abs(q.z - 0.3)) * 0.8;
    let blink = smoothstep(0.92, 1.0, sin(t * 5.0 + f32(eHit) * 2.3) * 0.5 + 0.5);
    sc += vec3f(2.0) * blink * exp(-length(q - vec3f(0.0, 0.62, 0.9)) * 6.0);
    col = sc;
  } else if (kind == 2) {
    let hp = ro + rd * hitT;
    let nn = normalize(hp - ball);
    let rim = pow(1.0 - max(dot(nn, -rd), 0.0), 2.0);
    var bc = nc_ballBody(flash) + vec3f(1.2, 0.6, 0.25) * rim;
    // too close to the eye, the ball turns to glass — it must never blind you
    let fade = smoothstep(11.0, 4.5, hitT);
    if (fade > 0.01) {
      let ro2 = ro + rd * (hitT + 2.8);
      let hit2 = nc_traceCity(ro2, rd, 200.0, 32);
      var back = nc_sky(rd);
      if (hit2.kind == 1) {
        let hp2 = ro2 + rd * hit2.t;
        back = nc_facade(hp2, hit2.n, hit2.cell);
        back = mix(back, horizon, 1.0 - exp(-(hitT + 2.8 + hit2.t) * 0.014));
      } else if (hit2.kind == 0) {
        back = mix(vec3f(0.015, 0.017, 0.024), horizon, 0.5);
      }
      bc = mix(bc, back, fade * 0.8);
      bc += vec3f(0.9, 0.5, 0.2) * rim * 0.35 * fade;   // a warm glass edge stays
    }
    col = bc;
  } else if (kind == 1) {
    let hp = ro + rd * hitT;
    var mcol = nc_facade(hp, hit.n, hit.cell);
    mcol += nc_sky(hit.n) * 0.04;
    col = mix(mcol, horizon, 1.0 - exp(-hitT * 0.014));
  } else if (kind == 0) {
    let hp = ro + rd * hitT;
    let rip = vnoise(hp.xz * 0.8 + vec2f(0.0, t * 0.3)) - 0.5;
    let rn = normalize(vec3f(rip * 0.08, 1.0, rip * 0.08));
    let rrd = normalize(reflect(rd, rn) + vec3f(1.0e-4));
    let refl = nc_reflColor(hp + rn * 0.05, rrd, ball, flash);
    let fres = pow(1.0 - max(-rd.y, 0.0), 4.0);
    var road = mix(vec3f(0.015, 0.017, 0.024), refl[0], clamp(fres * 0.7 + 0.15, 0.0, 1.0));
    road += refl[1] * 0.65;
    col = mix(road, horizon, 1.0 - exp(-hitT * 0.014));
  }

  col += nc_ballGlow(ro, rd, ball, hitT, flash);

  if (district) {
    // enemy glow + the red sting when one gets through
    for (var gi = 0; gi < 3; gi++) {
      let e = vec3f(uni(16 + gi * 3), uni(17 + gi * 3), uni(18 + gi * 3));
      if (e.y < 0.01) { continue; }
      let oc2 = e - ro;
      let tca = dot(oc2, rd);
      if (tca > 0.0 && tca < hitT + 3.0) {
        let dsq = max(dot(oc2, oc2) - tca * tca, 0.0);
        let cf2 = uni(30 + gi * 3);
        var tr2 = vec3f(2.0, 0.32, 0.16);
        if (i32(floor(cf2)) == 1) { tr2 = vec3f(2.6, 0.12, 0.10); }
        if (i32(floor(cf2)) == 2) { tr2 = vec3f(3.2, 0.55, 0.28); }
        col += tr2 * exp(-dsq * 0.55) * (0.22 + fract(cf2) * 0.5);
      }
    }
    // a ship dies loud: an expanding shell in its own color
    let bT = uni(40);
    if (bT > 0.001) {
      let bC = vec3f(uni(37), uni(38), uni(39));
      var tr3 = vec3f(2.0, 0.32, 0.16);
      if (i32(uni(41) + 0.5) == 1) { tr3 = vec3f(2.6, 0.12, 0.10); }
      if (i32(uni(41) + 0.5) == 2) { tr3 = vec3f(3.2, 0.55, 0.28); }
      let oc3 = bC - ro;
      let tca3 = dot(oc3, rd);
      if (tca3 > 0.0 && tca3 < hitT + 6.0) {
        let dsq3 = max(dot(oc3, oc3) - tca3 * tca3, 0.0);
        let shell = exp(-pow((sqrt(dsq3) - bT * 9.0) * 1.4, 2.0));
        col += tr3 * shell * (1.0 - bT) * 2.4;
        col += vec3f(3.0, 2.4, 1.6) * exp(-dsq3 * 2.0) * max(1.0 - bT * 3.0, 0.0) * 3.0;
      }
    }
    // proximity alarm breathes at the bottom of the glass
    col += vec3f(1.6, 0.12, 0.10) * uni(27) * pow(max(-p.y, 0.0), 2.0) * (0.6 + 0.4 * sin(t * 9.0));
    col += vec3f(1.4, 0.06, 0.04) * uni(25) * pow(length(p), 2.0) * 0.8;
  }

  return vec4f(col, 1.0);
}

— STEP HOOKS (JAVASCRIPT) —

hook · nc_core

Nocturne core — ROLLBACK ball watchdog (was re-centering ball to mid-screen); original ball physics restored + wd.ui glass hearts (meter pips) kept

try {
  if (sim.worldData.__fresh) { delete sim.worldData.__fresh; delete sim.worldData.__ncB }
  if (sim.worldData.__ncReset) { delete sim.worldData.__ncReset; delete sim.worldData.__ncB }
  const wd = sim.worldData
  if (!wd.__ncB || wd.__ncB.v !== 4) wd.__ncB = { v: 4, t: 0, alt: 3, valt: 0, flash: 0, red: 0, bounces: 0, kills: 0, through: 0, pvx: 0, pvy: 0, enemies: [null, null, null], spawnT: 2 }
  const S = wd.__ncB
  const pdt = Math.min(dt, 0.05)
  const SND = []   // every voice this tick, flushed once at the end
  for (const f of sim.fields.values()) {
    const P = f.properties
    const hx = P && (P.get ? P.get('hx') : P.hx)
    if (typeof hx === 'number') {
      f.transform.x = hx; f.transform.y = P.get ? P.get('hy') : P.hy
      f.transform.vx = 0; f.transform.vy = 0
    }
  }

  const ball = sim.fields.get('nc_ball_f')
  if (ball) {
    const T = ball.transform
    if (!S.phys) S.phys = { x: 274, y: 320, vx: 30, vy: 20 }
    const TB = S.phys
    S.t += pdt
    S.ramCd = Math.max(0, (S.ramCd || 0) - pdt)

    // ── the dolly: down the street, then a slow pull back (150s round trip).
    //    One clock; the GPU reads it from uni(26) so eye and hand agree. ──
    const cyc = (S.t % 150) / 150
    const tri = cyc < 0.5 ? cyc * 2 : 2 - cyc * 2
    const camWZ = -55 + 100 * tri
    const camGY = camWZ * 4 + 256

    const RO = [4.5, 5.6, camWZ], LOOK = [0, -0.09950, 0.99504], RT = [1, 0, 0], UP = [0, 0.99504, 0.09950]
    const bh = S.alt + 1.3
    const gx0 = wd.mouse_x, gy0 = wd.mouse_y
    const downEdge = wd.mouse_down && !S.mHeld
    S.mHeld = wd.mouse_down ? 1 : 0
    // ── holding far ATTRACTS the ball; once it reaches your hand it is GRABBED
    //    and rides the cursor — release is a real throw ──
    let hand = false
    if (wd.mouse_down && typeof gx0 === 'number') {
      const p2x = (gx0 / 256) - 1, p2y = 1 - (gy0 / 256)
      const rd3 = [RT[0]*p2x + UP[0]*p2y*0.72 + LOOK[0]*1.5,
                   RT[1]*p2x + UP[1]*p2y*0.72 + LOOK[1]*1.5,
                   RT[2]*p2x + UP[2]*p2y*0.72 + LOOK[2]*1.5]
      if (S.handBh === undefined) { S.handBh = Math.min(bh, 4.6) }
      const tt2 = (S.handBh - RO[1]) / rd3[1]
      if (tt2 > 0) {
        hand = true
        if (downEdge) SND.push({ frequency: 420, duration: 0.06, volume: 0.25, type: 'sine' })
        const px2 = Math.max(255, Math.min(293, (RO[0] + rd3[0]*tt2) * 4 + 256))
        const py2 = Math.max(camGY + 8, Math.min(486, (RO[2] + rd3[2]*tt2) * 4 + 256))
        const dx = px2 - TB.x, dy = py2 - TB.y
        const dd = Math.hypot(dx, dy) || 1
        const cvx = (px2 - (S.hpx ?? px2)) / Math.max(pdt, 1e-4)
        const cvy = (py2 - (S.hpy ?? py2)) / Math.max(pdt, 1e-4)
        S.cvx = (S.cvx || 0) * 0.6 + cvx * 0.4
        S.cvy = (S.cvy || 0) * 0.6 + cvy * 0.4
        S.hpx = px2; S.hpy = py2
        if ((S.ramCd || 0) <= 0 && (S.held || dd < 30)) {
          S.held = 1
          TB.x += (px2 - TB.x) * Math.min(1, pdt * 16)
          TB.y += (py2 - TB.y) * Math.min(1, pdt * 16)
          TB.vx = Math.max(-380, Math.min(380, S.cvx))
          TB.vy = Math.max(-380, Math.min(380, S.cvy))
        } else {
          const pull = 1500 * Math.min(1, dd / 110)
          TB.vx += (dx / dd) * pull * pdt
          TB.vy += (dy / dd) * pull * pdt
          TB.vx *= (1 - 1.7 * pdt); TB.vy *= (1 - 1.7 * pdt)
        }
        S.flash = Math.max(S.flash, 0.2)
      }
    } else if (S.mWas) {
      SND.push({ frequency: 300, duration: 0.08, volume: 0.25, type: 'triangle' })
      if (S.held && Math.hypot(TB.vx, TB.vy) > 90) S.free = 1
      S.held = 0; S.hpx = undefined; S.hpy = undefined; S.cvx = 0; S.cvy = 0; S.handBh = undefined
    }
    S.mWas = wd.mouse_down ? 1 : 0
    {
      const sp2 = Math.hypot(TB.vx, TB.vy)
      if (sp2 > 380) { TB.vx *= 380 / sp2; TB.vy *= 380 / sp2 }
    }
    const grabbed = hand
    const dvx = TB.vx - S.pvx, dvy = TB.vy - S.pvy
    if (dvx * dvx + dvy * dvy > 900) { S.flash = 1; S.bounces++ }

    if (!grabbed) {
    if (S.free) {
      // thrown: momentum is yours; air and walls take it back gradually
      TB.vx *= (1 - 0.22 * pdt); TB.vy *= (1 - 0.22 * pdt)
      if (Math.hypot(TB.vx, TB.vy) < 70) S.free = 0
    } else {
    // leash: a soft spring toward the street ahead of the camera
    TB.vx += ((274) - TB.x) * 0.9 * pdt + Math.sin(S.t * 0.9) * 6 * pdt
    TB.vy += ((camGY + 60) - TB.y) * 0.35 * pdt
    const sp = Math.hypot(TB.vx, TB.vy)
    if (sp > 1 && sp < 45) { const k = 60 / sp; TB.vx *= k; TB.vy *= k }
    if (sp >= 110) { const k = 110 / sp; TB.vx *= k; TB.vy *= k }
    if (sp <= 1) { TB.vx = 60; TB.vy = 40 }
    }

    }
    // ── altitude: W lifts, S sinks — your keys rule the sky, gravity rules the rest ──
    const lift = (wd.key_w ? 1 : 0) - (wd.key_s ? 1 : 0)
    if (lift !== 0) { S.valt += lift * 30 * pdt; S.pilot = 1.0 }
    S.pilot = Math.max(0, (S.pilot || 0) - pdt)
    if (S.pilot > 0 || grabbed) {
      S.valt *= Math.exp(-2.5 * pdt)   // a steady hand hovers
    } else {
      S.valt -= 9 * pdt
      if (S.alt < 0.1 && Math.abs(S.valt) < 2.5) S.valt = 7
    }
    S.alt += S.valt * pdt
    if (S.alt < 0) { S.alt = 0; S.valt = Math.abs(S.valt) * 0.9; S.flash = Math.max(S.flash, 0.7) }
    if (S.alt > 5) { S.alt = 5; S.valt = Math.min(S.valt, 0) }
    S.pvx = TB.vx; S.pvy = TB.vy
    S.flash *= Math.exp(-4 * pdt)

    // integrate
    TB.x += TB.vx * pdt
    TB.y += TB.vy * pdt
    // ── the street holds the ball: canyon walls left/right, view range ahead ──
    const rest = S.free ? 0.75 : 1
    if (TB.x < 257) { TB.x = 257; TB.vx = Math.abs(TB.vx) * rest }
    if (TB.x > 291) { TB.x = 291; TB.vx = -Math.abs(TB.vx) * rest }
    const zlo = camGY + 8, zhi = Math.min(camGY + 175, 486)
    if (TB.y < zlo) { TB.y = zlo; TB.vy = Math.abs(TB.vy) * rest }
    if (TB.y > zhi) { TB.y = zhi; TB.vy = -Math.abs(TB.vy) * rest }
    // towers stay solid
    {
      const RB = 6
      for (const f of sim.fields.values()) {
        const P = f.properties
        const hx2 = P && (P.get ? P.get('hx') : P.hx)
        if (typeof hx2 !== 'number' || f.id === 'nc_ball_f' || f.id === 'nc_city_f') continue
        const hw = f.w / 2, hh3 = f.h / 2
        const qx = Math.max(f.transform.x - hw, Math.min(TB.x, f.transform.x + hw))
        const qy = Math.max(f.transform.y - hh3, Math.min(TB.y, f.transform.y + hh3))
        const ddx = TB.x - qx, ddy = TB.y - qy
        const d2 = ddx*ddx + ddy*ddy
        if (d2 < RB*RB && d2 > 1e-6) {
          const dd = Math.sqrt(d2)
          const nx = ddx/dd, ny = ddy/dd
          TB.x = qx + nx*RB; TB.y = qy + ny*RB
          const vn = TB.vx*nx + TB.vy*ny
          if (vn < 0) {
            const kr = S.free ? 1.75 : 1.9
            TB.vx -= kr*vn*nx; TB.vy -= kr*vn*ny
            if (-vn > 40) { S.flash = 1; S.bounces++ }
          }
        }
      }
    }
    // ── the interceptors: three ship classes run the street ──
    //    DRIFTER (cyan) weaves · DODGER (amber) jukes your throws ·
    //    HUNTER (magenta) guns for the camera line and burns hard near it
    S.spawnT -= pdt
    for (let i = 0; i < 3; i++) {
      const E = S.enemies[i]
      if (!E) {
        if (S.spawnT <= 0) {
          const heat = Math.min(1, (S.kills + S.through * 2) / 24)
          const roll = Math.random()
          const cls = roll < 0.24 + heat * 0.3 ? 2 : (roll < 0.6 ? 1 : 0)
          S.enemies[i] = { x: 258 + Math.random() * 32, y: Math.min(camGY + 160 + Math.random() * 25, 484),
                           a: 1.6 + Math.random() * 2.2, sp: (cls === 2 ? 19 : 13) + Math.random() * 10,
                           ph: Math.random() * 6.28, cls, lvx: 0, flare: 0, dcd: 0, yaw: 0, bank: 0 }
          S.spawnT = Math.max(1.6, 2.5 + Math.random() * 2.5 - heat * 1.2)
          if (cls === 2) SND.push({ frequency: 68, duration: 0.35, volume: 0.11, type: 'sawtooth' })
        }
        continue
      }
      E.dcd = Math.max(0, E.dcd - pdt)
      E.flare *= Math.exp(-3 * pdt)
      let sp = E.sp
      if (E.cls === 2) {
        const near = Math.max(0, 1 - (E.y - camGY) / 120)
        sp *= 1 + near * 1.3
        E.flare = Math.max(E.flare, near * 0.8)
      }
      E.y -= sp * pdt
      const wx2 = Math.sin(S.t * 1.3 + E.ph) * (E.cls === 0 ? 7 : 4)
      if (E.cls >= 1 && E.dcd <= 0) {
        const bd = Math.hypot(TB.x - E.x, TB.y - E.y)
        const closing = ((E.x - TB.x) * TB.vx + (E.y - TB.y) * TB.vy) / Math.max(bd, 1)
        if (bd < 34 && closing > 70) {
          E.lvx = (TB.x > E.x ? -1 : 1) * (E.cls === 2 ? 55 : 75)
          E.dcd = 1.1
          E.flare = 1
          SND.push({ frequency: 480, duration: 0.07, volume: 0.12, type: 'triangle' },
                    { frequency: 700, duration: 0.09, volume: 0.10, type: 'triangle' })
        }
      }
      E.lvx *= Math.exp(-2.2 * pdt)
      E.x = Math.max(257, Math.min(291, E.x + (wx2 + E.lvx) * pdt))
      E.yaw = Math.max(-0.6, Math.min(0.6, -(wx2 + E.lvx) / 55))
      E.bank = Math.max(-0.8, Math.min(0.8, (wx2 + E.lvx) / 45))
      const hd = Math.hypot(TB.x - E.x, TB.y - E.y)
      if (hd < 11 && Math.abs(bh - E.a) < 2.6 && S.held && Math.hypot(TB.vx, TB.vy) <= 60) {
        // camping in traffic: the ship RAMS the ball out of your hand — a trade
        S.held = 0; S.free = 1; S.ramCd = 0.8
        TB.vx = (TB.x - E.x) * 18 + E.lvx * 2
        TB.vy = 160
        S.flash = 1; S.red = Math.max(S.red || 0, 0.6)
        SND.push({ frequency: 180, duration: 0.25, volume: 0.4, type: 'sawtooth' },
                  { frequency: 120, duration: 0.35, volume: 0.3, type: 'square' })
        S.enemies[i] = null
        continue
      }
      if (hd < 10 && Math.abs(bh - E.a) < 2.6 && Math.hypot(TB.vx, TB.vy) > 60) {
        S.kills++; S.flash = 1
        S.boom = { x: E.x, a: E.a, y: E.y, t: 0, cls: E.cls }
        SND.push({ frequency: 660, duration: 0.22, volume: 0.4, type: 'square' })
        if (E.cls === 2) SND.push({ frequency: 880, duration: 0.3, volume: 0.22, type: 'square' },
                                   { frequency: 1320, duration: 0.4, volume: 0.15, type: 'sine' })
        S.enemies[i] = null
        continue
      }
      if (E.y < camGY + 6) {
        S.through++; S.red = 1
        SND.push({ frequency: 90, duration: 0.4, volume: 0.5, type: 'sawtooth' })
        S.enemies[i] = null
        if (S.through >= 3) {
          SND.push({ frequency: 220, duration: 0.3, volume: 0.5, type: 'sawtooth' },
                    { frequency: 110, duration: 0.6, volume: 0.5, type: 'sawtooth' },
                    { frequency: 55, duration: 1.0, volume: 0.5, type: 'sine' })
          wd.__ncReset = true
        }
      }
    }
    if (S.boom) { S.boom.t += pdt / 0.7; if (S.boom.t >= 1) S.boom = null }

    // ── the district's voice: engine hum, street heartbeat, proximity sonar ──
    S.audHum = (S.audHum || 0) - pdt
    const spd = Math.hypot(TB.vx, TB.vy)
    if (S.audHum <= 0) {
      S.audHum = 0.34
      SND.push({ frequency: 46 + spd * 0.45, duration: 0.3, volume: 0.045 + (spd / 380) * 0.07, type: 'sine' })
      if (S.held) SND.push({ frequency: 92 + spd * 0.9, duration: 0.18, volume: 0.035, type: 'triangle' })
    }
    S.audBeat = (S.audBeat || 0) - pdt
    if (S.audBeat <= 0) {
      S.audBeat = 1.9
      SND.push({ frequency: 41, duration: 0.5, volume: 0.05 + tri * 0.03, type: 'sine' })
    }
    let danger = 0, dCls = 0
    for (const E of S.enemies) if (E) {
      const d3 = Math.max(0, 1 - (E.y - (camGY + 6)) / 70)
      if (d3 > danger) { danger = d3; dCls = E.cls }
    }
    S.danger = danger
    // ── the crossing: survive to the end of the buildings — the real win ──
    if (cyc > 0.495 && !S.crossed) {
      S.crossed = 1
      S.wave = (S.wave || 1) + 1
      S.through = 0   // full hearts: you crossed the district
      S.banner = { text: 'DISTRICT CROSSED - THE NIGHT IS YOURS', t: 4 }
      if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent('cafe:caption', {
        detail: { text: 'district crossed - the night is yours', kind: 'tuned' } }))
      SND.push({ frequency: 392, duration: 0.35, volume: 0.32, type: 'triangle' },
                { frequency: 523, duration: 0.4, volume: 0.30, type: 'triangle' },
                { frequency: 659, duration: 0.5, volume: 0.28, type: 'triangle' },
                { frequency: 1046, duration: 0.9, volume: 0.26, type: 'sine' })
    }
    if (cyc < 0.05 && S.crossed) { S.crossed = 0 }
    // ── the night comes in waves: clear ten ships and the district yields a breath ──
    if (S.kills >= (S.wave || 1) * 10) {
      S.wave = (S.wave || 1) + 1
      S.through = Math.max(0, S.through - 1)
      if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent('cafe:caption', {
        detail: { text: 'WAVE ' + (S.wave - 1) + ' CLEARED - the district yields', kind: 'tuned' } }))
      S.banner = { text: 'WAVE ' + (S.wave - 1) + ' CLEARED', t: 3 }
      SND.push({ frequency: 523, duration: 0.3, volume: 0.3, type: 'triangle' },
                { frequency: 784, duration: 0.4, volume: 0.28, type: 'triangle' },
                { frequency: 1046, duration: 0.6, volume: 0.25, type: 'sine' })
    }
    S.audSon = (S.audSon || 0) - pdt
    if (danger > 0.12 && S.audSon <= 0) {
      S.audSon = 0.95 - danger * 0.75
      SND.push({ frequency: dCls === 2 ? 1170 : (dCls === 1 ? 990 : 880), duration: 0.05,
                 volume: 0.045 + danger * 0.07, type: 'square' })
    }
    S.red = (S.red || 0) * Math.exp(-2.5 * pdt)
    const hearts = Math.max(0, 3 - S.through)
    if (S.banner) { S.banner.t -= pdt; if (S.banner.t <= 0) S.banner = null }
    // UI through the SOLVER (wd.ui glass), never the deprecated wd.hud. Hearts
    // are three meter pips (the ui glyph font is ASCII - a heart char renders as
    // nothing). Re-declare only when content changes; rev bump = re-solve.
    if (wd.hud) delete wd.hud
    const uiSig = hearts + '|' + (S.wave || 1) + '|' + S.kills + '|' + (S.banner ? S.banner.text : '')
    if (S.uiSig !== uiSig) {
      S.uiSig = uiSig
      S.uiRev = (S.uiRev || 0) + 1
      wd.ui = { rev: S.uiRev, root: [
        { id: 'nc_status', kind: 'panel', anchor: { vx: 0, vy: 0 }, align: 'tl', dx: 12, dy: 12, glass: true, children: [
          { id: 'nc_hearts', kind: 'row', gap: 6, children: [0, 1, 2].map(i => (
            { id: 'nc_h' + i, kind: 'meter', value: i < hearts ? 1 : 0, hue: '#ff4a5e', w: 26 }
          )) },
          { id: 'nc_wave', kind: 'text', text: 'WAVE ' + (S.wave || 1) + ' \u00b7 KILLS ' + S.kills, fontSize: 11, color: 'rgba(255,255,255,0.62)' },
        ] },
        ...(S.banner ? [{ id: 'nc_banner', kind: 'panel', anchor: { gx: 256, gy: 116 }, align: 'tc', glass: true, children: [
          { id: 'nc_banner_t', kind: 'text', text: S.banner.text, fontSize: 18, color: '#ffd682' },
        ] }] : []),
      ] }
    }

    // park the field at the ball's projected screen position (its true face)
    {
      const w3 = [(TB.x - 256) / 4, bh, (TB.y - 256) / 4]
      const v3 = [w3[0]-RO[0], w3[1]-RO[1], w3[2]-RO[2]]
      const f3 = v3[0]*LOOK[0] + v3[1]*LOOK[1] + v3[2]*LOOK[2]
      S.esx = ((v3[0]*RT[0]+v3[1]*RT[1]+v3[2]*RT[2]) * 1.5 / f3 + 1) * 256
      S.esy = (1 - (v3[0]*UP[0]+v3[1]*UP[1]+v3[2]*UP[2]) * 1.5 / (0.72 * f3)) * 256
      T.x = S.esx; T.y = S.esy; T.vx = 0; T.vy = 0
    }
    const wx = (S.phys.x - 256) / 4
    const wz = (S.phys.y - 256) / 4
    const eu = []
    const ep = []
    for (let i = 0; i < 3; i++) {
      const E = S.enemies[i]
      eu.push(E ? (E.x - 256) / 4 : 0, E ? E.a : 0, E ? (E.y - 256) / 4 : 0)
      ep.push(E ? E.yaw || 0 : 0, E ? E.bank || 0 : 0, E ? E.cls + Math.min(0.95, E.flare || 0) : 0)
    }
    const B3 = S.boom
    wd.gpuUniforms = [0, wx, S.alt + 1.3, wz, S.flash, T.vx / 4, S.valt, T.vy / 4,
                      0, 0, 0, 0, 0, 0, 0, 1, ...eu, S.red, camWZ, S.danger || 0, ...ep,
                      B3 ? (B3.x - 256) / 4 : 0, B3 ? B3.a : 0, B3 ? (B3.y - 256) / 4 : 0,
                      B3 ? Math.min(0.999, B3.t) : 0, B3 ? B3.cls : 0]

    if (SND.length) wd.__play_sound = SND
    ball.name = 'Ball — ' + S.kills + ' down · ' + S.through + ' got through'
  }

} catch (e) { /* keep the sim alive */ }

hook · nc_perf_probe

Frame-cadence probe at worldData.__perf.

try {
  const wd = sim.worldData
  if (!wd.__perf || wd.__perf.v !== 3) wd.__perf = { v: 3, n: 0, sum: 0, max: 0, o20: 0, o50: 0, fN: 0, fSum: 0, fMax: 0, fO20: 0, uN: 0 }
  const P = wd.__perf
  if (dt > 0 && dt < 1) {
    P.n++; P.sum += dt
    if (dt > P.max) P.max = dt
    if (dt > 0.020) P.o20++
    if (dt > 0.050) P.o50++
    const foc = (typeof document !== 'undefined') && document.hasFocus()
    if (foc) { P.fN++; P.fSum += dt; if (dt > P.fMax) P.fMax = dt; if (dt > 0.020) P.fO20++ }
    else { P.uN++ }
    P.fpsAll = Math.round(P.n / P.sum)
    P.fpsFocused = P.fN > 5 ? Math.round(P.fN / P.fSum) : 0
    P.fpsUnfocused = P.uN > 5 ? Math.round(P.uN / (P.sum - P.fSum)) : 0
    P.pctUnfocused = Math.round(100 * P.uN / P.n)
  }
} catch (e) { /* keep the sim alive */ }