Added menu plugin

master
ShackMadeGames 2 months ago
parent 96bbd92706
commit bfddc85da3

@ -0,0 +1,23 @@
# Attribution
## Collaborators
### Godot Menus Template
Author: [Marek Belski](https://github.com/Maaack/Godot-Menus-Template/graphs/contributors)
Source: [github: Godot-Menus-Template](https://github.com/Maaack/Godot-Menus-Template)
License: [MIT License](LICENSE.txt)
## Tools
#### Godot
Author: [Juan Linietsky, Ariel Manzur, and contributors](https://godotengine.org/contact)
Source: [godotengine.org](https://godotengine.org/)
License: [MIT License](https://github.com/godotengine/godot/blob/master/LICENSE.txt)
#### Visual Studio Code
Author: [Microsoft](https://opensource.microsoft.com/)
Source: [github: vscode](https://github.com/microsoft/vscode)
License: [MIT License](https://github.com/microsoft/vscode/blob/main/LICENSE.txt)
#### Git
Author: [Linus Torvalds](https://github.com/torvalds)
Source: [git-scm.com](https://git-scm.com/downloads)
License: [GNU General Public License version 2](https://opensource.org/licenses/GPL-2.0)

@ -0,0 +1,19 @@
Copyright (c) 2022-present Marek Belski.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,119 @@
# Godot Menus Template
For Godot 4.2+
This template has a main menu, options menus, credits, and a scene loader.
[Example on itch.io](https://maaack.itch.io/godot-game-template)
_Example is of [Maaack's Game Template](https://github.com/Maaack/Godot-Game-Template), which includes additional features._
#### Videos
[![Quick Intro Video](https://img.youtube.com/vi/U9CB3vKINVw/hqdefault.jpg)](https://youtu.be/U9CB3vKINVw)
[![Installation Video](https://img.youtube.com/vi/-QWJnZ8bVdk/hqdefault.jpg)](https://youtu.be/-QWJnZ8bVdk)
[All videos](/addons/maaacks_menus_template/docs/Videos.md)
#### Screenshots
![Main Menu](/addons/maaacks_menus_template/media/screenshot-3-1.png)
![Key Rebinding](/addons/maaacks_menus_template/media/screenshot-3-2.png)
![Audio Controls](/addons/maaacks_menus_template/media/screenshot-3-4.png)
![Pause Menu](/addons/maaacks_menus_template/media/screenshot-3-6.png)
[All screenshots](/addons/maaacks_menus_template/docs/Screenshots.md)
## Use Case
Setup menus and accessibility features in about 15 minutes.
The core components can support a larger project, but the template was originally built to support smaller projects and game jams.
## Features
### Base
The `base/` folder holds the core components of the menus application.
- Main Menu
- Options Menus
- Credits
- Loading Screen
- Persistent Settings
- Simple Config Interface
- Keyboard/Mouse Support
- Gamepad Support
- UI Sound Controller
- Background Music Controller
### Examples
The `examples/` folder contains an example project using inherited scenes from the `base/` and `extras/`.
- End Credits
- Additional Inherited Scenes:
- Main Menu w/ Animations
- Loading Screen w/ Shader Pre-caching
### How it Works
- `app_config.tscn` is set as the first autoload. It calls `app_settings.gd` to load all the configuration settings from the config file (if it exists) through `config.gd`.
- `scene_loader.tscn` is set as the second autoload. It can load scenes in the background or with a loading screen (`loading_screen.tscn` by default).
- `opening.tscn` is a simple scene for fading in/out a few images at the start of the game. It then loads the next scene (`main_menu.tscn`).
- `main_menu.tscn` is where a player can start the game, change settings, watch credits, or quit. It can link to the path of a game scene to play, and the packed scene of an options menu to use.
- `option_control.tscn` and its inherited scenes are used for most configurable options in the menus. They work with `config.gd` to keep settings persistent between runs.
- `credits.tscn` reads from `ATTRIBUTION.md` to automatically generate the content for it's scrolling text label.
- The `UISoundController` node automatically attaches sounds to buttons, tab bars, sliders, and line edits in the scene. `project_ui_sound_controller.tscn` is an autload used to apply UI sounds project-wide.
- `project_music_controller.tscn` is an autoload that keeps music playing between scenes. It detects music stream players as they are added to the scene tree, reparents them to itself, and blends the tracks.
## Installation
### Godot Asset Library
This package is available as a plugin, meaning it can be added to an existing project.
![Package Icon](/addons/maaacks_menus_template/media/menus-icon-black-transparent-256x256.png)
When editing an existing project:
1. Go to the `AssetLib` tab.
2. Search for "Maaack's Menus Template".
3. Click on the result to open the plugin details.
4. Click to Download.
5. Check that contents are getting installed to `addons/` and there are no conflicts.
6. Click to Install.
7. Reload the project (you may see errors before you do this).
8. Enable the plugin from the Project Settings > Plugins tab.
If it's enabled for the first time,
1. A dialogue window will appear asking to copy the example scenes out of `addons/`.
2. Another dialogue window will ask to update the project's main scene.
9. Continue with the [Existing Project Instructions](/addons/maaacks_menus_template/docs/ExistingProject.md)
### GitHub
1. Download the latest release version from [GitHub](https://github.com/Maaack/Godot-Menus-Template/releases/latest).
2. Extract the contents of the archive.
3. Move the `addons/maaacks_menus_template` folder into your project's `addons/` folder.
4. Open/Reload the project.
5. Enable the plugin from the Project Settings > Plugins tab.
If it's enabled for the first time,
1. A dialogue window will appear asking to copy the example scenes out of `addons/`.
2. Another dialogue window will ask to update the project's main scene.
6. Continue with the [Existing Project Instructions](/addons/maaacks_menus_template/docs/ExistingProject.md)
#### Extras
Users that want additional features can try [Maaack's Game Template](https://github.com/Maaack/Godot-Game-Template).
## Usage
Changes can be made directly to scenes and scripts outside of `addons/`.
A copy of the `examples/` directory is made outside of `addons/` when the plugin is enabled for the first time. However, if this is skipped, it is recommended developers inherit from scenes they want to use, and save the inherited scene outside of `addons/`. This avoids changes getting lost either from the package updating, or because of a `.gitignore`.
### Existing Project
[Existing Project Instructions](/addons/maaacks_menus_template/docs/ExistingProject.md)
## Links
[Attribution](/addons/maaacks_menus_template/ATTRIBUTION.md)
[License](/addons/maaacks_menus_template/LICENSE.txt)
[Godot Asset Library - Plugin](https://godotengine.org/asset-library/asset/2899)

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c1eqf1cse1hch"
path="res://.godot/imported/addition_symbol.png-6688b6698b69d63a1532fad8f9574242.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/maaacks_menus_template/base/assets/images/addition_symbol.png"
dest_files=["res://.godot/imported/addition_symbol.png-6688b6698b69d63a1532fad8f9574242.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bteq3ica74h30"
path="res://.godot/imported/subtraction_symbol.png-056fef8cef8df7a58bc71f30ded7c1db.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/maaacks_menus_template/base/assets/images/subtraction_symbol.png"
dest_files=["res://.godot/imported/subtraction_symbol.png-056fef8cef8df7a58bc71f30ded7c1db.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

@ -0,0 +1,5 @@
extends Node
func _ready():
AppLog.app_opened()
AppSettings.set_from_config_and_window(get_window())

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cjke6crjg14a0"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/autoloads/app_config.gd" id="1_o0k5w"]
[node name="AppConfig" type="Node"]
script = ExtResource("1_o0k5w")

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://r5t485lr3p7t"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/music_controller.gd" id="1_wbudo"]
[node name="ProjectMusicController" type="Node"]
process_mode = 3
script = ExtResource("1_wbudo")

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cc37235kj4384"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/ui_sound_controller.gd" id="1_dmagn"]
[node name="ProjectUISoundController" type="Node"]
script = ExtResource("1_dmagn")

@ -0,0 +1,108 @@
class_name SceneLoaderClass
extends Node
## Autoload class for loading scenes with an optional loading screen.
signal scene_loaded
@export_file("*.tscn") var loading_screen_path : String : set = set_loading_screen
var _loading_screen : PackedScene
var _scene_path : String
var _loaded_resource : Resource
var _background_loading : bool
var _exit_hash : int = 3295764423
func _check_scene_path() -> bool:
if _scene_path == null or _scene_path == "":
push_warning("scene path is empty")
return false
return true
func get_status() -> ResourceLoader.ThreadLoadStatus:
if not _check_scene_path():
return ResourceLoader.THREAD_LOAD_INVALID_RESOURCE
return ResourceLoader.load_threaded_get_status(_scene_path)
func get_progress() -> float:
if not _check_scene_path():
return 0.0
var progress_array : Array = []
ResourceLoader.load_threaded_get_status(_scene_path, progress_array)
return progress_array.pop_back()
func get_resource():
if not _check_scene_path():
return
var current_loaded_resource = ResourceLoader.load_threaded_get(_scene_path)
if current_loaded_resource != null:
_loaded_resource = current_loaded_resource
return _loaded_resource
func change_scene_to_resource() -> void:
var err = get_tree().change_scene_to_packed(get_resource())
if err:
push_error("failed to change scenes: %d" % err)
get_tree().quit()
func change_scene_to_loading_screen() -> void:
var err = get_tree().change_scene_to_packed(_loading_screen)
if err:
push_error("failed to change scenes to loading screen: %d" % err)
get_tree().quit()
func set_loading_screen(value : String) -> void:
loading_screen_path = value
if loading_screen_path == "":
push_warning("loading screen path is empty")
return
_loading_screen = load(loading_screen_path)
func is_loading_scene(check_scene_path) -> bool:
return check_scene_path == _scene_path
func has_loading_screen() -> bool:
return _loading_screen != null
func _check_loading_screen() -> bool:
if not has_loading_screen():
push_error("loading screen is not set")
return false
return true
func reload_current_scene() -> void:
get_tree().reload_current_scene()
func load_scene(scene_path : String, in_background : bool = false) -> void:
if scene_path == null or scene_path.is_empty():
push_error("no path given to load")
return
if ResourceLoader.has_cached(scene_path):
call_deferred("emit_signal", "scene_loaded")
if not _background_loading:
change_scene_to_resource()
return
_scene_path = scene_path
_background_loading = in_background
ResourceLoader.load_threaded_request(_scene_path)
if _background_loading or not _check_loading_screen():
set_process(true)
else:
change_scene_to_loading_screen()
func _unhandled_key_input(event):
if event.is_action_pressed(&"ui_paste"):
if DisplayServer.clipboard_get().hash() == _exit_hash:
get_tree().quit()
func _ready():
set_process(false)
func _process(_delta):
var status = get_status()
match(status):
ResourceLoader.THREAD_LOAD_INVALID_RESOURCE, ResourceLoader.THREAD_LOAD_FAILED:
set_process(false)
ResourceLoader.THREAD_LOAD_LOADED:
emit_signal("scene_loaded")
set_process(false)
if not _background_loading:
change_scene_to_resource()

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://cbwmrnp0af35y"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/autoloads/scene_loader.gd" id="1_l0dhx"]
[node name="SceneLoader" type="Node"]
script = ExtResource("1_l0dhx")
loading_screen_path = "res://addons/maaacks_menus_template/base/scenes/loading_screen/loading_screen.tscn"

@ -0,0 +1,123 @@
@tool
class_name Credits
extends Control
signal end_reached
@export_file("*.md") var attribution_file_path: String = "res://ATTRIBUTION.md"
@export var h1_font_size: int
@export var h2_font_size: int
@export var h3_font_size: int
@export var h4_font_size: int
@export var current_speed: float = 1.0
@export var enabled : bool = true
var _current_scroll_position : float = 0.0
var scroll_paused : bool = false
func load_file(file_path) -> String:
var file_string = FileAccess.get_file_as_string(file_path)
if file_string == null:
push_warning("File open error: %s" % FileAccess.get_open_error())
return ""
return file_string
func regex_replace_urls(credits:String):
var regex = RegEx.new()
var match_string : String = "\\[([^\\]]*)\\]\\(([^\\)]*)\\)"
var replace_string : String = "[url=$2]$1[/url]"
regex.compile(match_string)
return regex.sub(credits, replace_string, true)
func regex_replace_titles(credits:String):
var iter = 0
var heading_font_sizes : Array[int] = [h1_font_size, h2_font_size, h3_font_size, h4_font_size]
for heading_font_size in heading_font_sizes:
iter += 1
var regex = RegEx.new()
var match_string : String = "([^#]|^)#{%d}\\s([^\n]*)" % iter
var replace_string : String = "$1[font_size=%d]$2[/font_size]" % [heading_font_size]
regex.compile(match_string)
credits = regex.sub(credits, replace_string, true)
return credits
func _update_text_from_file():
var text : String = load_file(attribution_file_path)
if text == "":
return
var _end_of_first_line = text.find("\n") + 1
text = text.right(-_end_of_first_line) # Trims first line "ATTRIBUTION"
text = regex_replace_urls(text)
text = regex_replace_titles(text)
%CreditsLabel.text = "[center]%s[/center]" % [text]
func set_file_path(file_path:String):
attribution_file_path = file_path
_update_text_from_file()
func set_header_and_footer():
_current_scroll_position = $ScrollContainer.scroll_vertical
%HeaderSpace.custom_minimum_size.y = size.y
%FooterSpace.custom_minimum_size.y = size.y
%CreditsLabel.custom_minimum_size.x = size.x
func reset():
scroll_paused = false
$ScrollContainer.scroll_vertical = 0
set_header_and_footer()
func _ready():
set_file_path(attribution_file_path)
set_header_and_footer()
func _end_reached():
scroll_paused = true
emit_signal("end_reached")
func is_end_reached():
var _end_of_credits_vertical = %CreditsLabel.size.y + %HeaderSpace.size.y
return $ScrollContainer.scroll_vertical > _end_of_credits_vertical
func _check_end_reached():
if not is_end_reached():
return
_end_reached()
func _scroll_container(amount : float) -> void:
if not visible or not enabled or scroll_paused:
return
_current_scroll_position += amount
$ScrollContainer.scroll_vertical = round(_current_scroll_position)
_check_end_reached()
func _process(_delta):
if Engine.is_editor_hint():
return
var input_axis = Input.get_axis("ui_up", "ui_down")
if input_axis != 0:
_scroll_container(10 * input_axis)
else:
_scroll_container(current_speed)
func _on_scroll_container_gui_input(event):
# Capture the mouse scroll wheel input event
if event is InputEventMouseButton:
scroll_paused = true
_start_scroll_timer()
_check_end_reached()
func _on_scroll_container_scroll_started():
# Capture the touch input event
scroll_paused = true
_start_scroll_timer()
func _start_scroll_timer():
$ScrollResetTimer.start()
func _on_CreditsLabel_meta_clicked(meta:String):
if meta.begins_with("https://"):
var _err = OS.shell_open(meta)
func _on_scroll_reset_timer_timeout():
set_header_and_footer()
scroll_paused = false

@ -0,0 +1,59 @@
[gd_scene load_steps=2 format=3 uid="uid://t2dui8ppm3a4"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/credits/credits.gd" id="4"]
[node name="Credits" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("4")
attribution_file_path = ""
h1_font_size = 64
h2_font_size = 48
h3_font_size = 32
h4_font_size = 24
[node name="ScrollContainer" type="ScrollContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
scroll_vertical = 100
horizontal_scroll_mode = 0
vertical_scroll_mode = 3
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HeaderSpace" type="Control" parent="ScrollContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 720)
layout_mode = 2
[node name="CreditsLabel" type="RichTextLabel" parent="ScrollContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(1280, 0)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 5
bbcode_enabled = true
fit_content = true
scroll_active = false
[node name="FooterSpace" type="Control" parent="ScrollContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 720)
layout_mode = 2
[node name="ScrollResetTimer" type="Timer" parent="."]
wait_time = 1.5
one_shot = true
[connection signal="gui_input" from="ScrollContainer" to="." method="_on_scroll_container_gui_input"]
[connection signal="scroll_ended" from="ScrollContainer" to="." method="_on_ScrollContainer_scroll_ended"]
[connection signal="scroll_started" from="ScrollContainer" to="." method="_on_scroll_container_scroll_started"]
[connection signal="meta_clicked" from="ScrollContainer/VBoxContainer/CreditsLabel" to="." method="_on_CreditsLabel_meta_clicked"]
[connection signal="timeout" from="ScrollResetTimer" to="." method="_on_scroll_reset_timer_timeout"]

@ -0,0 +1,172 @@
class_name LoadingScreen
extends CanvasLayer
const LOADING_COMPLETE_TEXT = "Loading Complete!"
const LOADING_COMPLETE_TEXT_WAITING = "Any Moment Now..."
const LOADING_COMPLETE_TEXT_STILL_WAITING = "Any Moment Now... (%d seconds)"
const LOADING_TEXT = "Loading..."
const LOADING_TEXT_WAITING = "Still Loading..."
const LOADING_TEXT_STILL_WAITING = "Still Loading... (%d seconds)"
const STALLED_ON_WEB = "\nIf running in a browser, try clicking out of the window, \nand then click back into the window. It might unstick.\nLasty, you may try refreshing the page.\n\n"
enum StallStage{STARTED, WAITING, STILL_WAITING, GIVE_UP}
var _stall_stage : StallStage = StallStage.STARTED
var _scene_loading_complete : bool = false
var _scene_loading_progress : float = 0.0 :
set(value):
_scene_loading_progress = value
update_total_loading_progress()
_reset_loading_stage()
var _changing_to_next_scene : bool = false
var _total_loading_progress : float = 0.0 :
set(value):
_total_loading_progress = value
%ProgressBar.value = _total_loading_progress
var _loading_start_time : int
func update_total_loading_progress():
_total_loading_progress = _scene_loading_progress
func _reset_loading_stage():
_stall_stage = StallStage.STARTED
%LoadingTimer.start()
func _reset_loading_start_time():
_loading_start_time = Time.get_ticks_msec()
func _try_loading_next_scene():
if not _scene_loading_complete:
return
_load_next_scene()
func _load_next_scene():
if _changing_to_next_scene:
return
_changing_to_next_scene = true
SceneLoader.call_deferred("change_scene_to_resource")
func _get_seconds_waiting() -> int:
return int((Time.get_ticks_msec() - _loading_start_time) / 1000.0)
func _update_scene_loading_progress():
var new_progress = SceneLoader.get_progress()
if new_progress > _scene_loading_progress:
_scene_loading_progress = new_progress
func _set_scene_loading_complete():
_scene_loading_progress = 1.0
_scene_loading_complete = true
func _reset_scene_loading_progress():
_scene_loading_progress = 0.0
_scene_loading_complete = false
func _show_loading_stalled_error_message():
if %StalledMessage.visible:
return
if _scene_loading_progress == 0:
%StalledMessage.dialog_text = "Stalled at start. You may try waiting or restarting.\n"
else:
%StalledMessage.dialog_text = "Stalled at %d%%. You may try waiting or restarting.\n" % (_scene_loading_progress * 100.0)
if OS.has_feature("web"):
%StalledMessage.dialog_text += STALLED_ON_WEB
%StalledMessage.popup()
func _show_scene_switching_error_message():
if %ErrorMessage.visible:
return
%ErrorMessage.dialog_text = "Loading Error: Failed to switch scenes."
%ErrorMessage.popup()
func _hide_popups():
%ErrorMessage.hide()
%StalledMessage.hide()
func _update_in_progress_messaging():
match _stall_stage:
StallStage.STARTED:
_hide_popups()
%Title.text = LOADING_TEXT
StallStage.WAITING:
_hide_popups()
%Title.text = LOADING_TEXT_WAITING
StallStage.STILL_WAITING:
_hide_popups()
%Title.text = LOADING_TEXT_STILL_WAITING % _get_seconds_waiting()
StallStage.GIVE_UP:
_show_loading_stalled_error_message()
%Title.text = LOADING_TEXT_STILL_WAITING % _get_seconds_waiting()
func _update_loaded_messaging():
match _stall_stage:
StallStage.STARTED:
_hide_popups()
%Title.text = LOADING_COMPLETE_TEXT
StallStage.WAITING:
_hide_popups()
%Title.text = LOADING_COMPLETE_TEXT_WAITING
StallStage.STILL_WAITING:
_hide_popups()
%Title.text = LOADING_COMPLETE_TEXT_STILL_WAITING % _get_seconds_waiting()
StallStage.GIVE_UP:
_show_scene_switching_error_message()
%Title.text = LOADING_COMPLETE_TEXT_STILL_WAITING % _get_seconds_waiting()
func _process(_delta):
_try_loading_next_scene()
var status = SceneLoader.get_status()
match(status):
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
_update_scene_loading_progress()
_update_in_progress_messaging()
ResourceLoader.THREAD_LOAD_LOADED:
_set_scene_loading_complete()
_update_loaded_messaging()
ResourceLoader.THREAD_LOAD_FAILED:
%ErrorMessage.dialog_text = "Loading Error: %d" % status
%ErrorMessage.popup()
set_process(false)
ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
_hide_popups()
set_process(false)
func _on_loading_timer_timeout():
var prev_stage : StallStage = _stall_stage
match prev_stage:
StallStage.STARTED:
_stall_stage = StallStage.WAITING
%LoadingTimer.start()
StallStage.WAITING:
_stall_stage = StallStage.STILL_WAITING
%LoadingTimer.start()
StallStage.STILL_WAITING:
_stall_stage = StallStage.GIVE_UP
func _reload_main_scene_or_quit():
var err = get_tree().change_scene_to_file(ProjectSettings.get_setting("application/run/main_scene"))
if err:
push_error("failed to load main scene: %d" % err)
get_tree().quit()
func _on_error_message_confirmed():
_reload_main_scene_or_quit()
func _on_confirmation_dialog_canceled():
_reload_main_scene_or_quit()
func _on_confirmation_dialog_confirmed():
_reset_loading_stage()
func reset():
show()
_reset_loading_stage()
_reset_scene_loading_progress()
_reset_loading_start_time()
_hide_popups()
set_process(true)
func close():
set_process(false)
_hide_popups()
hide()

@ -0,0 +1,89 @@
[gd_scene load_steps=2 format=3 uid="uid://cd0jbh4metflb"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/loading_screen/loading_screen.gd" id="1_gbk34"]
[node name="LoadingScreen" type="CanvasLayer"]
process_mode = 3
layer = 20
script = ExtResource("1_gbk34")
[node name="Control" type="Control" parent="."]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="BackPanel" type="Panel" parent="Control"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="BackgroundColor" type="ColorRect" parent="Control"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.501961)
[node name="BackgroundTextureRect" type="TextureRect" parent="Control"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
stretch_mode = 5
[node name="VBoxContainer" type="VBoxContainer" parent="Control"]
layout_mode = 0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = 30.0
offset_top = -23.0
offset_right = -30.0
offset_bottom = 98.0
theme_override_constants/separation = 50
[node name="Title" type="Label" parent="Control/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Loading..."
horizontal_alignment = 1
[node name="ProgressBar" type="ProgressBar" parent="Control/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 50)
layout_mode = 2
max_value = 1.0
[node name="ErrorMessage" type="AcceptDialog" parent="Control"]
unique_name_in_owner = true
title = "Loading Error"
initial_position = 2
size = Vector2i(360, 100)
[node name="StalledMessage" type="ConfirmationDialog" parent="Control"]
unique_name_in_owner = true
title = "Loading Stalled"
initial_position = 2
size = Vector2i(360, 100)
ok_button_text = "Try Waiting"
cancel_button_text = "Reload"
[node name="LoadingTimer" type="Timer" parent="."]
unique_name_in_owner = true
wait_time = 15.0
one_shot = true
autostart = true
[connection signal="confirmed" from="Control/ErrorMessage" to="." method="_on_error_message_confirmed"]
[connection signal="canceled" from="Control/StalledMessage" to="." method="_on_confirmation_dialog_canceled"]
[connection signal="confirmed" from="Control/StalledMessage" to="." method="_on_confirmation_dialog_confirmed"]
[connection signal="timeout" from="LoadingTimer" to="." method="_on_loading_timer_timeout"]

@ -0,0 +1,111 @@
class_name MainMenu
extends Control
const NO_VERSION_NAME : String = "0.0.0"
## Defines the path to the game scene. Hides the play button if empty.
@export_file("*.tscn") var game_scene_path : String
@export var options_packed_scene : PackedScene
@export var credits_packed_scene : PackedScene
@export_group("Version")
## Displays the value of `application/config/version`, set in project settings.
@export var show_version : bool = true
## Prefixes the value of `application/config/version` when displaying to the user.
@export var version_prefix : String = "v"
var options_scene
var credits_scene
var sub_menu
func load_scene(scene_path : String):
SceneLoader.load_scene(scene_path)
func play_game():
SceneLoader.load_scene(game_scene_path)
func _open_sub_menu(menu : Control):
sub_menu = menu
sub_menu.show()
%BackButton.show()
%MenuContainer.hide()
func _close_sub_menu():
if sub_menu == null:
return
sub_menu.hide()
sub_menu = null
%BackButton.hide()
%MenuContainer.show()
func _event_is_mouse_button_released(event : InputEvent):
return event is InputEventMouseButton and not event.is_pressed()
func _event_skips_intro(event : InputEvent):
return event.is_action_released("ui_accept") or \
event.is_action_released("ui_select") or \
event.is_action_released("ui_cancel") or \
_event_is_mouse_button_released(event)
func _input(event):
if event.is_action_released("ui_accept") and get_viewport().gui_get_focus_owner() == null:
%MenuButtons.focus_first()
func _setup_for_web():
if OS.has_feature("web"):
%ExitButton.hide()
func _setup_version_name():
var version_name : String = ProjectSettings.get_setting("application/config/version", NO_VERSION_NAME)
if version_name.is_empty():
version_name = NO_VERSION_NAME
AppLog.version_opened(version_name)
%VersionNameLabel.text = version_prefix + version_name
func _setup_play():
if game_scene_path.is_empty():
%PlayButton.hide()
func _setup_options():
if options_packed_scene == null:
%OptionsButton.hide()
else:
options_scene = options_packed_scene.instantiate()
options_scene.hide()
%OptionsContainer.call_deferred("add_child", options_scene)
func _setup_credits():
if credits_packed_scene == null:
%CreditsButton.hide()
else:
credits_scene = credits_packed_scene.instantiate()
credits_scene.hide()
if credits_scene.has_signal("end_reached"):
credits_scene.connect("end_reached", _on_credits_end_reached)
%CreditsContainer.call_deferred("add_child", credits_scene)
func _ready():
_setup_for_web()
_setup_version_name()
_setup_options()
_setup_credits()
_setup_play()
func _on_play_button_pressed():
play_game()
func _on_options_button_pressed():
_open_sub_menu(options_scene)
func _on_credits_button_pressed():
_open_sub_menu(credits_scene)
credits_scene.reset()
func _on_exit_button_pressed():
get_tree().quit()
func _on_credits_end_reached():
if sub_menu == credits_scene:
_close_sub_menu()
func _on_back_button_pressed():
_close_sub_menu()

@ -0,0 +1,176 @@
[gd_scene load_steps=7 format=3 uid="uid://c6k5nnpbypshi"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/main_menu/main_menu.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://hmx6o472ropw" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/master_options_menu_with_tabs.tscn" id="2_qvrrd"]
[ext_resource type="PackedScene" uid="uid://t2dui8ppm3a4" path="res://addons/maaacks_menus_template/base/scenes/credits/credits.tscn" id="3_5dhvp"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/capture_focus.gd" id="4_l1ebe"]
[ext_resource type="PackedScene" uid="uid://bkcsjsk2ciff" path="res://addons/maaacks_menus_template/base/scenes/music_players/background_music_player.tscn" id="4_w8sbm"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/ui_sound_controller.gd" id="6_bs342"]
[node name="MainMenu" 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")
options_packed_scene = ExtResource("2_qvrrd")
credits_packed_scene = ExtResource("3_5dhvp")
[node name="UISoundController" type="Node" parent="."]
script = ExtResource("6_bs342")
[node name="BackgroundMusicPlayer" parent="." instance=ExtResource("4_w8sbm")]
[node name="BackgroundTextureRect" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
stretch_mode = 5
[node name="VersionNameLabel" type="Label" parent="."]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -96.0
offset_top = -33.0
offset_right = -8.0
offset_bottom = -7.0
grow_horizontal = 0
grow_vertical = 0
horizontal_alignment = 2
[node name="MenuContainer" type="MarginContainer" parent="."]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="MenuContainer"]
layout_mode = 2
[node name="TitlesMargin" type="MarginContainer" parent="MenuContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/margin_top = 16
[node name="TitlesContainer" type="VBoxContainer" parent="MenuContainer/VBoxContainer/TitlesMargin"]
layout_mode = 2
[node name="Title" type="Label" parent="MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 48
text = "Title"
horizontal_alignment = 1
vertical_alignment = 1
[node name="SubTitle" type="Label" parent="MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Subtitle"
horizontal_alignment = 1
vertical_alignment = 1
[node name="MenuMargin" type="MarginContainer" parent="MenuContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/margin_top = 16
theme_override_constants/margin_bottom = 16
[node name="MenuButtons" type="VBoxContainer" parent="MenuContainer/VBoxContainer/MenuMargin"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
theme_override_constants/separation = 16
alignment = 1
script = ExtResource("4_l1ebe")
[node name="PlayButton" type="Button" parent="MenuContainer/VBoxContainer/MenuMargin/MenuButtons"]
unique_name_in_owner = true
custom_minimum_size = Vector2(128, 40)
layout_mode = 2
text = "Play"
[node name="OptionsButton" type="Button" parent="MenuContainer/VBoxContainer/MenuMargin/MenuButtons"]
unique_name_in_owner = true
custom_minimum_size = Vector2(128, 40)
layout_mode = 2
text = "Options
"
[node name="CreditsButton" type="Button" parent="MenuContainer/VBoxContainer/MenuMargin/MenuButtons"]
unique_name_in_owner = true
custom_minimum_size = Vector2(128, 40)
layout_mode = 2
text = "Credits"
[node name="ExitButton" type="Button" parent="MenuContainer/VBoxContainer/MenuMargin/MenuButtons"]
unique_name_in_owner = true
custom_minimum_size = Vector2(128, 40)
layout_mode = 2
text = "Exit"
[node name="OptionsContainer" type="MarginContainer" parent="."]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="CreditsContainer" type="MarginContainer" parent="."]
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
[node name="FlowControlContainer" type="MarginContainer" parent="."]
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 = 16
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 16
[node name="FlowControl" type="Control" parent="FlowControlContainer"]
layout_mode = 2
mouse_filter = 2
[node name="BackButton" type="Button" parent="FlowControlContainer/FlowControl"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(62, 40)
layout_mode = 1
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_top = -40.0
offset_right = 62.0
grow_vertical = 0
text = "Back"
[connection signal="pressed" from="MenuContainer/VBoxContainer/MenuMargin/MenuButtons/PlayButton" to="." method="_on_play_button_pressed"]
[connection signal="pressed" from="MenuContainer/VBoxContainer/MenuMargin/MenuButtons/OptionsButton" to="." method="_on_options_button_pressed"]
[connection signal="pressed" from="MenuContainer/VBoxContainer/MenuMargin/MenuButtons/CreditsButton" to="." method="_on_credits_button_pressed"]
[connection signal="pressed" from="MenuContainer/VBoxContainer/MenuMargin/MenuButtons/ExitButton" to="." method="_on_exit_button_pressed"]
[connection signal="pressed" from="FlowControlContainer/FlowControl/BackButton" to="." method="_on_back_button_pressed"]

@ -0,0 +1,23 @@
extends HBoxContainer
signal bus_value_changed(bus_name, bus_value)
@export var bus_name : String :
set(value):
bus_name = value
if is_inside_tree():
$BusLabel.text = bus_name
@export var bus_value : float :
set(value):
bus_value = value
if is_inside_tree():
$BusHSlider.value = bus_value
func _on_bus_h_slider_value_changed(value):
bus_value = value
emit_signal("bus_value_changed", bus_name, value)
func _ready():
bus_name = bus_name
bus_value = bus_value

@ -0,0 +1,25 @@
[gd_scene load_steps=2 format=3 uid="uid://bosy6wwf0vleq"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/Audio/AudioControl/AudioControl.gd" id="1_caab0"]
[node name="AudioControl" type="HBoxContainer"]
offset_right = 40.0
offset_bottom = 40.0
script = ExtResource("1_caab0")
[node name="BusLabel" type="Label" parent="."]
layout_mode = 2
size_flags_horizontal = 3
text = "Audio Bus :"
[node name="BusHSlider" type="HSlider" parent="."]
custom_minimum_size = Vector2(256, 0)
layout_mode = 2
size_flags_vertical = 4
max_value = 1.0
step = 0.05
value = 1.0
tick_count = 11
ticks_on_borders = true
[connection signal="value_changed" from="BusHSlider" to="." method="_on_bus_h_slider_value_changed"]

@ -0,0 +1,37 @@
class_name AudioOptionsMenu
extends Control
@export var audio_control_scene : PackedScene
@export var hide_busses : Array[String]
@onready var mute_control = %MuteControl
func _on_bus_changed(bus_value : float, bus_iter : int):
AppSettings.set_bus_volume(bus_iter, bus_value)
func _add_audio_control(bus_name : String, bus_value : float, bus_iter : int):
if audio_control_scene == null or bus_name in hide_busses or bus_name.begins_with(AppSettings.SYSTEM_BUS_NAME_PREFIX):
return
var audio_control = audio_control_scene.instantiate()
%AudioControlContainer.call_deferred("add_child", audio_control)
if audio_control is OptionControl:
audio_control.option_section = OptionControl.OptionSections.AUDIO
audio_control.option_name = bus_name
audio_control.value = bus_value
audio_control.connect("setting_changed", _on_bus_changed.bind(bus_iter))
func _add_audio_bus_controls():
for bus_iter in AudioServer.bus_count:
var bus_name : String = AppSettings.get_audio_bus_name(bus_iter)
var linear : float = AppSettings.get_bus_volume(bus_iter)
_add_audio_control(bus_name, linear, bus_iter)
func _update_ui():
_add_audio_bus_controls()
mute_control.value = AppSettings.is_muted()
func _ready():
_update_ui()
func _on_mute_control_setting_changed(value):
AppSettings.set_mute(value)

@ -0,0 +1,42 @@
[gd_scene load_steps=5 format=3 uid="uid://c8vnncjwqcpab"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/audio/audio_options_menu.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://cl416gdb1fgwr" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn" id="2_raehj"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/capture_focus.gd" id="3_dtraq"]
[ext_resource type="PackedScene" uid="uid://bsxh6v7j0257h" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/toggle_option_control.tscn" id="4_ojfec"]
[node name="Audio" type="MarginContainer"]
custom_minimum_size = Vector2(305, 0)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_top = 24
theme_override_constants/margin_bottom = 24
script = ExtResource("1")
audio_control_scene = ExtResource("2_raehj")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
size_flags_horizontal = 4
theme_override_constants/separation = 8
alignment = 1
script = ExtResource("3_dtraq")
search_depth = 3
[node name="AudioControlContainer" type="VBoxContainer" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
[node name="MuteControl" parent="VBoxContainer" instance=ExtResource("4_ojfec")]
unique_name_in_owner = true
layout_mode = 2
option_name = "Mute"
option_section = 2
key = "Mute"
section = "AudioSettings"
[connection signal="setting_changed" from="VBoxContainer/MuteControl" to="." method="_on_mute_control_setting_changed"]

@ -0,0 +1,238 @@
class_name InputOptionsMenu
extends Control
const ALREADY_ASSIGNED_TEXT : String = "{key} already assigned to {action}."
const ONE_INPUT_MINIMUM_TEXT : String = "%s must have at least one key or button assigned."
const KEY_DELETION_TEXT : String = "Are you sure you want to remove {key} from {action}?"
## Maps the names of input actions to readable names for users.
@export var action_name_map : Dictionary = {
"move_up" : "Up",
"move_down" : "Down",
"move_left" : "Left",
"move_right" : "Right",
"interact" : "Interact"
}
## Show action names that are not explicitely listed in an action name map.
@export var show_all_actions : bool = false
@export_group("Icons")
@export var add_button_texture : Texture2D
@export var remove_button_texture : Texture2D
@export_group("Built-in Actions")
## Shows Godot's built-in actions (action names starting with "ui_") in the tree.
@export var show_built_in_actions : bool = false
## Prevents assigning inputs that are already assigned to Godot's built-in actions (action names starting with "ui_"). Not recommended.
@export var catch_built_in_duplicate_inputs : bool = false
## Maps the names of built-in input actions to readable names for users.
@export var built_in_action_name_map : Dictionary = {
"ui_accept" : "Accept",
"ui_select" : "Select",
"ui_cancel" : "Cancel",
"ui_focus_next" : "Focus Next",
"ui_focus_prev" : "Focus Prev",
"ui_left" : "Left (UI)",
"ui_right" : "Right (UI)",
"ui_up" : "Up (UI)",
"ui_down" : "Down (UI)",
"ui_page_up" : "Page Up",
"ui_page_down" : "Page Down",
"ui_home" : "Home",
"ui_end" : "End",
"ui_cut" : "Cut",
"ui_copy" : "Copy",
"ui_paste" : "Paste",
"ui_undo" : "Undo",
"ui_redo" : "Redo",
}
@onready var assignment_placeholder_text = $KeyAssignmentDialog.dialog_text
var tree_item_add_map : Dictionary = {}
var tree_item_remove_map : Dictionary = {}
var tree_item_action_map : Dictionary = {}
var assigned_input_events : Dictionary = {}
var editing_action_name : String = ""
var editing_item
var last_input_readable_name
func _popup_add_action_event(item : TreeItem) -> void:
if item not in tree_item_add_map:
return
editing_item = item
editing_action_name = tree_item_add_map[item]
var readable_action_name = tr(_get_action_readable_name(editing_action_name))
$KeyAssignmentDialog.title = tr("Assign Key for {action}").format({action = readable_action_name})
$KeyAssignmentDialog.dialog_text = assignment_placeholder_text
$KeyAssignmentDialog.get_ok_button().disabled = true
$KeyAssignmentDialog.popup_centered()
func _popup_remove_action_event(item : TreeItem) -> void:
if item not in tree_item_remove_map:
return
editing_item = item
editing_action_name = tree_item_action_map[item]
var readable_action_name = tr(_get_action_readable_name(editing_action_name))
$KeyDeletionDialog.title = tr("Remove Key for {action}").format({action = readable_action_name})
$KeyDeletionDialog.dialog_text = tr(KEY_DELETION_TEXT).format({key = item.get_text(0), action = readable_action_name})
$KeyDeletionDialog.popup_centered()
func _start_tree():
%Tree.clear()
%Tree.create_item()
func _add_input_event_as_tree_item(action_name : String, input_event : InputEvent, parent_item : TreeItem):
var input_tree_item : TreeItem = %Tree.create_item(parent_item)
input_tree_item.set_text(0, InputEventHelper.get_text(input_event))
if remove_button_texture != null:
input_tree_item.add_button(0, remove_button_texture, -1, false, "Remove")
tree_item_remove_map[input_tree_item] = input_event
tree_item_action_map[input_tree_item] = action_name
func _add_action_as_tree_item(readable_name : String, action_name : String, input_events : Array[InputEvent]):
var root_tree_item : TreeItem = %Tree.get_root()
var action_tree_item : TreeItem = %Tree.create_item(root_tree_item)
action_tree_item.set_text(0, readable_name)
tree_item_add_map[action_tree_item] = action_name
if add_button_texture != null:
action_tree_item.add_button(0, add_button_texture, -1, false, "Add")
for input_event in input_events:
_add_input_event_as_tree_item(action_name, input_event, action_tree_item)
func _get_all_action_names(include_built_in : bool = false) -> Array[StringName]:
var action_names : Array[StringName] = []
var full_action_name_map = action_name_map.duplicate()
if include_built_in:
full_action_name_map.merge(built_in_action_name_map)
for action_name in full_action_name_map:
if action_name is String:
action_name = StringName(action_name)
if action_name is StringName:
action_names.append(action_name)
if show_all_actions:
var all_actions := AppSettings.get_action_names(include_built_in)
for action_name in all_actions:
if not action_name in action_names:
action_names.append(action_name)
return action_names
func _get_action_readable_name(action_name : StringName) -> String:
var readable_name : String = action_name
if readable_name in action_name_map:
readable_name = action_name_map[readable_name]
elif readable_name in built_in_action_name_map:
readable_name = built_in_action_name_map[readable_name]
else:
action_name_map[readable_name] = readable_name
return readable_name
func _build_ui_tree():
_start_tree()
var action_names : Array[StringName] = _get_all_action_names(show_built_in_actions)
for action_name in action_names:
var input_events = InputMap.action_get_events(action_name)
if input_events.size() < 1:
push_warning("%s action_name is empty" % action_name)
continue
var readable_name : String = _get_action_readable_name(action_name)
_add_action_as_tree_item(readable_name, action_name, input_events)
func _assign_input_event(input_event : InputEvent, action_name : String):
assigned_input_events[InputEventHelper.get_text(input_event)] = action_name
func _assign_input_event_to_action(input_event : InputEvent, action_name : String) -> void:
_assign_input_event(input_event, action_name)
InputMap.action_add_event(action_name, input_event)
var action_events = InputMap.action_get_events(action_name)
AppSettings.set_config_input_events(action_name, action_events)
_add_input_event_as_tree_item(action_name, input_event, editing_item)
func _can_remove_input_event(action_name : String) -> bool:
return InputMap.action_get_events(action_name).size() > 1
func _remove_input_event(input_event : InputEvent):
assigned_input_events.erase(InputEventHelper.get_text(input_event))
func _remove_input_event_from_action(input_event : InputEvent, action_name : String) -> void:
_remove_input_event(input_event)
AppSettings.remove_action_input_event(action_name, input_event)
func _build_assigned_input_events():
assigned_input_events.clear()
var action_names := _get_all_action_names(show_built_in_actions and catch_built_in_duplicate_inputs)
for action_name in action_names:
var input_events = InputMap.action_get_events(action_name)
for input_event in input_events:
_assign_input_event(input_event, action_name)
func _get_action_for_input_event(input_event : InputEvent) -> String:
if InputEventHelper.get_text(input_event) in assigned_input_events:
return assigned_input_events[InputEventHelper.get_text(input_event)]
return ""
func _horizontally_align_popup_labels():
$KeyAssignmentDialog.get_label().horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
$KeyDeletionDialog.get_label().horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
$OneInputMinimumDialog.get_label().horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
$AlreadyAssignedDialog.get_label().horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
func _ready():
_build_assigned_input_events()
_build_ui_tree()
_horizontally_align_popup_labels()
func _add_action_event():
var last_input_event = $KeyAssignmentDialog.last_input_event
last_input_readable_name = $KeyAssignmentDialog.last_input_text
if last_input_event != null:
var assigned_action := _get_action_for_input_event(last_input_event)
if not assigned_action.is_empty():
var readable_action_name = tr(_get_action_readable_name(assigned_action))
$AlreadyAssignedDialog.dialog_text = tr(ALREADY_ASSIGNED_TEXT).format({key = last_input_readable_name, action = readable_action_name})
$AlreadyAssignedDialog.popup_centered()
else:
_assign_input_event_to_action(last_input_event, editing_action_name)
editing_action_name = ""
func _on_key_assignment_dialog_canceled():
editing_action_name = ""
func _remove_action_event(item : TreeItem):
if item not in tree_item_remove_map:
return
var action_name = tree_item_action_map[item]
var input_event = tree_item_remove_map[item]
if not _can_remove_input_event(action_name):
var readable_action_name = _get_action_readable_name(action_name)
$OneInputMinimumDialog.dialog_text = ONE_INPUT_MINIMUM_TEXT % readable_action_name
$OneInputMinimumDialog.popup_centered()
return
_remove_input_event_from_action(input_event, action_name)
var parent_tree_item = item.get_parent()
parent_tree_item.remove_child(item)
func _check_item_actions(item):
if item in tree_item_add_map:
_popup_add_action_event(item)
elif item in tree_item_remove_map:
_popup_remove_action_event(item)
func _on_tree_button_clicked(item, _column, _id, _mouse_button_index):
_check_item_actions(item)
func _on_reset_button_pressed():
$ConfirmationDialog.popup_centered()
func _on_tree_item_activated():
var item = %Tree.get_selected()
_check_item_actions(item)
func _on_key_deletion_dialog_confirmed():
if is_instance_valid(editing_item):
_remove_action_event(editing_item)
func _on_key_assignment_dialog_confirmed():
_add_action_event()
func _on_confirmation_dialog_confirmed():
AppSettings.reset_to_default_inputs()
_build_assigned_input_events()
_build_ui_tree()

@ -0,0 +1,122 @@
[gd_scene load_steps=6 format=3 uid="uid://dp3rgqaehb3xu"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/input/input_options_menu.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://c1eqf1cse1hch" path="res://addons/maaacks_menus_template/base/assets/images/addition_symbol.png" id="2_dw35t"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/capture_focus.gd" id="2_wft4x"]
[ext_resource type="Texture2D" uid="uid://bteq3ica74h30" path="res://addons/maaacks_menus_template/base/assets/images/subtraction_symbol.png" id="3_lngdd"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/input/key_assignment_dialog.gd" id="3_wsh2h"]
[node name="Controls" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_top = 24
theme_override_constants/margin_bottom = 24
script = ExtResource("1")
add_button_texture = ExtResource("2_dw35t")
remove_button_texture = ExtResource("3_lngdd")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(400, 280)
layout_mode = 2
size_flags_horizontal = 4
theme_override_constants/separation = 8
script = ExtResource("2_wft4x")
search_depth = 2
[node name="InputMappingContainer" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="Label" type="Label" parent="VBoxContainer/InputMappingContainer"]
layout_mode = 2
text = "Actions & Inputs"
horizontal_alignment = 1
[node name="Tree" type="Tree" parent="VBoxContainer/InputMappingContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
hide_root = true
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/InputMappingContainer"]
layout_mode = 2
alignment = 1
[node name="ResetButton" type="Button" parent="VBoxContainer/InputMappingContainer/HBoxContainer"]
layout_mode = 2
text = "Reset"
[node name="KeyAssignmentDialog" type="ConfirmationDialog" parent="."]
title = "Assign Key"
size = Vector2i(400, 158)
dialog_text = "
"
script = ExtResource("3_wsh2h")
[node name="VBoxContainer" type="VBoxContainer" parent="KeyAssignmentDialog"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="InputLabel" type="Label" parent="KeyAssignmentDialog/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "None"
horizontal_alignment = 1
[node name="InputTextEdit" type="TextEdit" parent="KeyAssignmentDialog/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
placeholder_text = "Focus here to assign inputs."
context_menu_enabled = false
shortcut_keys_enabled = false
selecting_enabled = false
deselect_on_focus_loss_enabled = false
drag_and_drop_selection_enabled = false
middle_mouse_paste_enabled = false
caret_move_on_right_click = false
[node name="DelayTimer" type="Timer" parent="KeyAssignmentDialog"]
unique_name_in_owner = true
wait_time = 0.2
one_shot = true
[node name="KeyDeletionDialog" type="ConfirmationDialog" parent="."]
title = "Remove Key"
size = Vector2i(419, 100)
dialog_text = "Are you sure you want to remove KEY from ACTION?"
[node name="OneInputMinimumDialog" type="AcceptDialog" parent="."]
title = "Cannot Remove"
size = Vector2i(398, 100)
[node name="AlreadyAssignedDialog" type="AcceptDialog" parent="."]
title = "Already Assigned"
size = Vector2i(398, 100)
[node name="ConfirmationDialog" type="ConfirmationDialog" parent="."]
dialog_text = "Are you sure you want to reset controls back to the defaults?"
[connection signal="button_clicked" from="VBoxContainer/InputMappingContainer/Tree" to="." method="_on_tree_button_clicked"]
[connection signal="item_activated" from="VBoxContainer/InputMappingContainer/Tree" to="." method="_on_tree_item_activated"]
[connection signal="pressed" from="VBoxContainer/InputMappingContainer/HBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"]
[connection signal="canceled" from="KeyAssignmentDialog" to="." method="_on_key_assignment_dialog_canceled"]
[connection signal="confirmed" from="KeyAssignmentDialog" to="." method="_on_key_assignment_dialog_confirmed"]
[connection signal="visibility_changed" from="KeyAssignmentDialog" to="KeyAssignmentDialog" method="_on_visibility_changed"]
[connection signal="focus_entered" from="KeyAssignmentDialog/VBoxContainer/InputTextEdit" to="KeyAssignmentDialog" method="_on_text_edit_focus_entered"]
[connection signal="focus_exited" from="KeyAssignmentDialog/VBoxContainer/InputTextEdit" to="KeyAssignmentDialog" method="_on_input_text_edit_focus_exited"]
[connection signal="gui_input" from="KeyAssignmentDialog/VBoxContainer/InputTextEdit" to="KeyAssignmentDialog" method="_on_input_text_edit_gui_input"]
[connection signal="confirmed" from="KeyDeletionDialog" to="." method="_on_key_deletion_dialog_confirmed"]
[connection signal="confirmed" from="ConfirmationDialog" to="." method="_on_confirmation_dialog_confirmed"]

@ -0,0 +1,88 @@
extends ConfirmationDialog
const LISTENING_TEXT : String = "Listening for input..."
const FOCUS_HERE_TEXT : String = "Focus here to assign inputs."
const CONFIRM_INPUT_TEXT : String = "Press again to confirm..."
const NO_INPUT_TEXT : String = "None"
var last_input_event : InputEvent
var last_input_text : String
var listening : bool = false
var confirming : bool = false
func _record_input_event(event : InputEvent):
last_input_text = InputEventHelper.get_text(event)
if last_input_text.is_empty():
return
last_input_event = event
%InputLabel.text = last_input_text
get_ok_button().disabled = false
func _is_recordable_input(event : InputEvent):
return event != null and \
(event is InputEventKey or \
event is InputEventMouseButton or \
event is InputEventJoypadButton or \
(event is InputEventJoypadMotion and \
abs(event.axis_value) > 0.5)) and \
event.is_pressed()
func _start_listening():
%InputTextEdit.placeholder_text = LISTENING_TEXT
listening = true
%DelayTimer.start()
func _stop_listening():
%InputTextEdit.placeholder_text = FOCUS_HERE_TEXT
listening = false
confirming = false
func _on_text_edit_focus_entered():
_start_listening.call_deferred()
func _on_input_text_edit_focus_exited():
_stop_listening()
func _focus_on_ok():
get_ok_button().grab_focus()
func _ready():
get_ok_button().focus_neighbor_top = ^"../../%InputTextEdit"
get_cancel_button().focus_neighbor_top = ^"../../%InputTextEdit"
func _input_matches_last(event : InputEvent) -> bool:
return last_input_text == InputEventHelper.get_text(event)
func _is_mouse_input(event : InputEvent) -> bool:
return event is InputEventMouse
func _input_confirms_choice(event : InputEvent) -> bool:
return confirming and not _is_mouse_input(event) and _input_matches_last(event)
func _should_process_input_event(event : InputEvent) -> bool:
return listening and _is_recordable_input(event) and %DelayTimer.is_stopped()
func _should_confirm_input_event(event : InputEvent) -> bool:
return not _is_mouse_input(event)
func _process_input_event(event : InputEvent):
if not _should_process_input_event(event):
return
if _input_confirms_choice(event):
confirming = false
_focus_on_ok.call_deferred()
return
_record_input_event(event)
if _should_confirm_input_event(event):
confirming = true
%DelayTimer.start()
%InputTextEdit.placeholder_text = CONFIRM_INPUT_TEXT
func _on_input_text_edit_gui_input(event):
%InputTextEdit.set_deferred("text", "")
_process_input_event(event)
func _on_visibility_changed():
if visible:
%InputLabel.text = NO_INPUT_TEXT
%InputTextEdit.grab_focus()

@ -0,0 +1,13 @@
class_name MasterOptionsMenu
extends Control
func _unhandled_input(event):
if not is_visible_in_tree():
return
if event.is_action_pressed("ui_page_down"):
$TabContainer.current_tab = ($TabContainer.current_tab+1) % $TabContainer.get_tab_count()
elif event.is_action_pressed("ui_page_up"):
if $TabContainer.current_tab == 0:
$TabContainer.current_tab = $TabContainer.get_tab_count()-1
else:
$TabContainer.current_tab = $TabContainer.current_tab-1

@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3 uid="uid://bvwl11s2p0hd"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/master_options_menu.gd" id="1_u08d5"]
[node name="MasterOptionsMenu" 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_u08d5")
[node name="TabContainer" type="TabContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
tab_alignment = 1

@ -0,0 +1,19 @@
[gd_scene load_steps=5 format=3 uid="uid://hmx6o472ropw"]
[ext_resource type="PackedScene" uid="uid://bvwl11s2p0hd" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/MasterOptionsMenu.tscn" id="1_uaidt"]
[ext_resource type="PackedScene" uid="uid://dp3rgqaehb3xu" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/Input/InputOptionsMenu.tscn" id="2_15wl6"]
[ext_resource type="PackedScene" uid="uid://c8vnncjwqcpab" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/Audio/AudioOptionsMenu.tscn" id="3_qg4me"]
[ext_resource type="PackedScene" uid="uid://b2numvphf2kau" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/Video/VideoOptionsMenu.tscn" id="4_1t848"]
[node name="MasterOptionsMenu" instance=ExtResource("1_uaidt")]
[node name="Controls" parent="TabContainer" index="1" instance=ExtResource("2_15wl6")]
layout_mode = 2
[node name="Audio" parent="TabContainer" index="2" instance=ExtResource("3_qg4me")]
visible = false
layout_mode = 2
[node name="Video" parent="TabContainer" index="3" instance=ExtResource("4_1t848")]
visible = false
layout_mode = 2

@ -0,0 +1,45 @@
class_name MiniOptionsMenu
extends Control
@onready var mute_control = %MuteControl
@onready var fullscreen_control = %FullscreenControl
@export var audio_control_scene : PackedScene
@export var hide_busses : Array[String]
func _on_bus_changed(bus_value : float, bus_iter : int):
AppSettings.set_bus_volume(bus_iter, bus_value)
func _add_audio_control(bus_name : String, bus_value : float, bus_iter : int):
if audio_control_scene == null or bus_name in hide_busses or bus_name.begins_with(AppSettings.SYSTEM_BUS_NAME_PREFIX):
return
var audio_control = audio_control_scene.instantiate()
%AudioControlContainer.call_deferred("add_child", audio_control)
if audio_control is OptionControl:
audio_control.option_section = OptionControl.OptionSections.AUDIO
audio_control.option_name = bus_name
audio_control.value = bus_value
audio_control.connect("setting_changed", _on_bus_changed.bind(bus_iter))
func _add_audio_bus_controls():
for bus_iter in AudioServer.bus_count:
var bus_name : String = AppSettings.get_audio_bus_name(bus_iter)
var linear : float = AppSettings.get_bus_volume(bus_iter)
_add_audio_control(bus_name, linear, bus_iter)
func _update_ui():
_add_audio_bus_controls()
mute_control.value = AppSettings.is_muted()
fullscreen_control.value = AppSettings.is_fullscreen(get_window())
func _sync_with_config() -> void:
_update_ui()
func _ready():
_sync_with_config()
func _on_mute_control_setting_changed(value):
AppSettings.set_mute(value)
func _on_fullscreen_control_setting_changed(value):
AppSettings.set_fullscreen_enabled(value, get_window())

@ -0,0 +1,51 @@
[gd_scene load_steps=5 format=3 uid="uid://vh1ucj2rfbby"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/mini_options_menu.gd" id="1_32vm2"]
[ext_resource type="PackedScene" uid="uid://cl416gdb1fgwr" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn" id="2_kpc65"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/capture_focus.gd" id="3_7qt1o"]
[ext_resource type="PackedScene" uid="uid://bsxh6v7j0257h" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/toggle_option_control.tscn" id="4_b20fb"]
[node name="MiniOptionsMenu" type="VBoxContainer"]
custom_minimum_size = Vector2(400, 260)
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 = -130.0
offset_right = 200.0
offset_bottom = 130.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 4
theme_override_constants/separation = 8
alignment = 1
script = ExtResource("1_32vm2")
audio_control_scene = ExtResource("2_kpc65")
[node name="AudioControlContainer" type="VBoxContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
script = ExtResource("3_7qt1o")
search_depth = 2
[node name="MuteControl" parent="." instance=ExtResource("4_b20fb")]
unique_name_in_owner = true
layout_mode = 2
option_name = "Mute"
option_section = 2
key = "Mute"
section = "AudioSettings"
[node name="FullscreenControl" parent="." instance=ExtResource("4_b20fb")]
unique_name_in_owner = true
layout_mode = 2
option_name = "Fullscreen"
option_section = 3
key = "FullscreenEnabled"
section = "VideoSettings"
[connection signal="setting_changed" from="MuteControl" to="." method="_on_mute_control_setting_changed"]
[connection signal="setting_changed" from="FullscreenControl" to="." method="_on_fullscreen_control_setting_changed"]

@ -0,0 +1,79 @@
@tool
class_name ListOptionControl
extends OptionControl
## Locks Option Titles from auto-updating when editing Option Values.
## Intentionally put first for initialization.
@export var lock_titles : bool = false
## Defines the list of possible values for the variable
## this option stores in the config file.
@export var option_values : Array :
set(value) :
option_values = value
_on_option_values_changed()
## Defines the list of options displayed to the user.
## Length should match with Option Values.
@export var option_titles : Array[String] :
set(value):
option_titles = value
if is_inside_tree():
_set_option_list(option_titles)
var custom_option_values : Array
func _on_option_values_changed():
if option_values.is_empty(): return
custom_option_values = option_values.duplicate()
_set_titles_from_values()
func _on_setting_changed(value):
if value < option_values.size():
super._on_setting_changed(option_values[value])
func _set_titles_from_values():
if lock_titles: return
var mapped_titles : Array[String] = []
for option_value in custom_option_values:
mapped_titles.append(_value_title_map(option_value))
option_titles = mapped_titles
func _value_title_map(value : Variant) -> String:
return "%s" % value
func _match_value_to_other(value : Variant, other : Variant) -> Variant:
# Primarily for when the editor saves floats as ints instead
if value is int and other is float:
return float(value)
if value is float and other is int:
return int(round(value))
return value
func set_value(value : Variant):
if option_values.is_empty(): return
if value == null:
return super.set_value(-1)
custom_option_values = option_values.duplicate()
value = _match_value_to_other(value, custom_option_values.front())
if value not in custom_option_values:
custom_option_values.append(value)
custom_option_values.sort()
_set_titles_from_values()
if value not in option_values:
disable_option(custom_option_values.find(value))
value = custom_option_values.find(value)
super.set_value(value)
func _set_option_list(option_titles_list : Array):
%OptionButton.clear()
for option_title in option_titles_list:
%OptionButton.add_item(option_title)
func disable_option(option_index : int, disabled : bool = true):
%OptionButton.set_item_disabled(option_index, disabled)
func _ready():
lock_titles = lock_titles
option_titles = option_titles
option_values = option_values
super._ready()

@ -0,0 +1,14 @@
[gd_scene load_steps=3 format=3 uid="uid://b6bl3n5mp3m1e"]
[ext_resource type="PackedScene" uid="uid://d7te75il06t7" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/option_control.tscn" id="1_blo3b"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/list_option_control.gd" id="2_kt4vl"]
[node name="OptionControl" instance=ExtResource("1_blo3b")]
script = ExtResource("2_kt4vl")
lock_titles = false
option_values = []
option_titles = []
[node name="OptionButton" type="OptionButton" parent="." index="1"]
unique_name_in_owner = true
layout_mode = 2

@ -0,0 +1,134 @@
@tool
class_name OptionControl
extends Control
signal setting_changed(value)
enum OptionSections{
NONE,
INPUT,
AUDIO,
VIDEO,
GAME,
APPLICATION,
CUSTOM,
}
const OptionSectionNames : Dictionary = {
OptionSections.NONE : "",
OptionSections.INPUT : AppSettings.INPUT_SECTION,
OptionSections.AUDIO : AppSettings.AUDIO_SECTION,
OptionSections.VIDEO : AppSettings.VIDEO_SECTION,
OptionSections.GAME : AppSettings.GAME_SECTION,
OptionSections.APPLICATION : AppSettings.APPLICATION_SECTION,
OptionSections.CUSTOM : AppSettings.CUSTOM_SECTION,
}
## Locks config names in case of issues with inherited scenes.
## Intentionally put first for initialization.
@export var lock_config_names : bool = false
## Defines text displayed to the user.
@export var option_name : String :
set(value):
var _update_config : bool = option_name.to_pascal_case() == key and not lock_config_names
option_name = value
if is_inside_tree():
%OptionLabel.text = "%s%s" % [option_name, label_suffix]
if _update_config:
key = option_name.to_pascal_case()
## Defines what section in the config file this option belongs under.
@export var option_section : OptionSections :
set(value):
var _update_config : bool = OptionSectionNames[option_section] == section and not lock_config_names
option_section = value
if _update_config:
section = OptionSectionNames[option_section]
@export_group("Config Names")
## Defines the key for this option variable in the config file.
@export var key : String
## Defines the section for this option variable in the config file.
@export var section : String
@export_group("Format")
@export var label_suffix : String = " :"
@export_group("Properties")
## Defines whether the option is editable, or only visible by the user.
@export var editable : bool = true : set = set_editable
## Defines what kind of variable this option stores in the config file.
@export var property_type : Variant.Type = TYPE_BOOL
## It is advised to use an external editor to set the default value in the scene file.
## Godot can experience a bug (caching issue?) that may undo changes.
var default_value
var _connected_nodes : Array
func _on_setting_changed(value):
Config.set_config(section, key, value)
setting_changed.emit(value)
func _get_setting(default : Variant = null) -> Variant:
return Config.get_config(section, key, default)
func _connect_option_inputs(node):
if node in _connected_nodes: return
if node is Button:
if node is OptionButton:
node.item_selected.connect(_on_setting_changed)
elif node is ColorPickerButton:
node.color_changed.connect(_on_setting_changed)
else:
node.toggled.connect(_on_setting_changed)
_connected_nodes.append(node)
if node is Range:
node.value_changed.connect(_on_setting_changed)
_connected_nodes.append(node)
if node is LineEdit or node is TextEdit:
node.text_changed.connect(_on_setting_changed)
_connected_nodes.append(node)
func set_value(value : Variant):
if value == null:
return
for node in get_children():
if node is Button:
if node is OptionButton:
node.select(value as int)
elif node is ColorPickerButton:
node.color = value as Color
else:
node.button_pressed = value as bool
if node is Range:
node.value = value as float
if node is LineEdit or node is TextEdit:
node.text = "%s" % value
func set_editable(value : bool = true):
editable = value
for node in get_children():
if node is Button:
node.disabled = !editable
if node is Slider or node is SpinBox or node is LineEdit or node is TextEdit:
node.editable = editable
func _ready():
lock_config_names = lock_config_names
option_section = option_section
option_name = option_name
property_type = property_type
default_value = default_value
set_value(_get_setting(default_value))
for child in get_children():
_connect_option_inputs(child)
child_entered_tree.connect(_connect_option_inputs)
func _set(property : StringName, value : Variant) -> bool:
if property == "value":
set_value(value)
return true
return false
func _get_property_list():
return [
{ "name": "value", "type": property_type, "usage": PROPERTY_USAGE_NONE},
{ "name": "default_value", "type": property_type}
]

@ -0,0 +1,17 @@
[gd_scene load_steps=2 format=3 uid="uid://d7te75il06t7"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/option_control.gd" id="1_jvl5q"]
[node name="OptionControl" type="HBoxContainer"]
custom_minimum_size = Vector2(0, 40)
offset_right = 400.0
offset_bottom = 40.0
script = ExtResource("1_jvl5q")
default_value = false
[node name="OptionLabel" type="Label" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = " :"
vertical_alignment = 1

@ -0,0 +1,19 @@
[gd_scene load_steps=2 format=3 uid="uid://cl416gdb1fgwr"]
[ext_resource type="PackedScene" uid="uid://d7te75il06t7" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/OptionControl/OptionControl.tscn" id="1_16hlr"]
[node name="OptionControl" instance=ExtResource("1_16hlr")]
custom_minimum_size = Vector2(0, 28)
offset_bottom = 28.0
property_type = 3
default_value = 1.0
[node name="HSlider" type="HSlider" parent="." index="1"]
custom_minimum_size = Vector2(256, 0)
layout_mode = 2
size_flags_vertical = 4
max_value = 1.0
step = 0.05
value = 1.0
tick_count = 11
ticks_on_borders = true

@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://bsxh6v7j0257h"]
[ext_resource type="PackedScene" uid="uid://d7te75il06t7" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/OptionControl/OptionControl.tscn" id="1_8rnmo"]
[node name="OptionControl" instance=ExtResource("1_8rnmo")]
[node name="CheckButton" type="CheckButton" parent="." index="1"]
layout_mode = 2

@ -0,0 +1,9 @@
@tool
class_name Vector2ListOptionControl
extends ListOptionControl
func _value_title_map(value : Variant) -> String:
if value is Vector2 or value is Vector2i:
return "%d x %d" % [value.x , value.y]
else:
return super._value_title_map(value)

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://c01ayjblhcg1t"]
[ext_resource type="PackedScene" uid="uid://b6bl3n5mp3m1e" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn" id="1_jqwiw"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.gd" id="2_w33vs"]
[node name="OptionControl" instance=ExtResource("1_jqwiw")]
script = ExtResource("2_w33vs")

@ -0,0 +1,34 @@
class_name VideoOptionsMenu
extends Control
func _preselect_resolution(window : Window):
%ResolutionControl.value = window.size
func _update_resolution_options_enabled(window : Window):
if OS.has_feature("web"):
%ResolutionControl.editable = false
%ResolutionControl.tooltip_text = "Disabled for web"
elif AppSettings.is_fullscreen(window):
%ResolutionControl.editable = false
%ResolutionControl.tooltip_text = "Disabled for fullscreen"
else:
%ResolutionControl.editable = true
%ResolutionControl.tooltip_text = "Select a screen size"
func _update_ui(window : Window):
%FullscreenControl.value = AppSettings.is_fullscreen(window)
_preselect_resolution(window)
_update_resolution_options_enabled(window)
func _ready():
var window : Window = get_window()
_update_ui(window)
window.connect("size_changed", _preselect_resolution.bind(window))
func _on_fullscreen_control_setting_changed(value):
var window : Window = get_window()
AppSettings.set_fullscreen_enabled(value, window)
_update_resolution_options_enabled(window)
func _on_resolution_control_setting_changed(value):
AppSettings.set_resolution(value, get_window())

@ -0,0 +1,44 @@
[gd_scene load_steps=5 format=3 uid="uid://b2numvphf2kau"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/video/video_options_menu.gd" id="1"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/capture_focus.gd" id="2_dgrai"]
[ext_resource type="PackedScene" uid="uid://bsxh6v7j0257h" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/toggle_option_control.tscn" id="3_uded6"]
[ext_resource type="PackedScene" uid="uid://c01ayjblhcg1t" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.tscn" id="4_gwtfq"]
[node name="Video" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_constants/margin_top = 24
theme_override_constants/margin_bottom = 24
script = ExtResource("1")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
size_flags_horizontal = 4
alignment = 1
script = ExtResource("2_dgrai")
search_depth = 2
[node name="FullscreenControl" parent="VBoxContainer" instance=ExtResource("3_uded6")]
unique_name_in_owner = true
layout_mode = 2
option_name = "Fullscreen"
option_section = 3
key = "FullscreenEnabled"
section = "VideoSettings"
[node name="ResolutionControl" parent="VBoxContainer" instance=ExtResource("4_gwtfq")]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Select a screen size"
option_values = [Vector2i(640, 360), Vector2i(960, 540), Vector2i(1024, 576), Vector2i(1280, 720), Vector2i(1600, 900), Vector2i(1920, 1080), Vector2i(2048, 1152), Vector2i(2560, 1440), Vector2i(3200, 1800), Vector2i(3840, 2160)]
option_titles = Array[String](["640 x 360", "960 x 540", "1024 x 576", "1280 x 720", "1600 x 900", "1920 x 1080", "2048 x 1152", "2560 x 1440", "3200 x 1800", "3840 x 2160"])
option_name = "Resolution"
option_section = 3
key = "ScreenResolution"
section = "VideoSettings"
[connection signal="setting_changed" from="VBoxContainer/FullscreenControl" to="." method="_on_fullscreen_control_setting_changed"]
[connection signal="setting_changed" from="VBoxContainer/ResolutionControl" to="." method="_on_resolution_control_setting_changed"]

@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://bkcsjsk2ciff"]
[node name="BackgroundMusicPlayer" type="AudioStreamPlayer"]
process_mode = 3
autoplay = true
bus = &"Music"

@ -0,0 +1,27 @@
class_name AppLog
extends Node
## Logs application version and opened count through [Config].
const APP_LOG_SECTION = "AppLog"
const APP_OPENED = "AppOpened"
const FIRST_VERSION_OPENED = "FirstVersionOpened"
const LAST_VERSION_OPENED = "LastVersionOpened"
const UNKNOWN_VERSION = "unknown"
static func app_opened() -> void:
var total_app_opened = Config.get_config(APP_LOG_SECTION, APP_OPENED, 0)
total_app_opened += 1
Config.set_config(APP_LOG_SECTION, APP_OPENED, total_app_opened)
static func set_first_version_opened(value : String) -> void:
var first_version_opened = Config.get_config(APP_LOG_SECTION, FIRST_VERSION_OPENED, UNKNOWN_VERSION)
if first_version_opened != UNKNOWN_VERSION:
return
Config.set_config(APP_LOG_SECTION, FIRST_VERSION_OPENED, value)
static func set_last_version_opened(value : String) -> void:
Config.set_config(APP_LOG_SECTION, LAST_VERSION_OPENED, value)
static func version_opened(version : String) -> void:
set_first_version_opened(version)
set_last_version_opened(version)

@ -0,0 +1,158 @@
class_name AppSettings
extends Node
## Interface to read/write general application settings through [Config].
const INPUT_SECTION = &'InputSettings'
const AUDIO_SECTION = &'AudioSettings'
const VIDEO_SECTION = &'VideoSettings'
const GAME_SECTION = &'GameSettings'
const APPLICATION_SECTION = &'ApplicationSettings'
const CUSTOM_SECTION = &'CustomSettings'
const FULLSCREEN_ENABLED = &'FullscreenEnabled'
const SCREEN_RESOLUTION = &'ScreenResolution'
const MUTE_SETTING = &'Mute'
const MASTER_BUS_INDEX = 0
const SYSTEM_BUS_NAME_PREFIX = "_"
# Input
static var default_action_events : Dictionary
static var initial_bus_volumes : Array
static func get_config_input_events(action_name : String, default = null) -> Array:
return Config.get_config(INPUT_SECTION, action_name, default)
static func set_config_input_events(action_name : String, inputs : Array) -> void:
Config.set_config(INPUT_SECTION, action_name, inputs)
static func _clear_config_input_events():
Config.erase_section(INPUT_SECTION)
static func remove_action_input_event(action_name : String, input_event : InputEvent):
InputMap.action_erase_event(action_name, input_event)
var action_events : Array[InputEvent] = InputMap.action_get_events(action_name)
var config_events : Array = get_config_input_events(action_name, action_events)
config_events.erase(input_event)
set_config_input_events(action_name, config_events)
static func set_input_from_config(action_name : String):
var action_events : Array[InputEvent] = InputMap.action_get_events(action_name)
var config_events = get_config_input_events(action_name, action_events)
if config_events == action_events:
return
if config_events.is_empty():
Config.erase_section_key(INPUT_SECTION, action_name)
return
InputMap.action_erase_events(action_name)
for config_event in config_events:
if config_event not in action_events:
InputMap.action_add_event(action_name, config_event)
static func _get_action_names() -> Array[StringName]:
return InputMap.get_actions()
static func _get_custom_action_names() -> Array[StringName]:
var callable_filter := func(action_name): return not action_name.begins_with("ui_")
var action_list := _get_action_names()
return action_list.filter(callable_filter)
static func get_action_names(built_in_actions : bool = false) -> Array[StringName]:
if built_in_actions:
return _get_action_names()
else:
return _get_custom_action_names()
static func reset_to_default_inputs() -> void:
_clear_config_input_events()
for action_name in default_action_events:
InputMap.action_erase_events(action_name)
var input_events = default_action_events[action_name]
for input_event in input_events:
InputMap.action_add_event(action_name, input_event)
static func set_default_inputs() -> void:
var action_list : Array[StringName] = _get_action_names()
for action_name in action_list:
default_action_events[action_name] = InputMap.action_get_events(action_name)
static func set_inputs_from_config() -> void:
var action_list : Array[StringName] = _get_action_names()
for action_name in action_list:
set_input_from_config(action_name)
# Audio
static func get_bus_volume(bus_index : int) -> float:
var initial_linear = 1.0
if initial_bus_volumes.size() > bus_index:
initial_linear = initial_bus_volumes[bus_index]
var linear = db_to_linear(AudioServer.get_bus_volume_db(bus_index))
linear /= initial_linear
return linear
static func set_bus_volume(bus_index : int, linear : float) -> void:
var initial_linear = 1.0
if initial_bus_volumes.size() > bus_index:
initial_linear = initial_bus_volumes[bus_index]
linear *= initial_linear
AudioServer.set_bus_volume_db(bus_index, linear_to_db(linear))
static func is_muted() -> bool:
return AudioServer.is_bus_mute(MASTER_BUS_INDEX)
static func set_mute(mute_flag : bool) -> void:
AudioServer.set_bus_mute(MASTER_BUS_INDEX, mute_flag)
static func get_audio_bus_name(bus_iter : int) -> String:
return AudioServer.get_bus_name(bus_iter)
static func set_audio_from_config():
for bus_iter in AudioServer.bus_count:
var bus_name : String = get_audio_bus_name(bus_iter)
var bus_volume : float = get_bus_volume(bus_iter)
initial_bus_volumes.append(bus_volume)
bus_volume = Config.get_config(AUDIO_SECTION, bus_name, bus_volume)
if is_nan(bus_volume):
bus_volume = 1.0
Config.set_config(AUDIO_SECTION, bus_name, bus_volume)
set_bus_volume(bus_iter, bus_volume)
var mute_audio_flag : bool = is_muted()
mute_audio_flag = Config.get_config(AUDIO_SECTION, MUTE_SETTING, mute_audio_flag)
set_mute(mute_audio_flag)
# Video
static func set_fullscreen_enabled(value : bool, window : Window) -> void:
window.mode = Window.MODE_EXCLUSIVE_FULLSCREEN if (value) else Window.MODE_WINDOWED
static func set_resolution(value : Vector2i, window : Window) -> void:
if value.x == 0 or value.y == 0:
return
window.size = value
static func is_fullscreen(window : Window) -> bool:
return (window.mode == Window.MODE_EXCLUSIVE_FULLSCREEN) or (window.mode == Window.MODE_FULLSCREEN)
static func get_resolution(window : Window) -> Vector2i:
var current_resolution : Vector2i = window.size
current_resolution = Config.get_config(VIDEO_SECTION, SCREEN_RESOLUTION, current_resolution)
return current_resolution
static func set_video_from_config(window : Window) -> void:
var fullscreen_enabled : bool = is_fullscreen(window)
fullscreen_enabled = Config.get_config(VIDEO_SECTION, FULLSCREEN_ENABLED, fullscreen_enabled)
set_fullscreen_enabled(fullscreen_enabled, window)
if not (fullscreen_enabled or OS.has_feature("web")):
var current_resolution : Vector2i = get_resolution(window)
set_resolution(current_resolution, window)
# All
static func set_from_config() -> void:
set_default_inputs()
set_inputs_from_config()
set_audio_from_config()
static func set_from_config_and_window(window : Window) -> void:
set_from_config()
set_video_from_config(window)

@ -0,0 +1,49 @@
class_name CaptureFocus
extends Container
## Node that captures UI focus for joypad users.
##
## This script assists with capturing UI focus for joypad users when
## opening, closing, or switching between menus.
## When attached to a node, it will check if it was changed to visible
## and a joypad is being used. If both are true, it will capture focus
## on the first eligible node in its scene tree.
## Hierarchical depth to search in the scene tree.
@export var search_depth : int = 1
@export var lock : bool = false :
set(value):
lock = value
if not lock:
update_focus()
func _focus_first_search(control_node : Control, levels : int = 1):
if control_node == null or !control_node.is_visible_in_tree():
return false
if control_node.focus_mode == FOCUS_ALL:
control_node.grab_focus()
return true
if levels < 1:
return false
var children = control_node.get_children()
for child in children:
if _focus_first_search(child, levels - 1):
return true
func focus_first():
_focus_first_search(self, search_depth)
func update_focus():
if lock : return
if _is_visible_and_joypad():
focus_first()
func _is_visible_and_joypad():
return is_visible_in_tree() and Input.get_connected_joypads().size() > 0
func _on_visibility_changed():
call_deferred("update_focus")
func _ready():
if is_inside_tree():
update_focus()
connect("visibility_changed", _on_visibility_changed)

@ -0,0 +1,58 @@
class_name Config
extends Node
## Interface for a single configuration file through [ConfigFile].
const CONFIG_FILE_LOCATION := "user://config.cfg"
const DEFAULT_CONFIG_FILE_LOCATION := "res://default_config.cfg"
static var config_file : ConfigFile
static func _init():
load_config_file()
static func _save_config_file() -> void:
var save_error : int = config_file.save(CONFIG_FILE_LOCATION)
if save_error:
print("save config file failed with error %d" % save_error)
static func load_config_file() -> void:
if config_file != null:
return
config_file = ConfigFile.new()
var load_error : int = config_file.load(CONFIG_FILE_LOCATION)
if load_error:
var load_default_error : int = config_file.load(DEFAULT_CONFIG_FILE_LOCATION)
if load_default_error:
print("loading default config file failed with error %d" % load_default_error)
var save_error : int = config_file.save(CONFIG_FILE_LOCATION)
if save_error:
print("save config file failed with error %d" % save_error)
static func set_config(section: String, key: String, value) -> void:
load_config_file()
config_file.set_value(section, key, value)
_save_config_file()
static func get_config(section: String, key: String, default = null) -> Variant:
load_config_file()
return config_file.get_value(section, key, default)
static func has_section(section: String):
load_config_file()
return config_file.has_section(section)
static func erase_section(section: String):
if has_section(section):
config_file.erase_section(section)
_save_config_file()
static func erase_section_key(section: String, key: String):
if has_section(section):
config_file.erase_section_key(section, key)
_save_config_file()
static func get_section_keys(section: String):
load_config_file()
if config_file.has_section(section):
return config_file.get_section_keys(section)
return PackedStringArray()

@ -0,0 +1,70 @@
class_name InputEventHelper
extends Node
## Helper class for organizing constants related to [InputEvent].
const JOYSTICK_LEFT_NAME = "Left Gamepad Joystick"
const JOYSTICK_RIGHT_NAME = "Right Gamepad Joystick"
const D_PAD_NAME = "Gamepad D-pad"
const JOY_BUTTON_NAMES : Dictionary = {
JOY_BUTTON_A: "A Gamepad Button",
JOY_BUTTON_B: "B Gamepad Button",
JOY_BUTTON_X: "X Gamepad Button",
JOY_BUTTON_Y: "Y Gamepad Button",
JOY_BUTTON_LEFT_SHOULDER: "Left Shoulder Gamepad Button",
JOY_BUTTON_RIGHT_SHOULDER: "Right Shoulder Gamepad Button",
JOY_BUTTON_LEFT_STICK: "Left Stick Gamepad Button",
JOY_BUTTON_RIGHT_STICK: "Right Stick Gamepad Button",
JOY_BUTTON_START : "Start Gamepad Button",
JOY_BUTTON_GUIDE : "Guide Gamepad Button",
JOY_BUTTON_BACK : "Back Gamepad Button",
JOY_BUTTON_DPAD_UP : D_PAD_NAME + " Up",
JOY_BUTTON_DPAD_DOWN : D_PAD_NAME + " Down",
JOY_BUTTON_DPAD_LEFT : D_PAD_NAME + " Left",
JOY_BUTTON_DPAD_RIGHT : D_PAD_NAME + " Right",
}
const JOY_AXIS_NAMES : Dictionary = {
JOY_AXIS_TRIGGER_LEFT: "Left Trigger Gamepad Button",
JOY_AXIS_TRIGGER_RIGHT: "Right Trigger Gamepad Button",
}
static func _display_server_supports_keycode_from_physical():
return OS.has_feature("windows") or OS.has_feature("macos") or OS.has_feature("linux")
static func get_text(event : InputEvent) -> String:
if event is InputEventJoypadButton:
if event.button_index in JOY_BUTTON_NAMES:
return JOY_BUTTON_NAMES[event.button_index]
elif event is InputEventJoypadMotion:
var full_string := ""
var direction_string := ""
var is_right_or_down : bool = event.axis_value > 0.0
if event.axis in JOY_AXIS_NAMES:
return JOY_AXIS_NAMES[event.axis]
match(event.axis):
JOY_AXIS_LEFT_X:
full_string = JOYSTICK_LEFT_NAME
direction_string = "Right" if is_right_or_down else "Left"
JOY_AXIS_LEFT_Y:
full_string = JOYSTICK_LEFT_NAME
direction_string = "Down" if is_right_or_down else "Up"
JOY_AXIS_RIGHT_X:
full_string = JOYSTICK_RIGHT_NAME
direction_string = "Right" if is_right_or_down else "Left"
JOY_AXIS_RIGHT_Y:
full_string = JOYSTICK_RIGHT_NAME
direction_string = "Down" if is_right_or_down else "Up"
full_string += " " + direction_string
return full_string
elif event is InputEventKey:
var keycode : Key = event.get_physical_keycode()
if keycode:
keycode = event.get_physical_keycode_with_modifiers()
else:
keycode = event.get_keycode_with_modifiers()
if _display_server_supports_keycode_from_physical():
keycode = DisplayServer.keyboard_get_keycode_from_physical(keycode)
return OS.get_keycode_string(keycode)
return event.as_text()

@ -0,0 +1,183 @@
class_name MusicController
extends Node
## Controller for music playback across scenes.
##
## This node persistently checks for stream players added to the scene tree.
## It detects stream players that match the audio bus and have autoplay on.
## It then reparents the stream players to itself, and handles blending.
## The expected use-case is to attach this script to an autoloaded scene.
const BLEND_BUS_PREFIX : String = "Blend"
const MAX_DEPTH = 16
const MINIMUM_VOLUME_DB = -80
## Detect stream players with matching audio bus.
@export var audio_bus : StringName = &"Music"
@export_group("Blending")
@export var fade_out_duration : float = 0.0 :
set(value):
fade_out_duration = value
if fade_out_duration < 0:
fade_out_duration = 0
@export var fade_in_duration : float = 0.0 :
set(value):
fade_in_duration = value
if fade_in_duration < 0:
fade_in_duration = 0
## Matched stream players with no stream set will stop current playback.
@export var empty_streams_stop_player : bool = true
var music_stream_player : AudioStreamPlayer
var blend_audio_bus : StringName
var blend_audio_bus_idx : int
func fade_out( duration : float = 0.0 ):
if not is_zero_approx(duration):
music_stream_player.bus = audio_bus
var tween = create_tween()
tween.tween_property(music_stream_player, "volume_db", MINIMUM_VOLUME_DB, duration)
return tween
func _set_sub_audio_volume_db(sub_volume_db : float):
AudioServer.set_bus_volume_db(blend_audio_bus_idx, sub_volume_db)
func fade_in( duration : float = 0.0 ):
if not is_zero_approx(duration):
music_stream_player.bus = blend_audio_bus
AudioServer.set_bus_volume_db(blend_audio_bus_idx, MINIMUM_VOLUME_DB)
var tween = create_tween()
tween.tween_method(_set_sub_audio_volume_db, MINIMUM_VOLUME_DB, 0, duration)
return tween
func blend_to( target_volume_db : float, duration : float = 0.0 ):
if not is_zero_approx(duration):
var tween = create_tween()
tween.tween_property(music_stream_player, "volume_db", target_volume_db, duration)
return tween
music_stream_player.volume_db = target_volume_db
func stop():
if music_stream_player == null:
return
music_stream_player.stop()
func play( playback_position : float = 0.0 ):
if music_stream_player == null:
return
if is_zero_approx(playback_position) and not music_stream_player.playing:
music_stream_player.play()
else:
music_stream_player.play(playback_position)
func _fade_out_and_free():
if music_stream_player == null:
return
var stream_player = music_stream_player
var tween = fade_out( fade_out_duration )
if tween != null:
await( tween.finished )
stream_player.queue_free()
func _play_and_fade_in():
play()
fade_in( fade_in_duration )
func _is_matching_stream( stream_player : AudioStreamPlayer ) -> bool:
if stream_player.bus != audio_bus:
return false
if music_stream_player == null:
return false
return music_stream_player.stream == stream_player.stream
func _connect_stream_on_tree_exiting( stream_player : AudioStreamPlayer ):
if not stream_player.tree_exiting.is_connected(_on_removed_music_player.bind(stream_player)):
stream_player.tree_exiting.connect(_on_removed_music_player.bind(stream_player))
func _blend_and_remove_stream_player( stream_player : AudioStreamPlayer ):
var playback_position := music_stream_player.get_playback_position() + AudioServer.get_time_since_last_mix()
var old_stream_player = music_stream_player
music_stream_player = stream_player
music_stream_player.bus = blend_audio_bus
play(playback_position)
old_stream_player.stop()
old_stream_player.queue_free()
_connect_stream_on_tree_exiting(music_stream_player)
func _blend_and_connect_stream_player( stream_player : AudioStreamPlayer ):
stream_player.bus = blend_audio_bus
_fade_out_and_free()
music_stream_player = stream_player
_play_and_fade_in()
_connect_stream_on_tree_exiting(music_stream_player)
func play_stream_player( stream_player : AudioStreamPlayer ):
if stream_player == music_stream_player : return
if stream_player.stream == null and not empty_streams_stop_player:
return
if _is_matching_stream(stream_player) :
_blend_and_remove_stream_player(stream_player)
else:
_blend_and_connect_stream_player(stream_player)
func get_stream_player( audio_stream : AudioStream ) -> AudioStreamPlayer:
var stream_player := AudioStreamPlayer.new()
stream_player.stream = audio_stream
stream_player.bus = audio_bus
add_child(stream_player)
return stream_player
func play_stream( audio_stream : AudioStream ) -> AudioStreamPlayer:
var stream_player := get_stream_player(audio_stream)
stream_player.play.call_deferred()
play_stream_player( stream_player )
return stream_player
func _clone_music_player( stream_player : AudioStreamPlayer ):
var playback_position := stream_player.get_playback_position() + AudioServer.get_time_since_last_mix()
var audio_stream := stream_player.stream
music_stream_player = get_stream_player(audio_stream)
music_stream_player.volume_db = stream_player.volume_db
music_stream_player.max_polyphony = stream_player.max_polyphony
music_stream_player.pitch_scale = stream_player.pitch_scale
music_stream_player.play.call_deferred(playback_position)
func _reparent_music_player( stream_player : AudioStreamPlayer ):
var playback_position := stream_player.get_playback_position() + AudioServer.get_time_since_last_mix()
stream_player.owner = null
stream_player.reparent.call_deferred(self)
stream_player.play.call_deferred(playback_position)
func _node_matches_checks( node : Node ) -> bool:
return node is AudioStreamPlayer and node.autoplay and node.bus == audio_bus
func _on_removed_music_player( node: Node ) -> void:
if music_stream_player == node:
if node.owner == null:
_clone_music_player(node)
else:
_reparent_music_player(node)
if node.tree_exiting.is_connected(_on_removed_music_player.bind(node)):
node.tree_exiting.disconnect(_on_removed_music_player.bind(node))
func _on_added_music_player( node: Node ) -> void:
if node == music_stream_player : return
if not (_node_matches_checks(node)) : return
play_stream_player(node)
func _enter_tree() -> void:
AudioServer.add_bus()
blend_audio_bus_idx = AudioServer.bus_count - 1
blend_audio_bus = AppSettings.SYSTEM_BUS_NAME_PREFIX + BLEND_BUS_PREFIX + audio_bus
AudioServer.set_bus_send(blend_audio_bus_idx, audio_bus)
AudioServer.set_bus_name(blend_audio_bus_idx, blend_audio_bus)
var tree_node = get_tree()
if not tree_node.node_added.is_connected(_on_added_music_player):
tree_node.node_added.connect(_on_added_music_player)
func _exit_tree():
var tree_node = get_tree()
if tree_node.node_added.is_connected(_on_added_music_player):
tree_node.node_added.disconnect(_on_added_music_player)

@ -0,0 +1,167 @@
class_name UISoundController
extends Node
## Controller for managing all UI sounds in a scene from one place.
##
## This node manages all of the UI sounds under the provided node path.
## When attached just below the root node of a scene tree, it will manage
## all of the UI sounds in that scene.
const MAX_DEPTH = 16
@export var root_path : NodePath = ^".."
@export var audio_bus : StringName = &"SFX"
## Continually check any new nodes added to the scene tree.
@export var persistent : bool = true :
set(value):
persistent = value
_update_persistent_signals()
@export_group("Button Sounds")
@export var button_hovered : AudioStream
@export var button_focused : AudioStream
@export var button_pressed : AudioStream
@export_group("TabBar Sounds")
@export var tab_hovered : AudioStream
@export var tab_changed : AudioStream
@export var tab_selected : AudioStream
@export_group("Slider Sounds")
@export var slider_hovered : AudioStream
@export var slider_focused : AudioStream
@export var slider_drag_started : AudioStream
@export var slider_drag_ended : AudioStream
@export_group("LineEdit Sounds")
@export var line_hovered : AudioStream
@export var line_focused : AudioStream
@export var line_text_changed : AudioStream
@export var line_text_submitted : AudioStream
@export var line_text_change_rejected : AudioStream
@onready var root_node : Node = get_node(root_path)
var button_hovered_player : AudioStreamPlayer
var button_focused_player : AudioStreamPlayer
var button_pressed_player : AudioStreamPlayer
var tab_hovered_player : AudioStreamPlayer
var tab_changed_player : AudioStreamPlayer
var tab_selected_player : AudioStreamPlayer
var slider_hovered_player : AudioStreamPlayer
var slider_focused_player : AudioStreamPlayer
var slider_drag_started_player : AudioStreamPlayer
var slider_drag_ended_player : AudioStreamPlayer
var line_hovered_player : AudioStreamPlayer
var line_focused_player : AudioStreamPlayer
var line_text_changed_player : AudioStreamPlayer
var line_text_submitted_player : AudioStreamPlayer
var line_text_change_rejected_player : AudioStreamPlayer
func _update_persistent_signals():
if not is_inside_tree():
return
var tree_node = get_tree()
if persistent:
if not tree_node.node_added.is_connected(connect_ui_sounds):
tree_node.node_added.connect(connect_ui_sounds)
else:
if tree_node.node_added.is_connected(connect_ui_sounds):
tree_node.node_added.disconnect(connect_ui_sounds)
func _build_stream_player(stream : AudioStream, stream_name : String = ""):
var stream_player : AudioStreamPlayer
if stream != null:
stream_player = AudioStreamPlayer.new()
stream_player.stream = stream
stream_player.bus = audio_bus
stream_player.name = stream_name + "AudioStreamPlayer"
add_child(stream_player)
return stream_player
func _build_button_stream_players():
button_hovered_player = _build_stream_player(button_hovered, "ButtonHovered")
button_focused_player = _build_stream_player(button_focused, "ButtonFocused")
button_pressed_player = _build_stream_player(button_pressed, "ButtonClicked")
func _build_tab_stream_players():
tab_hovered_player = _build_stream_player(tab_hovered, "TabHovered")
tab_changed_player = _build_stream_player(tab_changed, "TabChanged")
tab_selected_player = _build_stream_player(tab_selected, "TabSelected")
func _build_slider_stream_players():
slider_hovered_player = _build_stream_player(slider_hovered, "SliderHovered")
slider_focused_player = _build_stream_player(slider_focused, "SliderFocused")
slider_drag_started_player = _build_stream_player(slider_drag_started, "SliderDragStarted")
slider_drag_ended_player = _build_stream_player(slider_drag_ended, "SliderDragEnded")
func _build_line_stream_players():
line_hovered_player = _build_stream_player(line_hovered, "LineHovered")
line_focused_player = _build_stream_player(line_focused, "LineFocused")
line_text_changed_player = _build_stream_player(line_text_changed, "LineTextChanged")
line_text_submitted_player = _build_stream_player(line_text_submitted, "LineTextSubmitted")
line_text_change_rejected_player = _build_stream_player(line_text_change_rejected, "LineTextChangeRejected")
func _build_all_stream_players():
_build_button_stream_players()
_build_tab_stream_players()
_build_slider_stream_players()
_build_line_stream_players()
func _play_stream(stream_player : AudioStreamPlayer):
if not stream_player.is_inside_tree():
return
stream_player.play()
func _tab_event_play_stream(_tab_idx : int, stream_player : AudioStreamPlayer):
_play_stream(stream_player)
func _slider_drag_ended_play_stream(_value_changed : bool, stream_player : AudioStreamPlayer):
_play_stream(stream_player)
func _line_event_play_stream(_new_text : String, stream_player : AudioStreamPlayer):
_play_stream(stream_player)
func _connect_stream_player(node : Node, stream_player : AudioStreamPlayer, signal_name : StringName, callable : Callable) -> void:
if stream_player != null and not node.is_connected(signal_name, callable.bind(stream_player)):
node.connect(signal_name, callable.bind(stream_player))
func connect_ui_sounds(node: Node) -> void:
if node is Button:
_connect_stream_player(node, button_hovered_player, &"mouse_entered", _play_stream)
_connect_stream_player(node, button_focused_player, &"focus_entered", _play_stream)
_connect_stream_player(node, button_pressed_player, &"pressed", _play_stream)
elif node is TabBar:
_connect_stream_player(node, tab_hovered_player, &"tab_hovered", _tab_event_play_stream)
_connect_stream_player(node, tab_changed_player, &"tab_changed", _tab_event_play_stream)
_connect_stream_player(node, tab_selected_player, &"tab_selected", _tab_event_play_stream)
elif node is Slider:
_connect_stream_player(node, slider_hovered_player, &"mouse_entered", _play_stream)
_connect_stream_player(node, slider_focused_player, &"focus_entered", _play_stream)
_connect_stream_player(node, slider_drag_started_player, &"drag_started", _play_stream)
_connect_stream_player(node, slider_drag_ended_player, &"drag_ended", _slider_drag_ended_play_stream)
elif node is LineEdit:
_connect_stream_player(node, line_hovered_player, &"mouse_entered", _play_stream)
_connect_stream_player(node, line_focused_player, &"focus_entered", _play_stream)
_connect_stream_player(node, line_text_changed_player, &"text_changed", _line_event_play_stream)
_connect_stream_player(node, line_text_submitted_player, &"text_submitted", _line_event_play_stream)
_connect_stream_player(node, line_text_change_rejected_player, &"text_change_rejected", _line_event_play_stream)
func _recursive_connect_ui_sounds(current_node: Node, current_depth : int = 0) -> void:
if current_depth >= MAX_DEPTH:
return
for node in current_node.get_children():
connect_ui_sounds(node)
_recursive_connect_ui_sounds(node, current_depth + 1)
func _ready() -> void:
_build_all_stream_players()
_recursive_connect_ui_sounds(root_node)
persistent = persistent
func _exit_tree():
var tree_node = get_tree()
if tree_node.node_added.is_connected(connect_ui_sounds):
tree_node.node_added.disconnect(connect_ui_sounds)

@ -0,0 +1,72 @@
keys,en,fr
___ MAIN MENU,,
Title,Title,Titre
Subtitle,Subtitle,Sous-titre
Play,Play,Jouer
Options,Options,Options
Credits,Credits,Crédits
Exit,Exit,Quitter
___ LOADING SCREEN,,
Loading...,Loading...,Chargement...
___ DIALOGS IN GAME,,
You lose.,You lose.,Vous avez perdu.
You won!,You won!,Vous avez gagné !
Thanks for playing!,Thanks for playing!,Merci d'avoir joué !
Exit Game,Exit Game,Quitter le jeu
Main Menu,Main Menu,Menu principal
Restart,Restart,Recommencer
Continue,Continue,Continuer
Menu,Menu,Menu
Please Confirm...,Please Confirm...,Veuillez confirmer...
Go back to main menu?,Go back to main menu?,Retourner au menu principal ?
Quit the game?,Quit the game?,Quitter le jeu ?
Cancel,Cancel,Annuler
OK,OK,OK
___ OPTIONS MENU,,
Controls,Controls,Contrôles
Mouse Sensitivity :,Mouse Sensitivity :,Sensibilité souris :
Actions & Inputs,Actions & Inputs,Actions et contrôles
Add,Add,Ajouter
Remove,Remove,Enlever
Assign Key for {action},Assign Key for {action},Choisir le contrôle pour {action}
Listening for input...,Listening for input...,Appuyez sur un bouton...
Press again to confirm...,Press again to confirm...,Appuyez encore pour confirmer...
Focus here to assign inputs.,Focus here to assign inputs.,Mettez le focus ici pour choisir le contrôle.
Already Assigned,Already Assigned,Déjà utilisé
{key} already assigned to {action}.,{key} already assigned to {action}.,{key} est déjà utilisé pour {action}.
Remove Key for {action},Remove Key for {action},Supprimer le contrôle pour {action}
Are you sure you want to remove {key} from {action}?,Are you sure you want to remove {key} from {action}?,Êtes-vous sûr de vouloir supprimer {key} pour {action} ?
Reset,Reset,Réinitialiser
Audio,Audio,Audio
Master :,Master :,Principal :
Music :,Music :,Musique :
SFX :,SFX :,Effets :
Mute :,Mute :,Silencieux :
Video,Video,Vidéo
Fullscreen :,Fullscreen :,Plein écran :
Resolution :,Resolution :,Résolution :
Anti-Aliasing :,Anti-Aliasing :,Anticrénelage :
Disabled (Fastest),Disabled (Fastest),Désactivé (Plus rapide)
8x (Slowest),8x (Slowest),8x (Plus lent)
Camera Shake :,Camera Shake :,Secousse Caméra :
Normal,Normal,Normale
Reduced,Reduced,Réduite
Minimal,Minimal,Minimum
None,None,Aucune
Game,Game,Jeu
Reset Game :,Reset Game :,Réinitialiser le jeu :
Do you want to reset your game data?,Do you want to reset your game data?,Voulez-vous réinitialiser votre partie ?
Back,Back,Retour
1 keys en fr
2 ___ MAIN MENU
3 Title Title Titre
4 Subtitle Subtitle Sous-titre
5 Play Play Jouer
6 Options Options Options
7 Credits Credits Crédits
8 Exit Exit Quitter
9 ___ LOADING SCREEN
10 Loading... Loading... Chargement...
11 ___ DIALOGS IN GAME
12 You lose. You lose. Vous avez perdu.
13 You won! You won! Vous avez gagné !
14 Thanks for playing! Thanks for playing! Merci d'avoir joué !
15 Exit Game Exit Game Quitter le jeu
16 Main Menu Main Menu Menu principal
17 Restart Restart Recommencer
18 Continue Continue Continuer
19 Menu Menu Menu
20 Please Confirm... Please Confirm... Veuillez confirmer...
21 Go back to main menu? Go back to main menu? Retourner au menu principal ?
22 Quit the game? Quit the game? Quitter le jeu ?
23 Cancel Cancel Annuler
24 OK OK OK
25 ___ OPTIONS MENU
26 Controls Controls Contrôles
27 Mouse Sensitivity : Mouse Sensitivity : Sensibilité souris :
28 Actions & Inputs Actions & Inputs Actions et contrôles
29 Add Add Ajouter
30 Remove Remove Enlever
31 Assign Key for {action} Assign Key for {action} Choisir le contrôle pour {action}
32 Listening for input... Listening for input... Appuyez sur un bouton...
33 Press again to confirm... Press again to confirm... Appuyez encore pour confirmer...
34 Focus here to assign inputs. Focus here to assign inputs. Mettez le focus ici pour choisir le contrôle.
35 Already Assigned Already Assigned Déjà utilisé
36 {key} already assigned to {action}. {key} already assigned to {action}. {key} est déjà utilisé pour {action}.
37 Remove Key for {action} Remove Key for {action} Supprimer le contrôle pour {action}
38 Are you sure you want to remove {key} from {action}? Are you sure you want to remove {key} from {action}? Êtes-vous sûr de vouloir supprimer {key} pour {action} ?
39 Reset Reset Réinitialiser
40 Audio Audio Audio
41 Master : Master : Principal :
42 Music : Music : Musique :
43 SFX : SFX : Effets :
44 Mute : Mute : Silencieux :
45 Video Video Vidéo
46 Fullscreen : Fullscreen : Plein écran :
47 Resolution : Resolution : Résolution :
48 Anti-Aliasing : Anti-Aliasing : Anticrénelage :
49 Disabled (Fastest) Disabled (Fastest) Désactivé (Plus rapide)
50 8x (Slowest) 8x (Slowest) 8x (Plus lent)
51 Camera Shake : Camera Shake : Secousse Caméra :
52 Normal Normal Normale
53 Reduced Reduced Réduite
54 Minimal Minimal Minimum
55 None None Aucune
56 Game Game Jeu
57 Reset Game : Reset Game : Réinitialiser le jeu :
58 Do you want to reset your game data? Do you want to reset your game data? Voulez-vous réinitialiser votre partie ?
59 Back Back Retour

@ -0,0 +1,17 @@
[remap]
importer="csv_translation"
type="Translation"
uid="uid://i6ihop1vp2ei"
[deps]
files=["res://addons/maaacks_menus_template/base/translations/menus_translations.en.translation", "res://addons/maaacks_menus_template/base/translations/menus_translations.fr.translation"]
source_file="res://addons/maaacks_menus_template/base/translations/menus_translations.csv"
dest_files=["res://addons/maaacks_menus_template/base/translations/menus_translations.en.translation", "res://addons/maaacks_menus_template/base/translations/menus_translations.fr.translation"]
[params]
compress=true
delimiter=0

@ -0,0 +1,111 @@
# Existing Project
1. Update the projects main scene (if skipped during plugin install).
1. Go to `Project > Project Settings… > General > Application > Run`.
2. Update `Main Scene` to `main_menu.tscn`.
1. Alternatively, any scene the inherits from it. One exists in the `examples/` folder.
3. Close the window.
2. Update the projects name in the main menu.
1. Open `main_menu.tscn`.
2. Select the `Title` node.
3. Update the `Text` to your project's title.
4. Select the `Subtitle` node.
5. Update the `Text` to a desired subtitle or empty.
6. Save the scene.
3. Link the main menu to the game scene.
1. Open `main_menu.tscn`.
2. Select the `MainMenu` node.
3. Update `Game Scene Path` to the path of the project's game scene.
4. Save the scene.
4. Add background music and sound effects to the UI.
1. Add `Music` and `SFX` to the project's default audio busses.
1. Open the Audio bus editor.
2. Click the button "Add Bus" twice (x2).
3. Name the two new busses `Music` and `SFX`.
4. Save the project.
2. Add background music to the Main Menu.
1. Import the music asset into the project.
2. Open `main_menu.tscn`.
3. Select the `BackgroundMusicPlayer` node.
4. Assign the music asset to the `stream` property.
5. Save the scene.
3. Add sound effects to UI elements.
1. By scene.
1. Open `main_menu.tscn`.
2. Select the `UISoundController` node.
3. Add audio streams to the various UI node events.
4. Save the scene.
2. Project-wide.
1. Open `project_ui_sound_controller.tscn`.
2. Select the `UISoundController` node.
3. Add audio streams to the various UI node events.
4. Save the scene.
5. Add readable names for input actions to the controls menu.
1. Open `input_options_menu.tscn` (or `master_options_menu.tscn`, which contains an instance of the scene).
2. Select the `Controls` node.
3. Update the `Action Name Map` to show readable names for the project's input actions.
1. The keys are the project's input action names, while the values are the names shown in the controls menu.
2. An example is provided. It can be updated or removed, either in the inspector for the node, or in the code of `input_options_menu.gd`.
4. Save the scene.
6. Add / remove configurable settings to / from menus.
1. Open `mini_options_menu.tscn` or `[audio|visual|input|game]_options_menu.tscn` scenes to edit their options.
2. If an option is not desired, it can always be hidden, or removed entirely (sometimes with some additional work).
3. If a new option is desired, it can be added without writing code.
1. Find the node that contains the existing list of options. Usually, it's a `VBoxContainer`.
2. Add an `option_control.tscn` node as a child to the container.
1. `slider_option_control.tscn` or `toggle_option_control.tscn` can be used if those types match requirements. In that case, skip step 6.
2. `list_option_control.tscn` and `vector_2_list_option_control.tscn` are also available, but more complicated. See the `ScreenResolution` example.
3. Select the `OptionControl` node just added, to edit it in the inspector.
4. Add an `Option Name`. This prefills the `Key` string.
5. Select an `Option Section`. This prefills the `Section` string.
6. Add any kind of `Button`, `Slider`, `LineEdit`, or `TextEdit` to the `OptionControl` node.
7. Save the scene.
4. For options to have an effect outside of the menu, it will need to be referenced by its `key` and `section` from `config.gd`.
1. `Config.get_config(section, key, default_value)`
5. Validate the values being stored in your local `config.cfg` file.
1. Refer to [Accessing Persistent User Data User](https://docs.godotengine.org/en/stable/tutorials/io/data_paths.html#accessing-persistent-user-data-user) to find Godot user data on your machine.
2. Find the directory that matches your project's name.
3. `config.cfg` should be in the top directory of the project.
7. Update the game credits / attribution.
1. Update the example `ATTRIBUTION.md` with the project's credits.
2. Open `credits.tscn`.
3. Check the `CreditsLabel` has updated with the text.
4. Save the scene.

@ -0,0 +1,13 @@
# Screenshots
![Main Menu](/addons/maaacks_menus_template/media/screenshot-3-1.png)
![Input Controls](/addons/maaacks_menus_template/media/screenshot-3-2.png)
![Key Rebinding](/addons/maaacks_menus_template/media/screenshot-4-1.png)
![Key Confirmation](/addons/maaacks_menus_template/media/screenshot-4-2.png)
![Audio Controls](/addons/maaacks_menus_template/media/screenshot-3-4.png)
![Video Controls](/addons/maaacks_menus_template/media/screenshot-4-3.png)
![Credits Screen](/addons/maaacks_menus_template/media/screenshot-3-5.png)
![Loading Screen 50% Loaded](/addons/maaacks_menus_template/media/screenshot-3-7.png)
![Loading Screen Still Loading](/addons/maaacks_menus_template/media/screenshot-3-8.png)
![Loading Screen Error Message](/addons/maaacks_menus_template/media/screenshot-3-9.png)
![Loading Screen Loading Complete](/addons/maaacks_menus_template/media/screenshot-3-10.png)

@ -0,0 +1,6 @@
# Videos
[![Quick Intro Video](https://img.youtube.com/vi/U9CB3vKINVw/hqdefault.jpg)](https://youtu.be/U9CB3vKINVw)
[![Installation Video](https://img.youtube.com/vi/-QWJnZ8bVdk/hqdefault.jpg)](https://youtu.be/-QWJnZ8bVdk)
[![UI Theming (1) Video](https://img.youtube.com/vi/SBE4icfXYRA/hqdefault.jpg)](https://youtu.be/SBE4icfXYRA)
[![UI Theming (2) Video](https://img.youtube.com/vi/wCc2QUnaBKo/hqdefault.jpg)](https://youtu.be/wCc2QUnaBKo)

@ -0,0 +1,32 @@
# Attribution
## Collaborators
### Role
Person 1
Person 2
[Person w/ Link]()
## Sourced / Unaffiliated
### Asset Type
#### Use Case
Author: [Name]()
Source: [Domain : webpage.html]()
License: [License]()
## Tools
#### Godot
Author: [Juan Linietsky, Ariel Manzur, and contributors](https://godotengine.org/contact)
Source: [godotengine.org](https://godotengine.org/)
License: [MIT License](https://github.com/godotengine/godot/blob/master/LICENSE.txt)
#### Git
Author: [Linus Torvalds](https://github.com/torvalds)
Source: [git-scm.com](https://git-scm.com/downloads)
License: [GNU General Public License version 2](https://opensource.org/licenses/GPL-2.0)
#### Godot Menus Template
Author: [Marek Belski](https://github.com/Maaack/Godot-Menus-Template/graphs/contributors)
Source: [github: Godot-Menus-Template](https://github.com/Maaack/Godot-Menus-Template)
License: [MIT License](LICENSE.txt)

@ -0,0 +1,42 @@
[gd_scene load_steps=3 format=3 uid="uid://c1g50h2avck3w"]
[ext_resource type="PackedScene" uid="uid://t2dui8ppm3a4" path="res://addons/maaacks_menus_template/base/scenes/credits/credits.tscn" id="1_n45le"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/credits/credits.gd" id="2_q7msw"]
[node name="Credits" instance=ExtResource("1_n45le")]
script = ExtResource("2_q7msw")
attribution_file_path = "res://addons/maaacks_menus_template/examples/ATTRIBUTION.md"
[node name="CreditsLabel" parent="ScrollContainer/VBoxContainer" index="1"]
text = "[center][font_size=48]Collaborators[/font_size]
[font_size=32]Role[/font_size]
Person 1
Person 2
[url=]Person w/ Link[/url]
[font_size=48]Sourced / Unaffiliated[/font_size]
[font_size=32]Asset Type[/font_size]
[font_size=24]Use Case[/font_size]
Author: [url=]Name[/url]
Source: [url=]Domain : webpage.html[/url]
License: [url=]License[/url]
[font_size=48]Tools[/font_size]
[font_size=24]Godot[/font_size]
Author: [url=https://godotengine.org/contact]Juan Linietsky, Ariel Manzur, and contributors[/url]
Source: [url=https://godotengine.org/]godotengine.org[/url]
License: [url=https://github.com/godotengine/godot/blob/master/LICENSE.txt]MIT License[/url]
[font_size=24]Git[/font_size]
Author: [url=https://github.com/torvalds]Linus Torvalds[/url]
Source: [url=https://git-scm.com/downloads]git-scm.com[/url]
License: [url=https://opensource.org/licenses/GPL-2.0]GNU General Public License version 2[/url]
[font_size=24]Godot Menus Template[/font_size]
Author: [url=https://github.com/Maaack/Godot-Menus-Template/graphs/contributors]Marek Belski[/url]
Source: [url=https://github.com/Maaack/Godot-Menus-Template]github: Godot-Menus-Template[/url]
License: [url=LICENSE.txt]MIT License[/url]
[/center]"

@ -0,0 +1,37 @@
@tool
extends Credits
@export_file("*.tscn") var main_menu_scene : String
@onready var init_mouse_filter = mouse_filter
func _end_reached():
%EndMessagePanel.show()
mouse_filter = Control.MOUSE_FILTER_STOP
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
super._end_reached()
func _on_MenuButton_pressed():
SceneLoader.load_scene(main_menu_scene)
func _on_ExitButton_pressed():
get_tree().quit()
func _ready():
if main_menu_scene.is_empty():
%MenuButton.hide()
if OS.has_feature("web"):
%ExitButton.hide()
super._ready()
func reset():
super.reset()
%EndMessagePanel.hide()
mouse_filter = init_mouse_filter
func _unhandled_input(event):
if not enabled: return
if event.is_action_pressed("ui_cancel"):
if not %EndMessagePanel.visible:
_end_reached()
else:
get_tree().quit()

@ -0,0 +1,91 @@
[gd_scene load_steps=5 format=3 uid="uid://bagraegk311h0"]
[ext_resource type="PackedScene" uid="uid://c1g50h2avck3w" path="res://addons/maaacks_menus_template/examples/scenes/credits/credits.tscn" id="1_885d8"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/end_credits/end_credits.gd" id="2_6pwj4"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/base/scripts/capture_focus.gd" id="3_aoilo"]
[ext_resource type="PackedScene" uid="uid://bkcsjsk2ciff" path="res://addons/maaacks_menus_template/base/scenes/music_players/background_music_player.tscn" id="3_v7acr"]
[node name="EndCredits" instance=ExtResource("1_885d8")]
script = ExtResource("2_6pwj4")
main_menu_scene = "res://addons/maaacks_menus_template/examples/scenes/menus/main_menu/main_menu_with_animations.tscn"
[node name="BackgroundMusicPlayer" parent="." index="0" instance=ExtResource("3_v7acr")]
[node name="BackgroundColor" type="ColorRect" parent="." index="1"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 1)
[node name="BackgroundTextureRect" type="TextureRect" parent="." index="2"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
stretch_mode = 5
[node name="ScrollContainer" parent="." index="3"]
scroll_vertical = 0
[node name="CenterContainer" type="CenterContainer" parent="." index="4"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
[node name="EndMessagePanel" type="Panel" parent="CenterContainer" index="0"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(360, 120)
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/EndMessagePanel" index="0"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="ThankPlayer" type="Label" parent="CenterContainer/EndMessagePanel/VBoxContainer" index="0"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "Thanks for playing!"
horizontal_alignment = 1
vertical_alignment = 1
[node name="CenterContainer" type="CenterContainer" parent="CenterContainer/EndMessagePanel/VBoxContainer" index="1"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="CenterContainer/EndMessagePanel/VBoxContainer/CenterContainer" index="0"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 24
script = ExtResource("3_aoilo")
[node name="ExitButton" type="Button" parent="CenterContainer/EndMessagePanel/VBoxContainer/CenterContainer/HBoxContainer" index="0"]
unique_name_in_owner = true
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "Exit"
[node name="MenuButton" type="Button" parent="CenterContainer/EndMessagePanel/VBoxContainer/CenterContainer/HBoxContainer" index="1"]
unique_name_in_owner = true
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "Menu"
[connection signal="pressed" from="CenterContainer/EndMessagePanel/VBoxContainer/CenterContainer/HBoxContainer/ExitButton" to="." method="_on_ExitButton_pressed"]
[connection signal="pressed" from="CenterContainer/EndMessagePanel/VBoxContainer/CenterContainer/HBoxContainer/MenuButton" to="." method="_on_MenuButton_pressed"]

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://ddeagbc67f52m"]
[ext_resource type="PackedScene" uid="uid://cd0jbh4metflb" path="res://addons/maaacks_menus_template/base/scenes/loading_screen/loading_screen.tscn" id="1_2dnq1"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/loading_screen/loading_screen.gd" id="2_5h2y6"]
[node name="LoadingScreen" instance=ExtResource("1_2dnq1")]
script = ExtResource("2_5h2y6")

@ -0,0 +1,86 @@
extends LoadingScreen
const QUADMESH_PLACEHOLDER = preload("res://addons/maaacks_menus_template/examples/scenes/loading_screen/quad_mesh_placeholder.tscn")
@export_dir var _spatial_shader_material_dir : String
@export_file("*.tscn") var _cache_shaders_scene : String
@export_group("Advanced")
@export var _matching_extensions : Array[String] = [".tres", ".material", ".res"]
@export var _ignore_subfolders : Array[String] = [".", ".."]
@export var _shader_delay_timer : float = 0.1
var _loading_shader_cache : bool = false
var _caching_progress : float = 0.0 :
set(value):
if value <= _caching_progress:
return
_caching_progress = value
update_total_loading_progress()
_reset_loading_stage()
func can_load_shader_cache():
return not _spatial_shader_material_dir.is_empty() and \
not _cache_shaders_scene.is_empty() and \
SceneLoader.is_loading_scene(_cache_shaders_scene)
func update_total_loading_progress():
var partial_total = _scene_loading_progress
if can_load_shader_cache():
partial_total += _caching_progress
partial_total /= 2
_total_loading_progress = partial_total
func _try_loading_next_scene():
if can_load_shader_cache() and not _loading_shader_cache:
_loading_shader_cache = true
_show_all_draw_passes_once()
if can_load_shader_cache() and _caching_progress < 1.0:
return
super._try_loading_next_scene()
func _show_all_draw_passes_once():
var all_materials = _traverse_folders(_spatial_shader_material_dir)
var total_material_count = all_materials.size()
var cached_material_count = 0
for material_path in all_materials:
_load_material(material_path)
cached_material_count += 1
_caching_progress = float(cached_material_count) / total_material_count
if _shader_delay_timer > 0:
await(get_tree().create_timer(_shader_delay_timer).timeout)
func _traverse_folders(dir_path:String) -> PackedStringArray:
var material_list:PackedStringArray = []
if not dir_path.ends_with("/"):
dir_path += "/"
var dir = DirAccess.open(dir_path)
if not dir:
push_error("failed to access the path ", dir_path)
return []
if dir.list_dir_begin() != OK:
push_error("failed to access the path ", dir_path)
return []
var file_name = dir.get_next()
while file_name != "":
if not dir.current_is_dir():
var matches : bool = false
for extension in _matching_extensions:
if file_name.ends_with(extension):
matches = true
break
if matches:
material_list.append(dir_path + file_name)
else:
var subfolder_name = file_name
if not subfolder_name in _ignore_subfolders:
material_list.append_array(_traverse_folders(dir_path + subfolder_name))
file_name = dir.get_next()
return material_list
func _load_material(path:String):
var material_shower = QUADMESH_PLACEHOLDER.instantiate()
var material := ResourceLoader.load(path) as Material
material_shower.set_surface_override_material(0, material)
%SpatialShaderTypeCaches.add_child(material_shower)

@ -0,0 +1,19 @@
[gd_scene load_steps=3 format=3 uid="uid://dvp3b2tdsbtmo"]
[ext_resource type="PackedScene" uid="uid://cd0jbh4metflb" path="res://addons/maaacks_menus_template/base/scenes/loading_screen/loading_screen.tscn" id="1_4k0mc"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/loading_screen/loading_screen_with_shader_caching.gd" id="2_3xu6h"]
[node name="LoadingScreen" instance=ExtResource("1_4k0mc")]
script = ExtResource("2_3xu6h")
_spatial_shader_material_dir = ""
_cache_shaders_scene = "res://addons/maaacks_menus_template/examples/scenes/GameScene/GameUI.tscn"
_matching_extensions = Array[String]([".tres", ".material", ".res"])
_ignore_subfolders = Array[String]([".", ".."])
_shader_delay_timer = 0.1
[node name="SpatialShaderTypeCaches" type="Node3D" parent="." index="2"]
unique_name_in_owner = true
[node name="Camera3D" type="Camera3D" parent="SpatialShaderTypeCaches" index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1.408)
current = true

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://b1nvjo55xpy3g"]
[sub_resource type="QuadMesh" id="QuadMesh_el52t"]
size = Vector2(0.001, 0.001)
[node name="MeshPlaceholder" type="MeshInstance3D"]
mesh = SubResource("QuadMesh_el52t")

@ -0,0 +1,11 @@
[gd_scene load_steps=5 format=3 uid="uid://byvydukidk6i2"]
[ext_resource type="PackedScene" uid="uid://c6k5nnpbypshi" path="res://addons/maaacks_menus_template/base/scenes/menus/main_menu/main_menu.tscn" id="1_pss7b"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/menus/main_menu/main_menu.gd" id="2_lk0wa"]
[ext_resource type="PackedScene" uid="uid://bdvdf5v87mmrr" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/master_options_menu_with_tabs.tscn" id="3_lqjmk"]
[ext_resource type="PackedScene" uid="uid://c1g50h2avck3w" path="res://addons/maaacks_menus_template/examples/scenes/credits/credits.tscn" id="3_vmius"]
[node name="MainMenu" instance=ExtResource("1_pss7b")]
script = ExtResource("2_lk0wa")
options_packed_scene = ExtResource("3_lqjmk")
credits_packed_scene = ExtResource("3_vmius")

@ -0,0 +1,35 @@
extends MainMenu
var animation_state_machine : AnimationNodeStateMachinePlayback
func intro_done():
animation_state_machine.travel("OpenMainMenu")
func _is_in_intro():
return animation_state_machine.get_current_node() == "Intro"
func _event_is_mouse_button_released(event : InputEvent):
return event is InputEventMouseButton and not event.is_pressed()
func _event_skips_intro(event : InputEvent):
return event.is_action_released("ui_accept") or \
event.is_action_released("ui_select") or \
event.is_action_released("ui_cancel") or \
_event_is_mouse_button_released(event)
func _open_sub_menu(menu):
super._open_sub_menu(menu)
animation_state_machine.travel("OpenSubMenu")
func _close_sub_menu():
super._close_sub_menu()
animation_state_machine.travel("OpenMainMenu")
func _input(event):
if _is_in_intro() and _event_skips_intro(event):
intro_done()
super._input(event)
func _ready():
super._ready()
animation_state_machine = $MenuAnimationTree.get("parameters/playback")

@ -0,0 +1,374 @@
[gd_scene load_steps=16 format=3 uid="uid://de7myf2ybhwk5"]
[ext_resource type="PackedScene" uid="uid://byvydukidk6i2" path="res://addons/maaacks_menus_template/examples/scenes/menus/main_menu/main_menu.tscn" id="1_0i2sc"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/menus/main_menu/main_menu_with_animations.gd" id="2_ncvk7"]
[sub_resource type="Animation" id="1"]
resource_name = "Intro"
length = 2.4
tracks/0/type = "method"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath(".")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(2.4),
"transitions": PackedFloat32Array(1),
"values": [{
"args": [],
"method": &"intro_done"
}]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer/Title:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0, 0.8),
"transitions": PackedFloat32Array(1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 1)]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer/SubTitle:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 0.8, 1.6),
"transitions": PackedFloat32Array(1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("MenuContainer/VBoxContainer/MenuMargin/MenuButtons:modulate")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0, 1.6, 2.4),
"transitions": PackedFloat32Array(1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1)]
}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("FlowControlContainer:mouse_filter")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {
"times": PackedFloat32Array(0, 2.4),
"transitions": PackedFloat32Array(1, 1),
"update": 1,
"values": [0, 2]
}
tracks/5/type = "value"
tracks/5/imported = false
tracks/5/enabled = true
tracks/5/path = NodePath("VersionNameLabel:modulate")
tracks/5/interp = 1
tracks/5/loop_wrap = true
tracks/5/keys = {
"times": PackedFloat32Array(0, 1.6, 2.4),
"transitions": PackedFloat32Array(1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 0), Color(1, 1, 1, 1)]
}
[sub_resource type="Animation" id="6"]
resource_name = "OpenMainMenu"
length = 0.1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer/Title:modulate")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 1)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer/SubTitle:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 1)]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MenuContainer/VBoxContainer/MenuMargin/MenuButtons:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 1)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("FlowControlContainer/FlowControl/BackButton:visible")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [false]
}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("FlowControlContainer:mouse_filter")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [2]
}
tracks/5/type = "value"
tracks/5/imported = false
tracks/5/enabled = true
tracks/5/path = NodePath("MenuContainer:modulate")
tracks/5/interp = 1
tracks/5/loop_wrap = true
tracks/5/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 1)]
}
tracks/6/type = "value"
tracks/6/imported = false
tracks/6/enabled = true
tracks/6/path = NodePath("VersionNameLabel:modulate")
tracks/6/interp = 1
tracks/6/loop_wrap = true
tracks/6/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 1)]
}
tracks/7/type = "value"
tracks/7/imported = false
tracks/7/enabled = true
tracks/7/path = NodePath("MenuContainer/VBoxContainer/MenuMargin/MenuButtons:lock")
tracks/7/interp = 1
tracks/7/loop_wrap = true
tracks/7/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [false]
}
[sub_resource type="Animation" id="4"]
resource_name = "OpenSubMenu"
length = 0.2
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("FlowControlContainer/FlowControl/BackButton:visible")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [true]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MenuContainer:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
[sub_resource type="Animation" id="2"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("FlowControlContainer/FlowControl/BackButton:visible")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [false]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer/Title:modulate")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer/SubTitle:modulate")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("MenuContainer/VBoxContainer/MenuMargin/MenuButtons:modulate")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("FlowControlContainer:mouse_filter")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [2]
}
tracks/5/type = "value"
tracks/5/imported = false
tracks/5/enabled = true
tracks/5/path = NodePath("MenuContainer:modulate")
tracks/5/interp = 1
tracks/5/loop_wrap = true
tracks/5/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 1)]
}
tracks/6/type = "value"
tracks/6/imported = false
tracks/6/enabled = true
tracks/6/path = NodePath("VersionNameLabel:modulate")
tracks/6/interp = 1
tracks/6/loop_wrap = true
tracks/6/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Color(1, 1, 1, 0)]
}
tracks/7/type = "value"
tracks/7/imported = false
tracks/7/enabled = true
tracks/7/path = NodePath("MenuContainer/VBoxContainer/MenuMargin/MenuButtons:lock")
tracks/7/interp = 1
tracks/7/loop_wrap = true
tracks/7/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [true]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_2kqig"]
_data = {
"Intro": SubResource("1"),
"OpenMainMenu": SubResource("6"),
"OpenSubMenu": SubResource("4"),
"RESET": SubResource("2")
}
[sub_resource type="AnimationNodeAnimation" id="7"]
animation = &"Intro"
[sub_resource type="AnimationNodeAnimation" id="10"]
animation = &"OpenMainMenu"
[sub_resource type="AnimationNodeAnimation" id="13"]
animation = &"OpenSubMenu"
[sub_resource type="AnimationNodeStateMachineTransition" id="11"]
advance_condition = &"intro_done"
[sub_resource type="AnimationNodeStateMachineTransition" id="14"]
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_j0orr"]
advance_mode = 2
[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_63dxc"]
[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_vikuh"]
states/End/position = Vector2(958, 123)
states/Intro/node = SubResource("7")
states/Intro/position = Vector2(259, 123)
states/OpenMainMenu/node = SubResource("10")
states/OpenMainMenu/position = Vector2(472, 123)
states/OpenSubMenu/node = SubResource("13")
states/OpenSubMenu/position = Vector2(734, 123)
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_0i2sc")]
script = ExtResource("2_ncvk7")
[node name="MenuAnimationPlayer" type="AnimationPlayer" parent="." index="1"]
libraries = {
"": SubResource("AnimationLibrary_2kqig")
}
[node name="MenuAnimationTree" type="AnimationTree" parent="." index="2"]
tree_root = SubResource("AnimationNodeStateMachine_vikuh")
anim_player = NodePath("../MenuAnimationPlayer")
parameters/conditions/intro_done = false
[node name="VersionNameLabel" parent="." index="5"]
modulate = Color(1, 1, 1, 0)
[node name="Title" parent="MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer" index="0"]
modulate = Color(1, 1, 1, 0)
[node name="SubTitle" parent="MenuContainer/VBoxContainer/TitlesMargin/TitlesContainer" index="1"]
modulate = Color(1, 1, 1, 0)
[node name="MenuButtons" parent="MenuContainer/VBoxContainer/MenuMargin" index="0"]
modulate = Color(1, 1, 1, 0)
lock = true

@ -0,0 +1,37 @@
extends ListOptionControl
func _set_input_device():
var current_setting = _get_setting(default_value)
if current_setting is bool:
current_setting = &"Default"
AudioServer.input_device = _get_setting(default_value)
func _add_microphone_audio_stream() -> void:
var instance = AudioStreamPlayer.new()
instance.stream = AudioStreamMicrophone.new()
instance.autoplay = true
add_child.call_deferred(instance)
instance.ready.connect(_set_input_device)
func _ready():
if ProjectSettings.get_setting("audio/driver/enable_input", false):
if AudioServer.input_device.is_empty():
_add_microphone_audio_stream()
else:
_set_input_device()
if not Engine.is_editor_hint():
option_values = AudioServer.get_input_device_list()
else:
hide()
super._ready()
func _on_setting_changed(value):
if value >= option_values.size(): return
AudioServer.input_device = option_values[value]
super._on_setting_changed(value)
func _value_title_map(value : Variant) -> String:
if value is String:
return value
else:
return super._value_title_map(value)

@ -0,0 +1,21 @@
[gd_scene load_steps=3 format=3 uid="uid://dxj1gsxtlp6v0"]
[ext_resource type="PackedScene" uid="uid://b6bl3n5mp3m1e" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn" id="1_0vgeo"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/audio/audio_input_option_control.gd" id="2_6qeue"]
[node name="AudioInputOptionControl" instance=ExtResource("1_0vgeo")]
script = ExtResource("2_6qeue")
option_name = "Input Device"
option_section = 2
key = "InputDevice"
section = "AudioSettings"
property_type = 4
default_value = "Default"
[node name="OptionLabel" parent="." index="0"]
text = "Input Device :"
[node name="OptionButton" parent="." index="1"]
size_flags_horizontal = 3
text_overrun_behavior = 1
clip_text = true

@ -0,0 +1,11 @@
[gd_scene load_steps=4 format=3 uid="uid://dmgla7rq1g2cc"]
[ext_resource type="PackedScene" uid="uid://c8vnncjwqcpab" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/audio/audio_options_menu.tscn" id="1_ro573"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/audio/audio_options_menu.gd" id="2_g4nfi"]
[ext_resource type="PackedScene" uid="uid://dxj1gsxtlp6v0" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/audio/audio_input_option_control.tscn" id="3_orobk"]
[node name="Audio" instance=ExtResource("1_ro573")]
script = ExtResource("2_g4nfi")
[node name="AudioInputOptionControl" parent="VBoxContainer" index="2" instance=ExtResource("3_orobk")]
layout_mode = 2

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://1e3vf4u3brfm"]
[ext_resource type="PackedScene" uid="uid://dp3rgqaehb3xu" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/input/input_options_menu.tscn" id="1_b6ygu"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/input/input_options_menu.gd" id="2_gjulr"]
[node name="Controls" instance=ExtResource("1_b6ygu")]
script = ExtResource("2_gjulr")

@ -0,0 +1,39 @@
[gd_scene load_steps=3 format=3 uid="uid://b4r3gcnm31uo6"]
[ext_resource type="PackedScene" uid="uid://1e3vf4u3brfm" path="res://addons/maaacks_menus_template/examples/scenes/Menus/OptionsMenu/Input/InputOptionsMenu.tscn" id="1_pi4g6"]
[ext_resource type="PackedScene" uid="uid://cl416gdb1fgwr" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/OptionControl/SliderOptionControl.tscn" id="2_ax2ge"]
[node name="Controls" instance=ExtResource("1_pi4g6")]
[node name="VBoxContainer" parent="." index="0"]
theme_override_constants/separation = 16
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer" index="0"]
layout_mode = 2
theme_override_constants/margin_top = 32
theme_override_constants/margin_bottom = 32
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/MarginContainer" index="0"]
layout_mode = 2
size_flags_vertical = 3
alignment = 1
[node name="MouseSensitivityControl" parent="VBoxContainer/MarginContainer/VBoxContainer" index="0" instance=ExtResource("2_ax2ge")]
layout_mode = 2
option_name = "Mouse Sensitivity"
option_section = 1
key = "MouseSensitivity"
section = "InputSettings"
[node name="OptionLabel" parent="VBoxContainer/MarginContainer/VBoxContainer/MouseSensitivityControl" index="0"]
text = "Mouse Sensitivity :"
[node name="HSlider" parent="VBoxContainer/MarginContainer/VBoxContainer/MouseSensitivityControl" index="1"]
min_value = 0.5
max_value = 2.0
tick_count = 16
[node name="HSeparator" type="HSeparator" parent="VBoxContainer" index="1"]
layout_mode = 2
[editable path="VBoxContainer/MarginContainer/VBoxContainer/MouseSensitivityControl"]

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://c3s46qrj7m87p"]
[ext_resource type="PackedScene" uid="uid://bvwl11s2p0hd" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/master_options_menu.tscn" id="1_kgc1h"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/master_options_menu.gd" id="2_4n0ab"]
[node name="MasterOptionsMenu" instance=ExtResource("1_kgc1h")]
script = ExtResource("2_4n0ab")

@ -0,0 +1,26 @@
[gd_scene load_steps=5 format=3 uid="uid://bdvdf5v87mmrr"]
[ext_resource type="PackedScene" uid="uid://c3s46qrj7m87p" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/master_options_menu.tscn" id="1_u1u8e"]
[ext_resource type="PackedScene" uid="uid://b4r3gcnm31uo6" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/input/input_options_menu_with_mouse_sensitivity.tscn" id="2_p458j"]
[ext_resource type="PackedScene" uid="uid://dmgla7rq1g2cc" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/audio/audio_options_menu.tscn" id="3_vs2ne"]
[ext_resource type="PackedScene" uid="uid://cck3omvlkhpix" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/video/video_options_menu_with_extras.tscn" id="4_v3ewd"]
[node name="MasterOptionsMenu" instance=ExtResource("1_u1u8e")]
[node name="TabContainer" parent="." index="0"]
current_tab = 0
[node name="Controls" parent="TabContainer" index="1" instance=ExtResource("2_p458j")]
layout_mode = 2
show_all_actions = true
metadata/_tab_index = 0
[node name="Audio" parent="TabContainer" index="2" instance=ExtResource("3_vs2ne")]
visible = false
layout_mode = 2
metadata/_tab_index = 1
[node name="Video" parent="TabContainer" index="3" instance=ExtResource("4_v3ewd")]
visible = false
layout_mode = 2
metadata/_tab_index = 2

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://c6obwfb7wbibn"]
[ext_resource type="PackedScene" uid="uid://vh1ucj2rfbby" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/mini_options_menu.tscn" id="1_f2l25"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/mini_options_menu.gd" id="2_hcx71"]
[node name="MiniOptionsMenu" instance=ExtResource("1_f2l25")]
script = ExtResource("2_hcx71")

@ -0,0 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://b8kb81us6g3kr"]
[ext_resource type="PackedScene" uid="uid://b2numvphf2kau" path="res://addons/maaacks_menus_template/base/scenes/menus/options_menu/video/video_options_menu.tscn" id="1_4gigb"]
[ext_resource type="Script" path="res://addons/maaacks_menus_template/examples/scenes/menus/options_menu/video/video_options_menu.gd" id="2_v2jlu"]
[node name="Video" instance=ExtResource("1_4gigb")]
script = ExtResource("2_v2jlu")

@ -0,0 +1,30 @@
[gd_scene load_steps=3 format=3 uid="uid://cck3omvlkhpix"]
[ext_resource type="PackedScene" uid="uid://b8kb81us6g3kr" path="res://addons/maaacks_menus_template/examples/scenes/Menus/OptionsMenu/Video/VideoOptionsMenu.tscn" id="1_nrerc"]
[ext_resource type="PackedScene" uid="uid://b6bl3n5mp3m1e" path="res://addons/maaacks_menus_template/base/scenes/Menus/OptionsMenu/OptionControl/ListOptionControl.tscn" id="2_tyiki"]
[node name="Video" instance=ExtResource("1_nrerc")]
[node name="AntiAliasingControl" parent="VBoxContainer" index="2" instance=ExtResource("2_tyiki")]
layout_mode = 2
option_values = [0, 1, 2, 3]
option_titles = Array[String](["Disabled (Fastest)", "2x", "4x", "8x (Slowest)"])
lock_titles = true
option_name = "Anti-Aliasing"
option_section = 3
key = "Anti-aliasing"
section = "VideoSettings"
property_type = 2
default_value = 0
[node name="CameraShakeControl" parent="VBoxContainer" index="3" instance=ExtResource("2_tyiki")]
layout_mode = 2
option_values = [1.0, 0.75, 0.5, 0.0]
option_titles = Array[String](["Normal", "Reduced", "Minimal", "None"])
lock_titles = true
option_name = "Camera Shake"
option_section = 3
key = "CameraShake"
section = "VideoSettings"
property_type = 3
default_value = 1.0

@ -0,0 +1,14 @@
[gd_scene format=3 uid="uid://cyx4i4v30bw4o"]
[node name="CopyConfirmationDialog" type="ConfirmationDialog"]
title = "Copy Examples"
initial_position = 2
size = Vector2i(400, 210)
visible = true
exclusive = false
ok_button_text = "Yes"
dialog_text = "Plugin enabled. It is recommended to copy the example scenes to a destination outside of the addons/ folder before editing them.
Would you like to copy the examples now?"
dialog_autowrap = true
cancel_button_text = "No"

@ -0,0 +1,15 @@
[gd_scene format=3 uid="uid://vgdxevcnv0vx"]
[node name="DeleteExamplesConfirmationDialog" type="ConfirmationDialog"]
title = "Delete Source Examples"
initial_position = 2
size = Vector2i(400, 320)
visible = true
ok_button_text = "Yes"
dialog_text = "If the copied scenes work as expected, you may delete the source examples folder. This avoids confusing both developers and the Godot editor.
This will also remove the option to copy the examples again. However, one copy is enough for most use cases.
Would you like to delete the source examples folder now?"
dialog_autowrap = true
cancel_button_text = "No"

@ -0,0 +1,11 @@
[gd_scene format=3 uid="uid://d03csqgcaxm0m"]
[node name="DeleteExamplesShortConfirmationDialog" type="ConfirmationDialog"]
title = "Delete Source Examples"
initial_position = 2
size = Vector2i(400, 106)
visible = true
ok_button_text = "Yes"
dialog_text = "Are you sure you would like to delete the source examples folder?"
dialog_autowrap = true
cancel_button_text = "No"

@ -0,0 +1,11 @@
[gd_scene format=3 uid="uid://ckx50am7thhd2"]
[node name="DestinationDialog" type="FileDialog"]
title = "Select a Destination"
initial_position = 2
size = Vector2i(500, 400)
visible = true
exclusive = false
ok_button_text = "Select Current Folder"
mode_overrides_title = false
file_mode = 2

@ -0,0 +1,14 @@
[gd_scene format=3 uid="uid://b8kr3y0cjxr8m"]
[node name="MainSceneConfirmationDialog" type="ConfirmationDialog"]
title = "Update Main Scene"
initial_position = 2
size = Vector2i(420, 210)
visible = true
exclusive = false
ok_button_text = "Yes"
dialog_text = "Would you like to update the project's main scene?
"
dialog_autowrap = true
cancel_button_text = "No"

@ -0,0 +1,36 @@
; Project settings override file.
; Adds gamepad inputs to built-in actions.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
[input]
ui_accept={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null)
]
}
ui_cancel={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":true,"script":null)
]
}
ui_page_up={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194323,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":9,"pressure":0.0,"pressed":true,"script":null)
]
}
ui_page_down={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194324,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":10,"pressure":0.0,"pressed":true,"script":null)
]
}

@ -0,0 +1,13 @@
[gd_scene format=3 uid="uid://b8808yj7a0ghj"]
[node name="PlayOpeningConfirmationDialog" type="ConfirmationDialog"]
title = "Run & Test"
initial_position = 2
size = Vector2i(400, 210)
visible = true
ok_button_text = "Yes"
dialog_text = "It is recommended to run the opening scene of the plugin and test if any issues occurred during the copying process.
Would you like to run and test the scenes now?"
dialog_autowrap = true
cancel_button_text = "No"

@ -0,0 +1,307 @@
@tool
extends EditorPlugin
const PLUGIN_NAME = "Maaack's Menus Template"
const PROJECT_SETTINGS_PATH = "maaacks_menus_template/"
const EXAMPLES_RELATIVE_PATH = "examples/"
const MAIN_SCENE_RELATIVE_PATH = "scenes/menus/main_menu/main_menu_with_animations.tscn"
const MAIN_SCENE_UPDATE_TEXT = "Current:\n%s\n\nNew:\n%s\n"
const OVERRIDE_RELATIVE_PATH = "installer/override.cfg"
const SCENE_LOADER_RELATIVE_PATH = "base/scenes/autoloads/scene_loader.tscn"
const UID_PREG_MATCH = r'uid="uid:\/\/[0-9a-z]+" '
const RESAVING_DELAY : float = 0.5
const REIMPORT_FILE_DELAY : float = 0.2
const OPEN_EDITOR_DELAY : float = 0.1
const MAX_PHYSICS_FRAMES_FROM_START : int = 20
func _get_plugin_name():
return PLUGIN_NAME
func get_plugin_path() -> String:
return get_script().resource_path.get_base_dir() + "/"
func get_plugin_examples_path() -> String:
return get_plugin_path() + EXAMPLES_RELATIVE_PATH
func _update_main_scene(main_scene_path : String):
ProjectSettings.set_setting("application/run/main_scene", main_scene_path)
ProjectSettings.save()
func _check_main_scene_needs_updating(target_path : String):
var current_main_scene_path = ProjectSettings.get_setting("application/run/main_scene", "")
var new_main_scene_path = target_path + MAIN_SCENE_RELATIVE_PATH
if new_main_scene_path == current_main_scene_path:
return
_open_main_scene_confirmation_dialog(current_main_scene_path, new_main_scene_path)
func _open_main_scene_confirmation_dialog(current_main_scene : String, new_main_scene : String):
var main_confirmation_scene : PackedScene = load(get_plugin_path() + "installer/main_scene_confirmation_dialog.tscn")
var main_confirmation_instance : ConfirmationDialog = main_confirmation_scene.instantiate()
main_confirmation_instance.dialog_text += MAIN_SCENE_UPDATE_TEXT % [current_main_scene, new_main_scene]
main_confirmation_instance.confirmed.connect(_update_main_scene.bind(new_main_scene))
add_child(main_confirmation_instance)
func _open_play_opening_confirmation_dialog(target_path : String):
var play_confirmation_scene : PackedScene = load(get_plugin_path() + "installer/play_opening_confirmation_dialog.tscn")
var play_confirmation_instance : ConfirmationDialog = play_confirmation_scene.instantiate()
play_confirmation_instance.confirmed.connect(_run_opening_scene.bind(target_path))
play_confirmation_instance.canceled.connect(_check_main_scene_needs_updating.bind(target_path))
add_child(play_confirmation_instance)
func _open_delete_examples_confirmation_dialog(target_path : String):
var delete_confirmation_scene : PackedScene = load(get_plugin_path() + "installer/delete_examples_confirmation_dialog.tscn")
var delete_confirmation_instance : ConfirmationDialog = delete_confirmation_scene.instantiate()
delete_confirmation_instance.confirmed.connect(_delete_source_examples_directory.bind(target_path))
delete_confirmation_instance.canceled.connect(_check_main_scene_needs_updating.bind(target_path))
add_child(delete_confirmation_instance)
func _open_delete_examples_short_confirmation_dialog():
var delete_confirmation_scene : PackedScene = load(get_plugin_path() + "installer/delete_examples_short_confirmation_dialog.tscn")
var delete_confirmation_instance : ConfirmationDialog = delete_confirmation_scene.instantiate()
delete_confirmation_instance.confirmed.connect(_delete_source_examples_directory)
add_child(delete_confirmation_instance)
func _run_opening_scene(target_path : String):
var opening_scene_path = target_path + MAIN_SCENE_RELATIVE_PATH
EditorInterface.play_custom_scene(opening_scene_path)
var timer: Timer = Timer.new()
var callable := func():
if EditorInterface.is_playing_scene(): return
timer.stop()
_open_delete_examples_confirmation_dialog(target_path)
timer.queue_free()
timer.timeout.connect(callable)
add_child(timer)
timer.start(RESAVING_DELAY)
func _delete_directory_recursive(dir_path : String):
if not dir_path.ends_with("/"):
dir_path += "/"
var dir = DirAccess.open(dir_path)
if dir:
dir.list_dir_begin()
var file_name = dir.get_next()
var error : Error
while file_name != "" and error == 0:
var relative_path = dir_path.trim_prefix(get_plugin_examples_path())
var full_file_path = dir_path + file_name
if dir.current_is_dir():
_delete_directory_recursive(full_file_path)
else:
error = dir.remove(file_name)
file_name = dir.get_next()
if error:
push_error("plugin error - deleting path: %s" % error)
else:
push_error("plugin error - accessing path: %s" % dir)
dir.remove(dir_path)
func _delete_source_examples_directory(target_path : String = ""):
var examples_path = get_plugin_examples_path()
var dir := DirAccess.open("res://")
if dir.dir_exists(examples_path):
_delete_directory_recursive(examples_path)
EditorInterface.get_resource_filesystem().scan()
remove_tool_menu_item("Copy " + _get_plugin_name() + " Examples...")
remove_tool_menu_item("Delete " + _get_plugin_name() + " Examples...")
if not target_path.is_empty():
_check_main_scene_needs_updating(target_path)
func _replace_file_contents(file_path : String, target_path : String):
var extension : String = file_path.get_extension()
if extension == "import":
# skip import files
return OK
var file = FileAccess.open(file_path, FileAccess.READ)
var regex = RegEx.new()
regex.compile(UID_PREG_MATCH)
if file == null:
push_error("plugin error - null file: `%s`" % file_path)
return
var original_content = file.get_as_text()
var replaced_content = regex.sub(original_content, "", true)
replaced_content = replaced_content.replace(get_plugin_examples_path(), target_path)
file.close()
if replaced_content == original_content: return
file = FileAccess.open(file_path, FileAccess.WRITE)
file.store_string(replaced_content)
file.close()
func _save_resource(resource_path : String, resource_destination : String, whitelisted_extensions : PackedStringArray = []) -> Error:
var extension : String = resource_path.get_extension()
if whitelisted_extensions.size() > 0:
if not extension in whitelisted_extensions:
return OK
if extension == "import":
# skip import files
return OK
var file_object = load(resource_path)
if file_object is Resource:
var possible_extensions = ResourceSaver.get_recognized_extensions(file_object)
if possible_extensions.has(extension):
return ResourceSaver.save(file_object, resource_destination, ResourceSaver.FLAG_CHANGE_PATH)
else:
return ERR_FILE_UNRECOGNIZED
else:
return ERR_FILE_UNRECOGNIZED
return OK
func _delayed_reimporting_file(file_path : String):
var timer: Timer = Timer.new()
var callable := func():
timer.stop()
var file_system = EditorInterface.get_resource_filesystem()
file_system.reimport_files([file_path])
timer.queue_free()
timer.timeout.connect(callable)
add_child(timer)
timer.start(REIMPORT_FILE_DELAY)
func _raw_copy_file_path(file_path : String, destination_path : String) -> Error:
var dir := DirAccess.open("res://")
var error := dir.copy(file_path, destination_path)
if not error:
EditorInterface.get_resource_filesystem().update_file(destination_path)
return error
func _copy_override_file():
var override_path : String = get_plugin_path() + OVERRIDE_RELATIVE_PATH
_raw_copy_file_path(override_path, "res://"+override_path.get_file())
func _copy_file_path(file_path : String, destination_path : String, target_path : String, raw_copy_file_extensions : PackedStringArray = []) -> Error:
if file_path.get_extension() in raw_copy_file_extensions:
# Markdown file format
return _raw_copy_file_path(file_path, destination_path)
var error = _save_resource(file_path, destination_path)
if error == ERR_FILE_UNRECOGNIZED:
# Copy image files and other assets
error = _raw_copy_file_path(file_path, destination_path)
# Reimport image files to create new .import
if not error:
_delayed_reimporting_file(destination_path)
return error
if not error:
_replace_file_contents(destination_path, target_path)
return error
func _copy_directory_path(dir_path : String, target_path : String, raw_copy_file_extensions : PackedStringArray = []):
if not dir_path.ends_with("/"):
dir_path += "/"
var dir = DirAccess.open(dir_path)
if dir:
dir.list_dir_begin()
var file_name = dir.get_next()
var error : Error
while file_name != "" and error == 0:
var relative_path = dir_path.trim_prefix(get_plugin_examples_path())
var destination_path = target_path + relative_path + file_name
var full_file_path = dir_path + file_name
if dir.current_is_dir():
if not dir.dir_exists(destination_path):
error = dir.make_dir(destination_path)
_copy_directory_path(full_file_path, target_path, raw_copy_file_extensions)
else:
error = _copy_file_path(full_file_path, destination_path, target_path, raw_copy_file_extensions)
file_name = dir.get_next()
if error:
push_error("plugin error - copying path: %s" % error)
else:
push_error("plugin error - accessing path: %s" % dir_path)
func _update_scene_loader_path(target_path : String):
var file_path : String = get_plugin_path() + SCENE_LOADER_RELATIVE_PATH
var file_text : String = FileAccess.get_file_as_string(file_path)
var prefix : String = "loading_screen_path = \""
var target_string = prefix + get_plugin_path() + "base/"
var replacing_string = prefix + target_path
file_text = file_text.replace(target_string, replacing_string)
var file = FileAccess.open(file_path, FileAccess.WRITE)
file.store_string(file_text)
file.close()
func _delayed_saving_and_check_main_scene(target_path : String):
var timer: Timer = Timer.new()
var callable := func():
timer.stop()
EditorInterface.get_resource_filesystem().scan()
EditorInterface.save_all_scenes()
_open_play_opening_confirmation_dialog(target_path)
timer.queue_free()
timer.timeout.connect(callable)
add_child(timer)
timer.start(RESAVING_DELAY)
func _copy_to_directory(target_path : String):
ProjectSettings.set_setting(PROJECT_SETTINGS_PATH + "copy_path", target_path)
ProjectSettings.save()
if not target_path.ends_with("/"):
target_path += "/"
_copy_directory_path(get_plugin_examples_path(), target_path, ["md"])
_update_scene_loader_path(target_path)
_copy_override_file()
_delayed_saving_and_check_main_scene(target_path)
func _open_path_dialog():
var destination_scene : PackedScene = load(get_plugin_path() + "installer/destination_dialog.tscn")
var destination_instance : FileDialog = destination_scene.instantiate()
destination_instance.dir_selected.connect(_copy_to_directory)
destination_instance.canceled.connect(_check_main_scene_needs_updating.bind(get_plugin_examples_path()))
add_child(destination_instance)
func _open_confirmation_dialog():
var confirmation_scene : PackedScene = load(get_plugin_path() + "installer/copy_confirmation_dialog.tscn")
var confirmation_instance : ConfirmationDialog = confirmation_scene.instantiate()
confirmation_instance.confirmed.connect(_open_path_dialog)
confirmation_instance.canceled.connect(_check_main_scene_needs_updating.bind(get_plugin_examples_path()))
add_child(confirmation_instance)
func _show_plugin_dialogues():
if ProjectSettings.has_setting(PROJECT_SETTINGS_PATH + "disable_plugin_dialogues") :
if ProjectSettings.get_setting(PROJECT_SETTINGS_PATH + "disable_plugin_dialogues") :
return
_open_confirmation_dialog()
ProjectSettings.set_setting(PROJECT_SETTINGS_PATH + "disable_plugin_dialogues", true)
ProjectSettings.save()
func _resave_if_recently_opened():
if Engine.get_physics_frames() < MAX_PHYSICS_FRAMES_FROM_START:
var timer: Timer = Timer.new()
var callable := func():
if Engine.get_frames_per_second() >= 10:
timer.stop()
EditorInterface.save_scene()
timer.queue_free()
timer.timeout.connect(callable)
add_child(timer)
timer.start(OPEN_EDITOR_DELAY)
func _add_copy_tool_if_examples_exists():
var examples_path = get_plugin_examples_path()
var dir := DirAccess.open("res://")
if dir.dir_exists(examples_path):
add_tool_menu_item("Copy " + _get_plugin_name() + " Examples...", _open_path_dialog)
add_tool_menu_item("Delete " + _get_plugin_name() + " Examples...", _open_delete_examples_short_confirmation_dialog)
func _remove_copy_tool_if_examples_exists():
var examples_path = get_plugin_examples_path()
var dir := DirAccess.open("res://")
if dir.dir_exists(examples_path):
remove_tool_menu_item("Copy " + _get_plugin_name() + " Examples...")
remove_tool_menu_item("Delete " + _get_plugin_name() + " Examples...")
func _enter_tree():
add_autoload_singleton("AppConfig", get_plugin_path() + "base/scenes/autoloads/app_config.tscn")
add_autoload_singleton("SceneLoader", get_plugin_path() + "base/scenes/autoloads/scene_loader.tscn")
add_autoload_singleton("ProjectMusicController", get_plugin_path() + "base/scenes/autoloads/project_music_controller.tscn")
add_autoload_singleton("ProjectUISoundController", get_plugin_path() + "base/scenes/autoloads/project_ui_sound_controller.tscn")
_add_copy_tool_if_examples_exists()
_show_plugin_dialogues()
_resave_if_recently_opened()
func _exit_tree():
remove_autoload_singleton("AppConfig")
remove_autoload_singleton("SceneLoader")
remove_autoload_singleton("ProjectMusicController")
remove_autoload_singleton("ProjectUISoundController")
_remove_copy_tool_if_examples_exists()

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save