Верити: второй преследователь, общая сеть преследователей

- models/verity: модель verity2, звук из видео (verity.ogg), сцена с тем же
  мозгом, что у камня. В Backrooms вместо камня — Верити; в песочнице — Верити и камень.
- Сеть: синхронизация любого числа преследователей по имени (Net.chasers,
  submit_chasers/recv_chasers) вместо одного камня.
- rock-follow.gd: chaser_id, model_scale, emission_energy (чисто светящийся материал).
- Экспорт: models/verity только в пакетах карт.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 17:20:27 +05:00
co-authored by Claude Sonnet 5.5
parent af81050a1a
commit ed4ee86c8e
16 changed files with 288 additions and 27 deletions
+30 -9
View File
@@ -1,10 +1,13 @@
extends Node3D
# Камень преследует ближайшего живого игрока, обходя препятствия по навмешу, и ловит его.
# Преследователь (камень, Верити…): гонится за ближайшим живым игроком, обходя препятствия по навмешу, и ловит его.
# С sight_limited он замечает игрока лишь вблизи или в поле зрения без стен между ними;
# потеряв из виду, идёт к последнему месту, где видел, и дальше бродит по карте.
# В сети камень считает один клиент (владелец, его назначает сервер), остальные
# плавно повторяют его позицию — поэтому у всех игроков камень один и тот же.
@export var chaser_id := "rock" # Имя в сети: по нему владелец и остальные клиенты сопоставляют преследователей
@export var model_scale := 1.0 # Масштаб узла Model (если он есть): уменьшаем в тесных картах
@export var emission_energy := -1.0 # Модель светится сама (чёрная основа, цвет = свечение × это число); меньше 0 — как в модели
@export var speed := 15.0 # Скорость движения
@export var turn_speed := 6.0 # Как быстро разворачивается к игроку
@export var catch_distance := 3.0 # С какого расстояния (по горизонтали) ловит игрока
@@ -43,14 +46,31 @@ func _ready() -> void:
agent.target_desired_distance = 1.0
agent.path_desired_distance = 1.5
agent.path_height_offset = 0.25 # Навмеш лежит чуть выше пола
var model := get_node_or_null("Model")
if model != null and model_scale != 1.0:
model.scale = Vector3.ONE * model_scale
if emission_energy >= 0.0:
_set_emission(emission_energy)
for body in find_children("*", "CollisionObject3D", true, false):
_own_rids.append(body.get_rid()) # Луч зрения не должен упираться в сам камень
Net.rock = self
Net.chasers[chaser_id] = self
Net.peer_respawned.connect(_on_respawned)
# Копируем материалы модели (они общие для всех её экземпляров) и делаем их чисто светящимися:
# без белой основы лицо не выгорает на ярком свету и не пропадает при слабом свечении
func _set_emission(energy: float) -> void:
for mi in find_children("*", "MeshInstance3D", true, false):
for s in mi.mesh.get_surface_count():
var mat := mi.get_active_material(s) as BaseMaterial3D
if mat != null and mat.emission_enabled:
var copy := mat.duplicate() as BaseMaterial3D
copy.albedo_color = Color.BLACK
copy.emission_energy_multiplier = energy
mi.set_surface_override_material(s, copy)
func _exit_tree() -> void:
if Net.rock == self:
Net.rock = null
if Net.chasers.get(chaser_id) == self:
Net.chasers.erase(chaser_id)
func _physics_process(delta: float) -> void:
if player == null:
@@ -223,13 +243,14 @@ func _move_to(dest: Vector3, spd: float, stop_dist: float, delta: float) -> bool
# Камень считает другой клиент — плавно идём к тому, что он прислал
func _follow_owner(delta: float) -> void:
if not Net.rock_has_state:
var state = Net.chaser_states.get(chaser_id)
if state == null:
return
if global_position.distance_to(Net.rock_pos) > snap_distance:
global_position = Net.rock_pos
if global_position.distance_to(state.pos) > snap_distance:
global_position = state.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))
global_position = global_position.lerp(state.pos, 1.0 - exp(-sync_smoothing * delta))
rotation.y = lerp_angle(rotation.y, state.yaw, 1.0 - exp(-sync_smoothing * delta))
# Кто-то (мы или другой игрок) возродился. Если камень рядом с точкой возрождения —
# уводим его на старт, чтобы не поймал снова сразу. Решает только владелец камня.
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cn50f1trssdya"
path="res://.godot/imported/verity.glb-aae30371d8b76c9d120dd8fbd4a369c2.scn"
[deps]
source_file="res://models/verity/verity.glb"
dest_files=["res://.godot/imported/verity.glb-aae30371d8b76c9d120dd8fbd4a369c2.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
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://b7qmenkjh1wdk"
path="res://.godot/imported/verity.ogg-c8d59770635466ff43367f93a436eb90.oggvorbisstr"
[deps]
source_file="res://models/verity/verity.ogg"
dest_files=["res://.godot/imported/verity.ogg-c8d59770635466ff43367f93a436eb90.oggvorbisstr"]
[params]
loop=true
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
+25
View File
@@ -0,0 +1,25 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://models/Bochi-Rock/rock-follow.gd" id="1_script"]
[ext_resource type="PackedScene" path="res://models/verity/verity2.glb" id="2_model"]
[ext_resource type="AudioStream" path="res://models/verity/verity.ogg" id="3_audio"]
[node name="Verity" type="Node3D"]
script = ExtResource("1_script")
chaser_id = "verity"
emission_energy = 1.0
[node name="Model" type="Node3D" parent="."]
[node name="Mesh" parent="Model" instance=ExtResource("2_model")]
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 0, 1.59, 0)
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
height = 5.0
radius = 2.0
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
stream = ExtResource("3_audio")
autoplay = true
max_distance = 35.0
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://wup5p3waumce"
path="res://.godot/imported/verity2.glb-74448d25644050663fdf5f7cb5b2b561.scn"
[deps]
source_file="res://models/verity/verity2.glb"
dest_files=["res://.godot/imported/verity2.glb-74448d25644050663fdf5f7cb5b2b561.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c4fe3eo5c2w6g"
path="res://.godot/imported/verity2_unnamed.webp-5857f5fcb0e2a440b744a4c083bfb79c.ctex"
metadata={
"vram_texture": false
}
generator_parameters={
"md5": "5d3e31c7af9c06dc059d9fa5104b94dd"
}
[deps]
source_file="res://models/verity/verity2_unnamed.webp"
dest_files=["res://.godot/imported/verity2_unnamed.webp-5857f5fcb0e2a440b744a4c083bfb79c.ctex"]
[params]
compress/mode=0
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+44
View File
@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bxiwv7ayauag5"
path.s3tc="res://.godot/imported/verity_unnamed.webp-57398071db19959d3dcf9d9f175390fc.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
generator_parameters={
"md5": "5d3e31c7af9c06dc059d9fa5104b94dd"
}
[deps]
source_file="res://models/verity/verity_unnamed.webp"
dest_files=["res://.godot/imported/verity_unnamed.webp-57398071db19959d3dcf9d9f175390fc.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=0