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:
@@ -0,0 +1,24 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {smoothFix, arrivalRadius, insideWorld} from '../src/realworld.js';
|
||||
|
||||
test('GPS fixes blend by accuracy and a stale estimate is replaced', () => {
|
||||
const first = smoothFix(null, {x: 0, z: 0, accuracy: 10, time: 0});
|
||||
assert.deepEqual([first.x, first.z], [0, 0]);
|
||||
const equal = smoothFix(first, {x: 10, z: 0, accuracy: 10, time: 1000});
|
||||
assert.ok(Math.abs(equal.x - 5) < 1e-9, 'equal accuracy meets halfway');
|
||||
assert.ok(equal.accuracy < 10, 'two fixes are better than one');
|
||||
const poor = smoothFix(first, {x: 50, z: 0, accuracy: 60, time: 1000});
|
||||
assert.ok(poor.x < 3, 'a poor fix barely moves a good estimate');
|
||||
const stale = smoothFix(first, {x: 99, z: 0, accuracy: 30, time: 60000});
|
||||
assert.equal(stale.x, 99);
|
||||
});
|
||||
|
||||
test('Arrival radius widens with poor GPS and bounds check the world', () => {
|
||||
assert.equal(arrivalRadius(5), 20);
|
||||
assert.equal(arrivalRadius(20), 30);
|
||||
assert.equal(arrivalRadius(100), 40);
|
||||
const bounds = {minX: -10, minZ: -10, maxX: 10, maxZ: 10};
|
||||
assert.ok(insideWorld(bounds, 0, 0));
|
||||
assert.equal(insideWorld(bounds, 11, 0), false);
|
||||
});
|
||||
@@ -16,6 +16,7 @@ test('Discovery, combo and all-category bonuses are paid once', () => {
|
||||
assert.equal(total(buy(park, 'juice')), POINTS.purchase, 'same type: no combo');
|
||||
assert.equal(total(buy(park, 'cake')), POINTS.purchase + POINTS.combo);
|
||||
assert.equal(total(buy(park, 'water')), POINTS.purchase, 'combo only once');
|
||||
buy(machineById('factory'), 'water');
|
||||
buy(machineById('school-rahil'), 'water');
|
||||
buy(machineById('ferdowsi-university'), 'water');
|
||||
buy(machineById('azadi-metro'), 'water');
|
||||
@@ -29,11 +30,12 @@ test('Ranks and missions advance with progress', () => {
|
||||
assert.equal(rankFor(RANKS[1].xp).index, 1);
|
||||
assert.equal(rankFor(1e6).progress, 1);
|
||||
const state = createState();
|
||||
assert.equal(activeMission(state).id, 'first');
|
||||
assert.equal(activeMission(state).id, 'factory');
|
||||
purchase(state, machineById('factory'), 'water');
|
||||
assert.deepEqual(completeMissions(state).map(m => m.id), ['factory']);
|
||||
assert.equal(state.balance, 500 - 8 + 20, 'factory sells at half price');
|
||||
purchase(state, machineById('mellat-park'), 'water');
|
||||
const done = completeMissions(state);
|
||||
assert.deepEqual(done.map(m => m.id), ['first']);
|
||||
assert.equal(state.balance, 500 - 15 + 30);
|
||||
assert.deepEqual(completeMissions(state).map(m => m.id), ['first']);
|
||||
assert.deepEqual(completeMissions(state), [], 'rewards are not repeated');
|
||||
assert.equal(activeMission(state).id, 'school');
|
||||
});
|
||||
|
||||
+6
-4
@@ -5,7 +5,7 @@ import {toLocal, toLatLon, bearing, ORIGIN} from '../src/geo.js';
|
||||
import {createCollisionWorld, resolveCircle, blocked, clearFraction} from '../src/collision.js';
|
||||
import {buildGraph, routeBetween, nearestSegment, buildSegmentIndex} from '../src/routing.js';
|
||||
import {createCar, stepCar} from '../src/vehicle.js';
|
||||
import {machines} from '../src/locations.js';
|
||||
import {machines, machineById, FACTORY} from '../src/locations.js';
|
||||
|
||||
const data = JSON.parse(readFileSync(new URL('../src/data/mellat.json', import.meta.url)));
|
||||
|
||||
@@ -27,10 +27,12 @@ test('Circles are pushed out of footprints and segments stop at walls', () => {
|
||||
assert.ok(Math.abs(clearFraction(world, -10, 5, 10, 5) - .5) < .06);
|
||||
});
|
||||
|
||||
test('Real map: every machine is reachable on foot and by car from the start', () => {
|
||||
test('Real map: the factory connects by car to the city, and every city machine is reachable', () => {
|
||||
const walk = buildGraph(data.roads), drive = buildGraph(data.roads, {drivable: true});
|
||||
const start = machines[0].anchor;
|
||||
for (const m of machines) {
|
||||
const toCity = routeBetween(drive, FACTORY.center.x, FACTORY.center.z, -445.8, -275.7);
|
||||
assert.ok(toCity && toCity.length > 18000 && toCity.length < 23000, `factory → Park Mellat is ~20 km (${toCity?.length})`);
|
||||
const start = machineById('mellat-park').anchor;
|
||||
for (const m of machines.filter(m => m.anchor)) {
|
||||
const onFoot = routeBetween(walk, start.x, start.z, m.anchor.x, m.anchor.z);
|
||||
assert.ok(onFoot, `walk route to ${m.id}`);
|
||||
assert.ok(onFoot.length >= Math.hypot(m.anchor.x - start.x, m.anchor.z - start.z) - 1);
|
||||
|
||||
Reference in New Issue
Block a user