Files
my-world/models/Bochi-Rock/rock-follow.gd
T
aibrashandClaude Sonnet 5.5 b7ca18ae3d Общий камень-преследователь в мультиплеере
Камень считает один клиент (владелец, назначает сервер), остальные повторяют его позицию.
Цель — ближайший живой игрок; события смерти/возрождения пересылаются через сервер.
Сервер передаёт камень другому, если владелец вышел или замолчал.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 15:22:19 +05:00

126 lines
5.3 KiB
GDScript

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))