import { startTracking, requestCompass, insideWorld, createStabilizer, MAX_WALKING_SPEED, } from '../device/realworld.js'; import data from '../../data/mellat.json'; import { resolveCircle } from '../world/collision.js'; import { fa } from '../economy/gameState.js'; import { formatDistance } from '../../utils/geo.js'; /** * Runs the set real mode operation for this subsystem. * @param {*} context - context. * @param {*} on - on. * @returns {*} Operation result, when applicable. */ export function setRealMode(context, on) { if (on === context.real.on) return; context.real.stop?.(); Object.assign(context.real, { on, fix: null, heading: null, error: null, waiting: null, stop: null, }); for (const s of context.stations) if (s.def.test) s.model.group.visible = on; context.ui.classState('gps-toggle', 'on', on); context.ui.patch('gps-status', { hidden: !on, }); context.ui.patch('ar-toggle', { hidden: context.ui.patch('stable-toggle', { hidden: !on, }), }); // Real-world mode is first person: the player is where the phone is and sees what it faces. context.ui.classState('body', 'real', on); if (!on) { context.setArMode(false); context.arView.stopSensors(); return; } context.arView.startSensors(); context.cam.arPitch = 0; context.cam.yaw = Math.atan2(-Math.cos(context.player.heading), Math.sin(context.player.heading)); if (context.mode === 'drive') context.toggleCar(); context.mode = 'walk'; context.real.stop = startTracking({ onFix: (fix) => { context.real.fix = fix; context.real.error = null; }, onHeading: (heading) => (context.real.heading = heading), onError: (message) => (context.real.error = message), onStatus: (message) => (context.real.waiting = message), stabilizer: (context.real.stabilizer = createStabilizer()), }); context.real.stabilizer.stable = context.stableMode; context.say( 'father', 'حالت دنیای واقعی روشن شد؛ از چشم خودمون می‌بینیم و هر جا راه بریم، توی بازی هم همون‌جاییم.', ); context.setArMode(true); } // ---------- First-person real-world view ---------- // The view stands at the GPS position at eye height and turns with the phone. With the camera // picture on (AR) the city model is hidden and only the machines, their reach circles, the route // and the robot are drawn over the real street; with it off, the 3D city is shown first person. /** * Runs the set ar mode operation for this subsystem. * @param {*} context - context. * @param {*} on - on. * @returns {*} Operation result, when applicable. */ export async function setArMode(context, on) { if (on && !context.arMode) { const problem = await context.arView.start(); if (problem) { context.say('father', `${problem} فعلاً شهر سه‌بعدی رو از چشم خودمون می‌بینیم.`); return; } if (!context.real.on) { context.arView.stop(); return; } context.say( 'robot', 'دوربین روشن شد! گوشی رو صاف جلوی خودت بگیر؛ ستون نورانی طلایی جای دستگاهه.', 'excited', ); context.setTimeout(() => { if (context.real.on && !context.arView.hasOrientation) context.say('father', 'حسگر جهت گوشی جواب نمی‌ده؛ برای چرخیدن، صفحه رو با انگشت بکش.'); }, 3000); } else if (!on && context.arMode) context.arView.stop(); else return; context.arMode = on; for (const object of context.hiddenInAr()) object.visible = !on; context.scene.fog = on ? null : context.fog; context.ui.classState('body', 'ar', on); context.ui.classState('ar-toggle', 'on', on); } /** * Runs the ask real mode operation for this subsystem. * @param {*} context - context. * @param {*} thenStart - then Start. * @returns {*} Operation result, when applicable. */ export // The safety notice comes first; permissions are requested from the tap on its button (iOS needs that). function askRealMode(context, thenStart) { if (context.real.on) { context.setRealMode(false); return; } context.ui.open('real-intro'); context.ui.on('real-go', async () => { context.ui.close('real-intro'); await requestCompass(); context.setRealMode(true); if (thenStart) context.startMission(); }); } /** * Updates real from the active simulation. * @param {*} context - context. * @param {*} dt - dt. * @returns {*} Operation result, when applicable. */ export function updateReal(context, dt) { let uiStatus; if (context.real.error) uiStatus = `⚠ ${context.real.error}`; else if (!context.real.fix) uiStatus = `📍 ${context.real.waiting ?? 'در حال یافتن موقعیت…'}`; else if (!insideWorld(data.bounds, context.real.fix.x, context.real.fix.z)) { const nearest = Math.min( ...context .activeStations() .map((s) => Math.hypot(s.x - context.real.fix.x, s.z - context.real.fix.z)), ); uiStatus = `📍 خارج از محدودهٔ بازی · ${formatDistance(nearest)} تا نزدیک‌ترین دستگاه`; } else uiStatus = `📍 GPS ±${fa(Math.round(context.real.fix.accuracy))} متر${context.real.fix.speed > MAX_WALKING_SPEED ? ' · حرکت سریع' : ''}`; context.ui.patch('gps-status', { text: uiStatus, }); if (!context.real.fix || !insideWorld(data.bounds, context.real.fix.x, context.real.fix.z)) { context.player.speed = 0; return; } // Walk the avatar towards the (building-free) GPS point instead of teleporting it. const target = resolveCircle(context.collision, context.real.fix.x, context.real.fix.z, 1); const dx = target.x - context.player.x, dz = target.z - context.player.z, d = Math.hypot(dx, dz); if (d > 150) Object.assign(context.player, { x: target.x, z: target.z, }); const speed = d > 0.5 ? Math.min(8, Math.max(1.2, d * 1.5)) : 0; context.player.speed += (speed - context.player.speed) * Math.min(1, dt * 6); if (d > 0.5) { const step = Math.min(d, context.player.speed * dt); context.player.x += (dx / d) * step; context.player.z += (dz / d) * step; } const want = context.real.heading ?? (d > 0.5 ? Math.atan2(-dz, dx) : context.player.heading); // Stable mode also calms the compass, which jitters near cars and metal. context.player.heading += Math.atan2(Math.sin(want - context.player.heading), Math.cos(want - context.player.heading)) * Math.min(1, dt * (context.stableMode ? 2 : 5)); context.player.gait += dt * context.player.speed * 1.9; } // ---------- Loop ----------