Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 128 additions & 27 deletions systems/map/map_runtime.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const DOOR_WIDTH := 42.0
const DOOR_HEIGHT := 132.0
const DOOR_GLOW_COLOR := Color(0.66, 1.0, 0.84, 0.92)
const DOOR_FRAME_COLOR := Color(0.18, 0.22, 0.28, 0.98)
const FUNCTION_ROOM_INDICES := [4, 5, 6]
const FUNCTION_CHOICE_SOURCE_ROOM_INDEX := 3
const FUNCTION_CHOICE_Y_RATIOS := [0.28, 0.50, 0.72]

var world_root: Node2D = null
var spawn_marker: Marker2D = null
Expand All @@ -19,6 +22,7 @@ var map_camera: Camera2D = null
var map_cover_root: Node2D = null
var map_room_rects: Array[Rect2] = []
var map_walkable_rects: Array[Rect2] = []
var selected_function_room_index: int = -1


func setup(p_world_root: Node2D, p_spawn_marker: Marker2D, p_encounter_marker: Marker2D, p_rng: RandomNumberGenerator) -> void:
Expand Down Expand Up @@ -106,28 +110,86 @@ func _build_runtime_map_rooms() -> void:
map_room_rects.clear()
map_walkable_rects.clear()
var x_cursor := 0.0
var function_room_x := -1.0
for index in range(MapBrowserDemo.ROOM_PATHS.size()):
var texture := RuntimeTextureLoader.load_texture(String(MapBrowserDemo.ROOM_PATHS[index]))
if texture == null:
push_warning("Missing map texture: %s" % MapBrowserDemo.ROOM_PATHS[index])
continue
var sprite := Sprite2D.new()
sprite.name = "MapRoom%02d" % [index + 1]
sprite.texture = texture
sprite.centered = false
sprite.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
sprite.position = Vector2(x_cursor, 0.0)
map_root.add_child(sprite)
var room_rect := Rect2(sprite.position, Vector2(float(texture.get_width()), float(texture.get_height())))
if index == FUNCTION_ROOM_INDICES[0]:
function_room_x = x_cursor
var room_x := function_room_x if index in FUNCTION_ROOM_INDICES else x_cursor
var room_rect := Rect2(Vector2(room_x, 0.0), Vector2(float(texture.get_width()), float(texture.get_height())))
var walk_rect := _map_walkable_rect(index, room_rect)
map_room_rects.append(room_rect)
map_walkable_rects.append(walk_rect)
_add_runtime_room_walls(index, room_rect)
_add_runtime_room_doors(index, room_rect, walk_rect)
x_cursor += room_rect.size.x
if index not in FUNCTION_ROOM_INDICES:
_add_runtime_room_visual(index, texture, room_rect)
_add_runtime_room_walls(index, room_rect)
_add_runtime_room_doors(index, room_rect, walk_rect)
if index == FUNCTION_ROOM_INDICES[FUNCTION_ROOM_INDICES.size() - 1]:
x_cursor = function_room_x + room_rect.size.x
elif index not in FUNCTION_ROOM_INDICES:
x_cursor += room_rect.size.x
_add_runtime_room_props()


func _add_runtime_room_visual(index: int, texture: Texture2D, room_rect: Rect2) -> void:
var sprite := Sprite2D.new()
sprite.name = "MapRoom%02d" % [index + 1]
sprite.texture = texture
sprite.centered = false
sprite.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
sprite.position = room_rect.position
sprite.set_meta("room_index", index)
map_root.add_child(sprite)


func select_function_room(room_index: int) -> bool:
if selected_function_room_index >= 0 or room_index not in FUNCTION_ROOM_INDICES:
return false
var texture := RuntimeTextureLoader.load_texture(String(MapBrowserDemo.ROOM_PATHS[room_index]))
if texture == null:
return false
selected_function_room_index = room_index
_add_runtime_room_visual(room_index, texture, map_room_rects[room_index])
_add_runtime_room_walls(room_index, map_room_rects[room_index])
_add_runtime_room_doors(room_index, map_room_rects[room_index], map_walkable_rects[room_index])
_add_random_props_for_room(room_index)
return true


