diff --git a/README.md b/README.md index 1283459..c7a4116 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ ( | Backrooms | `maps/backrooms/backrooms.tscn` | `backrooms.pck` | Модель «Backrooms VR» (×1,4). Преследователь: **Верити** (масштаб 0,5). Выход «EXIT» в дальнем конце ведёт в хаб. Гул ламп генерируется кодом | | Parkour | `maps/parkour/parkour.tscn` | `parkour.pck` (7,4 МБ: небо 3,5 + модель 3,4 + музыка 1,1) | Мрачная антиутопия в духе Ghostrunner: дождь, дымный воздух, грязный мокрый бетон, натриевый оранжевый, тревожный красный и больной зелёный свет, надписи-приказы (COMPLY, CURFEW 22:00, RESTRICTED). **Забег на время** (старт — линия на краю стартовой платформы, финиш — вершина; рейтинг на сервере; R — заново). Подъём по мегабашне, ~700 м, 9 чекпоинтов (светящиеся ворота), без преследователей. Секции: крыши (одинарный/двойной прыжок, подтягивание) → каньон из четырёх стен для бега → башня из 9 уступов (двойной прыжок + подтягивание) → лазерный зал (подкат, прыжок, тайминг, рывок) → пропасть из 6 контейнеров (зазоры 16–24 м) → спираль вокруг светящегося ядра (+24 м) → вершина с порталом в хаб. Срыв ниже последнего чекпоинта на 32 м или касание лазера = «СОРВАЛСЯ!» и возрождение на чекпоинте. Музыка: `maps/parkour/audio/music.ogg` (Ogg Vorbis, моно 32 кГц, ~1 МБ; делается `ffmpeg -i исходник.mp3 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 music.ogg`), играет по кругу | | Race | `maps/race/race.tscn` | `race.pck` | Гонка на ногах до 8 игроков (кто быстрее до финиша), мрачный город: стартовая площадка → рынок → эстакада → крыши → каньон → ангар. 8 ворот-чекпоинтов, 2 лазера, развилки с обходами. Правила и сеть — раздел «Карта Race» ниже | -| Race 2 | `maps/race2/race2.tscn` | `race2.pck` (8,8 МБ) | «Верхние ярусы»: гонка по крышам и мостам над городом, 13 ворот, ~1,2 км безопасного пути; на каждом участке безопасный / рискованный / секретный путь; вентиляторы-подбросы и ускоряющие полосы. Правила те же, что у Race (общий код). Раздел «Карта Race 2» | +| Race 2 | `maps/race2/race2.tscn` | `race2.pck` (9,5 МБ) | «Верхние ярусы»: гонка по крышам и мостам над городом, 13 ворот, ~1,2 км безопасного пути; на каждом участке безопасный / рискованный / секретный путь; вентиляторы-подбросы и ускоряющие полосы. Правила те же, что у Race (общий код). Раздел «Карта Race 2» | **Правка и пересборка Parkour** (всё в `Blender models/parkour_02/`): 1. `layout.py` — единственный источник: плиты, стены, лазеры, чекпоинты и расчёт зазоров под мувсет (замеры ботом: прыжок на спринте 15 м, двойной 25 м, прыжок+рывок 19 м, двойной+рывок 28 м, бег по стене 1,8 с / 23 м, подтягивание на уступ до ~2,6 м; **стенами высоту не набрать** — прыжков от стены без бега только 2 за полёт). @@ -197,17 +197,17 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ ( ## Карта Race 2 («Верхние ярусы») -**Состояние: фаза 1** (раскладка, геометрия, ворота, правила, проходимость ботами, предварительный свет). **Фаза 2** — после одобрения владельцем: чистовое запекание (384+ сэмплов), своё небо (`build_sky.py`), своя музыка. Пока небо и песня — копии Race. +**Состояние: фаза 1** (раскладка, геометрия, ворота, правила, проходимость ботами, предварительный свет). **Фаза 2** — после одобрения владельцем: чистовое запекание (384+ сэмплов), своё небо (`build_sky.py`), своя музыка. Палитра своя (холодная индиго-ночь, пурпур и ледяная бирюза; ворота тусклые индиго / очередные маджента / пройденные циан), небо перерисовано (`build_sky.py`), свет запечён на 384 сэмплах. Музыка — «On My Way to Hell» (Poloz), с 1:40 исходника (`ffmpeg -ss 100 -i исходник.mp3 -vn -map_metadata -1 -ac 1 -ar 32000 -c:a libvorbis -q:a 0 maps/race2/audio/music.ogg`, ~3:40, 1,2 МБ; исходник вне проекта: `source-assets/music/`); играет с «СТАРТ!» с начала по кругу (`loop = true`, затухание 0,8 с). Стык петли на слух/по уровню не проверен. Исходники: `Blender models/race_02/` (README там же; репозитория у папки нет — файлы не коммитятся). Концепция согласована с владельцем: вертикальность и поток скорости в духе Ghostrunner, но атмосфера как у Race/Parkour (дождь, смог, мокрый бетон, натриевый/красный/больной зелёный свет, надписи-приказы), туман 0,011. Файлы: `maps/race2/` — `race2.tscn`, `race2.gd` (внешний вид, ворота со световыми столбами, лазеры, вентиляторы, полосы, срыв, дождь без дождя в траншее и шахте, гул, портал), `race2_data.gd` (**генерируется** `build_race.py`, не править), `race2.glb`, `sky.png`, `baked.gdshader`, `audio/music.ogg`. **Правила — общие с Race:** `race2.gd` создаёт тот же `maps/race/race_rules.gd` и передаёт ему `data` (= `race2_data.gd`) и `rating_id` (= `"race2"`); `race_rules.gd` берёт данные у карты -(`m.get("data")`, у Race — запасной `load` его `race_data.gd`). Пакет `race2.pck` (8,8 МБ) собирается пресетом **Race2** (в нём `include_filter`: `maps/race/race_rules.gd` и файлы карты); скрипты `objects/vent.gd` и `objects/boost_strip.gd` — в **основном** пакете. +(`m.get("data")`, у Race — запасной `load` его `race_data.gd`). Пакет `race2.pck` (9,5 МБ) собирается пресетом **Race2** (в нём `include_filter`: `maps/race/race_rules.gd` и файлы карты); скрипты `objects/vent.gd` и `objects/boost_strip.gd` — в **основном** пакете. Рейтинг: `Net.TIMED_MAPS["race2"] = {"min": 44}` (≈ 60 % от лучшего круга ботом по всем секретам ~72 с), `Net.MAP_TITLES["race2"] = "Гонка 2"`; в хабе портал «RACE 2» пятым в ряду. **Новые объекты** (локальный эффект на игроке, сетевой код не тронут; в `player/player.gd` — только хуки `apply_vent()` и `apply_boost()` и константы `VENT_ACCEL`, `VENT_MAX_RISE`, `BOOST_ACCEL`, `EFFECT_HOLD`; остальная физика и `MAX_SPEED = 22` не менялись): -- **Вентиляционный поток** (`objects/vent.gd`): пока игрок в столбе, вертикальная скорость растёт до 11 м/с (ускорение 60 м/с²), горизонтальная сохраняется, двойной прыжок возвращается. Столб шириной 6 м даёт подъём около 12 м на ступень (ступени башни — 12,5 м, с двойным прыжком). Визуал: тёмная решётка со светящимися щелями, мерцающий зелёный столб, слабый свет. -- **Ускоряющая полоса** (`objects/boost_strip.gd`): набор скорости вдоль полосы до 22 м/с (не выше `MAX_SPEED`), потом на земле она гасится как обычно (14 м/с²), в воздухе сохраняется. Измерено: на 22 м/с одиночный прыжок 25 м, двойной 42 м; **рывок сбрасывает скорость до 13**. Стрелки бирюзовые, бегут по ходу (жёлтые статичные — подсказки трассы). +- **Вентиляционный поток** (`objects/vent.gd`): пока игрок в столбе, вертикальная скорость растёт до **16 м/с** (ускорение 80 м/с²; раньше 11 и 60), горизонтальная сохраняется, двойной прыжок возвращается. Выход из столба на 16 м/с даёт ещё ~13 м высоты (v²/2g), поэтому ступень башни 12,5 м берётся с перелётом ~3 м; ступени 14–15 м в пробах ботом вели к промахам и оставлены 12,5. Шахта-лифт — скрытый поток (`hidden`: без столба, щелей и света), над окном знака нет. Визуал: тёмная решётка со светящимися щелями, мерцающий зелёный столб, слабый свет. +- **Ускоряющая полоса** (`objects/boost_strip.gd`): набор скорости вдоль полосы до 22 м/с (не выше `MAX_SPEED`), потом на земле она гасится как обычно (14 м/с²), в воздухе сохраняется. Измерено: на 22 м/с одиночный прыжок 25 м, двойной 42 м; **рывок сбрасывает скорость до 13**. Стрелки пурпурно-белые, бегут по ходу (статичные ледяные — подсказки трассы). **Участки и три пути** (подробности и координаты — `Blender models/race_02/README.md`, `layout.py`): старт на верхней платформе (8 слотов, шаг 2,8 м) → 2) мосты и крыши между башнями → 3) фасады: бег по четырём стенам → 4) площадь с краном → 5) пульсирующие лазерные мосты → 6) спуск по крышам → 7) башня-спираль из четырёх вентиляторов; на крыше ядра — финиш и портал. Безопасный путь 1085 м до подножия башни + 136 м башня = **1221 м** (план ~1100; можно сжать зоны 4 и 6), 13 ворот (ширина до 100 м там, где ветки расходятся широко), круг бота по безопасному пути ~99 с. @@ -228,7 +228,7 @@ tools/ export_all.sh, serve_web.py, deploy_server.sh, test/ ( Итого все секреты вместе экономят ~26 с из ~99 (оценка бота; человек на секретах рискует сорваться на последние ворота). **Правила на race2:** `MAP=race2 tools/test/race_game_test.sh` (бот проводит игроков через ворота телепортами по 0,35 м за кадр, на лазерном мосту выбирает полосу — проверяются отсчёт, места, итоги, один START, возрождение лицом по трассе, зритель; проходимость проверена отдельно). Рейтинг этим тестом не проверяется: круг бота ~60 с — запись возможна, но тест рейтинг не сверяет. Пакет: `godot --headless --export-pack Race2 /tmp/x/race2.pck`, затем `strings` по `sky.png`, `baked.gdshader`, `race2_data`, `race_rules.gd`, `music.ogg`, `race2.glb`, `race2.gd` (каждое > 0; vent/boost_strip в этом пакете нет — они в основном). Через портал хаба: `tools/test/race2_check.txt`. -**Что не сделано / известно:** свет запечён грубо (пятна), небо и музыка от Race; `race2.glb` 8,4 МБ против 5,2 у Race (в фазе 2 прореживать фон); у зон 1 и 7 нет рискованной ветки; зазор для безопасного пути на спуске к `lane` — 8 м при спуске 9,2 м; не мерялись: нагрузка в браузере и звук. **Выкладка:** `net/net.gd` изменён только константами (`TIMED_MAPS`, `MAP_TITLES`), RPC те же — но сервер без нового `net.gd` **не примет рекорды race2** (запись отклонит: карты нет в `TIMED_MAPS`), поэтому после пуша `grid-world` запустить `tools/deploy_server.sh`; порядок и бэкап — как в «Деплой». +**Что не сделано / известно:** после смены силы вентиляторов (11 -> 16 м/с) проходимость башни и траншеи проверена ботом только выборочно (cmp7_safe, secret7, cmp4_secret, risky4 прошли), полный прогон и таблица времён выше — до этой правки, не перемерены; подножие башни всё ещё тёмное; `race2.glb` 8,4 МБ против 5,2 у Race (в фазе 2 прореживать фон); у зон 1 и 7 нет рискованной ветки; зазор для безопасного пути на спуске к `lane` — 8 м при спуске 9,2 м; не мерялись: нагрузка в браузере и звук. **Выкладка:** `net/net.gd` изменён только константами (`TIMED_MAPS`, `MAP_TITLES`), RPC те же — но сервер без нового `net.gd` **не примет рекорды race2** (запись отклонит: карты нет в `TIMED_MAPS`), поэтому после пуша `grid-world` запустить `tools/deploy_server.sh`; порядок и бэкап — как в «Деплой». ## Ники и рейтинг diff --git a/maps/race2/audio/music.ogg b/maps/race2/audio/music.ogg index 2ed82f8..30b94e4 100644 Binary files a/maps/race2/audio/music.ogg and b/maps/race2/audio/music.ogg differ diff --git a/maps/race2/race2.gd b/maps/race2/race2.gd index 7dc1c48..751d538 100644 --- a/maps/race2/race2.gd +++ b/maps/race2/race2.gd @@ -48,16 +48,16 @@ func _ready() -> void: # Цвета поверхностей — те же значения, что в Blender (kit.py: SOLID); освещение уже запечено в цветах вершин const SURFACES := { - "Concrete": Color(0.11, 0.115, 0.125), "Dark": Color(0.016, 0.017, 0.02), "Steel": Color(0.13, 0.14, 0.15), - "PaintY": Color(0.38, 0.30, 0.06), "PaintR": Color(0.26, 0.08, 0.05), "PaintB": Color(0.05, 0.08, 0.11), - "PaintO": Color(0.33, 0.14, 0.05), + "Concrete": Color(0.095, 0.105, 0.15), "Dark": Color(0.012, 0.013, 0.024), "Steel": Color(0.115, 0.125, 0.185), + "PaintY": Color(0.30, 0.10, 0.36), "PaintR": Color(0.22, 0.05, 0.16), "PaintB": Color(0.04, 0.06, 0.18), + "PaintO": Color(0.14, 0.09, 0.30), } # Светящиеся (мрачная палитра): Cyan — холодный белёсый, Magenta — тревожный красный, Orange — натриевая лампа const GLOWS := { - "NeonCyan": [Color(0.50, 0.86, 0.82), 0.65], "NeonMagenta": [Color(0.95, 0.07, 0.04), 1.2], - "NeonOrange": [Color(1.0, 0.42, 0.08), 1.3], "NeonYellow": [Color(0.92, 0.62, 0.10), 1.1], - "NeonGreen": [Color(0.45, 0.85, 0.35), 1.0], "NeonBlue": [Color(0.25, 0.40, 0.70), 1.0], - "NeonRed": [Color(1.0, 0.05, 0.05), 1.5], "NeonWhite": [Color(1.0, 0.72, 0.42), 0.9], + "NeonCyan": [Color(0.30, 0.85, 0.98), 0.7], "NeonMagenta": [Color(0.85, 0.08, 0.65), 1.2], + "NeonOrange": [Color(0.55, 0.22, 1.0), 1.3], "NeonYellow": [Color(0.72, 0.84, 1.0), 1.0], + "NeonGreen": [Color(0.25, 0.95, 0.75), 1.0], "NeonBlue": [Color(0.28, 0.32, 0.92), 1.0], + "NeonRed": [Color(1.0, 0.06, 0.28), 1.5], "NeonWhite": [Color(0.72, 0.82, 1.0), 0.9], } # Коллизию создаёт импорт (узел Collision-colonly), здесь — внешний вид: материалы по именам из Blender @@ -120,10 +120,10 @@ func _build_environment() -> void: env.background_mode = Environment.BG_SKY env.sky = sky env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR # свет на самом игроке/призраках; уровень освещён запечённо - env.ambient_light_color = Color(0.26, 0.29, 0.33) + env.ambient_light_color = Color(0.20, 0.22, 0.40) env.ambient_light_energy = 0.6 env.fog_enabled = true - env.fog_light_color = Color(0.2, 0.16, 0.13) # грязный дымный воздух + env.fog_light_color = Color(0.10, 0.09, 0.24) # холодный индиго-дым высоких ярусов env.fog_density = 0.011 env.fog_sky_affect = 0.45 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC @@ -133,13 +133,13 @@ func _build_environment() -> void: env.glow_hdr_threshold = 1.0 env.adjustment_enabled = true env.adjustment_contrast = 1.15 - env.adjustment_saturation = 0.8 + env.adjustment_saturation = 0.9 var we := WorldEnvironment.new() we.environment = env add_child(we) var sun := DirectionalLight3D.new() # подсветка только для персонажей sun.rotation_degrees = Vector3(-55.0, 35.0, 0.0) - sun.light_color = Color(0.6, 0.68, 0.75) + sun.light_color = Color(0.5, 0.58, 0.95) sun.light_energy = 0.4 add_child(sun) @@ -156,9 +156,9 @@ func _emissive(color: Color, energy: float, alpha := 1.0) -> StandardMaterial3D: # ---------- ворота: высокие светящиеся рамы поперёк трассы ---------- -const GATE_OFF := Color(0.55, 0.30, 0.14) # ещё не очередные: тусклые -const GATE_NEXT := Color(1.0, 0.5, 0.15) # очередные: яркий натриевый -const GATE_DONE := Color(0.45, 0.85, 0.35) # пройдены: зелёные +const GATE_OFF := Color(0.30, 0.26, 0.55) # ещё не очередные: тусклый индиго +const GATE_NEXT := Color(1.0, 0.18, 0.85) # очередные: яркая маджента +const GATE_DONE := Color(0.25, 0.95, 0.95) # пройдены: ледяной циан # Угол рыскания из данных (0 = вдоль -Z, -90 = на +X) — это ровно rotation.y в Godot func gate_yaw(i: int) -> float: @@ -241,7 +241,7 @@ func _add_laser(d: Array) -> void: area.add_child(cs) var bm := BoxMesh.new() bm.size = d[1] - var mat := _emissive(Color(1.0, 0.04, 0.05), 2.4, 0.35 if d[1].y > 3.0 else 1.0) + var mat := _emissive(Color(1.0, 0.05, 0.22), 2.4, 0.35 if d[1].y > 3.0 else 1.0) var mesh := _part(bm, mat, Vector3.ZERO) area.add_child(mesh) add_child(area) @@ -266,6 +266,7 @@ func _add_vent(d: Array) -> void: a.set_script(load("res://objects/vent.gd")) a.size = d[1] a.height = d[2] + a.hidden = d[3] a.position = d[0] add_child(a) @@ -315,7 +316,7 @@ func _add_rain() -> void: var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA - mat.albedo_color = Color(0.7, 0.78, 0.82, 0.3) + mat.albedo_color = Color(0.62, 0.7, 0.95, 0.3) mat.billboard_mode = BaseMaterial3D.BILLBOARD_FIXED_Y mat.cull_mode = BaseMaterial3D.CULL_DISABLED quad.material = mat diff --git a/maps/race2/race2_data.gd b/maps/race2/race2_data.gd index 413006f..3f24803 100644 --- a/maps/race2/race2_data.gd +++ b/maps/race2/race2_data.gd @@ -45,14 +45,14 @@ const LASERS := [ [Vector3(112.00, 52.25, -608.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 1.50], # mR4 ] -# Вентиляционные потоки: центр решётки на полу, сечение (X, Z), высота столба +# Вентиляционные потоки: центр решётки на полу, сечение (X, Z), высота столба, скрытый (без света и столба — шахта-лифт) const VENTS := [ - [Vector3(107.00, 43.80, -450.00), Vector2(6.00, 6.00), 14.00], # v_tn - [Vector3(230.00, 23.80, -505.00), Vector2(6.00, 6.00), 15.00], # v1 - [Vector3(230.00, 36.30, -477.00), Vector2(6.00, 6.00), 15.00], # v2 - [Vector3(198.00, 48.80, -465.00), Vector2(6.00, 6.00), 15.00], # v3 - [Vector3(190.00, 61.30, -481.00), Vector2(6.00, 6.00), 15.00], # v4 - [Vector3(210.00, 23.80, -504.00), Vector2(8.00, 8.00), 53.00], # v_lift + [Vector3(107.00, 43.80, -450.00), Vector2(6.00, 6.00), 14.00, false], # v_tn + [Vector3(230.00, 23.80, -505.00), Vector2(6.00, 6.00), 15.00, false], # v1 + [Vector3(230.00, 36.30, -477.00), Vector2(6.00, 6.00), 15.00, false], # v2 + [Vector3(198.00, 48.80, -465.00), Vector2(6.00, 6.00), 15.00, false], # v3 + [Vector3(190.00, 61.30, -481.00), Vector2(6.00, 6.00), 15.00, false], # v4 + [Vector3(210.00, 23.80, -504.00), Vector2(8.00, 8.00), 53.00, true], # v_lift ] # Ускоряющие полосы: центр на полу, размер (ширина, длина), рыскание (направление разгона), скорость diff --git a/maps/race2/sky.png b/maps/race2/sky.png index 7a850e3..91586ca 100644 Binary files a/maps/race2/sky.png and b/maps/race2/sky.png differ diff --git a/objects/boost_strip.gd b/objects/boost_strip.gd index fc1e757..ac1cb82 100644 --- a/objects/boost_strip.gd +++ b/objects/boost_strip.gd @@ -5,12 +5,12 @@ extends Area3D @export var size := Vector2(6.0, 10.0) # ширина (X) и длина (Z), м @export var speed := 22.0 -@export var color := Color(0.35, 0.9, 0.85) # холодный бирюзовый: отличается от жёлтых стрелок-подсказок трассы +@export var color := Color(0.85, 0.45, 1.0) # пурпурно-белый: отличается от ледяных стрелок-подсказок трассы const SHADER := """ shader_type spatial; render_mode unshaded, blend_add, cull_disabled, depth_draw_never; -uniform vec4 tint : source_color = vec4(0.35, 0.9, 0.85, 1.0); +uniform vec4 tint : source_color = vec4(0.85, 0.45, 1.0, 1.0); uniform vec2 dims = vec2(6.0, 10.0); varying vec2 uv; void vertex() { uv = VERTEX.xz; } diff --git a/objects/vent.gd b/objects/vent.gd index d30286b..de3a6f2 100644 --- a/objects/vent.gd +++ b/objects/vent.gd @@ -6,7 +6,8 @@ extends Area3D @export var size := Vector2(6.0, 6.0) # сечение столба (X, Z), м @export var height := 14.0 # высота столба, м -@export var color := Color(0.45, 0.85, 0.35) +@export var color := Color(0.25, 0.95, 0.75) +@export var hidden := false # скрытый поток (шахта-лифт): только тёмная решётка, без столба, щелей и света const SHADER := """ shader_type spatial; @@ -55,7 +56,8 @@ func _build_visuals() -> void: col.material_override = sm col.position.y = height * 0.5 col.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - add_child(col) + if not hidden: + add_child(col) # решётка на полу: тёмная плита с светящимися щелями var dark := StandardMaterial3D.new() dark.albedo_color = Color(0.02, 0.025, 0.03) @@ -67,11 +69,13 @@ func _build_visuals() -> void: var plate := BoxMesh.new() plate.size = Vector3(size.x - 0.3, 0.08, size.y - 0.3) _add(plate, dark, Vector3(0, 0.04, 0)) - var n := int((size.x - 0.8) / 0.7) + var n := 0 if hidden else int((size.x - 0.8) / 0.7) for i in n: var slit := BoxMesh.new() slit.size = Vector3(0.22, 0.03, size.y - 1.0) _add(slit, glow, Vector3(-(size.x - 0.8) * 0.5 + (i + 0.5) * (size.x - 0.8) / n, 0.095, 0)) + if hidden: + return # слабый свет над решёткой var l := OmniLight3D.new() l.light_color = color diff --git a/player/player.gd b/player/player.gd index 7af844b..f94b57c 100644 --- a/player/player.gd +++ b/player/player.gd @@ -117,8 +117,8 @@ void fragment() { # Настройки чувствительности мыши # --- Внешние эффекты карт (objects/vent.gd, objects/boost_strip.gd): локально, в сеть не уходят --- -const VENT_ACCEL = 60.0 # Подъёмное ускорение в вентиляционном потоке -const VENT_MAX_RISE = 11.0 # Поток разгоняет вертикальную скорость не выше этой +const VENT_ACCEL = 80.0 # Подъёмное ускорение в вентиляционном потоке +const VENT_MAX_RISE = 16.0 # Поток разгоняет вертикальную скорость не выше этой const BOOST_ACCEL = 60.0 # Разгон на ускоряющей полосе (потолок — MAX_SPEED) const EFFECT_HOLD = 0.12 # Сколько секунд эффект держится после последнего вызова (область зовёт каждый кадр) var vent_timer: float = 0.0