Files
my-world/README.md
T
aibrashandClaude Sonnet 5.5 86a1821d27 Race 2: своя палитра и небо, чистовое запекание, вентиляторы сильнее, скрытая шахта, новая песня
- палитра целиком (индиго, пурпур, ледяная бирюза): SOLID/NEON, SURFACES/GLOWS, туман, ambient, свет запекания, ворота, полосы, надписи; новое небо
- запекание 384 сэмпла + огни у подножия башни
- вентилятор: VENT_MAX_RISE 16, VENT_ACCEL 80; шахта-лифт без знака и без столба/света (hidden)
- музыка Race 2: Poloz «On My Way to Hell» с 1:40

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 17:21:07 +05:00

540 lines
101 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
# Кароче это пишу я, владелец карты
1. Управление геймпадом
2. Новую карту гонки, стиль тот же киберпанк, надо чтобы было длиньше первой, и упор шел на маневренность и скорость
3. Чтобы на Tab показывался список игроков онлайн
Мультиплеерная игра от первого лица в браузере (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 2**; выход из карт обратно в хаб |
| 🔶 | **Race 2 «Верхние ярусы»** — вторая гонка (крыши и мосты над городом, бег по фасадам, лазерные мосты, спуск, башня; три пути на каждом участке; вентиляторы и ускоряющие полосы). **Фаза 1 готова** (раскладка, геометрия, проходимость ботами, правила); фаза 2 (чистовое запекание, своё небо и музыка) ждёт одобрения владельца — раздел «Карта Race 2» |
| ✅ | **Race** — гонка на ногах до 8 игроков (отсчёт, ворота по порядку, места, итоги, рекорд, песня на старте); **Parkour** — подъём по мегабашне с чекпоинтами, лазерами, дождём и песней; обе карты — мрачная киберпанк-антиутопия, свет запечён в Blender (см. «Карты и порталы», «Карта Race», «Заметки для будущего меня») |
| ✅ | Вкладка в браузере занимает **~300 МБ** (замер владельца, окт 2026) |
| ✅ | Карты качаются отдельными пакетами `.pck` при первом входе в портал (экран загрузки с прогрессом) |
| ✅ | Мультиплеер: выделенный сервер, игроки видят друг друга, **комнаты по картам** (в разных картах не пересекаются) |
| ✅ | Преследователи: **камень** и **Верити**; общие для всех игроков комнаты, ходят по навмешу, «видят» только вблизи или в поле зрения, могут потерять игрока |
| ✅ | Смерть при поимке и возрождение, красный экран «ПОЙМАЛ!» |
| ✅ | Паркур: спринт, рывок, подкат, **присед (C)**, бег по стенам, прыжок от стены, двойной прыжок, подтягивание на уступ, зум (ЛКМ) |
| ✅ | Управление **не зависит от раскладки** (русская и любая другая): действия `InputMap` на физические клавиши |
| ✅ | Деплой: CI из `grid-world` на сайт; скрипты экспорта, dev-сервера, тестов без окна, выкладки сервера |
| ❌ | Управление с помощью геймпада |
| ✅ | **Ники** (панель по N, подпись над игроками, в итогах гонки) и **общий рейтинг** лучших времён на сервере (топ-10 на Race/Parkour, таблица по Tab, доски в хабе, таймер и итоги Parkour, сдача в гонке на G) — раздел «Ники и рейтинг» |
| ❌ | Текстовый чат — **запланирован**, не сделан (план в «Дорожная карта → Ближайшее») |
| ❌ | Настроек (чувствительность мыши, громкость), меню паузы |
| ❌ | Серверных проверок (антиспуфинг позиций, античит): сервер только пересылает |
| ❌ | Сохранения прогресса, аккаунтов |
| ❌ | Автоматических тестов в 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 — «готов» в гонке, перезапуск забега в Parkour), `nick` (N), `leaderboard` (Tab), `giveup` (G).
| Клавиша | Действие |
|---|---|
| W A S D, стрелки | движение |
| Shift | спринт |
| Пробел | прыжок; держать у уступа в воздухе — подтянуться; у стены — прыжок от стены; в воздухе — двойной прыжок |
| C (держать) | **присед** (скорость 3,5, капсула вдвое ниже, пролезает под низкое) |
| Shift + C на бегу | **подкат** |
| Q | рывок |
| R | «готов» (карта Race); на карте Parkour — **забег заново** (на старт, таймер сброшен) |
| G (держать 1,5 с) | **сдаться** в гонке (только пока идёт гонка; отпустил раньше — отмена) |
| N | ввод/смена ника (панель по центру; Enter — подтвердить, Esc — пропустить/отмена); на время ввода игрок стоит |
| Tab | таблица лидеров текущей карты (топ-10 и «Вы: N-й»); в хабе и на картах без рейтинга — вкладки по картам, A/D или стрелки; повторный Tab или Esc закрывают |
| ЛКМ (зажать) | зум |
| Esc / клик | отпустить / захватить мышь |
Бег по стене — в воздухе у стены с зажатым «вперёд» на скорости. Ctrl не используется: в браузере `Ctrl+W` закрывает вкладку.
## Структура проекта
```
project.godot, export_presets.cfg
branding/ splash.png (заставка загрузки), icon.svg (иконка, фавиконка) — «Aman's World»
net/ сеть. Net — автолоад: и клиент, и сервер (один скрипт)
net.gd состояние комнат, преследователей, RPC, профиль и ники, рейтинг на сервере (см. «Сеть», «Ники и рейтинг»)
ghost.gd чужой игрок: модель, выбор анимации по флагам и скорости, цвет свечения от id, ник
server.tscn пустая сцена для запуска выделенного сервера
core/
map_loader.gd автолоад MapLoader: скачивает пакет карты и подключает его (см. «Загрузка карт»)
overlay.gd автолоад Overlay: панель ника (N) и таблица лидеров (Tab) поверх любой карты
lb_view.gd LbView: таблица рейтинга карты (панель Tab, итоги Race/Parkour) и общий стиль панелей
player/
player.tscn, player.gd игрок от первого лица: всё паркур-управление, смерть и возрождение
character/ character.glb — модель киберниндзя и 14 анимаций (исходник и скрипт сборки: `../Blender models/character_01/`)
objects/
portal.gd портал на другую карту (Area3D, рисуется кодом)
vent.gd вентиляционный поток (Area3D): тянет игрока вверх, горизонтальная скорость сохраняется (Race 2)
boost_strip.gd ускоряющая полоса (Area3D): добирает скорость до MAX_SPEED вдоль полосы (Race 2)
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 2**), race_data.gd (генерируется), baked.gdshader, race.glb, sky.png, audio/music.ogg
race2/ race2.tscn, race2.gd (внешний вид, ворота, лазеры, вентиляторы, полосы), race2_data.gd (генерируется), baked.gdshader, race2.glb, sky.png, audio/music.ogg; правила берёт из maps/race/race_rules.gd
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×7 м, строится кодом (пол-грид, стены, свет). Порталы в ряд по центру, влезает 6. Над порталами карт «на время» — доски с топ-3 (см. «Ники и рейтинг»). Безопасна, преследователей нет |
| Песочница | `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). **Забег на время** (старт — линия на краю стартовой платформы, финиш — вершина; рейтинг на сервере; R — заново). Подъём по мегабашне, ~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» ниже |
| Race 2 | `maps/race2/race2.tscn` | `race2.pck` (9,5 МБ) | «Верхние ярусы»: гонка по крышам и мостам над городом, 13 ворот, ~1,2 км безопасного пути; на каждом участке безопасный / рискованный / секретный путь; вентиляторы-подбросы и ускоряющие полосы. Правила те же, что у Race (общий код). Раздел «Карта Race 2» |
**Правка и пересборка 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 с, затем ожидание; готовность сброшена, игроки стоят где стояли.
- Зашедший в разгар круга бегает без зачёта, видит «ГОНКА ИДЁТ» (определяется по маякам других игроков) и участвует со следующего круга.
- **Сдача** (G, удержание 1,5 с): индикатор «СДАЮСЬ…» с полоской внизу, отпустил раньше — отмена. Только в фазе «гонка», у не финишировавшего. Сдавшийся возвращается на стартовую площадку (свободно бегает до конца круга), в итогах стоит ниже всех со строкой «сдался», **время не пишется** ни в итоги, ни в рейтинг. Сдавшиеся считаются завершившими для конца круга (все финишировали/сдались — итоги сразу; иначе 45 с после первого финиша), места остальных считаются без них.
- Срыв: ниже **последних пройденных ворот** на 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 фаза, **11 сдался** | время финиша или -1 | «маяк» раз в секунду и при смене фазы; по нему догоняют потерявшие событие и зашедшие позже |
| `RACE_GIVEUP` | — | время гонки на момент сдачи | игрок, сдавшийся (G) |
Время: на получении START остаток считается как `b - unix_now`; если он отличается от 6 с больше чем на 0,4 с (часы машин расходятся), считаем 5,9 с от получения. Время гонки у каждого = `(ticks - start_tick)`, финиш `b` пересылается точным числом, поэтому таблицы мест у всех совпадают. «Готов» учитывается только в ожидании; на START все флаги готовности сбрасываются. Ник — `Net.nick_of(id)` (как над призраком), цвет — `Color.from_hsv(id*0.618034…)`. В итогах справа — общий рейтинг карты (`LbView`), на финише клиент шлёт `Net.submit_my_record(rating_id, время)` .
**Проверка по существу** (всё в `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_giveup_test.py` — сдача: A отменяет удержание и сдаётся, B финиширует; итоги одинаковы, в рейтинге только B; во втором круге сдались оба — круг сразу закончился.
- `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` менялся при Race (`submit_race`/`recv_race`) и при никах/рейтинге (см. «Ники и рейтинг») → после пуша `grid-world` сразу `tools/deploy_server.sh root@192.168.10.110`.
## Карта Race 2 («Верхние ярусы»)
**Состояние: фаза 1** (раскладка, геометрия, ворота, правила, проходимость ботами, предварительный свет). **Фаза 2** — после одобрения владельцем: чистовое запекание (384+ сэмплов), своё небо (`build_sky.py`), своя музыка. Палитра своя (холодная индиго-ночь, пурпур и ледяная бирюза; ворота тусклые индиго / очередные маджента / пройденные циан), небо перерисовано (`build_sky.py`), свет запечён на 384 сэмплах. Музыка — «On My Way to Hell» (Poloz), с 1:40 исходника (`ffmpeg -ss 100 -i исходник.mp3 -vn -map_metadata -1 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 maps/race2/audio/music.ogg`, ~3:40, 1,2 МБ; исходник вне проекта: `source-assets/music/`); играет с «СТАРТ!» с начала по кругу (`loop = true`, затухание 0,8 с). Стык петли на слух/по уровню не проверен.
Исходники: `Blender models/race_02/` (README там же; репозитория у папки нет — файлы не коммитятся). Концепция согласована с владельцем: вертикальность и поток скорости в духе Ghostrunner, но атмосфера как у Race/Parkour (дождь, смог, мокрый бетон, натриевый/красный/больной зелёный свет, надписи-приказы), туман 0,011.
Файлы: `maps/race2/` — `race2.tscn`, `race2.gd` (внешний вид, ворота со световыми столбами, лазеры, вентиляторы, полосы, срыв, дождь без дождя в траншее и шахте, гул, портал), `race2_data.gd` (**генерируется** `build_race.py`, не править),
`race2.glb`, `sky.png`, `baked.gdshader`, `audio/music.ogg`. **Правила — общие с Race:** `race2.gd` создаёт тот же `maps/race/race_rules.gd` и передаёт ему `data` (= `race2_data.gd`) и `rating_id` (= `"race2"`); `race_rules.gd` берёт данные у карты
(`m.get("data")`, у Race — запасной `load` его `race_data.gd`). Пакет `race2.pck` (9,5 МБ) собирается пресетом **Race2** (в нём `include_filter`: `maps/race/race_rules.gd` и файлы карты); скрипты `objects/vent.gd` и `objects/boost_strip.gd` — в **основном** пакете.
Рейтинг: `Net.TIMED_MAPS["race2"] = {"min": 44}` (≈ 60 % от лучшего круга ботом по всем секретам ~72 с), `Net.MAP_TITLES["race2"] = "Гонка 2"`; в хабе портал «RACE 2» пятым в ряду.
**Новые объекты** (локальный эффект на игроке, сетевой код не тронут; в `player/player.gd` — только хуки `apply_vent()` и `apply_boost()` и константы `VENT_ACCEL`, `VENT_MAX_RISE`, `BOOST_ACCEL`, `EFFECT_HOLD`; остальная физика и `MAX_SPEED = 22` не менялись):
- **Вентиляционный поток** (`objects/vent.gd`): пока игрок в столбе, вертикальная скорость растёт до **16 м/с** (ускорение 80 м/с²; раньше 11 и 60), горизонтальная сохраняется, двойной прыжок возвращается. Выход из столба на 16 м/с даёт ещё ~13 м высоты (v²/2g), поэтому ступень башни 12,5 м берётся с перелётом ~3 м; ступени 14–15 м в пробах ботом вели к промахам и оставлены 12,5. Шахта-лифт — скрытый поток (`hidden`: без столба, щелей и света), над окном знака нет. Визуал: тёмная решётка со светящимися щелями, мерцающий зелёный столб, слабый свет.
- **Ускоряющая полоса** (`objects/boost_strip.gd`): набор скорости вдоль полосы до 22 м/с (не выше `MAX_SPEED`), потом на земле она гасится как обычно (14 м/с²), в воздухе сохраняется. Измерено: на 22 м/с одиночный прыжок 25 м, двойной 42 м; **рывок сбрасывает скорость до 13**. Стрелки пурпурно-белые, бегут по ходу (статичные ледяные — подсказки трассы).
**Участки и три пути** (подробности и координаты — `Blender models/race_02/README.md`, `layout.py`): старт на верхней платформе (8 слотов, шаг 2,8 м) → 2) мосты и крыши между башнями → 3) фасады: бег по четырём стенам → 4) площадь с краном → 5) пульсирующие лазерные мосты → 6) спуск по крышам → 7) башня-спираль из четырёх вентиляторов; на крыше ядра — финиш и портал.
Безопасный путь 1085 м до подножия башни + 136 м башня = **1221 м** (план ~1100; можно сжать зоны 4 и 6), 13 ворот (ширина до 100 м там, где ветки расходятся широко), круг бота по безопасному пути ~99 с.
Безопасный: зазоры 8–12 м, подъём до 1,5 м, спуск до 9,2 м. Рискованный: зазоры 16–24 м (двойной прыжок + рывок), бег по стенам. Секретный: скрытые тёмные площадки без подсветки и входы без подписей — окно «пустого квартала», траншея под ямой (подкат), техпроход под лазерами, стена w6, шахта-лифт.
Срыв ниже последних ворот на 32 м = «СОРВАЛСЯ!», возрождение на воротах (центр каждых ворот лежит на плите — проверено).
**Проверка ботами** (`Blender models/race_02/tests/`, `run.sh`; в headless с `--fixed-fps 60` — быстрее реального времени). Все ветки пройдены (результаты ниже — последний прогон, оценочные времена бота от одной точки зоны до другой, секунды):
| Зона | безопасный | рискованный | секретный | выигрыш секрета |
|---|---|---|---|---|
| 2 мосты | 13,2 | 10,8 | 9,5 | 3,7 |
| 3 фасады | 14,5 | 9,9 (4 стены, 4 из 4 попыток) | 10,3 | 4,2 |
| 4 площадь | 12,8 | 7,6 | 7,8 | 5,0 |
| 5 лазеры | 10,1 | 11,3 | 9,1 | 1,0 (бот не теряет времени на ритм лазеров) |
| 6 спуск | 15,5 | 12,4 | 10,9 (стена w6: 3 из 4 попыток, с запасом в 1,5 м не проходит) | 4,6 |
| 7 башня | 17,4 | — | 9,5 | 7,9 |
Итого все секреты вместе экономят ~26 с из ~99 (оценка бота; человек на секретах рискует сорваться на последние ворота). **Правила на race2:** `MAP=race2 tools/test/race_game_test.sh` (бот проводит игроков через ворота телепортами по 0,35 м за кадр, на лазерном мосту выбирает полосу — проверяются отсчёт, места, итоги, один START, возрождение лицом по трассе, зритель; проходимость проверена отдельно). Рейтинг этим тестом не проверяется: круг бота ~60 с — запись возможна, но тест рейтинг не сверяет.
Пакет: `godot --headless --export-pack Race2 /tmp/x/race2.pck`, затем `strings` по `sky.png`, `baked.gdshader`, `race2_data`, `race_rules.gd`, `music.ogg`, `race2.glb`, `race2.gd` (каждое > 0; vent/boost_strip в этом пакете нет — они в основном). Через портал хаба: `tools/test/race2_check.txt`.
**Что не сделано / известно:** после смены силы вентиляторов (11 -> 16 м/с) проходимость башни и траншеи проверена ботом только выборочно (cmp7_safe, secret7, cmp4_secret, risky4 прошли), полный прогон и таблица времён выше — до этой правки, не перемерены; подножие башни всё ещё тёмное; `race2.glb` 8,4 МБ против 5,2 у Race (в фазе 2 прореживать фон); у зон 1 и 7 нет рискованной ветки; зазор для безопасного пути на спуске к `lane` — 8 м при спуске 9,2 м; не мерялись: нагрузка в браузере и звук. **Выкладка:** `net/net.gd` изменён только константами (`TIMED_MAPS`, `MAP_TITLES`), RPC те же — но сервер без нового `net.gd` **не примет рекорды race2** (запись отклонит: карты нет в `TIMED_MAPS`), поэтому после пуша `grid-world` запустить `tools/deploy_server.sh`; порядок и бэкап — как в «Деплой».
## Ники и рейтинг
**Профиль** — `user://profile.cfg` (ConfigFile, секция `profile`): `client_id` (случайный 128-битный hex, 32 символа; создаётся при первом запуске, **не показывается**, опознаёт игрока в рейтинге; потерял файл/очистил данные сайта — стал новым игроком), `nick`, `asked` (уже спрашивали ник). В тестах путь задаётся `-- --profile=путь`.
**Ники.** При первом запуске в хабе — панель «Введите ник» (≤ 16 символов, Enter — подтвердить, Esc — пропустить: тогда «Игрок N» и больше не спрашиваем). Клавиша **N** (физическая, действие `nick`) в любой момент открывает панель смены с текущим ником; вставка и кириллица работают. Нормализация (`Net.normalize_nick`, один код на клиенте и **сервере**): `\n \r \t` -> пробел, прочие управляющие и невидимые символы (в т.ч. направляющие юникода, нулевой пробел) выбрасываются, пробелы по краям обрезаются, ≤ 16 символов, пустой -> «Игрок N» (N = `id % 1000`). Одинаковые ники в комнате допустимы. Чужой ник везде выводится обычным `Label`/`Label3D` (не RichText) — «[b]» оформление не ломает. Единая точка: `Net.nick_of(peer_id)` (свой — `Net.my_nick`), сигнал `Net.nick_changed(id)`. Где виден: подпись над призраком (`ghost.gd`), HUD и итоги гонки, таблицы рейтинга.
- Сеть: после подключения клиент шлёт `submit_profile(client_id, nick)` **раньше** `submit_map`; при смене ника — снова. Сервер хранит `peer_client` (peer -> client_id) и `client_nick`; при входе в комнату (`submit_map`) присылает вошедшему `recv_nick` всех присутствующих, а им — его; смену ника рассылает комнате; на выходе чистит. Клиент принимает `recv_nick` только при `map_ready` и очищает `Net.nicks` в `_reset_room_state()` — ники не просачиваются между комнатами (как состояние игроков).
- **Блокировка ввода** на время набора: `Net.input_blocked` (ставит `Overlay`). Игрок читает ввод опросом, поэтому `player.gd` при `input_blocked` стоит как при заморозке и не ловит `crouch/dash`, а клик мыши не захватывает курсор. Это **отдельно от `player.input_frozen`** (его ведёт гонка на отсчёте; закрытие панели его не сбрасывает). R/N/Tab/G в поле — обычные буквы/не действия; открывающая клавиша в поле не попадает (фокус берётся отложенно). После закрытия мышь снова захвачена.
**Рейтинг (сервер).** Карты «на время» и допустимые времена — константа `Net.TIMED_MAPS` в `net/net.gd`:
```gdscript
const TIMED_MAPS := {"race": {"min": 25.0}, "parkour": {"min": 40.0}}
const MAX_RECORD_TIME := 3600.0
```
Минимумы — **страховка от мусора, не античит**: время быстрее `min` или больше 3600 с сервер молча отбрасывает. Подкрутить — поправить числа (и выложить сервер вместе с клиентом, см. «Деплой»; клиент использует тот же список, чтобы знать, где рисовать доски).
- `submit_record(map_id, time)`: клиент сообщает свой результат (сервер берёт `client_id` и ник по peer). Проверки: карта из списка **и** игрок сейчас в её комнате, время конечное и в пределах, есть профиль, лимит **6 записей в минуту** на игрока (лишнее молча отбрасывается). Хранится лучшее время каждого `client_id` (худшее не затирает; ник обновляется при смене ника и/или улучшении), не более **100** записей на карту (худшие вытесняются). Ответ: `recv_record_result(map_id, time, rank, personal, map_record)` (место, «новый личный рекорд», «новый рекорд карты» = личный и 1-е место) и `recv_leaderboard`.
- `request_leaderboard(map_id)` -> `recv_leaderboard(map_id, top, my_rank, my_time)`: `top` — до 10 записей `[ник, время, unix-ts]` по возрастанию (при равенстве — кто раньше), `my_rank` — место запросившего (0 — нет записи), `my_time` — его лучшее. Лимит **12 запросов за 10 с** на игрока. Клиент хранит ответы в `Net.boards[map_id]`, сигнал `Net.leaderboard_updated(map_id)`.
- **Хранилище:** `user://leaderboard.json` на сервере. На боевом сервере `user://` = `/var/lib/my-world/.local/share/godot/app_userdata/world2-server/` (служба `my-world`: `StateDirectory=my-world`, `HOME=/var/lib/my-world`, DynamicUser) — папка постоянная, переживает перезапуск службы и `tools/deploy_server.sh` (тот копирует только `net/` и `project.godot`). Формат:
```json
{"version": 1, "maps": {"race": {"<client_id>": {"nick": "Алиса", "time": 52.318, "ts": 1793250000}}, "parkour": {}}}
```
Запись атомарная (во временный `.tmp`, затем переименование), не чаще раза в 2 с (`SAVE_EVERY`; первая после паузы — сразу). Чтение при старте; отсутствующий файл — пустой рейтинг; испорченный — сервер не падает, файл переименовывается в `leaderboard.json.bad`, рейтинг начинается заново; при загрузке отбрасываются неизвестные карты, неверные id и времена, лишнее сверх 100 на карту. **`systemctl stop/restart` не сбрасывает отложенную запись** (Godot по SIGTERM не присылает уведомление о закрытии): в худшем случае теряются результаты последних ≤ 2 с.
- **Бэкап / сброс:** `ssh root@192.168.10.110 'cp /var/lib/my-world/.local/share/godot/app_userdata/world2-server/leaderboard.json /root/leaderboard-$(date +%F).json'`; сброс — остановить службу (`systemctl stop my-world`), удалить файл, запустить. Проверить, что файл пишется в постоянную папку: после любого результата `ssh root@192.168.10.110 'ls -la /var/lib/my-world/.local/share/godot/app_userdata/world2-server/; journalctl -u my-world -n 5 --no-pager'` (в логе строка `[рекорд] race <ник> <время> (место N)`). Если путь не тот — `find /var/lib/my-world -name leaderboard.json`.
**Таблица лидеров (Tab, действие `leaderboard`).** `core/overlay.gd` + `core/lb_view.gd`: справа по центру, тёмная полупрозрачная подложка, место / ник / `мм:сс.мс`, свой ряд подсвечен, вне топ-10 — строка «Вы: N-й, время»; сверху остаётся таймер гонки, в центре — отсчёт. Игрок при показе не блокируется. Закрывается повторным Tab или Esc.
**Доски в хабе** (`hub.gd`, `_build_boards`): над порталами карт из `TIMED_MAPS` — Label3D на тёмной подложке с топ-3; запрос при входе в хаб и раз в 60 с; нет ответа — «—». У песочницы и Backrooms досок нет.
**Parkour: забег.** Таймер стартует при пересечении **линии на конце стартовой платформы** — Blender `y = 14` (`slab('start', -12, 12, -14, 14, …)` в `layout.py`), в Godot `z = -14`: константа `START_LINE_Z` в `maps/parkour/parkour.gd` (меняете раскладку — поправьте её). Финиш — **вершина**: Area3D 14×5×14 м над верхом платформы `summit`, центр `Data.PORTAL + (0, 2.5, 3)` (в `parkour_data.gd` PORTAL = центр summit + 3 м по Blender-y; `FINISH_SIZE`). Смерть и возрождение таймер не останавливают. На финише — `submit_record("parkour", время)` и панель итогов ~8 с (время, рейтинг, «НОВЫЙ РЕКОРД»). R — телепорт на старт, таймер и чекпоинты сброшены. Таймер маленький, сверху по центру.
**Проверка по существу** (всё в `tools/test/`, порт 9080 свободен):
- `nicks_lb_test.py [папка]` — локальный сервер (его `user://` в папке теста через `XDG_DATA_HOME`) и клиенты `lb_client.tscn`: ники у двоих, у позже зашедшего, смена, изоляция комнат, «[b]»; рейтинг: лучшее время без дублей, сортировка, топ-10 при 14 игроках, неверные записи, лимиты, повторный вход, **перезапуск сервера**, испорченный файл, обрезка 120 -> 100, подписи над призраками в настоящей сцене хаба; в конце `nick_ui_test.tscn` (панель ника, блокировка ввода, R/N/Tab/G в поле, Esc, Tab).
- `parkour_run_test.py` — забег: линия, таймер, смерть, финиш, отказ слишком быстрому времени, R, запись в рейтинге.
- `ui_tour.tscn --mode=hub|race|parkour` — кадры (`--write-movie`; рейтинг подставлен, сервер не нужен; добавляйте `--server-url=ws://127.0.0.1:1`, иначе окно подключится к реальному серверу и затрёт подставленное).
## Преследователи
`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)` | сервер → комната | игрок вышел |
| `submit_profile(client_id, nick)` / `recv_nick(id, nick)` | клиент → сервер / сервер → комната | профиль и ники (раздел «Ники и рейтинг») |
| `submit_record(map_id, time)` / `recv_record_result(map_id, time, rank, personal, map_record)` / `recv_leaderboard(map_id, top, my_rank, my_time)` | клиент → сервер → клиенту | рекорды |
| `request_leaderboard(map_id)` | клиент → сервер | запрос таблицы (ответ — `recv_leaderboard`) |
Флаги состояния (`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):
- Race 2: `MAP=race2 tools/test/race_game_test.sh` (правила, см. «Карта Race 2»); боты проходимости — `Blender models/race_02/tests/` (`run.sh`, `run_all.sh`, `compare.sh`, `frames.sh`); пакет через портал хаба — `tools/test/race2_check.txt`.
- `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 смотри только геометрию.
**Ники и рейтинг — грабли**
- Любой `Input`-опрос не знает о LineEdit: блокировать надо самому (`Net.input_blocked`), `input_frozen` не трогать (гонка). Клавиши, набираемые в LineEdit, до `_unhandled_input` не доходят (поле их съедает), но R/N в правилах всё равно проверяют `Net.input_blocked` — на всякий случай.
- `Input.parse_input_event` в тестах доставляет события по-настоящему (в поле попадают буквы, действия не срабатывают) — так проверена панель ника без окна. Режим мыши в headless не хранится: захват проверять руками.
- Тесты, которым нужен рейтинг, стартуют сервер с `XDG_DATA_HOME=<папка теста>` (иначе пишут в `~/.local/share/godot/app_userdata/world2-server/` разработчика). `pkill -f` с путём теста убивает собственную оболочку — глуши по PID.
- Время записи на сервере сравнивается с `min` из `TIMED_MAPS`: тест Parkour «состаривает» старт на 45 с (`run_start_tick -= 45000`), иначе пришлось бы бежать минуту.
- Хаб стал на 1 м выше (7 м): доски над порталами и «CHOOSE A MAP» над ними иначе перекрывались.
- SIGTERM не сбрасывает отложенную запись рейтинга (≤ 2 с) — см. раздел «Ники и рейтинг».
- Отправка профиля обязана идти **раньше** `submit_map` (оба `reliable`, порядок сохраняется) — иначе вошедший в комнату покажется остальным без ника.
- Правка набора RPC (`submit_profile`, `recv_nick`, `submit_record`, `request_leaderboard`, `recv_leaderboard`, `recv_record_result`) — клиент и сервер выкладываются **вместе**.
**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».
**Race 2 — что выяснено**
- Дальность прыжка (ровная плита, бот `race_02/tests/measure.gd`): на 13 м/с — 14,7 / 24,9 / 29,1 м (одиночный / двойной / рывок+двойной), на 17 — 19,4 / 32,7 / 37,1, на 22 — 25,1 / 42,3 / 46,5. **Рывок сбрасывает скорость до `DASH_EXIT_SPEED` = 13**: разгон полосой до рывка пропадает, после полосы прыгать без рывка.
- Бег по стенам съедает высоту: 4 стены каньона отнимают ~9 м — низ каньона (и всё после него) сделан на 9,2 м ниже крыш (`layout.ZP`); при 3 стенах хватало 4,3 м.
- Вентилятор шириной 6 м при горизонтальной скорости 13 м/с держит игрока ~0,46 с: чтобы подняться на 12,5 м, нужны сильный поток (`VENT_ACCEL = 60`, `VENT_MAX_RISE = 11`) и двойной прыжок. Шахта-лифт работает потому, что у шахты есть стены: игрок упирается в них, гасит скорость и поднимается весь путь.
- Подкат под плитой: центр капсулы стоит на 1,18 м над полом, полная высота ~2,4 м — под мостом нужен просвет ≥ 2,5 м (2,2 м не пролезть). Подкат с 22 м/с стартует со скорости ~27 и скользит ~55 м.
- Центр ворот лежит на плите: возрождение происходит ровно там. Широкие ворота на разлёте веток (cp6 — 100 м, cp10 — 56 м) допустимы: окно по высоте у пересечения — от −3 до +9 м от центра ворот.
- Лазерные пары (левая/правая половина моста, период 3 с, горят по 1 с с фазой 1,5 с) оставляют свободную полосу почти всё время (обе заняты ~10 % времени, при учёте поправки ±0,4 с).
- Боты (граблям не повторять): точка маршрута над пропастью разворачивает бота в воздухе (ориентиры только на плитах); прыжок держать до конца полёта (отпустил — «срезанный» прыжок вдвое короче); у ботов с `--client` запуск рядом с локальным тестовым сервером добавляет лишнего игрока в комнату; **не `pkill -f` с путём теста** (убьёт свою оболочку) — `ps … | grep '[g]odot'` и `kill` по PID.
- Запекание: `BAKE_SAMPLES=64 ./rebuild.sh` — проба за ~30 с; на дне между башнями (подножие финальной башни) свет получается тёмным — добавлены прожекторные панели на стенах ядра; чистовое запекание — фаза 2.
## Дорожная карта
### Телефон — не планируется
Владелец решил, что телефон не нужен (окт 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 на реальном устройстве.
### Ближайшее: текстовый чат (ПЛАН, не реализовано — ждёт команды владельца)
Ники **сделаны** (раздел «Ники и рейтинг»): `Net.nick_of(id)`, `Net.my_nick`, `Net.input_blocked` для панели ввода. Чат строится на них. Смена ника по команде `/ник` не нужна — есть клавиша N.
**Чат**
- Открытие: действие InputMap `chat` на физические клавиши T и Enter (работает на любой раскладке); Enter — отправить и закрыть, Esc — закрыть. Чат — в комнате текущей карты (в хабе/песочнице/Backrooms/Parkour/Race разные).
- UI: CanvasLayer, внизу слева, мрачный минимализм (тёмная полупрозрачная подложка, небольшой шрифт, без ярких цветов), последние ~8 строк, видны ~10 с и гаснут; `Ник: текст`, ник цветом игрока (цвет от id, как у призрака), системные строки серым («зашёл»/«вышел»). **Без BBCode**: чужой текст не должен менять оформление (Label или RichTextLabel с выключенным BBCode). Желательно пузырь с последним сообщением над призраком на ~6 с.
- **Ввод при наборе:** уже есть `Net.input_blocked` и образец — панель ника в `core/overlay.gd` (флаг отдельный от `input_frozen`, мышь освобождается и потом захватывается, открывающая клавиша в поле не попадает, R/N/Tab/G в поле не действия). Повторить для чата.
- Сеть: RPC `submit_chat(text)` → сервер валидирует и пересылает остальным в комнате (`recv_chat(id, text)`), отправитель видит своё сразу. **Серверные ограничения:** ≤ 200 символов, убрать управляющие, пустые отбросить, лимит ~5 сообщений за 5 с на игрока (молча отбрасывать). Команды `/…` обрабатываются на клиенте и не рассылаются.
**Проверка (по существу):** образец — `tools/test/nicks_lb_test.py`: чат только в своей комнате; лимиты длины/частоты; «[b]» не ломает оформление; при открытом чате бот не двигается, после закрытия — двигается; R/T в поле не срабатывают как действия. Регрессия: тест гонки, `mantle_test`, `measure_movement`. Кадры панели ника и чата через `--write-movie`: не перекрывают HUD гонки (сверху таймер, по центру отсчёт), кириллица рисуется.
**Выкладка:** меняется набор RPC → клиент и сервер вместе (порядок из «Деплой»), игрокам Ctrl+F5; старый клиент с новым сервером падает с «rpc node checksum failed».
### Идеи (оценка независимого консультанта, окт 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.
## Название и заставка
Игра называется **Aman's World** (`config/name`). Заставка загрузки — `branding/splash.png` (1280×720, генерируется скриптом на Pillow: дождь, силуэты домов, натриевое свечение; при смене надписи/стиля перерисуй и положи на то же место), иконка и фавиконка — `branding/icon.svg`. Всё задаётся в `project.godot → [application]` (`boot_splash/image`, `boot_splash/bg_color`, `config/icon`); веб-экспорт берёт из них `index.png`, `index.icon.png` и `<title>`. **Папка пользовательских данных осталась прежней** (`config/use_custom_user_dir`, `custom_user_dir_name="world2"`): профиль (`profile.cfg`) и рекорд (`race_best.cfg`) не должны потеряться при переименовании проекта. Серверный проект (`deploy/server/project.godot`) — отдельный, его имя не менялось.
## Журнал изменений (окт 2026)
Сделано одной серией работ (хронологически):
1. **Персонаж:** модель киберниндзя со скелетом (752 треугольника, 14 анимаций, IK ног) вместо капсул; цвет свечения от id игрока; свой персонаж спрятан от своей камеры. Сальто камеры при двойном прыжке делалось и убрано по просьбе.
2. **Подтягивание на уступы** починено (не работало от ~2 м на всех картах), замеры мувсета и тестовые сцены в `tools/test/`.
3. **Parkour:** подъём по мегабашне (крыши, каньон, башня, лазерный зал, контейнеры, спираль, вершина), запечённый свет, панорама неба, мокрый блеск, дождь, туман, песня; мрачная антиутопия.
4. **Race:** гонка на ногах (трасса 633 м из Blender, три развилки, 8 ворот), правила и HUD, новые сетевые события, песня на «СТАРТ!»; сделано агентами, проверено двухклиентным тестом на локальном сервере.
5. **Инфраструктура:** пресеты экспорта карт (с `include_filter` для файлов, грузимых по строке), проверка пакетов «по существу», выкладка сайта и сервера по порядку из «Деплой».
6. **Замеры:** вкладка в браузере ~300 МБ.
7. **Ники, общий рейтинг, таблица Tab, доски в хабе, сдача в гонке (G), таймер Parkour**, название «Aman's World» (заставка, иконка).
8. **Карта Metro (гонка вниз 1495 м со скользящими рельсами и бустером) сделана и УДАЛЕНА по решению владельца** («полный мусор», окт 2026): не понравилась. Из истории не восстанавливай без просьбы. Что узнали: (а) владельцу важнее всего скорость, маневренность и ощущение потока, как у первой гонки Race; (б) прежде чем отдавать большую карту агентам, показать владельцу концепцию и кадры черновика; (в) идея «скользящий рельс» (подхват в воздухе, езда 36 м/с, сход прыжком) работала технически, но потолок `MAX_SPEED = 22` обрезает скорость на сходе; (г) слишком светлые «пятнистые» залы из-за запекания и белые засветки — следить за силой света при запекании.
9. **Race 2 «Верхние ярусы» (фаза 1)**: вторая гонка по крышам и мостам над городом, 13 ворот, ~1,2 км, три пути на каждом участке, вентиляторы-подбросы и ускоряющие полосы (хуки в `player.gd`), общий с Race код правил (`race_rules.gd` берёт данные у карты), портал в хабе, рейтинг `race2`. Проходимость каждой ветки доказана ботами; чистовое запекание, небо и музыка — фаза 2.
## Лицензии и авторство
- Модель карты Backrooms — «Backrooms VR», автор VTX (https://sketchfab.com/VTX_car), **CC-BY 4.0** (`maps/backrooms/model/CREDITS.md`).
При публикации нужно указывать автора.
- Остальные модели и звуки — материалы владельца проекта.