func reset_function_room_selection() -> void:
selected_function_room_index = -1
if map_root != null:
for child in map_root.get_children():
var child_name := String(child.name)
if child_name.begins_with("MapRoom05") or child_name.begins_with("MapRoom06") or child_name.begins_with("MapRoom07") or child_name.begins_with("Room05") or child_name.begins_with("Room06") or child_name.begins_with("Room07"):
child.queue_free()
if map_cover_root != null:
for child in map_cover_root.get_children():
if int(child.get_meta("room_index", -1)) in FUNCTION_ROOM_INDICES:
child.queue_free()


func function_room_choice_for_position(world_position: Vector2) -> int:
if selected_function_room_index >= 0 or map_room_rects.size() <= FUNCTION_CHOICE_SOURCE_ROOM_INDEX:
return selected_function_room_index
var source_rect := map_room_rects[FUNCTION_CHOICE_SOURCE_ROOM_INDEX]
if world_position.x < source_rect.end.x - 46.0:
return -1
var walk_rect := map_walkable_rects[FUNCTION_CHOICE_SOURCE_ROOM_INDEX]
var best_index := 0
var best_distance := INF
for choice_index in range(FUNCTION_CHOICE_Y_RATIOS.size()):
var choice_y := walk_rect.position.y + walk_rect.size.y * float(FUNCTION_CHOICE_Y_RATIOS[choice_index])
var distance := absf(world_position.y - choice_y)
if distance < best_distance:
best_distance = distance
best_index = choice_index
return int(FUNCTION_ROOM_INDICES[best_index])


