@tool class_name PersistentZone extends XRToolsSceneBase ## Persistent Zone Node ## ## The [PersistentNode] class is an extension of [XRToolsSceneBase] which ## manages the state of the zones [PersistentItem] objects through the ## persistence system. # Group for world-data properties @export_group("World Data") ## This property specifies the persistent zone information @export var zone_info : PersistentZoneInfo signal numberOfFiresSignal(number: int) var numberOfFires = 0; # Add support for is_xr_class func is_xr_class(p_name : String) -> bool: return p_name == "PersistentZone" or super(p_name) # Called when the node enters the scene tree for the first time. func _ready() -> void: var fire_scene = preload("res://assets/fire/fire.tscn") var ground = $World/Ground/MeshInstance3D for i in range(30): spawn_on_surface(ground, fire_scene) numberOfFiresSignal.emit(numberOfFires) # call the base super() # Get configuration warnings func _get_configuration_warnings() -> PackedStringArray: var warnings := PackedStringArray() # Verify zone info is set if not zone_info: warnings.append("Zone ID not zet") # Return warnings return warnings ## Handle zone loaded func scene_loaded(user_data = null): super(user_data) # Save the current zone GameState.current_zone = self # Find all PersistentItem instances designed into the zone var items_in_zone := {} for node in XRTools.find_xr_children(self, "*", "PersistentItem"): items_in_zone[node.item_id] = node # Find the zone items the PersistentWorld thinks should be in this zone. var zone_items = PersistentWorld.instance.get_value(zone_info.zone_id) # Free items designed into the zone but PersistentWorld thinks should # be removed. if zone_items is Array: for item_id in items_in_zone: if not zone_items.has(item_id): var item : PersistentItem = items_in_zone[item_id] item.get_parent().remove_child(item) item.queue_free() # Load world-state for all items in the zone propagate_notification(Persistent.NOTIFICATION_LOAD_STATE) # Create items missing from the zone but PersistentWorld thinks should be # present. if zone_items is Array: for item_id in zone_items: if not items_in_zone.has(item_id): create_item_instance(item_id) # Create items held by the players left hand var left_pickup := XRToolsFunctionPickup.find_left($XROrigin3D) var left_item_id = PersistentWorld.instance.get_value("player.left_hand") if left_pickup and left_item_id is String: var left_instance := create_item_instance(left_item_id) if left_instance: left_instance.global_transform = left_pickup.global_transform left_pickup._pick_up_object.call_deferred(left_instance) # Create items held by the players right hand var right_pickup := XRToolsFunctionPickup.find_right($XROrigin3D) var right_item_id = PersistentWorld.instance.get_value("player.right_hand") if right_pickup and right_item_id is String: var right_instance := create_item_instance(right_item_id) if right_instance: right_instance.global_transform = right_pickup.global_transform right_pickup._pick_up_object.call_deferred(right_instance) ## Handle zone exiting func scene_exiting(user_data = null): super(user_data) # Ensure the zone state is saved before exiting the zone save_world_state() # Clear the current zone GameState.current_zone = self ## This method saves the state of the zone to the [PersistentWorld]. This gets ## called upon exiting the zone; but it should also be called before saving ## the game. func save_world_state() -> void: # Save world-state for all items in the zone propagate_notification(Persistent.NOTIFICATION_SAVE_STATE) # Identify items held directly by the zone var items_in_zone : Array[String] = [] for node in get_tree().get_nodes_in_group("persistent"): if is_item_held_by_zone(node): items_in_zone.append(node.item_id) # Save the items held by the zone PersistentWorld.instance.set_value(zone_info.zone_id, items_in_zone) # Handle items held in the players left hand var left_pickup := XRToolsFunctionPickup.find_left($XROrigin3D) var left_item := _get_held_persistent_item(left_pickup) if left_item: # The player.left_hand holds the item PersistentWorld.instance.set_value("player.left_hand", left_item.item_id) else: # The player.left_hand is empty PersistentWorld.instance.clear_value("player.left_hand") # Handle items held in the players right hand var right_pickup := XRToolsFunctionPickup.find_right($XROrigin3D) var right_item := _get_held_persistent_item(right_pickup) if right_item: # The player.right_hand holds the item PersistentWorld.instance.set_value("player.right_hand", right_item.item_id) else: # The player.right_hand is empty PersistentWorld.instance.clear_value("player.right_hand") ## Find the [PersistentZone] containing a given node static func find_instance(node : Node) -> PersistentZone: return XRTools.find_xr_ancestor( node, "*", "PersistentZone") as PersistentZone # Create a [PersistentItem] from its [param item_id]. This is used when # loading a scene that contains an item carried by the user from a different # scene. func create_item_instance(item_id : String) -> PersistentItem: # Get the items state information var state = PersistentWorld.instance.get_value(item_id) if not state is Dictionary: push_warning("Item %s not in world-data" % item_id) return null # Get the items type_id var item_type_id = state.get("type") if not item_type_id is String: push_warning("Item %s does not define type" % item_id) return null # Get the PersistentItemType var item_type := PersistentWorld.instance.item_database.get_type(item_type_id) if not item_type: push_warning("Item type %s not in database" % item_type_id) return null # Load the item scene var item_scene : PackedScene = load(item_type.instance_scene) if not item_scene: push_warning("Item scene %s not valid" % item_type.instance_scene) return null # Construct the item var item : PersistentItem = item_scene.instantiate() if not item: push_warning("Item scene %s not valid" % item_type.instance_scene) return null # Initialize the item item.item_id = item_id item.item_type = item_type item.propagate_notification(Persistent.NOTIFICATION_LOAD_STATE) add_child(item) return item # This method returns true if the node is an item held by a zone rather than # being held by some sort of persistent object such as a PersistentPocket or # an XRToolsFunctionPickup. static func is_item_held_by_zone(node : Node) -> bool: # Skip if not valid if not is_instance_valid(node): return false # Skip if not an PersistentItem if not node is PersistentItem: return false # If the node isn't held by anything valid then it's held by the zone if not is_instance_valid(node.get_picked_up_by()): return true # If held by a PersistentPocket then it's not held by the zone if node.get_picked_up_by() is PersistentPocket: return false # If held by an XRToolsFunctionPickup the it's not held by the zone if node.get_picked_up_by() is XRToolsFunctionPickup: return false # Node is held by a non-persistent mechanism in the zone push_warning("Item ", node.item_id, " held by non-persistent ", node.get_picked_up_by()) return true func spawn_on_surface(target_mesh_instance: MeshInstance3D, scene_to_spawn: PackedScene): var original_mesh = target_mesh_instance.mesh if original_mesh == null: print("Mesh is missing.") return var mesh : ArrayMesh # Handle QuadMesh or other primitive Meshes if original_mesh is PrimitiveMesh: mesh = ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, original_mesh.get_mesh_arrays()) elif original_mesh is ArrayMesh: mesh = original_mesh else: print("Unsupported mesh type.") return # Get surface data (assuming surface 0) if mesh.get_surface_count() == 0: print("Mesh has no surfaces.") return var arrays = mesh.surface_get_arrays(0) var vertices = arrays[Mesh.ARRAY_VERTEX] var indices = arrays[Mesh.ARRAY_INDEX] if vertices.is_empty() or indices.is_empty(): print("Mesh has no usable vertices or indices.") return # Try finding a face pointing mostly upwards var found_point = false var point = Vector3.ZERO var max_attempts = 100 # prevent infinite loops var attempts = 0 while not found_point and attempts < max_attempts: attempts += 1 # Choose a random triangle var tri_index = randi() % (indices.size() / 3) var i0 = indices[tri_index * 3] var i1 = indices[tri_index * 3 + 1] var i2 = indices[tri_index * 3 + 2] var a = vertices[i0] var b = vertices[i1] var c = vertices[i2] # Calculate the face normal var normal = ((b - a).cross(c - a)).normalized() # Check if face normal points mostly up (adjust threshold as needed) if normal.dot(Vector3.UP) > 0.7: # Get random point in triangle (barycentric coordinates) var r1 = randf() var r2 = randf() if r1 + r2 >= 1.0: r1 = 1.0 - r1 r2 = 1.0 - r2 point = a + (b - a) * r1 + (c - a) * r2 found_point = true numberOfFires += 1 if not found_point: print("No upward facing face found after", max_attempts, "attempts.") return # Transform to world space point = target_mesh_instance.global_transform * point # Instance and place your object var new_object = scene_to_spawn.instantiate() new_object.global_transform.origin = point new_object.scale = Vector3(3.0, 3.0, 3.0) get_tree().current_scene.add_child(new_object) # This method returns the persistent item primarily held by the pickup static func _get_held_persistent_item(pickup : XRToolsFunctionPickup) -> PersistentItem: # Fail if no pickup if not is_instance_valid(pickup): return null # Fail if item is not a PersistentItem var item := pickup.picked_up_object as PersistentItem if not item: return null # Fail if not active pickup, but merely second-hand grab if item.get_picked_up_by() != pickup: return null # Return the item return item