diff --git a/Player/character_body_3d.gd b/Player/character_body_3d.gd index f575663..917b056 100644 --- a/Player/character_body_3d.gd +++ b/Player/character_body_3d.gd @@ -98,6 +98,11 @@ const SLIDE_VIGNETTE = 0.45 const SPRINT_VIGNETTE = 0.2 const VIGNETTE_SPEED = 8.0 +# --- Смерть (камень поймал) --- +const DEATH_TIME = 2.5 # Сколько лежим перед возрождением +const DEATH_CAMERA_HEIGHT = 0.3 # Камера падает почти на землю +const DEATH_ROLL = 75.0 # И заваливается набок (градусы) + const VIGNETTE_SHADER = """ shader_type canvas_item; uniform float intensity : hint_range(0.0, 1.0) = 0.0; @@ -162,8 +167,18 @@ var is_zooming: bool = false # Виньетка var vignette_material: ShaderMaterial = null +var vignette_rect: ColorRect = null var vignette_value: float = 0.0 +# Смерть +signal died +signal respawned +var is_dead: bool = false +var death_timer: float = 0.0 +var spawn_transform: Transform3D +var death_overlay: ColorRect = null +var death_label: Label = null + # Коллайдер (опционально, ищется по имени CollisionShape3D) var collision_shape: CollisionShape3D = null var capsule: CapsuleShape3D = null @@ -188,6 +203,8 @@ func _ready() -> void: default_collision_y = collision_shape.position.y _setup_vignette() + _setup_death_screen() + spawn_transform = global_transform func _setup_vignette() -> void: var layer := CanvasLayer.new() @@ -198,6 +215,7 @@ func _setup_vignette() -> void: rect.mouse_filter = Control.MOUSE_FILTER_IGNORE layer.add_child(rect) rect.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + vignette_rect = rect var shader := Shader.new() shader.code = VIGNETTE_SHADER @@ -212,6 +230,9 @@ func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + if is_dead: + return + if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: var zoom_factor := head.fov / base_fov rotate_y(-event.relative.x * MOUSE_SENSITIVITY * zoom_factor) @@ -246,6 +267,10 @@ func _get_input_dir() -> Vector2: return input_dir.normalized() func _physics_process(delta: float) -> void: + if is_dead: + _process_death(delta) + return + slide_cooldown_timer = maxf(slide_cooldown_timer - delta, 0.0) dash_cooldown_timer = maxf(dash_cooldown_timer - delta, 0.0) wallrun_cooldown_timer = maxf(wallrun_cooldown_timer - delta, 0.0) @@ -812,9 +837,89 @@ func _update_camera(delta: float, input_dir: Vector2) -> void: vignette_value = lerp(vignette_value, target_vignette, VIGNETTE_SPEED * delta) if vignette_material: vignette_material.set_shader_parameter("intensity", vignette_value) + # Невидимая виньетка не должна стоить заливки всего экрана (важно для веба) + vignette_rect.visible = vignette_value > 0.005 func _head_bob(time: float) -> Vector3: var pos := Vector3.ZERO pos.y = sin(time * BOB_FREQ) * BOB_AMP pos.x = cos(time * BOB_FREQ / 2) * BOB_AMP return pos + +# --- СМЕРТЬ И ВОЗРОЖДЕНИЕ --- + +func _setup_death_screen() -> void: + var layer := CanvasLayer.new() + layer.layer = 11 + add_child(layer) + + death_overlay = ColorRect.new() + death_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE + death_overlay.color = Color(0.6, 0.0, 0.0, 0.0) + death_overlay.visible = false + layer.add_child(death_overlay) + death_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + + death_label = Label.new() + death_label.text = "ПОЙМАЛ!" + death_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + death_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + death_label.add_theme_font_size_override("font_size", 96) + death_label.add_theme_color_override("font_color", Color.WHITE) + death_label.add_theme_constant_override("outline_size", 12) + death_label.add_theme_color_override("font_outline_color", Color.BLACK) + death_label.visible = false + death_overlay.add_child(death_label) + death_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + +func die() -> void: + if is_dead: + return + is_dead = true + death_timer = DEATH_TIME + if is_sliding: + _end_slide() + is_dashing = false + is_wallrunning = false + is_mantling = false + velocity = Vector3.ZERO + shake_strength = 0.25 + death_label.visible = true + death_overlay.visible = true + died.emit() + +func _process_death(delta: float) -> void: + death_timer -= delta + + # Камера падает и заваливается набок, экран краснеет + var k := 1.0 - exp(-6.0 * delta) + head.position = head.position.lerp(Vector3(head_base_pos.x, DEATH_CAMERA_HEIGHT, head_base_pos.z), k) + head.rotation.z = lerpf(head.rotation.z, deg_to_rad(DEATH_ROLL), k) + head.fov = lerpf(head.fov, base_fov, k) + shake_strength = lerpf(shake_strength, 0.0, 10.0 * delta) + death_overlay.color.a = lerpf(death_overlay.color.a, 0.55, k) + + # Падаем на землю, если умерли в воздухе + velocity.x = 0.0 + velocity.z = 0.0 + if not is_on_floor(): + velocity += get_gravity() * delta + move_and_slide() + + if death_timer <= 0.0: + _respawn() + +func _respawn() -> void: + global_transform = spawn_transform + velocity = Vector3.ZERO + head.position = head_base_pos + head.rotation = Vector3.ZERO + current_roll = 0.0 + head_offset_y = 0.0 + land_offset = 0.0 + step_offset = 0.0 + death_overlay.color.a = 0.0 + death_label.visible = false + death_overlay.visible = false + is_dead = false + respawned.emit() diff --git a/World/nav-bake.gd b/World/nav-bake.gd new file mode 100644 index 0000000..75fb717 --- /dev/null +++ b/World/nav-bake.gd @@ -0,0 +1,47 @@ +extends NavigationRegion3D +# Запекает навмеш при старте игры по статическим коллизиям уровня (пол, здания, деревья). + +const SOURCE_GROUP := "nav_source" + +@export var exclude: Array[NodePath] = [] # Узлы, которые не должны попасть в навмеш (например, сам камень) +@export var bake_half_size := 300.0 # Половина стороны квадрата запекания вокруг центра мира +@export var cell_size := 1.0 # Крупная ячейка — быстрое запекание (в вебе нет потоков) +@export var agent_radius := 2.0 +@export var agent_height := 5.0 + +func _ready() -> void: + var nm := NavigationMesh.new() + nm.cell_size = cell_size + nm.cell_height = 0.25 + nm.agent_radius = agent_radius + nm.agent_height = agent_height + nm.agent_max_climb = 0.5 + nm.geometry_parsed_geometry_type = NavigationMesh.PARSED_GEOMETRY_STATIC_COLLIDERS + nm.geometry_source_geometry_mode = NavigationMesh.SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN + nm.geometry_source_group_name = SOURCE_GROUP + nm.filter_baking_aabb = AABB(Vector3(-bake_half_size, -10, -bake_half_size), Vector3(bake_half_size * 2, 60, bake_half_size * 2)) + navigation_mesh = nm + + var map := get_world_3d().navigation_map + NavigationServer3D.map_set_cell_size(map, nm.cell_size) + NavigationServer3D.map_set_cell_height(map, nm.cell_height) + + # Ждём кадр, чтобы процедурные узлы (лес) успели создать коллизии + await get_tree().process_frame + var excluded: Array[Node] = [] + for p in exclude: + var n := get_node_or_null(p) + if n: + excluded.append(n) + _mark_sources(get_parent(), excluded) + bake_navigation_mesh(true) + +# Помечает всё дерево сцены как источник геометрии, обходя исключённые узлы +func _mark_sources(node: Node, excluded: Array[Node]) -> void: + for c in node.get_children(): + if c == self or c in excluded: + continue + if excluded.any(func(e): return c.is_ancestor_of(e)): + _mark_sources(c, excluded) + else: + c.add_to_group(SOURCE_GROUP) diff --git a/World/nav-bake.gd.uid b/World/nav-bake.gd.uid new file mode 100644 index 0000000..357c0f3 --- /dev/null +++ b/World/nav-bake.gd.uid @@ -0,0 +1 @@ +uid://c2k1kj4a2s2su diff --git a/World/tron_01.ogg b/World/tron_01.ogg index 49e38cc..1db84eb 100644 Binary files a/World/tron_01.ogg and b/World/tron_01.ogg differ diff --git a/export_presets.cfg b/export_presets.cfg index e9b33d3..29b319b 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="" +exclude_filter="World/Sky/sunflowers_puresky_2k.hdr, models/Tree_01/Tree_01.glb" export_path="../grid-world/index.html" patches=PackedStringArray() patch_delta_encoding=false @@ -29,7 +29,7 @@ script_export_mode=2 custom_template/debug="" custom_template/release="" -variant/extensions_support=true +variant/extensions_support=false variant/thread_support=false vram_texture_compression/for_desktop=true vram_texture_compression/for_mobile=false diff --git a/main.tscn b/main.tscn index 7c2c1b7..dede99b 100644 --- a/main.tscn +++ b/main.tscn @@ -10,20 +10,14 @@ [ext_resource type="AudioStream" uid="uid://ddsynauqeoppw" path="res://World/tron_01.ogg" id="7_272bh"] [ext_resource type="AudioStream" uid="uid://280pvcvxt2n2" path="res://models/Bochi-Rock/Guitar.ogg" id="7_dg77c"] [ext_resource type="PackedScene" uid="uid://evml2s8rmerw" path="res://models/obelisk_01/Obelisk.glb" id="9_kek77"] -[ext_resource type="Script" uid="uid://b3ckck8y0rby6" path="res://models/Bochi-Rock/rock-path.gd" id="10_4c57u"] +[ext_resource type="Script" uid="uid://o2kqd0cy4knr" path="res://models/Bochi-Rock/rock-follow.gd" id="10_4c57u"] [ext_resource type="PackedScene" uid="uid://cowp33ejxcfcl" path="res://models/Bochi-Rock/Rock.glb" id="11_efxa6"] [ext_resource type="Script" uid="uid://cx16mgkh4bldx" path="res://World/forest.gd" id="12_forest"] [ext_resource type="PackedScene" uid="uid://b6d4btx7xn51j" path="res://models/Tree_01/Tree_01_low.glb" id="13_treelow"] [ext_resource type="Script" uid="uid://j5ultotjrstk" path="res://World/birds.gd" id="14_birds"] [ext_resource type="Script" uid="uid://chtck5eaaddrw" path="res://World/levitate.gd" id="15_levitate"] [ext_resource type="PackedScene" uid="uid://bmprcwbt78ghq" path="res://models/empire-state/Empire-State.glb" id="17_yaehf"] - -[sub_resource type="Curve3D" id="Curve3D_dg77c"] -_data = { -"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 5.2467575, -0.0029411316, 93.87327, 0, 0, 0, 0, 0, 0, 13.789459, -0.0029335022, 97.604256, 0, 0, 0, 0, 0, 0, 24.239388, -0.0029335022, 102.18522, 0, 0, 0, 0, 0, 0, 37.65388, -0.0029335022, 101.72713, 0, 0, 0, 0, 0, 0, 49.610317, -0.002937317, 93.816925, 0, 0, 0, 0, 0, 0, 54.721954, -0.002937317, 83.662735, 0, 0, 0, 0, 0, 0, 60.17699, -0.002937317, 67.371086, 0, 0, 0, 0, 0, 0, 63.19221, -0.002937317, 50.906624, 0, 0, 0, 0, 0, 0, 63.427063, -0.002937317, 36.6671, 0, 0, 0, 0, 0, 0, 60.360424, -0.002937317, 23.65346, 0, 0, 0, 0, 0, 0, 51.587524, -0.002937317, 10.339436, 0, 0, 0, 0, 0, 0, 37.92818, -0.002937317, -5.663192, 0, 0, 0, 0, 0, 0, 30.353395, -0.0029411316, -11.97097, 0, 0, 0, 0, 0, 0, 24.506142, -0.002937317, -11.578466, 0, 0, 0, 0, 0, 0, 23.492048, -0.0029411316, -8.094779, 0, 0, 0, 0, 0, 0, 26.842072, -0.0029411316, -2.955284, 0, 0, 0, 0, 0, 0, 30.540104, -0.002937317, -0.6889343, 0, 0, 0, 0, 0, 0, 32.266655, -0.0029411316, 7.519619, 0, 0, 0, 0, 0, 0, 29.88159, -0.002937317, 11.079437, 0, 0, 0, 0, 0, 0, 23.867472, -0.002937317, 15.516203, 0, 0, 0, 0, 0, 0, 20.547432, -0.0029411316, 15.771803, 0, 0, 0, 0, 0, 0, 17.486126, -0.0029411316, 10.689701, 0, 0, 0, 0, 0, 0, 12.567432, -0.0029411316, 9.993235, 0, 0, 0, 0, 0, 0, 8.265545, -0.002937317, 16.372534, 0, 0, 0, 0, 0, 0, 1.9048309, -0.002937317, 26.435099, 0, 0, 0, 0, 0, 0, -9.015602, -0.002937317, 34.162575, 0, 0, 0, 0, 0, 0, -19.431889, -0.002937317, 44.664474, 0, 0, 0, 0, 0, 0, -23.387104, -0.002937317, 59.80117, 0, 0, 0, 0, 0, 0, -21.00288, -0.0029335022, 71.96566, 0, 0, 0, 0, 0, 0, -12.79343, -0.0029335022, 78.17484, 0, 0, 0, 0, 0, 0, -3.5642548, -0.002937317, 81.15604, 0, 0, 0, 0, 0, 0, 5.206661, -0.002937317, 82.6712), -"tilts": PackedFloat32Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) -} -point_count = 32 +[ext_resource type="Script" uid="uid://c2k1kj4a2s2su" path="res://World/nav-bake.gd" id="18_navbake"] [sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_6opy2"] panorama = ExtResource("2_5vw27") @@ -67,20 +61,21 @@ script = ExtResource("15_levitate") amplitude = 5.0 period = 8.0 -[node name="rock" type="Path3D" parent="Architecture" unique_id=1419147983] -curve = SubResource("Curve3D_dg77c") - -[node name="PathFollow3D" type="PathFollow3D" parent="Architecture/rock" unique_id=2095285038] -transform = Transform3D(-0.4002386, -7.500245e-07, -0.916411, 5.684342e-14, 1, -8.184369e-07, 0.916411, -3.275701e-07, -0.4002386, 5.2467575, -0.0029411316, 93.87327) +[node name="rock" type="Node3D" parent="Architecture" unique_id=1419147983] +transform = Transform3D(-0.4002386, 0, -0.916411, 0, 1, 0, 0.916411, 0, -0.4002386, 5.2467575, -0.0029411316, 93.87327) script = ExtResource("10_4c57u") -[node name="Rock" parent="Architecture/rock/PathFollow3D" unique_id=1431376827 instance=ExtResource("11_efxa6")] +[node name="Rock" parent="Architecture/rock" unique_id=1431376827 instance=ExtResource("11_efxa6")] -[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Architecture/rock/PathFollow3D/Rock" unique_id=912581689] +[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Architecture/rock/Rock" unique_id=912581689] stream = ExtResource("7_dg77c") autoplay = true max_distance = 30.0 +[node name="NavigationAgent3D" type="NavigationAgent3D" parent="Architecture/rock" unique_id=1616493785] +height = 5.0 +radius = 2.0 + [node name="cyber_office" parent="Architecture" unique_id=1003431047 instance=ExtResource("1_2cqfq")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6.9035664, -0.2606399, -24.004528) @@ -124,7 +119,11 @@ max_height = 55.0 bird_scale = 0.9 [node name="CharacterBody3D" parent="." unique_id=365865796 instance=ExtResource("2_h2yge")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.0830812, 2.6370785, 0.44329834) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.0830812, 5.621685, 0.44329834) [node name="Empire-State" parent="." unique_id=913942978 instance=ExtResource("17_yaehf")] transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 188.18842, -0.0029239655, 0.5043459) + +[node name="Navigation" type="NavigationRegion3D" parent="." unique_id=1471038126] +script = ExtResource("18_navbake") +exclude = Array[NodePath]([NodePath("../Architecture/rock"), NodePath("../CharacterBody3D")]) diff --git a/models/Bochi-Rock/Guitar.ogg b/models/Bochi-Rock/Guitar.ogg index 0c1b553..1e24d25 100644 Binary files a/models/Bochi-Rock/Guitar.ogg and b/models/Bochi-Rock/Guitar.ogg differ diff --git a/models/Bochi-Rock/rock-follow.gd b/models/Bochi-Rock/rock-follow.gd new file mode 100644 index 0000000..be71a4a --- /dev/null +++ b/models/Bochi-Rock/rock-follow.gd @@ -0,0 +1,69 @@ +extends Node3D +# Камень преследует локального игрока, обходя препятствия по навмешу, и ловит его. + +@export var speed := 12.0 # Скорость движения +@export var turn_speed := 6.0 # Как быстро разворачивается к игроку +@export var catch_distance := 3.0 # С какого расстояния (по горизонтали) ловит игрока +@export var catch_height := 4.0 # Игрока выше этого над камнем не достать +@export var repath_distance := 1.0 # Пересчитываем путь, когда игрок сместился на столько + +@onready var agent: NavigationAgent3D = $NavigationAgent3D +@onready var start_transform := global_transform + +var player: Node3D = null + +func _ready() -> void: + agent.target_desired_distance = 1.0 + agent.path_desired_distance = 1.5 + agent.path_height_offset = 0.25 # Навмеш лежит чуть выше пола + +func _physics_process(delta: float) -> void: + if player == null: + player = get_tree().get_first_node_in_group("local_player") as Node3D + if player == null: + return + if player.has_signal("respawned"): + player.respawned.connect(_on_player_respawned) + + # Пока игрок лежит — стоим над ним + if player.get("is_dead"): + return + + var flat := player.global_position - global_position + var dy := flat.y + flat.y = 0.0 + if flat.length() <= catch_distance and absf(dy) <= catch_height: + _face(flat, delta) + if player.has_method("die"): + player.die() + return + + var step: Vector3 + var map := agent.get_navigation_map() + if NavigationServer3D.map_get_iteration_id(map) > 0: + if agent.target_position.distance_to(player.global_position) > repath_distance: + agent.target_position = player.global_position + if not agent.is_navigation_finished(): + step = agent.get_next_path_position() - global_position + + # Навмеш ещё не готов или игрок вне его — идём напрямую + if step == Vector3.ZERO: + step = flat + + var d := step.length() + if d < 0.001: + return + global_position += step / d * min(speed * delta, d) + _face(Vector3(step.x, 0.0, step.z), delta) + +# Игрок возродился — возвращаемся на старт, чтобы не поймать его сразу снова +func _on_player_respawned() -> void: + global_transform = start_transform + agent.target_position = global_position + +# Плавный разворот: модель смотрит вперёд по -Z +func _face(dir: Vector3, delta: float) -> void: + if dir.length_squared() < 0.0001: + return + var target_yaw := atan2(-dir.x, -dir.z) + rotation.y = lerp_angle(rotation.y, target_yaw, 1.0 - exp(-turn_speed * delta)) diff --git a/models/Bochi-Rock/rock-follow.gd.uid b/models/Bochi-Rock/rock-follow.gd.uid new file mode 100644 index 0000000..73125d2 --- /dev/null +++ b/models/Bochi-Rock/rock-follow.gd.uid @@ -0,0 +1 @@ +uid://o2kqd0cy4knr diff --git a/project.godot b/project.godot index 85bf9ff..977fb51 100644 --- a/project.godot +++ b/project.godot @@ -33,3 +33,5 @@ window/stretch/aspect="expand" rendering_device/driver.windows="d3d12" renderer/rendering_method="gl_compatibility" renderer/rendering_method.mobile="gl_compatibility" +lights_and_shadows/directional_shadow/size=2048 +lights_and_shadows/directional_shadow/soft_shadow_filter_quality=1