Compare commits

...
29 Commits
Author SHA1 Message Date
HamedandClaude Opus 5.5 e02347142b feat: add drivable black luxury cars, Apache motorcycles and lane splitting
CI/CD / test (push) Successful in 57s
CI/CD / publish (push) Successful in 59s
CI/CD / deploy (push) Successful in 6s
- Park black BMW, Lamborghini, Mazda and Nissan style cars in the factory
  yard; Mr Mohammadi now arrives in a Bugatti that becomes drivable once
  parked; the delivery truck is black.
- Add six Apache style motorcycles with a rider, lean and their own
  handling (BIKE spec, 130 km/h).
- Let the player take any traffic car; a garage module owns every
  drivable vehicle and resets them.
- Replace the 3.4 m circle test between vehicles with oriented boxes
  sized from each model, so contact only happens when bodies touch and
  bikes can filter between cars; traffic only brakes for its own lane.
- Add tests for box collisions, bike handling, traffic lane checks and
  taking traffic cars.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 09:33:19 +03:30
HamedandClaude Opus 5.5 526796126d fix: draw street map ground and repair mobile layout
CI/CD / test (push) Successful in 57s
CI/CD / publish (push) Successful in 1m8s
CI/CD / deploy (push) Successful in 6s
- Turn the satellite ground off behind SATELLITE_GROUND so OSM streets,
  parks and land use are drawn; the imagery index is no longer fetched.
- Restore phone rules lost in the CSS-to-JS theme migration (#rank
  height, toolbar button size) and replace invalid -var() values.
- Add a landscape-phone layout and make the briefing scrollable.
- Add two-finger pinch zoom to the big map.
- Add regression tests for the theme and the skipped imagery request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:51:40 +03:30
Hamed df930694f7 update
CI/CD / test (push) Successful in 1m20s
CI/CD / publish (push) Successful in 56s
CI/CD / deploy (push) Successful in 5s
2026-10-28 13:26:44 +03:30
Hamed fbfa8d011d docs: document React architecture and enforce migration checks in CI
CI/CD / test (push) Successful in 1m15s
CI/CD / publish (push) Successful in 1m21s
CI/CD / deploy (push) Successful in 6s
2026-10-28 12:53:12 +03:30
Hamed 55d4123fe5 feat: add factory sample machines and robot pricing conversations 2026-10-28 12:53:07 +03:30
Hamed d0da416923 refactor: separate React views, Redux snapshots and Three.js runtime 2026-10-28 12:26:40 +03:30
Hamed 6bd9b17173 refactor: centralize world and imagery requests in Axios services 2026-10-28 12:14:08 +03:30
Hamed 08b6cd464b chore: establish formatting baseline and verification scripts 2026-10-28 12:11:36 +03:30
HamedandClaude Opus 5.5 751af768ce Factory departments, staff and a faster family car
CI/CD / test (push) Successful in 23s
CI/CD / publish (push) Successful in 24s
CI/CD / deploy (push) Successful in 5s
The factory now has an engineering office with five named developers and
the first-mission vending machine, an accounting office, and the manager's
room. Mr. Mohammadi drives in with his KMC T8 pickup and walks to his desk.
The production hall has five work areas staffed by 40 instanced workers, and
a truck loads packed machines at the dock and drives out of the gate.

The family car is a BMW-style sports sedan (no badges) parked beside the
start point, with a top speed of 200 km/h. Driving is now sliced into
sub-steps so the car can't tunnel through walls. Microsoft footprints that
reach into the compound are dropped, and the gate sign reads correctly
from inside the yard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:27:46 +03:30
HamedandClaude Opus 5.5 9b289f76b0 Satellite ground and real building footprints
CI/CD / test (push) Successful in 18s
CI/CD / publish (push) Successful in 22s
CI/CD / deploy (push) Successful in 6s
- The ground is Esri World Imagery: ~1 m/pixel tiles around the city
  zone, the factory and 300 m either side of the drive between them,
  coarser tiles for the whole region. Tiles are hosted with the game
  (public/imagery, scripts/fetch-imagery.mjs), stream in near the camera,
  are cached by the service worker as visited, and replace the drawn
  roads and land areas
- ~141,500 Microsoft building footprints (ODbL) join OSM's ~4,000 in a
  compact delta-encoded file (public/data/buildings.bin, built by
  scripts/build-buildings.mjs). Heights are estimated from footprint
  area and land use, with industrial halls outside the ring road.
  Buildings are built in 500 m blocks as the camera approaches
- Caddy compresses the buildings file and caches map data for a week;
  the build targets ES2022 for top-level await
- Tests for decoding, factory clearance, machine reachability and tile
  placement

Imagery attribution: Esri, Maxar, Earthstar Geographics, and the GIS
User Community. Commercial use requires a suitable Esri licence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:30:02 +03:30
HamedandClaude Opus 5.5 5cea802b97 First-person real-world mode and centred notifications on phones
CI/CD / test (push) Successful in 19s
CI/CD / publish (push) Successful in 15s
CI/CD / deploy (push) Successful in 5s
- 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>
2026-10-26 15:00:28 +03:30
HamedandClaude Opus 5.5 6ce560089f Add camera AR view, GPS stable mode and wider machine range
CI/CD / test (push) Successful in 28s
CI/CD / publish (push) Successful in 17s
CI/CD / deploy (push) Successful in 4s
- Real-world mode opens the phone's rear camera as the game background;
  the 3D camera stands at the GPS position and follows the phone's
  orientation, so machines, beacons, range circles, the route and the
  robot appear in their real direction. City, traffic and family are
  hidden in this view; a toggle switches back to the 3D map, and drag
  turns the view where there is no orientation sensor
- Stable mode (default on): drops GPS jumps impossible on foot, holds the
  position until it drifts past half the GPS error, calms the compass
