- Перенос: Net→net, Player→player, World/*→maps/{hub,sandbox,backrooms}, core, objects,
models/*→chasers/{rock,verity} и maps/*/models. Имена внутри папок с .glb не менялись
(рядом лежат извлечённые текстуры). Все пути, пресеты экспорта и автолоады обновлены.
- Преследователи: rock-follow.gd → chasers/chaser.gd; камень в песочнице теперь инстанс
chasers/rock/rock.tscn (как Верити).
- Удалён мусор: неиспользуемое небо sunflowers, Tree_01.glb, rock-path.gd, старая
модель Верити, unwrap_cache (теперь в .gitignore).
- Основной пакет игры 70 КБ (камень и Верити уехали в пакеты карт).
- tools/: export_all.sh, serve_web.py (nginx-подобный dev-сервер), deploy_server.sh,
test/ (боты, временный проект и сервер). Net: флаг --client для headless-клиента.
- deploy/: боевые nginx.conf и my-world.service, project.godot выделенного сервера.
- README переписан: что есть и чего нет, структура, карты, преследователи, сеть, пакеты,
деплой, грабли, дорожная карта (телефон).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
128 lines
4.4 KiB
GDScript
128 lines
4.4 KiB
GDScript
@tool
|
|
extends Node3D
|
|
# Лес одним MultiMesh: одна отрисовка на все деревья, удобно для веба.
|
|
|
|
@export var tree_scene: PackedScene:
|
|
set(v):
|
|
tree_scene = v
|
|
_queue_rebuild()
|
|
@export var count := 60:
|
|
set(v):
|
|
count = v
|
|
_queue_rebuild()
|
|
@export var area_radius := 220.0 # Радиус леса вокруг центра
|
|
@export var spawn_clear_radius := 12.0 # Свободная зона вокруг старта
|
|
@export var building_clear_radius := 14.0 # Свободная зона вокруг построек
|
|
@export var path_clear_radius := 7.0 # Свободная зона вокруг пути камня
|
|
@export var scale_range := Vector2(1.3, 2.0)
|
|
@export var seed_value := 1:
|
|
set(v):
|
|
seed_value = v
|
|
_queue_rebuild()
|
|
@export var visible_distance := 160.0 # Дальше — деревья не рисуются
|
|
@export var trunk_radius := 1.4 # Коллизия ствола (до масштабирования)
|
|
@export var trunk_height := 9.0
|
|
@export var cast_shadows := false # Тени от 150 деревьев тяжелы для веба
|
|
|
|
func _ready() -> void:
|
|
_rebuild()
|
|
|
|
func _queue_rebuild() -> void:
|
|
if is_node_ready():
|
|
_rebuild.call_deferred()
|
|
|
|
func _rebuild() -> void:
|
|
if tree_scene == null:
|
|
return
|
|
for n in ["Trees", "TrunkColliders"]:
|
|
var old := get_node_or_null(n)
|
|
if old:
|
|
remove_child(old)
|
|
old.queue_free()
|
|
var inst := tree_scene.instantiate()
|
|
var mi := _find_mesh(inst)
|
|
if mi == null:
|
|
push_error("forest: в tree_scene нет MeshInstance3D")
|
|
return
|
|
var mesh := mi.mesh
|
|
var base := mi.transform # смещение меша внутри glb
|
|
|
|
var keep_out := _collect_keep_out()
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = seed_value
|
|
|
|
var xforms: Array[Transform3D] = []
|
|
var tries := 0
|
|
while xforms.size() < count and tries < count * 30:
|
|
tries += 1
|
|
var p := Vector2.from_angle(rng.randf() * TAU) * sqrt(rng.randf()) * area_radius
|
|
if p.length() < spawn_clear_radius or _blocked(p, keep_out):
|
|
continue
|
|
var s := rng.randf_range(scale_range.x, scale_range.y)
|
|
var b := Basis(Vector3.UP, rng.randf() * TAU).scaled(Vector3(s, s * rng.randf_range(0.9, 1.15), s))
|
|
xforms.append(Transform3D(b, Vector3(p.x, 0.0, p.y)) * base)
|
|
|
|
var mm := MultiMesh.new()
|
|
mm.transform_format = MultiMesh.TRANSFORM_3D
|
|
mm.mesh = mesh
|
|
mm.instance_count = xforms.size()
|
|
for i in xforms.size():
|
|
mm.set_instance_transform(i, xforms[i])
|
|
|
|
var mmi := MultiMeshInstance3D.new()
|
|
mmi.name = "Trees"
|
|
mmi.multimesh = mm
|
|
mmi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
mmi.visibility_range_end = visible_distance
|
|
mmi.visibility_range_end_margin = 20.0
|
|
mmi.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_DISABLED
|
|
mmi.custom_aabb = AABB(Vector3(-area_radius - 20, -5, -area_radius - 20), Vector3(area_radius * 2 + 40, 60, area_radius * 2 + 40))
|
|
add_child(mmi)
|
|
inst.free()
|
|
if not Engine.is_editor_hint():
|
|
_add_colliders(xforms)
|
|
|
|
# Мультимеш коллизий не даёт — ставим по цилиндру на ствол каждого дерева
|
|
func _add_colliders(xforms: Array[Transform3D]) -> void:
|
|
var body := StaticBody3D.new()
|
|
body.name = "TrunkColliders"
|
|
add_child(body)
|
|
for t in xforms:
|
|
var s := t.basis.get_scale()
|
|
var shape := CylinderShape3D.new()
|
|
shape.radius = trunk_radius * s.x
|
|
shape.height = trunk_height * s.y
|
|
var cs := CollisionShape3D.new()
|
|
cs.shape = shape
|
|
cs.position = Vector3(t.origin.x, t.origin.y + shape.height * 0.5, t.origin.z)
|
|
body.add_child(cs)
|
|
|
|
func _find_mesh(n: Node) -> MeshInstance3D:
|
|
if n is MeshInstance3D:
|
|
return n
|
|
for c in n.get_children():
|
|
var r := _find_mesh(c)
|
|
if r:
|
|
return r
|
|
return null
|
|
|
|
# Круги (x, z, радиус), куда деревья ставить нельзя
|
|
func _collect_keep_out() -> Array[Vector3]:
|
|
var out: Array[Vector3] = []
|
|
var arch := get_node_or_null("../Architecture")
|
|
if arch:
|
|
for c in arch.get_children():
|
|
if c is Path3D:
|
|
for pt in c.curve.get_baked_points():
|
|
var g: Vector3 = c.to_global(pt)
|
|
out.append(Vector3(g.x, g.z, path_clear_radius))
|
|
elif c is Node3D:
|
|
out.append(Vector3(c.global_position.x, c.global_position.z, building_clear_radius))
|
|
return out
|
|
|
|
func _blocked(p: Vector2, keep_out: Array[Vector3]) -> bool:
|
|
for k in keep_out:
|
|
if p.distance_squared_to(Vector2(k.x, k.y)) < k.z * k.z:
|
|
return true
|
|
return false
|