First-person real-world mode and centred notifications on phones
- Real-world mode is first person: the view stands at the GPS position at eye height and only real walking moves it; the phone's orientation aims it, with the camera picture (AR) or over the 3D city when the camera is off. Without an orientation sensor it follows the compass or walking direction and can be dragged. The family, car and run buttons are hidden in this mode - Orientation sensors are now separate from the camera stream in ar.js - On phones, dialogue, the buy prompt, banners, points and GPS status sit in the middle of the screen; the prompt can be tapped; the toolbar is one scrollable row and no longer covers the budget and clock Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,9 @@
|
|||||||
| ترمز دستی | Space | — |
|
| ترمز دستی | Space | — |
|
||||||
| نقشهٔ شهر | M یا کلیک روی نقشهٔ گوشه | لمس نقشه |
|
| نقشهٔ شهر | M یا کلیک روی نقشهٔ گوشه | لمس نقشه |
|
||||||
|
|
||||||
کنترلها با صفحهکلید فارسی هم کار میکنند. دوربین فقط سومشخص است.
|
کنترلها با صفحهکلید فارسی هم کار میکنند. دوربین در بازی عادی سومشخص است و در حالت دنیای واقعی اولشخص.
|
||||||
|
|
||||||
|
روی گوشی، اعلانها (گفتوگوها، دکمهٔ خرید، پیام مأموریت، امتیاز و وضعیت GPS) وسط صفحه نمایش داده میشوند. خود دکمهٔ خرید هم قابل لمس است.
|
||||||
|
|
||||||
## نقشه و مسیریابی
|
## نقشه و مسیریابی
|
||||||
|
|
||||||
@@ -60,13 +62,13 @@
|
|||||||
|
|
||||||
دکمهٔ «📍 بازی در دنیای واقعی» در صفحهٔ شروع یا «📍 دنیای واقعی» در نوار پایین این حالت را روشن میکند. اول نکتههای ایمنی نمایش داده میشود و بعد اجازهٔ موقعیت و قطبنما گرفته میشود.
|
دکمهٔ «📍 بازی در دنیای واقعی» در صفحهٔ شروع یا «📍 دنیای واقعی» در نوار پایین این حالت را روشن میکند. اول نکتههای ایمنی نمایش داده میشود و بعد اجازهٔ موقعیت و قطبنما گرفته میشود.
|
||||||
|
|
||||||
- خانواده روی نقشهٔ بازی همانجایی است که گوشی هست و با راه رفتن واقعی جابهجا میشود. جهتشان از قطبنمای گوشی یا جهت حرکت گرفته میشود.
|
- **اولشخص:** نما از چشم خودتان است. دوربین بازی در جای گوشی (GPS) و ارتفاع چشم قرار میگیرد و فقط با راه رفتن واقعی جابهجا میشود. صفحهکلید و جویاستیک در این حالت کاری نمیکنند. جهت نگاه از حسگر جهت گوشی گرفته میشود. اگر گوشی حسگر جهت نداشته باشد، جهت از قطبنما و جهت راه رفتن گرفته میشود و با کشیدن انگشت هم میچرخید.
|
||||||
- موقعیتهای GPS بر اساس دقتشان نرم و ترکیب میشوند. «رسیدن» به دستگاه یعنی رسیدن به محدودهٔ اطرافش، با شعاع ۲٫۵ برابر خطای GPS (بین ۲۰ تا ۵۰ متر). این محدوده با یک دایره روی زمین نشان داده میشود. وقتی رسیدید، روبات از دستگاه بیرون میآید و خرید ۲۰۰ امتیاز اضافه دارد.
|
- موقعیتهای GPS بر اساس دقتشان نرم و ترکیب میشوند. «رسیدن» به دستگاه یعنی رسیدن به محدودهٔ اطرافش، با شعاع ۲٫۵ برابر خطای GPS (بین ۲۰ تا ۵۰ متر). این محدوده با یک دایره روی زمین نشان داده میشود. وقتی رسیدید، روبات از دستگاه بیرون میآید و خرید ۲۰۰ امتیاز اضافه دارد.
|
||||||
- **حالت پایدار** (دکمهٔ «🎯 پایدار»، پیشفرض روشن) جلوی پرشهای GPS را میگیرد:
|
- **حالت پایدار** (دکمهٔ «🎯 پایدار»، پیشفرض روشن) جلوی پرشهای GPS را میگیرد:
|
||||||
- پرشهایی که با راه رفتن ممکن نیستند نادیده گرفته میشوند.
|
- پرشهایی که با راه رفتن ممکن نیستند نادیده گرفته میشوند.
|
||||||
- تا وقتی جابهجایی کمتر از حدود نصف خطای GPS باشد، مکان شما ثابت میماند.
|
- تا وقتی جابهجایی کمتر از حدود نصف خطای GPS باشد، مکان شما ثابت میماند.
|
||||||
- قطبنما و چرخش دوربین آرامتر دنبال میشوند.
|
- قطبنما و چرخش دوربین آرامتر دنبال میشوند.
|
||||||
- **نمای دوربین (AR):** با روشن شدن حالت دنیای واقعی، دوربین پشت گوشی پسزمینهٔ بازی میشود. دوربین بازی در جای گوشی (GPS) و همجهت با گوشی (قطبنما و حسگر جهت) قرار میگیرد، پس دستگاهها، ستون نور، دایرهٔ محدوده، مسیر طلایی و روبات در جهت واقعی روی تصویر دوربین دیده میشوند. مدل شهر و ماشینها در این نما پنهاناند. دکمهٔ «📷 دوربین» بین نمای دوربین و نقشهٔ سهبعدی جابهجا میکند. اگر گوشی حسگر جهت نداشته باشد، با کشیدن انگشت روی صفحه میچرخید.
|
- **نمای دوربین (AR):** با روشن شدن حالت دنیای واقعی، دوربین پشت گوشی پسزمینهٔ بازی میشود. دوربین بازی در جای گوشی (GPS) و همجهت با گوشی (قطبنما و حسگر جهت) قرار میگیرد، پس دستگاهها، ستون نور، دایرهٔ محدوده، مسیر طلایی و روبات در جهت واقعی روی تصویر دوربین دیده میشوند. مدل شهر و ماشینها در این نما پنهاناند. دکمهٔ «📷 دوربین» بین نمای دوربین و شهر سهبعدیِ اولشخص جابهجا میکند. اگر گوشی حسگر جهت نداشته باشد، با کشیدن انگشت روی صفحه میچرخید.
|
||||||
- در سرعت بیش از حدود ۲۲ کیلومتر بر ساعت خرید غیرفعال است. ماشین مجازی هم در این حالت استفاده نمیشود.
|
- در سرعت بیش از حدود ۲۲ کیلومتر بر ساعت خرید غیرفعال است. ماشین مجازی هم در این حالت استفاده نمیشود.
|
||||||
- بیرون از محدودهٔ بازی، فاصله تا نزدیکترین دستگاه نمایش داده میشود.
|
- بیرون از محدودهٔ بازی، فاصله تا نزدیکترین دستگاه نمایش داده میشود.
|
||||||
- موقعیت فقط روی خود گوشی پردازش میشود. دوربین و قطبنما به HTTPS نیاز دارند.
|
- موقعیت فقط روی خود گوشی پردازش میشود. دوربین و قطبنما به HTTPS نیاز دارند.
|
||||||
|
|||||||
@@ -51,13 +51,29 @@ export function createArView(video) {
|
|||||||
if (!event.absolute && compassOffset != null) alpha += compassOffset;
|
if (!event.absolute && compassOffset != null) alpha += compassOffset;
|
||||||
orientation = {alpha, beta: event.beta, gamma: event.gamma};
|
orientation = {alpha, beta: event.beta, gamma: event.gamma};
|
||||||
};
|
};
|
||||||
|
let sensing = false;
|
||||||
return {
|
return {
|
||||||
|
// Camera picture on (AR) or off (the 3D world fills the screen).
|
||||||
get active() {
|
get active() {
|
||||||
return Boolean(stream);
|
return Boolean(stream);
|
||||||
},
|
},
|
||||||
get hasOrientation() {
|
get hasOrientation() {
|
||||||
return Boolean(orientation);
|
return Boolean(orientation);
|
||||||
},
|
},
|
||||||
|
// Phone orientation, used by the first-person view with or without the camera picture.
|
||||||
|
startSensors() {
|
||||||
|
if (sensing) return;
|
||||||
|
sensing = true;
|
||||||
|
window.addEventListener('deviceorientationabsolute', onAbsolute);
|
||||||
|
window.addEventListener('deviceorientation', onOrientation);
|
||||||
|
},
|
||||||
|
stopSensors() {
|
||||||
|
sensing = false;
|
||||||
|
orientation = null;
|
||||||
|
absoluteSeen = false;
|
||||||
|
window.removeEventListener('deviceorientationabsolute', onAbsolute);
|
||||||
|
window.removeEventListener('deviceorientation', onOrientation);
|
||||||
|
},
|
||||||
// Opens the rear camera. Resolves to null on success or to a message explaining the failure.
|
// Opens the rear camera. Resolves to null on success or to a message explaining the failure.
|
||||||
async start() {
|
async start() {
|
||||||
if (stream) return null;
|
if (stream) return null;
|
||||||
@@ -71,37 +87,29 @@ export function createArView(video) {
|
|||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
video.hidden = false;
|
video.hidden = false;
|
||||||
await video.play().catch(() => {});
|
await video.play().catch(() => {});
|
||||||
window.addEventListener('deviceorientationabsolute', onAbsolute);
|
this.startSensors();
|
||||||
window.addEventListener('deviceorientation', onOrientation);
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
// Closes the camera picture; the sensors keep running for the first-person view.
|
||||||
stop() {
|
stop() {
|
||||||
stream?.getTracks().forEach(track => track.stop());
|
stream?.getTracks().forEach(track => track.stop());
|
||||||
stream = null;
|
stream = null;
|
||||||
orientation = null;
|
|
||||||
absoluteSeen = false;
|
|
||||||
video.srcObject = null;
|
video.srcObject = null;
|
||||||
video.hidden = true;
|
video.hidden = true;
|
||||||
window.removeEventListener('deviceorientationabsolute', onAbsolute);
|
|
||||||
window.removeEventListener('deviceorientation', onOrientation);
|
|
||||||
},
|
},
|
||||||
// Points the 3D camera like the phone. `smoothing` (per second) trades lag for steadiness.
|
// Points the 3D camera like the phone. With the camera picture on, the field of view matches it.
|
||||||
|
// `smoothing` (per second) trades lag for steadiness. Returns false without orientation data.
|
||||||
update(camera, dt, smoothing = 8) {
|
update(camera, dt, smoothing = 8) {
|
||||||
const screenAspect = innerWidth / innerHeight, videoAspect = video.videoWidth && video.videoHeight ? video.videoWidth / video.videoHeight : 4 / 3;
|
const fov = stream ? coverFov(video.videoWidth && video.videoHeight ? video.videoWidth / video.videoHeight : 4 / 3, innerWidth / innerHeight) : 62;
|
||||||
const fov = coverFov(videoAspect, screenAspect);
|
|
||||||
if (Math.abs(camera.fov - fov) > .1) {
|
if (Math.abs(camera.fov - fov) > .1) {
|
||||||
camera.fov = fov;
|
camera.fov = fov;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
}
|
}
|
||||||
if (!orientation) return;
|
if (!orientation) return false;
|
||||||
const angle = screen.orientation?.angle ?? window.orientation ?? 0;
|
const angle = screen.orientation?.angle ?? window.orientation ?? 0;
|
||||||
orientationQuaternion(target, orientation.alpha, orientation.beta, orientation.gamma, angle);
|
orientationQuaternion(target, orientation.alpha, orientation.beta, orientation.gamma, angle);
|
||||||
camera.quaternion.slerp(target, 1 - Math.exp(-dt * smoothing));
|
camera.quaternion.slerp(target, 1 - Math.exp(-dt * smoothing));
|
||||||
},
|
return true;
|
||||||
// Compass bearing (degrees from north) the camera is looking along.
|
|
||||||
heading(camera) {
|
|
||||||
const dir = camera.getWorldDirection(new THREE.Vector3());
|
|
||||||
return (Math.atan2(dir.x, -dir.z) * 180 / Math.PI + 360) % 360;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-19
@@ -279,10 +279,11 @@ canvas.addEventListener('wheel', e => {
|
|||||||
|
|
||||||
function look(dx, dy, k) {
|
function look(dx, dy, k) {
|
||||||
// In AR view the phone's orientation aims the camera; dragging only helps when there is no sensor.
|
// In AR view the phone's orientation aims the camera; dragging only helps when there is no sensor.
|
||||||
if (arMode) {
|
if (real.on) {
|
||||||
if (arView.hasOrientation) return;
|
if (arView.hasOrientation) return;
|
||||||
cam.yaw -= dx * k;
|
cam.yaw -= dx * k;
|
||||||
cam.arPitch = THREE.MathUtils.clamp((cam.arPitch ?? 0) - dy * k * .8, -1.2, 1.2);
|
cam.arPitch = THREE.MathUtils.clamp((cam.arPitch ?? 0) - dy * k * .8, -1.2, 1.2);
|
||||||
|
cam.lastMouse = elapsed;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cam.yaw -= dx * k;
|
cam.yaw -= dx * k;
|
||||||
@@ -491,6 +492,12 @@ function cancel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('#swipe').onclick = swipe;
|
$('#swipe').onclick = swipe;
|
||||||
|
// On phones the prompt itself is the button: tap it to buy, swipe the card, or get in/out of the car.
|
||||||
|
$('#prompt').onclick = () => {
|
||||||
|
if (payment.phase === 'ready') swipe();
|
||||||
|
else if (nearStation()) interact();
|
||||||
|
else toggleCar();
|
||||||
|
};
|
||||||
$('#cancel-payment').onclick = cancel;
|
$('#cancel-payment').onclick = cancel;
|
||||||
$('#shop-close').onclick = () => $('#shop').close();
|
$('#shop-close').onclick = () => $('#shop').close();
|
||||||
|
|
||||||
@@ -906,10 +913,16 @@ function setRealMode(on) {
|
|||||||
$('#gps-toggle').classList.toggle('on', on);
|
$('#gps-toggle').classList.toggle('on', on);
|
||||||
$('#gps-status').hidden = !on;
|
$('#gps-status').hidden = !on;
|
||||||
$('#ar-toggle').hidden = $('#stable-toggle').hidden = !on;
|
$('#ar-toggle').hidden = $('#stable-toggle').hidden = !on;
|
||||||
|
// Real-world mode is first person: the player is where the phone is and sees what it faces.
|
||||||
|
document.body.classList.toggle('real', on);
|
||||||
if (!on) {
|
if (!on) {
|
||||||
setArMode(false);
|
setArMode(false);
|
||||||
|
arView.stopSensors();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
arView.startSensors();
|
||||||
|
cam.arPitch = 0;
|
||||||
|
cam.yaw = Math.atan2(-Math.cos(player.heading), Math.sin(player.heading));
|
||||||
if (mode === 'drive') toggleCar();
|
if (mode === 'drive') toggleCar();
|
||||||
mode = 'walk';
|
mode = 'walk';
|
||||||
real.stop = startTracking({
|
real.stop = startTracking({
|
||||||
@@ -923,13 +936,14 @@ function setRealMode(on) {
|
|||||||
stabilizer: real.stabilizer = createStabilizer()
|
stabilizer: real.stabilizer = createStabilizer()
|
||||||
});
|
});
|
||||||
real.stabilizer.stable = stableMode;
|
real.stabilizer.stable = stableMode;
|
||||||
say('father', 'حالت دنیای واقعی روشن شد. هر جا ما بریم، روی نقشه هم همونجا هستیم.');
|
say('father', 'حالت دنیای واقعی روشن شد؛ از چشم خودمون میبینیم و هر جا راه بریم، توی بازی هم همونجاییم.');
|
||||||
setArMode(true);
|
setArMode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Camera AR view ----------
|
// ---------- First-person real-world view ----------
|
||||||
// In real-world mode the phone camera is the background; the city model is hidden and only the
|
// The view stands at the GPS position at eye height and turns with the phone. With the camera
|
||||||
// machines, their reach circles, the route and the robot are drawn over the real street.
|
// 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.
|
||||||
const arView = createArView($('#camera'));
|
const arView = createArView($('#camera'));
|
||||||
let arMode = false, stableMode = true;
|
let arMode = false, stableMode = true;
|
||||||
const hiddenInAr = () => [sky, city.group, factory.group, traffic.group, carModel.group, parkGate];
|
const hiddenInAr = () => [sky, city.group, factory.group, traffic.group, carModel.group, parkGate];
|
||||||
@@ -938,7 +952,7 @@ async function setArMode(on) {
|
|||||||
if (on && !arMode) {
|
if (on && !arMode) {
|
||||||
const problem = await arView.start();
|
const problem = await arView.start();
|
||||||
if (problem) {
|
if (problem) {
|
||||||
say('father', `${problem} فعلاً روی نقشه بازی میکنیم.`);
|
say('father', `${problem} فعلاً شهر سهبعدی رو از چشم خودمون میبینیم.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!real.on) {
|
if (!real.on) {
|
||||||
@@ -946,9 +960,8 @@ async function setArMode(on) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
say('robot', 'دوربین روشن شد! گوشی رو صاف جلوی خودت بگیر؛ ستون نورانی طلایی جای دستگاهه.', 'excited');
|
say('robot', 'دوربین روشن شد! گوشی رو صاف جلوی خودت بگیر؛ ستون نورانی طلایی جای دستگاهه.', 'excited');
|
||||||
cam.arPitch = 0;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (arMode && !arView.hasOrientation) say('father', 'حسگر جهت گوشی جواب نمیده؛ برای چرخیدن، صفحه رو با انگشت بکش.');
|
if (real.on && !arView.hasOrientation) say('father', 'حسگر جهت گوشی جواب نمیده؛ برای چرخیدن، صفحه رو با انگشت بکش.');
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} else if (!on && arMode) arView.stop();
|
} else if (!on && arMode) arView.stop();
|
||||||
else return;
|
else return;
|
||||||
@@ -957,10 +970,6 @@ async function setArMode(on) {
|
|||||||
scene.fog = on ? null : fog;
|
scene.fog = on ? null : fog;
|
||||||
document.body.classList.toggle('ar', on);
|
document.body.classList.toggle('ar', on);
|
||||||
$('#ar-toggle').classList.toggle('on', on);
|
$('#ar-toggle').classList.toggle('on', on);
|
||||||
if (!on) {
|
|
||||||
camera.fov = 62;
|
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#ar-toggle').onclick = () => setArMode(!arMode);
|
$('#ar-toggle').onclick = () => setArMode(!arMode);
|
||||||
@@ -1301,7 +1310,7 @@ function frame() {
|
|||||||
if (mode === 'walk' && engine && soundOn) engine.g.gain.setTargetAtTime(0, audio.currentTime, .2);
|
if (mode === 'walk' && engine && soundOn) engine.g.gain.setTargetAtTime(0, audio.currentTime, .2);
|
||||||
updateSubtitles(dt);
|
updateSubtitles(dt);
|
||||||
|
|
||||||
family.group.visible = mode === 'walk' && !arMode;
|
family.group.visible = mode === 'walk' && !real.on;
|
||||||
family.group.position.set(player.x, 0, player.z);
|
family.group.position.set(player.x, 0, player.z);
|
||||||
family.group.rotation.y = player.heading;
|
family.group.rotation.y = player.heading;
|
||||||
family.update({dt, time: elapsed, gait: player.gait, moving: player.speed > .4, shopping: state.busy || payment.phase !== 'idle', delivery: deliveryTimer > 0});
|
family.update({dt, time: elapsed, gait: player.gait, moving: player.speed > .4, shopping: state.busy || payment.phase !== 'idle', delivery: deliveryTimer > 0});
|
||||||
@@ -1316,13 +1325,25 @@ function frame() {
|
|||||||
updateReachCircles();
|
updateReachCircles();
|
||||||
if (routeMesh) routeMaterial.opacity = .55 + Math.sin(elapsed * 4) * .15;
|
if (routeMesh) routeMaterial.opacity = .55 + Math.sin(elapsed * 4) * .15;
|
||||||
|
|
||||||
if (playing && arMode) {
|
if (playing && real.on) {
|
||||||
// The phone is the camera: at the player's GPS position, eye height, pointing like the phone.
|
// First person: the phone is the camera, at the player's GPS position and eye height.
|
||||||
camera.position.set(player.x, 1.5, player.z);
|
camera.position.set(player.x, 1.6, player.z);
|
||||||
arView.update(camera, dt, stableMode ? 5 : 10);
|
if (!arView.update(camera, dt, stableMode ? 5 : 10)) {
|
||||||
if (!arView.hasOrientation) camera.rotation.set(cam.arPitch ?? 0, cam.yaw, 0, 'YXZ');
|
// No orientation sensor: face the compass or walking direction, or wherever the player drags.
|
||||||
|
if (real.heading != null && elapsed - cam.lastMouse > 2) {
|
||||||
|
const want = Math.atan2(-Math.cos(player.heading), Math.sin(player.heading));
|
||||||
|
cam.yaw += Math.atan2(Math.sin(want - cam.yaw), Math.cos(want - cam.yaw)) * Math.min(1, dt * 3);
|
||||||
|
}
|
||||||
|
camera.rotation.set(cam.arPitch ?? 0, cam.yaw, 0, 'YXZ');
|
||||||
|
}
|
||||||
camPos.copy(camera.position);
|
camPos.copy(camera.position);
|
||||||
} else if (playing) updateCamera(dt);
|
} else if (playing) {
|
||||||
|
if (camera.fov !== 62) {
|
||||||
|
camera.fov = 62;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
updateCamera(dt);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
const a = elapsed * .08;
|
const a = elapsed * .08;
|
||||||
camera.position.set(player.x + Math.cos(a) * 30, 18, player.z + Math.sin(a) * 30);
|
camera.position.set(player.x + Math.cos(a) * 30, 18, player.z + Math.sin(a) * 30);
|
||||||
|
|||||||
@@ -317,3 +317,39 @@ body.ar { background: #000; }
|
|||||||
body.ar #crosshair { display: none; }
|
body.ar #crosshair { display: none; }
|
||||||
#ar-toggle[hidden], #stable-toggle[hidden] { display: none; }
|
#ar-toggle[hidden], #stable-toggle[hidden] { display: none; }
|
||||||
#ar-toggle.on, #stable-toggle.on { background: var(--teal); color: #04201b; border-color: var(--teal); }
|
#ar-toggle.on, #stable-toggle.on { background: var(--teal); color: #04201b; border-color: var(--teal); }
|
||||||
|
|
||||||
|
/* ---------- Real-world first person, and notifications in the middle on phones ---------- */
|
||||||
|
body.real #crosshair, body.real #stick { display: none; }
|
||||||
|
#prompt { cursor: pointer; }
|
||||||
|
@media (max-width: 760px), (max-height: 520px) {
|
||||||
|
#banner { top: 24%; padding: 10px; }
|
||||||
|
#banner strong { font-size: 18px; }
|
||||||
|
#awards { top: 36%; }
|
||||||
|
#subtitle {
|
||||||
|
top: 50%; bottom: auto; transform: translate(-50%, -50%); width: min(92vw, 520px);
|
||||||
|
font-size: 15px; background: rgba(0, 0, 0, .7); border-radius: 10px;
|
||||||
|
}
|
||||||
|
#prompt { top: 64%; bottom: auto; font-size: 16px; padding: 12px 20px; border-radius: 10px; white-space: normal; text-align: center; max-width: 90vw; }
|
||||||
|
#gps-status {
|
||||||
|
position: fixed; top: 76px; left: 50%; transform: translateX(-50%); width: max-content; max-width: 92vw;
|
||||||
|
padding: 4px 12px; background: var(--panel-solid); border: 1px solid var(--line); border-radius: 999px; text-align: center;
|
||||||
|
}
|
||||||
|
#payment { bottom: auto; top: 50%; transform: translate(50%, -50%); }
|
||||||
|
}
|
||||||
|
/* Phone layout: one scrollable toolbar row, stats above it, compact rank bar above the compass,
|
||||||
|
and no car or run buttons while the GPS moves the player. */
|
||||||
|
body.real .touch-buttons [data-key='f'], body.real .touch-buttons [data-key='shift'] { display: none; }
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
#rank { top: 4px; padding: 2px 8px; font-size: 11px; }
|
||||||
|
#rank .bar { height: 3px; margin-top: 2px; }
|
||||||
|
#compass { top: 36px; }
|
||||||
|
/* Between the mission banner and the dialogue, clear of the minimap and mission list. */
|
||||||
|
#gps-status { top: 42%; }
|
||||||
|
#toolbar {
|
||||||
|
left: 8px; right: 8px; bottom: 8px; overflow-x: auto; flex-wrap: nowrap; scrollbar-width: none;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
}
|
||||||
|
#toolbar button { flex: 0 0 auto; white-space: nowrap; }
|
||||||
|
#status { bottom: 52px; }
|
||||||
|
#touch { bottom: 150px; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user