Start at the factory, add PWA install and GPS real-world mode
The game now begins inside the Negin Sazan Pishgam Radmehr Atousa vending-machine factory on the Mashhad–Chenaran road, and the world extends ~20 km along the real driving route to Park Mellat and Ferdowsi University. - Factory compound: production hall with assembly line, office, yard, gate and company signs; first mission at its half-price demo machine - Map data merged from four OSM extracts (factory zone, route corridor, region main roads, city zone), clipped to a 15 x 18 km region - City meshes chunked and culled by distance; traffic recycled around the player; minimap and city map drawn from cached tiles + overview - The Robot Market robot no longer walks with the family: it comes out of a machine when they arrive, greets them with expression portraits and goes back inside when they leave - PWA: manifest, icons from the robot avatar, offline service worker generated at build time, install button / iOS steps, update prompt; Vazirmatn font bundled instead of Google Fonts - Real-world mode: safety notice, GPS + compass tracking with accuracy smoothing, 20-40 m arrival radius, speed lock and in-person bonus Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+39
-16
@@ -43,33 +43,56 @@ export function createCarModel(color = '#d9d9d4') {
|
||||
|
||||
const COLORS = ['#e9e9e4', '#1d1f22', '#9aa3a8', '#d6c9a5', '#7e1f1d', '#2f4a6b', '#f2c94c', '#efefea'];
|
||||
|
||||
// Ambient cars that drive up and down the main boulevards and stop for anything in front of them.
|
||||
export function createTraffic(scene, data, count = 32) {
|
||||
const roads = data.roads.filter(r => r[0] <= 2).map(r => {
|
||||
// Ambient cars on the main roads. They stay within a few hundred metres of the player: a car that
|
||||
// falls too far behind is moved to a road ahead or around them. Cars stop for anything in their lane.
|
||||
export function createTraffic(scene, data, count = 28) {
|
||||
const roads = data.roads.filter(r => r[0] <= 3).map(r => {
|
||||
const pts = [];
|
||||
for (let i = 2; i < r.length; i += 2) pts.push(new THREE.Vector2(r[i], r[i + 1]));
|
||||
const lengths = [0];
|
||||
for (let i = 1; i < pts.length; i++) lengths.push(lengths[i - 1] + pts[i].distanceTo(pts[i - 1]));
|
||||
return {pts, lengths, total: lengths.at(-1)};
|
||||
}).filter(r => r.total > 140).sort((a, b) => b.total - a.total);
|
||||
const cars = [];
|
||||
for (let i = 0; i < Math.min(count, roads.length * 2); i++) {
|
||||
const road = roads[i % roads.length], model = createCarModel(COLORS[i % COLORS.length]);
|
||||
scene.add(model.group);
|
||||
cars.push({road, model, d: (i * 97) % road.total, dir: i % 2 ? 1 : -1, speed: 8 + (i % 5) * 1.3, wait: 0, x: 0, z: 0});
|
||||
}
|
||||
return {pts, lengths, total: lengths.at(-1), lanes: r[0] <= 1 ? 2 : 1};
|
||||
}).filter(r => r.total > 80);
|
||||
const sample = (road, d) => {
|
||||
let i = 1;
|
||||
while (i < road.lengths.length - 1 && road.lengths[i] < d) i++;
|
||||
const a = road.pts[i - 1], b = road.pts[i], t = (d - road.lengths[i - 1]) / (road.lengths[i] - road.lengths[i - 1] || 1);
|
||||
return {x: a.x + (b.x - a.x) * t, z: a.y + (b.y - a.y) * t, dx: b.x - a.x, dz: b.y - a.y};
|
||||
};
|
||||
function update(dt, obstacles) {
|
||||
let seed = 7;
|
||||
const random = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
|
||||
// Places a car on a random main-road point between 120 and 600 m from (x, z), if one exists.
|
||||
function respawn(car, x, z) {
|
||||
for (let attempt = 0; attempt < 40; attempt++) {
|
||||
const road = roads[Math.floor(random() * roads.length)], d = random() * road.total, p = sample(road, d);
|
||||
const dist = Math.hypot(p.x - x, p.z - z);
|
||||
if (dist < 120 || dist > 600) continue;
|
||||
Object.assign(car, {road, d, dir: random() < .5 ? 1 : -1, lane: 2.2 + (road.lanes > 1 && random() < .5 ? 3.2 : 0), speed: 9 + random() * 7});
|
||||
car.model.group.visible = true;
|
||||
return true;
|
||||
}
|
||||
car.model.group.visible = false;
|
||||
return false;
|
||||
}
|
||||
const cars = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const model = createCarModel(COLORS[i % COLORS.length]);
|
||||
scene.add(model.group);
|
||||
model.group.visible = false;
|
||||
cars.push({model, road: null, d: 0, dir: 1, lane: 2.2, speed: 10, x: 1e9, z: 1e9});
|
||||
}
|
||||
let cursor = 0;
|
||||
function update(dt, obstacles, around) {
|
||||
// Re-home a couple of distant cars per frame, which keeps the cost per frame small.
|
||||
for (let n = 0; n < 2; n++) {
|
||||
const car = cars[cursor = (cursor + 1) % cars.length];
|
||||
if (!car.road || Math.hypot(car.x - around.x, car.z - around.z) > 800) respawn(car, around.x, around.z);
|
||||
}
|
||||
for (const car of cars) {
|
||||
if (!car.road || !car.model.group.visible) continue;
|
||||
const here = sample(car.road, car.d), len = Math.hypot(here.dx, here.dz) || 1;
|
||||
const fx = here.dx / len * car.dir, fz = here.dz / len * car.dir;
|
||||
// Yield to the player and to other cars directly ahead in the same lane.
|
||||
const ahead = obstacles.concat(cars.filter(c => c !== car)).some(o => {
|
||||
const ahead = obstacles.concat(cars.filter(c => c !== car && c.model.group.visible)).some(o => {
|
||||
const ox = o.x - car.x, oz = o.z - car.z, along = ox * fx + oz * fz;
|
||||
return along > 0 && along < 9 && Math.abs(ox * fz - oz * fx) < 2.2;
|
||||
});
|
||||
@@ -81,8 +104,8 @@ export function createTraffic(scene, data, count = 32) {
|
||||
}
|
||||
// Keep to the right-hand lane relative to the direction of travel.
|
||||
const p = sample(car.road, car.d);
|
||||
car.x = p.x - fz * 2.2;
|
||||
car.z = p.z + fx * 2.2;
|
||||
car.x = p.x - fz * car.lane;
|
||||
car.z = p.z + fx * car.lane;
|
||||
car.model.group.position.set(car.x, 0, car.z);
|
||||
car.model.group.rotation.y = Math.atan2(-fz, fx);
|
||||
for (const wheel of car.model.wheels) wheel.rotation.z -= speed * dt / .36;
|
||||
|
||||
Reference in New Issue
Block a user