Static site
Browse files- index.html +2 -2
- static/game.js +0 -0
- static/main.js +2 -2
- 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=
|
| 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=
|
| 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) <
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 139 |
-
const
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
// a car ahead in our lane
|
| 142 |
-
|
| 143 |
-
|
| 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 |
}
|