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,60 @@
|
||||
import {toLocal} from './geo.js';
|
||||
|
||||
// Real-world mode: the family follows the phone's GPS position. Phone GPS in a city is off by
|
||||
// 5–20 m and jumps around, so fixes are blended by their accuracy and arrival uses a wide radius.
|
||||
|
||||
export const MAX_WALKING_SPEED = 6; // m/s (~22 km/h); faster means a vehicle, so buying pauses.
|
||||
|
||||
// Blends a new fix into the previous estimate, trusting whichever is more accurate.
|
||||
export function smoothFix(previous, fix) {
|
||||
if (!previous || fix.time - previous.time > 30000) return {...fix};
|
||||
const weight = previous.accuracy ** 2 / (previous.accuracy ** 2 + fix.accuracy ** 2);
|
||||
const dt = Math.max(.001, (fix.time - previous.time) / 1000);
|
||||
const x = previous.x + (fix.x - previous.x) * weight, z = previous.z + (fix.z - previous.z) * weight;
|
||||
// The blended estimate is a little better than either input, but never better than 3 m.
|
||||
const accuracy = Math.max(3, Math.sqrt(1 / (1 / previous.accuracy ** 2 + 1 / fix.accuracy ** 2)) + Math.hypot(x - previous.x, z - previous.z) * .2);
|
||||
return {x, z, accuracy, time: fix.time, speed: fix.speed ?? Math.hypot(x - previous.x, z - previous.z) / dt};
|
||||
}
|
||||
|
||||
// How close counts as "at the machine": never tighter than 20 m, wider when GPS is poor.
|
||||
export const arrivalRadius = accuracy => Math.min(40, Math.max(20, accuracy * 1.5));
|
||||
|
||||
export const insideWorld = (bounds, x, z) => x > bounds.minX && x < bounds.maxX && z > bounds.minZ && z < bounds.maxZ;
|
||||
|
||||
// Watches the phone's position and compass. Calls onFix({x, z, accuracy, speed, lat, lon}),
|
||||
// onHeading(radians in the game's heading convention) and onError(message).
|
||||
export function startTracking({onFix, onHeading, onError}) {
|
||||
if (!('geolocation' in navigator)) {
|
||||
onError('این دستگاه موقعیتیابی (GPS) ندارد.');
|
||||
return () => {};
|
||||
}
|
||||
let estimate = null;
|
||||
const watch = navigator.geolocation.watchPosition(position => {
|
||||
const {latitude: lat, longitude: lon, accuracy, speed} = position.coords, local = toLocal(lat, lon);
|
||||
estimate = smoothFix(estimate, {...local, accuracy: Math.max(3, accuracy), speed: Number.isFinite(speed) ? speed : null, time: position.timestamp});
|
||||
onFix({...estimate, lat, lon});
|
||||
}, error => onError(error.code === error.PERMISSION_DENIED ? 'اجازهٔ دسترسی به موقعیت داده نشد. از تنظیمات مرورگر اجازه بدهید.' : 'موقعیت پیدا نشد؛ به فضای باز بروید.'), {
|
||||
enableHighAccuracy: true, maximumAge: 2000, timeout: 20000
|
||||
});
|
||||
// Compass: iOS reports webkitCompassHeading; Android reports absolute alpha (counter-clockwise).
|
||||
const orientation = event => {
|
||||
const compass = event.webkitCompassHeading ?? (event.absolute && event.alpha != null ? 360 - event.alpha : null);
|
||||
// Game heading 0 faces east and grows counter-clockwise, compass 0 faces north and grows clockwise.
|
||||
if (compass != null) onHeading(Math.PI / 2 - compass * Math.PI / 180);
|
||||
};
|
||||
const eventName = 'ondeviceorientationabsolute' in window ? 'deviceorientationabsolute' : 'deviceorientation';
|
||||
window.addEventListener(eventName, orientation);
|
||||
return () => {
|
||||
navigator.geolocation.clearWatch(watch);
|
||||
window.removeEventListener(eventName, orientation);
|
||||
};
|
||||
}
|
||||
|
||||
// iOS needs an explicit permission prompt for the compass, and it must come from a tap.
|
||||
export async function requestCompass() {
|
||||
try {
|
||||
if (typeof DeviceOrientationEvent !== 'undefined' && DeviceOrientationEvent.requestPermission) await DeviceOrientationEvent.requestPermission();
|
||||
} catch {
|
||||
// Without the compass the family faces the direction of travel instead.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user