- Arrival range is 2.5x the GPS error (20–50 m), shown as a ground circle
- Tests for the stabilizer, AR orientation and field of view

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:14:04 +03:30
RyanandClaude Sonnet 5.5 9564880783 Don't fail the publish job when the package is already linked
CI/CD / test (push) Successful in 21s
CI/CD / publish (push) Successful in 15s
CI/CD / deploy (push) Successful in 4s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 11:39:34 +03:30
RyanandClaude Sonnet 5.5 c1141ad3af Deploy through the runner's Docker socket instead of SSH
CI/CD / test (push) Successful in 22s
CI/CD / publish (push) Failing after 18s
CI/CD / deploy (push) Skipped
The runner lives on the deployment server, so the deploy job runs
docker compose locally with a temporary registry login.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 11:37:45 +03:30
Hamed 85f562984b Merge remote-tracking branch 'origin/main'
CI/CD / publish (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
CI/CD / test (push) Canceled after 6m8s
2026-10-26 11:23:41 +03:30
HamedandClaude Opus 5.5 8cc0e8ce80 Rename the factory to Robot Market, fix GPS search and add GPS test points
- The factory company is now "شرکت روبات مارکت" on signs, dialogue,
  briefing, map label, manifest and README
- Real-world mode no longer shows "location not found" for every
  problem: it takes a quick coarse Wi-Fi/network fix first, keeps
  searching after a GPS timeout, and reports permission denied (1),
  location off (2), still searching (3) or a non-HTTPS page separately
- Three GPS test machines at real coordinates beside the factory, only
  present in real-world mode; the closest machine within reach is used
  and the arrival radius is twice the GPS error (12–40 m)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 11:23:22 +03:30
RyanandClaude Sonnet 5.5 894e3e25c6 Deploy from a Gitea registry image instead of building on the server
CI/CD / test (push) Failing after 7m19s
CI/CD / publish (push) Skipped
CI/CD / deploy (push) Skipped
CI now builds and pushes the image to the Gitea container registry, links
the package, and the deploy job ships only deploy/docker-compose.yml and an
.env naming the image to /opt/vending-machine-game.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 10:46:57 +03:30
HamedandClaude Opus 5.5 9832f303e8 Make CI and Docker builds survive an unreachable npm registry
CI/CD / test (push) Failing after 7m58s
CI/CD / deploy (push) Skipped
The Gitea runner hung for over 30 minutes on `npm ci` because it cannot
reach registry.npmjs.org. The registry is now configurable through the
NPM_REGISTRY repository variable, passed on to the server's Docker
build, and npm gives up after bounded retries. Jobs also have time
limits (test 15 min, deploy 30 min) so the runner is never held forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:15:49 +03:30
HamedandClaude Opus 5.5 22aa11bf32 Add Gitea Actions CI/CD
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
Every push and pull request installs, tests and builds the game. Pushes
to main then ship the sources to the server over SSH and restart the
site with docker compose; the deploy step is skipped until the
DEPLOY_HOST / DEPLOY_SSH_KEY secrets exist. An optional HEALTH_URL is
polled after deploying.

The Gitea server is now the `origin` remote; GitHub remains as `github`.
Gitea ignores .github/workflows while .gitea/workflows exists, so the
GitHub Pages workflow keeps running only on GitHub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:07:54 +03:30
HamedandClaude Opus 5.5 47b2b72ea9 Add Docker deployment with automatic HTTPS
Deploy game to GitHub Pages / deploy (push) Failing after 21m51s
A two-stage image runs the tests and builds the game with Node, then
serves dist/ with Caddy, which obtains and renews Let's Encrypt
certificates for robotmarkets.ir (configurable via SITE_ADDRESS).
HTTPS is required for installing the PWA and for GPS mode.

- Caddyfile: gzip/zstd, long-lived caching for hashed assets, no-cache
  for the page, service worker and manifest, SPA fallback, permissions
  policy for location and motion sensors
- docker-compose.yml: ports 80/443, persistent certificate volumes
- README: server setup, updates, running behind an existing proxy, and
  workarounds when Docker Hub is unreachable from servers in Iran

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:59:01 +03:30
HamedandClaude Opus 5.5 2f74788caa Fix the car getting stuck on the Chenaran highway
Traffic cars picked a random direction even on one-way carriageways,
so some drove head-on in the player's lane and stopped dead in front of
them; every bump also threw the player's car backwards while the
traffic car stayed put, so the player could not get past.

- Map data records one-way roads (oneway tags, motorways, roundabouts)
- Traffic drives with the flow in one of two lanes on one-way roads and
  on the right-hand half of two-way roads, spawning only from nearby roads
- Bumps slow the player down instead of reversing them; a traffic car
  pressed against for more than 0.8 s pulls away out of sight
- Regression test for traffic direction and lanes; ?debug collision log

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:29:58 +03:30
HamedandClaude Opus 5.5 0939dac9cd 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>
2026-10-26 08:56:06 +03:30
Hamed e2a1b5fba1 update 2026-10-25 12:47:13 +03:30
HamedandClaude Opus 5.5 95a2ae851d Turn the game into a third-person Mashhad city game
Replace the linear forest walk with an open area of real western Mashhad
built from OpenStreetMap data, starting at Park Mellat. A father and child,
guided by the Robot Market robot, find vending machines at a park, school,
universities, a metro station and a mall, on foot or by car.

- Iran province briefing map; only Mashhad is unlocked
- Third-person camera, building collision, arcade car and ambient traffic
- Minimap and full city map with addresses, opening hours and A* routing
- Purchase scoring, missions with cash rewards, ranks and an end screen
- Map data build script (OSM + Natural Earth) and new logic tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:27:48 +03:30
Hamed 8a49dc07fa Replace child character with Robot Market mascot 2026-10-22 14:36:12 +03:30
Hamed 6d4c4b7dbe Fix GitHub Pages deployment and document game URL 2026-10-21 11:06:58 +03:30
Hamed b5f3137fff update 2026-10-21 10:58:14 +03:30
Hamed f329acc61c update 2026-10-20 13:30:44 +03:30
Hamed 5857b11bd8 Add interactive picnic journey and synchronize gameplay stages 2026-10-20 12:26:14 +03:30
1158 changed files with 206716 additions and 2656 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "game",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "npx vite build && npx vite preview --port 4173 --strictPort"],
"port": 4173
}
]
}
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
.git
.github
.idea
.claude
*.log
+144
View File
@@ -0,0 +1,144 @@
# Gitea Actions. While .gitea/workflows exists Gitea ignores .github/workflows (GitHub Pages only).
#
# Every push and pull request: install, test and build.
# Push to main: build the Docker image, push it to the Gitea registry, then start it with
# docker compose through the runner's Docker socket (the runner lives on the deployment server).
#
# Repository settings → Actions → Secrets:
# REGISTRY_USERNAME Gitea user that publishes images
# REGISTRY_TOKEN its personal access token with package Read and Write
# (the built-in GITEA_TOKEN cannot publish packages)
# Repository settings → Actions → Variables (optional):
# NPM_REGISTRY npm mirror used by the runner and the server's Docker build when
# registry.npmjs.org is unreachable, e.g. from servers in Iran
# HEALTH_URL URL checked after deploy, e.g. https://robotmarkets.ir/
name: CI/CD
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# Deploys run one at a time, in push order.
concurrency:
group: ci-cd-${{ github.ref }}
cancel-in-progress: false
# Fail within minutes instead of hanging when the registry is slow or blocked.
env:
NPM_CONFIG_REGISTRY: ${{ vars.NPM_REGISTRY || 'https://registry.npmjs.org/' }}
NPM_CONFIG_FETCH_RETRIES: '3'
NPM_CONFIG_FETCH_TIMEOUT: '60000'
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: node:22-bookworm
steps:
- uses: actions/checkout@v4
- name: Install
run: npm ci --no-audit --no-fund
- name: Check formatting
run: npm run format:check
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
publish:
needs: test
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
image: ${{ steps.push.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Build, push and link the image
id: push
shell: bash
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
SOURCE_REPOSITORY: ${{ gitea.repository }}
COMMIT_SHA: ${{ gitea.sha }}
RUN_ID: ${{ gitea.run_id }}
run: |
set -euo pipefail
REGISTRY='git.my-rm.com'
OWNER="${SOURCE_REPOSITORY%%/*}"
REPO="${SOURCE_REPOSITORY#*/}"
IMAGE="$REGISTRY/${OWNER,,}/${REPO,,}:build-$(date -u +%Y%m%d-%H%M%S)-${RUN_ID}-${COMMIT_SHA:0:7}"
# Keep this job's registry credentials away from the host's own logins.
DOCKER_CONFIG="$(mktemp -d)"
export DOCKER_CONFIG
trap 'rm -rf "$DOCKER_CONFIG"' EXIT
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" --username "$REGISTRY_USERNAME" --password-stdin
docker build \
--build-arg "NPM_REGISTRY=$NPM_CONFIG_REGISTRY" \
--label "org.opencontainers.image.source=https://$REGISTRY/$SOURCE_REPOSITORY" \
--label "org.opencontainers.image.revision=$COMMIT_SHA" \
--tag "$IMAGE" .
docker push "$IMAGE"
# Show the package under this repository's Packages tab. Gitea answers 400 when the package
# is already linked (every run after the first), and the image is published either way.
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --request POST \
--header "Authorization: token $REGISTRY_TOKEN" \
"https://$REGISTRY/api/v1/packages/$OWNER/container/${REPO,,}/-/link/$REPO")"
case "$status" in
201) echo "Linked the package to $REPO" ;;
*) echo "Package link request returned HTTP $status (already linked is normal); continuing" ;;
esac
echo "image=$IMAGE" >> "$GITEA_OUTPUT"
echo "Published and linked: $IMAGE"
# The runner shares the server's Docker daemon, so deploying is a local docker compose call:
# no SSH, and the daemon pulls with the credentials of this job's temporary docker login.
deploy:
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Deploy the published image
shell: bash
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
IMAGE: ${{ needs.publish.outputs.image }}
run: |
set -euo pipefail
DOCKER_CONFIG="$(mktemp -d)"
export DOCKER_CONFIG
trap 'rm -rf "$DOCKER_CONFIG"' EXIT
printf '%s' "$REGISTRY_TOKEN" | docker login git.my-rm.com --username "$REGISTRY_USERNAME" --password-stdin
docker compose -p vending-machine-game -f deploy/docker-compose.yml pull
docker compose -p vending-machine-game -f deploy/docker-compose.yml up -d --remove-orphans
docker compose -p vending-machine-game -f deploy/docker-compose.yml ps
docker image prune -f
- name: Check the site
if: vars.HEALTH_URL != ''
shell: bash
env:
HEALTH_URL: ${{ vars.HEALTH_URL }}
run: |
for attempt in $(seq 1 20); do
if curl -fsS -o /dev/null "$HEALTH_URL"; then
echo "Site is up: $HEALTH_URL"
exit 0
fi
sleep 6
done
echo "Site did not answer at $HEALTH_URL" >&2
exit 1
+43
View File
@@ -0,0 +1,43 @@
name: Deploy game to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Check formatting
run: npm run format:check
- name: Lint
run: npm run lint
- name: Test game logic
run: npm test
- run: npm run build
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: dist
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
+4
View File
@@ -0,0 +1,4 @@
dist
node_modules
src/data
public
+7
View File
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
+387
View File
@@ -0,0 +1,387 @@
# AGENTS.md — Frontend Coding Conventions (React & Next.js)
These rules apply to every task in this repository. Follow them exactly. If a rule conflicts with existing code, follow the rule for new code and do not refactor unrelated files. If a rule is unclear, ask before guessing.
## 0. How this file is organized
- **Sections 1–13 are general** and apply to every project (React or Next.js).
- **Section 14 is framework-specific.** Detect the project type first:
- `next` in `package.json` dependencies → **Next.js** project → also follow 14.1.
- Otherwise (Vite, CRA, etc.) → **React** project → also follow 14.2.
- Never apply the rules of the other framework.
## 1. Core principles
1. **Readability beats cleverness.** Write the simplest code that works. A teammate must understand it on the first read.
2. **Easy to extend.** Small, single-purpose functions and components; clear layers; no hidden coupling.
3. **Don't repeat yourself, but don't abstract early.** Duplicate once, tolerate it twice, extract on the **third** occurrence (Rule of Three).
4. **One source of truth.** All HTTP goes through one Axios client; all app-wide state lives in one Redux store; all shared UI lives in `components/shared`.
5. **Documented.** Every function has a doc comment; important places have inline comments.
6. **Formatted before it runs.** Prettier runs automatically before dev and build.
## 2. Ask before you start (mandatory)
At the **start of every task**, before writing any code, ask the user these questions and wait for the answers:
1. **Language:** "Should this be written in **TypeScript** or **JavaScript**?"
2. **Ant Design:** "Do you need **Ant Design** for this, or should I use **only Tailwind CSS**?"
Rules:
- Never assume the answers, even if the repo already contains `tsconfig.json` or Ant Design. Always ask.
- Skip a question only if the user already answered it explicitly in the current task description.
- If Ant Design is not needed, use **Tailwind only** and do not import anything from `antd`.
- State the chosen answers in your final summary.
## 3. Stack
| Concern | Choice |
|---|---|
| Language | **Asked every task** (TypeScript or JavaScript). If TypeScript: `strict: true` |
| UI | React 18 (function components + hooks only) |
| Framework | Next.js App Router **or** React + Vite (detected, see section 0) |
| Global state | Redux Toolkit |
| HTTP | Axios (no RTK Query, no raw `fetch`) |
| Forms | React Hook Form |
| Styling | Tailwind CSS by default. Ant Design **only if the user confirms** |
| Formatting | Prettier |
| Testing | Vitest + React Testing Library (Cypress for E2E) |
Do not add a dependency without stating why and asking first.
## 4. Naming
### Files and folders
- **Component files:** `PascalCase.tsx` / `.jsx` → `UserCard.tsx`
- **Everything else:** `camelCase.ts` / `.js` → `useAuth.ts`, `userService.ts`, `authSlice.ts`, `formatDate.ts`
- **Folders:** `camelCase` (`userManagement/`). A folder holding one component plus companion files is `PascalCase` (`UserCard/`).
- **Tests:** next to the file → `UserCard.test.tsx`
- **Type files (TS):** `user.types.ts`
- **Exception:** Next.js reserved names (`page.tsx`, `layout.tsx`, `loading.tsx`, `route.ts`) and route folders follow Next.js rules.
### Code identifiers
| Kind | Style | Example |
|---|---|---|
| Variables, functions | camelCase | `totalPrice`, `calculateTotal()` |
| Components | PascalCase | `UserCard` |
| Hooks | `use` + PascalCase | `useDebounce` |
| Types, interfaces | PascalCase, **no `I` prefix** | `User`, `UserCardProps` |
| Props type | `<Component>Props` | `UserCardProps` |
| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
| Booleans | `is` / `has` / `can` / `should` | `isLoading`, `hasError` |
| Internal handlers | `handle` + Event | `handleSubmit` |
| Callback props | `on` + Event | `onSubmit` |
| Slice | `<feature>Slice` | `authSlice` |
| Thunks | verb + noun | `fetchUsers` |
| Selectors | `select` + Name | `selectCurrentUser` |
| Service functions | verb + noun | `getUsers`, `createOrder` |
- Prefer union types over `enum`: `type Status = 'idle' | 'loading' | 'failed'`.
- Names are descriptive. Abbreviate only `id`, `url`, `api`, `props`.
- Identifiers and comments are in **English**.
## 5. Project structure (layer-based)
The shared layers are **identical** in both project types and live in `src/`:
```
src/
components/
shared/ # reusable components used by 2+ pages/features
layout/ # Header, Sidebar, PageLayout
hooks/ # shared custom hooks
services/ # Axios client + API functions (the ONLY place for HTTP)
apiClient.ts
userService.ts
store/
index.ts # configureStore + RootState/AppDispatch
hooks.ts # useAppDispatch, useAppSelector
slices/
types/
utils/ # pure helpers
constants/
styles/
tests/
```
Only the routing layer differs (see section 14):
- **Next.js:** `src/app/` (routes only).
- **React (Vite):** `src/pages/`, `src/routes/`, `src/main.tsx`, `src/assets/`.
### Layer rules
- Route files (`app/` or `pages/`) compose components and wire data; no business logic beyond that.
- A component used by **one** page stays in that page's folder. On its **third** use (or when a second page needs it) move it to `components/shared`.
- `components/shared` must be **generic and props-driven**: no imports from pages/routes, `services`, or `store`.
- Components never call Axios. They call hooks, dispatch thunks, or use data passed as props.
- `utils` are pure functions (no React, no side effects).
- No circular imports. Use the `@/` alias for `src/`; avoid `../` chains deeper than two levels.
## 6. Writing simple, readable code
- **Max nesting: 2 levels** (an `if` inside a loop is the limit). Flatten with early returns, guard clauses, or by extracting a named function.
- **Max function length: ~30 lines.** If longer, split into well-named helpers.
- **One function, one job.** If you need "and" to describe it, split it.
- **Max 3 parameters.** Beyond that, pass a single object.
- **Rule of Three:** repeated logic/markup is extracted on the third occurrence into a util, hook, or `shared` component. Do not pre-abstract.
- No nested ternaries. Use `if`/early return or a lookup object.
- Name intermediate values instead of writing long chained expressions.
- No magic numbers or strings: move them to `constants/`.
- Prefer plain `map`/`filter`/`find` over clever `reduce` chains.
- Avoid premature optimization: add `useMemo`/`useCallback` only for a measured problem or a memoized child.
```ts
// Bad: nested and hard to scan
function getLabel(user) {
if (user) {
if (user.isActive) {
return user.name;
}
}
return 'Unknown';
}
// Good: guard clause, flat
function getLabel(user: User | null): string {
if (!user?.isActive) return 'Unknown';
return user.name;
}
```
## 7. Documentation and comments
### Every function gets a doc comment
Every function must have a short JSDoc block directly above it describing **what it does**: components, hooks, helpers, services, thunks, selectors, and handlers.
- First line: one clear sentence about the purpose.
- Add `@param` and `@returns` when the function has parameters or a return value.
- One line is enough for trivial functions.
- Not required for tiny inline callbacks (e.g. inside `.map()`).
```ts
/**
* Returns the display name of a user, or "Unknown" when the user is inactive.
* @param user - The user to read the name from.
* @returns The name to show in the UI.
*/
function getLabel(user: User | null): string {
if (!user?.isActive) return 'Unknown';
return user.name;
}
```
### Inline comments for important places
Always comment:
- **Why** something is done in a non-obvious way (workarounds, browser/device quirks, offline handling).
- Business rules, calculations, and necessary magic values.
- Complex conditions, regex, and data transformations.
- Side effects and cleanup in `useEffect`, and why the dependencies are what they are.
- Anything that looks like a bug but is intentional.
- Open work as `// TODO: <what and why>`.
Do **not** write comments that only repeat the code (`// increment i`). No commented-out code. No leftover `console.log`.
## 8. Components
- Function declarations for components; arrow functions for inline callbacks.
- One component per file, under ~150 lines. Split when larger.
- Define a props type right above the component (TS) or a `@typedef` (JS). Do not use `React.FC`.
- **Named exports only.** Default export only where a framework requires it (Next.js special files, `React.lazy` pages).
- Order: hooks → derived values → handlers → effects → early returns → JSX.
- Always render **loading, error, and empty** states for async UI.
- Extract repeated logic into custom hooks and repeated UI into `components/shared` (Rule of Three).
- Semantic HTML and basic accessibility (labels, alt text, focus).
```tsx
type UserCardProps = {
user: User;
onSelect: (id: string) => void;
};
/**
* Shows a clickable card for one user and reports the selection.
*/
export function UserCard({ user, onSelect }: UserCardProps) {
const handleClick = () => onSelect(user.id);
return (
<button type="button" onClick={handleClick}>
{user.name}
</button>
);
}
```
## 9. TypeScript / JavaScript
Follow the answer to the question in section 2.
### If TypeScript
- No `any`. Use `unknown` and narrow, or define a type.
- Type all parameters and exported return values; let inference handle locals.
- Request/response shapes live in `types/` and are shared by services and slices.
- `type` for unions and props; `interface` only when extending.
- No non-null assertion (`!`) without a comment explaining why it is safe.
- Files use `.ts` / `.tsx`.
### If JavaScript
- Files use `.js` / `.jsx`. Same naming and structure rules apply.
- Describe data shapes with JSDoc (`@typedef`, `@param {Type}`, `@returns {Type}`), kept in `types/`.
- Document props with a `@typedef` above the component.
- `const`/`let` only (never `var`), strict equality (`===`), optional chaining where needed.
## 10. API layer (all requests via Axios)
- **Every HTTP request goes through Axios.** Do not use `fetch`, `XMLHttpRequest`, or other HTTP clients. (WebSocket is not HTTP; see 14.2.)
- There is **one Axios instance**: `services/apiClient.ts`. It configures `baseURL`, timeout, auth header, and interceptors for token refresh and error normalization.
- Requests are made **only inside `services/`**, one file per domain (`userService.ts`). Services are plain async functions that return data and know nothing about Redux or React.
```ts
/**
* Fetches the list of all users from the API.
* @returns The users array.
*/
export async function getUsers(): Promise<User[]> {
const { data } = await apiClient.get<User[]>('/users');
return data;
}
```
- Endpoint paths are constants (in the service file or `constants/endpoints.ts`), not scattered strings.
- Errors are normalized **once** in the interceptor into a single shape (`{ message, status }`). Callers show `error.message`; they do not parse Axios errors themselves.
- Cancel or ignore stale requests with `AbortController` when a component can unmount mid-request.
## 11. Global state (Redux Toolkit)
The store holds **only** these categories. Anything else stays local (`useState`, or a small hook calling a service).
| Category | Slice | Holds |
|---|---|---|
| Auth & current user | `authSlice` | token, profile, permissions |
| General UI settings | `uiSlice` | theme, language, sidebar state |
| Device connection | `deviceSlice` | online/offline status, connected device info (only if the project talks to devices) |
| Shared cross-page data | one slice per domain (e.g. `lookupsSlice`) | data needed by 2+ pages |
| Shopping cart | `cartSlice` | cart items and quantities (header badge, cart page, product pages) |
Rules:
- One slice per feature in `store/slices/`. Slice shape for fetched data:
`{ items, status: 'idle' | 'loading' | 'succeeded' | 'failed', error: string | null }`.
- Async work uses `createAsyncThunk` that calls a **service**; handle `pending/fulfilled/rejected` in `extraReducers`.
- Use typed `useAppDispatch` / `useAppSelector` only (plain equivalents in JS).
- Write selectors beside the slice; components do not read deep state shapes directly.
- Page-local data (a table on one page, form state, open/closed flags) does **not** go in Redux.
- Keep the state shape documented: when adding a slice, add one line to the table above.
## 12. Formatting (Prettier is mandatory)
Code **must** be formatted with Prettier before the project runs in dev or build.
`package.json` scripts:
```json
{
"scripts": {
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,css,json,md}\"",
"predev": "npm run format",
"prebuild": "npm run format",
"typecheck": "tsc --noEmit",
"lint": "eslint . --max-warnings 0"
}
}
```
(`predev` and `prebuild` run automatically before `npm run dev` / `npm run build`. `typecheck` is TypeScript only.)
`.prettierrc`:
```json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
```
- Run `npm run format` after editing files and before reporting a task as done.
- Never hand-format against Prettier; never use `// prettier-ignore` without a reason in a comment.
- ESLint passes with zero warnings (`react-hooks`, `@typescript-eslint` if TS, import order).
- Import order: React → third-party → `@/` aliases → relative → styles. Remove unused imports.
## 13. Styling, testing, and git
### Styling
- **Default is Tailwind CSS only.** Use Ant Design **only** if the user said yes in section 2.
- Tailwind utilities for layout/spacing; `clsx` for conditional classes; extract a component when the same class list repeats 3+ times.
- When Ant Design is approved: customize via theme tokens, not CSS overrides, and do not mix it with custom components that do the same job.
- No inline `style` except for truly dynamic values. No hard-coded colors or sizes: use tokens.
- Mobile-first and responsive; for Persian UI use RTL-safe logical utilities (`ms-*`, `me-*`, `ps-*`, `pe-*`).
### Testing
- Test behavior via React Testing Library; query by role/label/text.
- Mock the network at the **service** boundary; never call real APIs.
- New utils, slices, and non-trivial components ship with tests; bug fixes ship with a regression test.
### Git
- Branches: `feature/<name>`, `fix/<name>`. Commits: Conventional Commits (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`).
- Keep changes small and focused; do not mix refactors with features.
## 14. Framework-specific rules
### 14.1 Next.js (App Router)
```
src/
app/ # routes only: page.tsx, layout.tsx, loading.tsx, route.ts
components/ hooks/ services/ store/ types/ utils/ constants/ styles/
```
- Components are **Server Components by default**. Add `'use client'` to the smallest leaf that needs state, effects, or browser APIs.
- Public/SEO data is fetched on the server (the same Axios client may be used server-side), not through Redux.
- Redux and browser-only hooks live in client components, provided via a client `Providers` component. Create the store only if client state is actually needed.
- Every page defines metadata (`generateMetadata`) with title and description.
- Use `next/image` and `next/link`. Use route handlers (`route.ts`) only when an API layer is truly needed.
- Never touch `window`/`localStorage` during server rendering.
- Default exports are allowed for `page`, `layout`, `loading`, `error`, `not-found`.
### 14.2 React (Vite SPA / PWA / embedded UI)
```
src/
assets/ # images, fonts, icons
pages/ # one folder per route; page-specific components live here
routes/ # router config, route guards
main.tsx
components/ hooks/ services/ store/ types/ utils/ constants/ styles/ tests/
```
- Lazy-load pages with `React.lazy` + `Suspense`.
- Assume **unreliable or no network** if the app targets devices (vending, PWA). Handle offline state explicitly; never block the UI on a failed request.
- Device communication over HTTP goes through Axios in `services/`. WebSocket/serial live in a dedicated `services/` module with typed messages and reconnect logic, updating `deviceSlice`.
- For ESP32 / Orange Pi targets keep bundles small: avoid heavy dependencies and unused polyfills.
- Document PWA caching/queue strategy in the PR description.
## 15. Refactoring an existing project
When asked to refactor a project to match this file:
1. **Detect the project type** (section 0) and ask the questions in section 2 before starting. Do not migrate between TypeScript and JavaScript unless explicitly asked.
2. **Audit first.** Scan the project and report what violates these rules (structure, naming, HTTP calls outside `services/`, missing doc comments, deep nesting, duplicated code, unformatted files). Do not change code yet; present a short plan and wait for approval.
3. **Preserve behavior.** A refactor must not change what the app does. No new features, no UI changes, no dependency upgrades.
4. **Work in small steps,** one concern at a time, in this order:
1. Add Prettier config and scripts, then format the project (formatting-only commit).
2. Centralize HTTP: create `apiClient` and move every request into `services/`.
3. Restructure folders and rename files per sections 4 and 5; update imports.
4. Organize global state per section 11; move page-local state out of Redux.
5. Extract duplicated code (Rule of Three) and move reusable components to `components/shared`.
6. Flatten nesting and split long functions (section 6).
7. Add doc comments and important inline comments (section 7).
5. **Verify after every step:** run `format`, `typecheck` (TS only), `lint`, and `test`, and make sure the project still builds. Fix breakage before moving on.
6. **One commit per step,** using Conventional Commits (`refactor: ...`).
7. **Do not touch** anything you are unsure about; list it in the final report instead.
## 16. Agent checklist (before finishing any task)
1. Did I detect the project type and ask the TypeScript/JavaScript and Ant Design questions (section 2)?
2. Did I read the surrounding code and match its patterns?
3. Is new code in the correct layer (section 5)? Any new HTTP call goes in `services/` via `apiClient`.
4. Nesting ≤ 2, functions ≤ ~30 lines, no third copy of repeated code?
5. Does every function have a JSDoc description, and do important places have inline comments?
6. Did I run `npm run format`, `typecheck` (TS only), `lint`, and `test`?
7. Did I summarize what changed, the chosen language/UI library, assumptions, and follow-ups?
8. Did I avoid touching unrelated files, configs, and dependencies?
+42
View File
@@ -0,0 +1,42 @@
# SITE_ADDRESS is the domain list, e.g. "robotmarkets.ir, www.robotmarkets.ir" (automatic HTTPS),
# or ":80" for plain HTTP behind another proxy or for a local test.
{$SITE_ADDRESS::80} {
root * /srv
# The defaults cover text and JSON; the binary buildings file compresses to about 60% too.
encode {
zstd
gzip
match {
# Repeated values for the same header are OR-ed.
header Content-Type text/*
header Content-Type application/json*
header Content-Type application/javascript*
header Content-Type application/manifest+json*
header Content-Type image/svg+xml*
header Content-Type application/octet-stream*
}
}
# Hashed build files never change; the page, service worker and manifest must always be revalidated
# so installed copies pick up new versions.
@hashed path /assets/*
header @hashed Cache-Control "public, max-age=31536000, immutable"
@fresh path / /index.html /sw.js /manifest.webmanifest
header @fresh Cache-Control "no-cache"
# Map data and satellite tiles only change with a new map build.
@mapdata path /imagery/* /data/*
header @mapdata Cache-Control "public, max-age=604800"
@manifest path /manifest.webmanifest
header @manifest Content-Type "application/manifest+json"
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
# Real-world mode needs location and the compass; the camera is reserved for the AR phase.
Permissions-Policy "geolocation=(self), camera=(self), accelerometer=(self), gyroscope=(self), magnetometer=(self)"
-Server
}
try_files {path} /index.html
file_server
}
+15
View File
@@ -0,0 +1,15 @@
# Stage 1: run the tests and build the static game with Vite.
FROM node:22-alpine AS build
WORKDIR /app
# Override with a reachable mirror where registry.npmjs.org is blocked (docker compose passes NPM_REGISTRY).
ARG NPM_REGISTRY=https://registry.npmjs.org/
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund --registry="$NPM_REGISTRY" --fetch-retries=3 --fetch-timeout=60000
COPY . .
RUN npm run format:check && npm run lint && npm test && npm run build
# Stage 2: serve dist/ with Caddy, which also obtains and renews the HTTPS certificate.
FROM caddy:2.8.4-alpine
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /app/dist /srv
EXPOSE 80 443 443/udp
+211 -31
View File
@@ -1,6 +1,108 @@
# تپهٔ آرزوها — روبات مارکت # عملیات مشهد — روبات مارکت
بازی سه‌بعدی JavaScript با Three.js: پدر و بچه در جنگل راه می‌روند، از دستگاه خرید می‌کنند و به پیک‌نیک می‌رسند. مدل‌ها با کد ساخته شده‌اند. 📚 [مستندات کامل بازی و راهنمای توسعه برای Agent](docs/README.md) — قوانین، سناریو، معماری، تاریخچه و بک‌لاگ بر اساس بررسی کد فعلی.
بازی سه‌بعدی مرورگری (React 18 + Three.js، JavaScript، Tailwind CSS) با حال‌وهوای بازی‌های اکشن سوم‌شخص، بدون سلاح و خشونت. یک پدر و فرزندش از کارخانهٔ سازندهٔ دستگاه‌ها راه می‌افتند، در خیابان‌های واقعی مشهد می‌گردند، دستگاه‌های وندینگ روبات مارکت را پیدا می‌کنند و با هر خرید امتیاز می‌گیرند. روبات راهنمای روبات مارکت داخل دستگاه‌ها زندگی می‌کند: وقتی خانواده به یک دستگاه می‌رسند، از آن بیرون می‌آید، خوش‌آمد می‌گوید و راهنمایی‌شان می‌کند.
بازی یک PWA است: روی گوشی نصب می‌شود و آفلاین هم اجرا می‌شود.
🎮 [اجرای آنلاین بازی](https://hamedabdollahzade.github.io/vending-machine-game/)
## نسخهٔ React و نمایشگاه کارخانه
رابط با React، وضعیت مشترک با Redux Toolkit و درخواست‌ها با Axios پیاده شده‌اند. موتور Three.js از رابط جداست. [معماری جدید و راهنمای Agent](docs/11-react-migration.md) مرجع ساختار فعلی است.
سه دستگاه نمونهٔ ۳۵، ۴۸ و ۶۰ کانال در حیاط کارخانه قرار دارند. با Q نزدیک نمونه‌ها یا دکمهٔ «مدل‌های کارخانه» داخل محوطه، قیمت و امکانات را از روبات بپرسید. قیمت دستگاه‌ها به‌ترتیب ۴٬۶۸۰٬۰۰۰٬۰۰۰، ۵٬۴۸۰٬۰۰۰٬۰۰۰ و ۵٬۹۸۰٬۰۰۰٬۰۰۰ ریال است؛ این معرفی بودجهٔ هتومان را کم نمی‌کند. تصاویر/GIFهای کاتالوگ هنوز اضافه نشده‌اند؛ نمونه‌های سه‌بعدی موجودند و رابط برای نبود فایل پیام جایگزین دارد.
```sh
npm test
npm run lint
npm run format:check
```
## داستان و هدف
- **شروع:** سالن تولید کارخانهٔ **شرکت روبات مارکت** (بزرگراه مشهد–چناران، خیابان کاله، کنار پارک علم و فناوری خراسان). کارخانه این بخش‌ها را دارد:
- **سالن تولید** با پنج بخش خم‌کاری، برش لیزر، رنگ‌کاری، یخچال‌سازی و مونتاژ، و ۴۰ کارگر مشغول کار؛
- **کامیون ارسال**: دستگاه‌های بسته‌بندی‌شده از سکوی بارگیری روی کامیون می‌روند. کامیون پر که شد از دروازه بیرون می‌رود و بعد خالی برمی‌گردد؛
- **دفتر فنی مهندسی** با پنج برنامه‌نویس که اسمشان بالای سرشان دیده می‌شود: حامد (React، هودی مشکی)، رایان (Go، هودی آبی)، علیرضا (C++، هودی مشکی)، رضا (C++، کت‌وشلوار) و محمد. دستگاه وندینگ اول بازی هم همین‌جاست؛
- **واحد حسابداری** با چهار همکار خانم؛
- **اتاق مدیریت**: آقای محمدی، مدیر شرکت، اول بازی با وانت KMC T8 وارد حیاط می‌شود و به اتاقش می‌رود.
با نزدیک شدن به هر کدام از این آدم‌ها، با خانواده حرف می‌زنند. ماشین خانواده، یک سدان اسپرت به سبک BMW با سرعت تا ۲۰۰ کیلومتر بر ساعت، درست کنار نقطهٔ شروع پارک است.
- **مسیر:** از کارخانه تا بوستان ملت حدود ۲۰ کیلومتر رانندگی است، از بزرگراه مشهد–چناران، بزرگراه پیامبر اعظم، بزرگراه آزادی و وکیل‌آباد.
- **نقشهٔ ایران:** صفحهٔ توجیه مأموریت نقشهٔ استان‌های ایران را نشان می‌دهد. فعلاً فقط مشهد (خراسان رضوی) باز است و شهرهای دیگر قفل‌اند.
- **محدودهٔ قابل بازی:** حدود ۱۵ در ۱۸ کیلومتر از شمال‌غرب تا غرب مشهد، همه از داده‌های واقعی 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) کار می‌کنند.
## اجرا ## اجرا
@@ -8,46 +110,124 @@
npm install npm install
npm run dev npm run dev
npm run build npm run build
node --test tests/*.test.js npm test
``` ```
## کنترل - `?quality=low` یا دکمهٔ «گرافیک» سایه‌ها را خاموش می‌کند (پیش‌فرض گوشی‌ها).
- `?debug` وضعیت بازی را در `window.game` در دسترس می‌گذارد.
- چپ / راست یا A / D: حرکت ## انتشار روی سرور با Docker
- بالا / پایین: زوم دوربین، بدون حرکت شخصیت
- دکمه‌های چهارجهته: نگه داشتن برای حرکت یا زوم در موبایل
- E یا دکمهٔ محصولات: باز کردن دستگاه در نزدیکی آن
- شروع دوباره: بازنشانی داستان، موجودی و زوم
گفت‌وگوها کوتاه‌اند و در کادر سفید پایین صفحه خودکار عوض می‌شوند. در طول تمام گفت‌وگوها، از جمله پل، حرکت آزاد است. هنگام خرید، تحویل محصول یا باز بودن دفترچه حرکت موقتاً متوقف می‌شود. تصویر 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
```
دستگاه شش ردیف ده‌تایی با شماره‌های ۱ تا ۶۰ دارد. فقط کانال‌های **۲۱ تا ۶۰** قابل خریدند. با نزدیک شدن به دستگاه، نشان E / تعامل روی کارت‌خوان نمایش داده می‌شود. با E یا لمس نشان، پنجرهٔ خرید باز می‌شود: فقط شماره‌های ۲۱ تا ۶۰ سمت راست و کد، محصول فرضی و قیمت کانال‌های قابل خرید سمت چپ قرار دارند. کلیک روی شماره یا محصول، کد را پر می‌کند؛ «پرداخت» یا Enter مرحلهٔ کارت‌خوان را باز می‌کند. دوربین به کارت‌خوان نزدیک می‌شود؛ سپس با دکمهٔ «کشیدن کارت» یا کلید C مبلغ کسر می‌شود، پیام خرید موفق نمایش داده می‌شود و محصول تحویل داده می‌شود. پیش از کارت کشیدن امکان انصراف بدون کسر موجودی وجود دارد. کدهای فارسی، عربی و انگلیسی پشتیبانی می‌شوند. کانال نامعتبر پول کم نمی‌کند. توضیح تبدیل واحد هم کنار دستگاه و هم داخل پنجره نمایش داده می‌شود. - **دامنهٔ دیگر:** `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
گردش‌کار `.gitea/workflows/ci-cd.yml` تست و build را اجرا می‌کند. برای main تصویر Docker در Registry ساخته و منتشر می‌شود؛ سپس runner از Docker socket مشترک سرور و فایل `deploy/docker-compose.yml` برای pull و up استفاده می‌کند. انتشار فعلی SSH نیست.
- Secrets لازم: `REGISTRY_USERNAME` و `REGISTRY_TOKEN` با دسترسی read/write به package.
- Variables اختیاری: `NPM_REGISTRY` برای mirror و `HEALTH_URL` برای بررسی پاسخ سایت.
- شبکهٔ خارجی `frontend_proxy` و Nginx Proxy Manager برای HTTPS در این روش لازم‌اند؛ خود کانتینر روی پورت ۸۰ داخلی است.
- Compose ریشه روش مستقل دیگری است که Caddy مستقیماً TLS می‌دهد؛ با Compose مخصوص deploy اشتباه نشود.
پیش از push نام remote و branch مقصد را بررسی کنید. [جزئیات انتشار و تفاوت روش‌ها](docs/06-development-and-deployment.md).
## ساخت دوبارهٔ دادهٔ نقشه
`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)
**فعلاً خاموش است.** زمین بازی از خیابان‌ها، پارک‌ها و کاربری زمین OpenStreetMap کشیده می‌شود. برای برگرداندن زمین ماهواره‌ای، `SATELLITE_GROUND` در `src/constants/world.js` را `true` کنید. در آن حالت جاده‌ها، لاین‌ها، پارک‌ها و حیاط‌ها از تصویر واقعی دیده می‌شوند و جاده‌ها و زمین‌های رسم‌شده ساخته نمی‌شوند.
```sh
node scripts/fetch-imagery.mjs
```
- **زوم ۱۷ (حدود ۱ متر در هر پیکسل):** محدودهٔ شهر، محدودهٔ کارخانه و ۳۰۰ متر دو طرف مسیر کارخانه تا بوستان ملت
- **زوم ۱۵:** کل منطقه، برای فاصله‌های دور
کاشی‌ها (حدود ۱۰۰۰ فایل، ۱۶ مگابایت) در `public/imagery` روی سرور خودمان هستند، پس در ایران و به‌صورت آفلاین هم کار می‌کنند. Service Worker آن‌ها را از قبل دانلود نمی‌کند و هر کاشی را وقتی بازیکن از کنارش رد شد نگه می‌دارد.
**مجوز:** استفادهٔ تجاری و میزبانی نسخه‌ای از تصاویر Esri نیاز به مجوز مناسب از Esri دارد. شرایط را با Esri بررسی کنید. نام منبع در پایین صفحهٔ بازی نمایش داده می‌شود.
## ساختار ## ساختار
- `src/main.js`: صحنه، دوربین، دستگاه، رابط و حلقهٔ بازی - `src/game/createGame.js و src/game/runtime/`: صحنه، دوربین سوم‌شخص، کنترل‌ها، رابط کاربری (HUD)، نقشه، خرید و حلقهٔ بازی
- `src/controls.js`: جهت حرکت و شرایط مجاز بودن حرکت؛ دیالوگ مانع حرکت نیست - `src/game/world/city.js`: ساخت شهر سه‌بعدی از داده‌ها (خیابان، پیاده‌رو، ساختمان، پارک، درخت، ورودی مترو، کوه‌ها) در تکه‌های ۲ کیلومتری که دورها پنهان می‌شوند
- `src/characters.js`: شخصیت‌های مفصلی، تناسب بدن، لباس، چشم‌ها، قدم زدن و تماس دست‌ها - `src/game/world/mapRender.js`: نقشهٔ گوشه، نقشهٔ کامل شهر (کاشی‌های کش‌شده و نمای کلی) و نقشهٔ ایران
- `src/payment.js`: مراحل پرداخت، انصراف و جلوگیری از برداشت تکراری - `src/game/world/routing.js`: گراف خیابان‌ها، مسیریابی A* و پیدا کردن نام خیابان
- `src/game-state.js`: قیمت‌های هتومان، موجودی، اعتبارسنجی کد و خرید - `src/game/world/collision.js`: برخورد با ساختمان‌ها برای شخصیت‌ها، ماشین و دوربین
- `src/story.js`: متن‌های کوتاه، فصل‌ها، انتخاب مسیر و کشف‌ها - `src/constants/locations.js`: دستگاه‌ها، آدرس‌ها، ساعت کاری و محصولات
- `src/style.css`: چیدمان واکنش‌گرا و کادر سفید متن - `src/game/economy/gameState.js`، `src/game/economy/payment.js`، `src/game/economy/scoring.js`: پول، خرید، پرداخت، امتیاز، درجه و مأموریت‌ها
- `tests/`: آزمون هر دو مسیر داستان، حرکت، پول و تبدیل‌های شخصیت‌ها - `src/game/models/characters.js`: پدر و فرزند، و روبات راهنما که از دستگاه‌ها بیرون می‌آید
- `src/game/models/factory.js`: محوطهٔ کارخانه، شامل سالن تولید، خط مونتاژ، دفتر، حیاط و تابلوها، به‌همراه دفترها، کامیون و ورود مدیر
- `src/game/models/staff.js`: آدم‌های کارخانه، یعنی کارمندهای مفصل (هودی، کت‌وشلوار، مانتو و روسری) با برچسب اسم، و ۴۰ کارگر به‌صورت instanced
- `src/game/models/carModels.js`: ماشین خانواده (سدان اسپرت)، وانت مدیر و کامیون
- `src/game/device/pwa.js` و `public/manifest.webmanifest`: نصب، کار آفلاین و پیام به‌روزرسانی
- `src/game/device/realworld.js`: GPS، قطب‌نما، نرم‌سازی و حالت پایدار موقعیت، و شعاع رسیدن
- `src/game/world/imagery.js`: زمین ماهواره‌ای که کاشی‌هایش با نزدیک شدن بارگذاری و با دور شدن آزاد می‌شوند
- `src/game/world/buildingsData.js`: خواندن فایل ساختمان‌های Microsoft
- `src/game/device/ar.js`: نمای دوربین (AR)، شامل تصویر دوربین پشت گوشی، جهت گوشی و زاویهٔ دید
- `src/game/models/vending.js`، `src/game/systems/vehicle.js`، `src/game/systems/traffic.js`: مدل دستگاه، فیزیک ماشین و ترافیک شهری
- `src/utils/geo.js`: تبدیل طول و عرض جغرافیایی به مختصات بازی
قیمت‌ها و موجودی فقط در ماژول game-state تعریف شده‌اند. محاسبات با هتومان صحیح انجام می‌شود؛ رابط با ارقام فارسی نمایش می‌دهد. ## محدودیت‌ها و گام‌های بعدی
## محدودیت‌ها - مدل‌ها هندسی و سبک‌اند و کیفیت گرافیکی بازی‌های AAA را ندارند.
- داده و ترافیک جهت یک‌طرفه دارند، اما مسیریابی هنوز دوطرفه است و چراغ راهنمایی شبیه‌سازی نمی‌شود؛ ترافیک روی خیابان‌های اصلی است.
- پیشنهادهای بعدی: امتیاز ایمنی (خط عابر و چراغ قرمز)، تاکسی و قطار شهری، چرخهٔ روز و شب، ذخیرهٔ پیشرفت، جدول امتیاز آنلاین، و محله‌ها و شهرهای دیگر.
مدل‌ها هنوز هندسی و سبک هستند؛ تناسب بدن و حرکت طبیعی‌تر شده اما مدل انسانی واقع‌گرایانه یا موشن‌کپچر اضافه نشده است. پرداخت واقعی، ذخیرهٔ پیشرفت و برخورد آزاد با درخت‌ها وجود ندارد. بازیکن در مسیر تعیین‌شده حرکت می‌کند. زمان کامل تجربه هنوز با بازیکن اندازه‌گیری نشده است. نقشه: © مشارکت‌کنندگان OpenStreetMap (ODbL). ساختمان‌ها: © Microsoft (ODbL). تصویر ماهواره‌ای: Esri, Maxar, Earthstar Geographics, and the GIS User Community. مرز استان‌ها: Natural Earth.
پنجره‌های خرید، دفترچه، پایان و پرداخت پس‌زمینهٔ شیشه‌ای نیمه‌شفاف دارند. نشان تعامل به موقعیت سه‌بعدی دستگاه متصل است و با حرکت دوربین جابه‌جا می‌شود.
## تعامل تک‌دکمه‌ای
گزینه‌های چندانتخابی داستان حذف شده‌اند. هر رویداد یک کار مشخص دارد: تماشای خرگوش، برداشتن نقاشی، ادامه در مسیر آفتابی و کامل کردن نقاشی با آسمان طلایی. برای انجام آن، کلید **F** یا همان دکمهٔ روی صفحه را بزنید. متن‌های معمولی خودکار عوض می‌شوند و حرکت آزاد می‌ماند. **E** تعامل با دستگاه و **C** کشیدن کارت است. انتخاب شمارهٔ محصول و دکمهٔ پرداخت در پنجرهٔ دستگاه باقی است.
+17
View File
@@ -0,0 +1,17 @@
# Server deployment: runs the prebuilt image from the Gitea registry behind Nginx Proxy Manager.
# NPM terminates HTTPS and forwards to http://vending-machine-game:80 on the frontend_proxy network.
# IMAGE is the full registry reference, set by the CI deploy job.
services:
game:
image: ${IMAGE:?set IMAGE}
container_name: vending-machine-game
restart: unless-stopped
environment:
# Plain HTTP inside the network; no host ports are published.
SITE_ADDRESS: ":80"
networks:
- frontend_proxy
networks:
frontend_proxy:
external: true
+23
View File
@@ -0,0 +1,23 @@
services:
game:
build:
context: .
args:
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
image: robot-market-game:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
# Point these domains' DNS A records at the server before the first start.
SITE_ADDRESS: ${SITE_ADDRESS:-robotmarkets.ir, www.robotmarkets.ir}
volumes:
# Keeps the HTTPS certificates across restarts and image updates.
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
+35
View File
@@ -0,0 +1,35 @@
# محصول، سناریو و دامنهٔ فعلی
## هویت
نام فعلی: «عملیات مشهد — روبات مارکت». بازی مرورگری فارسی، راست‌به‌چپ، سه‌بعدی، بدون سلاح و خشونت است. هدف اصلی کشف دستگاه‌های فرضی وندینگ در جغرافیای واقعی مشهد، خرید نمایشی و پیشرفت در مأموریت‌هاست. مختصات و نقشه واقعی‌اند؛ وجود یک دستگاه در بازی به معنای وجود دستگاه واقعی در آن نشانی نیست.
## سناریوی قابل اجرا
1. صفحهٔ briefing نقشهٔ ایران و متن مأموریت را نشان می‌دهد. فقط مشهد فعال است؛ شهرهای دیگر نمایش قفل دارند.
2. پس از شروع، پدر و کودک در حیاط کارخانهٔ شرکت روبات مارکت، کنار ماشین خانواده و رو به دفتر مهندسی هستند. README فعلی شروع را «سالن تولید» توصیف می‌کند؛ محل spawn واقعی در `factory.start` حیاط است.
3. خانواده می‌تواند سالن تولید، دفاتر و کارکنان را ببیند. گفت‌وگوهای آغازین به معرفی کارخانه و ورود مدیر اشاره می‌کنند.
4. اولین هدف، خرید از دستگاه دفتر فنی مهندسی است. روبات از کنار دستگاه بیرون می‌آید، خوش‌آمد می‌گوید و کنترل E و ماشین F را معرفی می‌کند.
5. خانواده با خودرو از کارخانه به شهر می‌رود؛ گراف فعلی مسیر کارخانه تا بوستان ملت را حدود ۱۸ تا ۲۳ کیلومتر محاسبه می‌کند (تست محدودهٔ طول مسیر).
6. اهداف شهری شامل بوستان ملت، مدرسه، دانشگاه فردوسی، مترو و مشهد مال هستند. خریدهای دیگر و کشف آزاد نیز ممکن‌اند.
7. تکمیل همهٔ هفت مأموریت، یا نبود هیچ خرید قابل پرداخت مطابق منطق فعلی پایان بودجه، پنجرهٔ پایان را باز می‌کند. پس از تکمیل همهٔ مأموریت‌ها دکمهٔ ادامه وجود دارد؛ «شروع دوباره» عملیات تازه می‌سازد.
ترتیب نمایشی مأموریت‌ها خطی است، اما شرایط تکمیل همهٔ مأموریت‌ها پس از هر خرید بررسی می‌شوند؛ انجام زودتر یک هدف ممنوع نیست. الزام «رفتن با ماشین» در متن بعضی مأموریت‌هاست و به‌عنوان شرط در رکورد خرید ذخیره/بررسی نمی‌شود.
## شخصیت‌ها
- پدر و کودک انسانی تیم قابل کنترل‌اند؛ یک player مشترک، جهت و انیمیشن آن‌ها را هدایت می‌کند.
- روبات فیروزه‌ای/سفید Robot Market راهنمای دستگاه‌هاست. با نزدیک‌شدن ظاهر، با دورشدن جمع و در سفر همراه خانواده نمی‌شود. مدل هندسی در `characters.js` و صورت‌های تصویری در `public/robot` هستند.
- روبات می‌تواند دست تکان بدهد، اشاره کند و همراه زیرنویس حرف بزند. صورت زیرنویس بر اساس نام فایل تصویری انتخاب می‌شود.
- کارکنان دفتر: حامد (React، هودی مشکی)، رایان (Go، هودی آبی)، علیرضا (C++، هودی مشکی)، رضا (C++، کت‌وشلوار)، محمد؛ چهار همکار حسابداری و آقای محمدی مدیر.
- نام React، Go و C++ در گفت‌وگو و مانیتور کارکنان بخشی از داستان کارخانه است؛ بازی با این فناوری‌ها نوشته نشده است.
## کارخانه
پنج بخش تولید: خم‌کاری، برش لیزر، رنگ‌کاری، یخچال‌سازی، مونتاژ؛ ۴۰ کارگر instanced، ابزار و قطعات متحرک، دستگاه‌های آماده، بسته‌بندی و بارگیری. کامیون هشت بسته را بار می‌زند، خارج می‌شود، مدتی غایب می‌ماند، بازمی‌گردد و دنده‌عقب به سکو می‌رسد. این چرخه تزئینی است؛ امتیاز و موجودی کالا را تغییر نمی‌دهد.
مدیر با بوگاتی مشکی وارد می‌شود، پارک می‌کند (از آن به بعد ماشینش سوارشدنی است)، تا دفتر راه می‌رود و می‌نشیند؛ گفت‌وگوی او بعد از نشستن فعال می‌شود. گفت‌وگوی هر محدوده/کارمند معمولاً یک بار در هر عملیات و با نزدیک‌شدن فعال می‌شود. برای متن دقیق، `factory.talks` و `greet`/`startMission` در `main.js` مرجع‌اند.
## خارج از دامنهٔ فعلی
پرداخت بانکی، سرور حساب کاربری، ذخیرهٔ پیشرفت، leaderboard آنلاین، شهر قابل بازی غیر از مشهد، مأموریت مبارزه، چرخهٔ روز/شب و شبیه‌سازی کامل قوانین ترافیک پیاده‌سازی نشده‌اند. پارک کوهسنگی و حرم/مرکز شهر در فهرست مناطق آینده‌اند، نه مأموریت فعال.
+73
View File
@@ -0,0 +1,73 @@
# قوانین بازی، اقتصاد و قراردادهای رفتاری
## حالت، زمان و ورودی
- حالت جابه‌جایی `walk` یا `drive` است. GPS حالت مستقل `real.on` است و حرکت را به موقعیت گوشی می‌سپارد.
- شروع: ۵۰۰ هتومان، صفر خرید، XP صفر، مأموریت انجام‌شدهٔ خالی، ساعت ۰۸:۳۰.
- هر ثانیهٔ واقعی فعال = ده ثانیهٔ بازی؛ `state.minutes += dt * 10 / 60`.
- briefing، فروشگاه، نقشه، پنجرهٔ پایان و مخفی‌بودن document شبیه‌سازی را متوقف می‌کنند. زیرنویس‌ها به‌تنهایی حرکت را نمی‌بندند. پنل پرداخت شبیه‌سازی را متوقف نمی‌کند، اما `busy` حرکت پیاده را محدود می‌کند.
- WASD و جهت‌ها: حرکت نسبی به دوربین/کنترل ماشین؛ Shift: دویدن؛ Space: ترمز دستی؛ F: سوار/پیاده؛ E: فروشگاه؛ C: کشیدن کارت؛ Escape: انصراف پیش از تأیید؛ M: نقشه.
- کنترل حروف با `KeyboardEvent.code` است تا کیبورد فارسی هم کار کند. چرخ ماوس فاصلهٔ دوربین را بین ۳ و ۱۲ تغییر می‌دهد. روی دسکتاپ Pointer Lock و در fallback/موبایل drag وجود دارد.
- موبایل joystick و دکمه‌های F، E و toggle دویدن دارد. E موبایل در مرحلهٔ ready کارت می‌کشد.
- سرعت هدف پیاده ۳٫۴ و دویدن ۶٫۵ متر/ثانیه با نرم‌سازی است. برخورد دایره‌ای شخصیت شعاع ۱ متر دارد. مرز جهان با فاصلهٔ ۱۵ متر محدود می‌شود.
## ماشین و موتور
همهٔ وسیله‌های حیاط کارخانه مشکی‌اند و سوارشدنی: بی‌ام‌وی خانواده کنار نقطهٔ شروع، لامبورگینی، مزدا و نیسان در پارکینگ کارکنان، شش موتور آپاچی کنار دفتر، و بوگاتی آقای محمدی پس از اینکه پارک کرد. هر ماشین ترافیک شهر هم با F گرفته می‌شود و تا reset همان‌جا که رها شده می‌ماند. کامیون بارگیری تزئینی است و سوار نمی‌شود. همه در `runtime/garage.js` نگه‌داری می‌شوند.
سوارشدن در فاصلهٔ حداکثر ۴٫۵ متر و خارج پرداخت/مشغولی؛ پیاده‌شدن در قدرمطلق سرعت حداکثر ۳ متر/ثانیه. UI پیشنهاد پیاده‌شدن را با شرط محدودتر سرعت زیر ۱ و دستگاه نزدیک نشان می‌دهد. سمت خروج راننده بررسی برخورد می‌شود.
ثابت‌های `CAR`: سقف جلو ۵۵٫۶ متر/ثانیه (حدود ۲۰۰ km/h)، عقب ۶، شتاب ۹٫۵، ترمز ۱۴، drag برابر ۱٫۶، فرمان حداکثر ۰٫۵۵ رادیان، wheelBase برابر ۲٫۷. فرمان در سرعت بالا محدودتر می‌شود. برخورد دیوار دو دایرهٔ جلو/عقب و حرکت زیرگام دارد. ثابت‌های `BIKE`: سقف ۳۶٫۱ (حدود ۱۳۰ km/h)، شتاب ۱۱، wheelBase برابر ۱٫۳۵؛ موتور در پیچ خم می‌شود. برخورد بین وسیله‌ها با جعبهٔ جهت‌دار به اندازهٔ واقعی مدل (SAT در `world/vehicleCollision.js`) است، پس فقط وقتی بدنه‌ها واقعاً به هم برسند سرعت کم می‌شود و موتور می‌تواند لایی بکشد. کاهش سرعت به زاویهٔ برخورد بستگی دارد: شاخ‌به‌شاخ ۶۰٪، مالیدن کناری تقریباً هیچ. ترافیک فقط برای چیزی که واقعاً در عرض خطش باشد ترمز می‌کند و NPC پس از تماس طولانی حذف/بازپدید می‌شود. جریمهٔ نقدی، خسارت قابل تعمیر و سوخت وجود ندارد.
## پول و محصول
۱۰ هتومان = ۱۰۰٬۰۰۰ ریال؛ محاسبات با هتومان صحیح و نمایش با رقم فارسی است. قیمت هر دستگاه `Math.round(baseCost * priceFactor)` است؛ مثال آب کارخانه `round(15 × .5) = 8`.
| محصول | نوع | قیمت پایه هتومان |
|---|---|---:|
| آب‌میوه | drink | ۳۵ |
| آب معدنی | drink | ۱۵ |
| شیر کاکائو | drink | ۴۰ |
| چای سرد | drink | ۴۵ |
| لیموناد | drink | ۴۰ |
| بیسکویت | snack | ۲۵ |
| شکلات | snack | ۳۰ |
| آجیل | snack | ۶۵ |
| کیک | snack | ۳۵ |
خرید نامحدود تا حد بودجه است؛ موجودی انبار، مصرف محصول، فروش دوباره و رزرو پول برای مأموریت نداریم. کانال‌های عددی ۲۱ تا ۶۰ متعلق به نسخهٔ قدیمی‌اند؛ فروشگاه فعلی کارت محصول با productId دارد.
`purchaseBlocker` به ترتیب دستگاه/محصول ناشناخته، busy، بسته‌بودن و کمبود پول را کنترل می‌کند. بازبودن شامل ساعت شروع و فاقد ساعت پایان است: `start <= minutes % 1440 < end`. بازهٔ عبور از نیمه‌شب پشتیبانی نشده است.
## پرداخت و تحویل
`idle → approaching → ready → approved → delivery → idle`
- بازکردن فروشگاه: پیاده، دستگاه نزدیک (۳٫۴ متر در حالت مجازی)، عملیات تمام‌نشده و بدون busy/پرداخت فعال.
- انتخاب محصول و `beginPayment`: بدون برداشت؛ busy روشن و دوربین نزدیک کارت‌خوان.
- بعد از بیش از ۱ ثانیه آماده؛ تنها C/دکمهٔ کشیدن کارت خرید را ثبت و مبلغ را یک بار کسر می‌کند.
- انصراف در approaching یا ready بدون تغییر پول/خرید؛ پس از approved مجاز نیست.
- پس از بیش از ۱٫۲ ثانیهٔ approved، تحویل ۲٫۶ ثانیه‌ای آغاز می‌شود. busy تا پایان تحویل باقی می‌ماند.
- رکورد خرید شامل machineId، category، productId، type، price و minutes است. XP و جایزهٔ مأموریت در پایان تحویل اعمال می‌شوند، نه لحظهٔ انتخاب.
- همه‌چیز نمایشی و داخل مرورگر است؛ اتصال درگاه یا تراکنش واقعی وجود ندارد.
## امتیاز و درجه
هر خرید ۱۰۰ XP؛ اولین خرید از هر machineId، ۲۵۰؛ اولین ترکیب drink و snack در همان دستگاه، ۱۵۰؛ تکمیل پنج دستهٔ شهری park/university/school/metro/mall، ۵۰۰. factory و test در پاداش پنج دسته و explorer شمرده نمی‌شوند. خرید در real.on بعد از تحویل ۲۰۰ XP اضافه می‌دهد؛ کشف/ترکیب برای دستگاه آزمایشی هم ممکن است.
| درجه | حداقل XP |
|---|---:|
| تازه‌وارد | ۰ |
| کاوشگر | ۶۰۰ |
| راه‌بلد | ۱۴۰۰ |
| شکارچی دستگاه | ۲۶۰۰ |
| استاد نقشه | ۴۲۰۰ |
| افسانهٔ مشهد | ۶۵۰۰ |
مأموریت‌ها تنها یک بار با missionsDone جایزه می‌دهند؛ مشخصات کامل در [مرجع](10-game-reference.md). activeMission اولین مأموریت انجام‌نشده است، ولی completeMissions همهٔ شرط‌ها را بررسی می‌کند.
## پایان و reset
`checkEnd` در پایان خرید و تقریباً هر نیم‌ثانیه بررسی می‌شود: همهٔ مأموریت‌ها کامل یا `canAffordAnything` منفی. تابع بودجه دستگاه ارزان قابل خرید فعلی/با پایان ساعت دیرتر در همان روز را می‌شمارد، نه امکان کسب پول در آینده؛ محدودیت‌های این رفتار در بازبینی آمده است.
reset پول، خرید، XP، ساعت، payment، spawn، ماشین، مأموریت، مقصد و greeting روبات را تازه می‌کند. reset فعلی پاک‌سازی کامل همهٔ timerها، گفت‌وگوها، GPS و انیمیشن کامیون نیست؛ Agent نباید آن را بدون تست کامل فرض کند.
+58
View File
@@ -0,0 +1,58 @@
# معماری و فناوری
## پشتهٔ فعلی
JavaScript ES Modules، Three.js/WebGL، Vite، HTML و CSS فارسی RTL و Canvas 2D برای نقشه، تابلو، تکسچر و portrait. فونت Vazirmatn با Fontsource داخل bundle می‌آید. نسخهٔ lock فعلی: Three.js `0.180.0`، Vite `6.4.4`، Fontsource `5.2.5`؛ در package.json نسخه‌ها بازهٔ caret دارند. Node 22 در CI/Docker استفاده می‌شود؛ target خروجی ES2022 برای top-level await است. TypeScript، React، موتور فیزیک بیرونی و framework رابط در بازی وجود ندارند.
## نقشهٔ ماژول‌ها
| فایل | مسئولیت |
|---|---|
| main.js | bootstrap، DOM، player/mode/camera، حلقه، اتصال پرداخت و جایزه، GPS/AR، UI و debug |
| game-state.js | catalog، پول و ساعت، خرید، blockers و امکان ادامه با بودجه |
| payment.js | ماشین حالت تراکنش و جلوگیری از برداشت دوباره |
| scoring.js | XP، درجه، مأموریت و جایزهٔ مأموریت |
| locations.js | مشخصات کارخانه، دستگاه‌ها، test points و مناطق قفل |
| characters.js | createFamily معمولی (پدر/کودک) و robot-only، مفصل و انیمیشن |
| factory.js | ساختمان/دکور/کارخانه، کارکنان، موقعیت spawn، talks، مدیر و کامیون |
| staff.js | مدل کارکنان مفصلی و crew به شکل InstancedMesh |
| car-models.js | سدان خانواده، کامیون و helperهای مشترک بدنه |
| luxuryCars.js | ماشین‌های لوکس مشکی به سبک بوگاتی، لامبورگینی، مزدا و نیسان |
| motorcycle.js | موتور آپاچی با راکب و بدنهٔ خم‌شونده |
| vehicle.js | state و فیزیک آرکید ماشین (`CAR`) و موتور (`BIKE`) |
| vehicleCollision.js | برخورد جعبهٔ جهت‌دار وسیله‌ها و هل‌دادن پیاده از وسیلهٔ پارک‌شده |
| garage.js | فهرست وسیله‌های سوارشدنی، سوار/پیاده، برخورد رانندگی و reset |
| traffic.js | ماشین‌های NPC، spawn و حرکت روی شبکهٔ اصلی و one-way داده |
| city.js | مدل شهر، زمین fallback، ساختمان، فضای سبز و chunk visibility |
| collision.js | spatial grid چندضلعی‌ها، resolveCircle و clearFraction دوربین |
| routing.js | گراف پیاده/سواره، A*، نزدیک‌ترین node/segment و طول مسیر |
| geo.js | تبدیل lat/lon و مختصات محلی، bearing و فاصلهٔ فارسی |
| map-render.js | minimap، fullmap و نقشهٔ استان‌های ایران با Canvas |
| vending.js | مدل دستگاه، slot، card، flap، beacon و محصول تحویلی |
| imagery.js | کاشی ماهواره‌ای، تبدیل Web Mercator، بارگذاری/آزادسازی texture |
| buildings-data.js | decode فایل باینری RMB2 |
| realworld.js | smoothFix، stabilizer، geolocation/compass و شعاع رسیدن |
| ar.js | rear camera، حسگر جهت، quaternion و تطبیق FOV |
| pwa.js | نصب، ثبت SW، اعلان نسخه و reload |
| textures.js | ساخت CanvasTexture مشترک |
| style.css / index.html | ساختار DOM، HUD، modalها و ظاهر desktop/mobile |
## جریان داده
`locations/catalog + map JSON + public binary/index → ساخت world/graph/collision → reset → requestAnimationFrame`.
حلقهٔ اصلی dt را عادی به ۰٫۲۵ ثانیه محدود و با گام‌های حداکثر ۱/۳۰ شبیه‌سازی می‌کند؛ debug حداکثر ۱ ثانیه دارد. در حالت فعال: ساعت، حرکت، payment، delivery، traffic؛ سپس route، hints، talks و guide. پس از آن انیمیشن‌ها، دوربین، HUD و render اجرا می‌شوند. توقف شبیه‌سازی الزاماً همهٔ انیمیشن‌های بصری را متوقف نمی‌کند.
`main.js` وضعیت مرکزی را در متغیرهای ماژولی نگه می‌دارد؛ event bus یا store جدا نداریم. `state` اقتصادی از `player/car/cam/real` جداست. `payment` در اتصال UI فیلدهای اضافی station/timer/delivering می‌گیرد که در createPayment تعریف نشده‌اند. Object.assign به createPayment فیلدهای اضافی را حذف نمی‌کند؛ هنگام refactor به این وابستگی توجه شود.
## قرارداد توسعه
- محاسبهٔ قیمت فقط از priceAt و catalog؛ UI نسخهٔ دوم قیمت نسازد.
- برداشت فقط در swipe موفق؛ XP/مأموریت پس از تحویل؛ busy تا پایان تحویل.
- دستگاه با id پایدار معرفی شود؛ مأموریت‌ها و خریدهای ذخیره‌شوندهٔ آینده به id وابسته‌اند.
- مبنای geo تغییر نکند مگر همهٔ JSON/binary/anchorها و تست‌ها با هم مهاجرت کنند.
- تغییر mode باید مسیر، دوربین، ورودی و HUD را در نظر بگیرد؛ تغییر real/AR باید lifecycle stream/sensor را هم رعایت کند.
- geometry، material و texture پویا هنگام حذف آزاد شوند؛ مدل تازه نباید هر frame asset بسازد.
- main.js حدود ۱۴۴۳ و factory.js حدود ۶۹۸ خط‌اند؛ جداسازی تدریجی ممکن است، ولی بازنویسی سراسری برای یک تسک کوچک ضرورت ندارد.
backend اختصاصی بازی وجود ندارد. Gitea Registry و Caddy زیرساخت build/میزبانی‌اند، نه API بازی. GPS و تصویر دوربین در این کد به سرور بازی ارسال نمی‌شوند؛ سرویس مکان‌یابی مرورگر و دانلود assetها همچنان محیط بیرونی خود را دارند.
+44
View File
@@ -0,0 +1,44 @@
# جهان، نقشه، داده‌ها و دارایی‌ها
## مختصات و قرارداد فایل
مبدأ `ORIGIN = {lat:36.315, lon:59.539}`، هر واحد یک متر، +X شرق و -Z شمال. تبدیل صفحهٔ مماس محلی با latitude/longitude انجام می‌شود؛ terrain ارتفاع جغرافیایی واقعی ندارد. جهت heading ماشین با `(cos(h), -sin(h))` در صفحهٔ XZ است. bearing صفر شمال و ۹۰ شرق است.
محدودهٔ فعلی: X از -۱۲۲۲۰ تا ۳۱۵۹؛ Z از -۱۶۴۵۱ تا ۲۰۷۸. `scripts/build-map-data.mjs` REGION را از lat 36.298 تا 36.462 و lon 59.405 تا 59.572 می‌گیرد.
`src/data/mellat.json`: origin، attribution، bounds، roadTypes، areaKinds، names، roads، buildings، areas، oneway، labels. snapshot: ۶۱۹۶ road، ۳۹۸۶ ساختمان OSM، ۲۶۸ area، ۲۹۱۰ شاخص road یک‌طرفه و ۹۵ label. این شمارش‌ها تعداد رکورد فایل‌اند، نه شمارش همهٔ خیابان‌های واقعی.
- road: `[typeIndex, nameIndex, x0, z0, x1, z1, ...]`
- building در runtime: `[height, x0, z0, ...]`
- جدول names برای lookup نام road استفاده می‌شود.
- دادهٔ استان‌ها در `src/data/iran.json` است.
## گراف و برخورد
buildGraph گراف پیاده با همهٔ مسیرها، و سواره با road type ≤ 7 می‌سازد. نزدیک‌ترین node از بزرگ‌ترین مؤلفهٔ متصل انتخاب می‌شود؛ A* با هزینهٔ طول کار می‌کند. connector ابتدا/انتهای مسیر به node نزدیک، خط مستقیم است. گراف فعلی دوطرفه است؛ oneway در routing مصرف نمی‌شود، هرچند traffic از آن استفاده می‌کند.
خط طلایی روی زمین فقط تا حدود ۷۰۰ متر از polyline ساخته می‌شود. نقشهٔ کامل و minimap مسیر کامل را می‌گیرند. هدف معمولاً مأموریت فعال است؛ waypoint دستی مقدم است و پس از خرید/اعلام مأموریت بعدی می‌تواند پاک شود.
برخورد بر مبنای footprintهای OSM + Microsoft + factory است؛ spatial cell برابر ۲۴ متر. resolveCircle برای player، خودرو و نقاط spawn استفاده می‌شود. clearFraction مانع ردشدن خط دید دوربین از دیوار می‌شود. ساختمان‌های Microsoft داخل محوطهٔ کارخانه کنار گذاشته می‌شوند.
## دارایی‌ها و بهینه‌سازی
- `public/data/buildings.bin`: قالب RMB2؛ header شامل count، minX، minZ، quantization step؛ هر polygon تعداد رأس/ارتفاع نیم‌متری و مختصات delta دارد. در snapshot بررسی‌شده ۱۴۱٬۴۹۷ ساختمان و ۳٬۴۷۸٬۹۱۰ بایت دارد. ارتفاع‌ها تخمینی‌اند. decoder فعلی برای فایل کوتاه/خراب اعتبارسنجی کامل ندارد.
- city بخش‌های ۲۰۰۰ متری و ساختمان‌ها بلوک‌های ۵۰۰ متری دارد؛ ساخت/visibility وابسته به فاصله است. worker و props با instancing کاهش draw call دارند.
- `public/imagery/index.json`: ۳۴۰ tile زوم۱۵ و ۶۶۰ tile زوم۱۷ (مجموع ۱۰۰۰ JPG). imagery هم‌زمان حداکثر ۶ درخواست می‌فرستد؛ tileهای دور geometry/material/texture آزاد می‌کنند.
- زوم۱۷: شعاع ۴۵۰ متر در low و ۷۵۰ در high؛ زوم۱۵: ۱۸۰۰ و ۲۸۰۰. محدودهٔ حذف ۱٫۲ برابر range است.
- زمین ماهواره‌ای با `SATELLITE_GROUND = false` در `src/constants/world.js` خاموش است. در این حالت index دانلود نمی‌شود و زمین و جاده‌ها از OSM کشیده می‌شوند. اگر روشن شود و index موجود باشد، زمین/جادهٔ procedural شهر جایگزین می‌شود. نبود دانلود bin/index fallback دارد؛ index موجود با JPGهای غایب لزوماً به fallback بدون تصویر تبدیل نمی‌شود.
- portraitهای روبات: front، happy، wink، surprised، laugh، love، curious، serious، excited؛ نام فایل با face زیرنویس تطبیق دارد.
- آیکون‌های PWA در `public/icons` و manifest در `public/manifest.webmanifest` هستند.
## بازتولید داده
```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
node scripts/build-buildings.mjs <iran-quadkey.csv.gz>
node scripts/fetch-imagery.mjs
```
این‌ها عملیات جدا از npm build هستند. raw exportها ورودی بیرونی‌اند؛ خودکار دانلود/بازنویسی نکنید مگر تسک نیاز داشته باشد. پس از تغییر REGION یا مبدأ باید anchors، factory، graph، binary، tile index و تست دسترس‌پذیری با هم بررسی شوند. `npm run build` فقط فایل‌های موجود را بسته‌بندی می‌کند.
Attribution در کد/رابط وجود دارد: OpenStreetMap، Microsoft، Esri/Maxar/Earthstar و Natural Earth. متن مجوز و شرایط استفادهٔ بیرونی در این بازبینی حقوقی بررسی نشده است؛ attribution فعلی در تغییر دارایی‌ها حفظ شود و متن README دربارهٔ منبع تصاویر مرجع پروژه باقی بماند.
+33
View File
@@ -0,0 +1,33 @@
# GPS، دوربین، حسگر و PWA
## حالت دنیای واقعی
کاربر از briefing یا نوار پایین real mode را می‌زند؛ modal معرفی/ایمنی و سپس اجازهٔ قطب‌نما و مکان‌یابی درخواست می‌شود. HTTPS یا localhost لازم است. joystick و صفحه‌کلید جای player را تغییر نمی‌دهند؛ updateReal موقعیت را از گوشی می‌گیرد. ماشین مجازی در این حالت قابل استفاده نیست. خانواده پنهان و دوربین در ارتفاع ۱٫۶ متر قرار می‌گیرد.
مکان‌یابی ابتدا getCurrentPosition تقریبی (۱۵ ثانیه timeout، maximumAge ۶۰ ثانیه) و سپس watchPosition دقیق (۳۰ ثانیه timeout و maximumAge ۳ ثانیه) دارد. timeout اولیهٔ watch فقط وضعیت جست‌وجو است؛ کد۱ permission، کد۲ unavailable و کد۳ timeout.
smoothFix وزن را با واریانس accuracy دو fix می‌سازد؛ پس از فاصلهٔ زمانی بیش از ۳۰ ثانیه fix تازه جایگزین می‌شود. accuracy کف ۳ متر دارد. stabilizer پرش خارج از دسترس را رد می‌کند، پس از سه رد پیاپی نمونهٔ بعدی امکان پذیرش دارد؛ تا drift بیش از max(3, accuracy/2) یا speed گزارش‌شده بیش از .7 نباشد مکان نمایشی ثابت می‌ماند.
updateReal نقطه را از برخورد ساختمان بیرون می‌آورد؛ اختلاف بالای ۱۵۰ متر teleport، در فاصله‌های کمتر حرکت نرم تا حداکثر ۸ m/s دارد. فقط fix داخل bounds اعمال می‌شود؛ خارج bounds پیام نزدیک‌ترین دستگاه نمایش داده می‌شود.
شعاع تعامل `min(50, max(20, accuracy × 2.5))` متر است. نزدیک‌ترین دستگاه فعال در این شعاع انتخاب می‌شود؛ سه test point فقط در real فعال‌اند. شرط سرعت بیش از ۶ m/s در interact جلوی بازکردن فروشگاه را می‌گیرد. این شرط در swipePayment تکرار نشده؛ محدودیت تست/بک‌لاگ محسوب می‌شود.
## AR
getUserMedia با rear/environment، video مطلوب ۱۲۸۰×۷۲۰ و بدون audio؛ تصویر video پشت WebGL alpha قرار می‌گیرد. city، imagery، factory، traffic، car، sky و parkGate در AR پنهان‌اند؛ دستگاه و روبات و مسیر روی تصویر می‌مانند. این AR بر پایهٔ GPS/قطب‌نماست، نه WebXR، تشخیص سطح یا occlusion واقعی. انتظار دقت سانتی‌متری درست نیست.
زاویهٔ گوشی به quaternion با deviceorientationabsolute یا fallback deviceorientation تبدیل می‌شود؛ iOS compassHeading می‌تواند offset شمال را اصلاح کند. FOV از فرض زاویهٔ ضلع بلند ۶۵ درجه و نسبت video/screen محاسبه می‌شود. stable mode چرخش را آرام‌تر می‌کند. بدون حسگر drag و جهت حرکت/compass fallback هستند.
خاموش‌کردن AR trackهای دوربین را stop می‌کند ولی حسگر برای نمای اول‌شخص ادامه دارد؛ خاموش‌کردن real باید tracking، AR و sensors را متوقف کند. reset معمول بازی فعلاً معادل خاموش‌کردن real نیست.
## PWA و ذخیره
setupPwa در build service worker نسبی `./sw.js` ثبت می‌کند؛ dev server ثبت نمی‌کند. نصب با beforeinstallprompt و راهنمای Add to Home Screen برای iOS فراهم است. manifest و icon محلی هستند.
Vite plugin فهرست assetهای build و public را precache می‌کند؛ JPGهای imagery مستثنا هستند و فقط پس از مشاهده cache می‌شوند. بنابراین «آفلاین» برای همهٔ نقاط ماهواره‌ای پس از اولین اجرا تضمین نشده است. Navigation network-first با fallback صفحهٔ cache؛ assetهای دیگر cache-first؛ imagery cache جدا دارد. نسخهٔ تازه با skip-waiting و controllerchange reload می‌شود.
localStorage فعلی فقط کیفیت گرافیک را نگه می‌دارد. پول، خرید، XP، مأموریت و موقعیت player ذخیرهٔ ماندگار ندارند. نصب PWA با ذخیرهٔ پیشرفت یکی نیست. حالت low پیش‌فرض ورودی coarse است: سایه خاموش و pixelRatio ۱؛ high تا ۱٫۷۵. query `?quality=low` و کلید گرافیک انتخاب را تغییر می‌دهند.
## آزمون دستی ضروری
مجوز ردشده/داده‌شده، قطع GPS، fix خارج جهان، نمونهٔ قدیمی و jump، نزدیک‌ترین test point، سرعت بالا حین پرداخت، دو بار روشن/خاموش، screen rotation، توقف camera track، بازگشت از background، Android/iOS، آفلاین مسیر بازدیدشده/جدید و update از نسخهٔ نصب‌شده. این‌ها در تست Node به‌طور انتهابه‌انتها پوشش ندارند.
+55
View File
@@ -0,0 +1,55 @@
# اجرا، تست، انتشار و نگهداری
## محیط توسعه
```sh
npm ci
npm run dev
node --test tests/*.test.js
npm run build
npm run preview
```
در محیط این مخزن طبق `/home/hamed/.codex/RTK.md` فرمان‌های shell با `rtk` پیشوند اجرا می‌شوند؛ مثال `rtk npm run build`. خروجی dist تولیدی و در gitignore است. Node 22 با CI هماهنگ است. query `?debug` شیء window.game را برای بررسی state/player/car/cam/payment/route و ابزارهای موجود در انتهای main در دسترس می‌گذارد؛ این دسترسی فقط ابزار توسعه است.
## پوشش تست فعلی
| فایل | تعریف تست | پوشش |
|---|---:|---|
| payment.test.js | ۳ | برداشت یک بار، مرحلهٔ ready، انصراف/بسته/نامعتبر/پول، قیمت و ساعت |
| scoring.test.js | ۲ | کشف/ترکیب/پنج دسته، درجه و جایزهٔ مأموریت |
| realworld.test.js | ۴ | smoothFix، arrival/bounds، stabilizer، quaternion و FOV |
| world.test.js | ۷ | geo، collision، مسیر کارخانه/دستگاه، خودرو، one-way ترافیک، binary و tile |
در تاریخ مبنا همهٔ این فایل‌ها موفق اجرا شدند. runner خروجی را به چهار فایل تجمیع کرده؛ عدد چهار در خلاصهٔ خروجی تعداد فایل‌هاست. تست browser E2E، اجرای فیزیکی گوشی، CI واقعی، build Docker و سایت زنده در این بازبینی اجرا نشده‌اند.
## GitHub Pages
`.github/workflows/pages.yml`: push به main یا اجرای دستی؛ checkout، Node22/npm ci، تست، build، configure-pages، upload فقط dist و deploy-pages با مجوز pages/id-token. در Settings → Pages، Source باید GitHub Actions باشد. `base:'./'` برای مسیر مخزن است. لینک ثبت‌شدهٔ پروژه: https://hamedabdollahzade.github.io/vending-machine-game/ (دسترس‌پذیری فعلی بررسی نشده).
## Docker مستقل
Dockerfile: build با node:22-alpine و npm ci + تست + build؛ runtime caddy:2.8.4-alpine و dist در /srv. Compose ریشه از سورس build، پورت‌های ۸۰/۴۴۳ و volume گواهی دارد؛ SITE_ADDRESS پیش‌فرض robotmarkets.ir و www.robotmarkets.ir، NPM_REGISTRY قابل تنظیم. در این مدل Caddy مسئول TLS است.
```sh
docker compose up -d --build
```
Caddy assets hashed را immutable یک‌ساله؛ HTML/SW/manifest را no-cache؛ mapdata را یک هفته cache می‌کند. zstd/gzip و headerهای geolocation/camera/sensor و nosniff تنظیم‌اند. try_files ناشناخته را به index.html می‌فرستد؛ برای فایل دادهٔ گمشده باید این رفتار در QA در نظر گرفته شود.
## Gitea فعلی؛ تفاوت مهم با README قدیمی
گردش‌کار `.gitea/workflows/ci-cd.yml` فعلی از SSH deployment استفاده نمی‌کند. توضیح قدیمی DEPLOY_HOST/DEPLOY_SSH_KEY در README برای این workflow معتبر نیست.
1. test job روی Node22: npm ci، تست و build؛ timeout پانزده دقیقه.
2. publish برای main و غیر PR: Docker build، push تصویر با tag زمان/run/SHA به git.my-rm.com، سپس تلاش link package به مخزن. ناموفق بودن link مانع انتشار تصویر نمی‌شود. timeout سی دقیقه.
3. deploy با image خروجی publish و Docker daemon مشترک runner/server: compose pull و up از `deploy/docker-compose.yml`، نمایش ps و image prune. timeout پانزده دقیقه.
4. HEALTH_URL اختیاری برای پاسخ HTTP تست می‌شود؛ این تست صحت بازی یا CSS/WebGL را تضمین نمی‌کند.
نیازها: secretهای REGISTRY_USERNAME و REGISTRY_TOKEN با مجوز package read/write؛ variable اختیاری NPM_REGISTRY و HEALTH_URL؛ runner دارای Docker CLI/Compose و دسترسی socket daemon میزبان. مقدار secretها در مستندات نوشته نشود.
Compose مخصوص deploy تصویر آمادهٔ IMAGE را می‌گیرد، container_name برابر vending-machine-game و شبکهٔ خارجی frontend_proxy دارد. پورت میزبان publish نمی‌کند؛ SITE_ADDRESS=:80 و TLS با Nginx Proxy Manager بیرون container است. پس آن را با compose ریشه که Caddy TLS دارد یکی ندانید.
## کنترل تغییر
قبل از کار git status و remote را بررسی کنید؛ origin را GitHub فرض نکنید (README از origin در Gitea و remote github یاد می‌کند). build محلی به معنی انتشار نیست. برای هر تسک، تست مرتبط + build و بررسی diff لازم است؛ تغییر داده/زیرساخت نیازمند بررسی خاص خود است. این مستندات خودشان دستور push یا deploy نیستند.
+36
View File
@@ -0,0 +1,36 @@
# برنامه‌های اجراشده و تاریخچهٔ تغییر جهت
این جدول از git log محلی استخراج شده؛ تاریخ دقیق و جزئیات هر مرحله را با `git show <sha>` بررسی کنید. پیام‌های «update» نقشهٔ محصول قابل استناد ندارند و از آن‌ها قابلیت حدس زده نشده است.
| مرحله | شاهد گیت | نتیجه/وضعیت کنونی |
|---|---|---|
| پایهٔ اولیه | f7c6aee | شروع مخزن |
| سفر جنگل و پیک‌نیک | 5857b11 | داستان و مراحل تعاملی؛ سناریوی تاریخی، با شهر جایگزین شده |
| Pages و لینک | 6d4c4b7 | workflow ساخت dist و README؛ workflow هنوز هست |
| روبات جای کودک | 8a49dc0 | در آن مرحله child تعویض شد؛ در نسخهٔ فعلی کودک انسانی برگشته و روبات نقش راهنمای دستگاه دارد |
| تبدیل به شهر مشهد | 95a2ae8 | جابه‌جایی آزاد سوم‌شخص، دستگاه شهری، خرید/امتیاز و نقشه |
| کارخانه/PWA/GPS | 0939dac | شروع کارخانه، نصب و حالت real |
| گیر ماشین بزرگراه | 2f74788 | اصلاح حرکت/برخورد در مسیر چناران |
| Docker/HTTPS | 47b2b72 | build چندمرحله و Caddy/Compose مستقل |
| Gitea CI/CD | 22aa11b | گردش‌کار اولیه |
| محدودیت registry | 9832f30 | registry قابل تنظیم و timeout/retry نصب |
| تصویر registry | 894e3e2 | انتشار image به‌جای build سورس روی سرور |
| نام کارخانه/GPS | 8cc0e8c | Robot Market، status موقعیت و سه نقطهٔ آزمایشی |
| Docker socket deploy | c1141ad | جایگزینی deploy SSH با daemon مشترک runner |
| تحمل خطای package link | 9564880 | خطای link مانع job نمی‌شود |
| AR و stable GPS | 6ce5600 | rear camera، نرم‌سازی و شعاع تعامل گسترده |
| real اول‌شخص/UI موبایل | 5cea802 | دوربین گوشی و اعلان‌های وسط صفحه |
| ماهواره/ساختمان | 9b289f7 | Esri tile و footprint Microsoft، lazy rendering |
| کارخانه مفصل و خودرو سریع | 751af76 | کارکنان و بخش‌ها، مدیر و کامیون، سقف سرعت حدود ۲۰۰ km/h |
## چیزهایی که نباید به‌عنوان برنامهٔ انجام‌شده ثبت شوند
ذخیرهٔ پیشرفت، leaderboard آنلاین، مأموریت ایمنی، چراغ قرمز، تاکسی/قطار شهری، شهر دیگر و چرخهٔ روز/شب صرفاً پیشنهادند. وجود نام فناوری در مانیتور کارکنان به معنای backend واقعی Go یا frontend React نیست. نمایش مقصد قفل‌شده به معنای منطقهٔ قابل بازی نیست.
## تصمیم‌های بالفعل که Agent باید حفظ کند
- تجربهٔ فعلی خانوادهٔ انسانی + روبات در دستگاه است؛ رجوع به پیام تاریخی جایگزینی روبات بدون بررسی کد، نقش‌ها را اشتباه می‌کند.
- اقتصاد، زمان و امتیاز در ماژول جدا؛ پرداخت نمایشی و برداشت پس از swipe.
- map و data محلی‌اند؛ runtime برای راه‌اندازی به سرویس route آنلاین متکی نیست.
- اسکریپت‌های بازتولید داده بخشی از build روزمره نیستند.
- مسیر deploy جاری Gitea image/socket است؛ SSH فقط شرح قدیمی README است.
+43
View File
@@ -0,0 +1,43 @@
# بازبینی کد و بک‌لاگ پیشنهادی
تاریخ: ۲۰۲۶/۱۰/۲۸، مبنا 751af76. بررسی: منبع همهٔ حوزه‌های اصلی، دادهٔ محلی، تست‌ها، build و فایل‌های انتشار. اجرای تعاملی WebGL/گوشی یا محیط production انجام نشده است. موارد زیر اصلاح‌شده محسوب نمی‌شوند. اولویت‌ها پیشنهاد مهندسی‌اند، نه تسک مصوب.
## یافته‌های قابل پیگیری
| شناسه | اولویت | شاهد و اثر | معیار پذیرش پیشنهادی |
|---|---|---|---|
| R01 | بالا | `swipePayment` شکست خرید را به idle برمی‌گرداند، ولی `swipe()` در main روی false فوراً return می‌کند. اگر دستگاه بین آماده‌شدن و کشیدن کارت بسته شود، پنل پرداخت ممکن است باز و بدون مسیر بازیابی بماند؛ cancel در idle false است. | بازتولید عبور ساعت پایان بین begin/ready/swipe؛ بدون برداشت، پیام خطا و پنل قابل بستن/تلاش مجدد، busy خاموش؛ تست اتصال UI |
| R02 | بالا | `canAffordAnything(state,machines)` در `checkEnd` test pointهای .5 قیمت را حتی در حالت غیرواقعی می‌شمارد. در UI و منطق بودجه تعریف «دستگاه فعال» یکسان نیست. با این حال factory نیز ارزان و شبانه‌روزی است، پس صرف حذف test در همین داده‌ها الزاماً نتیجهٔ تابع پایان را عوض نمی‌کند. دسترسی جغرافیایی نیز در تابع منظور نیست. | تعریف دستگاه‌های قابل احتساب بر اساس mode و دسترسی؛ تست حذف نقاط test در حالت عادی و مرز پول/ساعت |
| R03 | متوسط | `buildGraph` همهٔ یال‌ها را دوطرفه می‌سازد؛ oneway فقط در traffic است. خط مسیریابی ماشین ممکن است خلاف جهت راهنمایی کند. | گراف رانندگی جهت‌دار، پیاده طبق سیاست مطلوب؛ تست مسیر مجاز/ممنوع در دو جهت و مسیر کارخانه |
| R04 | متوسط | reset متغیرهای lines/lineTimer، timeoutهای reward/banner و stick را پاک نمی‌کند؛ real tracking/AR را خاموش نمی‌کند؛ factory.reset فقط resetArrival است و چرخهٔ کامیون ادامه دارد. | ابتدا تعریف سیاست reset؛ سپس reset وسط دیالوگ/پاداش/joystick/GPS/بارگیری بدون پیام یا ورودی عملیات قبلی؛ تست lifecycle |
| R05 | متوسط | ساعت بازی حین پرداخت ادامه دارد؛ checkEnd هر نیم‌ثانیه خریدها را می‌بیند ولی پاداش مأموریت بعد از تحویل اعمال می‌شود. اگر آخرین خرید پول را تمام کند، پایان بودجه می‌تواند پیش از جایزهٔ نجات‌دهنده نمایش داده شود. | تست خرید آخر با جایزهٔ نقدی، درحال‌تحویل پایان ندهد؛ پس از تسویهٔ پاداش وضعیت نهایی محاسبه شود |
| R06 | متوسط | شرط سرعت GPS فقط interact است. پس از بازشدن shop/آغاز payment سرعت و fix در swipe مجدداً اعتبارسنجی نمی‌شوند؛ paused بودن shop نیز updateReal را متوقف می‌کند. | سیاست اعتبار fix/سرعت/رسیدن در تأیید خرید تعریف شود؛ سناریؤ تغییر سرعت یا مکان وسط پرداخت بدون برداشت نامعتبر |
| R07 | متوسط | SW نسخه را از نام فایل‌ها می‌سازد و assets بدون hash مثل buildings.bin یا portrait در cache-first می‌مانند. تغییر فقط محتوای public با نام ثابت لزوماً cache جدید نمی‌سازد؛ cache imagery هم نسخه ندارد. | build با تغییر یک asset هم‌نام؛ نسخهٔ نصب‌شده فایل تازه بگیرد و tile قدیمی طبق سیاست مشخص invalidate شود |
| R08 | متوسط | main دانلود فایل optional را catch می‌کند، ولی decodeBuildings خارج catch است. پاسخ موفق با bin خراب یا HTML fallback می‌تواند bootstrap را قطع کند. | header/length/count معتبر یا خطای کنترل‌شده و fallback OSM؛ تست فایل کوتاه، magic غلط، truncated vertex |
| R09 | متوسط | وقتی imagery index موجود است city از satellite mode استفاده می‌کند، اما tile غایب/failed فقط failed علامت می‌خورد و دوباره تلاش نمی‌شود. آفلاین در منطقهٔ دیده‌نشده احتمالاً زمین کامل ندارد. | آزمون index موجود+JPG 404/offline؛ fallback خوانا و سیاست retry روشن |
| R10 | متوسط | `canAffordAnything` پس از بسته‌شدن همهٔ دستگاه‌های مقرون‌به‌صرفه به فردا نگاه نمی‌کند، ولی ساعت نمایش و isOpen روزانه modulo دارند. مأموریت مدرسه بدون پنجرهٔ شکست/انتظار ممکن است معطل بماند. | تصمیم محصول دربارهٔ روز دوم، شکست/تکرار مأموریت و انتظار؛ تست ۱۳:۳۰، ۲۳:۰۰ و ۰۰:۰۰ |
| R11 | پایین | `places` پایان از همهٔ خریدها و denominator از کل machines است؛ testها حتی وقتی غیرفعال‌اند در کل مکان‌ها می‌آیند. متن «با ماشین» فقط متن است و نه شرط مأموریت. | شمارش UI با دستگاه فعال و سیاست مأموریت مطابقت داشته باشد؛ پوشش test/real و خرید پیاده |
| R12 | متوسط | bundle JS حدود ۱٫۷۹ مگابایت و main بزرگ؛ world JSON و Three.js در payload شروع‌اند. تست فعلی FPS/زمان شروع یا مصرف حافظه را نمی‌سنجد. | baseline روی گوشی ضعیف قبل refactor؛ هدف عددی زمان شروع/FPS با توافق صاحب محصول، بدون حذف قابلیت |
R01 و R05 با import مستقیم توابع فعلی در Node نیز بررسی شدند:
- R01: مدرسه در minutes=809.98 وارد پرداخت/ready شد؛ در minutes=810، swipe برابر false، phase برابر idle، busy برابر false و balance همچنان 500 بود. مسیر main پس از false پنل را نمی‌بندد؛ اثر بصری پنل نیازمند بازتولید مرورگر است.
- R05: خرید آب کارخانه با balance=8، پس از swipe balance=0 و busy=true و canAffordAnything=false شد؛ completeMissions سپس balance=20 و canAffordAnything=true ساخت. این اختلاف پنجرهٔ checkEnd پیش از delivery را تأیید می‌کند؛ ترتیب نمایش modal نیازمند تست یکپارچه است.
- R02: در ۲۳:۰۰ با balance=8، با همهٔ تعاریف و بدون testها هر دو canAffordAnything=true بودند؛ کارخانه دلیل باقی‌ماندن نتیجه است. این مورد ناهماهنگی دامنه/سیاست است، نه ادعای اثبات‌شدهٔ تغییر خروجی پایان صرفاً به‌خاطر testها.
passing بودن تست‌های فعلی رد سناریوهای بالا نیست.
## ناهماهنگی اسناد قدیمی
- README هنوز Gitea deploy را SSH و DEPLOY_HOST توضیح می‌دهد؛ workflow فعلی registry + Docker socket است.
- README می‌گوید خیابان‌ها یک‌طرفه ندارند؛ داده و traffic one-way دارند اما routing ندارد. به‌جای حکم کلی این تفکیک لازم است.
- README شروع را سالن تولید نوشته؛ spawn واقعی حیاط کنار ماشین است.
- آرشیو مکالمهٔ قدیمی از روبات جای کودک و جنگل می‌گوید؛ وضعیت فعلی خانواده انسانی و روبات دستگاه است.
## پیشنهادهای محصول؛ نیازمند تصمیم
ذخیرهٔ پیشرفت محلی با schemaVersion و migration؛ برنامهٔ آزمون کامل موبایل؛ قواعد واقعی‌تر ترافیک/چراغ؛ پیشرفت مناطق و شهرها؛ مأموریت کارخانه/نقش کارکنان؛ تجربهٔ GPS با اعتبارسنجی روشن؛ accessibility و کاهش حرکت/مصرف باتری. backend و leaderboard تصمیم معماری/محصول جدا می‌خواهند؛ آن‌ها را صرفاً برای رفع یک UI bug اضافه نکنید.
## ترتیب اجرایی پیشنهادی
ابتدا صحت تراکنش و reset (R01/R04/R05)، سپس منطق mode/پایان (R02/R06/R10/R11)، سپس قابلیت اطمینان offline/data (R07/R08/R09)، سپس route direction (R03) و اندازه‌گیری کارایی (R12). هر تغییر بهتر است تسک مستقل با تست مربوط باشد.
+48
View File
@@ -0,0 +1,48 @@
# قرارداد تحویل تسک به Agent
## متن شروع پیشنهادی
> ابتدا docs/README.md و سندهای مرتبط با این تسک را بخوان. سپس کد فعلی و git status را بررسی کن؛ مستندات snapshot هستند و ممکن است پس از commit مبنا تغییر کرده باشند. رفتار فعلی، تصمیم محصول و پیشنهادهای آینده را یکی نگیر. فقط محدودهٔ این تسک را پیاده کن و مدارک تست، محدودیت و مستندات به‌روز را تحویل بده.
## قالب آمادهٔ تسک
```text
عنوان:
هدف کاربر و مسئله:
مراجع docs و شناسهٔ بک‌لاگ:
رفتار فعلی و مراحل بازتولید:
رفتار مطلوب (تصمیم صاحب بازی):
دامنهٔ تغییر / فایل‌های احتمالی:
خارج از دامنه:
قواعدی که باید حفظ شوند:
معیار پذیرش قابل مشاهده:
سناریوهای خطا و مرزها:
تست خودکار مرتبط:
تست دستی desktop/mobile/GPS/offline در صورت نیاز:
نیاز به asset یا دادهٔ بیرونی:
خروجی مورد انتظار (کد، سند، نتیجهٔ تست، تصویر در صورت نیاز):
مجوز commit/push/deploy و remote مقصد، اگر خواسته شده:
```
## چک‌لیست Agent
1. دستورهای AGENTS/RTK قابل اعمال و وضعیت تغییرات کاربر را بخوان؛ remote و branch را فرض نکن.
2. منبع حقیقت تسک را مشخص کن: اقتصاد game-state/scoring؛ مکان locations؛ نمایش main/style؛ حسگر realworld/ar؛ انتشار workflow/compose.
3. قبل از تغییر، تست مربوط و مسیر وابستگی را بفهم. برای bug ابتدا failing regression معنادار بساز؛ برای متن صرف تست تقلیدی نساز.
4. تغییر را با دامنهٔ کوچک انجام بده. محاسبهٔ قیمت یا شرط مأموریت را در UI دوباره تعریف نکن.
5. مقدار پول قبل/پس، busy، تراکنش یکتا، mode، timer و رفتار لغو را در تسک‌های مرتبط حفظ کن.
6. تست مرتبط و build را اجرا کن؛ برای GPU/permissions/offline از تست Node نتیجهٔ بصری استنتاج نکن.
7. سند مرتبط را هم اصلاح کن؛ قابلیت آینده را «انجام‌شده» علامت نزن مگر معیار پذیرش محقق شده باشد.
8. نتیجه را با فایل‌ها، شواهد، ریسک/محدودیت و عملیات واقعاً انجام‌شده گزارش کن.
## نمونهٔ تسک مستقل
عنوان: بازیابی UI پس از بسته‌شدن دستگاه در مرحلهٔ پرداخت (R01).
معیار پذیرش: در ۱۳:۲۹:۵۹ از مدرسه پرداخت را شروع کن، قبل swipe ساعت از ۱۳:۳۰ بگذرد؛ پول/خرید تغییر نکنند، پیام بسته‌بودن نمایش داده شود، پنل گیر نکند، کنترل حرکت برگردد و کاربر دستگاه دیگری انتخاب کند. لغو عادی و خرید موفق/برداشت یک‌باره همچنان پاس باشند. تغییر به حفظ ساعت پرداخت تنها با تصمیم صریح محصول انجام شود.
این نمونه پیشنهاد است و اجرای آن با ساخت این سند مجاز/انجام‌شده تلقی نمی‌شود.
## دستورهای مخزن و توصیف فناوری
در پایان این بررسی یک فایل AGENTS.md با دستورهای عمومی React/Next.js در workspace دیده شد. آن فایل از تغییرات این نوبت نیست. Agent توسعه باید نسخهٔ حاضر آن را پیش از کار بخواند و دستورهای قابل اعمال را رعایت کند؛ توصیف فناوری در این اسناد گزارش وضعیت واقعی کد است، نه جایگزین AGENTS یا مجوز مهاجرت به React/Redux/Tailwind. انتخاب زبان/UI برای توسعهٔ بعدی و هر تعارض عملی باید در همان تسک روشن شود. برای این تسک مستندسازی، زبان برنامه و کتابخانهٔ رابط تغییر نکرده‌اند.
+210
View File
@@ -0,0 +1,210 @@
# مرجع دقیق داده‌های قابل بازی
این جدول از exportهای کد در snapshot 751af76 استخراج شده است؛ با تغییر locations/game-state/scoring باید به‌روزرسانی شود.
## دستگاه‌ها
### دستگاه دفتر فنی مهندسی — factory
- دسته: کارخانه (factory)
- نشانی: بزرگراه مشهد–چناران، خیابان کاله، کنار پارک علم و فناوری خراسان، ساختمان اداری، دفتر فنی مهندسی
- ساعت: 00:00 تا 24:00؛ ضریب قیمت 0.5
- موقعیت تعریف‌شده: factory.demo در factory.js
- دستگاه عادی
- توضیح: دستگاه دفتر برنامه‌نویس‌ها؛ نصف قیمت برای مهمان‌های کارخانه
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 8 |
| آب‌میوه | juice | drink | 18 |
| چای سرد | tea | drink | 23 |
| بیسکویت | biscuit | snack | 13 |
| شکلات | chocolate | snack | 15 |
### بوستان ملت — mellat-park
- دسته: پارک (park)
- نشانی: بزرگراه وکیل‌آباد، ورودی جنوبی بوستان ملت، نزدیک ایستگاه مترو پارک ملت
- ساعت: 07:00 تا 23:00؛ ضریب قیمت 1
- موقعیت تعریف‌شده: anchor X=-487.6, Z=-326.4
- دستگاه عادی
- توضیح: نوشیدنی‌های خنک برای پیاده‌روی در پارک
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 15 |
| آب‌میوه | juice | drink | 35 |
| لیموناد | lemonade | drink | 40 |
| چای سرد | tea | drink | 45 |
| بیسکویت | biscuit | snack | 25 |
| شکلات | chocolate | snack | 30 |
| کیک | cake | snack | 35 |
| آجیل | nuts | snack | 65 |
### مجموعه آموزشی راحیل — school-rahil
- دسته: مدرسه (school)
- نشانی: بولوار امامت، کوچهٔ امامت ۲، کنار در ورودی مدرسه
- ساعت: 07:30 تا 13:30؛ ضریب قیمت 0.9
- موقعیت تعریف‌شده: anchor X=-554.6, Z=-569.7
- دستگاه عادی
- توضیح: فقط در ساعت کاری مدرسه (۷:۳۰ تا ۱۳:۳۰) باز است
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 14 |
| شیر کاکائو | milk | drink | 36 |
| آب‌میوه | juice | drink | 32 |
| بیسکویت | biscuit | snack | 23 |
| کیک | cake | snack | 32 |
### دانشگاه فردوسی مشهد — ferdowsi-university
- دسته: دانشگاه (university)
- نشانی: میدان آزادی، پردیس دانشگاه فردوسی، مسیر پیاده‌روی مرکزی
- ساعت: 07:00 تا 23:00؛ ضریب قیمت 0.8
- موقعیت تعریف‌شده: anchor X=-936, Z=645
- دستگاه عادی
- توضیح: تخفیف دانشجویی: ۲۰٪ ارزان‌تر
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 12 |
| چای سرد | tea | drink | 36 |
| شیر کاکائو | milk | drink | 32 |
| بیسکویت | biscuit | snack | 20 |
| شکلات | chocolate | snack | 24 |
| آجیل | nuts | snack | 52 |
| کیک | cake | snack | 28 |
### دانشکدهٔ ادبیات دکتر شریعتی — literature-faculty
- دسته: دانشگاه (university)
- نشانی: پردیس دانشگاه فردوسی، بولوار دانش، جلوی دانشکدهٔ ادبیات
- ساعت: 07:00 تا 23:00؛ ضریب قیمت 0.8
- موقعیت تعریف‌شده: anchor X=-417.1, Z=966.3
- دستگاه عادی
- توضیح: تخفیف دانشجویی: ۲۰٪ ارزان‌تر
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 12 |
| چای سرد | tea | drink | 36 |
| لیموناد | lemonade | drink | 32 |
| بیسکویت | biscuit | snack | 20 |
| شکلات | chocolate | snack | 24 |
### دانشگاه علوم پزشکی مشهد — medical-university
- دسته: دانشگاه (university)
- نشانی: میدان آزادی، پردیس دانشگاه علوم پزشکی مشهد
- ساعت: 07:00 تا 23:00؛ ضریب قیمت 0.8
- موقعیت تعریف‌شده: anchor X=-272.1, Z=150.6
- دستگاه عادی
- توضیح: تخفیف دانشجویی: ۲۰٪ ارزان‌تر
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 12 |
| آب‌میوه | juice | drink | 28 |
| شیر کاکائو | milk | drink | 32 |
| آجیل | nuts | snack | 52 |
| بیسکویت | biscuit | snack | 20 |
### ایستگاه مترو آزادی — azadi-metro
- دسته: ایستگاه مترو (metro)
- نشانی: بولوار ملک‌آباد، ورودی ایستگاه مترو آزادی
- ساعت: 06:00 تا 22:00؛ ضریب قیمت 1
- موقعیت تعریف‌شده: anchor X=494.1, Z=280.3
- دستگاه عادی
- توضیح: دستگاه کنار گیت‌های مترو
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 15 |
| چای سرد | tea | drink | 45 |
| آب‌میوه | juice | drink | 35 |
| شکلات | chocolate | snack | 30 |
| بیسکویت | biscuit | snack | 25 |
### مشهد مال — mashhad-mall
- دسته: مرکز خرید (mall)
- نشانی: خیابان اخوان ثالث، کنار ورودی پارکینگ مشهد مال
- ساعت: 10:00 تا 23:00؛ ضریب قیمت 1.1
- موقعیت تعریف‌شده: anchor X=1940.6, Z=-367.8
- دستگاه عادی
- توضیح: بیشترین تنوع محصول؛ قیمت کمی بالاتر
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 17 |
| آب‌میوه | juice | drink | 39 |
| شیر کاکائو | milk | drink | 44 |
| چای سرد | tea | drink | 50 |
| لیموناد | lemonade | drink | 44 |
| شکلات | chocolate | snack | 33 |
| کیک | cake | snack | 39 |
| آجیل | nuts | snack | 72 |
| بیسکویت | biscuit | snack | 28 |
### نقطهٔ آزمایشی ۱ — test-1
- دسته: نقطهٔ آزمایشی GPS (test)
- نشانی: کنار کارخانهٔ روبات مارکت · 36.450276, 59.423431
- ساعت: 00:00 تا 24:00؛ ضریب قیمت 0.5
- موقعیت تعریف‌شده: GPS 36.450276, 59.423431
- فقط حالت دنیای واقعی؛ نقطهٔ آزمایشی
- توضیح: برای آزمایش حالت دنیای واقعی: با گوشی تا اینجا راه بروید
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 8 |
| بیسکویت | biscuit | snack | 13 |
### نقطهٔ آزمایشی ۲ — test-2
- دسته: نقطهٔ آزمایشی GPS (test)
- نشانی: کنار کارخانهٔ روبات مارکت · 36.450172, 59.423209
- ساعت: 00:00 تا 24:00؛ ضریب قیمت 0.5
- موقعیت تعریف‌شده: GPS 36.450172, 59.423209
- فقط حالت دنیای واقعی؛ نقطهٔ آزمایشی
- توضیح: برای آزمایش حالت دنیای واقعی: با گوشی تا اینجا راه بروید
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 8 |
| بیسکویت | biscuit | snack | 13 |
### نقطهٔ آزمایشی ۳ — test-3
- دسته: نقطهٔ آزمایشی GPS (test)
- نشانی: کنار کارخانهٔ روبات مارکت · 36.449976, 59.422866
- ساعت: 00:00 تا 24:00؛ ضریب قیمت 0.5
- موقعیت تعریف‌شده: GPS 36.449976, 59.422866
- فقط حالت دنیای واقعی؛ نقطهٔ آزمایشی
- توضیح: برای آزمایش حالت دنیای واقعی: با گوشی تا اینجا راه بروید
| محصول | productId | نوع | قیمت نهایی هتومان |
|---|---|---|---:|
| آب معدنی | water | drink | 8 |
| بیسکویت | biscuit | snack | 13 |
## مأموریت‌ها
| ترتیب | id | عنوان | target | XP | هتومان | شرط تکمیل واقعی |
|---:|---|---|---|---:|---:|---|
| 1 | factory | در دفتر فنی مهندسی کارخانه از دستگاه وندینگ خرید کن | factory | 100 | 20 | وجود خرید factory |
| 2 | first | با ماشین تا بوستان ملت برو و اولین خرید شهری را انجام بده | mellat-park | 150 | 30 | وجود خرید mellat-park |
| 3 | school | قبل از تعطیلی مدرسه (۱۳:۳۰) از دستگاه مدرسه خرید کن | school-rahil | 200 | 40 | وجود خرید school-rahil؛ ساعت خرید از اعتبارسنجی خرید می‌آید |
| 4 | campus | به دانشگاه فردوسی برو و یک خوراکی بخر | ferdowsi-university | 200 | 40 | خرید snack از ferdowsi-university |
| 5 | metro | از دستگاه ایستگاه مترو آزادی خرید کن | azadi-metro | 150 | 30 | وجود خرید azadi-metro |
| 6 | mall | با ماشین تا مشهد مال برو و خرید کن | mashhad-mall | 250 | 50 | وجود خرید mashhad-mall |
| 7 | explorer | از ۵ مکان متفاوت شهر خرید کن | پویا/نزدیک‌ترین بازدیدنشده | 400 | 80 | حداقل ۵ machineId متفاوت در پنج دستهٔ شهری؛ factory/test حذف |
«پنج مکان» با «پنج دسته» متفاوت است: explorer مکان متمایز می‌شمارد؛ پاداش allCategories دسته متمایز. خریدهای از پیش انجام‌شده می‌توانند مأموریت غیر فعال را هم تکمیل کنند. رسیدن جغرافیایی یا سوارشدن ماشین در شرط مأموریت ذخیره نمی‌شود.
## نقاط ثابت
مرکز کارخانه X=-10308.4, Z=-14964.2 و محور (0.7071067811865476, 0.7071067811865476)؛ مکان دقیق ماشین و خانواده از factory.start/car محاسبه می‌شود.
+42
View File
@@ -0,0 +1,42 @@
# وضعیت پس از انتقال به React
این سند بر اسناد snapshot پیش از ریفکتور اولویت دارد. زبان JavaScript و رابط React 18 + Vite است؛ Ant Design و TypeScript اضافه نشده‌اند. Tailwind CSS استایل رابط را تولید می‌کند. Three.js و نسخهٔ قبلی Vite ارتقا داده نشده‌اند.
## معماری
- `src/main.jsx` و `routes/AppRoutes.jsx`: Provider و lazy/Suspense برای صفحهٔ بازی.
- `pages/game/`: briefing، HUD، نقشه، خرید/پرداخت، زیرنویس و نمایشگاه با JSX؛ بدون innerHTML و بدون ارسال مستقیم HTTP از کامپوننت.
- `components/shared/GameNode.jsx`: نمایش props و هماهنگی modal بومی؛ از store یا services وابستگی ندارد.
- `hooks/useGameEngine.js`: بارگذاری موتور و cleanup در unmount.
- `game/createGame.js`: راه‌اندازی مرتب زیرسیستم‌ها از runtimeBindings؛ توقف مشترک با AbortSignal.
- `game/runtime/`: چرخهٔ عمر، ورودی/حرکت، خرید، نقشه، HUD، برخوردهای داستانی، GPS/AR و render loop جدا هستند. context فقط برای state زندهٔ شبیه‌سازی و اشیای غیرقابل سریال‌سازی است؛ داخل Redux renderer/Scene/Texture نمی‌گذاریم.
- `game/models/`: ساخت هندسهٔ کارخانه، خانواده، کارکنان، خودرو، دستگاه و نمونه‌های نمایشگاه. factory/city/family اکنون مراحل مستقل ساخت و helperهای نام‌دار دارند.
- `game/economy/`: gameState، payment و scoring با قرارداد اقتصادی قبلی.
- `constants/`: مکان‌ها و showroomCatalog؛ `utils/`: geo و showroomAnswers؛ `types/`: JSDoc قراردادها.
- `services/apiClient.js`: تنها Axios instance، timeout و error normalization. worldService دادهٔ optional و tileها را می‌گیرد؛ imageryDownloadService اسکریپت آماده‌سازی تصویر را پوشش می‌دهد.
## مرز Redux و موتور
slice `game` تصویر قابل سریال‌سازی پیشرفت اقتصادی را برای بخش‌های مشترک رابط دارد. موتور تنها نویسندهٔ قواعد شبیه‌سازی است؛ رابط فرمان می‌فرستد و مستقلاً پول/XP تغییر نمی‌دهد. `interface` snapshot نمایش را نگه می‌دارد. bridge وضعیت pause را هم‌زمان برای موتور در دسترس می‌گذارد و انتشار React را به ده بار در ثانیه محدود می‌کند؛ WebGL مستقل render می‌کند. callbackها در registry محلی bridge‌اند و در Redux قرار ندارند. وضعیت موقت preview تصویر داخل کامپوننت است.
## استایل
`styles/game.css` ورودی Tailwind است. `styles/gameTheme.js` لایهٔ پایه و tokenهای ظاهر قبلی را برای جلوگیری از تغییر ناخواستهٔ UI نگه می‌دارد و از plugin تنظیمات Tailwind تولید می‌شود. کامپوننت‌های جدید نمایشگاه از utilityهای Tailwind استفاده می‌کنند. inline style تنها برای مختصات پویا/پیشرفت به کار می‌رود. ادامهٔ طراحی نباید لایهٔ CSS مستقل موازی بسازد.
## نمایشگاه
سه مدل rm-35/rm-48/rm-60 در حیاط، نزدیک نقطهٔ شروع و خودرو هستند. نمایش سه‌بعدی نمونه‌ها به‌ترتیب ۳۵، ۴۸ و ۶۰ slot دارد. با نزدیک‌شدن، نشان تعامل Q ظاهر می‌شود؛ در محوطه، دکمهٔ «مدل‌های کارخانه» نیز کاتالوگ را باز می‌کند. روبات قیمت و امکانات را از یک کاتالوگ مشترک پاسخ می‌دهد.
قیمت‌ها به ریال‌اند: ۴٬۶۸۰٬۰۰۰٬۰۰۰؛ ۵٬۴۸۰٬۰۰۰٬۰۰۰؛ ۵٬۹۸۰٬۰۰۰٬۰۰۰. این‌ها قیمت دستگاه کامل‌اند و به اقتصاد خوراکی/هتومان، XP، موجودی و مأموریت وصل نیستند. پاسخ‌ها محلی و تعیین‌شده‌اند؛ backend یا مدل زبانی بیرونی ندارند.
مسیر فایل‌های `images/machine-*-base.webp`، `images/machine-*-white.webp` و `gifs/machine-*.gif` ثبت شده، اما فایل اصلی هنوز تحویل نشده است. UI نبود تصویر را به‌وضوح نشان می‌دهد و مدل سه‌بعدی مستقل وجود دارد. مسیرهای ریشه‌ای کاتالوگ هنگام نمایش نسبت به پوشهٔ انتشار حل می‌شوند تا GitHub Pages هم کار کند.
## آزمون و انتشار
`npm test` شامل تست Node قبلی و Vitest/React Testing Library است. CIهای GitHub/Gitea و Dockerfile نیز همین فرمان را اجرا می‌کنند. `npm run lint`، `npm run format:check` و `npm run build` بررسی‌های دیگرند. نسخهٔ Vitest تازه انتخاب شده فاقد هشدارهای audit مرحلهٔ نصب است؛ نسخه‌های قبلی بازی ارتقا نیافتند.
Service Worker همچنان در Vite تولید می‌شود. با تأیید کاربر، `fetch` بومی داخل آن استثنای قاعدهٔ Axios است؛ interception و cache آفلاین آن حفظ شده‌اند. assetهای جدید build در precache می‌آیند؛ tileهای ماهواره‌ای فقط پس از مشاهده cache می‌شوند. صف ارسال آنلاین جدید اضافه نشده است.
## محدودیت بررسی
اجرای مرورگر محلی برای briefing، شروع، HUD، نقشه و پرسیدن قیمت نمایشگاه انجام شده است. تست حسگر واقعی، GPS میدانی، camera permission روی گوشی و انتشار روی سرور در این ریفکتور انجام نشده‌اند. یافته‌های رفتاری قدیمی docs/08 الزاماً اصلاح نشده‌اند؛ آن‌ها تسک‌های جدا هستند. بلوک داده/هندسه و بعضی الگوریتم‌های قدیمی هنوز می‌توانند طولانی‌تر از هدف تقریبی ۳۰ خط باشند؛ ادعای انطباق صددرصد تمام توابع و nesting وجود ندارد. برای تغییر بعدی هر ماژول، قواعد AGENTS روی کد تازه الزامی است.
+39
View File
@@ -0,0 +1,39 @@
# مرجع توسعهٔ بازی روبات مارکت
تاریخ بررسی: ۲۰۲۶/۱۰/۲۸ — مبنا: commit `751af76` در checkout محلی. این مجموعه از کد فعلی، تست‌ها، فایل‌های انتشار و تاریخچهٔ گیت استخراج شده است. وضعیت سرویس‌های آنلاین و اجرای بصری روی گوشی در این بررسی تأیید نشده‌اند.
## وضعیت جدید React
ابتدا [معماری و قابلیت‌های پس از انتقال به React](11-react-migration.md) را بخوانید. اسناد زیر snapshot پیش از ریفکتورند؛ قواعد اقتصادی حفظ شده‌اند، اما مسیر فایل‌ها، فناوری UI و فرمان تست تغییر کرده‌اند.
## مسیر مطالعه برای Agent
۱. [وضعیت و دامنه](01-product-and-scenario.md)
۲. [قوانین و اقتصاد](02-game-rules.md)
۳. [معماری و فناوری](03-architecture.md)
۴. [جهان، نقشه و داده](04-world-and-data.md)
۵. [GPS، AR و آفلاین](05-device-and-offline.md)
۶. [اجرا، تست و انتشار](06-development-and-deployment.md)
۷. [تاریخچهٔ کارهای انجام‌شده](07-implemented-plans.md)
۸. [بازبینی کد و بک‌لاگ پیشنهادی](08-review-and-backlog.md)
۹. [راهنمای تحویل تسک به Agent](09-agent-task-guide.md)
۱۰. [فهرست دقیق دستگاه‌ها و مأموریت‌ها](10-game-reference.md)
## نحوهٔ استفاده
- «پیاده‌سازی‌شده» یعنی در کد وجود دارد؛ تضمین صحت بصری یا آماده‌بودن محیط production نیست.
- «یافتهٔ کد» از بررسی استاتیک به دست آمده است؛ موارد نیازمند بازتولید صریحاً مشخص شده‌اند.
- «پیشنهاد» تصمیم محصول یا تسک مصوب محسوب نمی‌شود. صاحب بازی باید اولویت و رفتار مطلوب را تعیین کند.
- کد مرجع رفتار فعلی است. تغییر اقتصاد، سناریو، داده یا انتشار باید همراه به‌روزرسانی سند مربوط انجام شود.
- سناریوی جنگل و پیک‌نیک در تاریخچه قرار دارد؛ شرح نسخهٔ فعال نیست. روبات فعلی راهنمای دستگاه است و جای کودک راه نمی‌رود.
- مستندات هیچ مجوزی برای push، deploy، تغییر سرور، دانلود مجدد داده یا استفاده از secret ایجاد نمی‌کنند؛ محدودهٔ تسک کاربر تعیین‌کننده است.
## نتیجهٔ بررسی این نوبت
چهار فایل تست فعلی با ۱۶ تعریف تست بررسی و اجرای آن‌ها موفق شد. `npm run build` موفق بود: ۳۳ ماژول، JavaScript حدود ۱٬۷۹۴٫۳۵ کیلوبایت (gzip حدود ۵۷۶٫۶۱)، CSS حدود ۱۹٫۷۰ کیلوبایت. هشدار chunk بزرگ‌تر از ۵۰۰ کیلوبایت باقی است. این اعداد اندازهٔ bundle هستند، نه اندازهٔ کل public یا اندازه‌گیری FPS.
در این نوبت فقط مستندات اضافه/اصلاح می‌شوند؛ یافته‌های گزارش‌شده به‌صورت ضمنی اصلاح نشده‌اند. جزئیات محدودیت آزمون و کارهای پیشنهادی در سند بازبینی آمده است.
## محدودهٔ فایل‌های این تحویل
خروجی این کار پوشهٔ docs و لینک آن در README ریشه است. هنگام پایان بررسی، AGENTS.md و دو فایل src/data/iran.json و src/data/mellat.json نیز در وضعیت تغییرکردهٔ workspace دیده شدند؛ آن‌ها توسط این کار ایجاد/ویرایش نشده‌اند و در تحویل مستندسازی منظور نیستند. مبنای کد برنامه 751af76 است؛ شمارش داده‌ها از فایل‌های حاضر هنگام بررسی گرفته شده و باید پس از هر بازتولید داده تازه شود.
+28
View File
@@ -0,0 +1,28 @@
import js from '@eslint/js';
import globals from 'globals';
import hooks from 'eslint-plugin-react-hooks';
export default [
{ ignores: ['dist/**', 'node_modules/**', 'public/**', 'src/data/**', 'docs/**'] },
js.configs.recommended,
{
files: ['**/*.{js,jsx,mjs}'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: { ...globals.browser, ...globals.node },
parserOptions: { ecmaFeatures: { jsx: true } },
},
rules: {
'no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^[A-Z]', ignoreRestSiblings: true },
],
'no-empty': ['error', { allowEmptyCatch: true }],
},
},
{
files: ['src/**/*.{js,jsx}'],
plugins: { 'react-hooks': hooks },
rules: { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'error' },
},
];
+24 -1
View File
File diff suppressed because one or more lines are too long
+4257 -4
View File
File diff suppressed because it is too large Load Diff
+32 -4
View File
@@ -1,21 +1,49 @@
{ {
"name": "game", "name": "game",
"version": "1.0.0", "version": "1.0.0",
"description": "A Persian 3D forest walk and vending-machine game", "description": "A Persian third-person 3D city game: find Robot Market vending machines across real Mashhad streets",
"main": "index.js",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
"build": "vite build", "build": "vite build",
"preview": "vite preview --host 0.0.0.0" "preview": "vite preview --host 0.0.0.0",
"format": "prettier --write \"src/**/*.{js,jsx,css,md}\" \"tests/**/*.js\" \"*.{js,html}\" \"scripts/*.mjs\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,css,md}\" \"tests/**/*.js\" \"*.{js,html}\" \"scripts/*.mjs\"",
"predev": "npm run format",
"prebuild": "npm run format",
"test": "npm run test:logic && npm run test:ui",
"test:logic": "node --test tests/*.test.js",
"test:ui": "vitest run",
"lint": "eslint . --max-warnings 0"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/vazirmatn": "^5.2.5",
"@reduxjs/toolkit": "^2.13.0",
"axios": "^1.21.0",
"clsx": "^2.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.3.1",
"three": "^0.180.0" "three": "^0.180.0"
}, },
"devDependencies": { "devDependencies": {
"vite": "^6.4.3" "@eslint/js": "^9.39.5",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.6.2",
"eslint": "^9.39.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.13.0",
"jsdom": "^26.1.0",
"postcss": "^8.5.32",
"prettier": "^3.9.10",
"tailwindcss": "^3.4.19",
"vite": "^6.4.3",
"vitest": "^4.1.12"
} }
} }
+1
View File
@@ -0,0 +1 @@
export default { plugins: { tailwindcss: {}, autoprefixer: {} } };
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show More