936 lines
43 KiB
GDScript
936 lines
43 KiB
GDScript
@tool
|
|
class_name DungeonGenerator3D
|
|
extends Node3D
|
|
|
|
signal done_generating()
|
|
signal generating_failed()
|
|
|
|
# This can be set to a callable to customize room spawning in a fine grained way.
|
|
# Function signature should be Callable(room_instances : Array[DungeonRoom3D], rng : RandomNumberGenerator) -> Array[DungeonRoom3D]
|
|
var custom_get_rooms_function = null
|
|
|
|
# For any vars which may be accessed from multiple threads
|
|
var t_mutex := Mutex.new()
|
|
|
|
enum BuildStage { NOT_STARTED = -2, PREPARING = -1, PLACE_ROOMS = 0, PLACE_STAIRS = 1, SEPARATE_ROOMS = 2, CONNECT_ROOMS = 3, FINALIZING = 4, DONE = 5 }
|
|
var stage : BuildStage = BuildStage.NOT_STARTED :
|
|
set(v): t_mutex.lock(); stage = v; t_mutex.unlock();
|
|
get: t_mutex.lock(); var v = stage; t_mutex.unlock(); return v;
|
|
|
|
## Add all the rooms for the dungeon generator to use in this array.
|
|
## Each one must inherit DungeonRoom.
|
|
## Not necessary to place the corridor room here. Place that in the corridor_room_scene property.
|
|
@export var room_scenes : Array[PackedScene] = []
|
|
## The corridor room is a special room scene which must be a 1x1x1 (in voxels) scene inheriting DungeonRoom which is used to connect all the placed rooms.
|
|
@export var corridor_room_scene : PackedScene
|
|
|
|
## Dungeon grid size measured in voxel units, voxel size is chosen in the voxel_scale property.
|
|
@export var dungeon_size := Vector3i(10,10,10) :
|
|
set(v):
|
|
dungeon_size = v.clamp(Vector3i(1,1,1),Vector3i(9999,9999,9999))
|
|
|
|
## Voxel scale/size in world units. Controls the standardized 1x1x1 room size of the dungeon.
|
|
## This should match the voxel scale on each of your rooms.voxel_scale
|
|
## If the voxel scale is different, the rooms will be scaled (perhaps non uniformly) to match the DungeonGenerator's voxel scale.
|
|
@export var voxel_scale := Vector3(10,10,10) :
|
|
set(v):
|
|
voxel_scale = v.clamp(Vector3(0.0001,0.0001,0.0001),Vector3(9999,9999,9999))
|
|
|
|
## Seed to use when generating the dungeon. Blank for random. You can call .generate(seed : int) to override.
|
|
## I'm setting this as a string because it didn't seem like you could set very large ints from editor if set to int.
|
|
@export var generate_seed : String = "" :
|
|
set(v):
|
|
var stripped_value = v.strip_edges().replace(r"[^0-9-]", "")
|
|
if stripped_value.begins_with("-"):
|
|
stripped_value = "-" + stripped_value.replace("-", "")
|
|
else:
|
|
stripped_value = stripped_value.replace("-", "")
|
|
generate_seed = stripped_value
|
|
|
|
@export var generate_on_ready : bool = true
|
|
## Depending on the dungeon kit used and the separation/connecting algorithm, it's possible that
|
|
## a dungeon may not be able to correctly generate and have all rooms reachable/not overlapping.
|
|
## In that case, the algorithm can simply restart from the beginning and try again.
|
|
@export var max_retries : int = 1
|
|
## Max total iterations in generate loop running one pass for the current stage.
|
|
## Generation stages: PLACE_ROOMS, PLACE_STAIRS, SEPARATE_ROOMS, CONNECT_ROOMS
|
|
@export var max_safe_iterations : int = 250
|
|
|
|
## Run generation of dungeon on a separate thread.
|
|
## It is safe to access vars .stage, .is_currently_generating, and .failed_to_generate but otherwise you should use .call_deferred_thread_group() to call any functions.
|
|
## Does not work in editor - was causing crashes so I disabled it.
|
|
@export var generate_threaded := false
|
|
|
|
## Generate the dungeon in editor. If you added any custom rooms which extend DungeonRoom,
|
|
## they also need the @tool directive for this to work.
|
|
@export var editor_button_generate_dungeon : bool = false :
|
|
set(value):
|
|
generate()
|
|
|
|
## Abort the generation started in editor
|
|
@export var abort_editor_button : bool = false :
|
|
set(value):
|
|
abort_generation()
|
|
|
|
@export_group("AStar room connection options")
|
|
enum AStarHeuristics { NONE_DIJKSTRAS = 0, MANHATTAN = 1, EUCLIDEAN = 2 }
|
|
## What heuristic to use for the AStar room connection algorithm.
|
|
## Euclidan - Standard, tends towards straight corridors connecting rooms.
|
|
## Manhattan - May lead to zigzagging corridors between rooms.
|
|
## Dijkstra's - No heuristic, this turns AStar into Dijkstra's algorithm. Guaranteed to find the shortest possible path but may lead to zigzagging corridors.
|
|
@export var astar_heuristic : AStarHeuristics = AStarHeuristics.EUCLIDEAN
|
|
## Increasing the heuristic scale may make the path less optimal but can help reduce zigzagging corridors.
|
|
## A heuristic of 3.0 (with either manhattan or euclidean, Dijkstra's already means 0 heuristic scale) may help reduce zigzagging corridors.
|
|
@export var heuristic_scale : float = 1.0
|
|
## By making the corridors cost less to walk through, the algorithm will tend towards merging into single corridors,
|
|
## thus making hallways more compact.
|
|
@export var corridor_cost_multiplier : float = 0.25
|
|
## Similar to corridor cost, setting this lower makes it so the algorithm will walk through a rooms to connect 2 rooms/doors, thus saving corridors placements.
|
|
## You could also set it greater than 1 to make the algorithm less likely to walk through existing (non-corridor) rooms.
|
|
@export var room_cost_multiplier : float = 0.25
|
|
## After connecting all rooms, some doors may not have been connected.
|
|
## By setting the cost for astar to walk through rooms higher at this stage, it encourages more interesting room connections,
|
|
## rather than just putting 1x1x1 corridor caps at doors.
|
|
@export var room_cost_at_end_for_required_doors : float = 2.0
|
|
|
|
@export_group("Debug options")
|
|
@export var show_debug_in_editor : bool = true
|
|
@export var show_debug_in_game : bool = false
|
|
## Whether to place the dungeon rooms so far even if the generation failed.
|
|
@export var place_even_if_fail : bool = false
|
|
@export var visualize_generation_progress : bool = false
|
|
# By default hide debug after gen. Othewise will lag.
|
|
@export var hide_debug_visuals_for_all_generated_rooms : bool = true
|
|
@export var cycle_debug_draw_when_press_n : bool = false
|
|
## A timer will be called with this wait time for the next iteration of the generation loop
|
|
@export_range(0, 1000, 1, "or_greater", "suffix:ms") var visualize_generation_wait_between_iterations : int = 100
|
|
|
|
###########################
|
|
## INIT/PROCESS/BUILTINS ##
|
|
###########################
|
|
|
|
func _init():
|
|
RenderingServer.set_debug_generate_wireframes(true)
|
|
|
|
func _input(event):
|
|
if cycle_debug_draw_when_press_n and event is InputEventKey and Input.is_key_pressed(KEY_N):
|
|
var vp = get_viewport()
|
|
vp.debug_draw = (vp.debug_draw + 1 ) % 4
|
|
|
|
var _debug_view = null
|
|
func add_debug_view_if_not_exist():
|
|
if not _debug_view:
|
|
_debug_view = preload("res://addons/SimpleDungeons/debug_visuals/DungeonGenerator3DDebugView.gd").new()
|
|
add_child(_debug_view)
|
|
|
|
func _ready():
|
|
add_debug_view_if_not_exist()
|
|
if Engine.is_editor_hint():
|
|
return
|
|
if generate_on_ready:
|
|
generate()
|
|
|
|
func _process(delta):
|
|
if Engine.is_editor_hint():
|
|
# Debug view doesn't get added in Editor sometimes, like if you manually drag script on.
|
|
for c in get_children():
|
|
if c is DungeonRoom3D and not c.virtualized_from:
|
|
c.add_debug_view_if_not_exist()
|
|
return
|
|
if _visualization_in_progress and Time.get_ticks_msec() - _last_iteration_end_time > visualize_generation_wait_between_iterations:
|
|
_run_generate_loop(false)
|
|
|
|
###################################
|
|
## GENERATION ENTRY POINT & LOOP ##
|
|
###################################
|
|
|
|
# Mostly setting up proper error handling, threads, visualization here.
|
|
# Most of the interesting stuff is in the build logic portion of the code below.
|
|
|
|
var room_instances : Array[DungeonRoom3D]
|
|
var corridor_room_instance : DungeonRoom3D
|
|
var iterations := 0
|
|
var retry_attempts := 0
|
|
var rooms_container : Node3D
|
|
var rng := RandomNumberGenerator.new()
|
|
|
|
var running_thread : Thread
|
|
var failed_to_generate := false :
|
|
set(v): t_mutex.lock(); failed_to_generate = v; t_mutex.unlock();
|
|
get: t_mutex.lock(); var v = failed_to_generate; t_mutex.unlock(); return v;
|
|
var full_abort_triggered := false :
|
|
set(v): t_mutex.lock(); full_abort_triggered = v; t_mutex.unlock();
|
|
get: t_mutex.lock(); var v = full_abort_triggered; t_mutex.unlock(); return v;
|
|
var is_currently_generating : bool :
|
|
get: return not (stage == BuildStage.NOT_STARTED or stage == BuildStage.DONE) and not failed_to_generate
|
|
|
|
var _is_generating_threaded = false
|
|
var _visualization_in_progress = false
|
|
var _last_iteration_end_time : int = Time.get_ticks_msec()
|
|
|
|
func generate(seed : int = int(generate_seed) if generate_seed.is_valid_int() else randi()) -> void:
|
|
if is_currently_generating:
|
|
_printerr("SimpleDungeons Error: Dungeon currently generating, cannot generate.")
|
|
return
|
|
|
|
stage = BuildStage.NOT_STARTED
|
|
|
|
if not validate_dungeon():
|
|
_printerr("SimpleDungeons Error: Cannot generate.")
|
|
return
|
|
|
|
rng = RandomNumberGenerator.new()
|
|
rng.seed = seed
|
|
print("DungeonGenerator3D generate(): Using seed ", seed)
|
|
|
|
cleanup_and_reset_dungeon_generator()
|
|
create_or_recreate_rooms_container()
|
|
get_preplaced_rooms() # Ensure cached because can't call get_children on threads
|
|
|
|
for room in get_preplaced_rooms():
|
|
room.snap_room_to_dungeon_grid()
|
|
if not room.validate_room():
|
|
_fail_generation("Could not validate preplaced rooms.")
|
|
return
|
|
room.ensure_doors_and_or_transform_cached_for_threads_and_virtualized_rooms()
|
|
|
|
stage = BuildStage.PREPARING
|
|
if not setup_room_instances_and_validate_before_generate():
|
|
_fail_generation("DungeonGenerator3D generation failed while setting up rooms.")
|
|
return
|
|
|
|
_is_generating_threaded = generate_threaded and not visualize_generation_progress
|
|
_visualization_in_progress = visualize_generation_progress
|
|
|
|
if _is_generating_threaded and Engine.is_editor_hint():
|
|
_is_generating_threaded = false
|
|
_printwarning("Disabling threaded generation because in editor. Kept running into crashes with editor threads, looked like Godot bugs, so disabling for now. You can still use visualize generation in editor. Threaded generation in game seems to work fine.")
|
|
|
|
if _is_generating_threaded:
|
|
running_thread = Thread.new()
|
|
running_thread.start(_run_generate_loop)
|
|
else:
|
|
_run_generate_loop()
|
|
|
|
func _run_generate_loop(first_call : bool = true) -> void:
|
|
if first_call:
|
|
retry_attempts = 0
|
|
iterations = 0
|
|
failed_to_generate = false
|
|
stage = BuildStage.PLACE_ROOMS
|
|
while retry_attempts <= max_retries and not full_abort_triggered:
|
|
while iterations < max_safe_iterations and not failed_to_generate and stage != BuildStage.FINALIZING:
|
|
var start_stage := stage
|
|
_run_one_loop_iteration_and_increment_iterations()
|
|
_last_iteration_end_time = Time.get_ticks_msec()
|
|
if _visualization_in_progress and stage != BuildStage.FINALIZING and not failed_to_generate:
|
|
return # Will be called again from _process
|
|
if stage == BuildStage.FINALIZING:
|
|
break
|
|
# If the dungeon failed to generate after looping through all stages until max_safe_iterations, retry.
|
|
retry_attempts += 1
|
|
if retry_attempts > max_retries:
|
|
_fail_generation("Reached max generation retries. Failed to generate. Failed at stage "+BuildStage.find_key(stage))
|
|
break
|
|
else:
|
|
clear_rooms_container_and_setup_for_next_iteration()
|
|
iterations = 0
|
|
failed_to_generate = false
|
|
stage = BuildStage.PLACE_ROOMS
|
|
if _visualization_in_progress:
|
|
return # Will be called again from _process
|
|
_printwarning("Generation failed on attempt "+str(retry_attempts)+" at stage "+BuildStage.find_key(stage)+". Retrying generation.")
|
|
|
|
_visualization_in_progress = false
|
|
if _is_generating_threaded:
|
|
if not failed_to_generate: _dungeon_finished_generating.call_deferred()
|
|
else: _dungeon_failed_generating.call_deferred()
|
|
else:
|
|
if not failed_to_generate: _dungeon_finished_generating()
|
|
else: _dungeon_failed_generating()
|
|
|
|
var _stage_just_changed = false
|
|
func _run_one_loop_iteration_and_increment_iterations() -> void:
|
|
if iterations == 0:
|
|
_stage_just_changed = true
|
|
|
|
var cur_stage = stage
|
|
# Each stage will increment stage once it is done.
|
|
# Also, each stage can call _abort_generation_and_fail if it encounters any errors.
|
|
if stage == BuildStage.PLACE_ROOMS:
|
|
place_room_iteration(_stage_just_changed)
|
|
elif stage == BuildStage.PLACE_STAIRS:
|
|
place_stairs_iteration(_stage_just_changed)
|
|
elif stage == BuildStage.SEPARATE_ROOMS:
|
|
# This looked to be so extremely slow to run all the checks after each separate iteration, so just going to iterate here:
|
|
separate_rooms_iteration(_stage_just_changed)
|
|
elif stage == BuildStage.CONNECT_ROOMS:
|
|
connect_rooms_iteration(_stage_just_changed)
|
|
|
|
_stage_just_changed = stage != cur_stage
|
|
iterations += 1
|
|
|
|
func _fail_generation(error : String = "Aborted generation") -> void:
|
|
_printerr("SimpleDungeons Error: ", error)
|
|
_printerr("SimpleDungeons Error: Failed to generate dungeon")
|
|
failed_to_generate = true
|
|
|
|
func abort_generation():
|
|
if not is_currently_generating:
|
|
_printwarning("DungeonGenerator3D not currently generating.")
|
|
return
|
|
failed_to_generate = true
|
|
full_abort_triggered = true
|
|
_printerr("abort_generation() called")
|
|
if running_thread and running_thread.is_alive() and OS.get_main_thread_id() == OS.get_thread_caller_id():
|
|
running_thread.wait_to_finish()
|
|
for room in room_instances:
|
|
room.queue_free()
|
|
for room in _rooms_placed:
|
|
room.queue_free()
|
|
rooms_container.queue_free()
|
|
if rooms_container.is_inside_tree():
|
|
rooms_container.get_parent().remove_child(rooms_container)
|
|
|
|
func _finalize_rooms(ready_callback = null) -> void:
|
|
if not rooms_container.is_inside_tree():
|
|
add_child(rooms_container)
|
|
rooms_container.owner = self.owner
|
|
for room in _rooms_placed.slice(0):
|
|
_rooms_placed.erase(room)
|
|
var unvirtualized = room.unvirtualize_and_free_clone_if_needed(rooms_container)
|
|
unvirtualized.owner = self.owner
|
|
_rooms_placed.push_back(unvirtualized)
|
|
for room in room_instances:
|
|
if room and is_instance_valid(room):
|
|
room.queue_free()
|
|
room_instances = []
|
|
if corridor_room_instance and is_instance_valid(corridor_room_instance):
|
|
corridor_room_instance.queue_free()
|
|
corridor_room_instance = null
|
|
if ready_callback is Callable:
|
|
if rooms_container.is_node_ready():
|
|
ready_callback.call_deferred()
|
|
else:
|
|
rooms_container.ready.connect(ready_callback)
|
|
|
|
|
|
func _dungeon_finished_generating() -> void:
|
|
_finalize_rooms(_emit_done_signals)
|
|
|
|
func _dungeon_failed_generating() -> void:
|
|
if place_even_if_fail:
|
|
_finalize_rooms()
|
|
elif not visualize_generation_progress:
|
|
rooms_container.queue_free()
|
|
for room in _rooms_placed:
|
|
room.queue_free()
|
|
for room in room_instances:
|
|
room.queue_free()
|
|
_emit_failed_signal.call_deferred() # Ensure rooms container placed/might be on thread.
|
|
|
|
# Emit done signals for dungeon & place_room for all DungeonRooms.
|
|
func _emit_done_signals():
|
|
stage = BuildStage.DONE
|
|
# Also need to call emit signal for each of place_rooms
|
|
for room in _rooms_placed:
|
|
if not room.original_ready_func_called:
|
|
_printwarning("_ready not called on "+room.name+". Room placement, finalization, and doors will be broken. Make sure to call super._ready() at the top of your _ready func when inheriting DungeonRoom3D.")
|
|
room.dungeon_done_generating.emit()
|
|
for preplaced_room in find_children("*", "DungeonRoom3D", false):
|
|
preplaced_room.dungeon_done_generating.emit()
|
|
print("DungeonGenerator3D finished generating.")
|
|
for room in room_instances:
|
|
room.queue_free()
|
|
done_generating.emit()
|
|
|
|
func _emit_failed_signal(): # So I can call_deferred
|
|
if running_thread:
|
|
running_thread.wait_to_finish()
|
|
generating_failed.emit()
|
|
|
|
#########################
|
|
## DUNGEON BUILD LOGIC ##
|
|
#########################
|
|
|
|
# Placing rooms: A random room is selected from room_scenes until the min_count of all is matched
|
|
|
|
var _rooms_placed : Array[DungeonRoom3D]
|
|
var _custom_rand_rooms : Array[DungeonRoom3D]
|
|
var _use_custom_rand_rooms := false
|
|
func place_room_iteration(first_call_in_loop : bool) -> void:
|
|
if first_call_in_loop:
|
|
_rooms_placed = []
|
|
_custom_rand_rooms = []
|
|
_use_custom_rand_rooms = false
|
|
if custom_get_rooms_function is Callable:
|
|
_use_custom_rand_rooms = true
|
|
_custom_rand_rooms = custom_get_rooms_function.call(room_instances, rng)
|
|
if not _custom_rand_rooms is Array or len(_custom_rand_rooms) == 0:
|
|
_fail_generation("custom_get_rooms_function takes should return a non-empty Array of DungeonRoom3Ds.")
|
|
_printwarning("custom_get_rooms_function takes (room_instances : Array[DungeonRoom3D], rng_seeded : RandomNumberGenerator) as the arguments and should use .create_clone_and_make_virtual_unless_visualizing() to clone and then position with .set_position_by_grid_pos(Vector3i) or .rotation = 0 through 3 for number of 90 degree y rotations for the room.")
|
|
return
|
|
for room in _custom_rand_rooms:
|
|
if not room is DungeonRoom3D:
|
|
_fail_generation("custom_get_rooms_function supplied an object that is not a DungeonRoom3D. Ensure all rooms supplied inherit DungeonRoom3D, and use the @tool annotation if generating in editor.")
|
|
return
|
|
if room_instances.find(room) != -1:
|
|
_fail_generation("custom_get_rooms_function supplied a room instance without cloning it. Always use DungeonRoom3D.create_clone_and_make_virtual_unless_visualizing() to create room instances.")
|
|
return
|
|
|
|
var rand_room : DungeonRoom3D
|
|
if _use_custom_rand_rooms:
|
|
rand_room = _custom_rand_rooms.pop_front()
|
|
else:
|
|
if get_rooms_less_than_max_count(false).size() > 0:
|
|
rand_room = get_randomly_positioned_room()
|
|
|
|
if rand_room:
|
|
place_room(rand_room)
|
|
|
|
if _use_custom_rand_rooms:
|
|
if _custom_rand_rooms.size() == 0:
|
|
stage += 1
|
|
else:
|
|
if get_rooms_less_than_min_count(false).size() == 0:
|
|
if _rooms_placed.size() == 0:
|
|
_fail_generation("Unable to place any rooms. Ensure min_count and max_count are set correctly on rooms.")
|
|
else:
|
|
stage += 1
|
|
|
|
# Placing stairs:
|
|
|
|
# Array [DungeonRoom3D, grid_pos_y] for where to place rooms
|
|
var _stair_rooms_and_placements = []
|
|
var _stair_rooms_placed_count = {}
|
|
func place_stairs_iteration(first_call_in_loop : bool) -> void:
|
|
if first_call_in_loop:
|
|
_stair_rooms_placed_count = {}
|
|
for s in get_stair_rooms_from_instances():
|
|
_stair_rooms_placed_count[s] = 0
|
|
_stair_rooms_and_placements = _make_and_solve_floors_graph()
|
|
if failed_to_generate:
|
|
return # solve failed and called abort
|
|
|
|
if _stair_rooms_and_placements.size() > 0:
|
|
var stair_and_y_pos = _stair_rooms_and_placements.pop_back()
|
|
_stair_rooms_placed_count[stair_and_y_pos[0]] += 1
|
|
var room = stair_and_y_pos[0].create_clone_and_make_virtual_unless_visualizing()
|
|
var y_pos = stair_and_y_pos[1]
|
|
room.room_rotations = rng.randi_range(0,4)
|
|
room.set_position_by_grid_pos(Vector3i(
|
|
rng.randi_range(0, dungeon_size.x - room.get_grid_aabbi(true).size.x),
|
|
y_pos,
|
|
rng.randi_range(0, dungeon_size.z - room.get_grid_aabbi(true).size.z)))
|
|
place_room(room)
|
|
elif get_stair_rooms_from_instances().filter(func(s): return s.min_count > _stair_rooms_placed_count[s]).size() > 0:
|
|
var stairs_less_than_min = get_stair_rooms_from_instances().filter(func(s): return s.min_count > _stair_rooms_placed_count[s])
|
|
var room_original = stairs_less_than_min[rng.randi() % stairs_less_than_min.size()]
|
|
_stair_rooms_placed_count[room_original] += 1
|
|
var room = room_original.create_clone_and_make_virtual_unless_visualizing()
|
|
room.room_rotations = rng.randi_range(0,4)
|
|
room.set_position_by_grid_pos(Vector3i(
|
|
rng.randi_range(0, dungeon_size.x - room.get_grid_aabbi(true).size.x),
|
|
rng.randi_range(0, dungeon_size.y - room.get_grid_aabbi(true).size.y),
|
|
rng.randi_range(0, dungeon_size.z - room.get_grid_aabbi(true).size.z)))
|
|
place_room(room)
|
|
else:
|
|
stage += 1
|
|
|
|
# Encapsulate some checks for stair room instances & how they can connect two or more floors.
|
|
class StairRoomInfo:
|
|
var inst : DungeonRoom3D
|
|
var stair_gaps = [] # Has door(s) leading 1 floors up, 2 floors up, etc.
|
|
var stair_gaps_dict = {} # Gap:local door y position. To check y offset to place room at when connecting floors.
|
|
var lowest_door_local_y_pos : int
|
|
var available_to_use : int # Make sure not to go above stair's max_count value
|
|
|
|
# Helper funcs for the stair chains where it might fail
|
|
var _saved_available_to_use : int = 0
|
|
func save_available_to_use():
|
|
_saved_available_to_use = available_to_use
|
|
func restore_available_to_use():
|
|
available_to_use =_saved_available_to_use
|
|
|
|
func _init(room_instance : DungeonRoom3D):
|
|
self.inst = room_instance
|
|
available_to_use = room_instance.max_count
|
|
|
|
var door_y_positions = []
|
|
for door in room_instance.get_doors_cached():
|
|
if not door.local_pos.y in door_y_positions:
|
|
door_y_positions.push_back(door.local_pos.y)
|
|
door_y_positions.sort()
|
|
stair_gaps_dict = {}
|
|
for i in range(0, len(door_y_positions)):
|
|
for j in range(i+1, len(door_y_positions)):
|
|
var gap = door_y_positions[j] - door_y_positions[i]
|
|
if not stair_gaps_dict.has(gap): stair_gaps_dict[gap] = []
|
|
stair_gaps_dict[gap].append(door_y_positions[i])
|
|
stair_gaps = stair_gaps_dict.keys()
|
|
|
|
# format: [[DungeonRoom3D, grid_pos_y], ...]. returns [] if no valid way to connect those floors
|
|
func get_valid_connect_positions(floor_1 : int, floor_2 : int) -> Array:
|
|
if available_to_use == 0: return []
|
|
|
|
var bottom_floor : int = min(floor_1, floor_2)
|
|
var top_floor : int = max(floor_1, floor_2)
|
|
var gap : int = top_floor - bottom_floor
|
|
if stair_gaps_dict.has(gap):
|
|
# Find actual valid pos from door local y positions, where doors spanning gap will land on floor_1 and floor_2
|
|
var valid_offsets = stair_gaps_dict[gap].filter(func(y_offset : int): return floor_1 - y_offset >= 0 and floor_1 - y_offset + inst.size_in_voxels.y <= inst.dungeon_generator.dungeon_size.y)
|
|
return valid_offsets.map(func(y_offset : int): return [inst, floor_1 - y_offset])
|
|
return []
|
|
|
|
# Returns empty array if couldn't find a chain of stair rooms to connect the two floors.
|
|
# The main case I want to solve here is just a simple chain of multiple 2 floor stairs to connect a more than 2 floor gap.
|
|
# There are a ton of edge cases, which I won't even check for.
|
|
# This is some heuristic algorithm I thought of which just tries to find a set of rooms which when chained together,
|
|
# continually closes the gap between floors. Should work for basic cases and also some more advanced ones.
|
|
func _find_stair_chain_to_connect_floors(floor_1 : int, floor_2 : int, floor_graph : TreeGraph, stair_info_dict : Dictionary) -> Array:
|
|
var stair_chain := []
|
|
var bottom_floor : int = min(floor_1, floor_2)
|
|
var top_floor : int = max(floor_1, floor_2)
|
|
var cur_floor := bottom_floor
|
|
for s in stair_info_dict.values():
|
|
s.save_available_to_use()
|
|
while cur_floor != top_floor:
|
|
# Valid floor = any floor which is closer to top_floor than cur_floor.
|
|
var valid_to_floors := range(dungeon_size.y).filter(func(floor : int): return abs(floor - top_floor) < abs(top_floor - cur_floor))
|
|
var valid_stair_placements := {}
|
|
for to_floor in valid_to_floors:
|
|
for stair_info in stair_info_dict.values():
|
|
var valid_connect = stair_info.get_valid_connect_positions(cur_floor, to_floor)
|
|
if valid_connect.size() > 0:
|
|
if not valid_stair_placements.has(to_floor): valid_stair_placements[to_floor] = []
|
|
valid_stair_placements[to_floor].append_array(valid_connect)
|
|
if valid_stair_placements.keys().size() == 0:
|
|
for s in stair_info_dict.values():
|
|
s.restore_available_to_use()
|
|
return [] # None found
|
|
var to_floor : int = valid_stair_placements.keys()[rng.randi() % valid_stair_placements.keys().size()]
|
|
var choose = valid_stair_placements[to_floor][rng.randi() % valid_stair_placements[to_floor].size()]
|
|
if not floor_graph.has_node(cur_floor) or not floor_graph.has_node(to_floor) or not floor_graph.are_nodes_connected(cur_floor, to_floor):
|
|
stair_chain.push_back(choose) # No need unless not connected already
|
|
stair_info_dict[choose[0]].available_to_use -= 1
|
|
cur_floor = to_floor
|
|
return stair_chain
|
|
|
|
# returns Array with elements in format [room : DungeonRoom3D, room_grid_pos_y : int]
|
|
func _make_and_solve_floors_graph() -> Array:
|
|
# Set up a tree (union find) graph of all the floors with doors on them.
|
|
# This keeps track of what floors are connected by rooms spanning multiple floors.
|
|
var floors_tree_graph := TreeGraph.new()
|
|
var add_room_to_floors_graph := (func(room : DungeonRoom3D, room_y_pos : int):
|
|
for door in room.get_doors_cached():
|
|
var door_exit_y = door.local_pos.y + room_y_pos
|
|
if not door_exit_y in floors_tree_graph.get_all_nodes():
|
|
floors_tree_graph.add_node(door_exit_y)
|
|
floors_tree_graph.connect_nodes(door_exit_y, room.get_doors_cached()[0].local_pos.y + room_y_pos))
|
|
var rooms = []
|
|
rooms.append_array(_rooms_placed)
|
|
rooms.append_array(get_preplaced_rooms())
|
|
for room in rooms:
|
|
add_room_to_floors_graph.call(room, room.get_grid_pos().y)
|
|
|
|
if not floors_tree_graph.is_fully_connected() and get_stair_rooms_from_instances().size() == 0:
|
|
_fail_generation("No stair rooms defined. Add a room with with is_stair_room set to true.")
|
|
return []
|
|
|
|
# Solve the stair graph. We'll use a heuristic algorithm.
|
|
# Tried to think of something elegant to do this but it may be harder than I thought.
|
|
# This should be fine for 99% of cases. For truly odd stair patterns in dungeons you can preplace stairs or mod yourself.
|
|
|
|
var stair_info_dict : Dictionary = {}
|
|
for room in get_stair_rooms_from_instances():
|
|
stair_info_dict[room] = StairRoomInfo.new(room)
|
|
|
|
var stairs_to_add : Array = [] # element format: [DungeonRoom3D, grid_pos_y]
|
|
while not floors_tree_graph.is_fully_connected():
|
|
# Simple case - look for when we can directly connect 2 floors with a stair
|
|
var stair_able_to_connect_floors_directly = []
|
|
for f1 in floors_tree_graph.get_all_nodes():
|
|
for f2 in floors_tree_graph.get_all_nodes().filter(func(_f2): return _f2 > f1):
|
|
if floors_tree_graph.are_nodes_connected(f1, f2): continue
|
|
for s in stair_info_dict.values():
|
|
# Just looking for 1 valid element [DungeonRoom3D, grid_pos_y]
|
|
stair_able_to_connect_floors_directly.append_array(s.get_valid_connect_positions(f1, f2))
|
|
if stair_able_to_connect_floors_directly.size() > 0:
|
|
break
|
|
if stair_able_to_connect_floors_directly.size() > 0: break
|
|
if stair_able_to_connect_floors_directly.size() > 0: break
|
|
if stair_able_to_connect_floors_directly.size() > 0:
|
|
var stair_and_pos = stair_able_to_connect_floors_directly[rng.randi() % stair_able_to_connect_floors_directly.size()]
|
|
stairs_to_add.push_back(stair_and_pos)
|
|
add_room_to_floors_graph.call(stair_and_pos[0], stair_and_pos[1])
|
|
stair_info_dict[stair_and_pos[0]].available_to_use -= 1
|
|
continue
|
|
# There are many cases where it's not possible to directly connect floors,
|
|
# but you could chain more than 1 stair room together to connect them.
|
|
# So here we'll do a non exhaustive search which attempts to cover most of these cases.
|
|
var stair_room_chain_to_connect_floors = []
|
|
for f1 in floors_tree_graph.get_all_nodes():
|
|
var nearest_floors_up = floors_tree_graph.get_all_nodes().filter(func(_f2): return _f2 > f1)
|
|
nearest_floors_up.sort_custom(func(fa,fb): return abs(fb - f1) > abs(fa - f1))
|
|
for f2 in nearest_floors_up:
|
|
if floors_tree_graph.are_nodes_connected(f1, f2): continue
|
|
stair_room_chain_to_connect_floors = _find_stair_chain_to_connect_floors(f1, f2, floors_tree_graph, stair_info_dict)
|
|
if stair_room_chain_to_connect_floors.size() > 0:
|
|
break
|
|
if stair_room_chain_to_connect_floors.size() > 0: break
|
|
if stair_room_chain_to_connect_floors.size() > 0:
|
|
for room_and_pos in stair_room_chain_to_connect_floors:
|
|
add_room_to_floors_graph.call(room_and_pos[0], room_and_pos[1])
|
|
stairs_to_add.push_back(room_and_pos)
|
|
continue
|
|
|
|
_fail_generation("Failed to connect all floors together with stairs. Ensure you have at least 1 DungeonRoom3D with 'is_stair_room' set to true with 2 or more doors leading different floors. Simplest is a 2 floor room with 1 door on each floor.")
|
|
_printerr("Stair algorithm failed. If your stairs are shaped very oddly it can fail, it's not an exhaustive search but should work for most cases. Also ensure stairs max_count is enough to connect all the floors in your dungeon.")
|
|
for s in stair_info_dict.values():
|
|
_printwarning("Room "+str(s.inst.name)+" max_count is "+str(s.inst.max_count)+". One potential reason this could fail is you need to increase the max_count on your stair room(s) so all floors can be connected.")
|
|
return []
|
|
|
|
return stairs_to_add
|
|
|
|
# Separating rooms:
|
|
|
|
var _aabbis_with_doors = {}
|
|
func separate_rooms_iteration(first_call_in_loop : bool) -> void:
|
|
var rooms = get_all_placed_and_preplaced_rooms()
|
|
if first_call_in_loop:
|
|
_aabbis_with_doors = {}
|
|
for room in rooms:
|
|
_aabbis_with_doors[room] = room.get_grid_aabbi(true)
|
|
|
|
var any_overlap := false
|
|
for i in range(0, len(rooms)):
|
|
for j in range(i + 1, len(rooms)):
|
|
var fast_check = _aabbis_with_doors[rooms[i]].intersects(_aabbis_with_doors[rooms[j]])
|
|
if fast_check and rooms[i].overlaps_room(rooms[j]):
|
|
if not rooms[i] in get_preplaced_rooms():
|
|
rooms[i].push_away_from_and_stay_within_bounds(rooms[j])
|
|
_aabbis_with_doors[rooms[i]] = rooms[i].get_grid_aabbi(true)
|
|
if not rooms[j] in get_preplaced_rooms():
|
|
rooms[j].push_away_from_and_stay_within_bounds(rooms[i])
|
|
_aabbis_with_doors[rooms[j]] = rooms[j].get_grid_aabbi(true)
|
|
any_overlap = true
|
|
|
|
if not any_overlap:
|
|
stage += 1
|
|
|
|
# Connecting rooms:
|
|
|
|
var _astar3d : DungeonAStar3D
|
|
var _quick_room_check_dict : Dictionary
|
|
var _quick_corridors_check_dict : Dictionary
|
|
var _non_corridor_rooms : Array
|
|
var _rooms_to_connect : Array
|
|
var _all_doors_dict : Dictionary
|
|
var _required_doors_dict : Dictionary
|
|
var _last_rooms_to_connect_counts := []
|
|
func connect_rooms_iteration(first_call_in_loop : bool) -> void:
|
|
if first_call_in_loop:
|
|
_rooms_to_connect = []
|
|
_last_rooms_to_connect_counts = []
|
|
_non_corridor_rooms = []
|
|
_quick_room_check_dict = {}
|
|
_quick_corridors_check_dict = {}
|
|
_all_doors_dict = {}
|
|
_required_doors_dict = {}
|
|
for room in get_all_placed_and_preplaced_rooms():
|
|
var doors = room.get_doors_cached()
|
|
if room.get_doors_cached().size() > 0:
|
|
_rooms_to_connect.push_back(room)
|
|
_non_corridor_rooms.push_back(room)
|
|
for door in doors:
|
|
if get_grid_aabbi().contains_point(door.exit_pos_grid):
|
|
_all_doors_dict[door.exit_pos_grid] = door
|
|
if not door.optional:
|
|
_required_doors_dict[door.exit_pos_grid] = door
|
|
var aabbi = room.get_grid_aabbi(false)
|
|
for x in aabbi.size.x: for y in aabbi.size.y: for z in aabbi.size.z:
|
|
_quick_room_check_dict[aabbi.position + Vector3i(x,y,z)] = room
|
|
# Init after corridors/room dict setup
|
|
_astar3d = DungeonAStar3D.new(self, _quick_room_check_dict, _quick_corridors_check_dict)
|
|
# Sort rooms by door y positions. Likely to make astar connections more stable
|
|
_rooms_to_connect.sort_custom(func(a,b): return b.get_doors_cached()[0].exit_pos_grid.y > a.get_doors_cached()[0].exit_pos_grid.y)
|
|
|
|
# Can exit early if _rooms_to_connect stops going down, aka we tried shuffling and couldn't find a connection
|
|
# Don't think this is necessary actually, I just exit below instead if it fails ever.
|
|
#_last_rooms_to_connect_counts.push_front(len(_rooms_to_connect))
|
|
#_last_rooms_to_connect_counts = _last_rooms_to_connect_counts.filter(func(c): return c == _last_rooms_to_connect_counts[0])
|
|
#if len(_last_rooms_to_connect_counts) > (len(_rooms_to_connect) * 4) and _last_rooms_to_connect_counts[0] == len(_rooms_to_connect):
|
|
#_fail_generation("Unable to connect all rooms")
|
|
#return
|
|
|
|
# First, just pathfind through all the rooms, one to the next, until all rooms are connected
|
|
if len(_rooms_to_connect) >= 2:
|
|
var room_0_pos = _rooms_to_connect[0].get_grid_aabbi(false).position
|
|
var room_1_pos = _rooms_to_connect[1].get_grid_aabbi(false).position
|
|
var connect_path := _astar3d.get_vec3i_path(room_0_pos, room_1_pos)
|
|
var room_a := _rooms_to_connect.pop_front()
|
|
if len(connect_path) == 0:
|
|
#print("Failed somehow")
|
|
#_rooms_to_connect.insert(rng.randi_range(1, len(_rooms_to_connect)), room_a)
|
|
_fail_generation("Failed to fully connect dungeon rooms with corridors.")
|
|
return
|
|
#print("Connecting ", room_a.name, " to ", _rooms_to_connect[0].name, ". Result: ", connect_path)
|
|
for corridor_pos in connect_path:
|
|
if not _quick_room_check_dict.has(corridor_pos) and not _quick_corridors_check_dict.has(corridor_pos):
|
|
var room := corridor_room_instance.create_clone_and_make_virtual_unless_visualizing()
|
|
room.set_position_by_grid_pos(corridor_pos)
|
|
place_room(room)
|
|
_quick_corridors_check_dict[corridor_pos] = room
|
|
return
|
|
|
|
# Next, not all the doors may be connected, so we have to do some strategy to nicely connect the required doors remaining.
|
|
for required_door in _required_doors_dict.values().slice(0): # Slice necessary? Not sure.
|
|
# No need to connect doors which already have a corridor
|
|
if _quick_room_check_dict.has(required_door.exit_pos_grid) or _quick_corridors_check_dict.has(required_door.exit_pos_grid):
|
|
_required_doors_dict.erase(required_door)
|
|
continue
|
|
# Get other room doors which are closest to the required door
|
|
var closest_other_room_doors = _all_doors_dict.values().slice(0)#filter(func(d): return d.room != required_door.room)
|
|
closest_other_room_doors.sort_custom(func(a,b):
|
|
# Make sure doors not on same floor sorted far after
|
|
# Also make sure doors of same room sorted far after
|
|
var b_dist = Vector3(b.exit_pos_grid - required_door.exit_pos_grid).length()
|
|
var a_dist = Vector3(a.exit_pos_grid - required_door.exit_pos_grid).length()
|
|
if a.exit_pos_grid.y != required_door.exit_pos_grid.y or a.room == required_door.room:
|
|
a_dist += dungeon_size.x + dungeon_size.y + dungeon_size.z
|
|
if b.exit_pos_grid.y != required_door.exit_pos_grid.y or b.room == required_door.room:
|
|
b_dist += dungeon_size.x + dungeon_size.y + dungeon_size.z
|
|
return b_dist > a_dist)
|
|
|
|
_astar3d.cap_required_doors_phase = true
|
|
var connect_path := _astar3d.get_vec3i_path(required_door.exit_pos_grid, closest_other_room_doors[0].exit_pos_grid)
|
|
for corridor_pos in connect_path:
|
|
if not _quick_room_check_dict.has(corridor_pos) and not _quick_corridors_check_dict.has(corridor_pos):
|
|
var room := corridor_room_instance.create_clone_and_make_virtual_unless_visualizing()
|
|
room.set_position_by_grid_pos(corridor_pos)
|
|
place_room(room)
|
|
_quick_corridors_check_dict[corridor_pos] = room
|
|
_required_doors_dict.erase(required_door)
|
|
return
|
|
|
|
stage = BuildStage.FINALIZING
|
|
|
|
####################################
|
|
## DUNGEON BUILD HELPER FUNCTIONS ##
|
|
####################################
|
|
|
|
func get_rooms_less_than_min_count(include_stairs : bool):
|
|
return room_instances.filter(func(room : DungeonRoom3D):
|
|
if not include_stairs and room.is_stair_room:
|
|
return false
|
|
var already_in_tree = _rooms_placed.filter(func(placed_room : DungeonRoom3D):
|
|
return room.get_original_packed_scene() == placed_room.get_original_packed_scene())
|
|
return len(already_in_tree) < room.min_count)
|
|
|
|
func get_rooms_less_than_max_count(include_stairs : bool):
|
|
return room_instances.filter(func(room : DungeonRoom3D):
|
|
if not include_stairs and room.is_stair_room:
|
|
return false
|
|
var already_in_tree = _rooms_placed.filter(func(placed_room : DungeonRoom3D):
|
|
return room.get_original_packed_scene() == placed_room.get_original_packed_scene())
|
|
return len(already_in_tree) < room.min_count)
|
|
|
|
func get_randomly_positioned_room() -> DungeonRoom3D:
|
|
var room : DungeonRoom3D = get_rooms_less_than_max_count(false)[rng.randi() % get_rooms_less_than_max_count(false).size()]
|
|
room = room.create_clone_and_make_virtual_unless_visualizing()
|
|
var buf := dungeon_size - room.get_grid_aabbi(false).size
|
|
var rand_pos : Vector3i = Vector3i(rng.randi_range(0, buf.x), rng.randi_range(0, buf.y), rng.randi_range(0, buf.z))
|
|
room.room_rotations = rng.randi_range(0,3)
|
|
room.set_position_by_grid_pos(rand_pos)
|
|
return room
|
|
|
|
func place_room(room : DungeonRoom3D) -> void:
|
|
room.dungeon_generator = self # Incase wasn't set yet
|
|
_rooms_placed.push_back(room)
|
|
if visualize_generation_progress:
|
|
rooms_container.add_child(room)
|
|
room.snap_room_to_dungeon_grid()
|
|
|
|
###################################
|
|
## INITIALIZE/CLEANUP GENERATION ##
|
|
###################################
|
|
|
|
func create_or_recreate_rooms_container() -> void:
|
|
if rooms_container and is_instance_valid(rooms_container):
|
|
if rooms_container.is_inside_tree():
|
|
rooms_container.get_parent().remove_child(rooms_container)
|
|
rooms_container.queue_free()
|
|
if get_node_or_null("RoomsContainer"): # Incase it's still a child if rooms_container was null somehow
|
|
var rc = get_node_or_null("RoomsContainer")
|
|
remove_child(rc)
|
|
rc.queue_free()
|
|
rooms_container = Node3D.new()
|
|
rooms_container.name = "RoomsContainer"
|
|
if visualize_generation_progress:
|
|
add_child(rooms_container)
|
|
|
|
func clear_rooms_container_and_setup_for_next_iteration():
|
|
if visualize_generation_progress:
|
|
for c in rooms_container.get_children():
|
|
c.queue_free()
|
|
rooms_container.remove_child(c)
|
|
else:
|
|
for room in _rooms_placed:
|
|
room.queue_free()
|
|
_rooms_placed = []
|
|
|
|
# Clears the room & corridor instances pool, unless they are in tree.
|
|
# For flexibility checking if in tree
|
|
# Room instances are used as a library to run checks on,
|
|
# but could allow placing them as last room of their kind to save some memory/an instantiate call.
|
|
func _clear_room_instances() -> void:
|
|
for room in room_instances:
|
|
if room and is_instance_valid(room):
|
|
room.queue_free()
|
|
if corridor_room_instance and is_instance_valid(corridor_room_instance):
|
|
corridor_room_instance.queue_free()
|
|
room_instances = []
|
|
corridor_room_instance = null
|
|
|
|
func cleanup_and_reset_dungeon_generator() -> void:
|
|
if is_currently_generating:
|
|
_fail_generation("Dungeon reset while generating.")
|
|
if running_thread:
|
|
if running_thread.is_alive() or running_thread.is_started():
|
|
running_thread.wait_to_finish()
|
|
running_thread = null
|
|
if get_node_or_null("RoomsContainer"):
|
|
var rc = get_node_or_null("RoomsContainer")
|
|
remove_child(rc)
|
|
rc.queue_free()
|
|
_clear_room_instances()
|
|
failed_to_generate = false
|
|
full_abort_triggered = false
|
|
iterations = 0
|
|
retry_attempts = 0
|
|
stage = BuildStage.NOT_STARTED
|
|
|
|
func setup_room_instances_and_validate_before_generate() -> bool:
|
|
if not room_scenes:
|
|
_printerr("SimpleDungeons Error: No DungeonRoom3D room scenes set.")
|
|
return false
|
|
var inst_arr : Array[DungeonRoom3D] = []
|
|
for s in room_scenes:
|
|
if not s: continue
|
|
var inst = s.instantiate()
|
|
if not inst is DungeonRoom3D:
|
|
_printerr("SimpleDungeons Error: "+s.resource_path+" room scene does not inherit DungeonRoom3D. Also may need @tool annotation if generating in editor.")
|
|
return false
|
|
else:
|
|
inst.dungeon_generator = self
|
|
# Need to save door info before cloning/making virtual copies w/o actual nodes/meshes inside for performance
|
|
inst.ensure_doors_and_or_transform_cached_for_threads_and_virtualized_rooms()
|
|
inst_arr.append(inst as DungeonRoom3D)
|
|
room_instances = inst_arr
|
|
corridor_room_instance = corridor_room_scene.instantiate() if corridor_room_scene else null
|
|
if corridor_room_instance:
|
|
#corridor_room_instance.dungeon_generator = self
|
|
corridor_room_instance.set("dungeon_generator", self)
|
|
#corridor_room_instance.ensure_doors_and_or_transform_cached_for_threads_and_virtualized_rooms()
|
|
return validate_dungeon()
|
|
|
|
####################
|
|
## UTIL FUNCTIONS ##
|
|
####################
|
|
|
|
# printerr() and push_warning() eat my outputs a lot. Regular prints are more reliable.
|
|
func _printerr(str : String, str2 : String = "", str3 : String = "", str4 : String = ""):
|
|
print_rich("[color=#FF3531]"+(str+str2+str3+str4)+"[/color]")
|
|
func _printwarning(str : String, str2 : String = "", str3 : String = "", str4 : String = ""):
|
|
print_rich("[color=#FFF831]"+(str+str2+str3+str4)+"[/color]")
|
|
|
|
func get_grid_aabbi() -> AABBi:
|
|
return AABBi.new(Vector3i(0,0,0), dungeon_size)
|
|
|
|
func get_room_at_pos(grid_pos : Vector3i) -> DungeonRoom3D:
|
|
if stage > BuildStage.CONNECT_ROOMS:
|
|
# Can use these vars for speedup if past the connect rooms stage where we set them
|
|
var quick_check = _quick_room_check_dict.get(grid_pos)
|
|
return quick_check if quick_check else _quick_corridors_check_dict.get(grid_pos)
|
|
for room in get_all_placed_and_preplaced_rooms():
|
|
if room.get_grid_aabbi(false).contains_point(grid_pos):
|
|
return room
|
|
return null
|
|
|
|
var _preplaced_rooms_cached : Array = []
|
|
func get_preplaced_rooms() -> Array:
|
|
var rooms := []
|
|
if OS.get_thread_caller_id() != OS.get_main_thread_id():
|
|
return _preplaced_rooms_cached.slice(0)
|
|
else:
|
|
rooms.assign(get_children().filter(func(c): return c is DungeonRoom3D))
|
|
_preplaced_rooms_cached = rooms.slice(0)
|
|
return rooms
|
|
|
|
func get_all_placed_and_preplaced_rooms() -> Array:
|
|
var rooms := get_preplaced_rooms()
|
|
rooms.append_array(_rooms_placed)
|
|
return rooms
|
|
|
|
# Any rooms with doors leading to more than 1 floor are considered stairs
|
|
func get_stair_rooms_from_instances() -> Array:
|
|
var rooms := []
|
|
rooms.assign(room_instances.filter(func(c):
|
|
if not c is DungeonRoom3D: return false
|
|
if not c.is_stair_room: return false
|
|
var floors_dict = {}
|
|
for door in c.get_doors_cached():
|
|
floors_dict[door.local_pos.y] = true
|
|
return floors_dict.keys().size() >= 2))
|
|
return rooms
|
|
|
|
################
|
|
## VALIDATION ##
|
|
################
|
|
|
|
# Returns true if no errors found before generating.
|
|
# Calls callbacks with warning/error string if any.
|
|
# Upon generate, also calls validation checks on each of the rooms.
|
|
func validate_dungeon(error_callback = null, warning_callback = null) -> bool:
|
|
# printerr and push_warning do not always work. Only print() seems to reliably output.
|
|
if not warning_callback is Callable: warning_callback = (func(str): _printwarning("SimpleDungeons Warning: ", str))
|
|
if not error_callback is Callable: error_callback = (func(str): _printerr("SimpleDungeons Error: ", str))
|
|
var any_errors : = {"err": false} # So lambda closure captures
|
|
error_callback = (func(str): any_errors["err"] = true; error_callback.call(str))
|
|
|
|
if not corridor_room_scene:
|
|
error_callback.call("No corridor room scene set. Add a 1x1x1 (in voxels) corridor room scene.")
|
|
if room_scenes.size() == 0:
|
|
error_callback.call("No rooms added. Add DungeonRoom scenes to the room_scenes property.")
|
|
|
|
if not is_currently_generating:
|
|
return not any_errors["err"]
|
|
|
|
# Build stage checks:
|
|
|
|
if not room_instances is Array or len(room_instances) == 0:
|
|
error_callback.call("No rooms added. Cannot generate dungeon.")
|
|
if room_instances is Array:
|
|
for room in room_instances:
|
|
if not room is DungeonRoom3D:
|
|
error_callback.call("Room "+room.name+" does not inherit DungeonRoom3D! Also add @tool to dungeon room script if generating in editor.")
|
|
else:
|
|
if not room.has_method("validate_room"):
|
|
error_callback.call("validate_room() method not found on room "+room.name+". Ensure it inherits DungeonRoom3D and has the @tool annotation if you're trying to generate in editor.")
|
|
room.validate_room(error_callback, warning_callback)
|
|
if not corridor_room_instance is DungeonRoom3D:
|
|
var corridor_name = corridor_room_instance.name if corridor_room_instance else "null"
|
|
error_callback.call("Corridor Room "+corridor_name+" does not inherit DungeonRoom3D!")
|
|
if corridor_room_instance is DungeonRoom3D and corridor_room_instance.size_in_voxels != Vector3i(1,1,1):
|
|
error_callback.call("Corridor Room must be 1x1x1 in voxels.")
|
|
if corridor_room_instance and not corridor_room_instance.has_method("get_doors"):
|
|
error_callback.call("get_doors() method not found on the corridor room. Ensure it inherits DungeonRoom3D and has the @tool annotation if you're trying to generate in editor.")
|
|
if corridor_room_instance is DungeonRoom3D and (corridor_room_instance.get_doors().size() != 4 or corridor_room_instance.get_doors().any(func(d): return not d.optional)):
|
|
error_callback.call("Corridor Room must have 4 optional doors")
|
|
if corridor_room_instance is DungeonRoom3D:
|
|
corridor_room_instance.validate_room(error_callback, warning_callback)
|
|
if corridor_room_instance.size_in_voxels != Vector3i(1,1,1):
|
|
error_callback.call("Corridor room scene must be 1x1x1 in voxels. It is used to connect the rooms with hallways.")
|
|
|
|
return not any_errors["err"]
|