class_name SceneTools extends Object
## Tools for specialized operations in a scene

const RAY_MASK := 0b00100101


## Get the Node3D in the given group which is spatially closest to the target node.
##
## If there is no such node, e.g. if the group is empty, the result will be null.
static func closest_in_group(target: Node3D, group_name: String) -> Node3D:
	var closest: Node3D
	var min_dist_sq := INF
	for n: Node in target.get_tree().get_nodes_in_group(group_name):
		if n is Node3D:
			var dist_sq := (n as Node3D).global_position.distance_squared_to(target.global_position)
			if dist_sq < min_dist_sq:
				min_dist_sq = dist_sq
				closest = n
	return closest


## Does the player have an unobstructed line-of-sight to the given point?
static func player_can_see(target: Vector3) -> bool:
	if not is_instance_valid(Player.instance):
		return false

	var camera := Player.instance.get_viewport().get_camera_3d()

	if not camera.is_position_in_frustum(target):
		return false

	var query := PhysicsRayQueryParameters3D.create(camera.global_position, target, RAY_MASK)
	# TODO may need to add an "exceptions" parameter for the source's body
	var raycast := Player.instance.get_world_3d().direct_space_state.intersect_ray(query)
	# If raycast is empty, view is unoccluded and player has line-of-sight!
	return not raycast


## Get a random Node3D from the given group which is not currently visible to the player
##
## Returns null is there is no such node, either because the group is empty
## or the player can see every node in the group.
static func pick_unseen_from_group(group_name: String) -> Node3D:
	if not is_instance_valid(Player.instance):
		return null

	var nodes: Array[Node] = Player.instance.get_tree().get_nodes_in_group(group_name)
	while nodes:
		var candidate: Node = nodes.pick_random()
		nodes.erase(candidate)
		var target := candidate as Node3D
		if target and not SceneTools.player_can_see(target.global_position):
			return target

	return null
