Turn the game into a third-person Mashhad city game
Replace the linear forest walk with an open area of real western Mashhad built from OpenStreetMap data, starting at Park Mellat. A father and child, guided by the Robot Market robot, find vending machines at a park, school, universities, a metro station and a mall, on foot or by car. - Iran province briefing map; only Mashhad is unlocked - Third-person camera, building collision, arcade car and ambient traffic - Minimap and full city map with addresses, opening hours and A* routing - Purchase scoring, missions with cash rewards, ranks and an end screen - Map data build script (OSM + Natural Earth) and new logic tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+176
@@ -0,0 +1,176 @@
|
||||
// Road graph, A* routing and nearest-street lookup over the OSM road polylines.
|
||||
// A road is [typeIndex, nameIndex, x0, z0, x1, z1, …]; ways that share an OSM node share coordinates.
|
||||
export const DRIVABLE_MAX_TYPE = 7; // trunk … service in data.roadTypes
|
||||
|
||||
export function buildGraph(roads, {drivable = false} = {}) {
|
||||
const ids = new Map(), xs = [], zs = [], adj = [];
|
||||
const node = (x, z) => {
|
||||
const k = `${x},${z}`;
|
||||
if (!ids.has(k)) {
|
||||
ids.set(k, xs.length);
|
||||
xs.push(x);
|
||||
zs.push(z);
|
||||
adj.push([]);
|
||||
}
|
||||
return ids.get(k);
|
||||
};
|
||||
for (const road of roads) {
|
||||
if (drivable && road[0] > DRIVABLE_MAX_TYPE) continue;
|
||||
let prev = node(road[2], road[3]);
|
||||
for (let i = 4; i < road.length; i += 2) {
|
||||
const next = node(road[i], road[i + 1]), cost = Math.hypot(xs[next] - xs[prev], zs[next] - zs[prev]);
|
||||
if (next !== prev) {
|
||||
adj[prev].push(next, cost);
|
||||
adj[next].push(prev, cost);
|
||||
}
|
||||
prev = next;
|
||||
}
|
||||
}
|
||||
const graph = {xs, zs, adj, main: largestComponent(adj), grid: new Map()};
|
||||
graph.main.forEach((inMain, i) => {
|
||||
if (!inMain) return;
|
||||
const k = cellKey(Math.floor(xs[i] / 50), Math.floor(zs[i] / 50));
|
||||
if (!graph.grid.has(k)) graph.grid.set(k, []);
|
||||
graph.grid.get(k).push(i);
|
||||
});
|
||||
return graph;
|
||||
}
|
||||
|
||||
const cellKey = (cx, cz) => cx * 100003 + cz;
|
||||
|
||||
function largestComponent(adj) {
|
||||
const label = new Int32Array(adj.length).fill(-1), sizes = [];
|
||||
for (let s = 0; s < adj.length; s++) {
|
||||
if (label[s] >= 0) continue;
|
||||
const id = sizes.length, stack = [s];
|
||||
let size = 0;
|
||||
label[s] = id;
|
||||
while (stack.length) {
|
||||
const n = stack.pop();
|
||||
size++;
|
||||
for (let i = 0; i < adj[n].length; i += 2) if (label[adj[n][i]] < 0) {
|
||||
label[adj[n][i]] = id;
|
||||
stack.push(adj[n][i]);
|
||||
}
|
||||
}
|
||||
sizes.push(size);
|
||||
}
|
||||
const best = sizes.indexOf(Math.max(...sizes));
|
||||
return Array.from(label, l => l === best);
|
||||
}
|
||||
|
||||
// Nearest node of the main connected component, searching outward ring by ring.
|
||||
export function nearestNode(graph, x, z) {
|
||||
const cx = Math.floor(x / 50), cz = Math.floor(z / 50);
|
||||
let best = -1, bestD = Infinity;
|
||||
for (let ring = 0; ring < 40; ring++) {
|
||||
for (let i = cx - ring; i <= cx + ring; i++) for (let j = cz - ring; j <= cz + ring; j++) {
|
||||
if (Math.max(Math.abs(i - cx), Math.abs(j - cz)) !== ring) continue;
|
||||
for (const n of graph.grid.get(cellKey(i, j)) ?? []) {
|
||||
const d = Math.hypot(graph.xs[n] - x, graph.zs[n] - z);
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best >= 0 && bestD < ring * 50) break;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
export function findPath(graph, start, goal) {
|
||||
if (start < 0 || goal < 0) return null;
|
||||
const {xs, zs, adj} = graph, g = new Map([[start, 0]]), from = new Map(), closed = new Set();
|
||||
const h = n => Math.hypot(xs[n] - xs[goal], zs[n] - zs[goal]);
|
||||
const heap = [[h(start), start]];
|
||||
const push = item => {
|
||||
heap.push(item);
|
||||
for (let i = heap.length - 1; i > 0;) {
|
||||
const p = (i - 1) >> 1;
|
||||
if (heap[p][0] <= heap[i][0]) break;
|
||||
[heap[p], heap[i]] = [heap[i], heap[p]];
|
||||
i = p;
|
||||
}
|
||||
};
|
||||
const pop = () => {
|
||||
const top = heap[0], last = heap.pop();
|
||||
if (heap.length) {
|
||||
heap[0] = last;
|
||||
for (let i = 0; ;) {
|
||||
const l = i * 2 + 1, r = l + 1;
|
||||
let m = i;
|
||||
if (l < heap.length && heap[l][0] < heap[m][0]) m = l;
|
||||
if (r < heap.length && heap[r][0] < heap[m][0]) m = r;
|
||||
if (m === i) break;
|
||||
[heap[m], heap[i]] = [heap[i], heap[m]];
|
||||
i = m;
|
||||
}
|
||||
}
|
||||
return top;
|
||||
};
|
||||
while (heap.length) {
|
||||
const [, n] = pop();
|
||||
if (n === goal) {
|
||||
const path = [n];
|
||||
while (from.has(path[0])) path.unshift(from.get(path[0]));
|
||||
return path;
|
||||
}
|
||||
if (closed.has(n)) continue;
|
||||
closed.add(n);
|
||||
for (let i = 0; i < adj[n].length; i += 2) {
|
||||
const m = adj[n][i], cost = g.get(n) + adj[n][i + 1];
|
||||
if (cost < (g.get(m) ?? Infinity)) {
|
||||
g.set(m, cost);
|
||||
from.set(m, n);
|
||||
push([cost + h(m), m]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function routeBetween(graph, ax, az, bx, bz) {
|
||||
const path = findPath(graph, nearestNode(graph, ax, az), nearestNode(graph, bx, bz));
|
||||
if (!path) return null;
|
||||
const points = [{x: ax, z: az}, ...path.map(n => ({x: graph.xs[n], z: graph.zs[n]})), {x: bx, z: bz}];
|
||||
return {points, length: polylineLength(points)};
|
||||
}
|
||||
|
||||
export function polylineLength(points) {
|
||||
let total = 0;
|
||||
for (let i = 1; i < points.length; i++) total += Math.hypot(points[i].x - points[i - 1].x, points[i].z - points[i - 1].z);
|
||||
return total;
|
||||
}
|
||||
|
||||
// Spatial index of road segments, used for street names and keeping trees off paths.
|
||||
export function buildSegmentIndex(roads) {
|
||||
const grid = new Map();
|
||||
for (const road of roads) for (let i = 4; i < road.length; i += 2) {
|
||||
const seg = [road[i - 2], road[i - 1], road[i], road[i + 1], road[0], road[1]];
|
||||
for (let cx = Math.floor(Math.min(seg[0], seg[2]) / 40); cx <= Math.floor(Math.max(seg[0], seg[2]) / 40); cx++)
|
||||
for (let cz = Math.floor(Math.min(seg[1], seg[3]) / 40); cz <= Math.floor(Math.max(seg[1], seg[3]) / 40); cz++) {
|
||||
const k = cellKey(cx, cz);
|
||||
if (!grid.has(k)) grid.set(k, []);
|
||||
grid.get(k).push(seg);
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
export function nearestSegment(index, x, z, maxDistance, filter = () => true) {
|
||||
let best = null, bestD = maxDistance;
|
||||
for (let cx = Math.floor((x - maxDistance) / 40); cx <= Math.floor((x + maxDistance) / 40); cx++)
|
||||
for (let cz = Math.floor((z - maxDistance) / 40); cz <= Math.floor((z + maxDistance) / 40); cz++)
|
||||
for (const s of index.get(cellKey(cx, cz)) ?? []) {
|
||||
if (!filter(s)) continue;
|
||||
const dx = s[2] - s[0], dz = s[3] - s[1];
|
||||
const t = Math.max(0, Math.min(1, ((x - s[0]) * dx + (z - s[1]) * dz) / (dx * dx + dz * dz || 1)));
|
||||
const d = Math.hypot(x - s[0] - dx * t, z - s[1] - dz * t);
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = {type: s[4], name: s[5], distance: d, x: s[0] + dx * t, z: s[1] + dz * t, dx, dz};
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
Reference in New Issue
Block a user