Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
556d3af615 | ||
|
|
af462364c0 |
@@ -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 смотри только геометрию.
|
||||
|
||||
## Дорожная карта
|
||||
|
||||
### Цель: играть с телефона
|
||||
|
||||
+1
-1
@@ -159,7 +159,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="resources"
|
||||
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"
|
||||
export_path="../grid-world/parkour.pck"
|
||||
patches=PackedStringArray()
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -29,6 +29,7 @@ func _ready() -> void:
|
||||
_add_laser(l)
|
||||
_add_rain()
|
||||
_add_ambience()
|
||||
_add_music()
|
||||
_add_exit()
|
||||
|
||||
# Цвета поверхностей — те же значения, что в Blender (kit.py: SOLID); освещение уже запечено в цветах вершин
|
||||
@@ -334,3 +335,13 @@ func _add_ambience() -> void:
|
||||
ap.volume_db = -13.0
|
||||
ap.autoplay = true
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://c1vd8asub3ox6
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://ddgc0duo4j3mq
|
||||
@@ -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")
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfy83ai6f6hw5
|
||||
@@ -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")
|
||||
@@ -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])
|
||||
@@ -0,0 +1 @@
|
||||
uid://drhimdbliiox8
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user