diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn index 835789c..d93a5f1 100644 --- a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn +++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn @@ -22,7 +22,7 @@ size_flags_horizontal = 4 size_flags_vertical = 4 theme_override_constants/separation = 8 script = ExtResource("2_wft4x") -search_depth = 2 +search_depth = 4 [node name="InputMappingContainer" type="VBoxContainer" parent="VBoxContainer"] layout_mode = 2 diff --git a/addons/maaacks_game_template/base/scripts/capture_focus.gd b/addons/maaacks_game_template/base/scripts/capture_focus.gd index 575eb28..11daf6a 100644 --- a/addons/maaacks_game_template/base/scripts/capture_focus.gd +++ b/addons/maaacks_game_template/base/scripts/capture_focus.gd @@ -1,5 +1,5 @@ class_name CaptureFocus -extends Container +extends Control ## Node that captures UI focus for games with a hidden mouse or joypad enabled. ## ## This script assists with capturing UI focus when @@ -11,6 +11,7 @@ extends Container ## Hierarchical depth to search in the scene tree. @export var search_depth : int = 1 @export var enabled : bool = false +@export var null_focus_enabled : bool = true @export var joypad_enabled : bool = true @export var mouse_hidden_enabled : bool = true @@ -27,6 +28,8 @@ func _focus_first_search(control_node : Control, levels : int = 1): return false if control_node.focus_mode == FOCUS_ALL: control_node.grab_focus() + if control_node is ItemList: + control_node.select(0) return true if levels < 1: return false @@ -45,6 +48,7 @@ func update_focus(): func _should_capture_focus(): return enabled or \ + (get_viewport().gui_get_focus_owner() == null and null_focus_enabled) or \ (Input.get_connected_joypads().size() > 0 and joypad_enabled) or \ (Input.mouse_mode not in [Input.MOUSE_MODE_VISIBLE, Input.MOUSE_MODE_CONFINED] and mouse_hidden_enabled) diff --git a/addons/maaacks_game_template/examples/scenes/menus/level_select_menu/level_select_menu.gd b/addons/maaacks_game_template/examples/scenes/menus/level_select_menu/level_select_menu.gd new file mode 100644 index 0000000..7dc3293 --- /dev/null +++ b/addons/maaacks_game_template/examples/scenes/menus/level_select_menu/level_select_menu.gd @@ -0,0 +1,35 @@ +extends Control + +## Loads a simple ItemList node within a margin container. SceneLister updates +## the available scenes in the directory provided. Activating a level will update +## the GameState's current_level, and emit a signal. The main menu node will trigger +## a load action from that signal. + +@onready var level_buttons_container: ItemList = %LevelButtonsContainer +@onready var scene_lister: SceneLister = $SceneLister + +signal level_selected + +func _ready() -> void: + add_levels_to_container() + +## A fresh level list is propgated into the ItemList, and the file names are cleaned +func add_levels_to_container(): + level_buttons_container.clear() + var max_level_reached := GameStateExample.get_max_level_reached() + var level_iter := 0 + for file_path in scene_lister.files: + if level_iter > max_level_reached : break + level_iter += 1 + # Extract the file name from the path + var file_name = file_path.get_file() # e.g., "level_1.tscn" + # Clean up the file name + file_name = file_name.trim_suffix(".tscn") # Remove the ".tscn" extension + file_name = file_name.replace("_", " ") # Replace underscores with spaces + file_name = file_name.capitalize() # Convert to proper case + var button_name = str(file_name) + level_buttons_container.add_item(button_name) + +func _on_level_buttons_container_item_activated(index: int) -> void: + GameStateExample.set_current_level(index) + level_selected.emit() diff --git a/addons/maaacks_game_template/examples/scenes/menus/level_select_menu/level_select_menu.tscn b/addons/maaacks_game_template/examples/scenes/menus/level_select_menu/level_select_menu.tscn new file mode 100644 index 0000000..79fef08 --- /dev/null +++ b/addons/maaacks_game_template/examples/scenes/menus/level_select_menu/level_select_menu.tscn @@ -0,0 +1,49 @@ +[gd_scene load_steps=4 format=3 uid="uid://dtu0ffm1m5mr7"] + +[ext_resource type="Script" path="res://addons/maaacks_game_template/examples/scenes/menus/level_select_menu/level_select_menu.gd" id="1_dm7vg"] +[ext_resource type="Script" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_88qak"] +[ext_resource type="Script" path="res://addons/maaacks_game_template/extras/scripts/scene_lister.gd" id="2_g3cvb"] + +[node name="LevelSelectMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_dm7vg") + +[node name="Control" type="Control" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("2_88qak") + +[node name="LevelButtonsContainer" type="ItemList" parent="Control"] +unique_name_in_owner = true +custom_minimum_size = Vector2(400, 0) +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -200.0 +offset_top = -17.5 +offset_right = 200.0 +offset_bottom = 17.5 +grow_horizontal = 2 +grow_vertical = 2 +auto_height = true +item_count = 1 +item_0/text = "1 - ExampleLevel" + +[node name="SceneLister" type="Node" parent="."] +script = ExtResource("2_g3cvb") +files = Array[String](["res://addons/maaacks_game_template/examples/scenes/game_scene/levels/level_1.tscn", "res://addons/maaacks_game_template/examples/scenes/game_scene/levels/level_2.tscn", "res://addons/maaacks_game_template/examples/scenes/game_scene/levels/level_3.tscn"]) +directory = "res://addons/maaacks_game_template/examples/scenes/game_scene/levels" + +[connection signal="item_activated" from="Control/LevelButtonsContainer" to="." method="_on_level_buttons_container_item_activated"] diff --git a/addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.gd b/addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.gd index 307579a..0689b31 100644 --- a/addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.gd +++ b/addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.gd @@ -1,5 +1,8 @@ extends MainMenu +@export var level_select_packed_scene: PackedScene + +var level_select_scene var animation_state_machine : AnimationNodeStateMachinePlayback func load_game_scene(): @@ -33,6 +36,14 @@ func _close_sub_menu(): super._close_sub_menu() animation_state_machine.travel("OpenMainMenu") +func _setup_level_select(): + if level_select_packed_scene != null: + level_select_scene = level_select_packed_scene.instantiate() + level_select_scene.hide() + %LevelSelectContainer.call_deferred("add_child", level_select_scene) + if level_select_scene.has_signal("level_selected"): + level_select_scene.connect("level_selected", load_game_scene) + func _input(event): if _is_in_intro() and _event_skips_intro(event): intro_done() @@ -41,12 +52,18 @@ func _input(event): func _ready(): super._ready() + _setup_level_select() animation_state_machine = $MenuAnimationTree.get("parameters/playback") func _setup_game_buttons(): super._setup_game_buttons() if GameStateExample.has_game_state(): %ContinueGameButton.show() + if GameStateExample.get_max_level_reached() > 0: + %LevelSelectButton.show() func _on_continue_game_button_pressed(): load_game_scene() + +func _on_level_select_button_pressed(): + _open_sub_menu(level_select_scene) diff --git a/addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.tscn b/addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.tscn index 8434567..3e8da46 100644 --- a/addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.tscn +++ b/addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.tscn @@ -1,9 +1,10 @@ -[gd_scene load_steps=18 format=3 uid="uid://c63y6b25bs4bk"] +[gd_scene load_steps=19 format=3 uid="uid://c63y6b25bs4bk"] [ext_resource type="PackedScene" uid="uid://c6k5nnpbypshi" path="res://addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn" id="1_0i2sc"] [ext_resource type="Script" path="res://addons/maaacks_game_template/examples/scenes/menus/main_menu/main_menu_with_animations.gd" id="2_ncvk7"] [ext_resource type="PackedScene" uid="uid://bdvdf5v87mmrr" path="res://addons/maaacks_game_template/examples/scenes/menus/options_menu/master_options_menu_with_tabs.tscn" id="3_tuy3s"] [ext_resource type="PackedScene" uid="uid://c1g50h2avck3w" path="res://addons/maaacks_game_template/examples/scenes/credits/credits.tscn" id="4_lwsll"] +[ext_resource type="PackedScene" uid="uid://dtu0ffm1m5mr7" path="res://addons/maaacks_game_template/examples/scenes/menus/level_select_menu/level_select_menu.tscn" id="5_4pbct"] [sub_resource type="Animation" id="1"] resource_name = "Intro" @@ -351,6 +352,7 @@ graph_offset = Vector2(-180.277, 49) [node name="MainMenu" instance=ExtResource("1_0i2sc")] script = ExtResource("2_ncvk7") +level_select_packed_scene = ExtResource("5_4pbct") game_scene_path = "res://addons/maaacks_game_template/examples/scenes/game_scene/game_ui.tscn" options_packed_scene = ExtResource("3_tuy3s") credits_packed_scene = ExtResource("4_lwsll") @@ -371,6 +373,9 @@ modulate = Color(1, 1, 1, 0) [node name="TitleContainer" parent="MenuContainer/TitleMargin" index="0"] modulate = Color(1, 1, 1, 0) +[node name="TitleLabel" parent="MenuContainer/TitleMargin/TitleContainer" index="0"] +text = "Game Template" + [node name="SubTitleContainer" parent="MenuContainer/SubTitleMargin" index="0"] modulate = Color(1, 1, 1, 0) @@ -387,4 +392,23 @@ custom_minimum_size = Vector2(128, 40) layout_mode = 2 text = "Continue" +[node name="LevelSelectButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer" index="2"] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(128, 40) +layout_mode = 2 +text = "Level Select" + +[node name="LevelSelectContainer" type="MarginContainer" parent="." index="9"] +unique_name_in_owner = true +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 32 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 32 + [connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/ContinueGameButton" to="." method="_on_continue_game_button_pressed"] +[connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/LevelSelectButton" to="." method="_on_level_select_button_pressed"] diff --git a/addons/maaacks_game_template/examples/scripts/game_state.gd b/addons/maaacks_game_template/examples/scripts/game_state.gd index 8fb19bc..1f7c540 100644 --- a/addons/maaacks_game_template/examples/scripts/game_state.gd +++ b/addons/maaacks_game_template/examples/scripts/game_state.gd @@ -31,6 +31,12 @@ static func get_current_level() -> int: return 0 return game_state.current_level +static func get_max_level_reached() -> int: + var game_state = get_game_state() + if not game_state: + return 0 + return game_state.max_level_reached + static func level_reached(level_number): var game_state = get_game_state() if not game_state: diff --git a/addons/maaacks_game_template/examples/scripts/level_list_state_manager.gd b/addons/maaacks_game_template/examples/scripts/level_list_state_manager.gd index 9a45ae1..001d2ac 100644 --- a/addons/maaacks_game_template/examples/scripts/level_list_state_manager.gd +++ b/addons/maaacks_game_template/examples/scripts/level_list_state_manager.gd @@ -5,7 +5,8 @@ func set_current_level_id(value): GameStateExample.level_reached(value) func get_current_level_id() -> int: - return GameStateExample.get_current_level() if force_level == -1 else force_level + current_level_id = GameStateExample.get_current_level() if force_level == -1 else force_level + return current_level_id func _advance_level(): super._advance_level() diff --git a/scenes/menus/level_select_menu/level_select_menu.gd b/scenes/menus/level_select_menu/level_select_menu.gd new file mode 100644 index 0000000..8274453 --- /dev/null +++ b/scenes/menus/level_select_menu/level_select_menu.gd @@ -0,0 +1,35 @@ +extends Control + +## Loads a simple ItemList node within a margin container. SceneLister updates +## the available scenes in the directory provided. Activating a level will update +## the GameState's current_level, and emit a signal. The main menu node will trigger +## a load action from that signal. + +@onready var level_buttons_container: ItemList = %LevelButtonsContainer +@onready var scene_lister: SceneLister = $SceneLister + +signal level_selected + +func _ready() -> void: + add_levels_to_container() + +## A fresh level list is propgated into the ItemList, and the file names are cleaned +func add_levels_to_container(): + level_buttons_container.clear() + var max_level_reached := GameState.get_max_level_reached() + var level_iter := 0 + for file_path in scene_lister.files: + if level_iter > max_level_reached : break + level_iter += 1 + # Extract the file name from the path + var file_name = file_path.get_file() # e.g., "level_1.tscn" + # Clean up the file name + file_name = file_name.trim_suffix(".tscn") # Remove the ".tscn" extension + file_name = file_name.replace("_", " ") # Replace underscores with spaces + file_name = file_name.capitalize() # Convert to proper case + var button_name = str(file_name) + level_buttons_container.add_item(button_name) + +func _on_level_buttons_container_item_activated(index: int) -> void: + GameState.set_current_level(index) + level_selected.emit() diff --git a/scenes/menus/level_select_menu/level_select_menu.tscn b/scenes/menus/level_select_menu/level_select_menu.tscn new file mode 100644 index 0000000..e130a54 --- /dev/null +++ b/scenes/menus/level_select_menu/level_select_menu.tscn @@ -0,0 +1,49 @@ +[gd_scene load_steps=4 format=3 uid="uid://crgwq4t7ljint"] + +[ext_resource type="Script" path="res://scenes/menus/level_select_menu/level_select_menu.gd" id="1_h6hut"] +[ext_resource type="Script" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_c723y"] +[ext_resource type="Script" path="res://addons/maaacks_game_template/extras/scripts/scene_lister.gd" id="3_odcm1"] + +[node name="LevelSelectMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_h6hut") + +[node name="Control" type="Control" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("2_c723y") + +[node name="LevelButtonsContainer" type="ItemList" parent="Control"] +unique_name_in_owner = true +custom_minimum_size = Vector2(400, 0) +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -200.0 +offset_top = -17.5 +offset_right = 200.0 +offset_bottom = 17.5 +grow_horizontal = 2 +grow_vertical = 2 +auto_height = true +item_count = 1 +item_0/text = "1 - ExampleLevel" + +[node name="SceneLister" type="Node" parent="."] +script = ExtResource("3_odcm1") +files = Array[String](["res://scenes/game_scene/levels/level_1.tscn", "res://scenes/game_scene/levels/level_2.tscn", "res://scenes/game_scene/levels/level_3.tscn"]) +directory = "res://scenes/game_scene/levels" + +[connection signal="item_activated" from="Control/LevelButtonsContainer" to="." method="_on_level_buttons_container_item_activated"] diff --git a/scenes/menus/main_menu/main_menu_with_animations.gd b/scenes/menus/main_menu/main_menu_with_animations.gd index 9aafd8f..d86adb1 100644 --- a/scenes/menus/main_menu/main_menu_with_animations.gd +++ b/scenes/menus/main_menu/main_menu_with_animations.gd @@ -1,5 +1,8 @@ extends MainMenu +@export var level_select_packed_scene: PackedScene + +var level_select_scene var animation_state_machine : AnimationNodeStateMachinePlayback func load_game_scene(): @@ -33,6 +36,14 @@ func _close_sub_menu(): super._close_sub_menu() animation_state_machine.travel("OpenMainMenu") +func _setup_level_select(): + if level_select_packed_scene != null: + level_select_scene = level_select_packed_scene.instantiate() + level_select_scene.hide() + %LevelSelectContainer.call_deferred("add_child", level_select_scene) + if level_select_scene.has_signal("level_selected"): + level_select_scene.connect("level_selected", load_game_scene) + func _input(event): if _is_in_intro() and _event_skips_intro(event): intro_done() @@ -41,12 +52,18 @@ func _input(event): func _ready(): super._ready() + _setup_level_select() animation_state_machine = $MenuAnimationTree.get("parameters/playback") func _setup_game_buttons(): super._setup_game_buttons() if GameState.has_game_state(): %ContinueGameButton.show() + if GameState.get_max_level_reached() > 0: + %LevelSelectButton.show() func _on_continue_game_button_pressed(): load_game_scene() + +func _on_level_select_button_pressed(): + _open_sub_menu(level_select_scene) diff --git a/scenes/menus/main_menu/main_menu_with_animations.tscn b/scenes/menus/main_menu/main_menu_with_animations.tscn index 43b37c4..c6439ba 100644 --- a/scenes/menus/main_menu/main_menu_with_animations.tscn +++ b/scenes/menus/main_menu/main_menu_with_animations.tscn @@ -1,9 +1,10 @@ -[gd_scene load_steps=18 format=3 uid="uid://8ymyrorpy02h"] +[gd_scene load_steps=19 format=3 uid="uid://cctjxrnxut1t7"] -[ext_resource type="PackedScene" path="res://addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn" id="1_ol417"] -[ext_resource type="Script" path="res://scenes/menus/main_menu/main_menu_with_animations.gd" id="2_0jb1p"] -[ext_resource type="PackedScene" path="res://scenes/menus/options_menu/master_options_menu_with_tabs.tscn" id="3_v43e6"] -[ext_resource type="PackedScene" path="res://scenes/credits/credits.tscn" id="4_58732"] +[ext_resource type="PackedScene" path="res://addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn" id="1_6btwc"] +[ext_resource type="Script" path="res://scenes/menus/main_menu/main_menu_with_animations.gd" id="2_qyyu4"] +[ext_resource type="PackedScene" path="res://scenes/menus/level_select_menu/level_select_menu.tscn" id="3_lmrqr"] +[ext_resource type="PackedScene" path="res://scenes/menus/options_menu/master_options_menu_with_tabs.tscn" id="4_p1xti"] +[ext_resource type="PackedScene" path="res://scenes/credits/credits.tscn" id="5_svaqx"] [sub_resource type="Animation" id="1"] resource_name = "Intro" @@ -349,11 +350,12 @@ states/Start/position = Vector2(82, 123) transitions = ["Intro", "OpenMainMenu", SubResource("11"), "OpenMainMenu", "OpenSubMenu", SubResource("14"), "Start", "Intro", SubResource("AnimationNodeStateMachineTransition_j0orr"), "OpenSubMenu", "OpenMainMenu", SubResource("AnimationNodeStateMachineTransition_63dxc")] graph_offset = Vector2(-180.277, 49) -[node name="MainMenu" instance=ExtResource("1_ol417")] -script = ExtResource("2_0jb1p") +[node name="MainMenu" instance=ExtResource("1_6btwc")] +script = ExtResource("2_qyyu4") +level_select_packed_scene = ExtResource("3_lmrqr") game_scene_path = "res://scenes/game_scene/game_ui.tscn" -options_packed_scene = ExtResource("3_v43e6") -credits_packed_scene = ExtResource("4_58732") +options_packed_scene = ExtResource("4_p1xti") +credits_packed_scene = ExtResource("5_svaqx") [node name="MenuAnimationPlayer" type="AnimationPlayer" parent="." index="1"] libraries = { @@ -387,4 +389,23 @@ custom_minimum_size = Vector2(128, 40) layout_mode = 2 text = "Continue" +[node name="LevelSelectButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer" index="2"] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(128, 40) +layout_mode = 2 +text = "Level Select" + +[node name="LevelSelectContainer" type="MarginContainer" parent="." index="9"] +unique_name_in_owner = true +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 32 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 32 + [connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/ContinueGameButton" to="." method="_on_continue_game_button_pressed"] +[connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/LevelSelectButton" to="." method="_on_level_select_button_pressed"] diff --git a/scripts/game_state.gd b/scripts/game_state.gd index 608642f..dd0597f 100644 --- a/scripts/game_state.gd +++ b/scripts/game_state.gd @@ -31,6 +31,12 @@ static func get_current_level() -> int: return 0 return game_state.current_level +static func get_max_level_reached() -> int: + var game_state = get_game_state() + if not game_state: + return 0 + return game_state.max_level_reached + static func level_reached(level_number): var game_state = get_game_state() if not game_state: diff --git a/scripts/level_list_state_manager.gd b/scripts/level_list_state_manager.gd index b348ae7..e6238b8 100644 --- a/scripts/level_list_state_manager.gd +++ b/scripts/level_list_state_manager.gd @@ -5,7 +5,8 @@ func set_current_level_id(value): GameState.level_reached(value) func get_current_level_id() -> int: - return GameState.get_current_level() if force_level == -1 else force_level + current_level_id = GameState.get_current_level() if force_level == -1 else force_level + return current_level_id func _advance_level(): super._advance_level()