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:
2026-10-26 08:56:06 +03:30
co-authored by Claude Opus 5.5
parent e2a1b5fba1
commit 0939dac9cd
39 changed files with 1111 additions and 228 deletions
+6 -4
View File
@@ -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');
});