From 556d3af6159a14d01309e8a818882e9d2f238905 Mon Sep 17 00:00:00 2001 From: aibrash Date: Wed, 28 Oct 2026 15:05:29 +0500 Subject: [PATCH] =?UTF-8?q?README:=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D1=83=D0=B4=D1=83=D1=89?= =?UTF-8?q?=D0=B5=D0=B3=D0=BE=20=D0=BC=D0=B5=D0=BD=D1=8F;=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=81=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=B2=20tools/test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Договорённости с владельцем, замеры мувсета (стенами высоту не набрать), исправление подтягивания, конвейер Parkour и его грабли, ловушка с ресурсами, загружаемыми по строке - tools/test: measure_movement, mantle_test, parkour_gaps, parkour_tour, parkour_check.txt Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1 --- README.md | 38 +++++++++++- tools/test/mantle_test.gd | 51 ++++++++++++++++ tools/test/mantle_test.gd.uid | 1 + tools/test/mantle_test.tscn | 6 ++ tools/test/measure_movement.gd | 98 ++++++++++++++++++++++++++++++ tools/test/measure_movement.gd.uid | 1 + tools/test/measure_movement.tscn | 6 ++ tools/test/parkour_check.txt | 22 +++++++ tools/test/parkour_gaps.gd | 68 +++++++++++++++++++++ tools/test/parkour_gaps.gd.uid | 1 + tools/test/parkour_gaps.tscn | 6 ++ tools/test/parkour_tour.gd | 26 ++++++++ tools/test/parkour_tour.gd.uid | 1 + tools/test/parkour_tour.tscn | 6 ++ 14 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 tools/test/mantle_test.gd create mode 100644 tools/test/mantle_test.gd.uid create mode 100644 tools/test/mantle_test.tscn create mode 100644 tools/test/measure_movement.gd create mode 100644 tools/test/measure_movement.gd.uid create mode 100644 tools/test/measure_movement.tscn create mode 100644 tools/test/parkour_check.txt create mode 100644 tools/test/parkour_gaps.gd create mode 100644 tools/test/parkour_gaps.gd.uid create mode 100644 tools/test/parkour_gaps.tscn create mode 100644 tools/test/parkour_tour.gd create mode 100644 tools/test/parkour_tour.gd.uid create mode 100644 tools/test/parkour_tour.tscn diff --git a/README.md b/README.md index cb830ef..d1790d2 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,8 @@ maps/ у каждой карты своя папка — он hub/ стартовая комната: hub.tscn, hub.gd (порталы строятся из списка PORTALS) sandbox/ песочница: sandbox.tscn, forest.gd, birds.gd, levitate.gd, nav_bake.gd, 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 (запечённый навмеш), model/ (готовая модель «Backrooms VR» + 27 извлечённых текстур + CREDITS.md) 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/sandbox/sandbox.tscn` | `world.pck` | Киберпанк-мир: пол-шахматка, здания, 60 деревьев (MultiMesh), птицы, парящий обелиск, звук `tron_01`. Преследователи: **камень** и **Верити**. Портал «HUB» возвращает в хаб | | Backrooms | `maps/backrooms/backrooms.tscn` | `backrooms.pck` | Модель «Backrooms VR» (×1,4). Преследователь: **Верити** (масштаб 0,5). Выход «EXIT» в дальнем конце ведёт в хаб. Гул ламп генерируется кодом | -| Parkour | `maps/parkour/parkour.tscn` | `parkour.pck` (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/`): 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://` сами. - Файлы-«не ресурсы» (`.mp4`, `.md`) не экспортируются; потому исходное видео Верити лежит вне проекта (`source-assets`). - Предупреждения `invalid UID … using text path instead` при загрузке пакета безвредны. +- **Ресурсы, которые скрипт грузит строкой** (`load("res://…")`), в пакет карты **не попадают**: пресет `export_filter=resources` берёт только сцену и её прямые зависимости (`preload` тоже находится). Добавляй такие файлы в `include_filter` пресета карты (у Parkour: `sky.png, baked.gdshader, parkour_data.gd, audio/*`). Признак беды: пакет «слишком маленький». Проверка: `strings parkour.pck | grep -c sky.png` (должно быть > 0). Раз это случилось: карта тихо грузится без неба, шейдера и логики, а проверка «сцена называется Parkour» этого не замечает — **проверяй по существу** (см. «Тесты»). - **Кэш зависимостей редактора** у недавно созданных/изменённых сцен может устареть, и экспорт пакета карты пропустит модели и звуки (пакет «слишком маленький»). `tools/export_all.sh` делает `touch` сцен перед экспортом. Признак беды: размер пакета не вырос после добавления ресурсов. @@ -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`). В браузере: 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 или по точному признаку. ### Деплой @@ -276,6 +284,32 @@ ssh root@192.168.10.110 'journalctl -u my-world -f' # лог: «+ игрок - **Ввод:** `Input.is_key_pressed(KEY_W)` проверяет символ, на русской раскладке это «Ц». Только действия `InputMap` на физические клавиши. - **Пресеты:** при отключённом `extensions_support` — один `index.wasm`; включение даёт ещё `index.side.wasm` 44 МБ. +## Заметки для будущего меня + +**Договорённости с владельцем (важно не нарушать)** +- Коммитить локально можно, **пушить и выкладывать — только по прямой просьбе**. Сервер выкладывать нужно лишь при правках `net/*` с изменением RPC. +- Parkour — **мрачная антиутопия** (дождь, грязный смог, мокрый бетон, натриевый оранжевый, красный, больной зелёный; надписи-приказы COMPLY/CURFEW). Яркий розово-бирюзовый неон владелец назвал «детским». Туман 0,011 выбран из серии проб (0,02 владелец откатил как слишком густой), цвет тумана светлее фона — тёмный туман лишь затемняет. +- Сложность Parkour владельцу «нормально»; он просил и «не детский уровень» — если захочет жёстче: зазоры 27–28 м (двойной прыжок + рывок), лазеры в каньоне, быстрее пульсы, реже чекпоинты. +- Сальто камеры при двойном прыжке делалось и **убрано по просьбе** (тошнит, не видно куда прыгать). Без просьбы не возвращать. +- Чекпоинты должны быть **заметными** (раньше тонкие кольца на полу не нашли): сейчас ворота 5 м с надписью CHECKPOINT. +- Мой стиль ответов: по-русски, коротко, без перечисления всего подряд; честно говорить, что проверено, а что нет. + +**Мувсет (замеры ботом, спринт 13 м/с)** — нужны при любом проектировании карт: +одиночный прыжок 15 м и +1,9 м вверх; двойной 25 м и +3,5 м; прыжок+рывок 19 м; двойной+рывок 28 м; бег по стене 1,8 с (23 м), провал 1,4 м; прыжок от стены ~5 м в сторону. **Стенами высоту не набрать**: прыжков от стены без бега только `MAX_WALL_JUMPS = 2` за полёт, а прыжок с бега по стене почти ничего не даёт — высоту дают двойной прыжок и подтягивание (уступ до ~2,6 м над полом, с двойным прыжком до ~4,5). Шахта-«лифт» из стен невозможна; в Parkour вместо неё башня из уступов. + +**Подтягивание** (`player.gd → _try_mantle`): цель — центр капсулы на `default_capsule_height * 0.5 + 0.05` над поверхностью уступа. Раньше стояло `+ 0.05` (центр на уровне поверхности): тело оказывалось наполовину в уступе, физика выталкивала назад, и на уступах от 2 м не работало (на всех картах!). Проверка — `mantle_test.tscn`. + +**Конвейер Parkour** (`Blender models/parkour_02/`): `layout.py` (единственный источник раскладки) → `build_parkour.py` (в Blender: геометрия `Solid`/`Neon`/`Collision-colonly`, пишет `parkour_data.gd`) → `bake.py` (`blender -b Parkour_02.blend -P bake.py`: запекание Cycles в цвета вершин + экспорт GLB, ~1 мин) → `build_sky.py` (`blender -b -P build_sky.py`: панорама ~2 мин) → `godot --headless --import --path .`. Правка раскладки требует всех четырёх шагов; по ходу собирай и смотри картинки (снимки `parkour_tour`). +- **Освещение — в цветах вершин**, а не в динамических источниках. Сетка граней режется на ячейки ≤ 1,6 м (`kit.Mesh.grid`), поэтому число треугольников растёт быстро: сейчас Solid ~50 тыс. четырёхугольников, Neon ~22 тыс. Фон и дно — грубая сетка (9–80 м). Не раздувай: веб. +- glTF хранит цвета вершин нормализованными (до 1), поэтому в `bake.py` значения сжаты ×0.125, а шейдер `baked.gdshader` умножает на `light_scale = 8`. Освещённость без этого обрезается на единице. +- Яркость уровня: сила света в `bake.py` (Moon/CityGlow/Smog и цвет мира) × `exposure` в `parkour.gd` (2,1) × цвет материала (`kit.SOLID` и `SURFACES` в `parkour.gd` — **должны совпадать**). Первые попытки были чёрными: средняя запечённая освещённость ~0,05 × альбедо 0,11 → почти ноль. +- Тексты (`kit.Mesh.text`): без поворота смотрит в −Y (читается, если смотреть вдоль +Y); для видимой с +X грани нужен `yaw = +π/2`, с −X — `−π/2`, для видимой со стороны +Y — `π`. Ошибка с ориентацией даёт зеркальные надписи. Кривые букв огрублены (`resolution_u = 2`) — иначе десятки тысяч лишних треугольников. +- Имена в русском интерфейсе Blender переведены: узлы шейдеров/мира искать **по `type`**, не по имени (`'Background'` не найдётся). +- Экспорт цветов вершин в GLB требует активного атрибута (`export_vertex_color='ACTIVE'`); Cycles-запекание в `target='VERTEX_COLORS'`. +- Панорама неба: мипы включены в `sky.png.import` (`mipmaps/generate=true`, `compress/mode=2`) — по ним размытые отражения в мокрых полах (`textureLod` в шейдере). Дно города в панораме — отдельный материал: общий шейдер дымки на огромной плоскости давал белое. +- Не мерялось: нагрузка Parkour в браузере (~145 тыс. треугольников + дождь 2200 частиц, небо ~6 МБ в видеопамяти), звук (шум дождя, гул, песня) на слух. +- Расходный ресурс: `mcp__blender__look` очень дорог по токенам (сотни тысяч за сессию) — снимки просматривай из Godot (`parkour_tour`), в Blender смотри только геометрию. + ## Дорожная карта ### Цель: играть с телефона diff --git a/tools/test/mantle_test.gd b/tools/test/mantle_test.gd new file mode 100644 index 0000000..c22176a --- /dev/null +++ b/tools/test/mantle_test.gd @@ -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() diff --git a/tools/test/mantle_test.gd.uid b/tools/test/mantle_test.gd.uid new file mode 100644 index 0000000..9e643f0 --- /dev/null +++ b/tools/test/mantle_test.gd.uid @@ -0,0 +1 @@ +uid://c1vd8asub3ox6 diff --git a/tools/test/mantle_test.tscn b/tools/test/mantle_test.tscn new file mode 100644 index 0000000..9e2613e --- /dev/null +++ b/tools/test/mantle_test.tscn @@ -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") diff --git a/tools/test/measure_movement.gd b/tools/test/measure_movement.gd new file mode 100644 index 0000000..ec00660 --- /dev/null +++ b/tools/test/measure_movement.gd @@ -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() diff --git a/tools/test/measure_movement.gd.uid b/tools/test/measure_movement.gd.uid new file mode 100644 index 0000000..d86aa5d --- /dev/null +++ b/tools/test/measure_movement.gd.uid @@ -0,0 +1 @@ +uid://ddgc0duo4j3mq diff --git a/tools/test/measure_movement.tscn b/tools/test/measure_movement.tscn new file mode 100644 index 0000000..dc6ccc1 --- /dev/null +++ b/tools/test/measure_movement.tscn @@ -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") diff --git a/tools/test/parkour_check.txt b/tools/test/parkour_check.txt new file mode 100644 index 0000000..7dab34f --- /dev/null +++ b/tools/test/parkour_check.txt @@ -0,0 +1,22 @@ +# Проверка карты Parkour «по существу» после прохода через портал с настоящими пакетами. +# Вставь в tools/test/bot.gd ВРЕМЕННОЙ копии проекта (make_test_project.sh) перед строкой print("[%s] t=%4.1f сцена=...") +# и запусти: godot --headless --main-pack /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]) diff --git a/tools/test/parkour_gaps.gd b/tools/test/parkour_gaps.gd new file mode 100644 index 0000000..b4b14c3 --- /dev/null +++ b/tools/test/parkour_gaps.gd @@ -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) diff --git a/tools/test/parkour_gaps.gd.uid b/tools/test/parkour_gaps.gd.uid new file mode 100644 index 0000000..9a777fe --- /dev/null +++ b/tools/test/parkour_gaps.gd.uid @@ -0,0 +1 @@ +uid://cfy83ai6f6hw5 diff --git a/tools/test/parkour_gaps.tscn b/tools/test/parkour_gaps.tscn new file mode 100644 index 0000000..4bc105b --- /dev/null +++ b/tools/test/parkour_gaps.tscn @@ -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") diff --git a/tools/test/parkour_tour.gd b/tools/test/parkour_tour.gd new file mode 100644 index 0000000..dc052e5 --- /dev/null +++ b/tools/test/parkour_tour.gd @@ -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]) diff --git a/tools/test/parkour_tour.gd.uid b/tools/test/parkour_tour.gd.uid new file mode 100644 index 0000000..abbe202 --- /dev/null +++ b/tools/test/parkour_tour.gd.uid @@ -0,0 +1 @@ +uid://drhimdbliiox8 diff --git a/tools/test/parkour_tour.tscn b/tools/test/parkour_tour.tscn new file mode 100644 index 0000000..86efcb6 --- /dev/null +++ b/tools/test/parkour_tour.tscn @@ -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")