179 lines
6.6 KiB
JavaScript
179 lines
6.6 KiB
JavaScript
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 ----------
|