diff --git a/README.md b/README.md index 51f0262..cb830ef 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ | | Что | |---|---| -| ✅ | Хаб с порталами; карты **Песочница** и **Backrooms**; выход из карт обратно в хаб | +| ✅ | Хаб с порталами; карты **Песочница**, **Backrooms** и **Parkour**; выход из карт обратно в хаб | | ✅ | Карты качаются отдельными пакетами `.pck` при первом входе в портал (экран загрузки с прогрессом) | | ✅ | Мультиплеер: выделенный сервер, игроки видят друг друга, **комнаты по картам** (в разных картах не пересекаются) | | ✅ | Преследователи: **камень** и **Верити**; общие для всех игроков комнаты, ходят по навмешу, «видят» только вблизи или в поле зрения, могут потерять игрока | @@ -42,7 +42,7 @@ godot --path . # можно запустить несколько ок ``` Карты в редакторе запускаются отдельно (F6 на сцене карты): `maps/sandbox/sandbox.tscn`, -`maps/backrooms/backrooms.tscn`. В редакторе и при запуске с диска порталы ведут на сцены напрямую (пакеты не качаются). +`maps/backrooms/backrooms.tscn`, `maps/parkour/parkour.tscn`. В редакторе и при запуске с диска порталы ведут на сцены напрямую (пакеты не качаются). ## Управление @@ -85,6 +85,7 @@ 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, см. «Карты и порталы») backrooms/ backrooms.tscn, backrooms.gd, backrooms_nav.res (запечённый навмеш), model/ (готовая модель «Backrooms VR» + 27 извлечённых текстур + CREDITS.md) deploy/ боевой конфиг nginx, systemd-юнит, project.godot выделенного сервера @@ -109,6 +110,16 @@ 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** (всё в `Blender models/parkour_02/`): +1. `layout.py` — единственный источник: плиты, стены, лазеры, чекпоинты и расчёт зазоров под мувсет (замеры ботом: прыжок на спринте 15 м, двойной 25 м, прыжок+рывок 19 м, двойной+рывок 28 м, бег по стене 1,8 с / 23 м, подтягивание на уступ до ~2,6 м; **стенами высоту не набрать** — прыжков от стены без бега только 2 за полёт). +2. `build_parkour.py` — в Blender (`exec`): строит коллизию (`Collision-colonly` — Godot делает из неё StaticBody), детальную геометрию `Solid`, светящиеся элементы `Neon`; пишет `parkour_data.gd` (чекпоинты, лазеры, старт, портал). Сохраняет `Parkour_02.blend`. +3. `bake.py` — `blender -b Parkour_02.blend -P bake.py`: Cycles запекает прямой+непрямой свет (неон светит сам, плюс холодный/пурпурный/бирюзовый свет) в **цвета вершин** (сжаты в 8 раз — шейдер умножает обратно) и экспортирует `parkour.glb`. ~1 мин. +4. `build_sky.py` — `blender -b -P build_sky.py`: панорама дальнего города 3072×1536 → `sky.png` (в импорте включены мипы — по ним размытые отражения). ~2 мин. +5. `godot --headless --import --path .`. +Палитра светящихся задана именами материалов из Blender (старые названия сохранены): `NeonCyan` — холодный белёсый, `NeonMagenta` — красный, `NeonOrange` — натриевая лампа. Настроение — в `parkour.gd` (туман, дождь `_add_rain`, гул `_add_ambience`, насыщенность и контраст), в `bake.py` (силы света), `build_sky.py` (дымное небо). +В Godot: `baked.gdshader` (unshaded: цвет материала × запечённый свет × «зерно» бетона по мировым координатам + влажный блеск — отражение панорамы на горизонтальных поверхностях); `parkour.gd` подменяет материалы по именам из Blender (`SURFACES`, `GLOWS`), строит ворота-чекпоинты, лазеры (пульсирующие считают фазу по системным часам), окружение и портал. **Добавить карту:** 1. Создай `maps/<имя>/<имя>.tscn`: корень Node3D, внутри игрок (`player/player.tscn`), портал обратно (`objects/portal.gd`, diff --git a/export_presets.cfg b/export_presets.cfg index 242385a..7757cbf 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -10,7 +10,7 @@ dedicated_server=false custom_features="" export_filter="all_resources" include_filter="" -exclude_filter="tools/*, maps/sandbox/*, maps/backrooms/*, chasers/rock/*, chasers/verity/*" +exclude_filter="tools/*, maps/sandbox/*, maps/backrooms/*, maps/parkour/*, chasers/rock/*, chasers/verity/*" export_path="../grid-world/index.html" patches=PackedStringArray() patch_delta_encoding=false @@ -150,3 +150,53 @@ progressive_web_app/icon_512x512="" progressive_web_app/background_color=Color(0, 0, 0, 1) threads/emscripten_pool_size=8 threads/godot_pool_size=4 + +[preset.3] + +name="Parkour" +platform="Web" +dedicated_server=false +custom_features="" +export_filter="resources" +export_files=PackedStringArray("res://maps/parkour/parkour.tscn") +include_filter="" +exclude_filter="player/*, objects/*, chasers/chaser.gd" +export_path="../grid-world/parkour.pck" +patches=PackedStringArray() +patch_delta_encoding=false +patch_delta_compression_level_zstd=19 +patch_delta_min_reduction=0.1 +patch_delta_include_filters="*" +patch_delta_exclude_filters="" +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.3.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +variant/thread_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/ensure_cross_origin_isolation_headers=true +progressive_web_app/offline_page="" +progressive_web_app/display=3 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) +threads/emscripten_pool_size=8 +threads/godot_pool_size=4 diff --git a/maps/hub/hub.gd b/maps/hub/hub.gd index 4d8ec76..0518e2e 100644 --- a/maps/hub/hub.gd +++ b/maps/hub/hub.gd @@ -6,6 +6,7 @@ extends Node3D const PORTALS := [ {"title": "SANDBOX", "map": "world", "scene": "res://maps/sandbox/sandbox.tscn", "pack": "world.pck", "color": Color(0.2, 0.9, 1.0)}, {"title": "BACKROOMS", "map": "backrooms", "scene": "res://maps/backrooms/backrooms.tscn", "pack": "backrooms.pck", "color": Color(1.0, 0.85, 0.25)}, + {"title": "PARKOUR", "map": "parkour", "scene": "res://maps/parkour/parkour.tscn", "pack": "parkour.pck", "color": Color(1.0, 0.2, 0.65)}, ] const ROOM_WIDTH := 28.0 diff --git a/maps/parkour/baked.gdshader b/maps/parkour/baked.gdshader new file mode 100644 index 0000000..efad6aa --- /dev/null +++ b/maps/parkour/baked.gdshader @@ -0,0 +1,42 @@ +shader_type spatial; +// Поверхности уровня Parkour: освещение запечено в цвета вершин (Blender, Cycles), динамических источников нет. +// Итог = цвет материала × запечённый свет × мелкое «зерно» бетона (по мировым координатам) + слабый блик на скользящих углах. +render_mode unshaded; + +uniform vec3 albedo_color = vec3(0.1); +uniform float light_scale = 8.0; // цвета вершин сжаты в 8 раз при экспорте (см. bake.py) +uniform float exposure = 1.0; +uniform sampler2D grain : repeat_enable, filter_linear_mipmap; +uniform float grain_scale = 0.3; +uniform float grain_amount = 0.45; +uniform sampler2D sky_tex : source_color, filter_linear_mipmap; // панорама неба: отражается в «мокрых» горизонтальных поверхностях +uniform float wet = 0.6; + +varying vec3 wpos; +varying vec3 wnorm; + +void vertex() { + wpos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; + wnorm = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz); +} + +vec2 pano_uv(vec3 d) { + return vec2(atan(d.x, -d.z) / (2.0 * PI) + 0.5, acos(clamp(d.y, -1.0, 1.0)) / PI); +} + +void fragment() { + vec3 n = abs(wnorm); + n /= (n.x + n.y + n.z + 0.0001); + float g = texture(grain, wpos.zy * grain_scale).r * n.x + + texture(grain, wpos.xz * grain_scale).r * n.y + + texture(grain, wpos.xy * grain_scale).r * n.z; + float gr = mix(1.0 - grain_amount * 0.5, 1.0 + grain_amount * 0.5, g); + vec3 lit = COLOR.rgb * light_scale; + vec3 nv = normalize(NORMAL); + float ndv = clamp(dot(nv, normalize(VIEW)), 0.0, 1.0); + float fres = 0.05 + 0.95 * pow(1.0 - ndv, 4.0); + vec3 rw = normalize((INV_VIEW_MATRIX * vec4(reflect(-normalize(VIEW), nv), 0.0)).xyz); + float up = smoothstep(0.6, 0.95, wnorm.y); // блестят только горизонтальные поверхности + vec3 refl = textureLod(sky_tex, pano_uv(rw), 3.0).rgb; + ALBEDO = albedo_color * lit * gr * exposure + refl * fres * wet * up * (0.5 + 0.5 * min(lit.r + lit.g + lit.b, 1.0)); +} diff --git a/maps/parkour/baked.gdshader.uid b/maps/parkour/baked.gdshader.uid new file mode 100644 index 0000000..dcf28e1 --- /dev/null +++ b/maps/parkour/baked.gdshader.uid @@ -0,0 +1 @@ +uid://dj3xyip7rr8h3 diff --git a/maps/parkour/parkour.gd b/maps/parkour/parkour.gd new file mode 100644 index 0000000..34a5078 --- /dev/null +++ b/maps/parkour/parkour.gd @@ -0,0 +1,336 @@ +extends Node3D +# Паркур в стиле Ghostrunner: подъём по мегабашне от крыш до светящегося ядра. Сцена живёт в отдельном +# пакете (parkour.pck); модель — parkour.glb, данные раскладки — parkour_data.gd: оба генерирует скрипт +# `Blender models/parkour_02/build_parkour.py` из layout.py (там же описано, как считалась проходимость). +# Упал ниже чекпоинта на KILL_DEPTH, попал в лазер — возрождение на последнем чекпоинте. Преследователей нет. + +const Data = preload("res://maps/parkour/parkour_data.gd") +const KILL_DEPTH := 32.0 # Ниже последнего чекпоинта на столько — срыв +const GATE_HEIGHT := 5.0 +const GATE_HALF_WIDTH := 3.0 + +var player: Node3D +var _lasers: Array = [] # {area, mat, mesh, mode, period, on, phase} +var _hud_label: Label +var _hud_timer := 0.0 + +func _ready() -> void: + Net.enter_map("parkour") + player = get_tree().get_first_node_in_group("local_player") + player.global_position = Data.START + player.spawn_transform = Transform3D(Basis.IDENTITY, Data.START) + player.death_label.text = "СОРВАЛСЯ!" + _prepare_map() + _build_environment() + _build_hud() + for pos in Data.CHECKPOINTS: + _add_checkpoint(pos) + for l in Data.LASERS: + _add_laser(l) + _add_rain() + _add_ambience() + _add_exit() + +# Цвета поверхностей — те же значения, что в 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), +} +# Светящиеся (мрачная палитра): 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], +} + +# Коллизию создаёт импорт (узел Collision-colonly), здесь — внешний вид: материалы по именам из Blender +func _prepare_map() -> void: + var shader := load("res://maps/parkour/baked.gdshader") as Shader + var noise := FastNoiseLite.new() + noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH + noise.frequency = 0.04 + noise.fractal_octaves = 4 + var grain := NoiseTexture2D.new() + grain.noise = noise + grain.width = 512 + grain.height = 512 + grain.seamless = true + var sky_tex := load("res://maps/parkour/sky.png") as Texture2D + var cache := {} + for mi: MeshInstance3D in $Map.find_children("*", "MeshInstance3D", true, false): + mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + for s in mi.mesh.get_surface_count(): + var src := mi.mesh.surface_get_material(s) + var key: String = src.resource_name if src != null else "" + if not cache.has(key): + if SURFACES.has(key): + var m := ShaderMaterial.new() + m.shader = shader + m.set_shader_parameter("albedo_color", Vector3(SURFACES[key].r, SURFACES[key].g, SURFACES[key].b)) + m.set_shader_parameter("grain", grain) + m.set_shader_parameter("sky_tex", sky_tex) + m.set_shader_parameter("exposure", 2.1) + cache[key] = m + elif GLOWS.has(key): + cache[key] = _emissive(GLOWS[key][0], GLOWS[key][1]) + else: + cache[key] = null + if cache[key] != null: + mi.set_surface_override_material(s, cache[key]) + +func _physics_process(delta: float) -> void: + if player == null: + return + if not player.is_dead: + if player.global_position.y < player.spawn_transform.origin.y - KILL_DEPTH: + player.die() + else: + for l in _lasers: + if _laser_active(l) and l.area.overlaps_body(player): + player.die() + break + _update_lasers() + _update_rain() + if _hud_timer > 0.0: + _hud_timer -= delta + _hud_label.modulate.a = clampf(_hud_timer / 0.5, 0.0, 1.0) + +# ---------- окружение ---------- + +func _build_environment() -> void: + var sky_mat := PanoramaSkyMaterial.new() + sky_mat.panorama = load("res://maps/parkour/sky.png") + var sky := Sky.new() + sky.sky_material = sky_mat + var env := Environment.new() + env.background_mode = Environment.BG_SKY + env.sky = sky + env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR # свет на самом игроке/призраках; уровень освещён запечённо + env.ambient_light_color = Color(0.26, 0.29, 0.33) + env.ambient_light_energy = 0.6 + env.fog_enabled = true + env.fog_light_color = Color(0.2, 0.16, 0.13) # грязный дымный воздух + env.fog_density = 0.011 + env.fog_sky_affect = 0.45 + env.tonemap_mode = Environment.TONE_MAPPER_FILMIC + env.glow_enabled = true + env.glow_intensity = 0.55 + env.glow_bloom = 0.1 + env.glow_hdr_threshold = 1.0 + env.adjustment_enabled = true + env.adjustment_contrast = 1.15 + env.adjustment_saturation = 0.8 + 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_energy = 0.4 + add_child(sun) + +func _build_hud() -> void: + var layer := CanvasLayer.new() + layer.layer = 12 + add_child(layer) + _hud_label = Label.new() + _hud_label.text = "ЧЕКПОИНТ" + _hud_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _hud_label.add_theme_font_size_override("font_size", 44) + _hud_label.add_theme_color_override("font_color", Color(0.5, 0.85, 0.4)) + _hud_label.add_theme_constant_override("outline_size", 8) + _hud_label.add_theme_color_override("font_outline_color", Color.BLACK) + _hud_label.modulate.a = 0.0 + layer.add_child(_hud_label) + _hud_label.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) + _hud_label.position.y = 90.0 + +func _emissive(color: Color, energy: float, alpha := 1.0) -> StandardMaterial3D: + var m := StandardMaterial3D.new() + m.albedo_color = Color(0, 0, 0, alpha) + m.emission_enabled = true + m.emission = color + m.emission_energy_multiplier = energy + if alpha < 1.0: + m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + m.cull_mode = BaseMaterial3D.CULL_DISABLED + return m + +# ---------- чекпоинты: высокие светящиеся ворота поперёк трассы ---------- + +func _add_checkpoint(pos: Vector3) -> void: + var mat := _emissive(Color(1.0, 0.5, 0.15), 0.9) + var sheet_mat := _emissive(Color(1.0, 0.5, 0.15), 0.3, 0.08) + var root := Node3D.new() + root.position = Vector3(pos.x, pos.y - 1.2, pos.z) # pos — центр капсулы, ворота ставим на пол + add_child(root) + for s in [-1.0, 1.0]: + var bm := BoxMesh.new() + bm.size = Vector3(0.35, GATE_HEIGHT, 0.35) + root.add_child(_part(bm, mat, Vector3(s * GATE_HALF_WIDTH, GATE_HEIGHT * 0.5, 0))) + var top := BoxMesh.new() + top.size = Vector3(GATE_HALF_WIDTH * 2 + 0.35, 0.35, 0.35) + root.add_child(_part(top, mat, Vector3(0, GATE_HEIGHT, 0))) + var qm := QuadMesh.new() # слабая светящаяся «плёнка» в проёме + qm.size = Vector2(GATE_HALF_WIDTH * 2, GATE_HEIGHT) + root.add_child(_part(qm, sheet_mat, Vector3(0, GATE_HEIGHT * 0.5, 0))) + var label := Label3D.new() + label.text = "CHECKPOINT" + label.billboard = BaseMaterial3D.BILLBOARD_ENABLED + label.pixel_size = 0.012 + label.modulate = Color(1.0, 0.62, 0.3) + label.outline_modulate = Color.BLACK + label.position = Vector3(0, GATE_HEIGHT + 1.0, 0) + root.add_child(label) + + var area := Area3D.new() + area.collision_layer = 0 + area.collision_mask = 1 + area.position = Vector3(0, GATE_HEIGHT * 0.5, 0) + var cs := CollisionShape3D.new() + var shape := BoxShape3D.new() + shape.size = Vector3(GATE_HALF_WIDTH * 2 + 6.0, GATE_HEIGHT + 2.0, 8.0) + cs.shape = shape + area.add_child(cs) + root.add_child(area) + area.body_entered.connect(func(body: Node3D) -> void: + if body != player or player.spawn_transform.origin.is_equal_approx(pos): + return + player.spawn_transform = Transform3D(Basis.IDENTITY, pos) + var green := Color(0.45, 0.85, 0.35) + mat.emission = green + sheet_mat.emission = green + label.modulate = green + _hud_timer = 2.0 + ) + +func _part(mesh: Mesh, mat: Material, pos: Vector3) -> MeshInstance3D: + var mi := MeshInstance3D.new() + mi.mesh = mesh + mi.material_override = mat + mi.position = pos + mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + return mi + +# ---------- лазеры ---------- + +func _add_laser(d: Array) -> void: + var area := Area3D.new() + area.collision_layer = 0 + area.collision_mask = 1 + area.position = d[0] + var cs := CollisionShape3D.new() + var shape := BoxShape3D.new() + shape.size = d[1] + cs.shape = shape + area.add_child(cs) + var bm := BoxMesh.new() + bm.size = d[1] + var mat := _emissive(Color(1.0, 0.04, 0.05), 2.4, 0.35 if d[1].y > 3.0 else 1.0) + var mesh := _part(bm, mat, Vector3.ZERO) + area.add_child(mesh) + add_child(area) + _lasers.append({"area": area, "mat": mat, "mesh": mesh, "mode": d[2], "period": d[3], "on": d[4], "phase": d[5]}) + +# Пульсирующие лазеры считают фазу по системным часам — у всех игроков примерно одинаково +func _laser_active(l: Dictionary) -> bool: + if l.mode == 0: + return true + return fposmod(Time.get_unix_time_from_system() + l.phase, l.period) < l.on + +func _update_lasers() -> void: + for l in _lasers: + var active := _laser_active(l) + l.mat.emission_energy_multiplier = 2.4 if active else 0.06 + l.mesh.visible = active or l.mode == 1 + +# ---------- выход ---------- + +func _add_exit() -> void: + var portal := Area3D.new() + portal.set_script(load("res://objects/portal.gd")) + portal.target_map = "hub" + portal.target_scene = "res://maps/hub/hub.tscn" + portal.title = "HUB" + portal.color = Color(0.3, 1, 0.45) + portal.position = Data.PORTAL + add_child(portal) + +# ---------- дождь и звук ---------- + +var _rain: GPUParticles3D + +# Ливень вокруг игрока: тонкие вертикальные штрихи, летящие по ветру; в закрытом зале не идёт +func _add_rain() -> void: + _rain = GPUParticles3D.new() + _rain.amount = 2200 + _rain.lifetime = 0.8 + _rain.local_coords = false + _rain.visibility_aabb = AABB(Vector3(-30, -30, -30), Vector3(60, 60, 60)) + var pm := ParticleProcessMaterial.new() + pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX + pm.emission_box_extents = Vector3(26.0, 0.5, 26.0) + pm.direction = Vector3(0.12, -1.0, 0.05) + pm.spread = 2.0 + pm.initial_velocity_min = 34.0 + pm.initial_velocity_max = 40.0 + pm.gravity = Vector3.ZERO + _rain.process_material = pm + var quad := QuadMesh.new() + quad.size = Vector2(0.018, 0.7) + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.albedo_color = Color(0.7, 0.78, 0.82, 0.3) + mat.billboard_mode = BaseMaterial3D.BILLBOARD_FIXED_Y + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + quad.material = mat + _rain.draw_pass_1 = quad + _rain.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + add_child(_rain) + +func _update_rain() -> void: + if _rain == null or player == null: + return + _rain.global_position = player.global_position + Vector3(0, 12, 0) + var p := player.global_position + _rain.emitting = not (p.z < -Data.HALL_Y0 + 2.0 and p.z > -Data.HALL_Y1 - 2.0 and absf(p.x) < 11.0 and p.y > Data.HALL_Z - 1.0 and p.y < Data.HALL_Z + 14.0) + +# Низкий промышленный гул и шум дождя: 3 секунды без щелчка на стыке петли (края сведены перекрёстным затуханием) +func _add_ambience() -> void: + const RATE := 22050 + const FADE := 4410 + var n := RATE * 3 + var rng := RandomNumberGenerator.new() + rng.seed = 13 + var raw := PackedFloat32Array() + raw.resize(n + FADE) + var lp := 0.0 + for i in n + FADE: + lp = lerpf(lp, rng.randf_range(-1.0, 1.0), 0.55) # мягкий «шипящий» шум + raw[i] = lp + var data := PackedByteArray() + data.resize(n * 2) + for i in n: + var v := raw[i] + if i < FADE: + var t := float(i) / FADE + v = raw[i] * sin(t * PI * 0.5) + raw[n + i] * cos(t * PI * 0.5) + var tt := float(i) / RATE + var hum := 0.16 * sin(TAU * 42.0 * tt) + 0.08 * sin(TAU * 63.0 * tt) + 0.05 * sin(TAU * 21.0 * tt) + data.encode_s16(i * 2, int(clampf(v * 0.45 + hum, -1.0, 1.0) * 14000.0)) + var wav := AudioStreamWAV.new() + wav.format = AudioStreamWAV.FORMAT_16_BITS + wav.mix_rate = RATE + wav.stereo = false + wav.loop_mode = AudioStreamWAV.LOOP_FORWARD + wav.loop_begin = 0 + wav.loop_end = n + wav.data = data + var ap := AudioStreamPlayer.new() + ap.stream = wav + ap.volume_db = -13.0 + ap.autoplay = true + add_child(ap) diff --git a/maps/parkour/parkour.gd.uid b/maps/parkour/parkour.gd.uid new file mode 100644 index 0000000..c0f4dd5 --- /dev/null +++ b/maps/parkour/parkour.gd.uid @@ -0,0 +1 @@ +uid://g0v15asgxuhr diff --git a/maps/parkour/parkour.glb b/maps/parkour/parkour.glb new file mode 100644 index 0000000..5291b11 Binary files /dev/null and b/maps/parkour/parkour.glb differ diff --git a/maps/parkour/parkour.glb.import b/maps/parkour/parkour.glb.import new file mode 100644 index 0000000..65f8aec --- /dev/null +++ b/maps/parkour/parkour.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dghn3q2a0e8c7" +path="res://.godot/imported/parkour.glb-ad1d8c5d55c4e3638879e9fa5979f001.scn" + +[deps] + +source_file="res://maps/parkour/parkour.glb" +dest_files=["res://.godot/imported/parkour.glb-ad1d8c5d55c4e3638879e9fa5979f001.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/maps/parkour/parkour.tscn b/maps/parkour/parkour.tscn new file mode 100644 index 0000000..72be546 --- /dev/null +++ b/maps/parkour/parkour.tscn @@ -0,0 +1,12 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://maps/parkour/parkour.gd" id="1_level"] +[ext_resource type="PackedScene" uid="uid://dghn3q2a0e8c7" path="res://maps/parkour/parkour.glb" id="2_map"] +[ext_resource type="PackedScene" uid="uid://c3efk6k8xnf57" path="res://player/player.tscn" id="3_player"] + +[node name="Parkour" type="Node3D"] +script = ExtResource("1_level") + +[node name="Map" parent="." instance=ExtResource("2_map")] + +[node name="Player" parent="." instance=ExtResource("3_player")] diff --git a/maps/parkour/parkour_data.gd b/maps/parkour/parkour_data.gd new file mode 100644 index 0000000..1058e45 --- /dev/null +++ b/maps/parkour/parkour_data.gd @@ -0,0 +1,36 @@ +# Сгенерировано Blender models/parkour_02/build_parkour.py из layout.py — не править руками +extends RefCounted + +const START := Vector3(0.00, 1.25, 6.00) +const PORTAL := Vector3(-20.14, 79.00, -676.46) +# Зал (для отключения дождя): Y трассы -> -Z, поэтому HALL_Y0/HALL_Y1 — расстояния по трассе +const HALL_Y0 := 354.0 +const HALL_Y1 := 444.0 +const HALL_Z := 44.0 + +# Чекпоинты: центр капсулы игрока +const CHECKPOINTS := [ + Vector3(0.00, 11.00, -193.00), # cp1 + Vector3(0.00, 10.20, -302.00), # cp2 + Vector3(4.20, 27.70, -319.00), # cp2b + Vector3(0.00, 45.20, -338.00), # cp3 + Vector3(0.00, 45.20, -454.00), # cp4 + Vector3(2.00, 49.20, -544.00), # cp5 + Vector3(0.00, 53.20, -648.00), # cp6 + Vector3(28.25, 65.20, -720.71), # cp7 + Vector3(-20.14, 80.20, -668.46), # cp8 +] + +# Лазеры: центр, размер, режим (0 — всегда, 1 — пульсирует), период, время включения, сдвиг +const LASERS := [ + [Vector3(0.00, 45.60, -366.25), Vector3(16.00, 0.60, 0.50), 0, 0.00, 0.00, 0.00], # g1_low + [Vector3(0.00, 44.45, -378.25), Vector3(16.00, 0.90, 0.50), 0, 0.00, 0.00, 0.00], # g2_floor + [Vector3(0.00, 44.45, -392.25), Vector3(16.00, 0.90, 0.50), 1, 2.40, 1.20, 0.00], # g3_a + [Vector3(0.00, 45.60, -392.25), Vector3(16.00, 0.60, 0.50), 1, 2.40, 1.20, 1.20], # g3_b + [Vector3(0.00, 45.60, -406.25), Vector3(16.00, 0.60, 0.50), 0, 0.00, 0.00, 0.00], # g4_a + [Vector3(0.00, 45.60, -411.25), Vector3(16.00, 0.60, 0.50), 0, 0.00, 0.00, 0.00], # g4_b + [Vector3(0.00, 45.60, -416.25), Vector3(16.00, 0.60, 0.50), 0, 0.00, 0.00, 0.00], # g4_c + [Vector3(0.00, 50.00, -426.25), Vector3(16.00, 12.00, 0.50), 1, 3.00, 2.00, 0.00], # g5_wall + [Vector3(-4.50, 44.45, -436.25), Vector3(7.00, 0.90, 0.50), 0, 0.00, 0.00, 0.00], # g6_low + [Vector3(4.50, 45.60, -436.25), Vector3(7.00, 0.60, 0.50), 0, 0.00, 0.00, 0.00], # g6_mid +] diff --git a/maps/parkour/parkour_data.gd.uid b/maps/parkour/parkour_data.gd.uid new file mode 100644 index 0000000..a5d8699 --- /dev/null +++ b/maps/parkour/parkour_data.gd.uid @@ -0,0 +1 @@ +uid://stvuf5x6u7h4 diff --git a/maps/parkour/sky.png b/maps/parkour/sky.png new file mode 100644 index 0000000..4b7611c Binary files /dev/null and b/maps/parkour/sky.png differ diff --git a/maps/parkour/sky.png.import b/maps/parkour/sky.png.import new file mode 100644 index 0000000..a1910ae --- /dev/null +++ b/maps/parkour/sky.png.import @@ -0,0 +1,41 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c5lqkgvekqu1s" +path.s3tc="res://.godot/imported/sky.png-c9375457cc5d86ef34d2b54fae5a3058.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://maps/parkour/sky.png" +dest_files=["res://.godot/imported/sky.png-c9375457cc5d86ef34d2b54fae5a3058.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/tools/export_all.sh b/tools/export_all.sh index 70cdf3f..c4e961a 100755 --- a/tools/export_all.sh +++ b/tools/export_all.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Экспорт веб-клиента и пакетов карт. По умолчанию — в ../grid-world (репозиторий сайта). # tools/export_all.sh [папка_вывода] -# Пакеты карт (backrooms.pck, world.pck) нужно выкладывать вместе с index.* — клиент качает их при входе в портал. +# Пакеты карт (backrooms.pck, world.pck, parkour.pck) нужно выкладывать вместе с index.* — клиент качает их при входе в портал. set -euo pipefail cd "$(dirname "$0")/.." OUT="${1:-../grid-world}" @@ -14,4 +14,5 @@ find maps chasers -name '*.tscn' -exec touch {} + godot --headless --export-release Web "$OUT/index.html" godot --headless --export-pack Backrooms "$OUT/backrooms.pck" godot --headless --export-pack World "$OUT/world.pck" +godot --headless --export-pack Parkour "$OUT/parkour.pck" ls -la "$OUT"/*.pck "$OUT"/index.wasm | awk '{printf "%7.2f МБ %s\n", $5/1e6, $9}'