func _map_walkable_rect(index: int, room_rect: Rect2) -> Rect2:
var ratio: Rect2 = MapBrowserDemo.WALKABLE_AREAS[min(index, MapBrowserDemo.WALKABLE_AREAS.size() - 1)]
return Rect2(
Expand All @@ -140,6 +202,9 @@ func _add_runtime_room_walls(index: int, room_rect: Rect2) -> void:
var wall_rects := MapBrowserDemo.get_room_wall_rects(index, room_rect)
for wall_index in range(wall_rects.size()):
_add_runtime_wall("Room%02dWall%02d" % [index + 1, wall_index + 1], wall_rects[wall_index])
var circle_collisions := MapBrowserDemo.get_room_circle_collisions(index, room_rect)
for circle_index in range(circle_collisions.size()):
_add_runtime_circle_wall("Room%02dCircle%02d" % [index + 1, circle_index + 1], circle_collisions[circle_index])


func _add_runtime_wall(wall_name: String, rect: Rect2) -> void:
Expand All @@ -158,6 +223,25 @@ func _add_runtime_wall(wall_name: String, rect: Rect2) -> void:
shape.shape = rectangle
body.add_child(shape)

func _add_runtime_circle_wall(wall_name: String, data: Dictionary) -> void:
if map_root == null:
return
var radius := float(data.get("radius", 0.0))
if radius <= 0.0:
return
var body := StaticBody2D.new()
body.name = wall_name
body.collision_layer = 1
body.collision_mask = 2
body.add_to_group("projectile_blocker")
body.position = data.get("center", Vector2.ZERO) as Vector2
map_root.add_child(body)
var shape := CollisionShape2D.new()
var circle := CircleShape2D.new()
circle.radius = radius
shape.shape = circle
body.add_child(shape)


func _add_runtime_room_doors(room_index: int, room_rect: Rect2, walk_rect: Rect2) -> void:
if map_root == null:
Expand All @@ -166,7 +250,11 @@ func _add_runtime_room_doors(room_index: int, room_rect: Rect2, walk_rect: Rect2
var exit_center := Vector2(room_rect.end.x - 10.0, walk_rect.get_center().y)
if room_index > 0:
_add_door_marker("Room%02dEntrance" % [room_index + 1], entrance_center, true)
if room_index < MapBrowserDemo.ROOM_PATHS.size() - 1:
if room_index == FUNCTION_CHOICE_SOURCE_ROOM_INDEX:
for choice_index in range(FUNCTION_CHOICE_Y_RATIOS.size()):
var choice_center := Vector2(exit_center.x, walk_rect.position.y + walk_rect.size.y * float(FUNCTION_CHOICE_Y_RATIOS[choice_index]))
_add_door_marker("FunctionChoice%02d" % [choice_index + 1], choice_center, false)
elif room_index < MapBrowserDemo.ROOM_PATHS.size() - 1:
_add_door_marker("Room%02dExit" % [room_index + 1], exit_center, false)


Expand Down Expand Up @@ -224,20 +312,26 @@ func _add_runtime_room_props() -> void:
map_cover_root.name = "RuntimeRoomProps"
map_cover_root.z_index = 6
map_root.add_child(map_cover_root)
var manifest := MapBrowserDemo.load_generated_prop_manifest()
for room_index in range(map_room_rects.size()):
var candidates := MapBrowserDemo.get_generated_prop_candidates(manifest, room_index)
if candidates.is_empty():
if room_index in FUNCTION_ROOM_INDICES:
continue
candidates.shuffle()
var count: int = min(reward_rng.randi_range(MapBrowserDemo.RANDOM_PROP_MIN_PER_ROOM, MapBrowserDemo.RANDOM_PROP_MAX_PER_ROOM), candidates.size())
var placed_rects: Array[Rect2] = []
var placed := 0
for candidate in candidates:
if placed >= count:
break
if _try_add_generated_room_prop(room_index, candidate, placed_rects):
placed += 1
_add_random_props_for_room(room_index)


func _add_random_props_for_room(room_index: int) -> void:
var manifest := MapBrowserDemo.load_generated_prop_manifest()
var candidates := MapBrowserDemo.get_generated_prop_candidates(manifest, room_index)
if candidates.is_empty():
return
candidates.shuffle()
var count: int = min(reward_rng.randi_range(MapBrowserDemo.RANDOM_PROP_MIN_PER_ROOM, MapBrowserDemo.RANDOM_PROP_MAX_PER_ROOM), candidates.size())
var placed_rects: Array[Rect2] = []
var placed := 0
for candidate in candidates:
if placed >= count:
break
if _try_add_generated_room_prop(room_index, candidate, placed_rects):
placed += 1


func _room_prop_candidates(room_index: int) -> Array[Dictionary]:
Expand All @@ -263,7 +357,7 @@ func _try_add_generated_room_prop(room_index: int, candidate: Dictionary, placed
var source_width := maxf(1.0, float(source_size[0]))
var source_height := maxf(1.0, float(source_size[1]))
var texture_to_room_scale := Vector2(room_rect.size.x / source_width, room_rect.size.y / source_height)
texture_to_room_scale *= MapBrowserDemo.RANDOM_PROP_WORLD_SCALE
texture_to_room_scale *= MapBrowserDemo.RANDOM_PROP_WORLD_SCALE * MapBrowserDemo.generated_prop_scale_multiplier(candidate)
var prop_size := Vector2(float(texture.get_width()) * texture_to_room_scale.x, float(texture.get_height()) * texture_to_room_scale.y)
if not MapBrowserDemo.is_generated_prop_size_usable(prop_size, room_rect.size):
return false
Expand All @@ -284,10 +378,17 @@ func _add_generated_room_prop(room_index: int, candidate: Dictionary, texture: T
body.name = "Room%02d%sProp" % [room_index + 1, String(candidate.get("name", "Generated")).replace(" ", "")]
body.collision_layer = 1
body.collision_mask = 2
body.add_to_group("projectile_blocker")
var walkable_prop := MapBrowserDemo.generated_prop_is_walkable(candidate)
if walkable_prop:
body.collision_layer = 0
body.collision_mask = 0
else:
body.add_to_group("projectile_blocker")
body.position = world_position
body.set_meta("room_index", room_index)
body.set_meta("prop_key", MapBrowserDemo.generated_prop_key(candidate))
body.set_meta("prop_size", Vector2(float(texture.get_width()) * texture_to_room_scale.x, float(texture.get_height()) * texture_to_room_scale.y))
body.set_meta("walkable_prop", walkable_prop)
map_cover_root.add_child(body)

var sprite := Sprite2D.new()
Expand All @@ -298,7 +399,7 @@ func _add_generated_room_prop(room_index: int, candidate: Dictionary, texture: T
sprite.centered = true
body.add_child(sprite)

var polygons := MapBrowserDemo.build_alpha_collision_polygons(texture, texture_to_room_scale)
var polygons := MapBrowserDemo.build_generated_prop_collision_polygons(candidate, texture, texture_to_room_scale)
for index in range(polygons.size()):
var shape := CollisionPolygon2D.new()
shape.name = "AlphaCollision%02d" % [index + 1]
Expand Down
103 changes: 95 additions & 8 deletions tests/smoke_map_random_props.gd
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,25 @@ func _run() -> void:
quit(1)
return

var alpha_collision_count := 0
var collidable_prop_count := 0
var room_rects: Array = map_runtime.get("map_room_rects")
var walkable_rects: Array = map_runtime.get("map_walkable_rects")
var props_by_room := {}
for body in prop_bodies:
if body is StaticBody2D and not (body as StaticBody2D).is_in_group("projectile_blocker"):
push_error("Random prop is not a projectile blocker: %s" % body.name)
quit(1)
return
alpha_collision_count += body.find_children("AlphaCollision*", "CollisionPolygon2D", true, false).size()
var static_body := body as StaticBody2D
var walkable_prop := bool(static_body.get_meta("walkable_prop", false))
var collision_count := body.find_children("AlphaCollision*", "CollisionPolygon2D", true, false).size()
if walkable_prop:
if static_body.is_in_group("projectile_blocker") or collision_count != 0:
push_error("Walkable ground prop unexpectedly blocks movement: %s" % body.name)
quit(1)
return
else:
if not static_body.is_in_group("projectile_blocker") or collision_count == 0:
push_error("Random cover prop is missing collision: %s" % body.name)
quit(1)
return
collidable_prop_count += 1
var sprite := body.get_node_or_null("Sprite") as Sprite2D
if sprite == null or sprite.texture == null:
push_error("Random prop is missing sprite texture: %s" % body.name)
Expand All @@ -71,8 +80,79 @@ func _run() -> void:
quit(1)
return

if alpha_collision_count < prop_bodies.size():
push_error("Random props did not create alpha collision polygons")
if collidable_prop_count == 0:
push_error("Runtime map did not create any collidable cover props")
quit(1)
return

var manifest := MapBrowserDemo.load_generated_prop_manifest()
var room_four_candidates := MapBrowserDemo.get_generated_prop_candidates(manifest, 3)
var hut_candidate := _find_candidate(room_four_candidates, "Room04Prop01")
var ground_seal_candidate := _find_candidate(room_four_candidates, "Room04Prop35")
if hut_candidate.is_empty() or ground_seal_candidate.is_empty():
push_error("Room 4 prop manifest is missing hut or ground seal assets")
quit(1)
return
if not is_equal_approx(MapBrowserDemo.generated_prop_scale_multiplier(hut_candidate), 1.20):
push_error("Room 4 hut scale multiplier is incorrect")
quit(1)
return
for prop_key in ["0:Room01Prop01", "0:Room01Prop03", "0:Room01Prop07", "1:Room02Prop13", "1:Room02Prop15"]:
if not is_equal_approx(float(MapBrowserDemo.GENERATED_PROP_SCALE_MULTIPLIERS.get(prop_key, 1.0)), 0.50):
push_error("Oversized prop was not reduced by 50%%: %s" % prop_key)
quit(1)
return
var hut_texture := load(String(hut_candidate.get("path", ""))) as Texture2D
if hut_texture == null or MapBrowserDemo.build_generated_prop_collision_polygons(hut_candidate, hut_texture, Vector2.ONE).size() != 1:
push_error("Room 4 hut did not create its reduced footprint collision")
quit(1)
return
var ground_seal_texture := load(String(ground_seal_candidate.get("path", ""))) as Texture2D
if not MapBrowserDemo.generated_prop_is_walkable(ground_seal_candidate) or not MapBrowserDemo.build_generated_prop_collision_polygons(ground_seal_candidate, ground_seal_texture, Vector2.ONE).is_empty():
push_error("Room 4 ground seal should be fully walkable")
quit(1)
return

var expected_bottom_ratios := [0.75, 0.76, 0.76, 0.76]
for room_index in range(expected_bottom_ratios.size()):
var room_rect := room_rects[room_index] as Rect2
var expected_top := room_rect.position.y + room_rect.size.y * float(expected_bottom_ratios[room_index])
var found_bottom_blocker := false
for wall_rect in MapBrowserDemo.get_room_wall_rects(room_index, room_rect):
if wall_rect.position.y <= expected_top + 0.5 and wall_rect.end.y >= room_rect.end.y - 0.5 and wall_rect.size.x >= room_rect.size.x * 0.70:
found_bottom_blocker = true
break
if not found_bottom_blocker:
push_error("Room %d bottom buildings are missing roof collision" % [room_index + 1])
quit(1)
return

var plaza_statue := world_root.find_child("Room04Circle01", true, false) as StaticBody2D
if plaza_statue == null or not plaza_statue.is_in_group("projectile_blocker"):
push_error("Central plaza statue collision is missing")
quit(1)
return
var statue_shape := plaza_statue.get_child(0) as CollisionShape2D if plaza_statue.get_child_count() > 0 else null
if statue_shape == null or not (statue_shape.shape is CircleShape2D):
push_error("Central plaza statue does not use a circular collision")
quit(1)
return

var function_rect_a := room_rects[4] as Rect2
var function_rect_b := room_rects[5] as Rect2
var function_rect_c := room_rects[6] as Rect2
if function_rect_a.position != function_rect_b.position or function_rect_b.position != function_rect_c.position or function_rect_a.size.distance_to(function_rect_b.size) > 2.0 or function_rect_b.size.distance_to(function_rect_c.size) > 2.0:
push_error("The three function rooms do not share one branch slot")
quit(1)
return
var branch_source := room_rects[3] as Rect2
var chosen_room := map_runtime.function_room_choice_for_position(Vector2(branch_source.end.x, (walkable_rects[3] as Rect2).get_center().y))
if chosen_room != 5 or not map_runtime.select_function_room(chosen_room):
push_error("Middle function-room door did not select the armory")
quit(1)
return
if world_root.find_child("MapRoom06", true, false) == null or world_root.find_child("MapRoom05", true, false) != null or world_root.find_child("MapRoom07", true, false) != null:
push_error("Function-room branch loaded more than the chosen room")
quit(1)
return

Expand All @@ -84,3 +164,10 @@ func _run() -> void:
return

quit(0)


func _find_candidate(candidates: Array[Dictionary], candidate_name: String) -> Dictionary:
for candidate in candidates:
if String(candidate.get("name", "")) == candidate_name:
return candidate
return {}
12 changes: 12 additions & 0 deletions tests/smoke_run_flow.gd
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,18 @@ func _run() -> void:
push_error("Second map encounter did not begin after first map reward")
quit(1)
return
if world._next_encounter_index(4) != 7 or world._next_encounter_index(5) != 7 or world._next_encounter_index(6) != 7:
push_error("Function-room route did not converge on the inner gate")
quit(1)
return
var visible_recovery_position: Vector2 = (world.player_character as Node2D).global_position + Vector2(8.0, 0.0)
world.gate_walk_target = visible_recovery_position
world.player_character.visible = false
world._finish_gate_walk()
if not world.player_character.visible or world.player_character.global_position != visible_recovery_position:
push_error("Door transition did not recover the player position and visibility")
quit(1)
return

# The final chamber is an extra encounter slot backed by FINAL_BOSS_SCENES.
world.encounter_index = world._encounter_count() - 1
Expand Down
Loading
Loading