# عملیات مشهد — روبات مارکت بازی سه‌بعدی مرورگری (Three.js) با حال‌وهوای بازی‌های اکشن سوم‌شخص، بدون سلاح و خشونت. یک پدر و فرزندش از کارخانهٔ سازندهٔ دستگاه‌ها راه می‌افتند، در خیابان‌های واقعی مشهد می‌گردند، دستگاه‌های وندینگ روبات مارکت را پیدا می‌کنند و با هر خرید امتیاز می‌گیرند. روبات راهنمای روبات مارکت داخل دستگاه‌ها زندگی می‌کند: وقتی خانواده به یک دستگاه می‌رسند، از آن بیرون می‌آید، خوش‌آمد می‌گوید و راهنمایی‌شان می‌کند. بازی یک PWA است: روی گوشی نصب می‌شود و آفلاین هم اجرا می‌شود. 🎮 [اجرای آنلاین بازی](https://hamedabdollahzade.github.io/vending-machine-game/) ## داستان و هدف - **شروع:** سالن تولید کارخانهٔ **شرکت روبات مارکت** (بزرگراه مشهد–چناران، خیابان کاله، کنار پارک علم و فناوری خراسان). کارخانه سالن تولید با خط مونتاژ، دفتر، حیاط، دروازه و تابلوی شرکت دارد. ماشین خانواده در حیاط پارک است. - **مسیر:** از کارخانه تا بوستان ملت حدود ۲۰ کیلومتر رانندگی است، از بزرگراه مشهد–چناران، بزرگراه پیامبر اعظم، بزرگراه آزادی و وکیل‌آباد. - **نقشهٔ ایران:** صفحهٔ توجیه مأموریت نقشهٔ استان‌های ایران را نشان می‌دهد. فعلاً فقط مشهد (خراسان رضوی) باز است و شهرهای دیگر قفل‌اند. - **محدودهٔ قابل بازی:** حدود ۱۵ در ۱۸ کیلومتر از شمال‌غرب تا غرب مشهد، همه از داده‌های واقعی OpenStreetMap: - جزئیات کامل اطراف کارخانه و محدودهٔ بوستان ملت و دانشگاه فردوسی - همهٔ خیابان‌ها و ساختمان‌ها در فاصلهٔ ۲۵۰ متری مسیر رانندگی - شبکهٔ خیابان‌های اصلی در کل منطقه - **دستگاه‌ها:** دستگاه نمونهٔ کارخانه (نصف قیمت)، بوستان ملت، مجموعه آموزشی راحیل (فقط ۷:۳۰ تا ۱۳:۳۰)، دانشگاه فردوسی و دانشکدهٔ ادبیات (۲۰٪ تخفیف دانشجویی)، دانشگاه علوم پزشکی، ایستگاه مترو آزادی، مشهد مال (از ساعت ۱۰). همهٔ دستگاه‌ها و محصولاتشان فرضی‌اند. ## کنترل | کار | صفحه‌کلید | موبایل | |---|---|---| | حرکت / رانندگی | W A S D یا جهت‌ها | جوی‌استیک | | دویدن | Shift | دکمهٔ «دویدن» | | چرخاندن دوربین | ماوس (با کلیک قفل می‌شود) یا کشیدن | کشیدن روی صفحه | | زوم دوربین | چرخ ماوس | — | | سوار و پیاده شدن از ماشین | F | دکمهٔ F | | خرید از دستگاه | E | دکمهٔ E | | کشیدن کارت | C | دکمهٔ E | | ترمز دستی | Space | — | | نقشهٔ شهر | M یا کلیک روی نقشهٔ گوشه | لمس نقشه | کنترل‌ها با صفحه‌کلید فارسی هم کار می‌کنند. دوربین در بازی عادی سوم‌شخص است و در حالت دنیای واقعی اول‌شخص. روی گوشی، اعلان‌ها (گفت‌وگوها، دکمهٔ خرید، پیام مأموریت، امتیاز و وضعیت GPS) وسط صفحه نمایش داده می‌شوند. خود دکمهٔ خرید هم قابل لمس است. ## نقشه و مسیریابی با کلیک روی نقشهٔ گرد گوشهٔ صفحه، نقشهٔ کامل شهر باز می‌شود. فهرست دستگاه‌ها با آدرس، ساعت کاری، باز یا بسته بودن، محصولات و فاصله نمایش داده می‌شود. «مسیریابی تا اینجا» مسیر را روی شبکهٔ واقعی خیابان‌ها پیدا می‌کند (پیاده روی همهٔ مسیرها، با ماشین فقط روی خیابان‌های سواره‌رو). مسیر به شکل خط طلایی روی زمین، روی نقشهٔ گوشه و روی قطب‌نمای بالای صفحه نمایش داده می‌شود. بدون انتخاب دستی، هدف مأموریت فعلی مقصد است. ## امتیاز | رویداد | امتیاز | |---|---| | هر خرید | ۱۰۰ | | اولین خرید در هر مکان (کشف) | ۲۵۰ | | خرید نوشیدنی و خوراکی از یک دستگاه | ۱۵۰ | | خرید از هر پنج نوع مکان شهری | ۵۰۰ | | خرید در حالت دنیای واقعی | ۲۰۰ | هفت مأموریت امتیاز و پاداش نقدی (هتومان) دارند. درجه‌ها از «تازه‌وارد» تا «افسانهٔ مشهد» بالا می‌روند. بودجهٔ اولیه ۵۰۰ هتومان است (۱۰ هتومان = ۱۰۰٬۰۰۰ ریال). وقتی پول کافی برای هیچ خریدی نماند، یا همهٔ مأموریت‌ها انجام شود، عملیات تمام می‌شود. هر ثانیهٔ واقعی ده ثانیه از ساعت بازی است. ## نصب روی گوشی (PWA) - **اندروید و Chrome کامپیوتر:** دکمهٔ «نصب بازی روی گوشی» در صفحهٔ شروع، یا گزینهٔ Install در منوی مرورگر - **آیفون:** در Safari، دکمهٔ Share و بعد Add to Home Screen. دکمهٔ نصب در بازی همین راهنما را نشان می‌دهد. آیکون از آواتار روبات روبات مارکت ساخته شده (`public/icons`). بعد از اولین اجرا همهٔ فایل‌ها، از جمله نقشه و فونت Vazirmatn (داخل پروژه، بدون Google Fonts)، کش می‌شوند و بازی آفلاین اجرا می‌شود. با انتشار نسخهٔ جدید، پیام «نسخهٔ جدید بازی آماده است» نمایش داده می‌شود. Service Worker را `vite.config.js` در زمان build می‌سازد. ## حالت دنیای واقعی (GPS) دکمهٔ «📍 بازی در دنیای واقعی» در صفحهٔ شروع یا «📍 دنیای واقعی» در نوار پایین این حالت را روشن می‌کند. اول نکته‌های ایمنی نمایش داده می‌شود و بعد اجازهٔ موقعیت و قطب‌نما گرفته می‌شود. - **اول‌شخص:** نما از چشم خودتان است. دوربین بازی در جای گوشی (GPS) و ارتفاع چشم قرار می‌گیرد و فقط با راه رفتن واقعی جابه‌جا می‌شود. صفحه‌کلید و جوی‌استیک در این حالت کاری نمی‌کنند. جهت نگاه از حسگر جهت گوشی گرفته می‌شود. اگر گوشی حسگر جهت نداشته باشد، جهت از قطب‌نما و جهت راه رفتن گرفته می‌شود و با کشیدن انگشت هم می‌چرخید. - موقعیت‌های GPS بر اساس دقتشان نرم و ترکیب می‌شوند. «رسیدن» به دستگاه یعنی رسیدن به محدودهٔ اطرافش، با شعاع ۲٫۵ برابر خطای GPS (بین ۲۰ تا ۵۰ متر). این محدوده با یک دایره روی زمین نشان داده می‌شود. وقتی رسیدید، روبات از دستگاه بیرون می‌آید و خرید ۲۰۰ امتیاز اضافه دارد. - **حالت پایدار** (دکمهٔ «🎯 پایدار»، پیش‌فرض روشن) جلوی پرش‌های GPS را می‌گیرد: - پرش‌هایی که با راه رفتن ممکن نیستند نادیده گرفته می‌شوند. - تا وقتی جابه‌جایی کمتر از حدود نصف خطای GPS باشد، مکان شما ثابت می‌ماند. - قطب‌نما و چرخش دوربین آرام‌تر دنبال می‌شوند. - **نمای دوربین (AR):** با روشن شدن حالت دنیای واقعی، دوربین پشت گوشی پس‌زمینهٔ بازی می‌شود. دوربین بازی در جای گوشی (GPS) و هم‌جهت با گوشی (قطب‌نما و حسگر جهت) قرار می‌گیرد، پس دستگاه‌ها، ستون نور، دایرهٔ محدوده، مسیر طلایی و روبات در جهت واقعی روی تصویر دوربین دیده می‌شوند. مدل شهر و ماشین‌ها در این نما پنهان‌اند. دکمهٔ «📷 دوربین» بین نمای دوربین و شهر سه‌بعدیِ اول‌شخص جابه‌جا می‌کند. اگر گوشی حسگر جهت نداشته باشد، با کشیدن انگشت روی صفحه می‌چرخید. - در سرعت بیش از حدود ۲۲ کیلومتر بر ساعت خرید غیرفعال است. ماشین مجازی هم در این حالت استفاده نمی‌شود. - بیرون از محدودهٔ بازی، فاصله تا نزدیک‌ترین دستگاه نمایش داده می‌شود. - موقعیت فقط روی خود گوشی پردازش می‌شود. دوربین و قطب‌نما به HTTPS نیاز دارند. - اول یک موقعیت سریع و تقریبی از Wi-Fi یا شبکهٔ موبایل گرفته می‌شود و بعد GPS دقیقش می‌کند. وضعیت زیر نام خیابان با کد خطا نمایش داده می‌شود: - **کد ۱:** اجازهٔ Location داده نشده - **کد ۲:** Location گوشی خاموش است - **کد ۳:** هنوز سیگنال نرسیده (جست‌وجو ادامه دارد) روی کامپیوتر معمولاً موقعیتی پیدا نمی‌شود، چون Chrome برای کامپیوتر از سرویس موقعیت گوگل استفاده می‌کند که از ایران در دسترس نیست. با گوشی تست کنید. **نقطه‌های آزمایشی GPS:** سه دستگاه آزمایشی فقط در حالت دنیای واقعی کنار کارخانه ظاهر می‌شوند، در مختصات `36.450276, 59.423431`، `36.450172, 59.423209` و `36.449976, 59.422866`. مختصات، نقطه‌ای است که باید آنجا بایستید. با گوشی تا هر نقطه راه بروید: روبات بیرون می‌آید و دکمهٔ خرید فعال می‌شود. شعاع رسیدن ۲٫۵ برابر خطای GPS است (بین ۲۰ تا ۵۰ متر) و همیشه نزدیک‌ترین دستگاه انتخاب می‌شود. دوربین، GPS و حسگرها فقط روی HTTPS (یا localhost) کار می‌کنند. ## اجرا ```sh npm install npm run dev npm run build node --test tests/*.test.js ``` - `?quality=low` یا دکمهٔ «گرافیک» سایه‌ها را خاموش می‌کند (پیش‌فرض گوشی‌ها). - `?debug` وضعیت بازی را در `window.game` در دسترس می‌گذارد. ## انتشار روی سرور با Docker تصویر Docker دو مرحله دارد: Node تست‌ها را اجرا و بازی را build می‌کند، و بعد Caddy فایل‌ها را سرو می‌کند. Caddy گواهی HTTPS را از Let's Encrypt خودکار می‌گیرد و تمدید می‌کند. HTTPS برای نصب روی گوشی و GPS ضروری است. **پیش‌نیاز:** - رکورد A دامنه‌های `robotmarkets.ir` و `www.robotmarkets.ir` به IP سرور اشاره کند. - پورت‌های ۸۰ و ۴۴۳ سرور باز و آزاد باشند. ```sh git clone git@github.com:hamedAbdollahzade/vending-machine-game.git && cd vending-machine-game docker compose up -d --build docker compose logs -f ``` - **دامنهٔ دیگر:** `SITE_ADDRESS="example.ir, www.example.ir" docker compose up -d --build` - **به‌روزرسانی:** `git pull && docker compose up -d --build`. گواهی‌ها در volume `caddy_data` می‌مانند. بازی‌های نصب‌شده روی گوشی پیام نسخهٔ جدید را نشان می‌دهند. - **اگر روی سرور از قبل Nginx یا پروکسی دیگری روی ۸۰ و ۴۴۳ هست:** در `docker-compose.yml` پورت‌ها را به `"8080:80"` تغییر دهید و `SITE_ADDRESS=":80"` بگذارید. بعد دامنه را در همان پروکسی به `127.0.0.1:8080` بفرستید. در این حالت HTTPS با همان پروکسی است. **سرور داخل ایران:** Docker Hub و گاهی npm به IPهای ایران سرویس نمی‌دهند. دو راه وجود دارد: 1. **mirror داخلی برای Docker Hub:** در `/etc/docker/daemon.json` یک mirror تعریف کنید، مثلاً `{"registry-mirrors": ["https://docker.arvancloud.ir"]}`، و بعد `sudo systemctl restart docker`. 2. **ساخت روی کامپیوتر خودتان و انتقال تصویر آماده:** ```sh docker compose build docker save robot-market-game:latest | gzip > robot-market-game.tar.gz scp robot-market-game.tar.gz docker-compose.yml user@server:~/game/ ssh user@server 'cd ~/game && gunzip -c robot-market-game.tar.gz | docker load && docker compose up -d --no-build' ``` ## CI/CD روی Gitea مخزن اصلی `ssh://git@git.my-rm.com:2222/Hamed/vending-machine-game.git` با نام `origin` است. GitHub با نام `github` باقی مانده. گردش‌کار [.gitea/workflows/ci-cd.yml](.gitea/workflows/ci-cd.yml) با هر push خودش اجرا می‌شود: - **هر push و pull request:** نصب، تست‌ها و build - **push به `main`:** بعد از قبولی تست‌ها، کد از راه SSH به سرور فرستاده می‌شود و `docker compose up -d --build` اجرا می‌شود. **راه‌اندازی یک‌باره:** 1. در سرور، Docker و Docker Compose نصب باشد و کاربر SSH بتواند `docker` اجرا کند (عضو گروه `docker` یا root). 2. یک کلید SSH مخصوص انتشار بسازید و کلید عمومی را به `~/.ssh/authorized_keys` سرور اضافه کنید: ```sh ssh-keygen -t ed25519 -N '' -f deploy_key -C gitea-deploy ssh-copy-id -i deploy_key.pub user@server ``` 3. در Gitea، بخش Settings → Actions → Secrets مخزن، این‌ها را تعریف کنید: - `DEPLOY_HOST`: آدرس سرور - `DEPLOY_SSH_KEY`: محتوای فایل `deploy_key` - در صورت نیاز: `DEPLOY_USER` (پیش‌فرض root)، `DEPLOY_PORT` (پیش‌فرض ۲۲) و `DEPLOY_KNOWN_HOSTS` (خروجی `ssh-keyscan server`) 4. اختیاری، در بخش Variables: - `DEPLOY_PATH`: پوشهٔ روی سرور - `SITE_ADDRESS`: دامنه‌ها - `HEALTH_URL`: مثلاً `https://robotmarkets.ir/`، تا بعد از انتشار بالا آمدن سایت چک شود - `NPM_REGISTRY`: اگر runner یا سرور به `registry.npmjs.org` دسترسی ندارد (معمول در سرورهای ایران)، آدرس یک mirror در دسترس npm را اینجا بگذارید. همین مقدار برای build روی سرور هم استفاده می‌شود. تا وقتی `DEPLOY_HOST` تعریف نشده، مرحلهٔ انتشار رد می‌شود و فقط تست و build اجرا می‌شوند. هر مرحله محدودیت زمانی دارد (تست ۱۵ دقیقه، انتشار ۳۰ دقیقه) تا runner هیچ‌وقت بی‌پایان معطل نماند. ## ساخت دوبارهٔ دادهٔ نقشه `src/data/mellat.json` و `src/data/iran.json` از خروجی خام OpenStreetMap (Overpass API) و Natural Earth ساخته می‌شوند. کوئری‌ها در ابتدای اسکریپت آمده‌اند: ```sh node scripts/build-map-data.mjs --osm city.json zone-factory.json corridor.json major.json --poi poi.json --provinces ne_10m_admin_1_states_provinces.geojson ``` چهار خروجی OSM (محدودهٔ شهر، محدودهٔ کارخانه، کریدور مسیر و خیابان‌های اصلی) با هم ادغام می‌شوند و عنصرهای تکراری حذف می‌شوند. خیابان‌ها هم در مرز منطقه بریده می‌شوند. برای افزودن محدودهٔ جدید، یک خروجی دیگر به `--osm` اضافه کنید و در صورت نیاز `REGION` را در اسکریپت بزرگ‌تر کنید. ### ساختمان‌ها (Microsoft) OSM فقط حدود ۴٬۰۰۰ ساختمان این منطقه را دارد. بقیه (حدود ۱۴۱٬۵۰۰ ساختمان) از مجموعهٔ ساختمان‌های جهانی Microsoft (مجوز ODbL) می‌آیند و در `public/data/buildings.bin` ذخیره می‌شوند. این فایل ۳٫۵ مگابایت است و فشرده‌شده حدود ۲ مگابایت منتقل می‌شود. ```sh node scripts/build-buildings.mjs <فایل quadkey ایران، مثلاً 123012100 .csv.gz> ``` فایل از [فهرست Microsoft](https://minedbuildings.z5.web.core.windows.net/global-buildings/dataset-links.csv) گرفته می‌شود (ردیف `Iran,123012100`). داده‌های Microsoft برای ایران ارتفاع ندارند، پس ارتفاع تخمین زده می‌شود: | موقعیت و مساحت | ارتفاع | |---|---| | پارک | یک طبقه | | صنعتی، یا بیرون از کمربندی پیامبر اعظم، بالای ۲۵۰ متر مربع | سوله، ۶ تا ۹ متر | | بیرون از کمربندی، کوچک‌تر | ۱ تا ۲ طبقه | | داخل شهر، زیر ۴۰ متر مربع | ۱ طبقه | | داخل شهر، زیر ۱۲۰ متر مربع | ۲ تا ۳ طبقه | | داخل شهر، زیر ۳۰۰ متر مربع | ۳ تا ۴ طبقه | | داخل شهر، زیر ۸۰۰ متر مربع | ۴ تا ۶ طبقه | | داخل شهر، بزرگ‌تر | ۵ تا ۸ طبقه | ساختمان‌هایی که OSM هم دارد، و ساختمان‌های داخل محوطهٔ کارخانه (که بازی خودش می‌سازد) حذف می‌شوند. ساختمان‌ها در بلوک‌های ۵۰۰ متری و فقط وقتی بازیکن نزدیک می‌شود ساخته می‌شوند. ### تصویر ماهواره‌ای (Esri World Imagery) زمین بازی تصویر ماهواره‌ای واقعی است و جاده‌ها، لاین‌ها، پارک‌ها و حیاط‌ها همان‌طور که در واقعیت هستند دیده می‌شوند. با وجود تصویر، جاده‌ها و زمین‌های رسم‌شده ساخته نمی‌شوند. ```sh node scripts/fetch-imagery.mjs ``` - **زوم ۱۷ (حدود ۱ متر در هر پیکسل):** محدودهٔ شهر، محدودهٔ کارخانه و ۳۰۰ متر دو طرف مسیر کارخانه تا بوستان ملت - **زوم ۱۵:** کل منطقه، برای فاصله‌های دور کاشی‌ها (حدود ۱۰۰۰ فایل، ۱۶ مگابایت) در `public/imagery` روی سرور خودمان هستند، پس در ایران و به‌صورت آفلاین هم کار می‌کنند. Service Worker آن‌ها را از قبل دانلود نمی‌کند و هر کاشی را وقتی بازیکن از کنارش رد شد نگه می‌دارد. **مجوز:** استفادهٔ تجاری و میزبانی نسخه‌ای از تصاویر Esri نیاز به مجوز مناسب از Esri دارد. شرایط را با Esri بررسی کنید. نام منبع در پایین صفحهٔ بازی نمایش داده می‌شود. ## ساختار - `src/main.js`: صحنه، دوربین سوم‌شخص، کنترل‌ها، رابط کاربری (HUD)، نقشه، خرید و حلقهٔ بازی - `src/city.js`: ساخت شهر سه‌بعدی از داده‌ها (خیابان، پیاده‌رو، ساختمان، پارک، درخت، ورودی مترو، کوه‌ها) در تکه‌های ۲ کیلومتری که دورها پنهان می‌شوند - `src/map-render.js`: نقشهٔ گوشه، نقشهٔ کامل شهر (کاشی‌های کش‌شده و نمای کلی) و نقشهٔ ایران - `src/routing.js`: گراف خیابان‌ها، مسیریابی A* و پیدا کردن نام خیابان - `src/collision.js`: برخورد با ساختمان‌ها برای شخصیت‌ها، ماشین و دوربین - `src/locations.js`: دستگاه‌ها، آدرس‌ها، ساعت کاری و محصولات - `src/game-state.js`، `src/payment.js`، `src/scoring.js`: پول، خرید، پرداخت، امتیاز، درجه و مأموریت‌ها - `src/characters.js`: پدر و فرزند، و روبات راهنما که از دستگاه‌ها بیرون می‌آید - `src/factory.js`: محوطهٔ کارخانه، شامل سالن تولید، خط مونتاژ، دفتر، حیاط و تابلوها - `src/pwa.js` و `public/manifest.webmanifest`: نصب، کار آفلاین و پیام به‌روزرسانی - `src/realworld.js`: GPS، قطب‌نما، نرم‌سازی و حالت پایدار موقعیت، و شعاع رسیدن - `src/imagery.js`: زمین ماهواره‌ای که کاشی‌هایش با نزدیک شدن بارگذاری و با دور شدن آزاد می‌شوند - `src/buildings-data.js`: خواندن فایل ساختمان‌های Microsoft - `src/ar.js`: نمای دوربین (AR)، شامل تصویر دوربین پشت گوشی، جهت گوشی و زاویهٔ دید - `src/vending.js`، `src/vehicle.js`، `src/traffic.js`: مدل دستگاه، فیزیک ماشین و ترافیک شهری - `src/geo.js`: تبدیل طول و عرض جغرافیایی به مختصات بازی ## محدودیت‌ها و گام‌های بعدی - مدل‌ها هندسی و سبک‌اند و کیفیت گرافیکی بازی‌های AAA را ندارند. - خیابان‌ها یک‌طرفه و چراغ راهنمایی ندارند، و ترافیک فقط روی بلوارهای اصلی است. - پیشنهادهای بعدی: امتیاز ایمنی (خط عابر و چراغ قرمز)، تاکسی و قطار شهری، چرخهٔ روز و شب، ذخیرهٔ پیشرفت، جدول امتیاز آنلاین، و محله‌ها و شهرهای دیگر. نقشه: © مشارکت‌کنندگان OpenStreetMap (ODbL). ساختمان‌ها: © Microsoft (ODbL). تصویر ماهواره‌ای: Esri, Maxar, Earthstar Geographics, and the GIS User Community. مرز استان‌ها: Natural Earth.