extends Node3D ## NS-16: composes ground pick + server authority; see `ground_pick.gd` / ## `position_authority_client.gd`. NS-18: interaction POST + radius glow preview (see child nodes). ## NS-23: bakes `NavigationRegion3D` on startup; boot snap vs nav goal from authority signal. const MOVE_REJECT_MSG_SECONDS: float = 4.0 ## Bump on each rejection so older one-shot timers do not clear a newer message. var _move_reject_msg_token: int = 0 @onready var _camera: Camera3D = $World/Camera3D @onready var _nav_region: NavigationRegion3D = $World/NavigationRegion3D @onready var _player: CharacterBody3D = $Player @onready var _ground_pick: Node3D = $GroundPick @onready var _authority: Node = $PositionAuthorityClient @onready var _radius_preview: Node3D = $World/InteractionMarkers @onready var _move_reject_label: Label = $UICanvas/MoveRejectLabel func _ready() -> void: await get_tree().process_frame _nav_region.bake_navigation_mesh() _ground_pick.set("fallback_camera", _camera) _ground_pick.connect("target_chosen", Callable(self, "_on_target_chosen")) _authority.connect( "authoritative_position_received", Callable(self, "_on_authoritative_position") ) _authority.connect("move_rejected", Callable(self, "_on_move_rejected")) _authority.call("sync_from_server") if _radius_preview.has_method("setup_player"): _radius_preview.call("setup_player", _player) func _on_target_chosen(world: Vector3) -> void: _authority.call("submit_move_target", world) func _on_authoritative_position(world: Vector3, apply_as_snap: bool) -> void: if apply_as_snap: _player.snap_to_server(world) else: _player.set_authoritative_nav_goal(world) func _on_move_rejected(reason_code: String) -> void: # Rejected POST never reached VERIFY_GET, so no new nav target was applied; do not clear an # in-flight path from a prior successful move. push_warning("Move rejected: %s" % reason_code) _move_reject_msg_token += 1 var token: int = _move_reject_msg_token _move_reject_label.text = "Move rejected: %s" % reason_code get_tree().create_timer(MOVE_REJECT_MSG_SECONDS).timeout.connect( func(): if token == _move_reject_msg_token: _move_reject_label.text = "", CONNECT_ONE_SHOT )