From ac82a28f3b93638b67c402590006bd3b968c5d6e Mon Sep 17 00:00:00 2001 From: Voyager Date: Tue, 1 Sep 2026 17:30:34 -0400 Subject: [PATCH] Harden home exits and interior presentation --- homes/home_decor_controller.gd | 4 ++ homes/home_world_service.gd | 99 +++++++++++++++++++++++++-------- network/network_home_service.gd | 16 ++++-- ui/home_decor_toolbar.gd | 27 +++++++-- 4 files changed, 114 insertions(+), 32 deletions(-) diff --git a/homes/home_decor_controller.gd b/homes/home_decor_controller.gd index f67207f..380bdae 100644 --- a/homes/home_decor_controller.gd +++ b/homes/home_decor_controller.gd @@ -1,6 +1,8 @@ class_name HomeDecorController extends Node +signal exit_requested + const DecorCatalogType = preload("res://homes/decor_catalog.gd") const DRAG_RESPONSE: float = 22.0 const DRAG_POSITION_EPSILON: float = 0.005 @@ -64,6 +66,8 @@ func setup( _toolbar.rotate_requested.connect(_rotate_selected) _toolbar.store_requested.connect(_store_selected) _toolbar.cancel_requested.connect(_clear_selection) + if not _toolbar.exit_requested.is_connected(exit_requested.emit): + _toolbar.exit_requested.connect(exit_requested.emit) _toolbar.set_mode(_mode) _refresh_toolbar() diff --git a/homes/home_world_service.gd b/homes/home_world_service.gd index 6148776..1309339 100644 --- a/homes/home_world_service.gd +++ b/homes/home_world_service.gd @@ -39,6 +39,13 @@ const INTERIOR_SPACING: float = 14.0 const INTERIOR_MAX_SPAWN_SLOTS: int = 64 const TENT_INTERIOR_SIZE := Vector2(4.0, 4.0) const STARTER_RV_INTERIOR_SIZE := Vector2(4.0, 6.0) +const STARTER_RV_EXIT_LOCAL_X: float = 1.45 +const INTERIOR_EXIT_LOCAL_Y: float = 0.35 +const INTERIOR_EXIT_DOORWAY_HALF_WIDTH: float = 0.48 +const INTERIOR_EXIT_TRIGGER_DEPTH: float = 0.45 +const INTERIOR_SHELL_COLLISION_META: StringName = ( + &"home_interior_shell_collision" +) const MIN_EXTERIOR_TRIANGLE_AREA: float = 1.0 const MIN_EXTERIOR_SEPARATION: float = 14.6 const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(6.65, 3.73) @@ -585,9 +592,14 @@ func is_avatar_near_interior_exit( ) -> bool: if avatar == null: return false - return avatar.global_position.distance_to( - get_interior_exit_position(owner_fingerprint) - ) <= INTERIOR_EXIT_DISTANCE + for exit_position: Vector3 in get_interior_exit_positions( + owner_fingerprint + ): + if avatar.global_position.distance_to(exit_position) <= ( + INTERIOR_EXIT_DISTANCE + ): + return true + return false func has_avatar_crossed_interior_exit_threshold( @@ -597,22 +609,30 @@ func has_avatar_crossed_interior_exit_threshold( if avatar == null: return false var local_position: Vector3 = interior_global_to_local( - owner_fingerprint, - avatar.global_position, + owner_fingerprint, avatar.global_position ) - var threshold_position: Vector3 = interior_global_to_local( - owner_fingerprint, - get_interior_exit_position(owner_fingerprint), - ) - var exit_direction: Vector3 = get_interior_exit_direction( + for exit_position: Vector3 in get_interior_exit_positions( owner_fingerprint - ) - return ( - (local_position - threshold_position).dot(exit_direction) >= 0.0 - and absf(local_position.z - threshold_position.z) <= 0.85 - and local_position.y >= -0.25 - and local_position.y <= 2.25 - ) + ): + var threshold_position: Vector3 = interior_global_to_local( + owner_fingerprint, exit_position + ) + var exit_direction: Vector3 = get_interior_exit_direction_near( + owner_fingerprint, exit_position + ) + var outward_distance: float = ( + (local_position - threshold_position).dot(exit_direction) + ) + if ( + outward_distance >= 0.0 + and outward_distance <= INTERIOR_EXIT_TRIGGER_DEPTH + and absf(local_position.z - threshold_position.z) + <= INTERIOR_EXIT_DOORWAY_HALF_WIDTH + and local_position.y >= -0.25 + and local_position.y <= 2.25 + ): + return true + return false func get_interior_exit_direction(owner_fingerprint: String) -> Vector3: @@ -625,11 +645,41 @@ func get_interior_exit_direction(owner_fingerprint: String) -> Vector3: func get_interior_exit_position(owner_fingerprint: String) -> Vector3: - var tier: int = _home_tier(owner_fingerprint) - return get_interior_anchor(owner_fingerprint) + ( - Vector3(-STARTER_RV_INTERIOR_SIZE.x * 0.5 + 0.6, 0.35, 0.0) - if tier >= PlayerHomeStateType.STARTER_RV_TIER - else Vector3(TENT_INTERIOR_SIZE.x * 0.5 - 0.55, 0.35, 0.0) + return get_interior_exit_positions(owner_fingerprint)[0] + + +func get_interior_exit_positions( + owner_fingerprint: String, +) -> Array[Vector3]: + var anchor: Vector3 = get_interior_anchor(owner_fingerprint) + if _home_tier(owner_fingerprint) >= PlayerHomeStateType.STARTER_RV_TIER: + return [ + anchor + Vector3( + -STARTER_RV_EXIT_LOCAL_X, INTERIOR_EXIT_LOCAL_Y, 0.0 + ), + anchor + Vector3( + STARTER_RV_EXIT_LOCAL_X, INTERIOR_EXIT_LOCAL_Y, 0.0 + ), + ] + return [ + anchor + Vector3( + TENT_INTERIOR_SIZE.x * 0.5 - 0.55, + INTERIOR_EXIT_LOCAL_Y, + 0.0, + ), + ] + + +func get_interior_exit_direction_near( + owner_fingerprint: String, + world_position: Vector3, +) -> Vector3: + if _home_tier(owner_fingerprint) < PlayerHomeStateType.STARTER_RV_TIER: + return Vector3.RIGHT + return ( + Vector3.RIGHT + if world_position.x >= get_interior_anchor(owner_fingerprint).x + else Vector3.LEFT ) @@ -1200,9 +1250,14 @@ func _add_imported_mesh_collision(mesh_instance: MeshInstance3D) -> void: var shape: ConcavePolygonShape3D = mesh_instance.mesh.create_trimesh_shape() if shape == null: return + # Imported room shells are authored to render from both sides, but concave + # collision defaults to front faces only. Interior walls therefore need + # back-face collision or the player can walk through their inward side. + shape.backface_collision = true var body := StaticBody3D.new() body.name = "%sCollision" % mesh_instance.name body.transform = mesh_instance.transform + body.set_meta(INTERIOR_SHELL_COLLISION_META, true) var collision := CollisionShape3D.new() collision.shape = shape body.add_child(collision) diff --git a/network/network_home_service.gd b/network/network_home_service.gd index 4885961..6f78cd6 100644 --- a/network/network_home_service.gd +++ b/network/network_home_service.gd @@ -63,6 +63,8 @@ func setup( _session.peer_authenticated.connect(_on_peer_authenticated) if not _session.peer_removed.is_connected(_on_peer_removed): _session.peer_removed.connect(_on_peer_removed) + if not _spawn_service.avatar_spawned.is_connected(_on_avatar_spawned): + _spawn_service.avatar_spawned.connect(_on_avatar_spawned) if not _local_home.changed.is_connected(_on_local_home_changed): _local_home.changed.connect(_on_local_home_changed) @@ -1126,11 +1128,8 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void: if ( _session.get_peer_space(peer_id) != NetworkHomeProtocolType.make_space_id(owner_fingerprint) - or not _world_service.is_avatar_near_interior_exit( - avatar, owner_fingerprint - ) ): - _send_transition_rejection(peer_id, "Move closer to the home door.") + _send_transition_rejection(peer_id, "You are not inside that home.") return _apply_host_transition( peer_id, @@ -1276,6 +1275,15 @@ func _refresh_peer_visibility() -> void: ) +func _on_avatar_spawned(peer_id: int, _avatar: Player) -> void: + var local_peer_id: int = _session.get_local_peer_id() + if local_peer_id <= 0 or peer_id == local_peer_id: + return + _spawn_service.set_peer_presentation_visible( + peer_id, _session.peers_share_space(local_peer_id, peer_id) + ) + + func _on_local_home_changed() -> void: if _suppress_local_submission or _publish_queued: return diff --git a/ui/home_decor_toolbar.gd b/ui/home_decor_toolbar.gd index 50ae444..0214c86 100644 --- a/ui/home_decor_toolbar.gd +++ b/ui/home_decor_toolbar.gd @@ -5,26 +5,34 @@ signal placement_mode_requested(mode: int) signal rotate_requested signal store_requested signal cancel_requested +signal exit_requested var _mode_button: Button var _selection_label: Label var _rotate_button: Button var _store_button: Button var _clear_button: Button +var _exit_button: Button var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID var _interactive: bool = true var _selection_active: bool = false +var _active: bool = false +var _menu_obscured: bool = false func _ready() -> void: _build_ui() - hide() + _refresh_visibility() func set_active(active: bool) -> void: - if visible == active: - return - visible = active + _active = active + _refresh_visibility() + + +func set_menu_obscured(obscured: bool) -> void: + _menu_obscured = obscured + _refresh_visibility() func set_interactive(interactive: bool) -> void: @@ -59,13 +67,17 @@ func report_status(message: String) -> void: _selection_label.text = message +func _refresh_visibility() -> void: + visible = _active and not _menu_obscured + + func _build_ui() -> void: set_anchors_preset(Control.PRESET_TOP_RIGHT) - offset_left = -500.0 + offset_left = -610.0 offset_top = 0.0 offset_right = 0.0 offset_bottom = 106.0 - custom_minimum_size = Vector2(500.0, 0.0) + custom_minimum_size = Vector2(610.0, 0.0) z_index = 85 mouse_filter = Control.MOUSE_FILTER_STOP var panel_style: StyleBoxFlat = UtilityPageStyle.panel_style() @@ -102,6 +114,9 @@ func _build_ui() -> void: _store_button.pressed.connect(store_requested.emit) _clear_button = _add_tool_button(row, "clear", "clear selection", 82.0) _clear_button.pressed.connect(cancel_requested.emit) + _exit_button = _add_tool_button(row, "Exit RV", "exit the RV", 100.0) + _exit_button.name = "ExitRVButton" + _exit_button.pressed.connect(exit_requested.emit) _selection_label = Label.new() _selection_label.text = "use held decor to place; click or drag to edit" _selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART