gd: added menu template

This commit is contained in:
2025-06-10 18:46:20 +02:00
parent f9a6c42b14
commit c554e24b01
421 changed files with 12371 additions and 2 deletions

View File

@ -0,0 +1,34 @@
class_name GameWonMenu
extends OverlaidMenu
signal continue_pressed
signal main_menu_pressed
func _handle_cancel_input():
if $ConfirmExit.visible:
$ConfirmExit.hide()
elif $ConfirmMainMenu.visible:
$ConfirmMainMenu.hide()
else:
super._handle_cancel_input()
func _ready():
if OS.has_feature("web"):
%ExitButton.hide()
func _on_exit_button_pressed():
$ConfirmExit.popup_centered()
func _on_main_menu_button_pressed():
$ConfirmMainMenu.popup_centered()
func _on_confirm_main_menu_confirmed():
main_menu_pressed.emit()
close()
func _on_confirm_exit_confirmed():
get_tree().quit()
func _on_close_button_pressed():
continue_pressed.emit()
close()

View File

@ -0,0 +1 @@
uid://gi8tms6wsj1n

View File

@ -0,0 +1,61 @@
[gd_scene load_steps=3 format=3 uid="uid://4brssbq1ghsw"]
[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_87sd7"]
[ext_resource type="Script" uid="uid://gi8tms6wsj1n" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.gd" id="2_hi7oy"]
[node name="GameWonMenu" instance=ExtResource("1_87sd7")]
process_mode = 3
script = ExtResource("2_hi7oy")
pauses_game = true
[node name="MenuPanelContainer" parent="." index="1"]
custom_minimum_size = Vector2(432, 240)
[node name="TitleMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="0"]
visible = false
[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
visible = true
theme_override_constants/margin_top = 64
theme_override_constants/margin_bottom = 64
[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
text = "[center]You won![/center]"
scroll_active = false
[node name="MenuButtons" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin" index="0"]
custom_minimum_size = Vector2(400, 0)
vertical = false
[node name="ExitButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 3
text = "Exit"
[node name="MainMenuButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 3
text = "Main Menu"
[node name="CloseButton" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="2"]
size_flags_horizontal = 3
text = "Continue"
[node name="ConfirmMainMenu" type="ConfirmationDialog" parent="." index="2"]
auto_translate_mode = 1
initial_position = 2
dialog_text = "Go back to main menu?"
[node name="ConfirmExit" type="ConfirmationDialog" parent="." index="3"]
auto_translate_mode = 1
initial_position = 2
dialog_text = "Quit the game?"
[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/ExitButton" to="." method="_on_exit_button_pressed"]
[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/MainMenuButton" to="." method="_on_main_menu_button_pressed"]
[connection signal="confirmed" from="ConfirmMainMenu" to="." method="_on_confirm_main_menu_confirmed"]
[connection signal="confirmed" from="ConfirmExit" to="." method="_on_confirm_exit_confirmed"]

View File

@ -0,0 +1,34 @@
class_name LevelLostMenu
extends OverlaidMenu
signal restart_pressed
signal main_menu_pressed
func _handle_cancel_input():
if $ConfirmExit.visible:
$ConfirmExit.hide()
elif $ConfirmMainMenu.visible:
$ConfirmMainMenu.hide()
else:
super._handle_cancel_input()
func _ready():
if OS.has_feature("web"):
%ExitButton.hide()
func _on_exit_button_pressed():
$ConfirmExit.popup_centered()
func _on_main_menu_button_pressed():
$ConfirmMainMenu.popup_centered()
func _on_confirm_main_menu_confirmed():
main_menu_pressed.emit()
close()
func _on_confirm_exit_confirmed():
get_tree().quit()
func _on_close_button_pressed():
restart_pressed.emit()
close()

View File

@ -0,0 +1 @@
uid://ckh3w3xa6qjk0

View File

@ -0,0 +1,59 @@
[gd_scene load_steps=3 format=3 uid="uid://dkq3nhkmhu4je"]
[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_ok347"]
[ext_resource type="Script" uid="uid://ckh3w3xa6qjk0" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.gd" id="2_6r1n8"]
[node name="LevelLostMenu" instance=ExtResource("1_ok347")]
process_mode = 3
script = ExtResource("2_6r1n8")
pauses_game = true
[node name="MenuPanelContainer" parent="." index="1"]
custom_minimum_size = Vector2(432, 240)
[node name="TitleMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="0"]
visible = false
[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
visible = true
theme_override_constants/margin_top = 64
theme_override_constants/margin_bottom = 64
[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
text = "[center]You lost...[/center]"
scroll_active = false
[node name="MenuButtons" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin" index="0"]
custom_minimum_size = Vector2(400, 0)
vertical = false
[node name="ExitButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Exit"
[node name="MainMenuButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Main Menu"
[node name="CloseButton" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="2"]
size_flags_horizontal = 3
text = "Restart"
[node name="ConfirmMainMenu" type="ConfirmationDialog" parent="." index="2"]
auto_translate_mode = 1
initial_position = 2
dialog_text = "Go back to main menu?"
[node name="ConfirmExit" type="ConfirmationDialog" parent="." index="3"]
auto_translate_mode = 1
initial_position = 2
dialog_text = "Quit the game?"
[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/ExitButton" to="." method="_on_exit_button_pressed"]
[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/MainMenuButton" to="." method="_on_main_menu_button_pressed"]
[connection signal="confirmed" from="ConfirmMainMenu" to="." method="_on_confirm_main_menu_confirmed"]
[connection signal="confirmed" from="ConfirmExit" to="." method="_on_confirm_exit_confirmed"]

View File

@ -0,0 +1,27 @@
class_name LevelWonMenu
extends OverlaidMenu
signal continue_pressed
signal restart_pressed
signal main_menu_pressed
func _input(event):
if event.is_action_pressed("ui_cancel"):
if $ConfirmMainMenu.visible:
$ConfirmMainMenu.hide()
get_viewport().set_input_as_handled()
func _on_main_menu_button_pressed():
$ConfirmMainMenu.popup_centered()
func _on_confirm_main_menu_confirmed():
main_menu_pressed.emit()
close()
func _on_restart_button_pressed():
restart_pressed.emit()
close()
func _on_close_button_pressed():
continue_pressed.emit()
close()

View File

@ -0,0 +1 @@
uid://bdic7jebf0y7a

View File

@ -0,0 +1,53 @@
[gd_scene load_steps=3 format=3 uid="uid://y3vtx0e0shv4"]
[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_nknag"]
[ext_resource type="Script" uid="uid://bdic7jebf0y7a" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.gd" id="2_klq7f"]
[node name="LevelWonMenu" instance=ExtResource("1_nknag")]
process_mode = 3
script = ExtResource("2_klq7f")
pauses_game = true
[node name="MenuPanelContainer" parent="." index="1"]
custom_minimum_size = Vector2(432, 240)
[node name="TitleMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="0"]
visible = false
[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
visible = true
theme_override_constants/margin_top = 64
theme_override_constants/margin_bottom = 64
[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
text = "[center]Level complete![/center]"
scroll_active = false
[node name="MenuButtons" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin" index="0"]
custom_minimum_size = Vector2(400, 0)
vertical = false
[node name="MainMenuButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Main Menu"
[node name="RestartButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Restart"
[node name="CloseButton" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="2"]
size_flags_horizontal = 3
text = "Continue"
[node name="ConfirmMainMenu" type="ConfirmationDialog" parent="." index="2"]
auto_translate_mode = 1
initial_position = 2
dialog_text = "Go back to main menu?"
[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/MainMenuButton" to="." method="_on_main_menu_button_pressed"]
[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/RestartButton" to="." method="_on_restart_button_pressed"]
[connection signal="confirmed" from="ConfirmMainMenu" to="." method="_on_confirm_main_menu_confirmed"]

View File

@ -0,0 +1,70 @@
#!/bin/bash
# asset checker command
# Used for quickly checking that assets (like audio files) are being used where expected.
#
# Recursively searches through scene files (.tscn, .scn, .res)
# for occurrences of asset types (default: AudioStream).
# It then outputs the paths of assets discovered,
# along with the file names that use them.
short_flag=false
asset_type="AudioStream"
print_usage() {
printf "Usage: -sa %s\n" "$asset_type"
}
while getopts 'a:s' flag; do
case "${flag}" in
a)
asset_type="${OPTARG}"
;;
s)
short_flag=true
;;
*)
print_usage
exit 1
;;
esac
done
# Initialize an associative array to store paths and corresponding files
declare -A path_files
while IFS=: read -r file line; do
path=$(echo "$line" | grep -o 'path="[^"]*' | cut -d'"' -f2)
if [ -n "$path" ]; then
# Append the current file to the string of files for this path
# Note: Bash does not support having arrays as values of associative array.
# Using a pipe `|` separator instead, and then splitting on output
if [ -z "${path_files["$path"]}" ]; then
path_files["$path"]=$file
else
path_files["$path"]+="|$file"
fi
fi
done < <(egrep -ir --include=*.{tscn,scn,res} "type=\"$asset_type\"")
# Get the paths and sort them
sorted_paths=()
for key in "${!path_files[@]}"; do
sorted_paths+=("$key")
done
IFS=$'\n' sorted_paths=($(sort <<< "${sorted_paths[*]}"))
unset IFS
# Print out the results
for path in "${sorted_paths[@]}"; do
# Note: Bash does not support having arrays as values of associative array.
# Splitting the concatenated files string on the pipe `|` separator.
IFS='|' read -r -a files_array <<< "${path_files[$path]}"
files_count=${#files_array[@]}
printf "%-80s | Uses: %s\n" "$path" "$files_count"
if ! $short_flag ; then
for file in "${files_array[@]}"; do
printf "\t%82s\n" "$file"
done
echo
fi
done

View File

@ -0,0 +1,44 @@
#!/bin/bash
# butler manager command
# Uploads directories as builds to matching itch.io channels.
# HTML5 => html5
# Linux => linux
# Windows => win
# MacOS => osx
file=upload_destination.txt
directories=("HTML5" "Linux" "Windows" "MacOS")
channels=("html5" "linux" "win" "osx")
# Check if the file exists
if [ ! -e $file ]; then
# File doesn't exist, create an empty one
touch $file
fi
# File exists, read the first line into a variable
read -r destination < $file
if [ -z "$destination" ]; then
# File is empty, prompt the user for input
echo "Please enter the build destination (username/project-url-after-slash)."
read -r user_input
# Save user input to the file
echo "$user_input" > "$file"
echo "Destination saved to $file."
destination="$user_input"
fi
# Check for the existence of directories and upload contents
for ((i=0; i<${#directories[@]}; i++)); do
dir="${directories[i]}"
channel="${channels[i]}"
if [ -d "$dir" ]; then
echo butler push ./$dir/ $destination:$channel
butler push ./$dir/ $destination:$channel
else
echo "Directory '$dir' does not exist."
fi
done

View File

@ -0,0 +1,9 @@
class_name CaptureMouse
extends Control
## Control node that captures the mouse for games that require it.
##
## Used for games that use the mouse to move the camera (ex. FPS or third-person shooters).
func _gui_input(event):
if event is InputEventMouseButton and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

View File

@ -0,0 +1 @@
uid://dqdyrkm3jily6

View File

@ -0,0 +1,48 @@
@tool
class_name LevelListLoader
extends SceneLister
## Extends [SceneLister] to manage level advancement through [GameStateExample].
signal level_load_started
signal level_loaded
signal levels_finished
## Container where the level instance will be added.
@export var level_container : Node
var current_level : Node
func get_level_file(level_id : int):
if files.is_empty():
push_error("levels list is empty")
return
if level_id >= files.size():
push_error("level_id is out of bounds of the levels list")
level_id = files.size() - 1
return files[level_id]
func _attach_level(level_resource : Resource):
assert(level_container != null, "level_container is null")
var instance = level_resource.instantiate()
level_container.call_deferred("add_child", instance)
return instance
func load_level(level_id : int):
if is_instance_valid(current_level):
current_level.queue_free()
await current_level.tree_exited
current_level = null
var level_file = get_level_file(level_id)
if level_file == null:
levels_finished.emit()
return
SceneLoader.load_scene(level_file, true)
level_load_started.emit()
await SceneLoader.scene_loaded
current_level = _attach_level(SceneLoader.get_resource())
level_loaded.emit()
func _ready():
if Engine.is_editor_hint():
# Text files get a `.remap` extension added on export.
_refresh_files()

View File

@ -0,0 +1 @@
uid://crbo2e4ndbyvk

View File

@ -0,0 +1,143 @@
class_name LevelListManager
extends Node
## Manager of level progress and the result screens between them.
##
## A helper script to assign to a node in a scene.
## It works with a level list loader and a loading screen
## to advance levels and open menus when players win or lose.
## Required reference to a level list loader in the scene.
@export var level_list_loader : LevelListLoader
## Required path to a main menu scene.
@export_file("*.tscn") var main_menu_scene : String
## Optional path to an ending scene.
@export_file("*.tscn") var ending_scene : String
@export var auto_load : bool = true
@export_group("Screens")
## Optional reference to a loading screen in the scene.
@export var level_loading_screen : LoadingScreen
## Optional win screen to be shown after the last level is won.
@export var game_won_scene : PackedScene
## Optional lose screen to be shown after the level is lost.
@export var level_lost_scene : PackedScene
## Optional level compete screen to be shown after the level is won.
@export var level_won_scene : PackedScene
## Loads a level on start.
@export_group("Debugging")
@export var force_level : int = -1
## Reference to the current level node.
var current_level
var current_level_id : int :
set = set_current_level_id
func set_current_level_id(value : int) -> void:
current_level_id = value
func _try_connecting_signal_to_node(node : Node, signal_name : String, callable : Callable) -> void:
if node.has_signal(signal_name) and not node.is_connected(signal_name, callable):
node.connect(signal_name, callable)
func _try_connecting_signal_to_level(signal_name : String, callable : Callable) -> void:
_try_connecting_signal_to_node(current_level, signal_name, callable)
func _load_main_menu() -> void:
SceneLoader.load_scene(main_menu_scene)
func _advance_level() -> bool:
if is_on_last_level(): return false
current_level_id += 1
return true
func _advance_and_load_main_menu() -> void:
_advance_level()
_load_main_menu()
func _load_ending() -> void:
if ending_scene:
SceneLoader.load_scene(ending_scene)
else:
_load_main_menu()
func _on_level_lost() -> void:
if level_lost_scene:
var instance = level_lost_scene.instantiate()
get_tree().current_scene.add_child(instance)
_try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
_try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
else:
_reload_level()
func get_current_level_id() -> int:
return current_level_id if force_level == -1 else force_level
func load_current_level() -> void:
level_list_loader.load_level(get_current_level_id())
func _advance_and_reload() -> void:
var _prior_level_id = get_current_level_id()
_advance_level()
current_level_id = _prior_level_id
load_current_level()
func _load_next_level() -> void:
_advance_level()
load_current_level()
func _reload_level() -> void:
load_current_level()
func _load_win_screen_or_ending() -> void:
if game_won_scene:
var instance = game_won_scene.instantiate()
get_tree().current_scene.add_child(instance)
_try_connecting_signal_to_node(instance, &"continue_pressed", _load_ending)
_try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
_try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
else:
_load_ending()
func _load_level_complete_screen_or_next_level() -> void:
if level_won_scene:
var instance = level_won_scene.instantiate()
get_tree().current_scene.add_child(instance)
_try_connecting_signal_to_node(instance, &"continue_pressed", _load_next_level)
_try_connecting_signal_to_node(instance, &"restart_pressed", _advance_and_reload)
_try_connecting_signal_to_node(instance, &"main_menu_pressed", _advance_and_load_main_menu)
else:
_load_next_level()
func is_on_last_level() -> bool:
return get_current_level_id() + 1 >= level_list_loader.files.size()
func _on_level_won():
if is_on_last_level():
_load_win_screen_or_ending()
else:
_load_level_complete_screen_or_next_level()
func _connect_level_signals() -> void:
_try_connecting_signal_to_level(&"level_won", _on_level_won)
_try_connecting_signal_to_level(&"level_lost", _on_level_lost)
_try_connecting_signal_to_level(&"level_skipped", _load_next_level)
func _on_level_loader_level_loaded() -> void:
current_level = level_list_loader.current_level
await current_level.ready
_connect_level_signals()
if level_loading_screen:
level_loading_screen.close()
func _on_level_loader_levels_finished() -> void:
_load_win_screen_or_ending()
func _on_level_loader_level_load_started() -> void:
if level_loading_screen:
level_loading_screen.reset()
func _ready() -> void:
level_list_loader.level_loaded.connect(_on_level_loader_level_loaded)
level_list_loader.levels_finished.connect(_on_level_loader_levels_finished)
level_list_loader.level_load_started.connect(_on_level_loader_level_load_started)
if auto_load:
load_current_level()

View File

@ -0,0 +1 @@
uid://yakisxcmflgk

View File

@ -0,0 +1,52 @@
@tool
class_name LevelLoader
extends Node
## Loads scenes into a container.
signal level_load_started
signal level_loaded
signal level_ready
## Container where the level instance will be added.
@export var level_container : Node
## Loads a level on start.
@export var auto_load : bool = false
@export var current_level_path : String
@export_group("Debugging")
@export var force_level : String
@export var current_level : Node
var is_loading : bool = false
func get_current_level_path() -> String:
return current_level_path if force_level.is_empty() else force_level
func _attach_level(level_resource : Resource):
assert(level_container != null, "level_container is null")
var instance = level_resource.instantiate()
level_container.call_deferred("add_child", instance)
return instance
func load_level(level_path : String = get_current_level_path()):
if is_loading : return
if is_instance_valid(current_level):
current_level.queue_free()
await current_level.tree_exited
current_level = null
is_loading = true
current_level_path = level_path
SceneLoader.load_scene(level_path, true)
level_load_started.emit()
await SceneLoader.scene_loaded
is_loading = false
current_level = _attach_level(SceneLoader.get_resource())
level_loaded.emit()
await current_level.ready
level_ready.emit()
func reload_level():
load_level()
func _ready():
if auto_load:
load_level()

View File

@ -0,0 +1 @@
uid://bbymrin0cm704

View File

@ -0,0 +1,23 @@
@tool
extends Node
class_name SceneLister
## Helper class for listing all the scenes in a directory.
## List of paths to scene files.
## Prefilled in the editor by selecting a directory.
@export var files : Array[String]
## Prefill files with any scenes in the directory.
@export_dir var directory : String :
set(value):
directory = value
_refresh_files()
func _refresh_files():
if not is_inside_tree() or directory.is_empty(): return
var dir_access = DirAccess.open(directory)
if dir_access:
files.clear()
for file in dir_access.get_files():
if not file.ends_with(".tscn"):
continue
files.append(directory + "/" + file)

View File

@ -0,0 +1 @@
uid://wjq7li836lwj