Death242 commited on
Commit
cb0e58a
·
verified ·
1 Parent(s): c8585ae

Static site

Browse files
Files changed (4) hide show
  1. index.html +2 -2
  2. static/game.js +0 -0
  3. static/main.js +2 -2
  4. static/nav.js +75 -14
index.html CHANGED
@@ -8,7 +8,7 @@
8
  <meta name="theme-color" content="#10131b">
9
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
  <link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;800;900&family=Inter:wght@400;500;600;700&family=Noto+Kufi+Arabic:wght@700&display=swap" rel="stylesheet">
11
- <link rel="stylesheet" href="/static/style.css?v=1793116079">
12
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Crect width=%2764%27 height=%2764%27 rx=%2714%27 fill=%27%23050506%27/%3E%3Ccircle cx=%2732%27 cy=%2732%27 r=%2716%27 fill=%27none%27 stroke=%27%23fff%27 stroke-width=%275%27/%3E%3C/svg%3E">
13
  <script type="importmap">
14
  { "imports": {
@@ -215,6 +215,6 @@
215
  <div id="toast" role="status"></div>
216
  <noscript><p style="position:fixed;inset:auto 0 0;padding:16px;background:#111;color:#fff;text-align:center">VOID needs JavaScript for the 3D car.</p></noscript>
217
  <script src="/static/hub.js" data-app="void" defer></script>
218
- <script type="module" src="/static/main.js?v=1793116079"></script>
219
  </body>
220
  </html>
 
8
  <meta name="theme-color" content="#10131b">
9
  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
  <link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;800;900&family=Inter:wght@400;500;600;700&family=Noto+Kufi+Arabic:wght@700&display=swap" rel="stylesheet">
11
+ <link rel="stylesheet" href="/static/style.css?v=1793116888">
12
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 64 64%27%3E%3Crect width=%2764%27 height=%2764%27 rx=%2714%27 fill=%27%23050506%27/%3E%3Ccircle cx=%2732%27 cy=%2732%27 r=%2716%27 fill=%27none%27 stroke=%27%23fff%27 stroke-width=%275%27/%3E%3C/svg%3E">
13
  <script type="importmap">
14
  { "imports": {
 
215
  <div id="toast" role="status"></div>
216
  <noscript><p style="position:fixed;inset:auto 0 0;padding:16px;background:#111;color:#fff;text-align:center">VOID needs JavaScript for the 3D car.</p></noscript>
217
  <script src="/static/hub.js" data-app="void" defer></script>
218
+ <script type="module" src="/static/main.js?v=1793116888"></script>
219
  </body>
220
  </html>
static/game.js CHANGED
The diff for this file is too large to render. See raw diff
 
static/main.js CHANGED
@@ -135,7 +135,7 @@ document.getElementById("gSens").addEventListener("input", e => { SET.sens = +e.
135
  const heads = new THREE.SpotLight(0xfff1d6, 0, 70, .55, .6, 1.2); heads.position.set(2.2, .7, 0); heads.target.position.set(18, -1.5, 0); carRoot.add(heads, heads.target);
136
  police = createPolice(scene, game.api, sound, life);
137
  races = createRaces(scene, game.api, sound, W.canvasTex, net);
138
- nav = createNav(scene, game.api, W, ZONES, races.starts);
139
  const extras = createExtras(W, game.api, nav, sound);
140
  document.getElementById("gTaxi").onclick = () => extras.toggle();
141
  // people knocked over: a thud, a shake, and the police notice; they bounce off walls instead of flying through them
@@ -681,4 +681,4 @@ document.addEventListener("visibilitychange", () => {
681
  if (document.hidden) { cancelAnimationFrame(raf); if (mode === "watch" && w.playing) pause(); if (sound.ctx) sound.ctx.suspend(); }
682
  else { clock.getDelta(); loop(); if (sound.on && sound.ctx) sound.ctx.resume(); }
683
  });
684
- window.VOID = { contracts, step: (n = 1, dt = 1 / 30) => { for (let i = 0; i < n; i++) frameLoop(dt); }, W, car, life, carCube, carEnv, get police() { return police; }, get races() { return races; }, get nav() { return nav; }, net, orbit, go, seek: t => { w.t = t; if (w.playing) playFrom(t); }, pause, get w() { return w; }, game }; // handy for testing in the console
 
135
  const heads = new THREE.SpotLight(0xfff1d6, 0, 70, .55, .6, 1.2); heads.position.set(2.2, .7, 0); heads.target.position.set(18, -1.5, 0); carRoot.add(heads, heads.target);
136
  police = createPolice(scene, game.api, sound, life);
137
  races = createRaces(scene, game.api, sound, W.canvasTex, net);
138
+ nav = createNav(scene, game.api, W, ZONES, races.starts, () => clock.elapsedTime);
139
  const extras = createExtras(W, game.api, nav, sound);
140
  document.getElementById("gTaxi").onclick = () => extras.toggle();
141
  // people knocked over: a thud, a shake, and the police notice; they bounce off walls instead of flying through them
 
681
  if (document.hidden) { cancelAnimationFrame(raf); if (mode === "watch" && w.playing) pause(); if (sound.ctx) sound.ctx.suspend(); }
682
  else { clock.getDelta(); loop(); if (sound.on && sound.ctx) sound.ctx.resume(); }
683
  });
684
+ window.VOID = { clock: () => clock, contracts, step: (n = 1, dt = 1 / 30) => { for (let i = 0; i < n; i++) frameLoop(dt); }, W, car, life, carCube, carEnv, get police() { return police; }, get races() { return races; }, get nav() { return nav; }, net, orbit, go, seek: t => { w.t = t; if (w.playing) playFrom(t); }, pause, get w() { return w; }, game }; // handy for testing in the console
static/nav.js CHANGED
@@ -3,14 +3,14 @@
3
  // keeps to the speed, slows for corners and for cars ahead. If the place is in the air (a rooftop helipad) or
4
  // you are flying, it flies there instead: climbs over the towers, cruises, then slows down and lands.
5
  import * as THREE from "three";
6
- import { GRID } from "./city.js";
7
 
8
  const ord = n => n + (n % 100 > 10 && n % 100 < 14 ? "th" : ["th", "st", "nd", "rd"][n % 10] || "th");
9
  export const avName = i => i === 6 ? "Park Ave" : `${ord(i + 1)} Ave`;
10
  export const stName = j => j === 10 ? "Broadway" : `${ord(Math.abs(j - 10) * 7 + 14)} St`;
11
  const clamp = (x, a, b) => Math.max(a, Math.min(b, x)), wrap = a => { while (a > Math.PI) a -= 2 * Math.PI; while (a < -Math.PI) a += 2 * Math.PI; return a; };
12
 
13
- export function createNav(scene, api, W, ZONES, starts) {
14
  const { ax, sz, NX, NZ } = GRID, $ = id => document.getElementById(id);
15
  // the street graph: every intersection, linked along every open segment, plus the bridge
16
  const id = (i, j) => i * NZ + j, nodes = [];
@@ -19,6 +19,7 @@ export function createNav(scene, api, W, ZONES, starts) {
19
  for (let i = 0; i < NX; i++) for (let j = 0; j < NZ; j++) { if (i < NX - 1 && GRID.hasX(j, i)) link(id(i, j), id(i + 1, j)); if (j < NZ - 1 && GRID.hasZ(i, j)) link(id(i, j), id(i, j + 1)); }
20
  const extra = (x, z, to) => { nodes.push({ x, z, n: [] }); link(nodes.length - 1, to); return nodes.length - 1; };
21
  const bridgeMid = extra(1400, 0, id(12, 10)), brooklyn = extra(1690, 0, bridgeMid);
 
22
  const nearestNode = (x, z) => { let b = 0, bd = 1e18; nodes.forEach((n, k) => { const d = (n.x - x) ** 2 + (n.z - z) ** 2; if (d < bd) { bd = d; b = k; } }); return b; };
23
  function path(a, b) { // Dijkstra over ~280 nodes: instant
24
  const dist = new Float64Array(nodes.length).fill(Infinity), prev = new Int32Array(nodes.length).fill(-1), done = new Uint8Array(nodes.length);
@@ -39,7 +40,9 @@ export function createNav(scene, api, W, ZONES, starts) {
39
  ];
40
  // the route drawn on the road
41
  const lineMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(0x3dfcff).multiplyScalar(1.6), transparent: true, opacity: .55, depthWrite: false, toneMapped: false });
42
- let routeMesh = null, dest = null, pts = [], auto = false, lastRoute = 0, info = "";
 
 
43
  function draw() {
44
  if (routeMesh) { scene.remove(routeMesh); routeMesh.geometry.dispose(); routeMesh = null; }
45
  if (pts.length < 2 || dest.air) return;
@@ -55,9 +58,14 @@ export function createNav(scene, api, W, ZONES, starts) {
55
  lastRoute = performance.now();
56
  if (dest.air) { pts = [[p.x, p.z], [dest.x, dest.z]]; draw(); return; }
57
  const a = nearestNode(p.x, p.z), b = nearestNode(dest.x, dest.z), ids = path(a, b);
58
- pts = [[p.x, p.z], ...ids.map(k => [nodes[k].x, nodes[k].z]), [dest.x, dest.z]];
59
  // drop a first node that is behind us
60
- if (pts.length > 3) { const [x0, z0] = pts[0], [x1, z1] = pts[1], [x2, z2] = pts[2]; if ((x1 - x0) * (x2 - x1) + (z1 - z0) * (z2 - z1) < 0 && Math.hypot(x1 - x0, z1 - z0) < 40) pts.splice(1, 1); }
 
 
 
 
 
61
  draw();
62
  }
63
  function setDest(d) { dest = d; if (!d) { pts = []; draw(); auto = false; syncAuto(); $("gGps").textContent = ""; return; } plan(); api.say(`GPS: ${d.name}`, 2); }
@@ -78,7 +86,7 @@ export function createNav(scene, api, W, ZONES, starts) {
78
  for (let n = k + 1; n < pts.length - 1; n++) {
79
  const [x0, z0] = pts[n - 1], [x1, z1] = pts[n], [x2, z2] = pts[n + 1], a1 = Math.atan2(z1 - z0, x1 - x0), a2 = Math.atan2(z2 - z1, x2 - x1), turn = wrap(a2 - a1);
80
  if (Math.abs(turn) > .5) {
81
- const node = nodes.find(q => Math.abs(q.x - x1) < 1 && Math.abs(q.z - z1) < 1), onto = node && node.i != null ? (Math.abs(x2 - x1) > Math.abs(z2 - z1) ? stName(node.j) : avName(node.i)) : "";
82
  return { turn, d, text: `${turn > 0 ? "↱ Turn right" : "↰ Turn left"} in ${Math.round(d / 10) * 10} m${onto ? " onto " + onto : ""}`, corner: d };
83
  }
84
  d += Math.hypot(x2 - x1, z2 - z1);
@@ -118,11 +126,19 @@ export function createNav(scene, api, W, ZONES, starts) {
118
  if (!auto || !dest) return null;
119
  if (overridden) { auto = false; syncAuto(); api.say("Autopilot off: you have control", 1.5); return null; }
120
  const V = g.vel.length(), flat = Math.hypot(g.x - dest.x, g.z - dest.z);
121
- const needFly = dest.air || g.mode === "air";
 
 
 
 
122
  if (needFly) {
123
  if (g.mode === "drive" && dest.air && flat < 18 && Math.abs(g.y - dest.y) < 3) { api.say(`You have arrived: ${dest.name}`, 3); const d = dest; setDest(null); void d; return { turn: 0, gas: 0, brake: 1 }; }
124
  if (g.mode === "drive") { api.fly(); return { turn: 0, gas: 0, brake: 0, up: 1 }; }
125
- const targetY = dest.air ? (flat < 400 ? dest.y + 28 : Math.max(380, dest.y + 40)) : flat < 400 ? 60 : 380;
 
 
 
 
126
  const head = Math.atan2(dest.z - g.z, dest.x - g.x), err = wrap(head - g.yaw), turn = clamp(err * 1.6, -1, 1);
127
  // climb or sink at a steady rate (never hauling the nose up into a stall)
128
  const wantVy = clamp((targetY - g.y) * .15, -12, 16), up = g.vel.y < wantVy - 1.5 ? 1 : 0, down = g.vel.y > wantVy + 3 ? 1 : 0;
@@ -135,13 +151,58 @@ export function createNav(scene, api, W, ZONES, starts) {
135
  return { turn, gas: g.stall > .3 ? 0 : 1, brake: 0, up, down };
136
  }
137
  // on the road
138
- const look = Math.max(10, V * 1.1), t = along(g.x, g.z, look), ins = instruction(g.x, g.z, t.k);
139
- const head = Math.atan2(t.z - g.z, t.x - g.x), err = wrap(head - g.yaw);
140
- let want = 26; if (ins.corner < 75) want = Math.min(want, 9 + Math.max(0, ins.corner - 20) * .3); if (flat < 70) want = Math.min(want, Math.max(0, (flat - 5) * .35)); if (Math.abs(err) > .6) want = Math.min(want, 8);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  // a car ahead in our lane
142
- for (const c of api.nearCars(g.x, g.z, 30)) { if (c.wreck) continue; const dx = c.x - g.x, dz = c.z - g.z, fwd = dx * Math.cos(g.yaw) + dz * Math.sin(g.yaw), side = -dx * Math.sin(g.yaw) + dz * Math.cos(g.yaw); if (fwd > 2 && fwd < 26 && Math.abs(side) < 2.4) want = Math.min(want, Math.max(0, c.v - 1 + (fwd - 9) * .5)); }
143
- const u = g.vel.x * Math.cos(g.yaw) + g.vel.z * Math.sin(g.yaw);
144
- return { turn: clamp(err * 2.2, -1, 1), gas: u < want - 1 ? clamp((want - u) / 6, .2, 1) : 0, brake: u > want + 2 ? clamp((u - want) / 8, .2, 1) : 0 };
145
  },
146
  };
147
  }
 
3
  // keeps to the speed, slows for corners and for cars ahead. If the place is in the air (a rooftop helipad) or
4
  // you are flying, it flies there instead: climbs over the towers, cruises, then slows down and lands.
5
  import * as THREE from "three";
6
+ import { GRID, phase } from "./city.js";
7
 
8
  const ord = n => n + (n % 100 > 10 && n % 100 < 14 ? "th" : ["th", "st", "nd", "rd"][n % 10] || "th");
9
  export const avName = i => i === 6 ? "Park Ave" : `${ord(i + 1)} Ave`;
10
  export const stName = j => j === 10 ? "Broadway" : `${ord(Math.abs(j - 10) * 7 + 14)} St`;
11
  const clamp = (x, a, b) => Math.max(a, Math.min(b, x)), wrap = a => { while (a > Math.PI) a -= 2 * Math.PI; while (a < -Math.PI) a += 2 * Math.PI; return a; };
12
 
13
+ export function createNav(scene, api, W, ZONES, starts, clockT = () => performance.now() / 1000) {
14
  const { ax, sz, NX, NZ } = GRID, $ = id => document.getElementById(id);
15
  // the street graph: every intersection, linked along every open segment, plus the bridge
16
  const id = (i, j) => i * NZ + j, nodes = [];
 
19
  for (let i = 0; i < NX; i++) for (let j = 0; j < NZ; j++) { if (i < NX - 1 && GRID.hasX(j, i)) link(id(i, j), id(i + 1, j)); if (j < NZ - 1 && GRID.hasZ(i, j)) link(id(i, j), id(i, j + 1)); }
20
  const extra = (x, z, to) => { nodes.push({ x, z, n: [] }); link(nodes.length - 1, to); return nodes.length - 1; };
21
  const bridgeMid = extra(1400, 0, id(12, 10)), brooklyn = extra(1690, 0, bridgeMid);
22
+ { let prev = extra(2200, 0, brooklyn); prev = extra(2640, 0, prev); const T = (W.city.mountains || {}).trail || []; for (const [x, , z] of T.slice(1)) prev = extra(x, z, prev); }
23
  const nearestNode = (x, z) => { let b = 0, bd = 1e18; nodes.forEach((n, k) => { const d = (n.x - x) ** 2 + (n.z - z) ** 2; if (d < bd) { bd = d; b = k; } }); return b; };
24
  function path(a, b) { // Dijkstra over ~280 nodes: instant
25
  const dist = new Float64Array(nodes.length).fill(Infinity), prev = new Int32Array(nodes.length).fill(-1), done = new Uint8Array(nodes.length);
 
40
  ];
41
  // the route drawn on the road
42
  const lineMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(0x3dfcff).multiplyScalar(1.6), transparent: true, opacity: .55, depthWrite: false, toneMapped: false });
43
+ let routeMesh = null, dest = null, pts = [], ptNode = [], auto = false, lastRoute = 0, info = "", skyCache = null, stuck = 0, backT = 0;
44
+ // the highest thing between here and there (for picking a safe cruising height)
45
+ const tallestAlong = (x0, z0, x1, z1, pad = 30) => { let h = 0; const L = Math.hypot(x1 - x0, z1 - z0), n = Math.max(1, Math.ceil(L / 35)); for (let k = 0; k <= n; k++) { const x = x0 + (x1 - x0) * k / n, z = z0 + (z1 - z0) * k / n; for (const b of W.city.nearFast(x, z, pad)) if (Math.abs(x - b.x) < b.hw + pad && Math.abs(z - b.z) < b.hd + pad && b.h > h) h = b.h; h = Math.max(h, W.city.terrainH ? W.city.terrainH(x, z) + 15 : 0); } return h; };
46
  function draw() {
47
  if (routeMesh) { scene.remove(routeMesh); routeMesh.geometry.dispose(); routeMesh = null; }
48
  if (pts.length < 2 || dest.air) return;
 
58
  lastRoute = performance.now();
59
  if (dest.air) { pts = [[p.x, p.z], [dest.x, dest.z]]; draw(); return; }
60
  const a = nearestNode(p.x, p.z), b = nearestNode(dest.x, dest.z), ids = path(a, b);
61
+ pts = [[p.x, p.z], ...ids.map(k => [nodes[k].x, nodes[k].z]), [dest.x, dest.z]]; ptNode = [null, ...ids, null];
62
  // drop a first node that is behind us
63
+ if (pts.length > 3) { const [x0, z0] = pts[0], [x1, z1] = pts[1], [x2, z2] = pts[2]; if ((x1 - x0) * (x2 - x1) + (z1 - z0) * (z2 - z1) < 0 && Math.hypot(x1 - x0, z1 - z0) < 40) { pts.splice(1, 1); ptNode.splice(1, 1); } }
64
+ // keep right: avenues use the inner lane (2.6 m off the middle), streets 3 m, Broadway 3.2 m
65
+ const laneOf = k => { const [x0, z0] = pts[k], [x1, z1] = pts[k + 1], nd = nodes[ptNode[k + 1] ?? ptNode[k]] || {}; return Math.abs(z1 - z0) > Math.abs(x1 - x0) ? 2.6 : nd.j === 10 ? 3.2 : 3; };
66
+ const nrm = k => { const [x0, z0] = pts[k], [x1, z1] = pts[k + 1], L = Math.hypot(x1 - x0, z1 - z0) || 1, w = laneOf(k); return [-(z1 - z0) / L * w, (x1 - x0) / L * w]; };
67
+ const off = pts.map((q, k) => { if (k === 0) return q; const a = nrm(k - 1), b = k < pts.length - 1 ? nrm(k) : a; const dot = (a[0] * b[0] + a[1] * b[1]) / (Math.hypot(...a) * Math.hypot(...b) || 1); return dot > .95 ? [q[0] + a[0], q[1] + a[1]] : dot < -.5 ? [q[0] + a[0], q[1] + a[1]] : [q[0] + a[0] + b[0], q[1] + a[1] + b[1]]; });
68
+ pts = off;
69
  draw();
70
  }
71
  function setDest(d) { dest = d; if (!d) { pts = []; draw(); auto = false; syncAuto(); $("gGps").textContent = ""; return; } plan(); api.say(`GPS: ${d.name}`, 2); }
 
86
  for (let n = k + 1; n < pts.length - 1; n++) {
87
  const [x0, z0] = pts[n - 1], [x1, z1] = pts[n], [x2, z2] = pts[n + 1], a1 = Math.atan2(z1 - z0, x1 - x0), a2 = Math.atan2(z2 - z1, x2 - x1), turn = wrap(a2 - a1);
88
  if (Math.abs(turn) > .5) {
89
+ const node = nodes.find(q => q.i != null && Math.abs(q.x - x1) < 6 && Math.abs(q.z - z1) < 6), onto = node && node.i != null ? (Math.abs(x2 - x1) > Math.abs(z2 - z1) ? stName(node.j) : avName(node.i)) : "";
90
  return { turn, d, text: `${turn > 0 ? "↱ Turn right" : "↰ Turn left"} in ${Math.round(d / 10) * 10} m${onto ? " onto " + onto : ""}`, corner: d };
91
  }
92
  d += Math.hypot(x2 - x1, z2 - z1);
 
126
  if (!auto || !dest) return null;
127
  if (overridden) { auto = false; syncAuto(); api.say("Autopilot off: you have control", 1.5); return null; }
128
  const V = g.vel.length(), flat = Math.hypot(g.x - dest.x, g.z - dest.z);
129
+ // long trips in a car that can fly go by air: straight over everything, then down onto the road at the end
130
+ const canFly = g.veh === "car" && (!g.ride || g.ride.model.kind.fly) && g.fuel > .1;
131
+ if (canFly && !dest.air && g.mode === "drive" && flat > 700 && !g.apFlyMsg) { g.apFlyMsg = true; api.say("Autopilot: long trip, flying there", 2.5); }
132
+ if (flat < 600) g.apFlyMsg = false;
133
+ const needFly = dest.air || g.mode === "air" || (canFly && flat > 700);
134
  if (needFly) {
135
  if (g.mode === "drive" && dest.air && flat < 18 && Math.abs(g.y - dest.y) < 3) { api.say(`You have arrived: ${dest.name}`, 3); const d = dest; setDest(null); void d; return { turn: 0, gas: 0, brake: 1 }; }
136
  if (g.mode === "drive") { api.fly(); return { turn: 0, gas: 0, brake: 0, up: 1 }; }
137
+ if (!skyCache || performance.now() - skyCache.t > 2000) skyCache = { t: performance.now(), h: tallestAlong(g.x, g.z, dest.x, dest.z) };
138
+ const clear = skyCache.h + 35, targetY = dest.air ? (flat < 400 ? Math.max(dest.y + 28, Math.min(clear, dest.y + 120)) : Math.max(380, dest.y + 40, clear)) : Math.max(70, clear); // ground trips: only as high as the tallest thing on the way
139
+ // something tall right in front of us: stop going forward and climb over it first
140
+ { const hx = Math.cos(g.yaw), hz = Math.sin(g.yaw); let wall = false; for (let d = 15; d <= 140 && !wall; d += 15) for (const b of W.city.nearFast(g.x + hx * d, g.z + hz * d, 12)) if (b.h > g.y - 4 && Math.abs(g.x + hx * d - b.x) < b.hw + 12 && Math.abs(g.z + hz * d - b.z) < b.hd + 12) { wall = true; break; }
141
+ if (wall && !(dest.air && flat < 30)) { g.hover = true; return { turn: clamp(wrap(Math.atan2(dest.z - g.z, dest.x - g.x) - g.yaw) * 1.6, -1, 1), gas: 0, brake: V > 8 ? 1 : 0, up: 1, down: 0 }; } }
142
  const head = Math.atan2(dest.z - g.z, dest.x - g.x), err = wrap(head - g.yaw), turn = clamp(err * 1.6, -1, 1);
143
  // climb or sink at a steady rate (never hauling the nose up into a stall)
144
  const wantVy = clamp((targetY - g.y) * .15, -12, 16), up = g.vel.y < wantVy - 1.5 ? 1 : 0, down = g.vel.y > wantVy + 3 ? 1 : 0;
 
151
  return { turn, gas: g.stall > .3 ? 0 : 1, brake: 0, up, down };
152
  }
153
  // on the road
154
+ const look = Math.max(9, V * .75), t = along(g.x, g.z, look), ins = instruction(g.x, g.z, t.k);
155
+ if (g.apPass) { const [ax1, az1] = pts[t.k], [bx1, bz1] = pts[t.k + 1], L1 = Math.hypot(bx1 - ax1, bz1 - az1) || 1; t.x += -(bz1 - az1) / L1 * g.apPass; t.z += (bx1 - ax1) / L1 * g.apPass; }
156
+ const head = Math.atan2(t.z - g.z, t.x - g.x), err = wrap(head - g.yaw), cy = Math.cos(g.yaw), sy = Math.sin(g.yaw);
157
+ const u = g.vel.x * cy + g.vel.z * sy;
158
+ // stuck against something: reverse and swing round for a moment, then try again
159
+ if (backT > 0) { backT -= dt; return { turn: -clamp(err * 2, -1, 1), gas: 0, brake: 1 }; }
160
+ // fast: flat out on the straights, then brake late and hard (the car can shed ~26 m/s every second; we plan on 12)
161
+ const A = 12, highway = Math.abs(g.z) < 30 && g.x > 1000, brakeTo = (v, d) => Math.sqrt(v * v + 2 * A * Math.max(0, d));
162
+ let want = highway ? 50 : 38, why = "cruise"; const lim = (v, tag) => { if (v < want) { want = v; why = tag; } };
163
+ if (ins.corner < 200) lim(brakeTo(Math.abs(ins.turn) > 2 ? 6 : 11, ins.corner - 16), "corner");
164
+ lim(brakeTo(0, flat - 6), "arrive");
165
+ if (Math.abs(err) > .6) lim(9, "heading");
166
+ // traffic lights: stop at the line unless it is green (or amber and too late to stop)
167
+ { let dAlong = Math.hypot(pts[t.k + 1][0] - g.x, pts[t.k + 1][1] - g.z), n = t.k + 1;
168
+ while (n < pts.length - 1 && ptNode[n] == null) { dAlong += Math.hypot(pts[n + 1][0] - pts[n][0], pts[n + 1][1] - pts[n][1]); n++; }
169
+ const nd = nodes[ptNode[n]];
170
+ if (nd && nd.i != null && dAlong < 60) {
171
+ const [px, pz] = pts[Math.max(0, n - 1)], axis = Math.abs(pts[n][0] - px) > Math.abs(pts[n][1] - pz) ? 0 : 1, ph = phase(nd.i, nd.j, clockT());
172
+ const green = axis === 0 ? ph === 0 : ph === 2, amber = axis === 0 ? ph === 1 : ph === 3, line = dAlong - (axis === 0 ? GRID.avW(nd.i) : GRID.stW(nd.j)) / 2 - 3.5;
173
+ if (!green && line > -1) {
174
+ let cross = false; for (const c of api.nearCars(nd.x, nd.z, 60)) { if (c.fly || c.wreck || c.taken || c.v < 2) continue; const toward = (nd.x - c.x) * Math.cos(c.yaw) + (nd.z - c.z) * Math.sin(c.yaw), d = Math.hypot(nd.x - c.x, nd.z - c.z); if (toward > -5 && toward / Math.max(c.v, 1) < 2.6 && d < 45 && Math.abs(Math.cos(c.yaw) * (axis === 0 ? 1 : 0) + Math.sin(c.yaw) * (axis === 1 ? 1 : 0)) < .5) { cross = true; break; } }
175
+ lim(cross ? brakeTo(0, line - .8) : brakeTo(16, line + 6), "light"); // nobody coming across: through it (a bit slower); else stop at the line
176
+ }
177
+ } }
178
+ // the car in front (in whatever lane we are in now): keep a short gap and brake late; a car crossing our path close ahead: give way
179
+ const [sx0, sz0] = pts[t.k], [sx1, sz1] = pts[t.k + 1], avenue = Math.abs(sz1 - sz0) > Math.abs(sx1 - sx0), passOff = g.apPass || 0;
180
+ let slowAhead = false, origBlocked = false, oncoming = false;
181
+ for (const c of api.nearCars(g.x, g.z, 90)) {
182
+ if (c.wreck || c.fly || c.taken) continue;
183
+ const dx = c.x - g.x, dz = c.z - g.z, fwd = dx * cy + dz * sy, side = -dx * sy + dz * cy, cvx = Math.cos(c.yaw) * c.v, cvz = Math.sin(c.yaw) * c.v, along2 = cvx * cy + cvz * sy, across = -cvx * sy + cvz * cy;
184
+ if (fwd > 1 && fwd < 70 && Math.abs(side) < 2.1) { const gap = fwd - c.kind.L / 2 - 3; lim(gap <= .8 ? 0 : brakeTo(Math.max(0, along2) * Math.min(1, gap / 6), gap - Math.max(3, u * .35)), "lead"); if (gap < 45 && along2 < 30) slowAhead = true; if (along2 < -1 && fwd < 90) oncoming = true; }
185
+ else if (fwd > 1 && fwd < 22 && Math.abs(side) < 9 && Math.abs(across) > 2.5 && side * across < 0) lim(2.5, "crossing"); // it is coming across in front
186
+ if (passOff && Math.abs(side + passOff) < 2.2 && fwd > -10 && fwd < 14) origBlocked = true; // still alongside the car we are passing
187
+ // emergency: will any car be within 2.6 m of us in the next 1.3 s if we keep going? then stop now
188
+ { const rvx = cvx - g.vel.x, rvz = cvz - g.vel.z, rv2 = rvx * rvx + rvz * rvz, tc = rv2 > .01 ? Math.max(0, Math.min(1.3, -(dx * rvx + dz * rvz) / rv2)) : 0, mx = dx + rvx * tc, mz = dz + rvz * tc;
189
+ if (Math.hypot(mx, mz) < 2.6 + c.kind.W * .3 && fwd > -1 && Math.hypot(dx, dz) < 32) lim(tc < .5 ? 0 : Math.max(0, u * .4), "emergency"); }
190
+ }
191
+ // overtaking: on an avenue into the other lane our way (3.6 m right); on a one-lane street through the oncoming lane (6 m left)
192
+ // when it is clear for long enough and no junction is close; back in as soon as we are past, or at once if anything comes the other way
193
+ const laneFree = (off, back, ahead, onc) => { const px = g.x - sy * off, pz = g.z + cy * off; for (const c of api.nearCars(px, pz, ahead + 6)) { if (c.fly || c.wreck || c.taken) continue; const dx = c.x - px, dz = c.z - pz, f = dx * cy + dz * sy, sd = -dx * sy + dz * cy; if (Math.abs(sd) < 2.3 && f > -back && f < (onc && (Math.cos(c.yaw) * cy + Math.sin(c.yaw) * sy) < 0 ? ahead : Math.min(ahead, 30))) return false; } return true; };
194
+ const toNode = Math.hypot(pts[t.k + 1][0] - g.x, pts[t.k + 1][1] - g.z);
195
+ if (!passOff) { if (slowAhead && ins.corner > 90 && u > 4) { if (avenue && laneFree(3.6, 9, 30, false)) g.apPass = 3.6; else if (!avenue && toNode > 55 && laneFree(-6, 6, 95, true)) g.apPass = -6; } }
196
+ else if ((passOff < 0 && (oncoming || toNode < 30 || ins.corner < 60)) || (!origBlocked && laneFree(-passOff, 10, 16, false)) || ins.corner < 45) g.apPass = 0;
197
+ // walls, posts, kerbs, parked things: look ahead along the way we are pointing
198
+ let wallAt = 1e9; { const reach = Math.max(8, u * u / (2 * A) + 8); for (let d = 3; d <= reach; d += 2.5) { if (api.hitBox(g.x + cy * d, g.z + sy * d, .5, 1.15)) { wallAt = d; lim(brakeTo(0, d - 4), "wall"); break; } } }
199
+ // pointing the wrong way (a car cannot turn on the spot): a three-point turn; nose to a wall: back off it
200
+ if ((Math.abs(err) > 1.7 && u < 5) || (wallAt < 6 && Math.abs(u) < 1.5 && Math.abs(err) > .35)) { backT = Math.abs(err) > 1.7 ? 1.3 : 1; return { turn: -clamp(err * 2, -1, 1), gas: 0, brake: 1 }; }
201
+ // stuck: we want to move (or something is in the way) and we are going nowhere
202
+ if ((want > 2 || wallAt < 6) && Math.abs(u) < .8) { stuck += dt; if (stuck > 1.6) { stuck = 0; backT = 1.2; plan(); } } else stuck = 0;
203
  // a car ahead in our lane
204
+ g.apWhy = why;
205
+ return { turn: clamp(err * (u > 25 ? 1.6 : 2.2), -1, 1), gas: u < want - .5 ? clamp((want - u) / 3, .3, 1) : 0, brake: u > want + .8 ? clamp((u - want) / 3, .3, 1) : 0, nitro: highway && u < want - 8 && Math.abs(err) < .1 };
 
206
  },
207
  };
208
  }