Добавил преследование

This commit is contained in:
2026-10-27 14:57:54 +05:00
parent 6b34b8593f
commit 823d68ffd6
10 changed files with 242 additions and 18 deletions
+105
View File
@@ -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()
+47
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://c2k1kj4a2s2su
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -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
+15 -16
View File
@@ -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")])
Binary file not shown.
+69
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
uid://o2kqd0cy4knr
+2
View File
@@ -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