Files
my-world/README.md
T

409 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# My World
Мультиплеерная игра от первого лица в браузере (Godot 4.7.2, Web). Игроки собираются в стартовой
комнате-хабе, заходят через порталы на разные карты и убегают там от преследователей — камня и Верити.
Паркур-управление: спринт, рывок, подкат, присед, бег по стенам, подтягивание на уступы.
- **Движок:** Godot **4.7.2**, рендерер **GL Compatibility** (нужен для Web), физика **Jolt**
- **Платформа:** Web (WebAssembly), без потоков (`thread_support=false`), без GDExtension (`extensions_support=false`)
- **Язык** комментариев в коде, интерфейса и документации: русский
- **Репозитории** (соседние папки в `/home/aibrash/Projects/My_World/`):
`my-world` — этот проект; `grid-world` — готовый веб-экспорт, который CI выкладывает на сайт;
`Blender models` — исходники моделей; `source-assets` — исходные файлы вне игры (например, видео, из которого сделан звук Верити)
## Состояние: что есть и чего нет
| | Что |
|---|---|
| ✅ | Хаб с порталами; карты **Песочница**, **Backrooms**, **Parkour** и **Race**; выход из карт обратно в хаб |
| ✅ | **Race** — гонка на ногах до 8 игроков (отсчёт, ворота по порядку, места, итоги, рекорд, песня на старте); **Parkour** — подъём по мегабашне с чекпоинтами, лазерами, дождём и песней; обе карты — мрачная киберпанк-антиутопия, свет запечён в Blender (см. «Карты и порталы», «Карта Race», «Заметки для будущего меня») |
| ✅ | Вкладка в браузере занимает **~300 МБ** (замер владельца, окт 2026) |
| ✅ | Карты качаются отдельными пакетами `.pck` при первом входе в портал (экран загрузки с прогрессом) |
| ✅ | Мультиплеер: выделенный сервер, игроки видят друг друга, **комнаты по картам** (в разных картах не пересекаются) |
| ✅ | Преследователи: **камень** и **Верити**; общие для всех игроков комнаты, ходят по навмешу, «видят» только вблизи или в поле зрения, могут потерять игрока |
| ✅ | Смерть при поимке и возрождение, красный экран «ПОЙМАЛ!» |
| ✅ | Паркур: спринт, рывок, подкат, **присед (C)**, бег по стенам, прыжок от стены, двойной прыжок, подтягивание на уступ, зум (ЛКМ) |
| ✅ | Управление **не зависит от раскладки** (русская и любая другая): действия `InputMap` на физические клавиши |
| ✅ | Деплой: CI из `grid-world` на сайт; скрипты экспорта, dev-сервера, тестов без окна, выкладки сервера |
| ❌ | Телефон: **не планируется** (решение владельца, окт 2026). Сенсорного управления нет, мобильных текстур нет |
| ⏳ | **Ники и текстовый чат** — следующая задача (см. «Дорожная карта») |
| ❌ | Настроек (чувствительность мыши, громкость), меню паузы |
| ❌ | Серверных проверок (антиспуфинг позиций, античит): сервер только пересылает |
| ❌ | Сохранения прогресса, аккаунтов |
| ❌ | Автоматических тестов в CI (есть боты для ручного прогона, см. «Тесты без окна») |
| ✅ | Чужой игрок — модель киберниндзя со скелетом (`player/character/character.glb`, 752 треугольника, 17 костей, 14 анимаций); свечение красится в цвет игрока от его id. Своё тело — та же модель, спрятанная от своей камеры |
## Быстрый старт
```bash
# запуск игры локально (стартует в хабе; без сервера — одиночная игра, клиент переподключается каждые 3 с)
godot --path . --windowed --resolution 960x540
# с локальным сервером (в другом терминале)
tools/test/make_test_server.sh /tmp/test-server && godot --headless --path /tmp/test-server
godot --path . # можно запустить несколько окон
```
Карты в редакторе запускаются отдельно (F6 на сцене карты): `maps/sandbox/sandbox.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`, `ready` (R — «готов» в гонке).
| Клавиша | Действие |
|---|---|
| W A S D, стрелки | движение |
| Shift | спринт |
| Пробел | прыжок; держать у уступа в воздухе — подтянуться; у стены — прыжок от стены; в воздухе — двойной прыжок |
| C (держать) | **присед** (скорость 3,5, капсула вдвое ниже, пролезает под низкое) |
| Shift + C на бегу | **подкат** |
| Q | рывок |
| R | «готов» (только на карте Race) |
| ЛКМ (зажать) | зум |
| Esc / клик | отпустить / захватить мышь |
Бег по стене — в воздухе у стены с зажатым «вперёд» на скорости. Ctrl не используется: в браузере `Ctrl+W` закрывает вкладку.
## Структура проекта
```
project.godot, export_presets.cfg, icon.svg
net/ сеть. Net — автолоад: и клиент, и сервер (один скрипт)
net.gd состояние комнат, преследователей, RPC (см. «Сеть»)
ghost.gd чужой игрок: модель, выбор анимации по флагам и скорости, цвет свечения от id, ник
player/character/ character.glb — модель и анимации (исходник и скрипт сборки: `../Blender models/character_01/`)
server.tscn пустая сцена для запуска выделенного сервера
core/
map_loader.gd автолоад MapLoader: скачивает пакет карты и подключает его (см. «Загрузка карт»)
player/
player.tscn, player.gd игрок от первого лица: всё паркур-управление, смерть и возрождение
objects/
portal.gd портал на другую карту (Area3D, рисуется кодом)
chasers/
chaser.gd общий «мозг» преследователя: навмеш, зрение, погоня, блуждание, поимка, синхронизация
rock/ камень: rock.tscn, Rock.glb (+ извлечённая текстура), Guitar.ogg
verity/ Верити: verity.tscn, verity2.glb (+ текстура), verity.ogg (звук из видео)
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 (запечённый навмеш),
model/ (готовая модель «Backrooms VR» + 27 извлечённых текстур + CREDITS.md)
deploy/ боевой конфиг nginx, systemd-юнит, project.godot выделенного сервера
tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (боты)
```
**Правила, которые стоит соблюдать**
- **Папки и файлы — в `snake_case`.** Каждая карта — в `maps/<имя>/`. Всё, что нужно только ей, лежит внутри.
Общее (игрок, преследователи, порталы, сеть) — вне `maps/`: оно входит в основной пакет игры.
- **Файлы рядом с `.glb` не переименовывать и не удалять.** Это текстуры, которые Godot вынул из модели при импорте
(`gltf/embedded_image_handling=Extract`), их имена зависят от имени `.glb`. Переименуешь `.glb` — импорт создаст дубли.
Папки с моделями переносить целиком можно (я так и делал).
- **`.import` и `.uid` коммитятся** и переносятся вместе с файлом (`git mv`). Кэш `.godot/` не коммитится; после переноса
файлов вне редактора его удаляют и делают `godot --headless --import --path .`.
- Пути в сценах — через `res://`; ссылки на карты (`PORTALS` в `hub.gd`, `target_scene` у порталов) — строками.
## Карты и порталы
| Карта | Сцена | Пакет | Что там |
|---|---|---|---|
| Хаб | `maps/hub/hub.tscn` | в основном пакете | Комната 28×18 м, строится кодом (пол-грид, стены, свет). Порталы SANDBOX и BACKROOMS в ряд по центру, влезает 6. Безопасна, преследователей нет |
| Песочница | `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 за полёт).
2. `build_parkour.py` — в Blender (`exec`): строит коллизию (`Collision-colonly` — Godot делает из неё StaticBody), детальную геометрию `Solid`, светящиеся элементы `Neon`; пишет `parkour_data.gd` (чекпоинты, лазеры, старт, портал). Сохраняет `Parkour_02.blend`.
3. `bake.py` — `blender -b Parkour_02.blend -P bake.py`: Cycles запекает прямой+непрямой свет (неон светит сам, плюс холодный/пурпурный/бирюзовый свет) в **цвета вершин** (сжаты в 8 раз — шейдер умножает обратно) и экспортирует `parkour.glb`. ~1 мин.
4. `build_sky.py` — `blender -b -P build_sky.py`: панорама дальнего города 3072×1536 → `sky.png` (в импорте включены мипы — по ним размытые отражения). ~2 мин.
5. `godot --headless --import --path .`.
Палитра светящихся задана именами материалов из Blender (старые названия сохранены): `NeonCyan` — холодный белёсый, `NeonMagenta` — красный, `NeonOrange` — натриевая лампа. Настроение — в `parkour.gd` (туман, дождь `_add_rain`, гул `_add_ambience`, насыщенность и контраст), в `bake.py` (силы света), `build_sky.py` (дымное небо).
В Godot: `baked.gdshader` (unshaded: цвет материала × запечённый свет × «зерно» бетона по мировым координатам + влажный блеск — отражение панорамы на горизонтальных поверхностях); `parkour.gd` подменяет материалы по именам из Blender (`SURFACES`, `GLOWS`), строит ворота-чекпоинты, лазеры (пульсирующие считают фазу по системным часам), окружение и портал.
**Добавить карту:**
1. Создай `maps/<имя>/<имя>.tscn`: корень Node3D, внутри игрок (`player/player.tscn`), портал обратно (`objects/portal.gd`,
`target_map="hub"`, `target_scene="res://maps/hub/hub.tscn"`), при необходимости преследователи.
В `_ready()` вызови `Net.enter_map("<имя>")` (идемпотентно; так сервер знает, в какой ты комнате).
2. Если нужны преследователи — навмеш: либо `NavigationRegion3D` + `maps/sandbox/nav_bake.gd` (запекание при старте, как в песочнице),
либо заранее запечённый `.res` (как у Backrooms: в вебе без потоков запекать на лету долго).
3. В `export_presets.cfg` добавь пресет-копию «World» (`export_filter="resources"`, `export_files=["res://maps/<имя>/<имя>.tscn"]`,
`exclude_filter="player/*, objects/*, chasers/chaser.gd"`, `export_path="../grid-world/<имя>.pck"`),
а в пресет «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`.
- **Поведение:** гонится за ближайшим живым игроком по навмешу, ловит на `catch_distance` (по горизонтали, не выше `catch_height`).
После поимки игрок умирает и возрождается через 2,5 с в стартовой точке; если преследователь рядом со стартом — уходит на свой старт.
- **Зрение** (`sight_limited = true`): замечает игрока, если он ближе `hearing_radius` (слышит даже за стеной) **или** в конусе `sight_fov`
до `sight_range` и луч до него не упирается в стену. Потерял из виду — идёт к последнему месту `lose_time` секунд, потом блуждает (`patrol_speed`, `wander_radius`).
Без `sight_limited` знает, где игрок, из любой точки.
- **Параметры** (экспорты узла): `chaser_id` (имя в сети), `speed`, `turn_speed`, `catch_distance`, `catch_height`, `sight_*`, `hearing_radius`,
`lose_time`, `patrol_speed`, `wander_radius`, `model_scale` (масштаб дочернего узла `Model`), `emission_energy` (чисто светящийся материал; у Верити 1,0).
- **Значения сейчас:** камень в песочнице — скорость 15, зрение 45 м / 130°, слух 8 м, лимит потери 5 с. Верити в песочнице — скорость 12, зрение 55 м / 110°, слух 10 м, потеря 6 с.
Верити в Backrooms — скорость 10, масштаб 0,5, зрение по умолчанию (30 м / 120° / слух 6 м / потеря 4 с).
- **Модели — плоские «карточки»** вдоль оси X; в сцене повёрнуты на 90° и подняты, чтобы стоять на полу. У `verity2` лицо нарисовано с одной стороны
(свечением), при погоне оно повёрнуто к игроку.
- **Добавить преследователя:** новая сцена по образцу `verity.tscn` (корень со скриптом `chaser.gd`, `chaser_id="имя"`, `NavigationAgent3D`),
инстанс в карту. Больше ничего: сеть синхронизирует всех по имени.
## Сеть
**Архитектура:** выделенный сервер на Godot (WebSocket, порт **9080**), клиенты — `WebSocketMultiplayerPeer`. Сервер **не знает про уровни**
и ничего не считает, только пересылает сообщения внутри «комнаты» (карты). Физика, управление, **преследователи** считаются на клиентах.
- `net/net.gd` — автолоад `Net`. `DisplayServer.get_name() == "headless"` → **сервер**, иначе клиент; флаг `--client` делает клиентом и headless (боты).
Клиент переподключается каждые 3 с. Адрес в вебе берётся из `window.location` (`wss://<домен>/ws` или `ws://`), на десктопе — `ws://127.0.0.1:9080` или `-- --server-url=...`.
- **Комнаты по картам.** Клиент при входе на карту шлёт `submit_map(имя)`, сервер отвечает `recv_map_ack`. Пока подтверждения нет (`Net.map_ready == false`),
клиент игнорирует чужие сообщения — так при смене карты не просачиваются старые пакеты. Игроки разных карт друг друга не видят.
- **Владелец преследователей.** В каждой комнате один клиент (первый вошедший, затем наименьший `peer_id` из оставшихся) симулирует **всех** преследователей и
шлёт их позиции 20 раз/с; остальные клиенты плавно повторяют. Если владелец молчит `ROCK_TIMEOUT` = 3 с (вкладка в фоне, лаги) или вышел — сервер передаёт роль другому;
преследователь продолжает с того же места. Поимку «своего» игрока всегда решает его собственный клиент по тому, что он видит.
- **Смерть и возрождение** шлются отдельно (`submit_life`), чтобы владелец не гнался за мёртвыми и мог увести преследователя от точки возрождения.
| RPC | Кто → кому | Что |
|---|---|---|
| `submit_map(map)` / `recv_map_ack(map)` | клиент → сервер / обратно | вход в комнату |
| `recv_rock_owner(id)` | сервер → клиенты | кто симулирует преследователей |
| `submit_state(pos, yaw, pitch, flags)` / `recv_state(id, …)` | клиент → сервер → остальные | состояние игрока, 20 Гц, `unreliable_ordered` |
| `submit_chasers(ids, positions, yaws)` / `recv_chasers(…)` | владелец → сервер → остальные | позиции всех преследователей, 20 Гц |
| `submit_life(dead, pos)` / `recv_life(id, dead, pos)` | клиент → сервер → остальные | смерть/возрождение, `reliable` |
| `recv_left(id)` | сервер → комната | игрок вышел |
Флаги состояния (`Net.FLAG_*`): подкат, бег по стене (+ сторона стены), рывок, подтягивание, «в воздухе». Присед передаётся как подкат; призрак отличает их по скорости. Скорость и двойной прыжок призрак считает сам по смещению позиции — в протокол они не входят.
**RPC требуют одинаковые путь узла и скрипт на сервере и клиенте**, поэтому `net.gd` один. Любое изменение RPC — **клиент и сервер выкладываются вместе**
(старый клиент с новым сервером не работает). Имена вроде `rock_owner`/`recv_rock_owner` остались от времени, когда преследователь был один.
## Загрузка карт пакетами (веб)
Основной пакет игры (`index.pck`) — только хаб, игрок, сеть и порталы (около 70 КБ). Движок — `index.wasm` (39,5 МБ, ~10 МБ с gzip).
Карты — отдельные `.pck`: `world.pck` (13,6 МБ), `backrooms.pck` (21,3 МБ). Портал вызывает `MapLoader.enter(map, scene, pack, title)`:
1. если сцена уже есть в проекте (редактор, десктоп, пакет уже скачан) — переход сразу;
2. иначе игра встаёт на паузу (преследователь не должен убить игрока на экране загрузки), показывается «Загрузка: <имя> N / M МБ»;
3. пакет качается `HTTPRequest` с `Cache-Control: no-cache` (браузер сверяет файл с сервером), записывается в `user://` и подключается через
`ProjectSettings.load_resource_pack(path, false)` (**`false`** — файлы основной игры в приоритете, устаревший пакет не затрёт код);
4. при ошибке на экране и в консоли пишется причина, портал можно повторить.
**Грабли, на которые уже наступили:**
- `HTTPRequest` в браузере: **`accept_gzip = false`** обязательно. Браузер сам распаковывает gzip, а движок видит `Content-Encoding: gzip` и пытается распаковать ещё раз (ошибка 8).
- **`download_file` в вебе не создаёт файл** — тело берём в память и пишем в `user://` сами.
- Файлы-«не ресурсы» (`.mp4`, `.md`) не экспортируются; потому исходное видео Верити лежит вне проекта (`source-assets`).
- Предупреждения `invalid UID … using text path instead` при загрузке пакета безвредны.
- **Ресурсы, которые скрипт грузит строкой** (`load("res://…")`), в пакет карты **не попадают**: пресет `export_filter=resources` берёт только сцену и её прямые зависимости (`preload` тоже находится). Добавляй такие файлы в `include_filter` пресета карты (у Parkour: `sky.png, baked.gdshader, parkour_data.gd, audio/*`). Признак беды: пакет «слишком маленький». Проверка: `strings parkour.pck | grep -c sky.png` (должно быть > 0). Раз это случилось: карта тихо грузится без неба, шейдера и логики, а проверка «сцена называется Parkour» этого не замечает — **проверяй по существу** (см. «Тесты»).
- **Кэш зависимостей редактора** у недавно созданных/изменённых сцен может устареть, и экспорт пакета карты пропустит модели и звуки (пакет «слишком маленький»).
`tools/export_all.sh` делает `touch` сцен перед экспортом. Признак беды: размер пакета не вырос после добавления ресурсов.
## Экспорт, тесты и деплой
### Экспорт
```bash
tools/export_all.sh # веб-клиент + backrooms.pck + world.pck в ../grid-world
```
Три пресета в `export_presets.cfg`: **Web** (хаб + общее; из него исключены `maps/sandbox`, `maps/backrooms`, `chasers/rock`, `chasers/verity`, `tools`),
**World** и **Backrooms** (`export_filter=resources` — сцена карты и всё, на что она ссылается, кроме общего из основного пакета). Шаблоны экспорта —
`~/.local/share/godot/export_templates/4.7.2.stable/` (есть только Web). Экспорт при выходе печатает безвредную ошибку `Parameter "singleton" is null`.
### Проверка сборки как на сервере
```bash
python3 tools/serve_web.py ../grid-world 8080 # nginx-подобно: COOP/COEP и gzip чанками
```
Открыть `http://127.0.0.1:8080`. На десктопном `godot --main-pack` пакеты карт качаются плоским `python3 -m http.server` (без gzip) через `-- --pack-url=http://127.0.0.1:8777/`.
### Тесты без окна (боты)
Автотестов в CI нет; для проверок есть боты. `tools/test/make_test_project.sh /tmp/tp` делает временную копию проекта с автозагрузкой `tools/test/bot.gd`
(телепортирует игрока по расписанию и печатает состояние раз в секунду), `tools/test/make_test_server.sh /tmp/ts` — локальный сервер в боевой раскладке.
```bash
godot --headless --path /tmp/ts & # сервер
godot --headless --path /tmp/tp res://maps/sandbox/sandbox.tscn -- --client --name=A &
godot --headless --path /tmp/tp res://maps/sandbox/sandbox.tscn -- --client --name=B
# цепочка карт на реальных пакетах (портал SANDBOX в хабе на (-2.25, -7.5), BACKROOMS на (2.25, -7.5)):
/tmp/tp/tools/export_all.sh /tmp/site && (cd /tmp/site && python3 -m http.server 8777 --bind 127.0.0.1 &)
godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url=http://127.0.0.1:8777/ --tp=2:-2.25,1,-7.5
```
Для картинок: `godot --path . --resolution 1152x648 --fixed-fps 10 --write-movie /tmp/shots/a.png --quit-after 25 res://maps/hub/hub.tscn` (окно мелькнёт; кадры — `a00000020.png`).
В браузере: Chrome (flatpak `com.google.Chrome`) `--headless=new --enable-logging=stderr --v=0 http://127.0.0.1:8080` — вывод Godot виден в строках `CONSOLE`.
**Готовые проверки** (`tools/test/`, запускаются как сцены: в режиме `--script` автозагрузки недоступны при компиляции, поэтому только `res://…tscn`; `-- --client`, иначе headless поднимет сервер на порту 9080):
- `measure_movement.tscn` — замеры мувсета (прыжки, рывок, стена); `mantle_test.tscn` — подтягивание на 40 вариантах уступов; `parkour_gaps.tscn` — проходимость контейнеров; `parkour_tour.tscn` — облёт карты со снимками (см. заголовок файла).
- **Проверяй по существу, а не по имени сцены.** Для карт из пакетов: собери тестовый проект (`make_test_project.sh`), в его `bot.gd` добавь печать фактов (есть ли у `current_scene` скрипт, подменён ли материал, есть ли `PanoramaSkyMaterial`, играет ли музыка, сколько лазеров) и пройди портал с `--pack-url`. Готовый код проверки — `tools/test/parkour_check.txt` (ловит пропавшие `sky.png`/шейдер/музыку/логику).
- Ввод ботом: `Input.action_press("jump")` работает для зажатия и `is_action_pressed`; рывок и подкат ловятся в `_unhandled_input`, поэтому для них нужно `Input.parse_input_event(InputEventAction)` с `pressed=true`.
- Грабли оболочки: в этом окружении `ls` — алиас на `eza` (используй `command ls`), `cp` спрашивает подтверждение (`command cp -f`), zsh не режет `$args` по пробелам (передавай аргументы отдельно), `echo ======` ломает команду.
**Осторожно:** `pkill -f`/`pgrep -f` с шаблоном, который есть и в твоей же командной строке, убивает собственный шелл; не используй `pkill chrome` — закроет браузер владельца. Останавливай по PID или по точному признаку.
### Деплой
```
Браузер ──https/wss──▶ Nginx Proxy Manager (SSL, домен на duckdns) ──http──▶ контейнер 192.168.10.110
nginx :80
├─ / → /opt/grid-world (статика игры)
└─ /ws → 127.0.0.1:9080 (игровой сервер)
```
**Сервер:** `root@192.168.10.110`, Debian 12, контейнер LXC в Proxmox, 1 ядро, 1 ГБ RAM, ssh по ключам. Нет `curl`, `rsync`, `ufw`; есть `python3`.
В Nginx Proxy Manager включено **Websockets Support** (без него `/ws` не работает). Боевой конфиг nginx: `deploy/nginx.conf`
(`/etc/nginx/sites-enabled/godot_game`); обязателен **gzip для `.wasm`/`.pck`** (иначе 39,5 + 21 МБ уходят как есть) и заголовки COOP/COEP.
**Клиент:** `tools/export_all.sh`, затем коммит и пуш в `grid-world` (`main`). Gitea (`git.ibrash.duckdns.org`, `aibrash/grid-world`) по пушу запускает
`.gitea/workflows/deploy.yml`: ssh на сервер и `git pull` в `/opt/grid-world`. Пакеты карт выкладываются вместе с `index.*`.
**Сервер:** отдельный минимальный проект в `/opt/my-world-server/` (`project.godot` + `net/` + официальный бинарник Godot 4.7.2 linux x86_64 `godot`; бинарник из Fedora на Debian не подходит,
берётся из `github.com/godotengine/godot-builds`). Служба systemd `my-world.service` (`deploy/my-world.service`, DynamicUser, `Restart=always`).
```bash
tools/deploy_server.sh root@192.168.10.110 # бэкап, копирование net/ и project.godot, пересборка кэша классов, перезапуск
ssh root@192.168.10.110 'journalctl -u my-world -f' # лог: «+ игрок …», «[карта] камень считает игрок …»
```
Трогать сервер нужно только при изменении `net/*`. **Порядок при изменении RPC:** сначала пуш `grid-world`, сразу следом `deploy_server.sh`; игрокам — Ctrl+F5.
Откат сервера: бэкап в `/opt/backup-my-world-server-<дата>/`.
> Сейчас на сервере ещё **старая раскладка** (`/opt/my-world-server/Net/`, заглавная). `deploy_server.sh` при следующем запуске переведёт её на `net/`.
> Код сети при этом по протоколу не изменился.
### Git
- Репо проекта: `git@git.ibrash.duckdns.org:aibrash/my-world.git`, основная ветка `master`; коммиты на русском с трейлером `Co-Authored-By`.
- **Пушить и выкладывать на сайт — только по прямой просьбе владельца.** Коммитить локально можно.
## Что выяснено по ходу работы
- **Окружение:** в рендерере Compatibility **объёмный туман не работает** (удалён из песочницы). SSAO и glow работают (неоновое свечение), оставлены.
В Backrooms включён обычный туман (`Environment.fog_*`), он работает.
- **Память:** раньше вкладка показывала около 1,6 ГБ; в окт 2026 владелец замерил **~300 МБ** (после замены капсул на модель со скелетом, новых карт и всех правок этой сессии). Что именно помогло — неизвестно (гипотезы прежних замеров: несжатые текстуры, HDR-небо, `.ogg` целиком в памяти). Проблему считаем снятой; повторно измерять при добавлении тяжёлого контента (небо Parkour/Race ~6 МБ в видеопамяти каждое).
Гипотезы: рост WASM-памяти, несжатые текстуры, HDR-небо 2k, `.ogg`, распаковываемый целиком. Измерения с тех пор не повторялись.
- **Материал Верити** — две поверхности: жёлтая основа и диск с лицом (текстура свечения на белой основе). Белая основа на ярком свету даёт бледность,
поэтому `emission_energy` делает материал чисто светящимся (чёрная основа).
- **Backrooms:** двери ~1,8 м, поэтому преследователь там масштабом 0,5 и навмеш запечён для агента радиусом 0,6 (иначе проход закрыт). Самый дальний зал за узкой дверью недостижим.
- **Ввод:** `Input.is_key_pressed(KEY_W)` проверяет символ, на русской раскладке это «Ц». Только действия `InputMap` на физические клавиши.
- **Пресеты:** при отключённом `extensions_support` — один `index.wasm`; включение даёт ещё `index.side.wasm` 44 МБ.
## Заметки для будущего меня
**Договорённости с владельцем (важно не нарушать)**
- Коммитить локально можно, **пушить и выкладывать — только по прямой просьбе**. Сервер выкладывать нужно лишь при правках `net/*` с изменением RPC.
- Parkour — **мрачная антиутопия** (дождь, грязный смог, мокрый бетон, натриевый оранжевый, красный, больной зелёный; надписи-приказы COMPLY/CURFEW). Яркий розово-бирюзовый неон владелец назвал «детским». Туман 0,011 выбран из серии проб (0,02 владелец откатил как слишком густой), цвет тумана светлее фона — тёмный туман лишь затемняет.
- Сложность Parkour владельцу «нормально»; он просил и «не детский уровень» — если захочет жёстче: зазоры 27–28 м (двойной прыжок + рывок), лазеры в каньоне, быстрее пульсы, реже чекпоинты.
- Сальто камеры при двойном прыжке делалось и **убрано по просьбе** (тошнит, не видно куда прыгать). Без просьбы не возвращать.
- Чекпоинты должны быть **заметными** (раньше тонкие кольца на полу не нашли): сейчас ворота 5 м с надписью CHECKPOINT.
- Мой стиль ответов: по-русски, коротко, без перечисления всего подряд; честно говорить, что проверено, а что нет.
**Мувсет (замеры ботом, спринт 13 м/с)** — нужны при любом проектировании карт:
одиночный прыжок 15 м и +1,9 м вверх; двойной 25 м и +3,5 м; прыжок+рывок 19 м; двойной+рывок 28 м; бег по стене 1,8 с (23 м), провал 1,4 м; прыжок от стены ~5 м в сторону. **Стенами высоту не набрать**: прыжков от стены без бега только `MAX_WALL_JUMPS = 2` за полёт, а прыжок с бега по стене почти ничего не даёт — высоту дают двойной прыжок и подтягивание (уступ до ~2,6 м над полом, с двойным прыжком до ~4,5). Шахта-«лифт» из стен невозможна; в Parkour вместо неё башня из уступов.
**Подтягивание** (`player.gd → _try_mantle`): цель — центр капсулы на `default_capsule_height * 0.5 + 0.05` над поверхностью уступа. Раньше стояло `+ 0.05` (центр на уровне поверхности): тело оказывалось наполовину в уступе, физика выталкивала назад, и на уступах от 2 м не работало (на всех картах!). Проверка — `mantle_test.tscn`.
**Конвейер Parkour** (`Blender models/parkour_02/`): `layout.py` (единственный источник раскладки) → `build_parkour.py` (в Blender: геометрия `Solid`/`Neon`/`Collision-colonly`, пишет `parkour_data.gd`) → `bake.py` (`blender -b Parkour_02.blend -P bake.py`: запекание Cycles в цвета вершин + экспорт GLB, ~1 мин) → `build_sky.py` (`blender -b -P build_sky.py`: панорама ~2 мин) → `godot --headless --import --path .`. Правка раскладки требует всех четырёх шагов; по ходу собирай и смотри картинки (снимки `parkour_tour`).
- **Освещение — в цветах вершин**, а не в динамических источниках. Сетка граней режется на ячейки ≤ 1,6 м (`kit.Mesh.grid`), поэтому число треугольников растёт быстро: сейчас Solid ~50 тыс. четырёхугольников, Neon ~22 тыс. Фон и дно — грубая сетка (9–80 м). Не раздувай: веб.
- glTF хранит цвета вершин нормализованными (до 1), поэтому в `bake.py` значения сжаты ×0.125, а шейдер `baked.gdshader` умножает на `light_scale = 8`. Освещённость без этого обрезается на единице.
- Яркость уровня: сила света в `bake.py` (Moon/CityGlow/Smog и цвет мира) × `exposure` в `parkour.gd` (2,1) × цвет материала (`kit.SOLID` и `SURFACES` в `parkour.gd` — **должны совпадать**). Первые попытки были чёрными: средняя запечённая освещённость ~0,05 × альбедо 0,11 → почти ноль.
- Тексты (`kit.Mesh.text`): без поворота смотрит в −Y (читается, если смотреть вдоль +Y); для видимой с +X грани нужен `yaw = +π/2`, с −X — `−π/2`, для видимой со стороны +Y — `π`. Ошибка с ориентацией даёт зеркальные надписи. Кривые букв огрублены (`resolution_u = 2`) — иначе десятки тысяч лишних треугольников.
- Имена в русском интерфейсе Blender переведены: узлы шейдеров/мира искать **по `type`**, не по имени (`'Background'` не найдётся).
- Экспорт цветов вершин в GLB требует активного атрибута (`export_vertex_color='ACTIVE'`); Cycles-запекание в `target='VERTEX_COLORS'`.
- Панорама неба: мипы включены в `sky.png.import` (`mipmaps/generate=true`, `compress/mode=2`) — по ним размытые отражения в мокрых полах (`textureLod` в шейдере). Дно города в панораме — отдельный материал: общий шейдер дымки на огромной плоскости давал белое.
- Не мерялось: нагрузка 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».
## Дорожная карта
### Телефон — не планируется
Владелец решил, что телефон не нужен (окт 2026). Если когда-нибудь вернёмся: нужны сенсорный стик и зона обзора (ввод уже сведён к действиям `InputMap`, достаточно `Input.action_press`), ориентация и полноэкранный режим по касанию,
экран «Нажми, чтобы начать» (аудио в браузере без жеста не стартует), мобильные текстуры ETC2/ASTC (`vram_texture_compression/for_mobile`; удвоит пакеты — делать отдельными `*.mobile.pck`), отключение SSAO/glow и `scaling_3d_scale` 0,6–0,75,
восстановление после ухода вкладки в фон (`submit_map` заново). Сначала измерить память и FPS на реальном устройстве.
### Ближайшее
- **Ники и текстовый чат** (выбор владельца, окт 2026): ник игрока вместо «Игрок N» над призраком, в итогах гонки и в HUD; строка чата (физическая клавиша, работает на любой раскладке) в комнате карты. Потребует новых RPC → выкладка клиента и сервера вместе, Ctrl+F5 игрокам.
### Идеи (оценка независимого консультанта, окт 2026; не обязательства)
- **Parkour/Race:** режим «время и призрак» (запись лучшего забега, показ призраком), старт гонки по таймеру при зависшем «готов» (сейчас один не нажавший R блокирует всех), развилки Race с большим выигрышем, спектейт для мёртвых, приватные комнаты по ссылке.
- **Мувсет и ощущение:** анимация приземления, звуки шагов/приземления/подката/бега по стене/рывка (самый заметный эффект при малых затратах), настройки и меню паузы (чувствительность, громкость, FOV).
- **Контент и социальное:** игрок в роли преследователя, голос по близости, подбираемые предметы/двери, гроза со вспышкой молнии на картах, кастомизация персонажа.
- **Инструменты:** номер версии протокола (чтобы старый клиент получал «обнови страницу»), автопроверка пакетов в CI, одна команда «собрать карту», экран credits (CC-BY Backrooms VR).
- Серверные проверки (антиспуфинг позиций), если игроков станет много.
## Персонаж
Модель и анимации **генерирует скрипт** `Blender models/character_01/build_character.py` (Blender 5.x: `exec` в окне или `blender -b -P`).
Он строит меш из гранёных сегментов (жёсткие веса: одна кость на сегмент), арматуру из 17 костей, считает позы и пишет 14 анимаций
(`idle walk run crouch_idle crouch_walk slide jump fall air_jump dash wallrun_R wallrun_L mantle death`), ноги ставятся через IK.
Экспорт: GLB, `export_animation_mode=NLA_TRACKS`, в `player/character/character.glb`. Материалы: `Suit` (тёмный) и `Glow` (свечение; в игре заменяется цветом игрока).
- **Выбор анимации** — `Ghost._choose()`: смерть > подтягивание > рывок > бег по стене > воздух > присед/подкат > бег/ходьба/стоит.
Скорость шага подгоняется `speed_scale` (`REF_SPEED`), между анимациями смешивание 0,15 с. Голова повторяет взгляд вверх-вниз.
- **Своё тело:** та же модель стоит в `player/player.tscn` (видна в редакторе, отбрасывает тень), но спрятана от своей камеры слоем 2 (`player.gd → _ready`). Не анимируется.
- **Выкладка сервера:** `ghost.gd` лежит и на сервере, где модели нет, поэтому модель грузится `load()` внутри `setup()`, а не `preload`.
- Изменить позы: правь функции анимаций в скрипте, перезапусти его в Blender, заново экспортируй GLB и импортируй в Godot.
## Журнал изменений (окт 2026)
Сделано одной серией работ (хронологически):
1. **Персонаж:** модель киберниндзя со скелетом (752 треугольника, 14 анимаций, IK ног) вместо капсул; цвет свечения от id игрока; свой персонаж спрятан от своей камеры. Сальто камеры при двойном прыжке делалось и убрано по просьбе.
2. **Подтягивание на уступы** починено (не работало от ~2 м на всех картах), замеры мувсета и тестовые сцены в `tools/test/`.
3. **Parkour:** подъём по мегабашне (крыши, каньон, башня, лазерный зал, контейнеры, спираль, вершина), запечённый свет, панорама неба, мокрый блеск, дождь, туман, песня; мрачная антиутопия.
4. **Race:** гонка на ногах (трасса 633 м из Blender, три развилки, 8 ворот), правила и HUD, новые сетевые события, песня на «СТАРТ!»; сделано агентами, проверено двухклиентным тестом на локальном сервере.
5. **Инфраструктура:** пресеты экспорта карт (с `include_filter` для файлов, грузимых по строке), проверка пакетов «по существу», выкладка сайта и сервера по порядку из «Деплой».
6. **Замеры:** вкладка в браузере ~300 МБ.
## Лицензии и авторство
- Модель карты Backrooms — «Backrooms VR», автор VTX (https://sketchfab.com/VTX_car), **CC-BY 4.0** (`maps/backrooms/model/CREDITS.md`).
При публикации нужно указывать автора.
- Остальные модели и звуки — материалы владельца проекта.