1209 lines
35 KiB
GDScript
1209 lines
35 KiB
GDScript
class_name HomeWorldService
|
|
extends Node3D
|
|
|
|
const NetworkHomeProtocolType = preload(
|
|
"res://network/network_home_protocol.gd"
|
|
)
|
|
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
|
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
|
const STARTER_MOTORCOACH_SCENE: PackedScene = preload(
|
|
"res://homes/assets/starter_motorcoach.glb"
|
|
)
|
|
|
|
const EXTERIOR_BODY_SIZE := Vector3(5.6, 2.5, 2.35)
|
|
const EXTERIOR_DOOR_LOCAL := Vector3(-0.2, 0.85, 1.48)
|
|
const EXTERIOR_SPAWN_LOCAL := Vector3(-0.2, 0.25, 2.45)
|
|
const EXTERIOR_INTERACTION_DISTANCE: float = 2.7
|
|
const INTERIOR_EXIT_DISTANCE: float = 2.1
|
|
const INTERIOR_ORIGIN := Vector3(1000.0, 300.0, 1000.0)
|
|
const INTERIOR_SPACING: float = 14.0
|
|
const INTERIOR_MAX_SPAWN_SLOTS: int = 64
|
|
const INTERIOR_FLOOR_SIZE := Vector3(8.0, 0.18, 4.5)
|
|
const INTERIOR_WALL_HEIGHT: float = 2.7
|
|
const MIN_EXTERIOR_TRIANGLE_AREA: float = 1.0
|
|
const MIN_EXTERIOR_SEPARATION: float = 9.0
|
|
const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(3.85, 2.55)
|
|
const EXTERIOR_SURFACE_INDEX_CELL_SIZE: float = 5.0
|
|
const EXTERIOR_MAX_SURFACE_HEIGHT_DELTA: float = 0.24
|
|
const EXTERIOR_CLEARANCE_HEIGHT: float = 2.7
|
|
const WORLD_GEOMETRY_COLLISION_LAYER: int = 1
|
|
const DECOR_SELECTION_COLLISION_LAYER: int = 1 << 8
|
|
# Includes the player's 0.45 m capsule radius plus enough movement margin for
|
|
# a full low-tick-rate frame before a newly solid placement can be reached.
|
|
const PLAYER_DECOR_CLEARANCE_RADIUS: float = 0.8
|
|
const BASIC_CUBE_SIZE := Vector3(1.0, 0.8, 0.8)
|
|
|
|
var _world: TestWorld
|
|
var _spawn_service: PlayerSpawnService
|
|
var _session: NetworkSession
|
|
var _dedicated_runtime: bool = false
|
|
var _exteriors_root: Node3D
|
|
var _interiors_root: Node3D
|
|
var _presentations_by_fingerprint: Dictionary[String, Dictionary] = {}
|
|
var _local_space: StringName = NetworkHomeProtocolType.WORLD_SPACE
|
|
var _decor_under_manipulation: Dictionary[String, bool] = {}
|
|
var _pending_solid_decor: Dictionary[String, Dictionary] = {}
|
|
|
|
|
|
func setup(
|
|
world: TestWorld,
|
|
spawn_service: PlayerSpawnService,
|
|
session: NetworkSession,
|
|
dedicated_runtime: bool,
|
|
) -> void:
|
|
_world = world
|
|
_spawn_service = spawn_service
|
|
_session = session
|
|
_dedicated_runtime = dedicated_runtime
|
|
_ensure_roots()
|
|
set_process(true)
|
|
|
|
|
|
func is_world_ready() -> bool:
|
|
return _world != null and _world.is_world_ready()
|
|
|
|
|
|
func clear_presentations() -> void:
|
|
_presentations_by_fingerprint.clear()
|
|
_decor_under_manipulation.clear()
|
|
_pending_solid_decor.clear()
|
|
if is_instance_valid(_exteriors_root):
|
|
for child: Node in _exteriors_root.get_children():
|
|
child.queue_free()
|
|
if is_instance_valid(_interiors_root):
|
|
for child: Node in _interiors_root.get_children():
|
|
child.queue_free()
|
|
|
|
|
|
func remove_presentation(owner_fingerprint: String) -> void:
|
|
var presentation: Dictionary = _presentations_by_fingerprint.get(
|
|
owner_fingerprint, {}
|
|
)
|
|
if presentation.is_empty():
|
|
return
|
|
_clear_decor_collision_tracking(owner_fingerprint)
|
|
_free_presentation_nodes(presentation)
|
|
_presentations_by_fingerprint.erase(owner_fingerprint)
|
|
|
|
|
|
func apply_manifest(data: Dictionary) -> bool:
|
|
if (
|
|
not is_world_ready()
|
|
or not NetworkHomeProtocolType.validate_manifest_broadcast(data)
|
|
):
|
|
return false
|
|
_ensure_roots()
|
|
var fingerprint: String = str(data["owner_fingerprint"])
|
|
var prior: Dictionary = _presentations_by_fingerprint.get(fingerprint, {})
|
|
var prior_instance_ids: Dictionary[String, bool] = {}
|
|
if not prior.is_empty():
|
|
var prior_home: Dictionary = prior.get("home", {})
|
|
for value: Variant in prior_home.get("placements", []):
|
|
if typeof(value) == TYPE_DICTIONARY:
|
|
prior_instance_ids[str(
|
|
(value as Dictionary).get("instance_id", "")
|
|
)] = true
|
|
if not prior.is_empty():
|
|
_clear_decor_collision_tracking(fingerprint)
|
|
_free_presentation_nodes(prior)
|
|
var exterior_transform := NetworkHomeProtocolType.array_to_transform(
|
|
data["exterior_transform"]
|
|
)
|
|
var interior_index: int = int(data["interior_index"])
|
|
var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data(
|
|
data["home"]
|
|
)
|
|
var exterior := _build_exterior(fingerprint, exterior_transform)
|
|
var interior := _build_interior(fingerprint, interior_index, home_data)
|
|
var presentation: Dictionary = {
|
|
"owner_peer_id": int(data["owner_peer_id"]),
|
|
"fingerprint": fingerprint,
|
|
"home": home_data,
|
|
"exterior_transform": exterior_transform,
|
|
"interior_index": interior_index,
|
|
"exterior": exterior,
|
|
"interior": interior,
|
|
}
|
|
_presentations_by_fingerprint[fingerprint] = presentation
|
|
_refresh_local_visibility()
|
|
if not prior.is_empty():
|
|
for value: Variant in home_data.get("placements", []):
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
var instance_id: String = str(
|
|
(value as Dictionary).get("instance_id", "")
|
|
)
|
|
if not instance_id.is_empty() and not prior_instance_ids.has(
|
|
instance_id
|
|
):
|
|
call_deferred(
|
|
"_spawn_decor_poof", fingerprint, instance_id
|
|
)
|
|
return true
|
|
|
|
|
|
func choose_exterior_transform(
|
|
owner_peer_id: int,
|
|
reserved_transforms: Array[Transform3D],
|
|
) -> Transform3D:
|
|
var spawn_transform: Transform3D = (
|
|
_world.get_player_spawn_transform()
|
|
if _world != null else Transform3D.IDENTITY
|
|
)
|
|
var triangles: Array[PackedVector3Array] = (
|
|
_world.get_spawn_surface_triangles(
|
|
[&"grass_lite", &"grass", &"sand"], -0.5
|
|
)
|
|
if _world != null else []
|
|
)
|
|
if not triangles.is_empty():
|
|
var surface_index: Dictionary = _build_surface_index(triangles)
|
|
var strict_result: Transform3D = _find_exterior_transform(
|
|
owner_peer_id,
|
|
triangles,
|
|
surface_index,
|
|
spawn_transform,
|
|
reserved_transforms,
|
|
true,
|
|
true,
|
|
)
|
|
if strict_result != Transform3D.IDENTITY:
|
|
return strict_result
|
|
var relaxed_result: Transform3D = _find_exterior_transform(
|
|
owner_peer_id,
|
|
triangles,
|
|
surface_index,
|
|
spawn_transform,
|
|
reserved_transforms,
|
|
false,
|
|
true,
|
|
)
|
|
if relaxed_result != Transform3D.IDENTITY:
|
|
return relaxed_result
|
|
var dry_result: Transform3D = _find_exterior_transform(
|
|
owner_peer_id,
|
|
triangles,
|
|
surface_index,
|
|
spawn_transform,
|
|
reserved_transforms,
|
|
false,
|
|
false,
|
|
)
|
|
if dry_result != Transform3D.IDENTITY:
|
|
push_warning(
|
|
"RV exterior used a dry fallback with obstructed clearance."
|
|
)
|
|
return dry_result
|
|
push_error("No dry, level RV exterior footprint was available.")
|
|
var fallback: Transform3D = spawn_transform
|
|
var column: int = posmod(owner_peer_id - 1, 4)
|
|
var row: int = maxi(floori(float(owner_peer_id - 1) / 4.0), 0)
|
|
fallback.origin += Vector3(
|
|
-12.0 + float(column) * 8.0,
|
|
0.0,
|
|
8.0 + float(row) * 7.0,
|
|
)
|
|
return fallback
|
|
|
|
|
|
func _find_exterior_transform(
|
|
owner_peer_id: int,
|
|
triangles: Array[PackedVector3Array],
|
|
surface_index: Dictionary,
|
|
spawn_transform: Transform3D,
|
|
reserved_transforms: Array[Transform3D],
|
|
require_spawn_clearance: bool,
|
|
require_obstacle_clearance: bool,
|
|
) -> Transform3D:
|
|
var triangle_count: int = triangles.size()
|
|
var start: int = posmod(owner_peer_id * 37, triangle_count)
|
|
var step: int = _coprime_step(triangle_count, 29)
|
|
for offset: int in triangle_count:
|
|
var triangle: PackedVector3Array = triangles[
|
|
(start + offset * step) % triangle_count
|
|
]
|
|
if not _is_exterior_candidate_triangle(triangle):
|
|
continue
|
|
var position: Vector3 = (
|
|
triangle[0] + triangle[1] + triangle[2]
|
|
) / 3.0
|
|
if (
|
|
require_spawn_clearance
|
|
and position.distance_to(spawn_transform.origin) < 6.0
|
|
):
|
|
continue
|
|
if _too_close_to_reserved(position, reserved_transforms):
|
|
continue
|
|
var yaw: float = float(posmod(owner_peer_id + offset, 4)) * PI * 0.5
|
|
var grounded_position: Vector3 = _ground_exterior_footprint(
|
|
position, yaw, triangles, surface_index
|
|
)
|
|
if not grounded_position.is_finite():
|
|
continue
|
|
if (
|
|
require_obstacle_clearance
|
|
and not _exterior_volume_is_clear(grounded_position, yaw)
|
|
):
|
|
continue
|
|
return Transform3D(Basis(Vector3.UP, yaw), grounded_position)
|
|
return Transform3D.IDENTITY
|
|
|
|
|
|
func _is_exterior_candidate_triangle(triangle: PackedVector3Array) -> bool:
|
|
if triangle.size() != 3:
|
|
return false
|
|
var normal: Vector3 = (triangle[1] - triangle[0]).cross(
|
|
triangle[2] - triangle[0]
|
|
)
|
|
var area: float = normal.length() * 0.5
|
|
return (
|
|
area >= MIN_EXTERIOR_TRIANGLE_AREA
|
|
and absf(normal.normalized().y) >= 0.94
|
|
)
|
|
|
|
|
|
func _ground_exterior_footprint(
|
|
position: Vector3,
|
|
yaw: float,
|
|
triangles: Array[PackedVector3Array],
|
|
surface_index: Dictionary,
|
|
) -> Vector3:
|
|
var heights: Array[float] = []
|
|
for local_sample: Vector2 in _exterior_footprint_samples():
|
|
var rotated_sample: Vector3 = Vector3(
|
|
local_sample.x, 0.0, local_sample.y
|
|
).rotated(Vector3.UP, yaw)
|
|
var sample := Vector2(
|
|
position.x + rotated_sample.x,
|
|
position.z + rotated_sample.z,
|
|
)
|
|
var height: float = _surface_height_at(
|
|
sample, position.y, triangles, surface_index
|
|
)
|
|
if not is_finite(height):
|
|
return Vector3.INF
|
|
heights.append(height)
|
|
var minimum_height: float = heights.min()
|
|
var maximum_height: float = heights.max()
|
|
if maximum_height - minimum_height > EXTERIOR_MAX_SURFACE_HEIGHT_DELTA:
|
|
return Vector3.INF
|
|
return Vector3(position.x, maximum_height, position.z)
|
|
|
|
|
|
func _exterior_footprint_samples() -> Array[Vector2]:
|
|
var samples: Array[Vector2] = []
|
|
for x_factor: float in [-1.0, -0.5, 0.0, 0.5, 1.0]:
|
|
for z_factor: float in [-1.0, 0.0, 1.0]:
|
|
samples.append(Vector2(
|
|
EXTERIOR_FOOTPRINT_HALF_EXTENTS.x * x_factor,
|
|
EXTERIOR_FOOTPRINT_HALF_EXTENTS.y * z_factor,
|
|
))
|
|
return samples
|
|
|
|
|
|
func _build_surface_index(
|
|
triangles: Array[PackedVector3Array],
|
|
) -> Dictionary:
|
|
var result: Dictionary = {}
|
|
for triangle_index: int in triangles.size():
|
|
var triangle: PackedVector3Array = triangles[triangle_index]
|
|
if triangle.size() != 3:
|
|
continue
|
|
var minimum_x: float = minf(
|
|
triangle[0].x, minf(triangle[1].x, triangle[2].x)
|
|
)
|
|
var maximum_x: float = maxf(
|
|
triangle[0].x, maxf(triangle[1].x, triangle[2].x)
|
|
)
|
|
var minimum_z: float = minf(
|
|
triangle[0].z, minf(triangle[1].z, triangle[2].z)
|
|
)
|
|
var maximum_z: float = maxf(
|
|
triangle[0].z, maxf(triangle[1].z, triangle[2].z)
|
|
)
|
|
var minimum_cell := Vector2i(
|
|
floori(minimum_x / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
|
|
floori(minimum_z / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
|
|
)
|
|
var maximum_cell := Vector2i(
|
|
floori(maximum_x / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
|
|
floori(maximum_z / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
|
|
)
|
|
for cell_x: int in range(minimum_cell.x, maximum_cell.x + 1):
|
|
for cell_y: int in range(minimum_cell.y, maximum_cell.y + 1):
|
|
var cell := Vector2i(cell_x, cell_y)
|
|
var bucket: Array = result.get(cell, [])
|
|
bucket.append(triangle_index)
|
|
result[cell] = bucket
|
|
return result
|
|
|
|
|
|
func _surface_height_at(
|
|
point: Vector2,
|
|
preferred_height: float,
|
|
triangles: Array[PackedVector3Array],
|
|
surface_index: Dictionary,
|
|
) -> float:
|
|
var cell := Vector2i(
|
|
floori(point.x / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
|
|
floori(point.y / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
|
|
)
|
|
var best_height: float = INF
|
|
var best_delta: float = INF
|
|
for value: Variant in surface_index.get(cell, []):
|
|
var triangle: PackedVector3Array = triangles[int(value)]
|
|
var height: float = _triangle_height_at(point, triangle)
|
|
if not is_finite(height):
|
|
continue
|
|
var delta: float = absf(height - preferred_height)
|
|
if delta < best_delta:
|
|
best_delta = delta
|
|
best_height = height
|
|
return best_height
|
|
|
|
|
|
func _triangle_height_at(
|
|
point: Vector2,
|
|
triangle: PackedVector3Array,
|
|
) -> float:
|
|
var a := Vector2(triangle[0].x, triangle[0].z)
|
|
var b := Vector2(triangle[1].x, triangle[1].z)
|
|
var c := Vector2(triangle[2].x, triangle[2].z)
|
|
var denominator: float = (
|
|
(b.y - c.y) * (a.x - c.x)
|
|
+ (c.x - b.x) * (a.y - c.y)
|
|
)
|
|
if absf(denominator) <= 0.000001:
|
|
return INF
|
|
var weight_a: float = (
|
|
(b.y - c.y) * (point.x - c.x)
|
|
+ (c.x - b.x) * (point.y - c.y)
|
|
) / denominator
|
|
var weight_b: float = (
|
|
(c.y - a.y) * (point.x - c.x)
|
|
+ (a.x - c.x) * (point.y - c.y)
|
|
) / denominator
|
|
var weight_c: float = 1.0 - weight_a - weight_b
|
|
const EDGE_EPSILON: float = 0.001
|
|
if (
|
|
weight_a < -EDGE_EPSILON
|
|
or weight_b < -EDGE_EPSILON
|
|
or weight_c < -EDGE_EPSILON
|
|
):
|
|
return INF
|
|
return (
|
|
weight_a * triangle[0].y
|
|
+ weight_b * triangle[1].y
|
|
+ weight_c * triangle[2].y
|
|
)
|
|
|
|
|
|
func _exterior_volume_is_clear(position: Vector3, yaw: float) -> bool:
|
|
if not is_inside_tree() or get_world_3d() == null:
|
|
return true
|
|
var shape := BoxShape3D.new()
|
|
shape.size = Vector3(
|
|
EXTERIOR_FOOTPRINT_HALF_EXTENTS.x * 2.0,
|
|
EXTERIOR_CLEARANCE_HEIGHT,
|
|
EXTERIOR_FOOTPRINT_HALF_EXTENTS.y * 2.0,
|
|
)
|
|
var query := PhysicsShapeQueryParameters3D.new()
|
|
query.shape = shape
|
|
query.transform = Transform3D(
|
|
Basis(Vector3.UP, yaw),
|
|
position + Vector3.UP * (EXTERIOR_CLEARANCE_HEIGHT * 0.5 + 0.2),
|
|
)
|
|
query.collision_mask = 1
|
|
query.collide_with_areas = false
|
|
query.collide_with_bodies = true
|
|
return get_world_3d().direct_space_state.intersect_shape(query, 1).is_empty()
|
|
|
|
|
|
func _coprime_step(value: int, preferred_step: int) -> int:
|
|
var step: int = mini(preferred_step, maxi(value - 1, 1))
|
|
while step > 1 and _greatest_common_divisor(step, value) != 1:
|
|
step -= 1
|
|
return step
|
|
|
|
|
|
func _greatest_common_divisor(first: int, second: int) -> int:
|
|
var a: int = absi(first)
|
|
var b: int = absi(second)
|
|
while b != 0:
|
|
var remainder: int = a % b
|
|
a = b
|
|
b = remainder
|
|
return a
|
|
|
|
|
|
func get_exterior_transform(owner_fingerprint: String) -> Transform3D:
|
|
var presentation: Dictionary = _presentations_by_fingerprint.get(
|
|
owner_fingerprint, {}
|
|
)
|
|
return presentation.get("exterior_transform", Transform3D.IDENTITY)
|
|
|
|
|
|
func get_exterior_prompt_position(owner_fingerprint: String) -> Vector3:
|
|
return get_exterior_transform(owner_fingerprint) * EXTERIOR_DOOR_LOCAL
|
|
|
|
|
|
func get_exterior_spawn_transform(owner_fingerprint: String) -> Transform3D:
|
|
var exterior_transform: Transform3D = get_exterior_transform(
|
|
owner_fingerprint
|
|
)
|
|
return Transform3D(
|
|
exterior_transform.basis,
|
|
exterior_transform * EXTERIOR_SPAWN_LOCAL,
|
|
)
|
|
|
|
|
|
func get_interior_spawn_transform(
|
|
owner_fingerprint: String,
|
|
visitor_offset: int = 0,
|
|
) -> Transform3D:
|
|
var anchor: Vector3 = get_interior_anchor(owner_fingerprint)
|
|
# `visitor_offset` is allocated by the host as a compact room-local slot.
|
|
# Keep it bounded as a last line of defense if a future caller accidentally
|
|
# supplies an engine peer ID instead of a slot.
|
|
var visitor_slot: int = posmod(visitor_offset, INTERIOR_MAX_SPAWN_SLOTS)
|
|
# Build the origin as a value rather than mutating Transform3D.origin.
|
|
var spawn_origin: Vector3 = anchor + Vector3(
|
|
-1.0 + float(posmod(visitor_slot, 4)) * 0.65,
|
|
0.25,
|
|
0.4 + float(floori(float(visitor_slot) / 4.0)) * 0.55,
|
|
)
|
|
return Transform3D(Basis.IDENTITY, spawn_origin)
|
|
|
|
|
|
func get_interior_anchor(owner_fingerprint: String) -> Vector3:
|
|
var presentation: Dictionary = _presentations_by_fingerprint.get(
|
|
owner_fingerprint, {}
|
|
)
|
|
var index: int = int(presentation.get("interior_index", 0))
|
|
return INTERIOR_ORIGIN + Vector3(float(index) * INTERIOR_SPACING, 0.0, 0.0)
|
|
|
|
|
|
func is_avatar_near_exterior(avatar: Player, owner_fingerprint: String) -> bool:
|
|
if avatar == null or not _presentations_by_fingerprint.has(owner_fingerprint):
|
|
return false
|
|
var transform: Transform3D = get_exterior_transform(owner_fingerprint)
|
|
var door_position: Vector3 = transform * EXTERIOR_DOOR_LOCAL
|
|
return avatar.global_position.distance_to(door_position) <= (
|
|
EXTERIOR_INTERACTION_DISTANCE
|
|
)
|
|
|
|
|
|
func is_avatar_near_interior_exit(
|
|
avatar: Player,
|
|
owner_fingerprint: String,
|
|
) -> bool:
|
|
if avatar == null:
|
|
return false
|
|
return avatar.global_position.distance_to(
|
|
get_interior_exit_position(owner_fingerprint)
|
|
) <= INTERIOR_EXIT_DISTANCE
|
|
|
|
|
|
func get_interior_exit_position(owner_fingerprint: String) -> Vector3:
|
|
return get_interior_anchor(owner_fingerprint) + Vector3(
|
|
-INTERIOR_FLOOR_SIZE.x * 0.5 + 0.6,
|
|
0.35,
|
|
0.0,
|
|
)
|
|
|
|
|
|
func get_interior_prompt_position(owner_fingerprint: String) -> Vector3:
|
|
return get_interior_exit_position(owner_fingerprint) + Vector3.UP * 1.2
|
|
|
|
|
|
func interior_local_to_global(
|
|
owner_fingerprint: String,
|
|
local_position: Vector3,
|
|
) -> Vector3:
|
|
return get_interior_anchor(owner_fingerprint) + local_position
|
|
|
|
|
|
func interior_global_to_local(
|
|
owner_fingerprint: String,
|
|
global_position: Vector3,
|
|
) -> Vector3:
|
|
return global_position - get_interior_anchor(owner_fingerprint)
|
|
|
|
|
|
func get_floor_position_from_screen(
|
|
owner_fingerprint: String,
|
|
camera: Camera3D,
|
|
screen_position: Vector2,
|
|
) -> Vector3:
|
|
if camera == null or owner_fingerprint.is_empty():
|
|
return Vector3.INF
|
|
var ray_origin: Vector3 = camera.project_ray_origin(screen_position)
|
|
var ray_direction: Vector3 = camera.project_ray_normal(screen_position)
|
|
if absf(ray_direction.y) <= 0.0001:
|
|
return Vector3.INF
|
|
var floor_y: float = get_interior_anchor(owner_fingerprint).y + 0.1
|
|
var distance: float = (floor_y - ray_origin.y) / ray_direction.y
|
|
if distance <= 0.0:
|
|
return Vector3.INF
|
|
return interior_global_to_local(
|
|
owner_fingerprint,
|
|
ray_origin + ray_direction * distance,
|
|
)
|
|
|
|
|
|
func find_decor_instance_at_screen(
|
|
owner_fingerprint: String,
|
|
camera: Camera3D,
|
|
screen_position: Vector2,
|
|
) -> String:
|
|
if camera == null or owner_fingerprint.is_empty():
|
|
return ""
|
|
var ray_origin: Vector3 = camera.project_ray_origin(screen_position)
|
|
var ray_end: Vector3 = ray_origin + camera.project_ray_normal(
|
|
screen_position
|
|
) * 50.0
|
|
var query := PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
|
|
query.collision_mask = DECOR_SELECTION_COLLISION_LAYER
|
|
query.collide_with_areas = false
|
|
var result: Dictionary = camera.get_world_3d().direct_space_state.intersect_ray(
|
|
query
|
|
)
|
|
var collider: Object = result.get("collider")
|
|
if collider is Node and (collider as Node).has_meta(&"decor_instance_id"):
|
|
if str((collider as Node).get_meta(&"decor_owner_fingerprint")) == (
|
|
owner_fingerprint
|
|
):
|
|
return str((collider as Node).get_meta(&"decor_instance_id"))
|
|
return ""
|
|
|
|
|
|
func get_decor_preview_transform(
|
|
owner_fingerprint: String,
|
|
instance_id: String,
|
|
) -> Dictionary:
|
|
var decor: Node3D = _get_decor_presentation(
|
|
owner_fingerprint, instance_id
|
|
)
|
|
if decor == null:
|
|
return {}
|
|
return {
|
|
"position": Vector3(decor.position.x, 0.0, decor.position.z),
|
|
"yaw": decor.rotation.y,
|
|
}
|
|
|
|
|
|
func set_decor_preview_transform(
|
|
owner_fingerprint: String,
|
|
instance_id: String,
|
|
position: Vector3,
|
|
yaw_radians: float,
|
|
) -> bool:
|
|
var decor: Node3D = _get_decor_presentation(
|
|
owner_fingerprint, instance_id
|
|
)
|
|
if decor == null or not position.is_finite() or not is_finite(yaw_radians):
|
|
return false
|
|
decor.position = Vector3(position.x, 0.18, position.z)
|
|
decor.rotation.y = yaw_radians
|
|
return true
|
|
|
|
|
|
func set_decor_manipulation_active(
|
|
owner_fingerprint: String,
|
|
instance_id: String,
|
|
active: bool,
|
|
) -> bool:
|
|
var decor: Node3D = _get_decor_presentation(
|
|
owner_fingerprint, instance_id
|
|
)
|
|
if decor == null:
|
|
return false
|
|
var body: StaticBody3D = decor.get_node_or_null(
|
|
"BasicCubeCollision"
|
|
) as StaticBody3D
|
|
if body == null:
|
|
return false
|
|
var key: String = _decor_collision_key(
|
|
owner_fingerprint, instance_id
|
|
)
|
|
if active:
|
|
_decor_under_manipulation[key] = true
|
|
_pending_solid_decor.erase(key)
|
|
body.collision_layer = DECOR_SELECTION_COLLISION_LAYER
|
|
return true
|
|
_decor_under_manipulation.erase(key)
|
|
_queue_decor_world_collision(
|
|
owner_fingerprint,
|
|
instance_id,
|
|
StringName(str(decor.get_meta(&"decor_id", ""))),
|
|
body,
|
|
)
|
|
return true
|
|
|
|
|
|
func is_decor_placement_clear_of_players(
|
|
owner_fingerprint: String,
|
|
decor_id: StringName,
|
|
local_position: Vector3,
|
|
yaw_radians: float,
|
|
) -> bool:
|
|
if (
|
|
owner_fingerprint.is_empty()
|
|
or not DecorCatalogType.has_product(decor_id)
|
|
or not local_position.is_finite()
|
|
or not is_finite(yaw_radians)
|
|
):
|
|
return false
|
|
if _spawn_service == null:
|
|
return true
|
|
var global_center: Vector3 = interior_local_to_global(
|
|
owner_fingerprint, local_position
|
|
)
|
|
var half_size: Vector2 = DecorCatalogType.get_footprint(decor_id) * 0.5
|
|
for peer_id: int in _spawn_service.get_peer_ids():
|
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
|
if avatar == null or not is_instance_valid(avatar):
|
|
continue
|
|
var offset := Vector2(
|
|
avatar.global_position.x - global_center.x,
|
|
avatar.global_position.z - global_center.z,
|
|
).rotated(-yaw_radians)
|
|
var nearest := Vector2(
|
|
clampf(offset.x, -half_size.x, half_size.x),
|
|
clampf(offset.y, -half_size.y, half_size.y),
|
|
)
|
|
if offset.distance_squared_to(nearest) < (
|
|
PLAYER_DECOR_CLEARANCE_RADIUS
|
|
* PLAYER_DECOR_CLEARANCE_RADIUS
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _get_decor_presentation(
|
|
owner_fingerprint: String,
|
|
instance_id: String,
|
|
) -> Node3D:
|
|
if owner_fingerprint.is_empty() or instance_id.is_empty():
|
|
return null
|
|
var presentation: Dictionary = _presentations_by_fingerprint.get(
|
|
owner_fingerprint, {}
|
|
)
|
|
var interior: Node3D = presentation.get("interior")
|
|
if not is_instance_valid(interior):
|
|
return null
|
|
return interior.get_node_or_null(
|
|
"Decor/%s" % instance_id
|
|
) as Node3D
|
|
|
|
|
|
func find_nearby_exterior_owner(avatar: Player) -> String:
|
|
if avatar == null:
|
|
return ""
|
|
var closest: String = ""
|
|
var closest_distance: float = EXTERIOR_INTERACTION_DISTANCE
|
|
for fingerprint: String in _presentations_by_fingerprint:
|
|
var transform: Transform3D = get_exterior_transform(fingerprint)
|
|
var distance: float = avatar.global_position.distance_to(
|
|
transform * EXTERIOR_DOOR_LOCAL
|
|
)
|
|
if distance <= closest_distance:
|
|
closest = fingerprint
|
|
closest_distance = distance
|
|
return closest
|
|
|
|
|
|
func set_local_space(space_id: StringName) -> void:
|
|
_local_space = space_id
|
|
_refresh_local_visibility()
|
|
|
|
|
|
func get_manifest(owner_fingerprint: String) -> Dictionary:
|
|
var presentation: Dictionary = _presentations_by_fingerprint.get(
|
|
owner_fingerprint, {}
|
|
)
|
|
return (presentation.get("home", {}) as Dictionary).duplicate(true)
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
_refresh_pending_decor_collisions()
|
|
if not _dedicated_runtime:
|
|
_refresh_nearest_wall()
|
|
|
|
|
|
func _queue_decor_world_collision(
|
|
owner_fingerprint: String,
|
|
instance_id: String,
|
|
decor_id: StringName,
|
|
body: StaticBody3D,
|
|
) -> void:
|
|
if (
|
|
owner_fingerprint.is_empty()
|
|
or instance_id.is_empty()
|
|
or not DecorCatalogType.has_product(decor_id)
|
|
or body == null
|
|
):
|
|
return
|
|
var key: String = _decor_collision_key(
|
|
owner_fingerprint, instance_id
|
|
)
|
|
body.collision_layer = DECOR_SELECTION_COLLISION_LAYER
|
|
_pending_solid_decor[key] = {
|
|
"owner_fingerprint": owner_fingerprint,
|
|
"instance_id": instance_id,
|
|
"decor_id": decor_id,
|
|
"body": body,
|
|
}
|
|
|
|
|
|
func _refresh_pending_decor_collisions() -> void:
|
|
if _pending_solid_decor.is_empty():
|
|
return
|
|
var keys: Array[String] = []
|
|
keys.assign(_pending_solid_decor.keys())
|
|
for key: String in keys:
|
|
if _decor_under_manipulation.has(key):
|
|
continue
|
|
var pending: Dictionary = _pending_solid_decor.get(key, {})
|
|
var body: StaticBody3D = pending.get("body") as StaticBody3D
|
|
var owner_fingerprint: String = str(
|
|
pending.get("owner_fingerprint", "")
|
|
)
|
|
var instance_id: String = str(pending.get("instance_id", ""))
|
|
var decor_id := StringName(str(pending.get("decor_id", "")))
|
|
var decor: Node3D = _get_decor_presentation(
|
|
owner_fingerprint, instance_id
|
|
)
|
|
if body == null or decor == null or not is_instance_valid(body):
|
|
_pending_solid_decor.erase(key)
|
|
continue
|
|
if not is_decor_placement_clear_of_players(
|
|
owner_fingerprint,
|
|
decor_id,
|
|
Vector3(decor.position.x, 0.0, decor.position.z),
|
|
decor.rotation.y,
|
|
):
|
|
continue
|
|
body.collision_layer = (
|
|
WORLD_GEOMETRY_COLLISION_LAYER
|
|
| DECOR_SELECTION_COLLISION_LAYER
|
|
)
|
|
_pending_solid_decor.erase(key)
|
|
|
|
|
|
func _clear_decor_collision_tracking(owner_fingerprint: String) -> void:
|
|
var prefix: String = "%s/" % owner_fingerprint
|
|
for key: String in _decor_under_manipulation.keys():
|
|
if key.begins_with(prefix):
|
|
_decor_under_manipulation.erase(key)
|
|
for key: String in _pending_solid_decor.keys():
|
|
if key.begins_with(prefix):
|
|
_pending_solid_decor.erase(key)
|
|
|
|
|
|
static func _decor_collision_key(
|
|
owner_fingerprint: String,
|
|
instance_id: String,
|
|
) -> String:
|
|
return "%s/%s" % [owner_fingerprint, instance_id]
|
|
|
|
|
|
func _ensure_roots() -> void:
|
|
if not is_instance_valid(_exteriors_root):
|
|
_exteriors_root = Node3D.new()
|
|
_exteriors_root.name = "RVExteriors"
|
|
add_child(_exteriors_root)
|
|
if not is_instance_valid(_interiors_root):
|
|
_interiors_root = Node3D.new()
|
|
_interiors_root.name = "RVInteriors"
|
|
add_child(_interiors_root)
|
|
|
|
|
|
func _build_exterior(
|
|
fingerprint: String,
|
|
world_transform: Transform3D,
|
|
) -> Node3D:
|
|
var root := Node3D.new()
|
|
root.name = "RV_%s" % fingerprint.left(8)
|
|
_exteriors_root.add_child(root)
|
|
root.global_transform = world_transform
|
|
if not _dedicated_runtime:
|
|
var motorcoach: Node3D = STARTER_MOTORCOACH_SCENE.instantiate() as Node3D
|
|
if motorcoach != null:
|
|
motorcoach.name = "StarterMotorcoach"
|
|
root.add_child(motorcoach)
|
|
else:
|
|
push_error("The starter motorcoach exterior could not be instantiated.")
|
|
_add_box_collision(
|
|
root,
|
|
"Body",
|
|
EXTERIOR_BODY_SIZE,
|
|
Vector3(0.0, 1.35, 0.0),
|
|
)
|
|
root.visible = not _dedicated_runtime
|
|
return root
|
|
|
|
|
|
func _build_interior(
|
|
fingerprint: String,
|
|
interior_index: int,
|
|
home_data: Dictionary,
|
|
) -> Node3D:
|
|
var root := Node3D.new()
|
|
root.name = "RVInterior_%s" % fingerprint.left(8)
|
|
root.position = INTERIOR_ORIGIN + Vector3(
|
|
float(interior_index) * INTERIOR_SPACING, 0.0, 0.0
|
|
)
|
|
var half_width: float = INTERIOR_FLOOR_SIZE.x * 0.5
|
|
var half_depth: float = INTERIOR_FLOOR_SIZE.z * 0.5
|
|
var wall_center_y: float = INTERIOR_WALL_HEIGHT * 0.5
|
|
_interiors_root.add_child(root)
|
|
_add_box(
|
|
root, "Floor", INTERIOR_FLOOR_SIZE, Vector3.ZERO,
|
|
Color("a6754e"), true,
|
|
)
|
|
_add_wall(
|
|
root, "WallNorth",
|
|
Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18),
|
|
Vector3(0.0, wall_center_y, -half_depth),
|
|
)
|
|
_add_wall(
|
|
root, "WallSouth",
|
|
Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18),
|
|
Vector3(0.0, wall_center_y, half_depth),
|
|
)
|
|
_add_wall(
|
|
root, "WallWest",
|
|
Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z),
|
|
Vector3(-half_width, wall_center_y, 0.0),
|
|
)
|
|
_add_wall(
|
|
root, "WallEast",
|
|
Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z),
|
|
Vector3(half_width, wall_center_y, 0.0),
|
|
)
|
|
for x_position: float in [-1.75, 1.75]:
|
|
_add_box(
|
|
root, "WallNorthWindow%s" % ("Left" if x_position < 0.0 else "Right"),
|
|
Vector3(1.65, 0.95, 0.04),
|
|
Vector3(x_position, 1.55, -half_depth + 0.11),
|
|
Color("153640"), false,
|
|
)
|
|
_add_box(
|
|
root, "WallSouthWindow%s" % ("Left" if x_position < 0.0 else "Right"),
|
|
Vector3(1.65, 0.95, 0.04),
|
|
Vector3(x_position, 1.55, half_depth - 0.11),
|
|
Color("153640"), false,
|
|
)
|
|
_add_box(
|
|
root, "WallEastWindow", Vector3(0.04, 0.95, 1.55),
|
|
Vector3(half_width - 0.11, 1.55, 0.0), Color("153640"), false,
|
|
)
|
|
_add_box(
|
|
root, "WallWestDoor", Vector3(0.04, 1.9, 1.25),
|
|
Vector3(-half_width + 0.11, 1.0, 0.0), Color("2897a8"), false,
|
|
)
|
|
var decor := Node3D.new()
|
|
decor.name = "Decor"
|
|
root.add_child(decor)
|
|
_rebuild_decor(decor, home_data, fingerprint)
|
|
root.visible = false
|
|
if _dedicated_runtime:
|
|
_set_geometry_visible(root, false)
|
|
return root
|
|
|
|
|
|
func _rebuild_decor(
|
|
parent: Node3D,
|
|
home_data: Dictionary,
|
|
owner_fingerprint: String,
|
|
) -> void:
|
|
for value: Variant in home_data.get("placements", []):
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
var placement: Dictionary = value
|
|
var decor_id := StringName(str(placement["decor_id"]))
|
|
if decor_id != DecorCatalogType.BASIC_CUBE_ID:
|
|
continue
|
|
var position_values: Array = placement["position"]
|
|
var placement_root := Node3D.new()
|
|
placement_root.name = str(placement["instance_id"])
|
|
placement_root.position = Vector3(
|
|
float(position_values[0]), 0.18, float(position_values[2])
|
|
)
|
|
placement_root.rotation.y = float(placement["yaw"])
|
|
placement_root.set_meta(
|
|
&"decor_instance_id", str(placement["instance_id"])
|
|
)
|
|
placement_root.set_meta(
|
|
&"decor_owner_fingerprint", owner_fingerprint
|
|
)
|
|
placement_root.set_meta(&"decor_id", String(decor_id))
|
|
parent.add_child(placement_root)
|
|
var body := _add_box(
|
|
placement_root, "BasicCube", BASIC_CUBE_SIZE,
|
|
Vector3(0.0, BASIC_CUBE_SIZE.y * 0.5, 0.0),
|
|
Color("b77a55"), true,
|
|
)
|
|
if body != null:
|
|
body.collision_layer = DECOR_SELECTION_COLLISION_LAYER
|
|
body.set_meta(
|
|
&"decor_instance_id", str(placement["instance_id"])
|
|
)
|
|
body.set_meta(
|
|
&"decor_owner_fingerprint", owner_fingerprint
|
|
)
|
|
body.set_meta(&"decor_id", String(decor_id))
|
|
_queue_decor_world_collision(
|
|
owner_fingerprint,
|
|
str(placement["instance_id"]),
|
|
decor_id,
|
|
body,
|
|
)
|
|
_add_box(
|
|
placement_root,
|
|
"BasicCubeFront",
|
|
Vector3(BASIC_CUBE_SIZE.x * 0.82, BASIC_CUBE_SIZE.y * 0.68, 0.025),
|
|
Vector3(
|
|
0.0,
|
|
BASIC_CUBE_SIZE.y * 0.5,
|
|
-BASIC_CUBE_SIZE.z * 0.5 - 0.013,
|
|
),
|
|
Color("2897a8"),
|
|
false,
|
|
)
|
|
|
|
|
|
func _add_wall(
|
|
parent: Node3D,
|
|
node_name: String,
|
|
size: Vector3,
|
|
position: Vector3,
|
|
) -> void:
|
|
_add_box(parent, node_name, size, position, Color("d6caaa"), true)
|
|
|
|
|
|
func _add_box(
|
|
parent: Node3D,
|
|
node_name: String,
|
|
size: Vector3,
|
|
position: Vector3,
|
|
color: Color,
|
|
with_collision: bool,
|
|
invert_faces: bool = false,
|
|
) -> StaticBody3D:
|
|
var mesh_instance := MeshInstance3D.new()
|
|
mesh_instance.name = node_name
|
|
var mesh := BoxMesh.new()
|
|
mesh.size = size
|
|
mesh.material = _material(color)
|
|
if invert_faces:
|
|
mesh.flip_faces = true
|
|
mesh_instance.mesh = mesh
|
|
mesh_instance.position = position
|
|
parent.add_child(mesh_instance)
|
|
if with_collision:
|
|
return _add_box_collision(parent, node_name, size, position)
|
|
return null
|
|
|
|
|
|
func _add_box_collision(
|
|
parent: Node3D,
|
|
node_name: String,
|
|
size: Vector3,
|
|
position: Vector3,
|
|
) -> StaticBody3D:
|
|
var body := StaticBody3D.new()
|
|
body.name = "%sCollision" % node_name
|
|
body.position = position
|
|
var collision := CollisionShape3D.new()
|
|
var shape := BoxShape3D.new()
|
|
shape.size = size
|
|
collision.shape = shape
|
|
body.add_child(collision)
|
|
parent.add_child(body)
|
|
return body
|
|
|
|
|
|
func _spawn_decor_poof(
|
|
owner_fingerprint: String,
|
|
instance_id: String,
|
|
) -> void:
|
|
var presentation: Dictionary = _presentations_by_fingerprint.get(
|
|
owner_fingerprint, {}
|
|
)
|
|
var interior: Node3D = presentation.get("interior")
|
|
if not is_instance_valid(interior):
|
|
return
|
|
var placement_root: Node3D = interior.get_node_or_null(
|
|
"Decor/%s" % instance_id
|
|
)
|
|
if placement_root == null:
|
|
return
|
|
var poof := CPUParticles3D.new()
|
|
poof.name = "DecorPoof"
|
|
poof.amount = 12
|
|
poof.lifetime = 0.45
|
|
poof.one_shot = true
|
|
poof.explosiveness = 0.9
|
|
poof.direction = Vector3.UP
|
|
poof.spread = 80.0
|
|
poof.initial_velocity_min = 0.7
|
|
poof.initial_velocity_max = 1.5
|
|
poof.scale_amount_min = 0.08
|
|
poof.scale_amount_max = 0.16
|
|
var particle_mesh := SphereMesh.new()
|
|
particle_mesh.radius = 0.06
|
|
particle_mesh.height = 0.12
|
|
particle_mesh.material = _material(Color("d9c5a3"))
|
|
poof.mesh = particle_mesh
|
|
placement_root.add_child(poof)
|
|
poof.emitting = true
|
|
get_tree().create_timer(poof.lifetime + 0.1).timeout.connect(
|
|
poof.queue_free
|
|
)
|
|
|
|
|
|
func _material(color: Color) -> StandardMaterial3D:
|
|
var material := StandardMaterial3D.new()
|
|
material.albedo_color = color
|
|
material.roughness = 1.0
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX
|
|
return material
|
|
|
|
|
|
func _free_presentation_nodes(presentation: Dictionary) -> void:
|
|
for key: String in ["exterior", "interior"]:
|
|
var node: Node = presentation.get(key)
|
|
if is_instance_valid(node):
|
|
node.queue_free()
|
|
|
|
|
|
func _refresh_local_visibility() -> void:
|
|
var active_fingerprint: String = ""
|
|
if String(_local_space).begins_with("rv:"):
|
|
active_fingerprint = String(_local_space).trim_prefix("rv:")
|
|
if _world != null:
|
|
_world.set_local_home_interior_presentation_active(
|
|
not active_fingerprint.is_empty()
|
|
)
|
|
for fingerprint: String in _presentations_by_fingerprint:
|
|
var presentation: Dictionary = _presentations_by_fingerprint[fingerprint]
|
|
var interior: Node3D = presentation.get("interior")
|
|
if is_instance_valid(interior):
|
|
interior.visible = (
|
|
not _dedicated_runtime and fingerprint == active_fingerprint
|
|
)
|
|
var exterior: Node3D = presentation.get("exterior")
|
|
if is_instance_valid(exterior):
|
|
exterior.visible = (
|
|
not _dedicated_runtime and active_fingerprint.is_empty()
|
|
)
|
|
var local_player: Player = (
|
|
_spawn_service.get_local_player()
|
|
if _spawn_service != null else null
|
|
)
|
|
if local_player == null:
|
|
return
|
|
var excluded_wall_rids: Array[RID] = []
|
|
if not active_fingerprint.is_empty():
|
|
var active: Dictionary = _presentations_by_fingerprint.get(
|
|
active_fingerprint, {}
|
|
)
|
|
var active_interior: Node3D = active.get("interior")
|
|
if is_instance_valid(active_interior):
|
|
for wall_name: String in [
|
|
"WallNorth", "WallSouth", "WallWest", "WallEast",
|
|
]:
|
|
var body: StaticBody3D = active_interior.get_node_or_null(
|
|
"%sCollision" % wall_name
|
|
)
|
|
if body != null:
|
|
excluded_wall_rids.append(body.get_rid())
|
|
local_player.set_rv_interior_camera_mode(
|
|
not active_fingerprint.is_empty(), excluded_wall_rids
|
|
)
|
|
|
|
|
|
func _refresh_nearest_wall() -> void:
|
|
if not String(_local_space).begins_with("rv:") or _spawn_service == null:
|
|
return
|
|
var fingerprint: String = String(_local_space).trim_prefix("rv:")
|
|
var presentation: Dictionary = _presentations_by_fingerprint.get(
|
|
fingerprint, {}
|
|
)
|
|
var interior: Node3D = presentation.get("interior")
|
|
var player: Player = _spawn_service.get_local_player()
|
|
if not is_instance_valid(interior) or player == null:
|
|
return
|
|
var camera: Camera3D = player.get_active_gameplay_camera()
|
|
if camera == null:
|
|
return
|
|
var local_camera: Vector3 = interior.to_local(camera.global_position)
|
|
var local_view_direction: Vector3 = (
|
|
interior.global_basis.inverse() * -camera.global_basis.z
|
|
).normalized()
|
|
var horizontal_view_weight: float = absf(local_view_direction.x)
|
|
var depth_view_weight: float = absf(local_view_direction.z)
|
|
var hidden_walls: Dictionary[String, bool] = {}
|
|
var outside_horizontal_wall: bool = (
|
|
absf(local_camera.x) >= INTERIOR_FLOOR_SIZE.x * 0.5 - 0.05
|
|
)
|
|
var outside_depth_wall: bool = (
|
|
absf(local_camera.z) >= INTERIOR_FLOOR_SIZE.z * 0.5 - 0.05
|
|
)
|
|
# At a diagonal, both near walls occupy the view. Use the camera's viewing
|
|
# direction to hide both instead of choosing only the dominant position
|
|
# axis and leaving the adjacent wall across the screen.
|
|
if (
|
|
outside_horizontal_wall
|
|
and horizontal_view_weight >= depth_view_weight * 0.35
|
|
):
|
|
hidden_walls[
|
|
"WallEast" if local_camera.x >= 0.0 else "WallWest"
|
|
] = true
|
|
if (
|
|
outside_depth_wall
|
|
and depth_view_weight >= horizontal_view_weight * 0.35
|
|
):
|
|
hidden_walls[
|
|
"WallSouth" if local_camera.z >= 0.0 else "WallNorth"
|
|
] = true
|
|
if hidden_walls.is_empty():
|
|
var fallback_wall: String = (
|
|
"WallEast" if local_camera.x >= 0.0 else "WallWest"
|
|
)
|
|
if depth_view_weight > horizontal_view_weight:
|
|
fallback_wall = (
|
|
"WallSouth" if local_camera.z >= 0.0 else "WallNorth"
|
|
)
|
|
hidden_walls[fallback_wall] = true
|
|
for wall_name: String in [
|
|
"WallNorth", "WallSouth", "WallWest", "WallEast",
|
|
]:
|
|
var wall: MeshInstance3D = interior.get_node_or_null(wall_name)
|
|
if wall != null:
|
|
wall.visible = not hidden_walls.has(wall_name)
|
|
for child: Node in interior.get_children():
|
|
if (
|
|
child is GeometryInstance3D
|
|
and String(child.name).begins_with(wall_name)
|
|
):
|
|
(child as GeometryInstance3D).visible = (
|
|
not hidden_walls.has(wall_name)
|
|
)
|
|
|
|
|
|
func _too_close_to_reserved(
|
|
position: Vector3,
|
|
reserved_transforms: Array[Transform3D],
|
|
) -> bool:
|
|
for transform: Transform3D in reserved_transforms:
|
|
if position.distance_to(transform.origin) < MIN_EXTERIOR_SEPARATION:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _set_geometry_visible(root: Node, visible: bool) -> void:
|
|
for node: Node in root.find_children("*", "GeometryInstance3D", true, false):
|
|
(node as GeometryInstance3D).visible = visible
|