Карта Race: гонка на ногах по киберпанк-антиутопии, 8 игроков

- maps/race: трасса 633 м из Blender (три развилки: срез по контейнерам, каньон со стенами
  против нижней дороги, труба с лазерами против коридора), запечённый свет, небо, дождь, гул;
  фоновая песня (Ogg Vorbis, ~0,6 МБ) включается на «СТАРТ!»
- Правила (race_rules.gd): R — готов; когда готовы все — отсчёт 6 с, заморозка в слотах,
  ворота строго по порядку, сплиты, место, итоги, личный рекорд (user://race_best.cfg)
- Сеть: Net.submit_race / recv_race / race_event (RACE_READY, START, CP, FINISH, STATE).
  ВНИМАНИЕ: набор RPC изменён — клиент и сервер выкладываются вместе, старый клиент
  с новым сервером не работает (rpc node checksum failed)
- player.gd: input_frozen и place() для режимов карт; project.godot: действие ready (R)
- Портал RACE в хабе, пресет экспорта Race (include_filter для ресурсов, грузимых строкой)
- tools/test: двухклиентный тест гонки на локальном сервере, бот, облёт, проверка пакета
- README: правила гонки, события, проверки, заметки

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1
This commit is contained in:
2026-10-28 17:28:05 +05:00
co-authored by Claude Sonnet 5.5
parent 2a6397da79
commit 9130202a51
31 changed files with 1775 additions and 4 deletions
+47 -2
View File
@@ -42,12 +42,12 @@ godot --path . # можно запустить несколько ок
```
Карты в редакторе запускаются отдельно (F6 на сцене карты): `maps/sandbox/sandbox.tscn`,
`maps/backrooms/backrooms.tscn`, `maps/parkour/parkour.tscn`. В редакторе и при запуске с диска порталы ведут на сцены напрямую (пакеты не качаются).
`maps/backrooms/backrooms.tscn`, `maps/parkour/parkour.tscn`, `maps/race/race.tscn`. В редакторе и при запуске с диска порталы ведут на сцены напрямую (пакеты не качаются).
## Управление
Клавиши привязаны к **физическому расположению** (так работает и на русской раскладке). Действия лежат в
`project.godot → [input]`: `move_forward/back/left/right`, `jump`, `sprint`, `crouch`, `dash`.
`project.godot → [input]`: `move_forward/back/left/right`, `jump`, `sprint`, `crouch`, `dash`, `ready` (R — «готов» в гонке).
| Клавиша | Действие |
|---|---|
@@ -57,6 +57,7 @@ godot --path . # можно запустить несколько ок
| C (держать) | **присед** (скорость 3,5, капсула вдвое ниже, пролезает под низкое) |
| Shift + C на бегу | **подкат** |
| Q | рывок |
| R | «готов» (только на карте Race) |
| ЛКМ (зажать) | зум |
| Esc / клик | отпустить / захватить мышь |
@@ -85,6 +86,7 @@ maps/ у каждой карты своя папка — он
hub/ стартовая комната: hub.tscn, hub.gd (порталы строятся из списка PORTALS)
sandbox/ песочница: sandbox.tscn, forest.gd, birds.gd, levitate.gd, nav_bake.gd,
floor/ sky/ audio/ models/ (здания, башня, обелиск, деревья, Эмпайр-стейт…)
race/ race.tscn, race.gd, race_rules.gd (правила гонки), race_data.gd (генерируется), race.glb, sky.png, audio/music.ogg
parkour/ parkour.tscn, parkour.gd (логика: чекпоинты, лазеры, дождь, звук, окружение), parkour_data.gd (генерируется),
baked.gdshader, parkour.glb (из Blender), sky.png (панорама), audio/music.ogg; см. «Заметки для будущего меня»
backrooms/ backrooms.tscn, backrooms.gd, backrooms_nav.res (запечённый навмеш),
@@ -112,6 +114,7 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (
| Песочница | `maps/sandbox/sandbox.tscn` | `world.pck` | Киберпанк-мир: пол-шахматка, здания, 60 деревьев (MultiMesh), птицы, парящий обелиск, звук `tron_01`. Преследователи: **камень** и **Верити**. Портал «HUB» возвращает в хаб |
| Backrooms | `maps/backrooms/backrooms.tscn` | `backrooms.pck` | Модель «Backrooms VR» (×1,4). Преследователь: **Верити** (масштаб 0,5). Выход «EXIT» в дальнем конце ведёт в хаб. Гул ламп генерируется кодом |
| Parkour | `maps/parkour/parkour.tscn` | `parkour.pck` (7,4 МБ: небо 3,5 + модель 3,4 + музыка 1,1) | Мрачная антиутопия в духе Ghostrunner: дождь, дымный воздух, грязный мокрый бетон, натриевый оранжевый, тревожный красный и больной зелёный свет, надписи-приказы (COMPLY, CURFEW 22:00, RESTRICTED). Подъём по мегабашне, ~700 м, 9 чекпоинтов (светящиеся ворота), без преследователей. Секции: крыши (одинарный/двойной прыжок, подтягивание) → каньон из четырёх стен для бега → башня из 9 уступов (двойной прыжок + подтягивание) → лазерный зал (подкат, прыжок, тайминг, рывок) → пропасть из 6 контейнеров (зазоры 16–24 м) → спираль вокруг светящегося ядра (+24 м) → вершина с порталом в хаб. Срыв ниже последнего чекпоинта на 32 м или касание лазера = «СОРВАЛСЯ!» и возрождение на чекпоинте. Музыка: `maps/parkour/audio/music.ogg` (Ogg Vorbis, моно 32 кГц, ~1 МБ; делается `ffmpeg -i исходник.mp3 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 music.ogg`), играет по кругу |
| Race | `maps/race/race.tscn` | `race.pck` | Гонка на ногах до 8 игроков (кто быстрее до финиша), мрачный город: стартовая площадка → рынок → эстакада → крыши → каньон → ангар. 8 ворот-чекпоинтов, 2 лазера, развилки с обходами. Правила и сеть — раздел «Карта Race» ниже |
**Правка и пересборка Parkour** (всё в `Blender models/parkour_02/`):
1. `layout.py` — единственный источник: плиты, стены, лазеры, чекпоинты и расчёт зазоров под мувсет (замеры ботом: прыжок на спринте 15 м, двойной 25 м, прыжок+рывок 19 м, двойной+рывок 28 м, бег по стене 1,8 с / 23 м, подтягивание на уступ до ~2,6 м; **стенами высоту не набрать** — прыжков от стены без бега только 2 за полёт).
@@ -133,6 +136,41 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (
а в пресет «Web» — `maps/<имя>/*` в `exclude_filter`. Добавь вызов в `tools/export_all.sh`.
4. В `PORTALS` (`maps/hub/hub.gd`) допиши запись `{title, map, scene, pack, color}` — портал появится сам.
## Карта Race (гонка)
Файлы: `maps/race/` — `race.tscn`, `race.gd` (внешний вид, ворота, лазеры, срыв, дождь/гул, **музыка**), `race_rules.gd` (правила, HUD, сеть), `race_data.gd`
(генерируется из `Blender models/race_01/`, не править), `race.glb`, `sky.png`, `baked.gdshader`, `audio/music.ogg`.
Пакет `race.pck` (в нём `include_filter`: sky.png, baked.gdshader, race_data.gd, race_rules.gd, race.gd, audio/*; без него файлы, которые скрипт грузит по строке, не попадают в пакет).
**Правила.** ОЖИДАНИЕ → ОТСЧЁТ → ГОНКА → ИТОГИ → ОЖИДАНИЕ.
- Ожидание: бегаем свободно. **R** (действие `ready`) переключает «готов»; подсказка «R — готов (n/m)». Все готовы (хотя бы один) → отсчёт.
- Отсчёт 6 с (секунда «приготовьтесь», затем 5…1): все телепортируются в слоты (номер = индекс в отсортированном списке id комнаты; больше 8 — рядами позади, шаг 3 м) лицом по трассе и **заморожены** (`player.input_frozen`; обзор мышью работает).
- Гонка: ворота только по порядку (пересечение плоскости ворот по ходу трассы в пределах полуширины+1 м; чужие ворота игнорируются). Последние ворота = финиш: фиксируется время, дальше бежим свободно. Сплит: «ВОРОТА n время ±разница с личным лучшим забегом».
- Место: финишировавшие — по времени, остальные — по числу ворот, затем по расстоянию до следующих ворот. Конец круга: финишировали все присутствующие **или** прошло 45 с после первого финиша; игрок вышел — убирается из расчётов (если остался один — бежит до финиша). Итоги ~8 с, затем ожидание; готовность сброшена, игроки стоят где стояли.
- Зашедший в разгар круга бегает без зачёта, видит «ГОНКА ИДЁТ» (определяется по маякам других игроков) и участвует со следующего круга.
- Срыв: ниже **последних пройденных ворот** на 32 м (до первых ворот — ниже стартового слота) или лазер → «СОРВАЛСЯ!», возрождение на последних воротах лицом по трассе (`Basis(UP, yaw)`; **знак проверен**: `rotation.y = yaw_deg` из `race_data.gd` даёт нужное направление: yaw -90 = на +X). Личный рекорд и сплиты лучшего забега — `user://race_best.cfg` (ConfigFile).
- **Музыка** `audio/music.ogg` (Ogg Vorbis, моно 32 кГц, 0,6 МБ; `ffmpeg -i исходник.mp3 -vn -map_metadata -1 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 music.ogg`) включается в момент «СТАРТ!» с начала, играет до возврата в ожидание и плавно (0,8 с) затихает; дождь и гул остаются под ней. Зашедший в разгар круга слышит её, пока идёт чужой круг.
**Кто что считает (сервер ничего не знает про гонку).** Каждый клиент ведёт свою копию состояния (`race_rules.gd`) и обменивается событиями `Net.RACE_*` через `submit_race(kind, a, b)` / `recv_race(id, kind, a, b)` (+ сигнал `Net.race_event`; сервер просто пересылает остальным в комнате):
| kind | a | b | Кто шлёт |
|---|---|---|---|
| `RACE_READY` | 1/0 | — | игрок при нажатии R |
| `RACE_START` | — | unix-время старта (отправитель: `now + 6`) | только **наименьший id** в комнате, когда видит, что готовы все (поэтому двое «последних готовых» дают один старт; второй START от большего id игнорируется) |
| `RACE_CP` | индекс ворот | время гонки | игрок при прохождении ворот |
| `RACE_FINISH` | — | время финиша | игрок на последних воротах |
| `RACE_STATE` | биты: 0..7 пройдено ворот, 8 готов, 9..10 фаза | время финиша или -1 | «маяк» раз в секунду и при смене фазы; по нему догоняют потерявшие событие и зашедшие позже |
Время: на получении START остаток считается как `b - unix_now`; если он отличается от 6 с больше чем на 0,4 с (часы машин расходятся), считаем 5,9 с от получения. Время гонки у каждого = `(ticks - start_tick)`, финиш `b` пересылается точным числом, поэтому таблицы мест у всех совпадают. «Готов» учитывается только в ожидании; на START все флаги готовности сбрасываются. Ник и цвет в таблице — как у призрака (`Игрок id%1000`, `Color.from_hsv(id*0.618034…)`).
**Проверка по существу** (всё в `tools/test/`):
- `race_game_test.sh [папка]` — локальный сервер + 2 клиента-бота (A, B) на карте race два круга; третий (C) заходит в разгар круга 1, участвует во втором и выходит посреди него. Проверяет: одинаковые таблицы мест на всех, один START на круг, слоты, синхронность старта, заморозку на отсчёте, музыку после старта, возрождение лицом по трассе (смерть и срыв), зрителя. Сверяет `race_game_check.py`; боты — `race_bot.gd` (автозагрузка во временной копии проекта) по маршруту `race_route.json`. Порт 9080 должен быть свободен; рекорды ботов пишутся в папку теста (`--race-best=`), настоящий не трогается.
- `race_tour.tscn` — кадры для проверки HUD/ворот (`--mode=gates|hud`, см. заголовок).
- Пакет: `godot --headless --export-pack Race /tmp/x/race.pck`, затем `strings /tmp/x/race.pck | grep -c` для `sky.png`, `baked.gdshader`, `race_data`, `race_rules.gd`, `music.ogg`, `race.glb` (каждое > 0).
- Пакетная загрузка через портал хаба: `tools/test/race_check.txt` (по образцу `parkour_check.txt`).
**Выкладка:** старый клиент с новым сервером (и наоборот) **не работает вообще** — Godot сверяет контрольную сумму набора RPC узла `/root/Net` («rpc node checksum failed»), проверено; поэтому порядок строго по README («Деплой»). `net/net.gd` изменён (новые RPC `submit_race`/`recv_race`) → после пуша `grid-world` сразу `tools/deploy_server.sh root@192.168.10.110`.
## Преследователи
`chasers/chaser.gd` — один скрипт для всех. Сцены (`rock.tscn`, `verity.tscn`) — это просто модель + звук + `NavigationAgent3D`.
@@ -310,6 +348,13 @@ ssh root@192.168.10.110 'journalctl -u my-world -f' # лог: «+ игрок
- Не мерялось: нагрузка Parkour в браузере (~145 тыс. треугольников + дождь 2200 частиц, небо ~6 МБ в видеопамяти), звук (шум дождя, гул, песня) на слух.
- Расходный ресурс: `mcp__blender__look` очень дорог по токенам (сотни тысяч за сессию) — снимки просматривай из Godot (`parkour_tour`), в Blender смотри только геометрию.
**Race — что выяснено**
- Имя `ready` у переменной в `Node` занято сигналом движка (`Node.ready`): флаги готовности названы `flags_ready`.
- Данные ворот — центр капсулы на полу: ворота ставим на `pos.y - 1.2`; слоты на 1,25 м, при заморозке игрок оседает на ~9 см (это нормально).
- Маршрут бота `route.json` проходит ворота ровно по центру; точки маршрута на воротах бот обязан «брать» только после пересечения плоскости (иначе срезает угол, как делал мой первый бот).
- В `--write-movie` с `canvas_items`+`expand` видимая область шире окна, и подписи у левого края обрезаются; HUD слева отступает на 64 px.
- Кадры гонки: `race_tour.tscn --mode=hud` (R, отсчёт, пробег, итоги). Подробности — раздел «Карта Race».
## Дорожная карта
### Цель: играть с телефона