Compare commits
5
Commits
b58c30a525
...
0ffba7311f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ffba7311f | ||
|
|
5d8b2165e3 | ||
|
|
3d017ffdca | ||
|
|
86a1821d27 | ||
|
|
c3e0ae40b2 |
@@ -21,7 +21,8 @@
|
||||
|
||||
| | Что |
|
||||
|---|---|
|
||||
| ✅ | Хаб с порталами; карты **Песочница**, **Backrooms**, **Parkour** и **Race**; выход из карт обратно в хаб |
|
||||
| ✅ | Хаб с порталами; карты **Песочница**, **Backrooms**, **Parkour**, **Race** и **Race 2**; выход из карт обратно в хаб |
|
||||
| 🔶 | **Race 2 «Верхние ярусы»** — вторая гонка (крыши и мосты над городом, бег по фасадам, лазерные мосты, спуск, башня; три пути на каждом участке; вентиляторы и ускоряющие полосы). **Фаза 1 готова** (раскладка, геометрия, проходимость ботами, правила); фаза 2 (чистовое запекание, своё небо и музыка) ждёт одобрения владельца — раздел «Карта Race 2» |
|
||||
| ✅ | **Race** — гонка на ногах до 8 игроков (отсчёт, ворота по порядку, места, итоги, рекорд, песня на старте); **Parkour** — подъём по мегабашне с чекпоинтами, лазерами, дождём и песней; обе карты — мрачная киберпанк-антиутопия, свет запечён в Blender (см. «Карты и порталы», «Карта Race», «Заметки для будущего меня») |
|
||||
| ✅ | Вкладка в браузере занимает **~300 МБ** (замер владельца, окт 2026) |
|
||||
| ✅ | Карты качаются отдельными пакетами `.pck` при первом входе в портал (экран загрузки с прогрессом) |
|
||||
@@ -94,6 +95,8 @@ player/
|
||||
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
|
||||
@@ -102,7 +105,8 @@ 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 (генерируется), baked.gdshader, race.glb, sky.png, audio/music.ogg
|
||||
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 (запечённый навмеш),
|
||||
@@ -131,6 +135,7 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (
|
||||
| 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 за полёт).
|
||||
@@ -190,6 +195,41 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (
|
||||
|
||||
**Выкладка (общее правило):** старый клиент с новым сервером (и наоборот) **не работает вообще** — 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=путь`.
|
||||
@@ -327,6 +367,7 @@ godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url
|
||||
В браузере: 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`.
|
||||
@@ -420,6 +461,16 @@ ssh root@192.168.10.110 'journalctl -u my-world -f' # лог: «+ игрок
|
||||
- В `--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.
|
||||
|
||||
## Дорожная карта
|
||||
|
||||
### Телефон — не планируется
|
||||
@@ -479,6 +530,8 @@ ssh root@192.168.10.110 'journalctl -u my-world -f' # лог: «+ игрок
|
||||
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`).
|
||||
|
||||
+51
-1
@@ -10,7 +10,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="tools/*, maps/sandbox/*, maps/backrooms/*, maps/parkour/*, maps/race/*, chasers/rock/*, chasers/verity/*"
|
||||
exclude_filter="tools/*, maps/sandbox/*, maps/backrooms/*, maps/parkour/*, maps/race/*, maps/race2/*, chasers/rock/*, chasers/verity/*"
|
||||
export_path="../grid-world/index.html"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
@@ -250,3 +250,53 @@ progressive_web_app/icon_512x512=""
|
||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||
threads/emscripten_pool_size=8
|
||||
threads/godot_pool_size=4
|
||||
|
||||
[preset.5]
|
||||
|
||||
name="Race2"
|
||||
platform="Web"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="resources"
|
||||
export_files=PackedStringArray("res://maps/race2/race2.tscn")
|
||||
include_filter="maps/race2/sky.png, maps/race2/baked.gdshader, maps/race2/race2_data.gd, maps/race/race_rules.gd, maps/race2/race2.gd, maps/race2/audio/*"
|
||||
exclude_filter="player/*, objects/*, chasers/chaser.gd"
|
||||
export_path="../grid-world/race2.pck"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.5.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
variant/extensions_support=false
|
||||
variant/thread_support=false
|
||||
vram_texture_compression/for_desktop=true
|
||||
vram_texture_compression/for_mobile=false
|
||||
html/export_icon=true
|
||||
html/custom_html_shell=""
|
||||
html/head_include=""
|
||||
html/canvas_resize_policy=2
|
||||
html/focus_canvas_on_start=true
|
||||
html/experimental_virtual_keyboard=false
|
||||
progressive_web_app/enabled=false
|
||||
progressive_web_app/ensure_cross_origin_isolation_headers=true
|
||||
progressive_web_app/offline_page=""
|
||||
progressive_web_app/display=3
|
||||
progressive_web_app/orientation=0
|
||||
progressive_web_app/icon_144x144=""
|
||||
progressive_web_app/icon_180x180=""
|
||||
progressive_web_app/icon_512x512=""
|
||||
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||
threads/emscripten_pool_size=8
|
||||
threads/godot_pool_size=4
|
||||
|
||||
@@ -8,6 +8,7 @@ const PORTALS := [
|
||||
{"title": "BACKROOMS", "map": "backrooms", "scene": "res://maps/backrooms/backrooms.tscn", "pack": "backrooms.pck", "color": Color(1.0, 0.85, 0.25)},
|
||||
{"title": "PARKOUR", "map": "parkour", "scene": "res://maps/parkour/parkour.tscn", "pack": "parkour.pck", "color": Color(1.0, 0.2, 0.65)},
|
||||
{"title": "RACE", "map": "race", "scene": "res://maps/race/race.tscn", "pack": "race.pck", "color": Color(1.0, 0.55, 0.12)},
|
||||
{"title": "RACE 2", "map": "race2", "scene": "res://maps/race2/race2.tscn", "pack": "race2.pck", "color": Color(0.45, 0.85, 0.35)},
|
||||
]
|
||||
|
||||
const ROOM_WIDTH := 28.0
|
||||
|
||||
@@ -9,7 +9,7 @@ extends Node
|
||||
# Сдача (клавиша G, удержание): игрока возвращает на стартовую площадку, время не пишется ни в итоги, ни в рейтинг;
|
||||
# сдавшиеся считаются завершившими для конца круга. Рейтинг: финиш -> Net.submit_my_record(rating_id, время).
|
||||
|
||||
const Data = preload("res://maps/race/race_data.gd")
|
||||
var Data = null # данные карты (race_data.gd / race2_data.gd): берутся у карты в setup(), по умолчанию — Race
|
||||
|
||||
enum Phase { WAIT, COUNTDOWN, RACE, RESULTS }
|
||||
|
||||
@@ -70,6 +70,7 @@ var _giveup_bar: ProgressBar
|
||||
func setup(m: Node3D) -> void:
|
||||
map = m
|
||||
player = m.player
|
||||
Data = m.get("data") if m.get("data") != null else load("res://maps/race/race_data.gd")
|
||||
if m.get("rating_id") != null:
|
||||
rating_id = m.rating_id
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://c3hge1rwjutk8"
|
||||
path="res://.godot/imported/music.ogg-f9a618edc60fbea7ca697db82c17868a.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/race2/audio/music.ogg"
|
||||
dest_files=["res://.godot/imported/music.ogg-f9a618edc60fbea7ca697db82c17868a.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
@@ -0,0 +1,42 @@
|
||||
shader_type spatial;
|
||||
// Поверхности уровня Parkour: освещение запечено в цвета вершин (Blender, Cycles), динамических источников нет.
|
||||
// Итог = цвет материала × запечённый свет × мелкое «зерно» бетона (по мировым координатам) + слабый блик на скользящих углах.
|
||||
render_mode unshaded;
|
||||
|
||||
uniform vec3 albedo_color = vec3(0.1);
|
||||
uniform float light_scale = 8.0; // цвета вершин сжаты в 8 раз при экспорте (см. bake.py)
|
||||
uniform float exposure = 1.0;
|
||||
uniform sampler2D grain : repeat_enable, filter_linear_mipmap;
|
||||
uniform float grain_scale = 0.3;
|
||||
uniform float grain_amount = 0.45;
|
||||
uniform sampler2D sky_tex : source_color, filter_linear_mipmap; // панорама неба: отражается в «мокрых» горизонтальных поверхностях
|
||||
uniform float wet = 0.6;
|
||||
|
||||
varying vec3 wpos;
|
||||
varying vec3 wnorm;
|
||||
|
||||
void vertex() {
|
||||
wpos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
wnorm = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||
}
|
||||
|
||||
vec2 pano_uv(vec3 d) {
|
||||
return vec2(atan(d.x, -d.z) / (2.0 * PI) + 0.5, acos(clamp(d.y, -1.0, 1.0)) / PI);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec3 n = abs(wnorm);
|
||||
n /= (n.x + n.y + n.z + 0.0001);
|
||||
float g = texture(grain, wpos.zy * grain_scale).r * n.x
|
||||
+ texture(grain, wpos.xz * grain_scale).r * n.y
|
||||
+ texture(grain, wpos.xy * grain_scale).r * n.z;
|
||||
float gr = mix(1.0 - grain_amount * 0.5, 1.0 + grain_amount * 0.5, g);
|
||||
vec3 lit = COLOR.rgb * light_scale;
|
||||
vec3 nv = normalize(NORMAL);
|
||||
float ndv = clamp(dot(nv, normalize(VIEW)), 0.0, 1.0);
|
||||
float fres = 0.05 + 0.95 * pow(1.0 - ndv, 4.0);
|
||||
vec3 rw = normalize((INV_VIEW_MATRIX * vec4(reflect(-normalize(VIEW), nv), 0.0)).xyz);
|
||||
float up = smoothstep(0.6, 0.95, wnorm.y); // блестят только горизонтальные поверхности
|
||||
vec3 refl = textureLod(sky_tex, pano_uv(rw), 3.0).rgb;
|
||||
ALBEDO = albedo_color * lit * gr * exposure + refl * fres * wet * up * (0.5 + 0.5 * min(lit.r + lit.g + lit.b, 1.0));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwrd86827arnc
|
||||
@@ -0,0 +1,397 @@
|
||||
extends Node3D
|
||||
# Карта «Гонка 2» — «Верхние ярусы» (RACE 2): забег по крышам и мостам мрачного города: старт на верхней платформе, мосты между
|
||||
# башнями, бег по фасадам, площадь с краном, пульсирующие лазерные мосты, спуск по крышам и подъём по спирали вентиляторов
|
||||
# на финальную башню. На каждом участке три пути: безопасный, рискованный и секретный (см. README, «Карта Race 2»).
|
||||
# Модель — race2.glb, данные — race2_data.gd (оба генерирует `Blender models/race_02/build_race.py` из layout.py).
|
||||
# Здесь: внешний вид (материалы по именам из GLB, окружение, дождь, гул), ворота, лазеры, вентиляторы, ускоряющие полосы, срыв,
|
||||
# возрождение на последних воротах. Правила гонки (ожидание, отсчёт, место, итоги, сеть) — общие с картой Race: maps/race/race_rules.gd
|
||||
# (ему передаётся data = race2_data.gd и rating_id); сервер про них ничего не знает.
|
||||
|
||||
const Data = preload("res://maps/race2/race2_data.gd")
|
||||
const Rules = preload("res://maps/race/race_rules.gd")
|
||||
const RATING_ID := "race2" # id карты в рейтинге сервера (Net.TIMED_MAPS)
|
||||
const KILL_DEPTH := 32.0 # Ниже последних пройденных ворот на столько — срыв (то же значение в race_data.gd)
|
||||
const GATE_HEIGHT := 5.0
|
||||
const FLOOR_TO_CENTER := 1.2 # Центр капсулы над полом в данных ворот и слотов
|
||||
|
||||
var player: Node3D
|
||||
var rules: Node
|
||||
var data := Data # для race_rules.gd (общий код правил)
|
||||
var rating_id := RATING_ID
|
||||
var _lasers: Array = [] # {area, mat, mesh, mode, period, on, phase}
|
||||
var _gates: Array = [] # {root, mat, sheet, label} — визуал ворот по индексам CHECKPOINTS
|
||||
|
||||
func _ready() -> void:
|
||||
Net.enter_map("race2")
|
||||
player = get_tree().get_first_node_in_group("local_player")
|
||||
var start: Vector3 = Data.START_SLOTS[0]
|
||||
player.global_position = start
|
||||
player.spawn_transform = Transform3D(Basis.IDENTITY, start)
|
||||
player.death_label.text = "СОРВАЛСЯ!"
|
||||
_prepare_map()
|
||||
_build_environment()
|
||||
for i in Data.CHECKPOINTS.size():
|
||||
_add_gate(i)
|
||||
for l in Data.LASERS:
|
||||
_add_laser(l)
|
||||
for v in Data.VENTS:
|
||||
_add_vent(v)
|
||||
for st in Data.STRIPS:
|
||||
_add_strip(st)
|
||||
_add_rain()
|
||||
_add_ambience()
|
||||
_add_exit()
|
||||
rules = Rules.new()
|
||||
rules.name = "Rules"
|
||||
rules.setup(self)
|
||||
add_child(rules)
|
||||
|
||||
# Цвета поверхностей — те же значения, что в Blender (kit.py: SOLID); освещение уже запечено в цветах вершин
|
||||
const SURFACES := {
|
||||
"Concrete": Color(0.095, 0.105, 0.15), "Dark": Color(0.012, 0.013, 0.024), "Steel": Color(0.115, 0.125, 0.185),
|
||||
"PaintY": Color(0.30, 0.10, 0.36), "PaintR": Color(0.22, 0.05, 0.16), "PaintB": Color(0.04, 0.06, 0.18),
|
||||
"PaintO": Color(0.14, 0.09, 0.30),
|
||||
}
|
||||
# Светящиеся (мрачная палитра): Cyan — холодный белёсый, Magenta — тревожный красный, Orange — натриевая лампа
|
||||
const GLOWS := {
|
||||
"NeonCyan": [Color(0.30, 0.85, 0.98), 0.7], "NeonMagenta": [Color(0.85, 0.08, 0.65), 1.2],
|
||||
"NeonOrange": [Color(0.55, 0.22, 1.0), 1.3], "NeonYellow": [Color(0.72, 0.84, 1.0), 1.0],
|
||||
"NeonGreen": [Color(0.25, 0.95, 0.75), 1.0], "NeonBlue": [Color(0.28, 0.32, 0.92), 1.0],
|
||||
"NeonRed": [Color(1.0, 0.06, 0.28), 1.5], "NeonWhite": [Color(0.72, 0.82, 1.0), 0.9],
|
||||
}
|
||||
|
||||
# Коллизию создаёт импорт (узел Collision-colonly), здесь — внешний вид: материалы по именам из Blender
|
||||
func _prepare_map() -> void:
|
||||
var shader := load("res://maps/race2/baked.gdshader") as Shader
|
||||
var noise := FastNoiseLite.new()
|
||||
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
|
||||
noise.frequency = 0.04
|
||||
noise.fractal_octaves = 4
|
||||
var grain := NoiseTexture2D.new()
|
||||
grain.noise = noise
|
||||
grain.width = 512
|
||||
grain.height = 512
|
||||
grain.seamless = true
|
||||
var sky_tex := load("res://maps/race2/sky.png") as Texture2D
|
||||
var cache := {}
|
||||
for mi: MeshInstance3D in $Map.find_children("*", "MeshInstance3D", true, false):
|
||||
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
for s in mi.mesh.get_surface_count():
|
||||
var src := mi.mesh.surface_get_material(s)
|
||||
var key: String = src.resource_name if src != null else ""
|
||||
if not cache.has(key):
|
||||
if SURFACES.has(key):
|
||||
var m := ShaderMaterial.new()
|
||||
m.shader = shader
|
||||
m.set_shader_parameter("albedo_color", Vector3(SURFACES[key].r, SURFACES[key].g, SURFACES[key].b))
|
||||
m.set_shader_parameter("grain", grain)
|
||||
m.set_shader_parameter("sky_tex", sky_tex)
|
||||
m.set_shader_parameter("exposure", 2.1)
|
||||
cache[key] = m
|
||||
elif GLOWS.has(key):
|
||||
cache[key] = _emissive(GLOWS[key][0], GLOWS[key][1])
|
||||
else:
|
||||
cache[key] = null
|
||||
if cache[key] != null:
|
||||
mi.set_surface_override_material(s, cache[key])
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if player == null:
|
||||
return
|
||||
if not player.is_dead:
|
||||
if player.global_position.y < player.spawn_transform.origin.y - KILL_DEPTH:
|
||||
player.die()
|
||||
else:
|
||||
for l in _lasers:
|
||||
if _laser_active(l) and l.area.overlaps_body(player):
|
||||
player.die()
|
||||
break
|
||||
_update_lasers()
|
||||
_update_rain()
|
||||
|
||||
# ---------- окружение ----------
|
||||
|
||||
func _build_environment() -> void:
|
||||
var sky_mat := PanoramaSkyMaterial.new()
|
||||
sky_mat.panorama = load("res://maps/race2/sky.png")
|
||||
var sky := Sky.new()
|
||||
sky.sky_material = sky_mat
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_SKY
|
||||
env.sky = sky
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR # свет на самом игроке/призраках; уровень освещён запечённо
|
||||
env.ambient_light_color = Color(0.20, 0.22, 0.40)
|
||||
env.ambient_light_energy = 0.6
|
||||
env.fog_enabled = true
|
||||
env.fog_light_color = Color(0.10, 0.09, 0.24) # холодный индиго-дым высоких ярусов
|
||||
env.fog_density = 0.011
|
||||
env.fog_sky_affect = 0.45
|
||||
env.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||
env.glow_enabled = true
|
||||
env.glow_intensity = 0.55
|
||||
env.glow_bloom = 0.1
|
||||
env.glow_hdr_threshold = 1.0
|
||||
env.adjustment_enabled = true
|
||||
env.adjustment_contrast = 1.15
|
||||
env.adjustment_saturation = 0.9
|
||||
var we := WorldEnvironment.new()
|
||||
we.environment = env
|
||||
add_child(we)
|
||||
var sun := DirectionalLight3D.new() # подсветка только для персонажей
|
||||
sun.rotation_degrees = Vector3(-55.0, 35.0, 0.0)
|
||||
sun.light_color = Color(0.5, 0.58, 0.95)
|
||||
sun.light_energy = 0.4
|
||||
add_child(sun)
|
||||
|
||||
func _emissive(color: Color, energy: float, alpha := 1.0) -> StandardMaterial3D:
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = Color(0, 0, 0, alpha)
|
||||
m.emission_enabled = true
|
||||
m.emission = color
|
||||
m.emission_energy_multiplier = energy
|
||||
if alpha < 1.0:
|
||||
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
m.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
return m
|
||||
|
||||
# ---------- ворота: высокие светящиеся рамы поперёк трассы ----------
|
||||
|
||||
const GATE_OFF := Color(0.30, 0.26, 0.55) # ещё не очередные: тусклый индиго
|
||||
const GATE_NEXT := Color(1.0, 0.18, 0.85) # очередные: яркая маджента
|
||||
const GATE_DONE := Color(0.25, 0.95, 0.95) # пройдены: ледяной циан
|
||||
|
||||
# Угол рыскания из данных (0 = вдоль -Z, -90 = на +X) — это ровно rotation.y в Godot
|
||||
func gate_yaw(i: int) -> float:
|
||||
return deg_to_rad(Data.CHECKPOINTS[i][2])
|
||||
|
||||
func gate_pos(i: int) -> Vector3:
|
||||
return Data.CHECKPOINTS[i][0]
|
||||
|
||||
func gate_half(i: int) -> float:
|
||||
return Data.CHECKPOINTS[i][1]
|
||||
|
||||
func _add_gate(i: int) -> void:
|
||||
var last: bool = i == Data.CHECKPOINTS.size() - 1
|
||||
var half: float = gate_half(i)
|
||||
var mat := _emissive(GATE_OFF, 0.9)
|
||||
var sheet := _emissive(GATE_OFF, 0.3, 0.07)
|
||||
var root := Node3D.new()
|
||||
root.position = gate_pos(i) - Vector3(0, FLOOR_TO_CENTER, 0) # в данных — центр капсулы, ворота ставим на пол
|
||||
root.rotation.y = gate_yaw(i) # локальный -Z ворот смотрит по трассе
|
||||
add_child(root)
|
||||
var post_h := GATE_HEIGHT
|
||||
for s in [-1.0, 1.0]:
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(0.4, post_h, 0.4)
|
||||
root.add_child(_part(bm, mat, Vector3(s * half, post_h * 0.5, 0)))
|
||||
var top := BoxMesh.new()
|
||||
top.size = Vector3(half * 2 + 0.4, 0.4, 0.4)
|
||||
root.add_child(_part(top, mat, Vector3(0, post_h, 0)))
|
||||
var beams: Array = [] # световые столбы над стойками: ворота видно издалека
|
||||
for s in [-1.0, 1.0]:
|
||||
var bm2 := BoxMesh.new()
|
||||
bm2.size = Vector3(0.7, 70.0, 0.7)
|
||||
var bmat := _emissive(GATE_OFF, 1.0, 0.10)
|
||||
root.add_child(_part(bm2, bmat, Vector3(s * half, post_h + 35.0, 0)))
|
||||
beams.append(bmat)
|
||||
var qm := QuadMesh.new() # слабая светящаяся «плёнка» в проёме
|
||||
qm.size = Vector2(half * 2, post_h)
|
||||
root.add_child(_part(qm, sheet, Vector3(0, post_h * 0.5, 0)))
|
||||
var label := Label3D.new()
|
||||
label.text = "FINISH" if last else "CHECKPOINT %d/%d" % [i + 1, Data.CHECKPOINTS.size()]
|
||||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
label.pixel_size = 0.022
|
||||
label.modulate = GATE_OFF
|
||||
label.outline_modulate = Color.BLACK
|
||||
label.position = Vector3(0, post_h + 1.2, 0)
|
||||
root.add_child(label)
|
||||
_gates.append({"root": root, "mat": mat, "sheet": sheet, "label": label, "beams": beams})
|
||||
|
||||
# Раскраска ворот по прогрессу игрока: пройденные зелёные, очередные яркие, остальные тусклые
|
||||
func set_gate_progress(passed: int) -> void:
|
||||
for i in _gates.size():
|
||||
var g: Dictionary = _gates[i]
|
||||
var c := GATE_DONE if i < passed else (GATE_NEXT if i == passed else GATE_OFF)
|
||||
g.mat.emission = c
|
||||
g.sheet.emission = c
|
||||
g.label.modulate = c
|
||||
for b in g.beams:
|
||||
b.emission = c
|
||||
g.mat.emission_energy_multiplier = 1.6 if i == passed else 0.9
|
||||
|
||||
func _part(mesh: Mesh, mat: Material, pos: Vector3) -> MeshInstance3D:
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
mi.material_override = mat
|
||||
mi.position = pos
|
||||
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
return mi
|
||||
|
||||
# ---------- лазеры ----------
|
||||
|
||||
func _add_laser(d: Array) -> void:
|
||||
var area := Area3D.new()
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 1
|
||||
area.position = d[0]
|
||||
var cs := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = d[1]
|
||||
cs.shape = shape
|
||||
area.add_child(cs)
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = d[1]
|
||||
var mat := _emissive(Color(1.0, 0.05, 0.22), 2.4, 0.35 if d[1].y > 3.0 else 1.0)
|
||||
var mesh := _part(bm, mat, Vector3.ZERO)
|
||||
area.add_child(mesh)
|
||||
add_child(area)
|
||||
_lasers.append({"area": area, "mat": mat, "mesh": mesh, "mode": d[2], "period": d[3], "on": d[4], "phase": d[5]})
|
||||
|
||||
# Пульсирующие лазеры считают фазу по системным часам — у всех игроков примерно одинаково
|
||||
func _laser_active(l: Dictionary) -> bool:
|
||||
if l.mode == 0:
|
||||
return true
|
||||
return fposmod(Time.get_unix_time_from_system() + l.phase, l.period) < l.on
|
||||
|
||||
func _update_lasers() -> void:
|
||||
for l in _lasers:
|
||||
var active := _laser_active(l)
|
||||
l.mat.emission_energy_multiplier = 2.4 if active else 0.3
|
||||
l.mesh.visible = active or l.mode == 1
|
||||
|
||||
# ---------- вентиляторы и ускоряющие полосы (objects/vent.gd, objects/boost_strip.gd) ----------
|
||||
|
||||
func _add_vent(d: Array) -> void:
|
||||
var a := Area3D.new()
|
||||
a.set_script(load("res://objects/vent.gd"))
|
||||
a.size = d[1]
|
||||
a.height = d[2]
|
||||
a.hidden = d[3]
|
||||
a.position = d[0]
|
||||
add_child(a)
|
||||
|
||||
func _add_strip(d: Array) -> void:
|
||||
var a := Area3D.new()
|
||||
a.set_script(load("res://objects/boost_strip.gd"))
|
||||
a.size = d[1]
|
||||
a.speed = d[3]
|
||||
a.position = d[0]
|
||||
a.rotation.y = deg_to_rad(d[2])
|
||||
add_child(a)
|
||||
|
||||
# ---------- выход ----------
|
||||
|
||||
func _add_exit() -> void:
|
||||
var portal := Area3D.new()
|
||||
portal.set_script(load("res://objects/portal.gd"))
|
||||
portal.target_map = "hub"
|
||||
portal.target_scene = "res://maps/hub/hub.tscn"
|
||||
portal.title = "HUB"
|
||||
portal.color = Color(0.3, 1, 0.45)
|
||||
portal.position = Data.PORTAL
|
||||
add_child(portal)
|
||||
|
||||
# ---------- дождь и звук ----------
|
||||
|
||||
var _rain: GPUParticles3D
|
||||
|
||||
# Ливень вокруг игрока: тонкие вертикальные штрихи, летящие по ветру; в закрытых местах (COVERED из race2_data.gd) не идёт
|
||||
func _add_rain() -> void:
|
||||
_rain = GPUParticles3D.new()
|
||||
_rain.amount = 2200
|
||||
_rain.lifetime = 0.8
|
||||
_rain.local_coords = false
|
||||
_rain.visibility_aabb = AABB(Vector3(-30, -30, -30), Vector3(60, 60, 60))
|
||||
var pm := ParticleProcessMaterial.new()
|
||||
pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX
|
||||
pm.emission_box_extents = Vector3(26.0, 0.5, 26.0)
|
||||
pm.direction = Vector3(0.12, -1.0, 0.05)
|
||||
pm.spread = 2.0
|
||||
pm.initial_velocity_min = 34.0
|
||||
pm.initial_velocity_max = 40.0
|
||||
pm.gravity = Vector3.ZERO
|
||||
_rain.process_material = pm
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(0.018, 0.7)
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.albedo_color = Color(0.62, 0.7, 0.95, 0.3)
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_FIXED_Y
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
quad.material = mat
|
||||
_rain.draw_pass_1 = quad
|
||||
_rain.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(_rain)
|
||||
|
||||
func _update_rain() -> void:
|
||||
if _rain == null or player == null:
|
||||
return
|
||||
_rain.global_position = player.global_position + Vector3(0, 12, 0)
|
||||
var p := player.global_position
|
||||
var covered := false
|
||||
for c in Data.COVERED: # траншея и шахта: x0, x1, z0, z1, пол, потолок (z0 > z1: по Z расстояние со знаком минус)
|
||||
if p.x > c[0] and p.x < c[1] and p.z < c[2] and p.z > c[3] and p.y > c[4] and p.y < c[5]:
|
||||
covered = true
|
||||
_rain.emitting = not covered
|
||||
|
||||
# Низкий промышленный гул и шум дождя: 3 секунды без щелчка на стыке петли (края сведены перекрёстным затуханием)
|
||||
func _add_ambience() -> void:
|
||||
const RATE := 22050
|
||||
const FADE := 4410
|
||||
var n := RATE * 3
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 13
|
||||
var raw := PackedFloat32Array()
|
||||
raw.resize(n + FADE)
|
||||
var lp := 0.0
|
||||
for i in n + FADE:
|
||||
lp = lerpf(lp, rng.randf_range(-1.0, 1.0), 0.55) # мягкий «шипящий» шум
|
||||
raw[i] = lp
|
||||
var data := PackedByteArray()
|
||||
data.resize(n * 2)
|
||||
for i in n:
|
||||
var v := raw[i]
|
||||
if i < FADE:
|
||||
var t := float(i) / FADE
|
||||
v = raw[i] * sin(t * PI * 0.5) + raw[n + i] * cos(t * PI * 0.5)
|
||||
var tt := float(i) / RATE
|
||||
var hum := 0.16 * sin(TAU * 42.0 * tt) + 0.08 * sin(TAU * 63.0 * tt) + 0.05 * sin(TAU * 21.0 * tt)
|
||||
data.encode_s16(i * 2, int(clampf(v * 0.45 + hum, -1.0, 1.0) * 14000.0))
|
||||
var wav := AudioStreamWAV.new()
|
||||
wav.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
wav.mix_rate = RATE
|
||||
wav.stereo = false
|
||||
wav.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
wav.loop_begin = 0
|
||||
wav.loop_end = n
|
||||
wav.data = data
|
||||
var ap := AudioStreamPlayer.new()
|
||||
ap.stream = wav
|
||||
ap.volume_db = -13.0
|
||||
ap.autoplay = true
|
||||
add_child(ap)
|
||||
|
||||
# Песня гонки (пока та же, что у Race; своя музыка — фаза 2): Ogg Vorbis, моно 32 кГц (~0,6 МБ). Включается в момент «СТАРТ!» (race_rules.gd), плавно затихает при возврате в ожидание
|
||||
const MUSIC_DB := -9.0
|
||||
var _music: AudioStreamPlayer
|
||||
var _music_tween: Tween
|
||||
|
||||
func set_music(on: bool) -> void:
|
||||
if _music == null:
|
||||
var stream := load("res://maps/race2/audio/music.ogg") as AudioStreamOggVorbis
|
||||
stream.loop = true
|
||||
_music = AudioStreamPlayer.new()
|
||||
_music.stream = stream
|
||||
_music.volume_db = MUSIC_DB
|
||||
add_child(_music)
|
||||
if _music_tween:
|
||||
_music_tween.kill()
|
||||
if on:
|
||||
if not _music.playing:
|
||||
_music.volume_db = MUSIC_DB
|
||||
_music.play() # каждый круг — с начала
|
||||
elif _music.playing:
|
||||
_music_tween = create_tween()
|
||||
_music_tween.tween_property(_music, "volume_db", -60.0, 0.8)
|
||||
_music_tween.tween_callback(_music.stop)
|
||||
@@ -0,0 +1 @@
|
||||
uid://djwekkr3vawop
|
||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
||||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bepdwblao0vt"
|
||||
path="res://.godot/imported/race2.glb-855c5ef0b2a873d4c22fd233e8a67730.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/race2/race2.glb"
|
||||
dest_files=["res://.godot/imported/race2.glb-855c5ef0b2a873d4c22fd233e8a67730.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
mesh_library/use_node_names_as_mesh_names=false
|
||||
array_mesh/deduplicate_surfaces=true
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=1
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://maps/race2/race2.gd" id="1_level"]
|
||||
[ext_resource type="PackedScene" path="res://maps/race2/race2.glb" id="2_map"]
|
||||
[ext_resource type="PackedScene" path="res://player/player.tscn" id="3_player"]
|
||||
|
||||
[node name="Race2" type="Node3D"]
|
||||
script = ExtResource("1_level")
|
||||
|
||||
[node name="Map" parent="." instance=ExtResource("2_map")]
|
||||
|
||||
[node name="Player" parent="." instance=ExtResource("3_player")]
|
||||
@@ -0,0 +1,88 @@
|
||||
# Сгенерировано Blender models/race_02/build_race.py из layout.py — не править руками
|
||||
extends RefCounted
|
||||
|
||||
# Старт: 8 слотов в ряд поперёк трассы, лицом по трассе (-Z); центр капсулы
|
||||
const START_SLOTS := [
|
||||
Vector3(-9.80, 61.25, -28.00),
|
||||
Vector3(-7.00, 61.25, -28.00),
|
||||
Vector3(-4.20, 61.25, -28.00),
|
||||
Vector3(-1.40, 61.25, -28.00),
|
||||
Vector3(1.40, 61.25, -28.00),
|
||||
Vector3(4.20, 61.25, -28.00),
|
||||
Vector3(7.00, 61.25, -28.00),
|
||||
Vector3(9.80, 61.25, -28.00),
|
||||
]
|
||||
|
||||
# Ворота по порядку: [центр капсулы на полу ворот, полуширина поперёк трассы (м), рыскание (град; 0 = -Z, -90 = на +X, 180 = на +Z)]. Последний = ФИНИШ
|
||||
# Ворота стоят там, где ветки (безопасная / рискованная / секретная) сливаются.
|
||||
const CHECKPOINTS := [
|
||||
[Vector3(0.00, 61.20, -70.00), 8.0, 0.0], # cp1
|
||||
[Vector3(0.00, 62.20, -104.00), 17.0, 0.0], # cp2
|
||||
[Vector3(107.00, 62.20, -232.00), 25.0, 0.0], # cp3
|
||||
[Vector3(109.00, 53.00, -352.00), 12.0, 0.0], # cp4
|
||||
[Vector3(110.00, 53.00, -384.00), 20.0, 0.0], # cp5
|
||||
[Vector3(110.00, 53.00, -468.00), 50.0, 0.0], # cp6
|
||||
[Vector3(110.00, 53.00, -508.00), 22.0, 0.0], # cp7
|
||||
[Vector3(110.00, 53.00, -577.00), 26.0, 0.0], # cp8
|
||||
[Vector3(110.00, 53.00, -634.00), 18.0, 0.0], # cp9
|
||||
[Vector3(150.00, 48.00, -652.00), 28.0, -90.0], # cp10
|
||||
[Vector3(200.00, 29.00, -560.00), 12.0, 180.0], # cp11
|
||||
[Vector3(203.00, 25.00, -530.00), 33.0, 180.0], # cp12
|
||||
[Vector3(210.00, 75.00, -488.00), 14.0, 180.0], # cp13
|
||||
]
|
||||
|
||||
# Лазеры: центр, размер, режим (0 — всегда, 1 — пульсирует), период, время включения, сдвиг
|
||||
const LASERS := [
|
||||
[Vector3(108.00, 52.25, -544.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 0.00], # mL0
|
||||
[Vector3(112.00, 52.25, -544.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 1.50], # mR0
|
||||
[Vector3(108.00, 53.45, -560.25), Vector3(4.00, 0.70, 0.50), 1, 3.00, 1.00, 0.00], # mL1
|
||||
[Vector3(112.00, 53.45, -560.25), Vector3(4.00, 0.70, 0.50), 1, 3.00, 1.00, 1.50], # mR1
|
||||
[Vector3(108.00, 52.25, -576.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 0.00], # mL2
|
||||
[Vector3(112.00, 52.25, -576.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 1.50], # mR2
|
||||
[Vector3(108.00, 53.45, -592.25), Vector3(4.00, 0.70, 0.50), 1, 3.00, 1.00, 0.00], # mL3
|
||||
[Vector3(112.00, 53.45, -592.25), Vector3(4.00, 0.70, 0.50), 1, 3.00, 1.00, 1.50], # mR3
|
||||
[Vector3(108.00, 52.25, -608.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 0.00], # mL4
|
||||
[Vector3(112.00, 52.25, -608.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 1.50], # mR4
|
||||
]
|
||||
|
||||
# Вентиляционные потоки: центр решётки на полу, сечение (X, Z), высота столба, скрытый (без света и столба — шахта-лифт)
|
||||
const VENTS := [
|
||||
[Vector3(107.00, 43.80, -450.00), Vector2(6.00, 6.00), 14.00, false], # v_tn
|
||||
[Vector3(230.00, 23.80, -513.00), Vector2(6.00, 6.00), 15.00, false], # v1
|
||||
[Vector3(230.00, 36.30, -477.00), Vector2(6.00, 6.00), 15.00, false], # v2
|
||||
[Vector3(198.00, 48.80, -465.00), Vector2(6.00, 6.00), 15.00, false], # v3
|
||||
[Vector3(190.00, 61.30, -481.00), Vector2(6.00, 6.00), 15.00, false], # v4
|
||||
[Vector3(210.00, 23.80, -501.75), Vector2(3.00, 3.50), 53.00, true], # v_lift
|
||||
]
|
||||
|
||||
# Ускоряющие полосы: центр на полу, размер (ширина, длина), рыскание (направление разгона), скорость
|
||||
const STRIPS := [
|
||||
[Vector3(61.00, 62.00, -105.00), Vector2(6.00, 8.00), -45.0, 22.0], # s_p2a
|
||||
[Vector3(0.00, 60.00, -70.00), Vector2(6.00, 14.00), 0.0, 22.0], # s_b1
|
||||
[Vector3(9.00, 61.00, -105.00), Vector2(6.00, 16.00), -90.0, 22.0], # s_T2
|
||||
[Vector3(79.00, 52.80, -304.00), Vector2(5.00, 8.00), 0.0, 22.0], # s_ruin
|
||||
[Vector3(109.00, 51.80, -335.00), Vector2(6.00, 8.00), 0.0, 22.0], # s_LM
|
||||
[Vector3(107.00, 43.80, -408.00), Vector2(5.00, 10.00), 0.0, 22.0], # s_tn
|
||||
[Vector3(101.00, 48.20, -524.00), Vector2(4.00, 12.00), 0.0, 22.0], # s_cw524
|
||||
[Vector3(101.00, 48.20, -548.00), Vector2(4.00, 12.00), 0.0, 22.0], # s_cw548
|
||||
[Vector3(101.00, 48.20, -572.00), Vector2(4.00, 12.00), 0.0, 22.0], # s_cw572
|
||||
[Vector3(101.00, 48.20, -596.00), Vector2(4.00, 12.00), 0.0, 22.0], # s_cw596
|
||||
[Vector3(149.00, 46.80, -649.00), Vector2(6.00, 14.00), -90.0, 22.0], # s_d1
|
||||
[Vector3(113.00, 51.80, -642.00), Vector2(6.00, 14.00), -90.0, 22.0], # s_q1
|
||||
[Vector3(202.00, 41.80, -652.00), Vector2(6.00, 14.00), 180.0, 22.0], # s_d3
|
||||
[Vector3(202.00, 34.80, -604.00), Vector2(6.00, 10.00), 180.0, 22.0], # s_d5
|
||||
[Vector3(200.00, 23.80, -530.00), Vector2(6.00, 10.00), 180.0, 22.0], # s_f0
|
||||
]
|
||||
|
||||
# Закрытые места без дождя (траншея, шахта): x0, x1, z0, z1 (Godot), пол, потолок
|
||||
const COVERED := [
|
||||
[102.0, 112.0, -408.0, -452.0, 42.8, 47.2], # траншея
|
||||
[206.5, 213.5, -505.5, -500.0, 22.8, 76.8], # шахта-лифт
|
||||
]
|
||||
|
||||
# Портал в хаб за финишем: позиция на полу площадки
|
||||
const PORTAL := Vector3(210.00, 73.80, -476.00)
|
||||
# Длина безопасного маршрута, м
|
||||
const TRACK_LENGTH := 1228.9
|
||||
# Срыв: на столько ниже последнего чекпоинта — возрождение
|
||||
const KILL_DEPTH := 32.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://c01qphr2te2ln
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.3 MiB |
@@ -0,0 +1,41 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://fqq72yjnny11"
|
||||
path.s3tc="res://.godot/imported/sky.png-b8c1bf32feecd75c07d68b59edc4fccf.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/race2/sky.png"
|
||||
dest_files=["res://.godot/imported/sky.png-b8c1bf32feecd75c07d68b59edc4fccf.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
+2
-2
@@ -28,8 +28,8 @@ const RACE_GIVEUP = 5 # игрок сдался (b: время гонки на
|
||||
|
||||
# Рейтинг лучших времён. Список карт «на время» и минимально допустимое время (страховка от мусора, не античит;
|
||||
# подкручивается здесь — см. README «Ники и рейтинг»). Сервер принимает только эти id; клиент по ним же решает, где рисовать доски.
|
||||
const TIMED_MAPS := {"race": {"min": 25.0}, "parkour": {"min": 40.0}}
|
||||
const MAP_TITLES := {"race": "Гонка", "parkour": "Паркур", "hub": "Хаб", "world": "Песочница", "backrooms": "Backrooms"}
|
||||
const TIMED_MAPS := {"race": {"min": 25.0}, "race2": {"min": 44.0}, "parkour": {"min": 40.0}}
|
||||
const MAP_TITLES := {"race": "Гонка", "race2": "Гонка 2", "parkour": "Паркур", "hub": "Хаб", "world": "Песочница", "backrooms": "Backrooms"}
|
||||
const MAX_RECORD_TIME := 3600.0
|
||||
const BOARD_TOP := 10 # сколько строк уходит клиенту
|
||||
const BOARD_KEEP := 100 # сколько записей на карту хранит сервер
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
extends Area3D
|
||||
# Ускоряющая полоса: на ней игрок, бегущий вдоль неё, быстро добирает скорость (до speed, но не выше MAX_SPEED игрока).
|
||||
# Направление — локальная ось -Z узла (rotation.y = рыскание, как у ворот гонки). Эффект локальный, в сеть не уходит.
|
||||
# Начало координат — центр полосы на полу. Рисуется кодом: бирюзовые стрелки, бегущие по ходу.
|
||||
|
||||
@export var size := Vector2(6.0, 10.0) # ширина (X) и длина (Z), м
|
||||
@export var speed := 22.0
|
||||
@export var color := Color(0.85, 0.45, 1.0) # пурпурно-белый: отличается от ледяных стрелок-подсказок трассы
|
||||
|
||||
const SHADER := """
|
||||
shader_type spatial;
|
||||
render_mode unshaded, blend_add, cull_disabled, depth_draw_never;
|
||||
uniform vec4 tint : source_color = vec4(0.85, 0.45, 1.0, 1.0);
|
||||
uniform vec2 dims = vec2(6.0, 10.0);
|
||||
varying vec2 uv;
|
||||
void vertex() { uv = VERTEX.xz; }
|
||||
void fragment() {
|
||||
// uv.y < 0 — вперёд (-Z); шевроны бегут вперёд
|
||||
float across = abs(uv.x) / (dims.x * 0.5);
|
||||
float t = -uv.y * 0.35 - TIME * 2.6 + across * 0.9;
|
||||
float chev = smoothstep(0.55, 0.62, fract(t)) * (1.0 - smoothstep(0.78, 0.9, fract(t)));
|
||||
float edge = smoothstep(0.86, 0.94, across);
|
||||
float end = smoothstep(0.0, 0.1, 0.5 - abs(uv.y) / dims.y);
|
||||
ALBEDO = tint.rgb;
|
||||
ALPHA = clamp((chev * 0.85 + edge * 0.7 + 0.1) * end, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 0
|
||||
collision_mask = 1
|
||||
var cs := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = Vector3(size.x, 2.4, size.y)
|
||||
cs.shape = shape
|
||||
cs.position.y = 1.2
|
||||
add_child(cs)
|
||||
var sh := Shader.new()
|
||||
sh.code = SHADER
|
||||
var sm := ShaderMaterial.new()
|
||||
sm.shader = sh
|
||||
sm.set_shader_parameter("tint", color)
|
||||
sm.set_shader_parameter("dims", size)
|
||||
var pm := PlaneMesh.new()
|
||||
pm.size = size
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = pm
|
||||
mi.material_override = sm
|
||||
mi.position.y = 0.07
|
||||
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(mi)
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
var dir := -global_transform.basis.z
|
||||
for b in get_overlapping_bodies():
|
||||
if b.has_method("apply_boost"):
|
||||
b.apply_boost(dir, speed)
|
||||
@@ -0,0 +1 @@
|
||||
uid://7fr8utm135ud
|
||||
@@ -0,0 +1,93 @@
|
||||
extends Area3D
|
||||
# Вентиляционный поток: столб воздуха вверх из решётки. Пока игрок внутри, player.apply_vent() тянет его вверх
|
||||
# (вертикальная скорость растёт до VENT_MAX_RISE, горизонтальная сохраняется, двойной прыжок возвращается).
|
||||
# Эффект локальный (только на игроке), в сеть не уходит. Рисуется кодом: решётка на полу, мерцающий столб, тонкий свет.
|
||||
# Начало координат — центр решётки на полу; столб идёт вверх на height.
|
||||
|
||||
@export var size := Vector2(6.0, 6.0) # сечение столба (X, Z), м
|
||||
@export var height := 14.0 # высота столба, м
|
||||
@export var color := Color(0.25, 0.95, 0.75)
|
||||
@export var hidden := false # скрытый поток (шахта-лифт): только тёмная решётка, без столба, щелей и света
|
||||
|
||||
const SHADER := """
|
||||
shader_type spatial;
|
||||
render_mode unshaded, blend_add, cull_disabled, depth_draw_never;
|
||||
uniform vec4 tint : source_color = vec4(0.45, 0.85, 0.35, 1.0);
|
||||
uniform float height = 14.0;
|
||||
varying vec3 lp;
|
||||
void vertex() { lp = VERTEX; }
|
||||
void fragment() {
|
||||
float y = lp.y / height;
|
||||
float bands = 0.55 + 0.45 * sin((lp.y * 0.9 - TIME * 7.0) + lp.x * 1.7 + lp.z * 2.3);
|
||||
float fade = smoothstep(0.0, 0.08, y) * (1.0 - smoothstep(0.55, 1.0, y));
|
||||
ALBEDO = tint.rgb;
|
||||
ALPHA = clamp(bands * fade * 0.22, 0.0, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 0
|
||||
collision_mask = 1
|
||||
var cs := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = Vector3(size.x, height, size.y)
|
||||
cs.shape = shape
|
||||
cs.position.y = height * 0.5
|
||||
add_child(cs)
|
||||
_build_visuals()
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
for b in get_overlapping_bodies():
|
||||
if b.has_method("apply_vent"):
|
||||
b.apply_vent()
|
||||
|
||||
func _build_visuals() -> void:
|
||||
# мерцающий столб
|
||||
var sh := Shader.new()
|
||||
sh.code = SHADER
|
||||
var sm := ShaderMaterial.new()
|
||||
sm.shader = sh
|
||||
sm.set_shader_parameter("tint", color)
|
||||
sm.set_shader_parameter("height", height)
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(size.x, height, size.y)
|
||||
var col := MeshInstance3D.new()
|
||||
col.mesh = bm
|
||||
col.material_override = sm
|
||||
col.position.y = height * 0.5
|
||||
col.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
if not hidden:
|
||||
add_child(col)
|
||||
# решётка на полу: тёмная плита с светящимися щелями
|
||||
var dark := StandardMaterial3D.new()
|
||||
dark.albedo_color = Color(0.02, 0.025, 0.03)
|
||||
var glow := StandardMaterial3D.new()
|
||||
glow.albedo_color = Color(0, 0, 0)
|
||||
glow.emission_enabled = true
|
||||
glow.emission = color
|
||||
glow.emission_energy_multiplier = 2.2
|
||||
var plate := BoxMesh.new()
|
||||
plate.size = Vector3(size.x - 0.3, 0.08, size.y - 0.3)
|
||||
_add(plate, dark, Vector3(0, 0.04, 0))
|
||||
var n := 0 if hidden else int((size.x - 0.8) / 0.7)
|
||||
for i in n:
|
||||
var slit := BoxMesh.new()
|
||||
slit.size = Vector3(0.22, 0.03, size.y - 1.0)
|
||||
_add(slit, glow, Vector3(-(size.x - 0.8) * 0.5 + (i + 0.5) * (size.x - 0.8) / n, 0.095, 0))
|
||||
if hidden:
|
||||
return
|
||||
# слабый свет над решёткой
|
||||
var l := OmniLight3D.new()
|
||||
l.light_color = color
|
||||
l.light_energy = 0.8
|
||||
l.omni_range = maxf(size.x, size.y) * 1.8
|
||||
l.position.y = 1.5
|
||||
add_child(l)
|
||||
|
||||
func _add(mesh: Mesh, mat: Material, pos: Vector3) -> void:
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
mi.material_override = mat
|
||||
mi.position = pos
|
||||
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(mi)
|
||||
@@ -0,0 +1 @@
|
||||
uid://v76iegf4bft
|
||||
@@ -116,6 +116,16 @@ void fragment() {
|
||||
"""
|
||||
|
||||
# Настройки чувствительности мыши
|
||||
# --- Внешние эффекты карт (objects/vent.gd, objects/boost_strip.gd): локально, в сеть не уходят ---
|
||||
const VENT_ACCEL = 80.0 # Подъёмное ускорение в вентиляционном потоке
|
||||
const VENT_MAX_RISE = 16.0 # Поток разгоняет вертикальную скорость не выше этой
|
||||
const BOOST_ACCEL = 60.0 # Разгон на ускоряющей полосе (потолок — MAX_SPEED)
|
||||
const EFFECT_HOLD = 0.12 # Сколько секунд эффект держится после последнего вызова (область зовёт каждый кадр)
|
||||
var vent_timer: float = 0.0
|
||||
var boost_timer: float = 0.0
|
||||
var boost_dir: Vector3 = Vector3.ZERO
|
||||
var boost_speed: float = MAX_SPEED
|
||||
|
||||
@export var MOUSE_SENSITIVITY: float = 0.003
|
||||
|
||||
# --- Настройки тряски камеры (Head Bobbing) ---
|
||||
@@ -418,17 +428,39 @@ func _physics_process(delta: float) -> void:
|
||||
if horizontal.length() > speed_cap:
|
||||
horizontal = horizontal.normalized() * speed_cap
|
||||
|
||||
# Полоса: разгоняет вдоль своего направления, но не выше потолка MAX_SPEED
|
||||
if boost_timer > 0.0:
|
||||
boost_timer -= delta
|
||||
var hs := horizontal.length()
|
||||
if hs > 3.0 and horizontal.normalized().dot(boost_dir) > 0.5:
|
||||
horizontal = horizontal.normalized() * maxf(hs, minf(hs + BOOST_ACCEL * delta, boost_speed)) # только добирает: быстрее полосы не тормозит
|
||||
if horizontal.length() > MAX_SPEED:
|
||||
horizontal = horizontal.normalized() * MAX_SPEED
|
||||
|
||||
velocity.x = horizontal.x
|
||||
velocity.z = horizontal.z
|
||||
|
||||
# Вентиляционный поток: тянет вверх (горизонтальная скорость не трогается), возвращает двойной прыжок
|
||||
if vent_timer > 0.0:
|
||||
vent_timer -= delta
|
||||
velocity.y = minf(velocity.y + VENT_ACCEL * delta, maxf(velocity.y, VENT_MAX_RISE))
|
||||
air_jumps_left = AIR_JUMPS
|
||||
|
||||
# 6. Камера
|
||||
_update_camera(delta, input_dir)
|
||||
|
||||
_move_and_track(delta)
|
||||
|
||||
# Вызывает область-вентилятор каждый физический кадр, пока игрок в потоке
|
||||
func apply_vent() -> void:
|
||||
vent_timer = EFFECT_HOLD
|
||||
|
||||
# Вызывает ускоряющая полоса каждый кадр: dir — куда она разгоняет (горизонтально), speed — до какой скорости
|
||||
func apply_boost(dir: Vector3, speed: float) -> void:
|
||||
boost_timer = EFFECT_HOLD
|
||||
boost_dir = Vector3(dir.x, 0.0, dir.z).normalized()
|
||||
boost_speed = minf(speed, MAX_SPEED)
|
||||
|
||||
# Двигаемся и отслеживаем приземление
|
||||
func _move_and_track(delta: float) -> void:
|
||||
var pre_vy := velocity.y
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Экспорт веб-клиента и пакетов карт. По умолчанию — в ../grid-world (репозиторий сайта).
|
||||
# tools/export_all.sh [папка_вывода]
|
||||
# Пакеты карт (backrooms.pck, world.pck, parkour.pck, race.pck) нужно выкладывать вместе с index.* — клиент качает их при входе в портал.
|
||||
# Пакеты карт (backrooms.pck, world.pck, parkour.pck, race.pck, race2.pck) нужно выкладывать вместе с index.* — клиент качает их при входе в портал.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT="${1:-../grid-world}"
|
||||
@@ -16,4 +16,5 @@ godot --headless --export-pack Backrooms "$OUT/backrooms.pck"
|
||||
godot --headless --export-pack World "$OUT/world.pck"
|
||||
godot --headless --export-pack Parkour "$OUT/parkour.pck"
|
||||
godot --headless --export-pack Race "$OUT/race.pck"
|
||||
godot --headless --export-pack Race2 "$OUT/race2.pck"
|
||||
ls -la "$OUT"/*.pck "$OUT"/index.wasm | awk '{printf "%7.2f МБ %s\n", $5/1e6, $9}'
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Проверка карты Race 2 «по существу» после прохода через портал RACE 2 с настоящими пакетами (по образцу parkour_check.txt).
|
||||
# Вставь в tools/test/bot.gd ВРЕМЕННОЙ копии проекта (make_test_project.sh) перед строкой print("[%s] t=%4.1f сцена=...")
|
||||
# Пакеты: tools/export_all.sh /tmp/site (НЕ в grid-world), затем (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:9.0,1,-7.5
|
||||
# (портал RACE 2 пятый в ряду: x = (4 - 2) * 4.5 = 9.0). Сервер в этом тесте не нужен.
|
||||
# Ожидание: скрипт=true, правила=true, шейдерных_мешей>=1, небо_панорама=true, ворот=13, лазеров=10, вентиляторов=6, полос=15 (см. race2_data.gd), музыка_в_файле=true, сдача=true.
|
||||
|
||||
var sc := get_tree().current_scene
|
||||
if sc != null and sc.name == "Race2" and int(t) % 3 == 0:
|
||||
var shaded := 0
|
||||
for mi in sc.find_children("*", "MeshInstance3D", true, false):
|
||||
if mi.get_surface_override_material_count() > 0 and mi.get_surface_override_material(0) is ShaderMaterial:
|
||||
shaded += 1
|
||||
var env := sc.find_children("*", "WorldEnvironment", false, false)
|
||||
var sky_ok: bool = env.size() > 0 and env[0].environment.sky != null and env[0].environment.sky.sky_material is PanoramaSkyMaterial and env[0].environment.sky.sky_material.panorama != null
|
||||
print(" ПРОВЕРКА Race2: скрипт=%s правила=%s шейдерных_мешей=%d небо_панорама=%s ворот=%d лазеров=%d вентиляторов=%d полос=%d музыка_в_файле=%s сдача=%s" % [
|
||||
sc.get_script() != null, sc.get_node_or_null("Rules") != null, shaded, sky_ok, sc._gates.size(), sc._lasers.size(),
|
||||
sc.find_children("*", "Area3D", false, false).filter(func(a): return a.get_script() != null and a.get_script().resource_path.ends_with("vent.gd")).size(),
|
||||
sc.find_children("*", "Area3D", false, false).filter(func(a): return a.get_script() != null and a.get_script().resource_path.ends_with("boost_strip.gd")).size(),
|
||||
ResourceLoader.exists("res://maps/race2/audio/music.ogg"), sc.get_node("Rules").has_method("_give_up")])
|
||||
+53
-2
@@ -8,9 +8,13 @@ extends Node
|
||||
# --fall-at-gate=<n>: после ворот n упасть в пустоту (проверка срыва ниже ворот)
|
||||
# --gu1=<план>, --gu2=<план>: сдача в круге 1 / 2. План: «cancel@1,giveup@2» — когда пройдено столько ворот,
|
||||
# держать G 1,0 с и отпустить (отмена) / держать до сдачи (проверка race_giveup_test.sh)
|
||||
# --map=race2: карта «Гонка 2». Бежать по её маршруту без физики бегуна нельзя (вентиляторы, лазеры, стены), поэтому бот
|
||||
# «проводит» игрока через ворота телепортами по ~0,35 м за кадр (меньше TELEPORT_JUMP: правила считают это бегом); проверяются правила, а не проходимость
|
||||
# (проходимость карты — боты в Blender models/race_02/tests/).
|
||||
# Факты печатаются строками «[имя] …»; сверяет их tools/test/race_game_test.sh.
|
||||
|
||||
const Data = preload("res://maps/race/race_data.gd")
|
||||
var Data = null # данные карты: race_data.gd или (--map=race2) race2_data.gd
|
||||
var map_id := "race"
|
||||
|
||||
var label := "?"
|
||||
var delay := 0.0
|
||||
@@ -46,6 +50,7 @@ var gu_after_cancel := false
|
||||
|
||||
func _ready() -> void:
|
||||
for a in OS.get_cmdline_user_args():
|
||||
if a.begins_with("--map="): map_id = a.substr(6)
|
||||
if a.begins_with("--name="): label = a.substr(7)
|
||||
if a.begins_with("--delay="): delay = float(a.substr(8))
|
||||
if a.begins_with("--expect="): expect = int(a.substr(9))
|
||||
@@ -57,6 +62,10 @@ func _ready() -> void:
|
||||
for e in a.substr(6).split(","):
|
||||
var q := e.split("@")
|
||||
gu_plan[int(a.substr(4, 1))].append({"mode": q[0], "gate": int(q[1])})
|
||||
if map_id == "race2":
|
||||
Data = load("res://maps/race2/race2_data.gd")
|
||||
else:
|
||||
Data = load("res://maps/race/race_data.gd")
|
||||
route = JSON.parse_string(FileAccess.get_file_as_string("res://tools/test/race_route.json")).safe
|
||||
|
||||
func gp(w: Array) -> Vector3:
|
||||
@@ -76,7 +85,7 @@ func say(text: String) -> void:
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
var sc := get_tree().current_scene
|
||||
if sc == null or sc.name != "Race":
|
||||
if sc == null or sc.name != ("Race2" if map_id == "race2" else "Race"):
|
||||
return
|
||||
rules = sc.get_node_or_null("Rules")
|
||||
p = get_tree().get_first_node_in_group("local_player")
|
||||
@@ -220,7 +229,49 @@ func _race() -> void:
|
||||
return
|
||||
_drive()
|
||||
|
||||
func _drive_tele() -> void:
|
||||
var g: int = rules.passed.get(rules.my_id, 0)
|
||||
if g >= Data.CHECKPOINTS.size() or p.is_dead:
|
||||
return
|
||||
var gate: Array = Data.CHECKPOINTS[g]
|
||||
var yaw := deg_to_rad(gate[2])
|
||||
var fwd := Vector3(-sin(yaw), 0, -cos(yaw))
|
||||
var tgt: Vector3 = gate[0] + fwd * 1.5
|
||||
var step := 0.35 # м за кадр (~21 м/с): круг ~60 с, как у настоящей гонки — иначе зритель C не успеет зайти в разгар круга
|
||||
# лазерный мост: идём медленно (12 м/с) и на ближних подступах к воротам выбираем полосу, где лазер выключен в момент пролёта
|
||||
var pos: Vector3 = p.global_position
|
||||
var lane := -1.0
|
||||
if pos.x > 105.0 and pos.x < 115.0 and pos.z < -535.0 and pos.z > -615.0:
|
||||
step = 0.2
|
||||
for l in Data.LASERS:
|
||||
if l[0].x < 110.0: # левая половина; правая — в паре
|
||||
var dz: float = pos.z - l[0].z # > 0 — ворота ещё впереди (идём в -Z)
|
||||
if dz > -1.0 and dz < 8.0:
|
||||
var t := Time.get_unix_time_from_system() + maxf(dz, 0.0) / 12.0
|
||||
var left_on: bool = fposmod(t + l[5], l[3]) < l[4] or fposmod(t + 0.4 + l[5], l[3]) < l[4] or fposmod(t - 0.4 + l[5], l[3]) < l[4]
|
||||
var right_on: bool = fposmod(t + 1.5 + l[5], l[3]) < l[4] or fposmod(t + 1.9 + l[5], l[3]) < l[4] or fposmod(t + 1.1 + l[5], l[3]) < l[4]
|
||||
lane = 112.0 if left_on else 108.0
|
||||
if left_on and right_on:
|
||||
lane = -2.0 # обе полосы заняты — стоим
|
||||
if lane == -2.0:
|
||||
return
|
||||
if lane > 0.0:
|
||||
p.global_position.x = lane
|
||||
pos.x = lane
|
||||
tgt.x = lane
|
||||
var d: Vector3 = tgt - pos
|
||||
if tgt.y - pos.y > 2.0: # ворота выше (крыша башни): сначала поднимаемся вертикально — иначе упрёмся в стену
|
||||
d = Vector3(0, minf(step, tgt.y - pos.y + 1.5), 0)
|
||||
elif d.length() > step:
|
||||
d = d.normalized() * step
|
||||
p.global_position = pos + d
|
||||
p.velocity = Vector3.ZERO
|
||||
p.rotation.y = yaw
|
||||
|
||||
func _drive() -> void:
|
||||
if map_id == "race2":
|
||||
_drive_tele()
|
||||
return
|
||||
var pos: Vector3 = p.global_position
|
||||
if fr % 120 == 0 and OS.get_cmdline_user_args().has("--bot-log"):
|
||||
say("бег: wi=%d поз=(%.1f, %.1f, %.1f) v=(%.1f, %.1f) пол=%s" % [wi, pos.x, pos.y, pos.z, p.velocity.x, p.velocity.z, p.is_on_floor()])
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
# Игровая проверка режима «Гонка» на локальном сервере: 2 клиента-бота (+ третий, который заходит в разгар круга 1,
|
||||
# наблюдает, участвует в круге 2 и выходит посреди него). Окно не открывается.
|
||||
# tools/test/race_game_test.sh [рабочая_папка=/tmp/race_game]
|
||||
# MAP=race2 — то же для карты «Гонка 2» (бот проводит игроков через ворота телепортами: проверяются правила).
|
||||
# Печатает факты и итог «ПРОВЕРКА ПРОЙДЕНА / ПРОВАЛЕНА». Порт сервера 9080 должен быть свободен.
|
||||
# Настоящий рекорд (user://race_best.cfg) не трогается: рекорд ботов идёт в файлы в рабочей папке.
|
||||
set -uo pipefail
|
||||
SRC="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
W="${1:-/tmp/race_game}"
|
||||
MAP="${MAP:-race}"
|
||||
if [ "$MAP" = race2 ]; then SCENE=res://maps/race2/race2.tscn; MARG="--map=race2"; else SCENE=res://maps/race/race.tscn; MARG=""; fi
|
||||
rm -rf "$W"; mkdir -p "$W"
|
||||
"$SRC/tools/test/make_test_server.sh" "$W/server" >/dev/null
|
||||
"$SRC/tools/test/make_test_project.sh" "$W/project" >/dev/null
|
||||
@@ -19,7 +22,7 @@ SERVER=$!
|
||||
sleep 2
|
||||
run() { # имя, остальные аргументы бота
|
||||
local n="$1"; shift
|
||||
timeout 420 godot --headless --path "$W/project" res://maps/race/race.tscn -- --client --race-log --race-best="$W/best_$n.cfg" --profile="$W/prof_$n.cfg" --name="$n" "$@" > "$W/$n.log" 2>&1 &
|
||||
timeout 420 godot --headless --path "$W/project" $SCENE -- --client $MARG --race-log --race-best="$W/best_$n.cfg" --profile="$W/prof_$n.cfg" --name="$n" "$@" > "$W/$n.log" 2>&1 &
|
||||
}
|
||||
run A --expect=2 --rounds=2 --die-at-gate=3
|
||||
PA=$!
|
||||
|
||||
Reference in New Issue
Block a user