Compare commits

..
14 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
1150 changed files with 206195 additions and 4352 deletions
+37 -45
View File
@@ -1,22 +1,16 @@
# Gitea Actions. While .gitea/workflows exists Gitea ignores .github/workflows (GitHub Pages only). # Gitea Actions. While .gitea/workflows exists Gitea ignores .github/workflows (GitHub Pages only).
# #
# Every push and pull request: install, test and build. # Every push and pull request: install, test and build.
# Push to main: build the Docker image, push it to the Gitea registry, then have the server pull it. # Push to main: build the Docker image, push it to the Gitea registry, then start it with
# The server holds no source code, only deploy/docker-compose.yml and a .env naming the image. # docker compose through the runner's Docker socket (the runner lives on the deployment server).
# #
# Repository settings → Actions → Secrets: # Repository settings → Actions → Secrets:
# REGISTRY_USERNAME Gitea user that publishes images # REGISTRY_USERNAME Gitea user that publishes images
# REGISTRY_TOKEN its personal access token with package Read and Write # REGISTRY_TOKEN its personal access token with package Read and Write
# (the built-in GITEA_TOKEN cannot publish packages) # (the built-in GITEA_TOKEN cannot publish packages)
# DEPLOY_HOST server address (deploy is skipped while this is empty)
# DEPLOY_SSH_KEY private key whose public key is in the server user's ~/.ssh/authorized_keys
# DEPLOY_USER SSH user, default root; it must be able to run docker
# DEPLOY_PORT SSH port, default 22
# DEPLOY_KNOWN_HOSTS optional output of `ssh-keyscan <host>`; otherwise the host key is scanned on each run
# Repository settings → Actions → Variables (optional): # Repository settings → Actions → Variables (optional):
# NPM_REGISTRY npm mirror used by the runner and the server's Docker build when # 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 # registry.npmjs.org is unreachable, e.g. from servers in Iran
# DEPLOY_PATH directory on the server, default /opt/vending-machine-game
# HEALTH_URL URL checked after deploy, e.g. https://robotmarkets.ir/ # HEALTH_URL URL checked after deploy, e.g. https://robotmarkets.ir/
name: CI/CD name: CI/CD
@@ -47,8 +41,12 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install - name: Install
run: npm ci --no-audit --no-fund run: npm ci --no-audit --no-fund
- name: Check formatting
run: npm run format:check
- name: Lint
run: npm run lint
- name: Test - name: Test
run: node --test tests/*.test.js run: npm test
- name: Build - name: Build
run: npm run build run: npm run build
@@ -90,56 +88,50 @@ jobs:
--tag "$IMAGE" . --tag "$IMAGE" .
docker push "$IMAGE" docker push "$IMAGE"
# Show the package under this repository's Packages tab. # Show the package under this repository's Packages tab. Gitea answers 400 when the package
curl --fail-with-body --silent --show-error --request POST \ # 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" \ --header "Authorization: token $REGISTRY_TOKEN" \
"https://$REGISTRY/api/v1/packages/$OWNER/container/${REPO,,}/-/link/$REPO" "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 "image=$IMAGE" >> "$GITEA_OUTPUT"
echo "Published and linked: $IMAGE" 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: deploy:
needs: publish needs: publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
container:
image: node:22-bookworm
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
DEPLOY_PATH: ${{ vars.DEPLOY_PATH }}
HEALTH_URL: ${{ vars.HEALTH_URL }}
IMAGE: ${{ needs.publish.outputs.image }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Deploy to the server - name: Deploy the published image
shell: bash
env:
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
IMAGE: ${{ needs.publish.outputs.image }}
run: | run: |
if [ -z "$DEPLOY_HOST" ] || [ -z "$DEPLOY_SSH_KEY" ]; then set -euo pipefail
echo "DEPLOY_HOST / DEPLOY_SSH_KEY secrets are not set: skipping deploy." DOCKER_CONFIG="$(mktemp -d)"
exit 0 export DOCKER_CONFIG
fi trap 'rm -rf "$DOCKER_CONFIG"' EXIT
port="${DEPLOY_PORT:-22}"
target="${DEPLOY_USER:-root}@${DEPLOY_HOST}" printf '%s' "$REGISTRY_TOKEN" | docker login git.my-rm.com --username "$REGISTRY_USERNAME" --password-stdin
dir="${DEPLOY_PATH:-/opt/vending-machine-game}" docker compose -p vending-machine-game -f deploy/docker-compose.yml pull
mkdir -p ~/.ssh && chmod 700 ~/.ssh docker compose -p vending-machine-game -f deploy/docker-compose.yml up -d --remove-orphans
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key && chmod 600 ~/.ssh/deploy_key docker compose -p vending-machine-game -f deploy/docker-compose.yml ps
if [ -n "$DEPLOY_KNOWN_HOSTS" ]; then docker image prune -f
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
else
ssh-keyscan -p "$port" "$DEPLOY_HOST" > ~/.ssh/known_hosts 2>/dev/null
fi
# Only the compose file goes to the server; the app itself comes from the registry image.
# The server user must already be logged in to git.my-rm.com (docker login, package Read token).
ssh_opts="-i $HOME/.ssh/deploy_key -p $port"
ssh $ssh_opts "$target" "mkdir -p '$dir'"
scp -i ~/.ssh/deploy_key -P "$port" deploy/docker-compose.yml "$target:$dir/docker-compose.yml"
ssh $ssh_opts "$target" "cd '$dir' && cp -f .env .env.previous 2>/dev/null; printf 'IMAGE=%s\n' '$IMAGE' > .env && docker compose pull && docker compose up -d --remove-orphans && docker image prune -f"
- name: Check the site - name: Check the site
if: env.HEALTH_URL != '' && env.DEPLOY_HOST != '' if: vars.HEALTH_URL != ''
shell: bash
env:
HEALTH_URL: ${{ vars.HEALTH_URL }}
run: | run: |
for attempt in $(seq 1 20); do for attempt in $(seq 1 20); do
if curl -fsS -o /dev/null "$HEALTH_URL"; then if curl -fsS -o /dev/null "$HEALTH_URL"; then
+5 -1
View File
@@ -27,8 +27,12 @@ jobs:
node-version: 22 node-version: 22
cache: npm cache: npm
- run: npm ci - run: npm ci
- name: Check formatting
run: npm run format:check
- name: Lint
run: npm run lint
- name: Test game logic - name: Test game logic
run: node --test tests/*.test.js run: npm test
- run: npm run build - run: npm run build
- uses: actions/configure-pages@v5 - uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3 - uses: actions/upload-pages-artifact@v3
+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?
+17 -1
View File
@@ -2,7 +2,20 @@
# or ":80" for plain HTTP behind another proxy or for a local test. # or ":80" for plain HTTP behind another proxy or for a local test.
{$SITE_ADDRESS::80} { {$SITE_ADDRESS::80} {
root * /srv root * /srv
encode zstd gzip # 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 # Hashed build files never change; the page, service worker and manifest must always be revalidated
# so installed copies pick up new versions. # so installed copies pick up new versions.
@@ -10,6 +23,9 @@
header @hashed Cache-Control "public, max-age=31536000, immutable" header @hashed Cache-Control "public, max-age=31536000, immutable"
@fresh path / /index.html /sw.js /manifest.webmanifest @fresh path / /index.html /sw.js /manifest.webmanifest
header @fresh Cache-Control "no-cache" 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 @manifest path /manifest.webmanifest
header @manifest Content-Type "application/manifest+json" header @manifest Content-Type "application/manifest+json"
+1 -1
View File
@@ -6,7 +6,7 @@ ARG NPM_REGISTRY=https://registry.npmjs.org/
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund --registry="$NPM_REGISTRY" --fetch-retries=3 --fetch-timeout=60000 RUN npm ci --no-audit --no-fund --registry="$NPM_REGISTRY" --fetch-retries=3 --fetch-timeout=60000
COPY . . COPY . .
RUN node --test tests/*.test.js && npm run build 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. # Stage 2: serve dist/ with Caddy, which also obtains and renews the HTTPS certificate.
FROM caddy:2.8.4-alpine FROM caddy:2.8.4-alpine
+101 -45
View File
@@ -1,21 +1,42 @@
# عملیات مشهد — روبات مارکت # عملیات مشهد — روبات مارکت
بازی سه‌بعدی مرورگری (Three.js) با حال‌وهوای بازی‌های اکشن سوم‌شخص، بدون سلاح و خشونت. یک پدر و فرزندش از کارخانهٔ سازندهٔ دستگاه‌ها راه می‌افتند، در خیابان‌های واقعی مشهد می‌گردند، دستگاه‌های وندینگ روبات مارکت را پیدا می‌کنند و با هر خرید امتیاز می‌گیرند. روبات راهنمای روبات مارکت داخل دستگاه‌ها زندگی می‌کند: وقتی خانواده به یک دستگاه می‌رسند، از آن بیرون می‌آید، خوش‌آمد می‌گوید و راهنمایی‌شان می‌کند. 📚 [مستندات کامل بازی و راهنمای توسعه برای Agent](docs/README.md) — قوانین، سناریو، معماری، تاریخچه و بک‌لاگ بر اساس بررسی کد فعلی.
بازی سه‌بعدی مرورگری (React 18 + Three.js، JavaScript، Tailwind CSS) با حال‌وهوای بازی‌های اکشن سوم‌شخص، بدون سلاح و خشونت. یک پدر و فرزندش از کارخانهٔ سازندهٔ دستگاه‌ها راه می‌افتند، در خیابان‌های واقعی مشهد می‌گردند، دستگاه‌های وندینگ روبات مارکت را پیدا می‌کنند و با هر خرید امتیاز می‌گیرند. روبات راهنمای روبات مارکت داخل دستگاه‌ها زندگی می‌کند: وقتی خانواده به یک دستگاه می‌رسند، از آن بیرون می‌آید، خوش‌آمد می‌گوید و راهنمایی‌شان می‌کند.
بازی یک PWA است: روی گوشی نصب می‌شود و آفلاین هم اجرا می‌شود. بازی یک PWA است: روی گوشی نصب می‌شود و آفلاین هم اجرا می‌شود.
🎮 [اجرای آنلاین بازی](https://hamedabdollahzade.github.io/vending-machine-game/) 🎮 [اجرای آنلاین بازی](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: - **محدودهٔ قابل بازی:** حدود ۱۵ در ۱۸ کیلومتر از شمال‌غرب تا غرب مشهد، همه از داده‌های واقعی OpenStreetMap:
- جزئیات کامل اطراف کارخانه و محدودهٔ بوستان ملت و دانشگاه فردوسی - جزئیات کامل اطراف کارخانه و محدودهٔ بوستان ملت و دانشگاه فردوسی
- همهٔ خیابان‌ها و ساختمان‌ها در فاصلهٔ ۲۵۰ متری مسیر رانندگی - همهٔ خیابان‌ها و ساختمان‌ها در فاصلهٔ ۲۵۰ متری مسیر رانندگی
- شبکهٔ خیابان‌های اصلی در کل منطقه - شبکهٔ خیابان‌های اصلی در کل منطقه
- **دستگاه‌ها:** دستگاه نمونهٔ کارخانه (نصف قیمت)، بوستان ملت، مجموعه آموزشی راحیل (فقط ۷:۳۰ تا ۱۳:۳۰)، دانشگاه فردوسی و دانشکدهٔ ادبیات (۲۰٪ تخفیف دانشجویی)، دانشگاه علوم پزشکی، ایستگاه مترو آزادی، مشهد مال (از ساعت ۱۰). همهٔ دستگاه‌ها و محصولاتشان فرضی‌اند. - **دستگاه‌ها:** دستگاه دفتر فنی مهندسی کارخانه (نصف قیمت)، بوستان ملت، مجموعه آموزشی راحیل (فقط ۷:۳۰ تا ۱۳:۳۰)، دانشگاه فردوسی و دانشکدهٔ ادبیات (۲۰٪ تخفیف دانشجویی)، دانشگاه علوم پزشکی، ایستگاه مترو آزادی، مشهد مال (از ساعت ۱۰). همهٔ دستگاه‌ها و محصولاتشان فرضی‌اند.
## کنترل ## کنترل
@@ -31,7 +52,9 @@
| ترمز دستی | Space | — | | ترمز دستی | Space | — |
| نقشهٔ شهر | M یا کلیک روی نقشهٔ گوشه | لمس نقشه | | نقشهٔ شهر | M یا کلیک روی نقشهٔ گوشه | لمس نقشه |
کنترل‌ها با صفحه‌کلید فارسی هم کار می‌کنند. دوربین فقط سوم‌شخص است. کنترل‌ها با صفحه‌کلید فارسی هم کار می‌کنند. دوربین در بازی عادی سوم‌شخص است و در حالت دنیای واقعی اول‌شخص.
روی گوشی، اعلان‌ها (گفت‌وگوها، دکمهٔ خرید، پیام مأموریت، امتیاز و وضعیت GPS) وسط صفحه نمایش داده می‌شوند. خود دکمهٔ خرید هم قابل لمس است.
## نقشه و مسیریابی ## نقشه و مسیریابی
@@ -60,8 +83,13 @@
دکمهٔ «📍 بازی در دنیای واقعی» در صفحهٔ شروع یا «📍 دنیای واقعی» در نوار پایین این حالت را روشن می‌کند. اول نکته‌های ایمنی نمایش داده می‌شود و بعد اجازهٔ موقعیت و قطب‌نما گرفته می‌شود. دکمهٔ «📍 بازی در دنیای واقعی» در صفحهٔ شروع یا «📍 دنیای واقعی» در نوار پایین این حالت را روشن می‌کند. اول نکته‌های ایمنی نمایش داده می‌شود و بعد اجازهٔ موقعیت و قطب‌نما گرفته می‌شود.
- خانواده روی نقشهٔ بازی همان‌جایی است که گوشی هست و با راه رفتن واقعی جابه‌جا می‌شود. جهتشان از قطب‌نمای گوشی یا جهت حرکت گرفته می‌شود. - **اول‌شخص:** نما از چشم خودتان است. دوربین بازی در جای گوشی (GPS) و ارتفاع چشم قرار می‌گیرد و فقط با راه رفتن واقعی جابه‌جا می‌شود. صفحه‌کلید و جوی‌استیک در این حالت کاری نمی‌کنند. جهت نگاه از حسگر جهت گوشی گرفته می‌شود. اگر گوشی حسگر جهت نداشته باشد، جهت از قطب‌نما و جهت راه رفتن گرفته می‌شود و با کشیدن انگشت هم می‌چرخید.
- موقعیت‌های GPS بر اساس دقتشان نرم و ترکیب می‌شوند. «رسیدن» به دستگاه با شعاع ۲۰ تا ۴۰ متر (بسته به دقت GPS) سنجیده می‌شود. وقتی رسیدید، روبات از دستگاه بیرون می‌آید و خرید ۲۰۰ امتیاز اضافه دارد. - موقعیت‌های GPS بر اساس دقتشان نرم و ترکیب می‌شوند. «رسیدن» به دستگاه یعنی رسیدن به محدودهٔ اطرافش، با شعاع ۲٫۵ برابر خطای GPS (بین ۲۰ تا ۵۰ متر). این محدوده با یک دایره روی زمین نشان داده می‌شود. وقتی رسیدید، روبات از دستگاه بیرون می‌آید و خرید ۲۰۰ امتیاز اضافه دارد.
- **حالت پایدار** (دکمهٔ «🎯 پایدار»، پیش‌فرض روشن) جلوی پرش‌های GPS را می‌گیرد:
- پرش‌هایی که با راه رفتن ممکن نیستند نادیده گرفته می‌شوند.
- تا وقتی جابه‌جایی کمتر از حدود نصف خطای GPS باشد، مکان شما ثابت می‌ماند.
- قطب‌نما و چرخش دوربین آرام‌تر دنبال می‌شوند.
- **نمای دوربین (AR):** با روشن شدن حالت دنیای واقعی، دوربین پشت گوشی پس‌زمینهٔ بازی می‌شود. دوربین بازی در جای گوشی (GPS) و هم‌جهت با گوشی (قطب‌نما و حسگر جهت) قرار می‌گیرد، پس دستگاه‌ها، ستون نور، دایرهٔ محدوده، مسیر طلایی و روبات در جهت واقعی روی تصویر دوربین دیده می‌شوند. مدل شهر و ماشین‌ها در این نما پنهان‌اند. دکمهٔ «📷 دوربین» بین نمای دوربین و شهر سه‌بعدیِ اول‌شخص جابه‌جا می‌کند. اگر گوشی حسگر جهت نداشته باشد، با کشیدن انگشت روی صفحه می‌چرخید.
- در سرعت بیش از حدود ۲۲ کیلومتر بر ساعت خرید غیرفعال است. ماشین مجازی هم در این حالت استفاده نمی‌شود. - در سرعت بیش از حدود ۲۲ کیلومتر بر ساعت خرید غیرفعال است. ماشین مجازی هم در این حالت استفاده نمی‌شود.
- بیرون از محدودهٔ بازی، فاصله تا نزدیک‌ترین دستگاه نمایش داده می‌شود. - بیرون از محدودهٔ بازی، فاصله تا نزدیک‌ترین دستگاه نمایش داده می‌شود.
- موقعیت فقط روی خود گوشی پردازش می‌شود. دوربین و قطب‌نما به HTTPS نیاز دارند. - موقعیت فقط روی خود گوشی پردازش می‌شود. دوربین و قطب‌نما به HTTPS نیاز دارند.
@@ -72,9 +100,9 @@
روی کامپیوتر معمولاً موقعیتی پیدا نمی‌شود، چون Chrome برای کامپیوتر از سرویس موقعیت گوگل استفاده می‌کند که از ایران در دسترس نیست. با گوشی تست کنید. روی کامپیوتر معمولاً موقعیتی پیدا نمی‌شود، چون Chrome برای کامپیوتر از سرویس موقعیت گوگل استفاده می‌کند که از ایران در دسترس نیست. با گوشی تست کنید.
**نقطه‌های آزمایشی GPS:** سه دستگاه آزمایشی فقط در حالت دنیای واقعی کنار کارخانه ظاهر می‌شوند، در مختصات `36.450276, 59.423431`، `36.450172, 59.423209` و `36.449976, 59.422866`. مختصات، نقطه‌ای است که باید آنجا بایستید. با گوشی تا هر نقطه راه بروید: روبات بیرون می‌آید و دکمهٔ خرید فعال می‌شود. شعاع رسیدن دو برابر خطای GPS است (بین ۱۲ تا ۴۰ متر) و همیشه نزدیک‌ترین دستگاه انتخاب می‌شود. **نقطه‌های آزمایشی GPS:** سه دستگاه آزمایشی فقط در حالت دنیای واقعی کنار کارخانه ظاهر می‌شوند، در مختصات `36.450276, 59.423431`، `36.450172, 59.423209` و `36.449976, 59.422866`. مختصات، نقطه‌ای است که باید آنجا بایستید. با گوشی تا هر نقطه راه بروید: روبات بیرون می‌آید و دکمهٔ خرید فعال می‌شود. شعاع رسیدن ۲٫۵ برابر خطای GPS است (بین ۲۰ تا ۵۰ متر) و همیشه نزدیک‌ترین دستگاه انتخاب می‌شود.
فاز بعد، حالت دوربین (AR)، روی همین پایه ساخته می‌شود: تصویر زندهٔ دوربین به‌عنوان پس‌زمینه، و دستگاه و روبات در جهت و فاصلهٔ واقعی روی آن. دوربین، GPS و حسگرها فقط روی HTTPS (یا localhost) کار می‌کنند.
## اجرا ## اجرا
@@ -82,7 +110,7 @@
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` یا دکمهٔ «گرافیک» سایه‌ها را خاموش می‌کند (پیش‌فرض گوشی‌ها). - `?quality=low` یا دکمهٔ «گرافیک» سایه‌ها را خاموش می‌کند (پیش‌فرض گوشی‌ها).
@@ -118,29 +146,14 @@ docker compose logs -f
## CI/CD روی Gitea ## CI/CD روی Gitea
مخزن اصلی `ssh://git@git.my-rm.com:2222/Hamed/vending-machine-game.git` با نام `origin` است. GitHub با نام `github` باقی مانده. گردش‌کار [.gitea/workflows/ci-cd.yml](.gitea/workflows/ci-cd.yml) با هر push خودش اجرا می‌شود: گردش‌کار `.gitea/workflows/ci-cd.yml` تست و build را اجرا می‌کند. برای main تصویر Docker در Registry ساخته و منتشر می‌شود؛ سپس runner از Docker socket مشترک سرور و فایل `deploy/docker-compose.yml` برای pull و up استفاده می‌کند. انتشار فعلی SSH نیست.
- **هر push و pull request:** نصب، تست‌ها و build - Secrets لازم: `REGISTRY_USERNAME` و `REGISTRY_TOKEN` با دسترسی read/write به package.
- **push به `main`:** بعد از قبولی تست‌ها، کد از راه SSH به سرور فرستاده می‌شود و `docker compose up -d --build` اجرا می‌شود. - 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).
1. در سرور، Docker و Docker Compose نصب باشد و کاربر SSH بتواند `docker` اجرا کند (عضو گروه `docker` یا root).
2. یک کلید SSH مخصوص انتشار بسازید و کلید عمومی را به `~/.ssh/authorized_keys` سرور اضافه کنید:
```sh
ssh-keygen -t ed25519 -N '' -f deploy_key -C gitea-deploy
ssh-copy-id -i deploy_key.pub user@server
```
3. در Gitea، بخش Settings → Actions → Secrets مخزن، این‌ها را تعریف کنید:
- `DEPLOY_HOST`: آدرس سرور
- `DEPLOY_SSH_KEY`: محتوای فایل `deploy_key`
- در صورت نیاز: `DEPLOY_USER` (پیش‌فرض root)، `DEPLOY_PORT` (پیش‌فرض ۲۲) و `DEPLOY_KNOWN_HOSTS` (خروجی `ssh-keyscan server`)
4. اختیاری، در بخش Variables:
- `DEPLOY_PATH`: پوشهٔ روی سرور
- `SITE_ADDRESS`: دامنه‌ها
- `HEALTH_URL`: مثلاً `https://robotmarkets.ir/`، تا بعد از انتشار بالا آمدن سایت چک شود
- `NPM_REGISTRY`: اگر runner یا سرور به `registry.npmjs.org` دسترسی ندارد (معمول در سرورهای ایران)، آدرس یک mirror در دسترس npm را اینجا بگذارید. همین مقدار برای build روی سرور هم استفاده می‌شود.
تا وقتی `DEPLOY_HOST` تعریف نشده، مرحلهٔ انتشار رد می‌شود و فقط تست و build اجرا می‌شوند. هر مرحله محدودیت زمانی دارد (تست ۱۵ دقیقه، انتشار ۳۰ دقیقه) تا runner هیچ‌وقت بی‌پایان معطل نماند.
## ساخت دوبارهٔ دادهٔ نقشه ## ساخت دوبارهٔ دادهٔ نقشه
@@ -152,26 +165,69 @@ node scripts/build-map-data.mjs --osm city.json zone-factory.json corridor.json
چهار خروجی OSM (محدودهٔ شهر، محدودهٔ کارخانه، کریدور مسیر و خیابان‌های اصلی) با هم ادغام می‌شوند و عنصرهای تکراری حذف می‌شوند. خیابان‌ها هم در مرز منطقه بریده می‌شوند. برای افزودن محدودهٔ جدید، یک خروجی دیگر به `--osm` اضافه کنید و در صورت نیاز `REGION` را در اسکریپت بزرگ‌تر کنید. چهار خروجی 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`: صحنه، دوربین سوم‌شخص، کنترل‌ها، رابط کاربری (HUD)، نقشه، خرید و حلقهٔ بازی - `src/game/createGame.js و src/game/runtime/`: صحنه، دوربین سوم‌شخص، کنترل‌ها، رابط کاربری (HUD)، نقشه، خرید و حلقهٔ بازی
- `src/city.js`: ساخت شهر سه‌بعدی از داده‌ها (خیابان، پیاده‌رو، ساختمان، پارک، درخت، ورودی مترو، کوه‌ها) در تکه‌های ۲ کیلومتری که دورها پنهان می‌شوند - `src/game/world/city.js`: ساخت شهر سه‌بعدی از داده‌ها (خیابان، پیاده‌رو، ساختمان، پارک، درخت، ورودی مترو، کوه‌ها) در تکه‌های ۲ کیلومتری که دورها پنهان می‌شوند
- `src/map-render.js`: نقشهٔ گوشه، نقشهٔ کامل شهر (کاشی‌های کش‌شده و نمای کلی) و نقشهٔ ایران - `src/game/world/mapRender.js`: نقشهٔ گوشه، نقشهٔ کامل شهر (کاشی‌های کش‌شده و نمای کلی) و نقشهٔ ایران
- `src/routing.js`: گراف خیابان‌ها، مسیریابی A* و پیدا کردن نام خیابان - `src/game/world/routing.js`: گراف خیابان‌ها، مسیریابی A* و پیدا کردن نام خیابان
- `src/collision.js`: برخورد با ساختمان‌ها برای شخصیت‌ها، ماشین و دوربین - `src/game/world/collision.js`: برخورد با ساختمان‌ها برای شخصیت‌ها، ماشین و دوربین
- `src/locations.js`: دستگاه‌ها، آدرس‌ها، ساعت کاری و محصولات - `src/constants/locations.js`: دستگاه‌ها، آدرس‌ها، ساعت کاری و محصولات
- `src/game-state.js`، `src/payment.js`، `src/scoring.js`: پول، خرید، پرداخت، امتیاز، درجه و مأموریت‌ها - `src/game/economy/gameState.js`، `src/game/economy/payment.js`، `src/game/economy/scoring.js`: پول، خرید، پرداخت، امتیاز، درجه و مأموریت‌ها
- `src/characters.js`: پدر و فرزند، و روبات راهنما که از دستگاه‌ها بیرون می‌آید - `src/game/models/characters.js`: پدر و فرزند، و روبات راهنما که از دستگاه‌ها بیرون می‌آید
- `src/factory.js`: محوطهٔ کارخانه، شامل سالن تولید، خط مونتاژ، دفتر، حیاط و تابلوها - `src/game/models/factory.js`: محوطهٔ کارخانه، شامل سالن تولید، خط مونتاژ، دفتر، حیاط و تابلوها، به‌همراه دفترها، کامیون و ورود مدیر
- `src/pwa.js` و `public/manifest.webmanifest`: نصب، کار آفلاین و پیام به‌روزرسانی - `src/game/models/staff.js`: آدم‌های کارخانه، یعنی کارمندهای مفصل (هودی، کت‌وشلوار، مانتو و روسری) با برچسب اسم، و ۴۰ کارگر به‌صورت instanced
- `src/realworld.js`: GPS، قطب‌نما، نرم‌سازی موقعیت و شعاع رسیدن - `src/game/models/carModels.js`: ماشین خانواده (سدان اسپرت)، وانت مدیر و کامیون
- `src/vending.js`، `src/vehicle.js`، `src/traffic.js`: مدل دستگاه، فیزیک ماشین و ترافیک شهری - `src/game/device/pwa.js` و `public/manifest.webmanifest`: نصب، کار آفلاین و پیام به‌روزرسانی
- `src/geo.js`: تبدیل طول و عرض جغرافیایی به مختصات بازی - `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`: تبدیل طول و عرض جغرافیایی به مختصات بازی
## محدودیت‌ها و گام‌های بعدی ## محدودیت‌ها و گام‌های بعدی
- مدل‌ها هندسی و سبک‌اند و کیفیت گرافیکی بازی‌های AAA را ندارند. - مدل‌ها هندسی و سبک‌اند و کیفیت گرافیکی بازی‌های AAA را ندارند.
- خیابان‌ها یک‌طرفه و چراغ راهنمایی ندارند، و ترافیک فقط روی بلوارهای اصلی است. - داده و ترافیک جهت یک‌طرفه دارند، اما مسیریابی هنوز دوطرفه است و چراغ راهنمایی شبیه‌سازی نمی‌شود؛ ترافیک روی خیابان‌های اصلی است.
- پیشنهادهای بعدی: امتیاز ایمنی (خط عابر و چراغ قرمز)، تاکسی و قطار شهری، چرخهٔ روز و شب، ذخیرهٔ پیشرفت، جدول امتیاز آنلاین، و محله‌ها و شهرهای دیگر. - پیشنهادهای بعدی: امتیاز ایمنی (خط عابر و چراغ قرمز)، تاکسی و قطار شهری، چرخهٔ روز و شب، ذخیرهٔ پیشرفت، جدول امتیاز آنلاین، و محله‌ها و شهرهای دیگر.
نقشه: © مشارکت‌کنندگان OpenStreetMap (ODbL). مرز استان‌ها: Natural Earth. نقشه: © مشارکت‌کنندگان OpenStreetMap (ODbL). ساختمان‌ها: © Microsoft (ODbL). تصویر ماهواره‌ای: Esri, Maxar, Earthstar Geographics, and the GIS User Community. مرز استان‌ها: Natural Earth.
+2 -2
View File
@@ -1,9 +1,9 @@
# Server deployment: runs the prebuilt image from the Gitea registry behind Nginx Proxy Manager. # 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. # NPM terminates HTTPS and forwards to http://vending-machine-game:80 on the frontend_proxy network.
# IMAGE is set in .env next to this file (CI writes it on every deploy). # IMAGE is the full registry reference, set by the CI deploy job.
services: services:
game: game:
image: ${IMAGE:?set IMAGE in .env} image: ${IMAGE:?set IMAGE}
container_name: vending-machine-game container_name: vending-machine-game
restart: unless-stopped restart: unless-stopped
environment: environment:
+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' },
},
];
+20 -169
View File
@@ -1,173 +1,24 @@
<!doctype html> <!doctype html>
<html lang="fa" dir="rtl"> <html lang="fa" dir="rtl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
<meta name="theme-color" content="#0d1412"> <meta name="theme-color" content="#0d1412" />
<title>عملیات مشهد | روبات مارکت</title> <title>عملیات مشهد | روبات مارکت</title>
<meta name="description" content="از کارخانهٔ روبات مارکت تا بوستان ملت: دستگاه‌های وندینگ را در مشهد پیدا کن و امتیاز بگیر."> <meta
<meta name="mobile-web-app-capable" content="yes"> name="description"
<meta name="apple-mobile-web-app-capable" content="yes"> content="از کارخانهٔ روبات مارکت تا بوستان ملت: دستگاه‌های وندینگ را در مشهد پیدا کن و امتیاز بگیر."
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> />
<meta name="apple-mobile-web-app-title" content="روبات مارکت"> <meta name="mobile-web-app-capable" content="yes" />
<link rel="manifest" href="./manifest.webmanifest"> <meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="icon" type="image/png" href="./icons/favicon-64.png"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png"> <meta name="apple-mobile-web-app-title" content="روبات مارکت" />
</head> <link rel="manifest" href="./manifest.webmanifest" />
<body> <link rel="icon" type="image/png" href="./icons/favicon-64.png" />
<canvas id="world"></canvas> <link rel="apple-touch-icon" href="./icons/apple-touch-icon.png" />
<div id="loading">در حال ساختن شهر مشهد…</div> </head>
<body>
<!-- Mission briefing over the map of Iran --> <div id="root"></div>
<section id="briefing"> <script type="module" src="/src/main.jsx"></script>
<div class="brief-map"><canvas id="iran"></canvas></div> </body>
<div class="brief-panel">
<img class="brief-robot" src="./robot/front.webp" alt="روبات راهنمای روبات مارکت" width="136" height="223">
<small class="tag">عملیات ۰۱ · خراسان رضوی</small>
<h1>عملیات مشهد</h1>
<p class="sub">روبات مارکت · شکار دستگاه‌های وندینگ</p>
<pre id="brief-text" aria-live="polite"></pre>
<button id="start-mission" type="button">شروع عملیات</button>
<div class="brief-actions">
<button id="real-mode" type="button" class="ghost">📍 بازی در دنیای واقعی (GPS)</button>
<button id="install" type="button" class="ghost" hidden>⬇ نصب بازی روی گوشی</button>
</div>
<p class="keys">حرکت: W A S D · دویدن: Shift · دوربین: ماوس · سوار و پیاده شدن: F · خرید: E · نقشه: M</p>
</div>
</section>
<!-- In-game HUD -->
<div id="hud" hidden>
<canvas id="compass" width="560" height="44"></canvas>
<button id="minimap-button" type="button" aria-label="باز کردن نقشهٔ شهر (M)">
<canvas id="minimap" width="220" height="220"></canvas>
<span class="map-hint">نقشه · M</span>
</button>
<div id="location"><span id="street">مشهد</span><small id="mode-label">پیاده</small><small id="gps-status" hidden></small></div>
<aside id="objectives">
<h2>مأموریت‌ها</h2>
<ol id="mission-list"></ol>
</aside>
<div id="waypoint" hidden><i></i><b id="waypoint-name"></b><span id="waypoint-distance"></span></div>
<div id="prompt" hidden></div>
<div id="subtitle" hidden><img id="subtitle-face" alt="" hidden><b id="subtitle-speaker"></b><span id="subtitle-text"></span></div>
<div id="awards" aria-live="polite"></div>
<div id="banner" hidden></div>
<div id="status">
<div class="stat"><small>بودجه</small><strong id="balance">۵۰۰</strong><span>هتومان</span></div>
<div class="stat"><small>ساعت</small><strong id="clock">۰۸:۳۰</strong></div>
<div class="stat" id="speed-stat" hidden><small>سرعت</small><strong id="speed">۰</strong><span>km/h</span></div>
</div>
<div id="rank"><b id="rank-name">تازه‌وارد</b><span id="xp">۰ XP</span><div class="bar"><i id="xp-bar"></i></div></div>
<div id="toolbar">
<button id="gps-toggle" type="button">📍 دنیای واقعی</button>
<button id="sound" type="button" aria-pressed="false">صدا: خاموش</button>
<button id="quality" type="button">گرافیک</button>
<button id="restart" type="button">شروع دوباره</button>
</div>
<div id="touch" hidden>
<div id="stick"><i></i></div>
<div class="touch-buttons">
<button type="button" data-key="f">F</button>
<button type="button" data-key="e">E</button>
<button type="button" data-key="shift">دویدن</button>
</div>
</div>
<div id="crosshair"></div>
<small id="attribution">نقشه: © مشارکت‌کنندگان OpenStreetMap</small>
</div>
<!-- Payment at the card reader -->
<section id="payment" hidden>
<small>کارت‌خوان روبات مارکت</small>
<h2 id="payment-title"></h2>
<p id="payment-step"></p>
<div class="actions">
<button id="swipe" type="button" disabled>کشیدن کارت · C</button>
<button id="cancel-payment" type="button" class="ghost">انصراف</button>
</div>
</section>
<dialog id="map-dialog">
<header>
<div class="tabs" role="tablist">
<button type="button" role="tab" data-tab="city" aria-selected="true">مشهد</button>
<button type="button" role="tab" data-tab="iran" aria-selected="false">ایران</button>
</div>
<button type="button" class="close" id="map-close" aria-label="بستن">✕</button>
</header>
<div class="map-body">
<div class="map-view">
<canvas id="fullmap"></canvas>
<canvas id="iran-map" hidden></canvas>
<p class="map-help">کشیدن: جابه‌جایی · چرخ ماوس: زوم · کلیک روی دستگاه: انتخاب</p>
</div>
<aside class="map-side">
<h3>دستگاه‌های روبات مارکت</h3>
<div id="machine-detail" hidden>
<h4 id="detail-name"></h4>
<p id="detail-address"></p>
<p id="detail-meta"></p>
<p id="detail-products"></p>
<button id="route-button" type="button">مسیریابی تا اینجا</button>
</div>
<ul id="machine-list"></ul>
</aside>
</div>
</dialog>
<dialog id="shop">
<header>
<div>
<small id="shop-category"></small>
<h2 id="shop-name"></h2>
<p id="shop-address"></p>
</div>
<button type="button" class="close" id="shop-close" aria-label="بستن">✕</button>
</header>
<p id="shop-note"></p>
<div id="products"></div>
<p class="rate">۱۰ هتومان = ۱۰۰٬۰۰۰ ریال · مبالغ نمایشی‌اند و پرداخت واقعی انجام نمی‌شود.</p>
</dialog>
<dialog id="real-intro">
<img src="./robot/serious.webp" alt="" width="72" height="68">
<h2>بازی در دنیای واقعی</h2>
<p>با روشن کردن این حالت، موقعیت گوشی شما (GPS) روی نقشهٔ بازی نمایش داده می‌شود. وقتی واقعاً به محل یک دستگاه روبات مارکت برسید، می‌توانید خرید کنید و ۲۰۰ امتیاز اضافه بگیرید.</p>
<ul>
<li>هنگام راه رفتن به خیابان و ماشین‌ها نگاه کنید، نه به صفحه.</li>
<li>بچه‌ها فقط همراه یک بزرگ‌تر بازی کنند.</li>
<li>هنگام رانندگی از این حالت استفاده نکنید؛ در سرعت بالا خرید غیرفعال می‌شود.</li>
<li>موقعیت شما فقط روی همین گوشی می‌ماند و جایی ارسال نمی‌شود.</li>
</ul>
<div class="actions">
<button type="button" id="real-go">قبول دارم، شروع کن</button>
<button type="button" id="real-cancel" class="ghost">انصراف</button>
</div>
</dialog>
<dialog id="ios-install">
<h2>نصب روی آیفون</h2>
<ol>
<li>در Safari دکمهٔ <b>Share</b> (مربع با فلش رو به بالا) را بزنید.</li>
<li>گزینهٔ <b>Add to Home Screen</b> را انتخاب کنید.</li>
<li>روبات مارکت مثل یک اپ روی صفحهٔ اصلی گوشی شما قرار می‌گیرد و آفلاین هم اجرا می‌شود.</li>
</ol>
<button type="button" id="ios-close">متوجه شدم</button>
</dialog>
<div id="update-toast" hidden>نسخهٔ جدید بازی آماده است <button type="button" id="update-reload">به‌روزرسانی</button></div>
<dialog id="end">
<small id="end-tag">پایان عملیات</small>
<h2 id="end-title"></h2>
<p id="end-text"></p>
<ul id="end-stats"></ul>
<div class="actions">
<button id="again" type="button">عملیات دوباره</button>
<button id="continue" type="button" class="ghost">ادامهٔ گشت در شهر</button>
</div>
</dialog>
<script type="module" src="/src/main.js"></script>
</body>
</html> </html>
+4247 -4
View File
File diff suppressed because it is too large Load Diff
+30 -2
View File
@@ -5,7 +5,15 @@
"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": "",
@@ -13,9 +21,29 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/vazirmatn": "^5.2.5", "@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: 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 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: 13 KiB

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