Compare commits

...
2 Commits
Author SHA1 Message Date
aibrashandClaude Sonnet 5.5 556d3af615 README: заметки для будущего меня; тестовые сцены в tools/test
- Договорённости с владельцем, замеры мувсета (стенами высоту не набрать), исправление
  подтягивания, конвейер Parkour и его грабли, ловушка с ресурсами, загружаемыми по строке
- tools/test: measure_movement, mantle_test, parkour_gaps, parkour_tour, parkour_check.txt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1
2026-10-28 15:05:29 +05:00
aibrashandClaude Sonnet 5.5 af462364c0 Parkour: фоновая песня и исправление пакета карты
- Добавлена фоновая песня (Ogg Vorbis, моно 32 кГц, ~1 МБ вместо 8 МБ MP3), играет по кругу
  поверх шума дождя; оригинальный MP3 удалён
- Исправлен пресет экспорта Parkour: ресурсы, которые скрипт грузит по строке (sky.png,
  baked.gdshader, parkour_data.gd, audio/*), не попадали в parkour.pck — на сайте карта
  оставалась без неба, шейдера и данных раскладки. Теперь они включены (пакет 3,1 -> 7,4 МБ)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1
2026-10-28 15:02:26 +05:00
18 changed files with 360 additions and 3 deletions
+36 -2
View File
@@ -85,7 +85,8 @@ maps/ у каждой карты своя папка — он
hub/ стартовая комната: hub.tscn, hub.gd (порталы строятся из списка PORTALS) hub/ стартовая комната: hub.tscn, hub.gd (порталы строятся из списка PORTALS)
sandbox/ песочница: sandbox.tscn, forest.gd, birds.gd, levitate.gd, nav_bake.gd, sandbox/ песочница: sandbox.tscn, forest.gd, birds.gd, levitate.gd, nav_bake.gd,
floor/ sky/ audio/ models/ (здания, башня, обелиск, деревья, Эмпайр-стейт…) floor/ sky/ audio/ models/ (здания, башня, обелиск, деревья, Эмпайр-стейт…)
parkour/ parkour.tscn, parkour.gd, parkour.glb (трасса из Blender, см. «Карты и порталы») 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 (запечённый навмеш), backrooms/ backrooms.tscn, backrooms.gd, backrooms_nav.res (запечённый навмеш),
model/ (готовая модель «Backrooms VR» + 27 извлечённых текстур + CREDITS.md) model/ (готовая модель «Backrooms VR» + 27 извлечённых текстур + CREDITS.md)
deploy/ боевой конфиг nginx, systemd-юнит, project.godot выделенного сервера deploy/ боевой конфиг nginx, systemd-юнит, project.godot выделенного сервера
@@ -110,7 +111,7 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (
| Хаб | `maps/hub/hub.tscn` | в основном пакете | Комната 28×18 м, строится кодом (пол-грид, стены, свет). Порталы SANDBOX и BACKROOMS в ряд по центру, влезает 6. Безопасна, преследователей нет | | Хаб | `maps/hub/hub.tscn` | в основном пакете | Комната 28×18 м, строится кодом (пол-грид, стены, свет). Порталы SANDBOX и BACKROOMS в ряд по центру, влезает 6. Безопасна, преследователей нет |
| Песочница | `maps/sandbox/sandbox.tscn` | `world.pck` | Киберпанк-мир: пол-шахматка, здания, 60 деревьев (MultiMesh), птицы, парящий обелиск, звук `tron_01`. Преследователи: **камень** и **Верити**. Портал «HUB» возвращает в хаб | | Песочница | `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» в дальнем конце ведёт в хаб. Гул ламп генерируется кодом | | Backrooms | `maps/backrooms/backrooms.tscn` | `backrooms.pck` | Модель «Backrooms VR» (×1,4). Преследователь: **Верити** (масштаб 0,5). Выход «EXIT» в дальнем конце ведёт в хаб. Гул ламп генерируется кодом |
| Parkour | `maps/parkour/parkour.tscn` | `parkour.pck` (3,4 МБ) | Мрачная антиутопия в духе Ghostrunner: дождь, дымный воздух, грязный мокрый бетон, натриевый оранжевый, тревожный красный и больной зелёный свет, надписи-приказы (COMPLY, CURFEW 22:00, RESTRICTED). Подъём по мегабашне, ~700 м, 9 чекпоинтов (светящиеся ворота), без преследователей. Секции: крыши (одинарный/двойной прыжок, подтягивание) → каньон из четырёх стен для бега → башня из 9 уступов (двойной прыжок + подтягивание) → лазерный зал (подкат, прыжок, тайминг, рывок) → пропасть из 6 контейнеров (зазоры 16–24 м) → спираль вокруг светящегося ядра (+24 м) → вершина с порталом в хаб. Срыв ниже последнего чекпоинта на 32 м или касание лазера = «СОРВАЛСЯ!» и возрождение на чекпоинте | | Parkour | `maps/parkour/parkour.tscn` | `parkour.pck` (7,4 МБ: небо 3,5 + модель 3,4 + музыка 1,1) | Мрачная антиутопия в духе Ghostrunner: дождь, дымный воздух, грязный мокрый бетон, натриевый оранжевый, тревожный красный и больной зелёный свет, надписи-приказы (COMPLY, CURFEW 22:00, RESTRICTED). Подъём по мегабашне, ~700 м, 9 чекпоинтов (светящиеся ворота), без преследователей. Секции: крыши (одинарный/двойной прыжок, подтягивание) → каньон из четырёх стен для бега → башня из 9 уступов (двойной прыжок + подтягивание) → лазерный зал (подкат, прыжок, тайминг, рывок) → пропасть из 6 контейнеров (зазоры 16–24 м) → спираль вокруг светящегося ядра (+24 м) → вершина с порталом в хаб. Срыв ниже последнего чекпоинта на 32 м или касание лазера = «СОРВАЛСЯ!» и возрождение на чекпоинте. Музыка: `maps/parkour/audio/music.ogg` (Ogg Vorbis, моно 32 кГц, ~1 МБ; делается `ffmpeg -i исходник.mp3 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 music.ogg`), играет по кругу |
**Правка и пересборка Parkour** (всё в `Blender models/parkour_02/`): **Правка и пересборка Parkour** (всё в `Blender models/parkour_02/`):
1. `layout.py` — единственный источник: плиты, стены, лазеры, чекпоинты и расчёт зазоров под мувсет (замеры ботом: прыжок на спринте 15 м, двойной 25 м, прыжок+рывок 19 м, двойной+рывок 28 м, бег по стене 1,8 с / 23 м, подтягивание на уступ до ~2,6 м; **стенами высоту не набрать** — прыжков от стены без бега только 2 за полёт). 1. `layout.py` — единственный источник: плиты, стены, лазеры, чекпоинты и расчёт зазоров под мувсет (замеры ботом: прыжок на спринте 15 м, двойной 25 м, прыжок+рывок 19 м, двойной+рывок 28 м, бег по стене 1,8 с / 23 м, подтягивание на уступ до ~2,6 м; **стенами высоту не набрать** — прыжков от стены без бега только 2 за полёт).
@@ -194,6 +195,7 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ (
- **`download_file` в вебе не создаёт файл** — тело берём в память и пишем в `user://` сами. - **`download_file` в вебе не создаёт файл** — тело берём в память и пишем в `user://` сами.
- Файлы-«не ресурсы» (`.mp4`, `.md`) не экспортируются; потому исходное видео Верити лежит вне проекта (`source-assets`). - Файлы-«не ресурсы» (`.mp4`, `.md`) не экспортируются; потому исходное видео Верити лежит вне проекта (`source-assets`).
- Предупреждения `invalid UID … using text path instead` при загрузке пакета безвредны. - Предупреждения `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` сцен перед экспортом. Признак беды: размер пакета не вырос после добавления ресурсов. `tools/export_all.sh` делает `touch` сцен перед экспортом. Признак беды: размер пакета не вырос после добавления ресурсов.
@@ -231,6 +233,12 @@ godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url
Для картинок: `godot --path . --resolution 1152x648 --fixed-fps 10 --write-movie /tmp/shots/a.png --quit-after 25 res://maps/hub/hub.tscn` (окно мелькнёт; кадры — `a00000020.png`). Для картинок: `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`. В браузере: Chrome (flatpak `com.google.Chrome`) `--headless=new --enable-logging=stderr --v=0 http://127.0.0.1:8080` — вывод Godot виден в строках `CONSOLE`.
**Готовые проверки** (`tools/test/`, запускаются как сцены: в режиме `--script` автозагрузки недоступны при компиляции, поэтому только `res://…tscn`; `-- --client`, иначе headless поднимет сервер на порту 9080):
- `measure_movement.tscn` — замеры мувсета (прыжки, рывок, стена); `mantle_test.tscn` — подтягивание на 40 вариантах уступов; `parkour_gaps.tscn` — проходимость контейнеров; `parkour_tour.tscn` — облёт карты со снимками (см. заголовок файла).
- **Проверяй по существу, а не по имени сцены.** Для карт из пакетов: собери тестовый проект (`make_test_project.sh`), в его `bot.gd` добавь печать фактов (есть ли у `current_scene` скрипт, подменён ли материал, есть ли `PanoramaSkyMaterial`, играет ли музыка, сколько лазеров) и пройди портал с `--pack-url`. Готовый код проверки — `tools/test/parkour_check.txt` (ловит пропавшие `sky.png`/шейдер/музыку/логику).
- Ввод ботом: `Input.action_press("jump")` работает для зажатия и `is_action_pressed`; рывок и подкат ловятся в `_unhandled_input`, поэтому для них нужно `Input.parse_input_event(InputEventAction)` с `pressed=true`.
- Грабли оболочки: в этом окружении `ls` — алиас на `eza` (используй `command ls`), `cp` спрашивает подтверждение (`command cp -f`), zsh не режет `$args` по пробелам (передавай аргументы отдельно), `echo ======` ломает команду.
**Осторожно:** `pkill -f`/`pgrep -f` с шаблоном, который есть и в твоей же командной строке, убивает собственный шелл; не используй `pkill chrome` — закроет браузер владельца. Останавливай по PID или по точному признаку. **Осторожно:** `pkill -f`/`pgrep -f` с шаблоном, который есть и в твоей же командной строке, убивает собственный шелл; не используй `pkill chrome` — закроет браузер владельца. Останавливай по PID или по точному признаку.
### Деплой ### Деплой
@@ -276,6 +284,32 @@ ssh root@192.168.10.110 'journalctl -u my-world -f' # лог: «+ игрок
- **Ввод:** `Input.is_key_pressed(KEY_W)` проверяет символ, на русской раскладке это «Ц». Только действия `InputMap` на физические клавиши. - **Ввод:** `Input.is_key_pressed(KEY_W)` проверяет символ, на русской раскладке это «Ц». Только действия `InputMap` на физические клавиши.
- **Пресеты:** при отключённом `extensions_support` — один `index.wasm`; включение даёт ещё `index.side.wasm` 44 МБ. - **Пресеты:** при отключённом `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 смотри только геометрию.
## Дорожная карта ## Дорожная карта
### Цель: играть с телефона ### Цель: играть с телефона
+1 -1
View File
@@ -159,7 +159,7 @@ dedicated_server=false
custom_features="" custom_features=""
export_filter="resources" export_filter="resources"
export_files=PackedStringArray("res://maps/parkour/parkour.tscn") export_files=PackedStringArray("res://maps/parkour/parkour.tscn")
include_filter="" include_filter="maps/parkour/sky.png, maps/parkour/baked.gdshader, maps/parkour/parkour_data.gd, maps/parkour/audio/*"
exclude_filter="player/*, objects/*, chasers/chaser.gd" exclude_filter="player/*, objects/*, chasers/chaser.gd"
export_path="../grid-world/parkour.pck" export_path="../grid-world/parkour.pck"
patches=PackedStringArray() patches=PackedStringArray()
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://bwtxmetryrkq8"
path="res://.godot/imported/music.ogg-56b8a9b1be163a3e8c872255da33f0f9.oggvorbisstr"
[deps]
source_file="res://maps/parkour/audio/music.ogg"
dest_files=["res://.godot/imported/music.ogg-56b8a9b1be163a3e8c872255da33f0f9.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
+11
View File
@@ -29,6 +29,7 @@ func _ready() -> void:
_add_laser(l) _add_laser(l)
_add_rain() _add_rain()
_add_ambience() _add_ambience()
_add_music()
_add_exit() _add_exit()
# Цвета поверхностей — те же значения, что в Blender (kit.py: SOLID); освещение уже запечено в цветах вершин # Цвета поверхностей — те же значения, что в Blender (kit.py: SOLID); освещение уже запечено в цветах вершин
@@ -334,3 +335,13 @@ func _add_ambience() -> void:
ap.volume_db = -13.0 ap.volume_db = -13.0
ap.autoplay = true ap.autoplay = true
add_child(ap) add_child(ap)
# Фоновая песня карты: Ogg Vorbis, моно 32 кГц (~1 МБ вместо 8 МБ исходного MP3), играет по кругу поверх шума дождя
func _add_music() -> void:
var stream := load("res://maps/parkour/audio/music.ogg") as AudioStreamOggVorbis
stream.loop = true
var ap := AudioStreamPlayer.new()
ap.stream = stream
ap.volume_db = -9.0
ap.autoplay = true
add_child(ap)
+51
View File
@@ -0,0 +1,51 @@
extends Node
# Подтягивание на уступы: тонкая полка и сплошной блок, коллизия «коробка» и «меш», ходьба и спринт, высоты 1,2–2,7 м.
# Запуск: godot --headless --path . res://tools/test/mantle_test.tscn -- --client (ожидание: везде «ДА»)
var root3d: Node3D
var p
# kind: "thin" (полка 0.6 м, под ней пусто) или "block" (сплошной блок от пола); shape: "box" или "mesh"
func build(kind: String, shape: String, h: float) -> void:
if root3d: root3d.queue_free()
await get_tree().physics_frame
root3d = Node3D.new(); add_child(root3d)
var sb := StaticBody3D.new(); root3d.add_child(sb)
var fl := CollisionShape3D.new(); var fb := BoxShape3D.new(); fb.size = Vector3(60, 1, 60); fl.shape = fb; fl.position = Vector3(0, -0.5, 0); sb.add_child(fl)
var size := Vector3(10, 0.6, 12) if kind == "thin" else Vector3(10, h, 12)
var cy := h - 0.3 if kind == "thin" else h * 0.5
var cs := CollisionShape3D.new()
if shape == "box":
var b := BoxShape3D.new(); b.size = size; cs.shape = b
else:
var m := BoxMesh.new(); m.size = size; cs.shape = m.create_trimesh_shape()
cs.position = Vector3(0, cy, -8 - size.z * 0.5) # передняя грань на z=-8
sb.add_child(cs)
func trial(kind: String, shape: String, h: float, sprint: bool) -> String:
await build(kind, shape, h)
p = (load("res://player/player.tscn") as PackedScene).instantiate()
root3d.add_child(p)
p.global_position = Vector3(0, 1.17, 0)
await get_tree().physics_frame
Input.action_press("move_forward")
if sprint: Input.action_press("sprint")
var f := 0; var jumped := false; var ok := false
while f < 420:
await get_tree().physics_frame; f += 1
var dz: float = p.global_position.z + 8.0 # до грани
if not jumped and dz < (-1.0 if not sprint else 0.0) + 2.4 and p.is_on_floor() and f > 30:
jumped = true; Input.action_press("jump")
if jumped and p.is_on_floor() and p.global_position.z < -8.2 and absf(p.global_position.y - (h + 1.16)) < 0.35:
ok = true; break
for a in ["move_forward","sprint","jump"]: Input.action_release(a)
var res := "ДА " if ok else "нет"
p.queue_free()
return res
func _ready() -> void:
print("высота уступа: 1.2 1.6 2.0 2.4 2.7")
for kind in ["thin", "block"]:
for shape in ["box", "mesh"]:
for sprint in [false, true]:
var row := "%-5s %-4s %-6s " % [kind, shape, "спринт" if sprint else "ходьба"]
for h in [1.2, 1.6, 2.0, 2.4, 2.7]:
row += await trial(kind, shape, h, sprint) + " "
print(row)
get_tree().quit()
+1
View File
@@ -0,0 +1 @@
uid://c1vd8asub3ox6
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://tools/test/mantle_test.gd" id="1"]
[node name="T" type="Node"]
script = ExtResource("1")
+98
View File
@@ -0,0 +1,98 @@
extends Node
# Замеры мувсета ботом: дальность прыжка / двойного / рывка на спринте, бег по стене, прыжок от стены.
# Запуск: godot --headless --path . res://tools/test/measure_movement.tscn -- --client (~1 мин, печатает таблицу)
# Замеры мувсета: дальность прыжков, рывок, бег по стене, прыжок от стены
var root3d: Node3D
var p
func world(with_floor: bool, wall: bool) -> void:
if root3d: root3d.queue_free()
await get_tree().physics_frame
root3d = Node3D.new(); add_child(root3d)
var sb := StaticBody3D.new(); root3d.add_child(sb)
if with_floor:
var cs := CollisionShape3D.new(); var b := BoxShape3D.new(); b.size = Vector3(400, 1, 800); cs.shape = b; cs.position = Vector3(0, -0.5, -300); sb.add_child(cs)
if wall:
var cs2 := CollisionShape3D.new(); var b2 := BoxShape3D.new(); b2.size = Vector3(1, 400, 800); cs2.shape = b2; cs2.position = Vector3(-4.5, 0, -300); sb.add_child(cs2)
var cs3 := CollisionShape3D.new(); var b3 := BoxShape3D.new(); b3.size = Vector3(1, 400, 800); cs3.shape = b3; cs3.position = Vector3(4.5, 0, -300); sb.add_child(cs3)
func spawn(pos: Vector3, vel := Vector3.ZERO) -> void:
p = (load("res://player/player.tscn") as PackedScene).instantiate()
root3d.add_child(p)
p.global_position = pos
p.velocity = vel
func press(a: String) -> void: Input.action_press(a)
func release(a: String) -> void: Input.action_release(a)
func tap_dash() -> void:
var e := InputEventAction.new(); e.action = "dash"; e.pressed = true
Input.parse_input_event(e)
func release_all() -> void:
for a in ["move_forward","move_left","move_right","sprint","jump","crouch","dash"]: Input.action_release(a)
func frames(n: int) -> void:
for i in n: await get_tree().physics_frame
# прыжок с разбега по ровному полу: variant: single / dj / dash / djdash / dashdj
func jump_exp(variant: String) -> void:
await world(true, false)
spawn(Vector3(0, 1.17, 0))
await frames(5)
press("move_forward"); press("sprint")
await frames(60)
var z0: float = p.global_position.z
press("jump")
var maxy: float = 0.0; var f := 0; var did2 := false; var did3 := false; var lz := 0.0
while f < 400:
await get_tree().physics_frame; f += 1
maxy = maxf(maxy, p.global_position.y - 1.17)
if not did2 and p.velocity.y <= 0.0 and not p.is_on_floor():
did2 = true
if variant in ["dj", "djdash"]:
release("jump"); await get_tree().physics_frame; press("jump")
elif variant in ["dash", "dashdj"]:
tap_dash()
if did2 and not did3 and f > 40 and variant in ["djdash", "dashdj"]:
did3 = true
if variant == "djdash": tap_dash()
else: release("jump"); await get_tree().physics_frame; press("jump")
if f > 10 and p.is_on_floor():
lz = p.global_position.z; break
release_all()
print("прыжок %-7s: дальность %.1f м, высота %.2f м, время %.2f с" % [variant, z0 - lz, maxy, f / 60.0])
p.queue_free()
# бег по стене: летим вдоль левой стены со скоростью v, стена слева
func wall_exp(v: float, jump_after: float, then: String) -> void:
await world(false, true)
spawn(Vector3(-3.4, 100, 0), Vector3(0, 0, -v))
press("move_forward")
var z0: float = p.global_position.z
var f := 0; var run_f := 0; var started := false; var y_start := 0.0; var z_start := 0.0; var jumped := false; var x_max := -99.0; var z_j := 0.0; var y_j := 0.0
var landed_x := 0.0; var landed_z := 0.0; var t_j := 0
while f < 500:
await get_tree().physics_frame; f += 1
if p.is_wallrunning:
if not started: started = true; y_start = p.global_position.y; z_start = p.global_position.z
run_f += 1
if started and not jumped and (run_f / 60.0) >= jump_after:
jumped = true; z_j = p.global_position.z; y_j = p.global_position.y; t_j = f
release("move_forward"); press("move_forward")
press("jump"); await get_tree().physics_frame; release("jump")
if then == "dash": await frames(15); tap_dash()
if then == "dj": await frames(25); press("jump"); await get_tree().physics_frame; release("jump")
if jumped:
x_max = maxf(x_max, p.global_position.x)
if p.global_position.y < y_j - 3.0:
landed_x = p.global_position.x; landed_z = p.global_position.z; break
if not jumped and started and not p.is_wallrunning and f > 30:
break
release_all()
if jump_after < 0: pass
print("стена v=%.0f: бежали %.2f с, %.1f м вдоль, провал по y %.2f м; прыжок от стены (после %.1fс, затем %s): отлёт от стены до x=%.1f (стена x=-4), вперёд %.1f м, за %.2f с" % [v, run_f / 60.0, (z_start - (z_j if jumped else p.global_position.z)), y_start - (y_j if jumped else p.global_position.y), jump_after, then, x_max, z_j - landed_z, (f - t_j) / 60.0])
p.queue_free()
func _ready() -> void:
for v in ["single", "dj", "dash", "djdash", "dashdj"]:
await jump_exp(v)
await wall_exp(13.0, 99.0, "none")
await wall_exp(13.0, 0.5, "none")
await wall_exp(13.0, 0.5, "dash")
await wall_exp(13.0, 0.5, "dj")
get_tree().quit()
+1
View File
@@ -0,0 +1 @@
uid://ddgc0duo4j3mq
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://tools/test/measure_movement.gd" id="1"]
[node name="T" type="Node"]
script = ExtResource("1")
+22
View File
@@ -0,0 +1,22 @@
# Проверка карты Parkour «по существу» после прохода через портал с настоящими пакетами.
# Вставь в tools/test/bot.gd ВРЕМЕННОЙ копии проекта (make_test_project.sh) перед строкой print("[%s] t=%4.1f сцена=...")
# и запусти: godot --headless --main-pack <site>/index.pck -- --client --name=T --pack-url=http://127.0.0.1:<порт>/ --tp=2:4.5,1,-8
# Ожидание: скрипт=true, шейдерных_мешей>=1, небо_панорама=true, музыка_играет=true, лазеров_и_ворот=10.
# Если в пакете не хватает файла, вместо этого будут false/0 (а имя сцены всё равно «Parkour»).
var sc := get_tree().current_scene
if sc != null and sc.name == "Parkour" 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 music := false
var areas := 0
for n in sc.get_children():
if n is AudioStreamPlayer and n.stream is AudioStreamOggVorbis and n.playing:
music = true
if n is Area3D and n.get_child_count() == 2:
areas += 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(" ПРОВЕРКА Parkour: скрипт=%s шейдерных_мешей=%d небо_панорама=%s музыка_играет=%s лазеров_и_ворот=%d" % [sc.get_script() != null, shaded, sky_ok, music, areas])
+68
View File
@@ -0,0 +1,68 @@
extends Node
# Проходимость пропасти контейнеров на настоящей карте Parkour: прыжок у края, двойной прыжок на спуске, подруливание к цели.
# Запуск: godot --headless --path . res://tools/test/parkour_gaps.tscn -- --client (ожидание: все зазоры «ДА»)
# Стартовые точки заданы в коде — после правки layout.py обнови их
var level
var p
var f := 0
# (имя, x, z_старта(центр), z_края, ожидаемая посадка z-диапазон, высота пола)
var cases := [
["k0->k1 (18 м)", 4.0, -465.0, -475.0, 45.0],
["k2->k3 (22 м)", 2.0, -544.0, -549.0, 48.0] ,
]
func _ready() -> void:
level = (load("res://maps/parkour/parkour.tscn") as PackedScene).instantiate()
add_child(level)
p = level.get_node("Player")
p.set_physics_process(false)
await get_tree().physics_frame
for variant in ["dj", "dash+dj"]:
# имя, x старта, пол старта, z старта, z края, пол цели, z цели ближний, дальний, x цели
for c in [
["exit->k1 18", 4.0, 44.0, -446.0, -457.7, 45.0, -476.0, -488.0, 5.0],
["k1->k2 16", 0.0, 45.0, -479.0, -487.7, 46.5, -504.0, -516.0, -4.0],
["k2->k3 22", -2.5, 46.5, -507.0, -515.7, 48.0, -538.0, -550.0, -2.5],
["k3->k4 24", 7.0, 48.0, -541.0, -549.7, 49.0, -574.0, -586.0, 7.5],
["k4->k5 20", 7.0, 49.0, -577.0, -585.7, 50.5, -606.0, -618.0, 2.0],
["k5->k6 22", 2.0, 50.5, -609.0, -617.7, 52.0, -640.0, -656.0, 0.0]]:
await trial(c, variant)
get_tree().quit()
func key(a: String, down: bool) -> void:
if down: Input.action_press(a)
else: Input.action_release(a)
func trial(c: Array, variant: String) -> void:
p.set_physics_process(true)
p.global_position = Vector3(c[1], c[2] + 1.18, c[3])
p.velocity = Vector3.ZERO
p.is_dead = false
await get_tree().physics_frame
key("move_forward", true); key("sprint", true)
var fr := 0; var jumped := false; var did := 0; var res := "промах"
while fr < 360:
await get_tree().physics_frame; fr += 1
if not jumped and p.is_on_floor() and p.global_position.z < c[4] + 1.0 and fr > 20:
jumped = true; key("jump", true); did = 0
if jumped and not p.is_on_floor(): # подруливание к центру цели
var dx: float = c[8] - p.global_position.x
key("move_left", dx < -0.6); key("move_right", dx > 0.6)
if jumped and did == 0 and not p.is_on_floor() and p.velocity.y < 0.0:
did = fr
if variant == "dj" or variant == "dj+dash": key("jump", false); await get_tree().physics_frame; key("jump", true)
if variant == "dash+dj":
var e := InputEventAction.new(); e.action = "dash"; e.pressed = true; Input.parse_input_event(e)
if jumped and did > 0 and variant == "dj+dash" and fr == did + 8:
var e2 := InputEventAction.new(); e2.action = "dash"; e2.pressed = true; Input.parse_input_event(e2)
if jumped and did > 0 and variant == "dash+dj" and fr == did + 14:
key("jump", false); await get_tree().physics_frame; key("jump", true)
if jumped and fr > did + 25 and did > 0 and p.is_on_floor():
var z: float = p.global_position.z
if z < c[6] + 0.5 and z > c[7] - 0.5 and absf(p.global_position.y - (c[5] + 1.16)) < 0.3:
res = "ДА (z=%.1f)" % z
else:
res = "упал/на старте (z=%.1f y=%.1f)" % [z, p.global_position.y]
break
if p.global_position.y < c[2] - 25.0:
res = "упал вниз"; break
for a in ["move_forward","sprint","jump","move_left","move_right"]: key(a, false)
print("%-8s %-10s %s" % [variant, c[0], res])
p.set_physics_process(false)
+1
View File
@@ -0,0 +1 @@
uid://cfy83ai6f6hw5
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://tools/test/parkour_gaps.gd" id="1"]
[node name="T" type="Node"]
script = ExtResource("1")
+26
View File
@@ -0,0 +1,26 @@
extends Node
# Облёт карты Parkour для скриншотов: телепорт игрока по точкам, кадры пишет Godot (--write-movie).
# Запуск (окно мелькнёт):
# godot --path . --resolution 1152x648 --fixed-fps 10 --write-movie /tmp/shots/p.png --quit-after 42 res://tools/test/parkour_tour.tscn -- --client
# Кадры: p00000005.png и т. д.; точки — массив spots (позиция в координатах Godot, рыскание, наклон головы)
var level
var p
var frame := 0
# позиция центра капсулы (Godot), рыскание в градусах, наклон головы в градусах
var spots := [
[Vector3(0, 1.25, 6), 0, 6], [Vector3(0, 11.0, -196), 0, 3], [Vector3(-3.4, 10.0, -222), 0, 0], [Vector3(2, 14.0, -310), 0, 20],
[Vector3(0, 46.0, -352), 0, 3], [Vector3(0, 46.0, -400), 0, 0], [Vector3(0, 49.0, -560), 0, 6], [Vector3(0, 76.0, -672), 0, 14],
]
func _ready() -> void:
level = (load("res://maps/parkour/parkour.tscn") as PackedScene).instantiate()
add_child(level)
p = level.get_node("Player")
p.set_physics_process(false)
func _process(_d: float) -> void:
frame += 1
var i := int(frame / 5)
if i < spots.size() and frame % 5 == 1:
var s = spots[i]
p.global_position = s[0]
p.rotation.y = deg_to_rad(s[1])
p.head.rotation.x = deg_to_rad(s[2])
+1
View File
@@ -0,0 +1 @@
uid://drhimdbliiox8
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://tools/test/parkour_tour.gd" id="1"]
[node name="T" type="Node"]
script = ExtResource("1")