extends Node3D # Камень преследует ближайшего живого игрока, обходя препятствия по навмешу, и ловит его. # В сети камень считает один клиент (владелец, его назначает сервер), остальные # плавно повторяют его позицию — поэтому у всех игроков камень один и тот же. @export var speed := 15.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 # Пересчитываем путь, когда игрок сместился на столько @export var respawn_safe_radius := 25.0 # Игрок возродился ближе этого к камню — камень уходит на старт @export var sync_smoothing := 12.0 # Плавность повтора чужого камня @export var snap_distance := 20.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 # Навмеш лежит чуть выше пола Net.rock = self Net.peer_respawned.connect(_on_respawned) func _exit_tree() -> void: if Net.rock == self: Net.rock = null 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 player.respawned.connect(func(): _on_respawned(player.global_position)) if Net.rock_is_local(): _chase(delta) else: _follow_owner(delta) _try_catch() # Своего игрока ловит его собственный клиент — по тому камню, который он видит func _try_catch() -> void: 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: player.die() # Ближайший по горизонтали живой игрок: свой или чужой func _pick_target() -> Node3D: var best: Node3D = null var best_d := INF var candidates: Array[Node3D] = [] if not player.get("is_dead"): candidates.append(player) for id in Net.ghosts: var g: Node3D = Net.ghosts[id] if is_instance_valid(g) and not Net.dead_peers.has(id): candidates.append(g) for c in candidates: var d := Vector2(c.global_position.x - global_position.x, c.global_position.z - global_position.z).length_squared() if d < best_d: best_d = d best = c return best func _chase(delta: float) -> void: var target := _pick_target() if target == null: return # Все лежат — стоим var flat := target.global_position - global_position flat.y = 0.0 if flat.length() <= catch_distance: _face(flat, delta) # Стоим над жертвой, пока её клиент не засчитает поимку return var step: Vector3 var map := agent.get_navigation_map() if NavigationServer3D.map_get_iteration_id(map) > 0: if agent.target_position.distance_to(target.global_position) > repath_distance: agent.target_position = target.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 _follow_owner(delta: float) -> void: if not Net.rock_has_state: return if global_position.distance_to(Net.rock_pos) > snap_distance: global_position = Net.rock_pos else: global_position = global_position.lerp(Net.rock_pos, 1.0 - exp(-sync_smoothing * delta)) rotation.y = lerp_angle(rotation.y, Net.rock_yaw, 1.0 - exp(-sync_smoothing * delta)) # Кто-то (мы или другой игрок) возродился. Если камень рядом с точкой возрождения — # уводим его на старт, чтобы не поймал снова сразу. Решает только владелец камня. func _on_respawned(pos: Vector3) -> void: if not Net.rock_is_local(): return if global_position.distance_to(pos) < respawn_safe_radius: 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))