class_name StandardWorldGenerator extends WorldGenerator ## Rooms used to generate the map @export var rooms: Array[PackedScene] = [] ## Maximum length of a path of rooms @export var max_room_path_length: int = 10 ## Room used to spawn the player @export var spawn_room: PackedScene = null # Rooms are sorted into these arrays based on their exits # Each PackedScene is a StandardRoom var _rooms_left: Array[PackedScene] = [] var _rooms_right: Array[PackedScene] = [] var _rooms_top: Array[PackedScene] = [] var _rooms_bottom: Array[PackedScene] = [] var _rooms_blocking: Array[PackedScene] = [] ## Number of total generated rooms var _room_count: int = 0 ## Progress tracker var _progress_tracker: ProgressTracker ## Marker groups var _markers: Dictionary = {} func _init() -> void: self._progress_tracker = ProgressTracker.new(4) ## Generates a map func _generate(map: TileMap) -> void: self._sort_rooms(_progress_tracker.next_step("Sorting Rooms")) # Create initial room var init_room = self._create_initial_room( map, self.spawn_room, _progress_tracker.next_step("Creating Initial Room") ) init_room.queue_free.call_deferred() # Create rooms self._create_rooms( map, init_room, self.max_room_path_length, _progress_tracker.next_step("Creating Rooms") ) map.update_internals() self._spawn_entities(map, _progress_tracker.next_step("Spawning Entities")) map.update_internals() ## Sorts rooms into arrays based on their exits func _sort_rooms(step_tracker: ProgressStepTracker) -> void: step_tracker.complete.call_deferred() step_tracker.set_substeps(len(self.rooms)) for room_scene in rooms: var room: StandardRoom = room_scene.instantiate() step_tracker.substep(room.name) if room.left: self._rooms_left.append(room_scene) if room.right: self._rooms_right.append(room_scene) if room.top: self._rooms_top.append(room_scene) if room.bottom: self._rooms_bottom.append(room_scene) if room.is_block(): self._rooms_blocking.append(room_scene) room.queue_free() if len(self._rooms_left) == 0: push_error("0 left rooms!") if len(self._rooms_right) == 0: push_error("0 right rooms!") if len(self._rooms_top) == 0: push_error("0 top rooms!") if len(self._rooms_bottom) == 0: push_error("0 bottom rooms!") if len(self._rooms_blocking) == 0: push_warning("0 blocking rooms!") ## Create initial room func _create_initial_room( map: TileMap, room: PackedScene, step_tracker: ProgressStepTracker ) -> StandardRoom: step_tracker.complete.call_deferred() # Instantiate spawn room var init_room: StandardRoom = room.instantiate() # Copy spawn room into map init_room.copy_to_map(map) self._room_count += 1 return init_room ## Creates rooms from the exits of the given room func _create_rooms( map: TileMap, parent_room: StandardRoom, max_path_length: int, step_tracker: ProgressStepTracker ) -> void: if step_tracker.get_substep_count() == 0: step_tracker.complete.call_deferred() ## This is an approximation of the number of rooms that will be created step_tracker.set_substeps( (max_path_length * (max_path_length + 1) / 2) * len(parent_room.get_exits()) ) var exits: Array[Vector2i] = parent_room.get_exits() # If the maximum path length has been exceeded, stop if max_path_length <= 0: for exit in exits: parent_room.seal_exit(map, parent_room.position, exit) return # If there are no more exits, stop if len(exits) == 0: return # If there are no more rooms to add, stop if ( len(self._rooms_left) == 0 and len(self._rooms_right) == 0 and len(self._rooms_top) == 0 and len(self._rooms_bottom) == 0 ): push_warning("No more rooms to add!") return # For every exit, try to add a room for exit in exits: # Find exit pos in current room var exit_pos: Vector2i = parent_room.get_exit_pos(exit) var room_edge_pos: Vector2i = exit_pos + Vector2i(parent_room.position) # Get the rooms that can be added in this direction var possible_rooms: Array[PackedScene] = ( self._get_rooms(parent_room.flip_vector(exit)).duplicate() ) # If there are no rooms that can be added in this direction, skip it if len(possible_rooms) == 0: continue # Get the room to add var room_instance: StandardRoom = null while len(possible_rooms) != 0 and room_instance == null: var i = randi() % len(possible_rooms) var possible_room = possible_rooms.pop_at(i).instantiate() var origin_pos: Vector2i = possible_room.get_exit_pos_offset(room_edge_pos, exit) possible_room.position = origin_pos if possible_room.is_overlapping(map): continue room_instance = possible_room if room_instance == null: parent_room.seal_exit(map, parent_room.position, exit) continue room_instance.queue_free.call_deferred() # Disable direction that the room was added from # since the parent room will already be there room_instance.disable_room_exit_opposite(exit) # Copy the room into the map room_instance.copy_to_map(map) self._room_count += 1 step_tracker.substep("%s rooms" % self._room_count) # Create rooms from the exits of the room that was just added self._create_rooms(map, room_instance, max_path_length - 1, step_tracker) # If there are no more exits, stop return ## Returns the rooms that can be added in the given direction func _get_rooms(exit: Vector2i) -> Array[PackedScene]: match exit: Vector2i.LEFT: return self._rooms_left Vector2i.RIGHT: return self._rooms_right Vector2i.UP: return self._rooms_top Vector2i.DOWN: return self._rooms_bottom push_error("Invalid exit: " + str(exit)) return [] ## Registers a marker with the world generator func register_marker(group: String, marker: Node2D) -> void: if not group in self._markers: self._markers[group] = [marker] else: self._markers[group].append(marker) ## Spawns entities in the map func _spawn_entities(map: TileMap, _step_tracker: ProgressStepTracker) -> void: _step_tracker.complete.call_deferred() var tree = map.get_tree() # Register entity markers for entity_marker in tree.get_nodes_in_group("genv2:entity_marker"): if entity_marker is StandardEntityMarker: entity_marker.register(self) _step_tracker.set_substeps(len(self._markers.keys())) for marker_id in self._markers: _step_tracker.substep.call_deferred(marker_id) var sample_marker: StandardEntityMarker = self._markers[marker_id][0] match sample_marker.spawn_method: StandardEntityMarker.EntitySpawnMethod.ALL: for marker in self._markers[marker_id]: map.erase_cell.call_deferred(0, map.local_to_map(marker.position)) var packed_entity: PackedScene = _select_entity( marker.entity_selection_method, marker.entities ) if packed_entity == null: continue var entity: Node2D = packed_entity.instantiate() entity.position = marker.position map.add_child(entity) StandardEntityMarker.EntitySpawnMethod.FURTHEST: var furthest_marker: StandardEntityMarker = self._markers[marker_id].pop_back() var furthest_distance: float = furthest_marker.position.distance_to(Vector2.ZERO) # Calculate furthest marker and destroy others for marker in self._markers[marker_id]: var calc_distance: float = marker.position.distance_to(Vector2.ZERO) if calc_distance > furthest_distance: map.erase_cell(0, map.local_to_map(furthest_marker.position)) furthest_marker = marker furthest_distance = calc_distance else: map.erase_cell(0, map.local_to_map(marker.position)) # Erase furthest marker map.erase_cell.call_deferred(0, map.local_to_map(furthest_marker.position)) # Get entity var packed_entity: PackedScene = _select_entity( furthest_marker.entity_selection_method, furthest_marker.entities ) if packed_entity == null: continue var entity: Node2D = packed_entity.instantiate() entity.position = furthest_marker.position map.add_child(entity) StandardEntityMarker.EntitySpawnMethod.ONCE: var i = randi() % len(self._markers[marker_id]) var marker = self._markers[marker_id][i] var packed_entity: PackedScene = _select_entity( marker.entity_selection_method, marker.entities ) if packed_entity != null: var entity: Node2D = packed_entity.instantiate() entity.position = marker.position map.add_child(entity) for _marker in self._markers[marker_id]: map.erase_cell(0, map.local_to_map(_marker.position)) StandardEntityMarker.EntitySpawnMethod.NONE: for marker in self._markers[marker_id]: map.erase_cell(0, map.local_to_map(marker.position)) _: push_error("Invalid spawning method!") continue ## Selects an entity from the given array based on the given selection method func _select_entity( mode: StandardEntityMarker.EntitySelectionMethod, entities: Array ) -> PackedScene: if len(entities) == 0: return null match mode: StandardEntityMarker.EntitySelectionMethod.RANDOM: var i = randi() % len(entities) return entities[i] StandardEntityMarker.EntitySelectionMethod.POP: var i = randi() % len(entities) return entities.pop_at(i) return null ## Returns the progress tracker for this world generator func _get_progress_tracker() -> ProgressTracker: return self._progress_tracker