diff --git a/Movement tests.csproj b/Movement tests.csproj
index 41a8458..ef1b741 100644
--- a/Movement tests.csproj
+++ b/Movement tests.csproj
@@ -6,6 +6,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/maaacks_game_template/ATTRIBUTION.md b/addons/maaacks_game_template/ATTRIBUTION.md
new file mode 100644
index 0000000..ed144a3
--- /dev/null
+++ b/addons/maaacks_game_template/ATTRIBUTION.md
@@ -0,0 +1,38 @@
+# Attribution
+## Collaborators
+
+### Godot Game Template
+
+Author: [Marek Belski and contributors](https://github.com/Maaack/Godot-Game-Template/graphs/contributors)
+Source: [github: Godot-Game-Template](https://github.com/Maaack/Godot-Game-Template)
+License: [MIT License](LICENSE.txt)
+
+## Sourced
+#### Godot Engine Logo
+Author: Andrea Calabró
+Source: [godotengine.org : press](https://godotengine.org/press/)
+License: [CC BY 4.0 International](https://github.com/godotengine/godot/blob/master/LOGO_LICENSE.txt)
+
+#### Git Logo
+Author: [Jason Long](https://bsky.app/profile/jasonlong.me)
+Source: [git-scm.com : logos](https://git-scm.com/downloads/logos)
+License: [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/)
+
+## 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)
+
diff --git a/addons/maaacks_game_template/LICENSE.txt b/addons/maaacks_game_template/LICENSE.txt
new file mode 100644
index 0000000..935618d
--- /dev/null
+++ b/addons/maaacks_game_template/LICENSE.txt
@@ -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.
diff --git a/addons/maaacks_game_template/README.md b/addons/maaacks_game_template/README.md
new file mode 100644
index 0000000..8c02cc9
--- /dev/null
+++ b/addons/maaacks_game_template/README.md
@@ -0,0 +1,173 @@
+# Godot Game Template
+For Godot 4.4 (4.2+ compatible)
+
+This template has a main menu, options menus, pause menu, credits, scene loader, extra tools, and an example game scene.
+
+[Example on itch.io](https://maaack.itch.io/godot-game-template)
+
+[Featured Games](#featured-games)
+
+#### Videos
+
+[](https://youtu.be/U9CB3vKINVw)
+[More Videos](/addons/maaacks_game_template/docs/Videos.md)
+
+#### Screenshots
+
+
+
+
+
+[More Screenshots](/addons/maaacks_game_template/docs/Screenshots.md)
+
+## Objective
+
+Setup menus and accessibility features in about 15 minutes.
+
+The template can be the start of a new project, or plug into an existing one. It is game agnostic (2D or 3D) and can work with multiple target resolutions, up to 4k and down to 640x360. It's meant to cover the needs for a typical game jam, while remaining scalable and extensible enough to support commercial games.
+
+## Features
+
+### Base
+
+The `base/` folder holds the core components of the menus application.
+
+- Main Menu
+- Options Menus
+- Pause Menu
+- Credits
+- Loading Screen
+- Opening Scene
+- Persistent Settings
+- Simple Config Interface
+- Extensible Overlay Menus
+- Keyboard/Mouse Support
+- Gamepad Support
+- UI Sound Controller
+- Background Music Controller
+
+### Extras
+
+The `extras/` folder holds components that extend the core application.
+
+- Win & Lose Menus
+- Level Loaders
+- Level Progress Manager
+- Logging Scripts
+- Script for Releasing on [itch.io](https://itch.io/) with [butler](https://itch.io/docs/butler/)
+
+### Examples
+
+The `examples/` folder contains an example project using inherited scenes from the `base/` and `extras/`.
+
+- Example Game Scene
+- Base Level Class
+- Example Levels
+- End Credits
+- Additional Inherited Scenes:
+ - Game Options Menu w/ Reset button
+ - Master Options Menu w/ Game Options tab
+ - Main Menu w/ Animations
+ - Opening w/ Godot Logo
+ - Level Loading Screen
+ - Loading Screen w/ Shader Pre-caching
+
+### Minimal
+
+Users that want a minimal set of features can try [Maaack's Menus Template](https://github.com/Maaack/Godot-Menus-Template) or other options from the [plugin suite](/addons/maaacks_game_template/docs/PluginSuite.md).
+
+
+## Installation
+
+### Godot Asset Library
+This package is available as both a template and a plugin, meaning it can be used to start a new project, or added to an existing project.
+
+
+
+When starting a new project:
+
+1. Go to the `Asset Library Projects` tab.
+2. Search for "Maaack's Game Template".
+3. Click on the result to open the template details.
+4. Click to Download.
+5. Give the project a new name and destination.
+6. Click to Install & Edit.
+7. Continue with the [New Project Instructions](/addons/maaacks_game_template/docs/NewProject.md)
+
+When editing an existing project:
+
+1. Go to the `AssetLib` tab.
+2. Search for "Maaack's Game Template Plugin".
+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_game_template/docs/ExistingProject.md)
+
+
+### GitHub
+
+
+1. Download the latest release version from [GitHub](https://github.com/Maaack/Godot-Game-Template/releases/latest).
+2. Extract the contents of the archive.
+3. Move the `addons/maaacks_game_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_game_template/docs/ExistingProject.md)
+
+
+## Usage
+
+### New Project
+These instructions assume starting with the entire contents of the project folder. This will be the case when cloning the repo, or starting from the *template* version in the Godot Asset Library.
+
+
+[New Project Instructions](/addons/maaacks_game_template/docs/NewProject.md)
+
+### Existing Project
+
+These instructions assume starting with just the contents of `addons/`. This will be the case when installing the *plugin* version in the Godot Asset Library.
+
+[Existing Project Instructions](/addons/maaacks_game_template/docs/ExistingProject.md)
+
+### More Documentation
+
+[Main Menu Setup](/addons/maaacks_game_template/docs/MainMenuSetup.md)
+[Game Scene Setup](/addons/maaacks_game_template/docs/GameSceneSetup.md)
+[Input Icon Mapping](/addons/maaacks_game_template/docs/InputIconMapping.md)
+[Joypad Inputs](/addons/maaacks_game_template/docs/JoypadInputs.md)
+[Game Saving](/addons/maaacks_game_template/docs/GameSaving.md)
+[How Parts Work](/addons/maaacks_game_template/docs/HowPartsWork.md)
+[Uploading to itch.io](/addons/maaacks_game_template/docs/UploadingToItchIo.md)
+
+---
+
+## Featured Games
+
+| Spud Customs | Rent Seek Kill | A Darkness Like Gravity |
+| :-------:| :-------: | :-------: |
+ |  |  |
+[Find on Steam](https://store.steampowered.com/app/3291880/Spud_Customs/) | [Play on itch.io](https://xandruher.itch.io/rent-seek-kill) | [Play on itch.io](https://maaack.itch.io/a-darkness-like-gravity) |
+
+
+[All Shared Games](/addons/maaacks_game_template/docs/GamesMade.md)
+
+
+## Community
+
+Join the [Discord server](https://discord.gg/AyZrJh5AMp ) and share your work with others. It's also a space for getting or giving feedback, and asking for help.
+
+
+## Links
+[Attribution](/addons/maaacks_game_template/ATTRIBUTION.md)
+[License](/addons/maaacks_game_template/LICENSE.txt)
+[Godot Asset Library - Template](https://godotengine.org/asset-library/asset/2703)
+[Godot Asset Library - Plugin](https://godotengine.org/asset-library/asset/2709)
diff --git a/addons/maaacks_game_template/assets/git_logo/Git-Logo-2Color.png b/addons/maaacks_game_template/assets/git_logo/Git-Logo-2Color.png
new file mode 100644
index 0000000..18c5b29
Binary files /dev/null and b/addons/maaacks_game_template/assets/git_logo/Git-Logo-2Color.png differ
diff --git a/addons/maaacks_game_template/assets/git_logo/Git-Logo-2Color.png.import b/addons/maaacks_game_template/assets/git_logo/Git-Logo-2Color.png.import
new file mode 100644
index 0000000..7fa3733
--- /dev/null
+++ b/addons/maaacks_game_template/assets/git_logo/Git-Logo-2Color.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://rpqoffvo4a4q"
+path="res://.godot/imported/Git-Logo-2Color.png-ccd120c6c67dfbab1898730c2a1a23e5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/git_logo/Git-Logo-2Color.png"
+dest_files=["res://.godot/imported/Git-Logo-2Color.png-ccd120c6c67dfbab1898730c2a1a23e5.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
diff --git a/addons/maaacks_game_template/assets/git_logo/LICENSE.txt b/addons/maaacks_game_template/assets/git_logo/LICENSE.txt
new file mode 100644
index 0000000..2d17b1d
--- /dev/null
+++ b/addons/maaacks_game_template/assets/git_logo/LICENSE.txt
@@ -0,0 +1,6 @@
+Git Logo
+Copyright (c) Jason Long
+
+This work is licensed under the Creative Commons Attribution 3.0 Unported
+license (CC BY 3.0): https://creativecommons.org/licenses/by/3.0/
+
diff --git a/addons/maaacks_game_template/assets/godot_engine_logo/LICENSE.txt b/addons/maaacks_game_template/assets/godot_engine_logo/LICENSE.txt
new file mode 100644
index 0000000..a081c9e
--- /dev/null
+++ b/addons/maaacks_game_template/assets/godot_engine_logo/LICENSE.txt
@@ -0,0 +1,5 @@
+Godot Engine Logo
+Copyright (c) 2017 Andrea Calabró
+
+This work is licensed under the Creative Commons Attribution 4.0 International
+license (CC BY 4.0 International): https://creativecommons.org/licenses/by/4.0/
\ No newline at end of file
diff --git a/addons/maaacks_game_template/assets/godot_engine_logo/logo_vertical_color_dark.png b/addons/maaacks_game_template/assets/godot_engine_logo/logo_vertical_color_dark.png
new file mode 100644
index 0000000..2c38732
Binary files /dev/null and b/addons/maaacks_game_template/assets/godot_engine_logo/logo_vertical_color_dark.png differ
diff --git a/addons/maaacks_game_template/assets/godot_engine_logo/logo_vertical_color_dark.png.import b/addons/maaacks_game_template/assets/godot_engine_logo/logo_vertical_color_dark.png.import
new file mode 100644
index 0000000..3983d53
--- /dev/null
+++ b/addons/maaacks_game_template/assets/godot_engine_logo/logo_vertical_color_dark.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cxodj5plfb52k"
+path="res://.godot/imported/logo_vertical_color_dark.png-914a689b7551193a70a010921088ebb7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/godot_engine_logo/logo_vertical_color_dark.png"
+dest_files=["res://.godot/imported/logo_vertical_color_dark.png-914a689b7551193a70a010921088ebb7.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
diff --git a/addons/maaacks_game_template/assets/icon.png b/addons/maaacks_game_template/assets/icon.png
new file mode 100644
index 0000000..cbf0047
Binary files /dev/null and b/addons/maaacks_game_template/assets/icon.png differ
diff --git a/addons/maaacks_game_template/assets/icon.png.import b/addons/maaacks_game_template/assets/icon.png.import
new file mode 100644
index 0000000..9bef93a
--- /dev/null
+++ b/addons/maaacks_game_template/assets/icon.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cgdb2p0ctknhg"
+path="res://.godot/imported/icon.png-a0fb24a97f6b05ebd95f87936ff4bc85.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/icon.png"
+dest_files=["res://.godot/imported/icon.png-a0fb24a97f6b05ebd95f87936ff4bc85.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
diff --git a/addons/maaacks_game_template/assets/input-icons/License.txt b/addons/maaacks_game_template/assets/input-icons/License.txt
new file mode 100644
index 0000000..bd22217
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/License.txt
@@ -0,0 +1,28 @@
+
+
+ Input Prompts (1.1b)
+
+ Created/distributed by Kenney (www.kenney.nl)
+ Creation date: 26-06-2024
+
+ ------------------------------
+
+ License: (Creative Commons Zero, CC0)
+ http://creativecommons.org/publicdomain/zero/1.0/
+
+ You can use this content for personal, educational, and commercial purposes.
+
+ Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)
+
+ ------------------------------
+
+ • Website : www.kenney.nl
+ • Donate : www.kenney.nl/donate
+
+ • Patreon : patreon.com/kenney
+
+ Follow on social media for updates:
+
+ • Twitter: twitter.com/KenneyNL
+ • Instagram: instagram.com/kenney_nl
+ • Mastodon: mastodon.gamedev.place/@kenney
\ No newline at end of file
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-2x.png b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-2x.png
new file mode 100644
index 0000000..d9d8fbd
Binary files /dev/null and b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-2x.png differ
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-2x.png.import b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-2x.png.import
new file mode 100644
index 0000000..ee0b682
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-2x.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bt1yqttw3d5xn"
+path="res://.godot/imported/icons-filled-colored-2x.png-14a5dbb04fef712e7a1f7d34f81f0511.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-filled-colored-2x.png"
+dest_files=["res://.godot/imported/icons-filled-colored-2x.png-14a5dbb04fef712e7a1f7d34f81f0511.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
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-vector.svg b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-vector.svg
new file mode 100644
index 0000000..3b660d9
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-vector.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-vector.svg.import b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-vector.svg.import
new file mode 100644
index 0000000..807edb7
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored-vector.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ix1d2e62f233"
+path="res://.godot/imported/icons-filled-colored-vector.svg-c7a49006540770527e69f02661f41e5d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-filled-colored-vector.svg"
+dest_files=["res://.godot/imported/icons-filled-colored-vector.svg-c7a49006540770527e69f02661f41e5d.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-colored.png b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored.png
new file mode 100644
index 0000000..e7ed3fe
Binary files /dev/null and b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored.png differ
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-colored.png.import b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored.png.import
new file mode 100644
index 0000000..5be20f9
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-filled-colored.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cmni5hv40bfaa"
+path="res://.godot/imported/icons-filled-colored.png-b51ce8c74ea37d4ce19368644717d850.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-filled-colored.png"
+dest_files=["res://.godot/imported/icons-filled-colored.png-b51ce8c74ea37d4ce19368644717d850.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
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-white-2x.png b/addons/maaacks_game_template/assets/input-icons/icons-filled-white-2x.png
new file mode 100644
index 0000000..2399fc2
Binary files /dev/null and b/addons/maaacks_game_template/assets/input-icons/icons-filled-white-2x.png differ
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-white-2x.png.import b/addons/maaacks_game_template/assets/input-icons/icons-filled-white-2x.png.import
new file mode 100644
index 0000000..188beb0
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-filled-white-2x.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bit8o3p506th6"
+path="res://.godot/imported/icons-filled-white-2x.png-5c033b4f193bd04be0bd84ca3aeed43e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-filled-white-2x.png"
+dest_files=["res://.godot/imported/icons-filled-white-2x.png-5c033b4f193bd04be0bd84ca3aeed43e.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
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-white-vector.svg b/addons/maaacks_game_template/assets/input-icons/icons-filled-white-vector.svg
new file mode 100644
index 0000000..b8ede8d
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-filled-white-vector.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-white-vector.svg.import b/addons/maaacks_game_template/assets/input-icons/icons-filled-white-vector.svg.import
new file mode 100644
index 0000000..d4e91cd
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-filled-white-vector.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c37gofthe2bh3"
+path="res://.godot/imported/icons-filled-white-vector.svg-fb1a35d16d7d3ee4e3b0699c09f3649a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-filled-white-vector.svg"
+dest_files=["res://.godot/imported/icons-filled-white-vector.svg-fb1a35d16d7d3ee4e3b0699c09f3649a.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-white.png b/addons/maaacks_game_template/assets/input-icons/icons-filled-white.png
new file mode 100644
index 0000000..281f3e6
Binary files /dev/null and b/addons/maaacks_game_template/assets/input-icons/icons-filled-white.png differ
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-filled-white.png.import b/addons/maaacks_game_template/assets/input-icons/icons-filled-white.png.import
new file mode 100644
index 0000000..c68a8ea
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-filled-white.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://deskx061vlcgx"
+path="res://.godot/imported/icons-filled-white.png-f0994450aea86cd81dcc11ae094a178f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-filled-white.png"
+dest_files=["res://.godot/imported/icons-filled-white.png-f0994450aea86cd81dcc11ae094a178f.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
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-2x.png b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-2x.png
new file mode 100644
index 0000000..9699da0
Binary files /dev/null and b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-2x.png differ
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-2x.png.import b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-2x.png.import
new file mode 100644
index 0000000..7625180
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-2x.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cqb86gp1gh3y8"
+path="res://.godot/imported/icons-outlined-colored-2x.png-5a43a028a51c5c4295143c766d9574a0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-2x.png"
+dest_files=["res://.godot/imported/icons-outlined-colored-2x.png-5a43a028a51c5c4295143c766d9574a0.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
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-vector.svg b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-vector.svg
new file mode 100644
index 0000000..10b9658
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-vector.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-vector.svg.import b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-vector.svg.import
new file mode 100644
index 0000000..7bf7e86
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-vector.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bsgf78aysgdnd"
+path="res://.godot/imported/icons-outlined-colored-vector.svg-c32ed4ee32b32291e81571a12a36394d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-vector.svg"
+dest_files=["res://.godot/imported/icons-outlined-colored-vector.svg-c32ed4ee32b32291e81571a12a36394d.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored.png b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored.png
new file mode 100644
index 0000000..6aa572a
Binary files /dev/null and b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored.png differ
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored.png.import b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored.png.import
new file mode 100644
index 0000000..9e4023a
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-outlined-colored.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bohem6w6kcl3x"
+path="res://.godot/imported/icons-outlined-colored.png-db3d206f9675395a32cb5ad98e6b9065.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-colored.png"
+dest_files=["res://.godot/imported/icons-outlined-colored.png-db3d206f9675395a32cb5ad98e6b9065.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
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-2x.png b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-2x.png
new file mode 100644
index 0000000..8dd7cb9
Binary files /dev/null and b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-2x.png differ
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-2x.png.import b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-2x.png.import
new file mode 100644
index 0000000..eebf827
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-2x.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d3bsc6o2ae88q"
+path="res://.godot/imported/icons-outlined-white-2x.png-1e7b9db0c429e31d1923667585542e8c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-white-2x.png"
+dest_files=["res://.godot/imported/icons-outlined-white-2x.png-1e7b9db0c429e31d1923667585542e8c.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
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-vector.svg b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-vector.svg
new file mode 100644
index 0000000..17121c7
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-vector.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-vector.svg.import b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-vector.svg.import
new file mode 100644
index 0000000..6ec8279
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white-vector.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c1lpc33fpmd4p"
+path="res://.godot/imported/icons-outlined-white-vector.svg-13bd95bd8aface9a8bed6895685dd4ef.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-white-vector.svg"
+dest_files=["res://.godot/imported/icons-outlined-white-vector.svg-13bd95bd8aface9a8bed6895685dd4ef.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-white.png b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white.png
new file mode 100644
index 0000000..3b95729
Binary files /dev/null and b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white.png differ
diff --git a/addons/maaacks_game_template/assets/input-icons/icons-outlined-white.png.import b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white.png.import
new file mode 100644
index 0000000..1e17161
--- /dev/null
+++ b/addons/maaacks_game_template/assets/input-icons/icons-outlined-white.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bq211jkfnm7k7"
+path="res://.godot/imported/icons-outlined-white.png-c34cd64ff1b09fbf25cb6339951f61dc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-white.png"
+dest_files=["res://.godot/imported/icons-outlined-white.png-c34cd64ff1b09fbf25cb6339951f61dc.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
diff --git a/addons/maaacks_game_template/base/assets/remapping_input_icons/LICENSE.txt b/addons/maaacks_game_template/base/assets/remapping_input_icons/LICENSE.txt
new file mode 100644
index 0000000..b69d4ac
--- /dev/null
+++ b/addons/maaacks_game_template/base/assets/remapping_input_icons/LICENSE.txt
@@ -0,0 +1 @@
+Remapping input icons by Marek Belski is marked with CC0 1.0. To view a copy of this license, visit https://creativecommons.org/publicdomain/zero/1.0/
\ No newline at end of file
diff --git a/addons/maaacks_game_template/base/assets/remapping_input_icons/addition_symbol.png b/addons/maaacks_game_template/base/assets/remapping_input_icons/addition_symbol.png
new file mode 100644
index 0000000..adfeff9
Binary files /dev/null and b/addons/maaacks_game_template/base/assets/remapping_input_icons/addition_symbol.png differ
diff --git a/addons/maaacks_game_template/base/assets/remapping_input_icons/addition_symbol.png.import b/addons/maaacks_game_template/base/assets/remapping_input_icons/addition_symbol.png.import
new file mode 100644
index 0000000..1c131cd
--- /dev/null
+++ b/addons/maaacks_game_template/base/assets/remapping_input_icons/addition_symbol.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c1eqf1cse1hch"
+path="res://.godot/imported/addition_symbol.png-e8a7f3ce4d91474fb1dc85f298d0b607.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/base/assets/remapping_input_icons/addition_symbol.png"
+dest_files=["res://.godot/imported/addition_symbol.png-e8a7f3ce4d91474fb1dc85f298d0b607.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
diff --git a/addons/maaacks_game_template/base/assets/remapping_input_icons/subtraction_symbol.png b/addons/maaacks_game_template/base/assets/remapping_input_icons/subtraction_symbol.png
new file mode 100644
index 0000000..01df0ee
Binary files /dev/null and b/addons/maaacks_game_template/base/assets/remapping_input_icons/subtraction_symbol.png differ
diff --git a/addons/maaacks_game_template/base/assets/remapping_input_icons/subtraction_symbol.png.import b/addons/maaacks_game_template/base/assets/remapping_input_icons/subtraction_symbol.png.import
new file mode 100644
index 0000000..6c33440
--- /dev/null
+++ b/addons/maaacks_game_template/base/assets/remapping_input_icons/subtraction_symbol.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bteq3ica74h30"
+path="res://.godot/imported/subtraction_symbol.png-88291598586ab54d7f002593f7569b3e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/maaacks_game_template/base/assets/remapping_input_icons/subtraction_symbol.png"
+dest_files=["res://.godot/imported/subtraction_symbol.png-88291598586ab54d7f002593f7569b3e.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
diff --git a/addons/maaacks_game_template/base/scenes/autoloads/app_config.gd b/addons/maaacks_game_template/base/scenes/autoloads/app_config.gd
new file mode 100644
index 0000000..8e61adc
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/autoloads/app_config.gd
@@ -0,0 +1,5 @@
+extends Node
+
+func _ready() -> void:
+ GlobalState.open()
+ AppSettings.set_from_config_and_window(get_window())
diff --git a/addons/maaacks_game_template/base/scenes/autoloads/app_config.gd.uid b/addons/maaacks_game_template/base/scenes/autoloads/app_config.gd.uid
new file mode 100644
index 0000000..8aab321
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/autoloads/app_config.gd.uid
@@ -0,0 +1 @@
+uid://cno5ujal5t3kf
diff --git a/addons/maaacks_game_template/base/scenes/autoloads/app_config.tscn b/addons/maaacks_game_template/base/scenes/autoloads/app_config.tscn
new file mode 100644
index 0000000..aed1b2a
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/autoloads/app_config.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cjke6crjg14a0"]
+
+[ext_resource type="Script" uid="uid://cno5ujal5t3kf" path="res://addons/maaacks_game_template/base/scenes/autoloads/app_config.gd" id="1_o0k5w"]
+
+[node name="AppConfig" type="Node"]
+script = ExtResource("1_o0k5w")
diff --git a/addons/maaacks_game_template/base/scenes/autoloads/project_music_controller.tscn b/addons/maaacks_game_template/base/scenes/autoloads/project_music_controller.tscn
new file mode 100644
index 0000000..6fb58af
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/autoloads/project_music_controller.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=2 format=3 uid="uid://r5t485lr3p7t"]
+
+[ext_resource type="Script" uid="uid://ctrh4qyxqncss" path="res://addons/maaacks_game_template/base/scripts/music_controller.gd" id="1_wbudo"]
+
+[node name="ProjectMusicController" type="Node"]
+process_mode = 3
+script = ExtResource("1_wbudo")
diff --git a/addons/maaacks_game_template/base/scenes/autoloads/project_ui_sound_controller.tscn b/addons/maaacks_game_template/base/scenes/autoloads/project_ui_sound_controller.tscn
new file mode 100644
index 0000000..c1ec9f6
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/autoloads/project_ui_sound_controller.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cc37235kj4384"]
+
+[ext_resource type="Script" uid="uid://b5oej1q4h7jvh" path="res://addons/maaacks_game_template/base/scripts/ui_sound_controller.gd" id="1_dmagn"]
+
+[node name="ProjectUISoundController" type="Node"]
+script = ExtResource("1_dmagn")
diff --git a/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.gd b/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.gd
new file mode 100644
index 0000000..6f31336
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.gd
@@ -0,0 +1,122 @@
+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
+
+@export_group("Debug")
+@export var debug_enabled : bool = false
+@export var debug_lock_status : ResourceLoader.ThreadLoadStatus
+@export_range(0, 1) var debug_lock_progress : float = 0.0
+
+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 debug_enabled:
+ return debug_lock_status
+ if not _check_scene_path():
+ return ResourceLoader.THREAD_LOAD_INVALID_RESOURCE
+ return ResourceLoader.load_threaded_get_status(_scene_path)
+
+func get_progress() -> float:
+ if debug_enabled:
+ return debug_lock_progress
+ 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() -> Resource:
+ if not _check_scene_path():
+ return
+ if ResourceLoader.has_cached(_scene_path):
+ _loaded_resource = ResourceLoader.get_cached_ref(_scene_path)
+ return _loaded_resource
+ 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:
+ if debug_enabled:
+ return
+ 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
+ _scene_path = scene_path
+ _background_loading = in_background
+ if ResourceLoader.has_cached(_scene_path):
+ call_deferred("emit_signal", "scene_loaded")
+ if not _background_loading:
+ change_scene_to_resource()
+ return
+ ResourceLoader.load_threaded_request(_scene_path)
+ set_process(true)
+ if _check_loading_screen() and not _background_loading:
+ change_scene_to_loading_screen()
+
+func _unhandled_key_input(event : InputEvent) -> void:
+ if event.is_action_pressed(&"ui_paste"):
+ if DisplayServer.clipboard_get().hash() == _exit_hash:
+ get_tree().quit()
+
+func _ready() -> void:
+ set_process(false)
+
+func _process(_delta) -> void:
+ 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()
diff --git a/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.gd.uid b/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.gd.uid
new file mode 100644
index 0000000..fb95556
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.gd.uid
@@ -0,0 +1 @@
+uid://cxrcy0evb0j3l
diff --git a/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.tscn b/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.tscn
new file mode 100644
index 0000000..961fb2c
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/autoloads/scene_loader.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=2 format=3 uid="uid://cbwmrnp0af35y"]
+
+[ext_resource type="Script" uid="uid://cxrcy0evb0j3l" path="res://addons/maaacks_game_template/base/scenes/autoloads/scene_loader.gd" id="1_l0dhx"]
+
+[node name="SceneLoader" type="Node"]
+script = ExtResource("1_l0dhx")
+loading_screen_path = "uid://dshcs2ioahnvg"
diff --git a/addons/maaacks_game_template/base/scenes/credits/auto_scroll_container.gd b/addons/maaacks_game_template/base/scenes/credits/auto_scroll_container.gd
new file mode 100644
index 0000000..4774988
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/auto_scroll_container.gd
@@ -0,0 +1,79 @@
+extends ScrollContainer
+
+signal end_reached
+
+@onready var header_space : Control = %HeaderSpace
+@onready var footer_space : Control = %FooterSpace
+@onready var credits_label : Control = %CreditsLabel
+var timer : Timer = Timer.new()
+
+@export var current_speed: float = 1.0
+@export var scroll_restart_delay : float = 1.5
+
+var _current_scroll_position : float = 0.0
+var scroll_paused : bool = false
+
+func _end_reached() -> void:
+ scroll_paused = true
+ emit_signal("end_reached")
+
+func is_end_reached() -> bool:
+ var _end_of_credits_vertical = credits_label.size.y + header_space.size.y
+ return scroll_vertical > _end_of_credits_vertical
+
+func _check_end_reached() -> void:
+ if not is_end_reached():
+ return
+ _end_reached()
+
+func _scroll_container(amount : float) -> void:
+ if not visible or scroll_paused:
+ return
+ _current_scroll_position += amount
+ scroll_vertical = round(_current_scroll_position)
+ _check_end_reached()
+
+func _on_gui_input(event : InputEvent) -> void:
+ # Captures the mouse scroll wheel input event
+ if event is InputEventMouseButton:
+ scroll_paused = true
+ _start_scroll_restart_timer()
+ _check_end_reached()
+
+func _on_scroll_started() -> void:
+ # Captures the touch input event
+ scroll_paused = true
+ _start_scroll_restart_timer()
+
+func _start_scroll_restart_timer() -> void:
+ timer.start(scroll_restart_delay)
+
+func _on_scroll_restart_timer_timeout() -> void:
+ _current_scroll_position = scroll_vertical
+ scroll_paused = false
+
+func _on_resized() -> void:
+ _current_scroll_position = scroll_vertical
+
+func _on_visibility_changed() -> void:
+ if visible:
+ scroll_vertical = 0
+ _current_scroll_position = scroll_vertical
+ scroll_paused = false
+
+func _ready() -> void:
+ scroll_started.connect(_on_scroll_started)
+ gui_input.connect(_on_gui_input)
+ resized.connect(_on_resized)
+ visibility_changed.connect(_on_visibility_changed)
+ timer.timeout.connect(_on_scroll_restart_timer_timeout)
+ add_child(timer)
+
+func _process(_delta : float) -> void:
+ 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)
diff --git a/addons/maaacks_game_template/base/scenes/credits/auto_scroll_container.gd.uid b/addons/maaacks_game_template/base/scenes/credits/auto_scroll_container.gd.uid
new file mode 100644
index 0000000..482b696
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/auto_scroll_container.gd.uid
@@ -0,0 +1 @@
+uid://gmrv6pgchkwc
diff --git a/addons/maaacks_game_template/base/scenes/credits/credits.gd b/addons/maaacks_game_template/base/scenes/credits/credits.gd
new file mode 100644
index 0000000..ec04308
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/credits.gd
@@ -0,0 +1,4 @@
+class_name Credits
+extends Control
+
+signal end_reached
diff --git a/addons/maaacks_game_template/base/scenes/credits/credits.gd.uid b/addons/maaacks_game_template/base/scenes/credits/credits.gd.uid
new file mode 100644
index 0000000..d676c76
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/credits.gd.uid
@@ -0,0 +1 @@
+uid://xc4ebbm5dxnh
diff --git a/addons/maaacks_game_template/base/scenes/credits/credits_label.gd b/addons/maaacks_game_template/base/scenes/credits/credits_label.gd
new file mode 100644
index 0000000..de8fd61
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/credits_label.gd
@@ -0,0 +1,87 @@
+@tool
+class_name CreditsLabel
+extends RichTextLabel
+
+@export_file("*.md") var attribution_file_path: String
+@export var auto_update : bool = true
+@export_group("Font Sizes")
+@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_group("Image Sizes")
+@export var max_image_width: int
+@export var max_image_height : int
+@export_group("Extra Options")
+@export var disable_images : bool = false
+@export var disable_urls : bool = false
+## For platforms that don't permit linking to other domains or products.
+@export var disable_opening_links: 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_imgs(credits:String) -> String:
+ var regex = RegEx.new()
+ var match_string := "!\\[([^\\]]*)\\]\\(([^\\)]*)\\)"
+ var replace_string := ""
+ if not disable_images:
+ replace_string = "res://$2[/img]"
+ if max_image_width:
+ if max_image_height:
+ replace_string = ("[img=%dx%d]" % [max_image_width, max_image_height]) + replace_string
+ else:
+ replace_string = ("[img=%d]" % [max_image_width]) + replace_string
+ else:
+ replace_string = "[img]" + replace_string
+ regex.compile(match_string)
+ regex.get_group_count()
+ return regex.sub(credits, replace_string, true)
+
+func regex_replace_urls(credits:String) -> String:
+ var regex = RegEx.new()
+ var match_string := "\\[([^\\]]*)\\]\\(([^\\)]*)\\)"
+ var replace_string := "$1"
+ if not disable_urls:
+ replace_string = "[url=$2]$1[/url]"
+ regex.compile(match_string)
+ return regex.sub(credits, replace_string, true)
+
+func regex_replace_titles(credits:String) -> 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 := "([^#]|^)#{%d}\\s([^\n]*)" % iter
+ var replace_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() -> void:
+ var file_text : String = load_file(attribution_file_path)
+ if file_text == "":
+ return
+ var _end_of_first_line = file_text.find("\n") + 1
+ file_text = file_text.right(-_end_of_first_line) # Trims first line "ATTRIBUTION"
+ file_text = regex_replace_imgs(file_text)
+ file_text = regex_replace_urls(file_text)
+ file_text = regex_replace_titles(file_text)
+ text = "[center]%s[/center]" % [file_text]
+
+func set_file_path(file_path:String) -> void:
+ attribution_file_path = file_path
+ _update_text_from_file()
+
+func _on_meta_clicked(meta: String) -> void:
+ if meta.begins_with("https://") and not disable_opening_links:
+ var _err = OS.shell_open(meta)
+
+func _ready() -> void:
+ if not auto_update: return
+ set_file_path(attribution_file_path)
diff --git a/addons/maaacks_game_template/base/scenes/credits/credits_label.gd.uid b/addons/maaacks_game_template/base/scenes/credits/credits_label.gd.uid
new file mode 100644
index 0000000..83d735f
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/credits_label.gd.uid
@@ -0,0 +1 @@
+uid://cc2wtqasev7le
diff --git a/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.gd b/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.gd
new file mode 100644
index 0000000..749542e
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.gd
@@ -0,0 +1,12 @@
+@tool
+class_name ScrollableCredits
+extends Credits
+
+@onready var credits_label : RichTextLabel = %CreditsLabel
+
+func _on_visibility_changed() -> void:
+ if visible:
+ credits_label.scroll_to_line(0)
+
+func _ready() -> void:
+ visibility_changed.connect(_on_visibility_changed)
diff --git a/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.gd.uid b/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.gd.uid
new file mode 100644
index 0000000..7310e4f
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.gd.uid
@@ -0,0 +1 @@
+uid://c5wuso5r3dwpw
diff --git a/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.tscn b/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.tscn
new file mode 100644
index 0000000..fb6e738
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/scrollable_credits.tscn
@@ -0,0 +1,29 @@
+[gd_scene load_steps=3 format=3 uid="uid://osxulxw2oas3"]
+
+[ext_resource type="Script" uid="uid://c5wuso5r3dwpw" path="res://addons/maaacks_game_template/base/scenes/credits/scrollable_credits.gd" id="1_hny8b"]
+[ext_resource type="Script" uid="uid://cc2wtqasev7le" path="res://addons/maaacks_game_template/base/scenes/credits/credits_label.gd" id="2_g23vg"]
+
+[node name="ScrollableCredits" 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_hny8b")
+
+[node name="CreditsLabel" type="RichTextLabel" 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
+bbcode_enabled = true
+script = ExtResource("2_g23vg")
+h1_font_size = 64
+h2_font_size = 48
+h3_font_size = 32
+h4_font_size = 24
+max_image_width = 80
diff --git a/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.gd b/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.gd
new file mode 100644
index 0000000..d5e96d1
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.gd
@@ -0,0 +1,20 @@
+class_name ScrollingCredits
+extends Credits
+
+@onready var header_space : Control = %HeaderSpace
+@onready var footer_space : Control = %FooterSpace
+@onready var credits_label : Control = %CreditsLabel
+
+func set_header_and_footer() -> void:
+ header_space.custom_minimum_size.y = size.y
+ footer_space.custom_minimum_size.y = size.y
+ credits_label.custom_minimum_size.x = size.x
+
+func _on_scroll_container_end_reached() -> void:
+ end_reached.emit()
+
+func _on_resized() -> void:
+ set_header_and_footer()
+
+func _ready() -> void:
+ resized.connect(_on_resized)
diff --git a/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.gd.uid b/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.gd.uid
new file mode 100644
index 0000000..522ce32
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.gd.uid
@@ -0,0 +1 @@
+uid://bnub0cq2y0deh
diff --git a/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.tscn b/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.tscn
new file mode 100644
index 0000000..5d59ec7
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/credits/scrolling_credits.tscn
@@ -0,0 +1,55 @@
+[gd_scene load_steps=4 format=3 uid="uid://t2dui8ppm3a4"]
+
+[ext_resource type="Script" uid="uid://gmrv6pgchkwc" path="res://addons/maaacks_game_template/base/scenes/credits/auto_scroll_container.gd" id="2_ak7hi"]
+[ext_resource type="Script" uid="uid://cc2wtqasev7le" path="res://addons/maaacks_game_template/base/scenes/credits/credits_label.gd" id="3_kngql"]
+[ext_resource type="Script" uid="uid://bnub0cq2y0deh" path="res://addons/maaacks_game_template/base/scenes/credits/scrolling_credits.gd" id="4"]
+
+[node name="ScrollingCredits" 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")
+
+[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
+script = ExtResource("2_ak7hi")
+
+[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
+script = ExtResource("3_kngql")
+h1_font_size = 64
+h2_font_size = 48
+h3_font_size = 32
+h4_font_size = 24
+max_image_width = 80
+
+[node name="FooterSpace" type="Control" parent="ScrollContainer/VBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(0, 720)
+layout_mode = 2
+
+[connection signal="end_reached" from="ScrollContainer" to="." method="_on_scroll_container_end_reached"]
diff --git a/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.gd b/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.gd
new file mode 100644
index 0000000..e1f41de
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.gd
@@ -0,0 +1,168 @@
+class_name LoadingScreen
+extends CanvasLayer
+
+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}
+
+@export_range(5, 60, 0.5, "or_greater") var state_change_delay : float = 15.0
+@export_group("State Messages")
+@export_subgroup("In Progress")
+@export var _in_progress : String = "Loading..."
+@export var _in_progress_waiting : String = "Still Loading..."
+@export var _in_progress_still_waiting : String = "Still Loading... (%d seconds)"
+@export_subgroup("Completed")
+@export var _complete : String = "Loading Complete!"
+@export var _complete_waiting : String = "Any Moment Now..."
+@export var _complete_still_waiting : String = "Any Moment Now... (%d seconds)"
+
+var _stall_stage : StallStage = StallStage.STARTED
+var _scene_loading_complete : bool = false
+var _scene_loading_progress : float = 0.0 :
+ set(value):
+ var _value_changed = _scene_loading_progress != value
+ _scene_loading_progress = value
+ if _value_changed:
+ update_total_loading_progress()
+ _reset_loading_stage()
+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() -> void:
+ _total_loading_progress = _scene_loading_progress
+
+func _reset_loading_stage() -> void:
+ _stall_stage = StallStage.STARTED
+ %LoadingTimer.start(state_change_delay)
+
+func _reset_loading_start_time() -> void:
+ _loading_start_time = Time.get_ticks_msec()
+
+func _get_seconds_waiting() -> int:
+ return int((Time.get_ticks_msec() - _loading_start_time) / 1000.0)
+
+func _update_scene_loading_progress() -> void:
+ var new_progress = SceneLoader.get_progress()
+ if new_progress > _scene_loading_progress:
+ _scene_loading_progress = new_progress
+
+func _set_scene_loading_complete() -> void:
+ _scene_loading_progress = 1.0
+ _scene_loading_complete = true
+
+func _reset_scene_loading_progress() -> void:
+ _scene_loading_progress = 0.0
+ _scene_loading_complete = false
+
+func _show_loading_stalled_error_message() -> void:
+ 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() -> void:
+ if %ErrorMessage.visible:
+ return
+ %ErrorMessage.dialog_text = "Loading Error: Failed to switch scenes."
+ %ErrorMessage.popup()
+
+func _hide_popups() -> void:
+ %ErrorMessage.hide()
+ %StalledMessage.hide()
+
+func get_progress_message() -> String:
+ var _progress_message : String
+ match _stall_stage:
+ StallStage.STARTED:
+ if _scene_loading_complete:
+ _progress_message = _complete
+ else:
+ _progress_message = _in_progress
+ StallStage.WAITING:
+ if _scene_loading_complete:
+ _progress_message = _complete_waiting
+ else:
+ _progress_message = _in_progress_waiting
+ StallStage.STILL_WAITING, StallStage.GIVE_UP:
+ if _scene_loading_complete:
+ _progress_message = _complete_still_waiting
+ else:
+ _progress_message = _in_progress_still_waiting
+ if _progress_message.contains("%d"):
+ _progress_message = _progress_message % _get_seconds_waiting()
+ return _progress_message
+
+func _update_progress_messaging() -> void:
+ %ProgressLabel.text = get_progress_message()
+ if _stall_stage == StallStage.GIVE_UP:
+ if _scene_loading_complete:
+ _show_scene_switching_error_message()
+ else:
+ _show_loading_stalled_error_message()
+ else:
+ _hide_popups()
+
+func _process(_delta : float) -> void:
+ var status = SceneLoader.get_status()
+ match(status):
+ ResourceLoader.THREAD_LOAD_IN_PROGRESS:
+ _update_scene_loading_progress()
+ _update_progress_messaging()
+ ResourceLoader.THREAD_LOAD_LOADED:
+ _set_scene_loading_complete()
+ _update_progress_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() -> void:
+ var prev_stage : StallStage = _stall_stage
+ match prev_stage:
+ StallStage.STARTED:
+ _stall_stage = StallStage.WAITING
+ %LoadingTimer.start(state_change_delay)
+ StallStage.WAITING:
+ _stall_stage = StallStage.STILL_WAITING
+ %LoadingTimer.start(state_change_delay)
+ StallStage.STILL_WAITING:
+ _stall_stage = StallStage.GIVE_UP
+
+func _reload_main_scene_or_quit() -> void:
+ 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() -> void:
+ _reload_main_scene_or_quit()
+
+func _on_confirmation_dialog_canceled() -> void:
+ _reload_main_scene_or_quit()
+
+func _on_confirmation_dialog_confirmed() -> void:
+ _reset_loading_stage()
+
+func reset() -> void:
+ show()
+ _reset_loading_stage()
+ _reset_scene_loading_progress()
+ _reset_loading_start_time()
+ _hide_popups()
+ set_process(true)
+
+func close() -> void:
+ set_process(false)
+ _hide_popups()
+ hide()
diff --git a/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.gd.uid b/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.gd.uid
new file mode 100644
index 0000000..eada645
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.gd.uid
@@ -0,0 +1 @@
+uid://dgeewyjjpk4qn
diff --git a/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.tscn b/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.tscn
new file mode 100644
index 0000000..f13ed51
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.tscn
@@ -0,0 +1,87 @@
+[gd_scene load_steps=2 format=3 uid="uid://cd0jbh4metflb"]
+
+[ext_resource type="Script" uid="uid://dgeewyjjpk4qn" path="res://addons/maaacks_game_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"]
+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)
+
+[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="ProgressLabel" 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
+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"]
diff --git a/addons/maaacks_game_template/base/scenes/menus/main_menu/config_name_label.gd b/addons/maaacks_game_template/base/scenes/menus/main_menu/config_name_label.gd
new file mode 100644
index 0000000..11d7420
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/main_menu/config_name_label.gd
@@ -0,0 +1,18 @@
+@tool
+extends Label
+class_name ConfigNameLabel
+## Displays the value of `application/config/name`, set in project settings.
+
+const NO_NAME_STRING : String = "Title"
+
+@export var lock : bool = false
+
+func update_name_label():
+ if lock: return
+ var config_name : String = ProjectSettings.get_setting("application/config/name", NO_NAME_STRING)
+ if config_name.is_empty():
+ config_name = NO_NAME_STRING
+ text = config_name
+
+func _ready():
+ update_name_label()
diff --git a/addons/maaacks_game_template/base/scenes/menus/main_menu/config_name_label.gd.uid b/addons/maaacks_game_template/base/scenes/menus/main_menu/config_name_label.gd.uid
new file mode 100644
index 0000000..27cd139
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/main_menu/config_name_label.gd.uid
@@ -0,0 +1 @@
+uid://bkwlopi4qn32o
diff --git a/addons/maaacks_game_template/base/scenes/menus/main_menu/config_version_label.gd b/addons/maaacks_game_template/base/scenes/menus/main_menu/config_version_label.gd
new file mode 100644
index 0000000..875d7fd
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/main_menu/config_version_label.gd
@@ -0,0 +1,18 @@
+@tool
+extends Label
+class_name ConfigVersionLabel
+## Displays the value of `application/config/version`, set in project settings.
+
+const NO_VERSION_STRING : String = "0.0.0"
+
+## Prefixes the value of `application/config/version` when displaying to the user.
+@export var version_prefix : String = "v"
+
+func update_version_label() -> void:
+ var config_version : String = ProjectSettings.get_setting("application/config/version", NO_VERSION_STRING)
+ if config_version.is_empty():
+ config_version = NO_VERSION_STRING
+ text = version_prefix + config_version
+
+func _ready() -> void:
+ update_version_label()
diff --git a/addons/maaacks_game_template/base/scenes/menus/main_menu/config_version_label.gd.uid b/addons/maaacks_game_template/base/scenes/menus/main_menu/config_version_label.gd.uid
new file mode 100644
index 0000000..ef1cc35
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/main_menu/config_version_label.gd.uid
@@ -0,0 +1 @@
+uid://dmkubt2nsnsbn
diff --git a/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.gd b/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.gd
new file mode 100644
index 0000000..8e7de55
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.gd
@@ -0,0 +1,94 @@
+class_name MainMenu
+extends Control
+
+## 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
+
+var options_scene
+var credits_scene
+var sub_menu
+
+func load_game_scene() -> void:
+ SceneLoader.load_scene(game_scene_path)
+
+func new_game() -> void:
+ load_game_scene()
+
+func _open_sub_menu(menu : Control) -> void:
+ sub_menu = menu
+ sub_menu.show()
+ %BackButton.show()
+ %MenuContainer.hide()
+
+func _close_sub_menu() -> void:
+ if sub_menu == null:
+ return
+ sub_menu.hide()
+ sub_menu = null
+ %BackButton.hide()
+ %MenuContainer.show()
+
+func _event_is_mouse_button_released(event : InputEvent) -> bool:
+ return event is InputEventMouseButton and not event.is_pressed()
+
+func _input(event : InputEvent) -> void:
+ if event.is_action_released("ui_cancel"):
+ if sub_menu:
+ _close_sub_menu()
+ else:
+ get_tree().quit()
+ if event.is_action_released("ui_accept") and get_viewport().gui_get_focus_owner() == null:
+ %MenuButtonsBoxContainer.focus_first()
+
+func _hide_exit_for_web() -> void:
+ if OS.has_feature("web"):
+ %ExitButton.hide()
+
+func _hide_new_game_if_unset() -> void:
+ if game_scene_path.is_empty():
+ %NewGameButton.hide()
+
+func _add_or_hide_options() -> void:
+ 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 _add_or_hide_credits() -> void:
+ 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() -> void:
+ _hide_exit_for_web()
+ _add_or_hide_options()
+ _add_or_hide_credits()
+ _hide_new_game_if_unset()
+
+func _on_new_game_button_pressed() -> void:
+ new_game()
+
+func _on_options_button_pressed() -> void:
+ _open_sub_menu(options_scene)
+
+func _on_credits_button_pressed() -> void:
+ _open_sub_menu(credits_scene)
+
+func _on_exit_button_pressed() -> void:
+ get_tree().quit()
+
+func _on_credits_end_reached() -> void:
+ if sub_menu == credits_scene:
+ _close_sub_menu()
+
+func _on_back_button_pressed() -> void:
+ _close_sub_menu()
diff --git a/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.gd.uid b/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.gd.uid
new file mode 100644
index 0000000..905b7fd
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.gd.uid
@@ -0,0 +1 @@
+uid://bhgs1upaahk3y
diff --git a/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn b/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn
new file mode 100644
index 0000000..18a61f8
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn
@@ -0,0 +1,220 @@
+[gd_scene load_steps=9 format=3 uid="uid://c6k5nnpbypshi"]
+
+[ext_resource type="Script" uid="uid://bhgs1upaahk3y" path="res://addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.gd" id="1"]
+[ext_resource type="PackedScene" uid="uid://bq2ti3hrjlgdl" path="res://menus/scenes/menus/options_menu/master_options_menu_with_tabs.tscn" id="2_73am8"]
+[ext_resource type="PackedScene" uid="uid://ct0yseu6qy88d" path="res://menus/scenes/credits/scrollable_credits.tscn" id="3_g46cd"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="4_l1ebe"]
+[ext_resource type="PackedScene" uid="uid://bkcsjsk2ciff" path="res://addons/maaacks_game_template/base/scenes/music_players/background_music_player.tscn" id="4_w8sbm"]
+[ext_resource type="Script" uid="uid://b5oej1q4h7jvh" path="res://addons/maaacks_game_template/base/scripts/ui_sound_controller.gd" id="6_bs342"]
+[ext_resource type="Script" uid="uid://dmkubt2nsnsbn" path="res://addons/maaacks_game_template/base/scenes/menus/main_menu/config_version_label.gd" id="6_pdiij"]
+[ext_resource type="Script" uid="uid://bkwlopi4qn32o" path="res://addons/maaacks_game_template/base/scenes/menus/main_menu/config_name_label.gd" id="7_j7612"]
+
+[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")
+game_scene_path = "uid://cxbskue0lj2gv"
+options_packed_scene = ExtResource("2_73am8")
+credits_packed_scene = ExtResource("3_g46cd")
+
+[node name="UISoundController" type="Node" parent="."]
+script = ExtResource("6_bs342")
+
+[node name="BackgroundMusicPlayer" parent="." instance=ExtResource("4_w8sbm")]
+bus = &"Master"
+
+[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="VersionMargin" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 8
+theme_override_constants/margin_bottom = 8
+
+[node name="VersionContainer" type="Control" parent="VersionMargin"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="VersionLabel" type="Label" parent="VersionMargin/VersionContainer"]
+layout_mode = 1
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -88.0
+offset_top = -26.0
+grow_horizontal = 0
+grow_vertical = 0
+text = "v0.0.0"
+horizontal_alignment = 2
+script = ExtResource("6_pdiij")
+
+[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="TitleMargin" type="MarginContainer" parent="MenuContainer"]
+layout_mode = 2
+theme_override_constants/margin_top = 24
+
+[node name="TitleContainer" type="Control" parent="MenuContainer/TitleMargin"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="TitleLabel" type="Label" parent="MenuContainer/TitleMargin/TitleContainer"]
+layout_mode = 1
+anchors_preset = 10
+anchor_right = 1.0
+offset_bottom = 67.0
+grow_horizontal = 2
+theme_override_font_sizes/font_size = 48
+text = "Movement tests"
+horizontal_alignment = 1
+vertical_alignment = 1
+script = ExtResource("7_j7612")
+
+[node name="SubTitleMargin" type="MarginContainer" parent="MenuContainer"]
+layout_mode = 2
+theme_override_constants/margin_top = 92
+
+[node name="SubTitleContainer" type="Control" parent="MenuContainer/SubTitleMargin"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="SubTitleLabel" type="Label" parent="MenuContainer/SubTitleMargin/SubTitleContainer"]
+layout_mode = 1
+anchors_preset = 10
+anchor_right = 1.0
+offset_bottom = 34.0
+grow_horizontal = 2
+theme_override_font_sizes/font_size = 24
+text = "Subtitle"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="MenuButtonsMargin" type="MarginContainer" parent="MenuContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/margin_top = 136
+theme_override_constants/margin_bottom = 8
+
+[node name="MenuButtonsContainer" type="Control" parent="MenuContainer/MenuButtonsMargin"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="MenuButtonsBoxContainer" type="BoxContainer" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -64.0
+offset_top = -104.0
+offset_right = 64.0
+offset_bottom = 104.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 4
+theme_override_constants/separation = 16
+alignment = 1
+vertical = true
+script = ExtResource("4_l1ebe")
+
+[node name="NewGameButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "New Game"
+
+[node name="OptionsButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Options"
+
+[node name="CreditsButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Credits"
+
+[node name="ExitButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer"]
+unique_name_in_owner = true
+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
+layout_mode = 1
+anchors_preset = 2
+anchor_top = 1.0
+anchor_bottom = 1.0
+offset_top = -31.0
+offset_right = 45.0
+grow_vertical = 0
+text = "Back"
+
+[connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/NewGameButton" to="." method="_on_new_game_button_pressed"]
+[connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/OptionsButton" to="." method="_on_options_button_pressed"]
+[connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/CreditsButton" to="." method="_on_credits_button_pressed"]
+[connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/ExitButton" to="." method="_on_exit_button_pressed"]
+[connection signal="pressed" from="FlowControlContainer/FlowControl/BackButton" to="." method="_on_back_button_pressed"]
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.gd
new file mode 100644
index 0000000..75dc814
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.gd
@@ -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) -> void:
+ AppSettings.set_bus_volume(bus_iter, bus_value)
+
+func _add_audio_control(bus_name : String, bus_value : float, bus_iter : int) -> void:
+ 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() -> void:
+ 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() -> void:
+ _add_audio_bus_controls()
+ mute_control.value = AppSettings.is_muted()
+
+func _ready() -> void:
+ _update_ui()
+
+func _on_mute_control_setting_changed(value : bool) -> void:
+ AppSettings.set_mute(value)
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.gd.uid
new file mode 100644
index 0000000..4e3c290
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://bwugqn2cjr41e
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.tscn
new file mode 100644
index 0000000..9f26066
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.tscn
@@ -0,0 +1,42 @@
+[gd_scene load_steps=5 format=3 uid="uid://c8vnncjwqcpab"]
+
+[ext_resource type="Script" uid="uid://bwugqn2cjr41e" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.gd" id="1"]
+[ext_resource type="PackedScene" uid="uid://cl416gdb1fgwr" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn" id="2_raehj"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="3_dtraq"]
+[ext_resource type="PackedScene" uid="uid://bsxh6v7j0257h" path="res://addons/maaacks_game_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"]
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.gd
new file mode 100644
index 0000000..ee155ef
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.gd
@@ -0,0 +1,296 @@
+@tool
+class_name InputActionsList
+extends Container
+
+const EMPTY_INPUT_ACTION_STRING = " "
+
+signal already_assigned(action_name : String, input_name : String)
+signal minimum_reached(action_name : String)
+signal button_clicked(action_name : String, readable_input_name : String)
+
+const BUTTON_NAME_GROUP_STRING : String = "%s:%d"
+
+@export var vertical : bool = true :
+ set(value):
+ vertical = value
+ if is_inside_tree():
+ %ParentBoxContainer.vertical = vertical
+
+@export_range(1, 5) var action_groups : int = 2
+@export var action_group_names : Array[String]
+@export var input_action_names : Array[StringName] :
+ set(value):
+ var _value_changed = input_action_names != value
+ input_action_names = value
+ if _value_changed:
+ var _new_readable_action_names : Array[String]
+ for action in input_action_names:
+ _new_readable_action_names.append(action.capitalize())
+ readable_action_names = _new_readable_action_names
+
+@export var readable_action_names : Array[String] :
+ set(value):
+ var _value_changed = readable_action_names != value
+ readable_action_names = value
+ if _value_changed:
+ var _new_action_name_map : Dictionary
+ for iter in range(input_action_names.size()):
+ var _input_name : StringName = input_action_names[iter]
+ var _readable_name : String = readable_action_names[iter]
+ _new_action_name_map[_input_name] = _readable_name
+ action_name_map = _new_action_name_map
+
+## Show action names that are not explicitely listed in an action name map.
+@export var show_all_actions : bool = true
+@export_group("Icons")
+@export var input_icon_mapper : InputIconMapper
+@export var expand_icon : bool = false
+@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 := InputEventHelper.BUILT_IN_ACTION_NAME_MAP
+@export_group("Debug")
+## Maps the names of input actions to readable names for users.
+@export var action_name_map : Dictionary
+
+var action_button_map : Dictionary = {}
+var button_readable_input_map : Dictionary = {}
+var assigned_input_events : Dictionary = {}
+var editing_action_name : String = ""
+var editing_action_group : int = 0
+var last_input_readable_name
+
+func _clear_list() -> void:
+ for child in %ParentBoxContainer.get_children():
+ if child == %ActionBoxContainer:
+ continue
+ child.queue_free()
+
+func _replace_action(action_name : String, readable_input_name : String = "") -> void:
+ var readable_action_name = tr(_get_action_readable_name(action_name))
+ button_clicked.emit(readable_action_name, readable_input_name)
+
+func _on_button_pressed(action_name : String, action_group : int) -> void:
+ editing_action_name = action_name
+ editing_action_group = action_group
+ _replace_action(action_name)
+
+func _new_action_box() -> Node:
+ var new_action_box : Node = %ActionBoxContainer.duplicate()
+ new_action_box.visible = true
+ new_action_box.vertical = !(vertical)
+ return new_action_box
+
+func _add_header() -> void:
+ if action_group_names.is_empty(): return
+ var new_action_box := _new_action_box()
+ for group_iter in range(action_groups):
+ var group_name := ""
+ if group_iter < action_group_names.size():
+ group_name = action_group_names[group_iter]
+ var new_label := Label.new()
+ new_label.size_flags_horizontal = SIZE_EXPAND_FILL
+ new_label.size_flags_vertical = SIZE_EXPAND_FILL
+ new_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ new_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ new_label.text = group_name
+ new_action_box.add_child(new_label)
+ %ParentBoxContainer.add_child(new_action_box)
+
+func _add_to_action_button_map(action_name : String, action_group : int, button_node : BaseButton) -> void:
+ var key_string : String = BUTTON_NAME_GROUP_STRING % [action_name, action_group]
+ action_button_map[key_string] = button_node
+
+func _get_button_by_action(action_name : String, action_group : int) -> Button:
+ var key_string : String = BUTTON_NAME_GROUP_STRING % [action_name, action_group]
+ if key_string in action_button_map:
+ return action_button_map[key_string]
+ return null
+
+func _update_next_button_disabled_state(action_name : String, action_group : int) -> void:
+ var button = _get_button_by_action(action_name, action_group)
+ if button:
+ button.disabled = false
+
+func _update_assigned_inputs_and_button(action_name : String, action_group : int, input_event : InputEvent) -> void:
+ var new_readable_input_name = InputEventHelper.get_text(input_event)
+ var button = _get_button_by_action(action_name, action_group)
+ if not button: return
+ var icon : Texture
+ if input_icon_mapper:
+ icon = input_icon_mapper.get_icon(input_event)
+ if icon:
+ button.icon = icon
+ else:
+ button.icon = null
+ if button.icon == null:
+ button.text = new_readable_input_name
+ else:
+ button.text = ""
+ var old_readable_input_name : String
+ if button in button_readable_input_map:
+ old_readable_input_name = button_readable_input_map[button]
+ assigned_input_events.erase(old_readable_input_name)
+ button_readable_input_map[button] = new_readable_input_name
+ assigned_input_events[new_readable_input_name] = action_name
+
+func _clear_button(action_name : String, action_group : int) -> void:
+ var button = _get_button_by_action(action_name, action_group)
+ if not button: return
+ button.icon = null
+ button.text = EMPTY_INPUT_ACTION_STRING
+ var old_readable_input_name : String
+ if button in button_readable_input_map:
+ old_readable_input_name = button_readable_input_map[button]
+ assigned_input_events.erase(old_readable_input_name)
+ button_readable_input_map[button] = EMPTY_INPUT_ACTION_STRING
+
+func _add_new_button(content : Variant, container: Control, disabled : bool = false) -> Button:
+ var new_button := Button.new()
+ new_button.size_flags_horizontal = SIZE_EXPAND_FILL
+ new_button.size_flags_vertical = SIZE_EXPAND_FILL
+ new_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ new_button.expand_icon = expand_icon
+ if content is Texture:
+ new_button.icon = content
+ elif content is String:
+ new_button.text = content
+ new_button.disabled = disabled
+ container.add_child(new_button)
+ return new_button
+
+func _connect_button_and_add_to_maps(button : Button, input_name : String, action_name : String, group_iter : int) -> void:
+ button.pressed.connect(_on_button_pressed.bind(action_name, group_iter))
+ button_readable_input_map[button] = input_name
+ _add_to_action_button_map(action_name, group_iter, button)
+
+func _add_action_options(action_name : String, readable_action_name : String, input_events : Array[InputEvent]) -> void:
+ var new_action_box = %ActionBoxContainer.duplicate()
+ new_action_box.visible = true
+ new_action_box.vertical = !(vertical)
+ new_action_box.get_child(0).text = readable_action_name
+ for group_iter in range(action_groups):
+ var input_event : InputEvent
+ if group_iter < input_events.size():
+ input_event = input_events[group_iter]
+ var text = InputEventHelper.get_text(input_event)
+ var is_disabled = group_iter > input_events.size()
+ if text.is_empty(): text = EMPTY_INPUT_ACTION_STRING
+ var icon : Texture
+ if input_icon_mapper:
+ icon = input_icon_mapper.get_icon(input_event)
+ var content = icon if icon else text
+ var button : Button = _add_new_button(content, new_action_box, is_disabled)
+ _connect_button_and_add_to_maps(button, text, action_name, group_iter)
+ %ParentBoxContainer.add_child(new_action_box)
+
+func _get_all_action_names(include_built_in : bool = false) -> Array[StringName]:
+ var action_names : Array[StringName] = input_action_names.duplicate()
+ var full_action_name_map = action_name_map.duplicate()
+ if include_built_in:
+ for action_name in built_in_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(input_name : StringName) -> String:
+ var readable_name : String
+ if input_name in action_name_map:
+ readable_name = action_name_map[input_name]
+ elif input_name in built_in_action_name_map:
+ readable_name = built_in_action_name_map[input_name]
+ else:
+ readable_name = input_name.capitalize()
+ action_name_map[input_name] = readable_name
+ return readable_name
+
+func _build_ui_list() -> void:
+ _clear_list()
+ _add_header()
+ 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:
+ continue
+ var readable_name : String = _get_action_readable_name(action_name)
+ _add_action_options(action_name, readable_name, input_events)
+
+func _assign_input_event(input_event : InputEvent, action_name : String) -> void:
+ assigned_input_events[InputEventHelper.get_text(input_event)] = action_name
+
+func _assign_input_event_to_action_group(input_event : InputEvent, action_name : String, action_group : int) -> void:
+ _assign_input_event(input_event, action_name)
+ var action_events := InputMap.action_get_events(action_name)
+ action_events.resize(action_events.size() + 1)
+ action_events[action_group] = input_event
+ InputMap.action_erase_events(action_name)
+ var final_action_events : Array[InputEvent]
+ for input_action_event in action_events:
+ if input_action_event == null: continue
+ final_action_events.append(input_action_event)
+ InputMap.action_add_event(action_name, input_action_event)
+ AppSettings.set_config_input_events(action_name, final_action_events)
+ action_group = min(action_group, final_action_events.size() - 1)
+ _update_assigned_inputs_and_button(action_name, action_group, input_event)
+ _update_next_button_disabled_state(action_name, action_group)
+
+func _build_assigned_input_events() -> void:
+ 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 add_action_event(last_input_text : String, last_input_event : InputEvent) -> void:
+ last_input_readable_name = 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))
+ already_assigned.emit(readable_action_name, last_input_readable_name)
+ else:
+ _assign_input_event_to_action_group(last_input_event, editing_action_name, editing_action_group)
+ editing_action_name = ""
+
+func _refresh_ui_list_button_content() -> void:
+ 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:
+ continue
+ var group_iter : int = 0
+ for input_event in input_events:
+ _update_assigned_inputs_and_button(action_name, group_iter, input_event)
+ group_iter += 1
+ while group_iter < action_groups:
+ _clear_button(action_name, group_iter)
+ group_iter += 1
+
+func reset() -> void:
+ AppSettings.reset_to_default_inputs()
+ _build_assigned_input_events()
+ _refresh_ui_list_button_content()
+
+func _ready() -> void:
+ if Engine.is_editor_hint(): return
+ vertical = vertical
+ _build_assigned_input_events()
+ _build_ui_list()
+ if input_icon_mapper:
+ input_icon_mapper.joypad_device_changed.connect(_refresh_ui_list_button_content)
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.gd.uid
new file mode 100644
index 0000000..920d4f8
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.gd.uid
@@ -0,0 +1 @@
+uid://b3q5fgjev8gyo
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.tscn
new file mode 100644
index 0000000..ce156c6
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.tscn
@@ -0,0 +1,45 @@
+[gd_scene load_steps=2 format=3 uid="uid://bxp45814v6ydv"]
+
+[ext_resource type="Script" uid="uid://b3q5fgjev8gyo" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.gd" id="1_cxorh"]
+
+[node name="InputActionsList" type="ScrollContainer"]
+custom_minimum_size = Vector2(560, 240)
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+follow_focus = true
+script = ExtResource("1_cxorh")
+action_groups = 3
+action_group_names = Array[String](["Primary", "Secondary", "Tertiary", "Quaternary", "Quinary"])
+input_action_names = Array[StringName]([&"move_forward", &"move_backward", &"move_up", &"move_down", &"move_left", &"move_right", &"interact"])
+readable_action_names = Array[String](["Move Forward", "Move Backward", "Move Up", "Move Down", "Move Left", "Move Right", "Interact"])
+action_name_map = {
+"interact": "Interact",
+"move_backward": "Move Backward",
+"move_down": "Move Down",
+"move_forward": "Move Forward",
+"move_left": "Move Left",
+"move_right": "Move Right",
+"move_up": "Move Up"
+}
+
+[node name="ParentBoxContainer" type="BoxContainer" parent="."]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+vertical = true
+
+[node name="ActionBoxContainer" type="BoxContainer" parent="ParentBoxContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="ActionNameLabel" type="Label" parent="ParentBoxContainer/ActionBoxContainer"]
+custom_minimum_size = Vector2(150, 0)
+layout_mode = 2
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.gd
new file mode 100644
index 0000000..926d805
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.gd
@@ -0,0 +1,215 @@
+class_name InputActionsTree
+extends Tree
+
+signal already_assigned(action_name : String, input_name : String)
+signal minimum_reached(action_name : String)
+signal add_button_clicked(action_name : String)
+signal remove_button_clicked(action_name : String, input_name : String)
+
+@export var input_action_names : Array[StringName] :
+ set(value):
+ var _value_changed = input_action_names != value
+ input_action_names = value
+ if _value_changed:
+ var _new_readable_action_names : Array[String]
+ for action in input_action_names:
+ _new_readable_action_names.append(action.capitalize())
+ readable_action_names = _new_readable_action_names
+
+@export var readable_action_names : Array[String] :
+ set(value):
+ var _value_changed = readable_action_names != value
+ readable_action_names = value
+ if _value_changed:
+ var _new_action_name_map : Dictionary
+ for iter in range(input_action_names.size()):
+ var _input_name : StringName = input_action_names[iter]
+ var _readable_name : String = readable_action_names[iter]
+ _new_action_name_map[_input_name] = _readable_name
+ action_name_map = _new_action_name_map
+
+## Show action names that are not explicitely listed in an action name map.
+@export var show_all_actions : bool = true
+@export_group("Icons")
+@export var add_button_texture : Texture2D
+@export var remove_button_texture : Texture2D
+@export var input_icon_mapper : InputIconMapper
+@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 := InputEventHelper.BUILT_IN_ACTION_NAME_MAP
+@export_group("Debug")
+## Maps the names of input actions to readable names for users.
+@export var action_name_map : Dictionary
+
+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 _start_tree() -> void:
+ clear()
+ create_item()
+
+func _add_input_event_as_tree_item(action_name : String, input_event : InputEvent, parent_item : TreeItem) -> void:
+ var input_tree_item : TreeItem = create_item(parent_item)
+ var icon : Texture
+ if input_icon_mapper:
+ icon = input_icon_mapper.get_icon(input_event)
+ if icon:
+ input_tree_item.set_icon(0, icon)
+ 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]) -> void:
+ var root_tree_item : TreeItem = get_root()
+ var action_tree_item : TreeItem = 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] = input_action_names.duplicate()
+ var full_action_name_map = action_name_map.duplicate()
+ if include_built_in:
+ for action_name in built_in_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(input_name : StringName) -> String:
+ var readable_name : String
+ if input_name in action_name_map:
+ readable_name = action_name_map[input_name]
+ elif input_name in built_in_action_name_map:
+ readable_name = built_in_action_name_map[input_name]
+ else:
+ readable_name = input_name.capitalize()
+ action_name_map[input_name] = readable_name
+ return readable_name
+
+func _build_ui_tree() -> void:
+ _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:
+ 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) -> void:
+ 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) -> void:
+ 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() -> void:
+ 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 add_action_event(last_input_text : String, last_input_event : InputEvent):
+ last_input_readable_name = 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))
+ already_assigned.emit(readable_action_name, last_input_readable_name)
+ else:
+ _assign_input_event_to_action(last_input_event, editing_action_name)
+ editing_action_name = ""
+
+func remove_action_event(item : TreeItem) -> void:
+ 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)
+ minimum_reached.emit(readable_action_name)
+ return
+ _remove_input_event_from_action(input_event, action_name)
+ var parent_tree_item = item.get_parent()
+ parent_tree_item.remove_child(item)
+
+func reset() -> void:
+ AppSettings.reset_to_default_inputs()
+ _build_assigned_input_events()
+ _build_ui_tree()
+
+func _add_item(item : TreeItem) -> void:
+ editing_item = item
+ editing_action_name = tree_item_add_map[item]
+ var readable_action_name = tr(_get_action_readable_name(editing_action_name))
+ add_button_clicked.emit(readable_action_name)
+
+func _remove_item(item : TreeItem) -> void:
+ editing_item = item
+ editing_action_name = tree_item_action_map[item]
+ var readable_action_name = tr(_get_action_readable_name(editing_action_name))
+ var item_text = item.get_text(0)
+ remove_button_clicked.emit(readable_action_name, item_text)
+
+func _check_item_actions(item : TreeItem) -> void:
+ if item in tree_item_add_map:
+ _add_item(item)
+ elif item in tree_item_remove_map:
+ _remove_item(item)
+
+func _on_button_clicked(item : TreeItem, _column, _id, _mouse_button_index) -> void:
+ _check_item_actions(item)
+
+func _on_item_activated() -> void:
+ var item = get_selected()
+ _check_item_actions(item)
+
+func _ready() -> void:
+ if Engine.is_editor_hint(): return
+ _build_assigned_input_events()
+ _build_ui_tree()
+ button_clicked.connect(_on_button_clicked)
+ item_activated.connect(_on_item_activated)
+ if input_icon_mapper:
+ input_icon_mapper.joypad_device_changed.connect(_build_ui_tree)
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.gd.uid
new file mode 100644
index 0000000..d09dd25
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.gd.uid
@@ -0,0 +1 @@
+uid://bp7d2e5djo2tp
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.tscn
new file mode 100644
index 0000000..89d2a24
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.tscn
@@ -0,0 +1,24 @@
+[gd_scene load_steps=4 format=3 uid="uid://ci6wgl2ngd35n"]
+
+[ext_resource type="Script" uid="uid://bp7d2e5djo2tp" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.gd" id="1_o33o4"]
+[ext_resource type="Texture2D" uid="uid://c1eqf1cse1hch" path="res://addons/maaacks_game_template/base/assets/remapping_input_icons/addition_symbol.png" id="2_ppi0j"]
+[ext_resource type="Texture2D" uid="uid://bteq3ica74h30" path="res://addons/maaacks_game_template/base/assets/remapping_input_icons/subtraction_symbol.png" id="3_hb3xh"]
+
+[node name="InputActionsTree" type="Tree"]
+custom_minimum_size = Vector2(400, 240)
+size_flags_vertical = 3
+hide_root = true
+script = ExtResource("1_o33o4")
+input_action_names = Array[StringName]([&"move_forward", &"move_backward", &"move_up", &"move_down", &"move_left", &"move_right", &"interact"])
+readable_action_names = Array[String](["Move Forward", "Move Backward", "Move Up", "Move Down", "Move Left", "Move Right", "Interact"])
+add_button_texture = ExtResource("2_ppi0j")
+remove_button_texture = ExtResource("3_hb3xh")
+action_name_map = {
+"interact": "Interact",
+"move_backward": "Move Backward",
+"move_down": "Move Down",
+"move_forward": "Move Forward",
+"move_left": "Move Left",
+"move_right": "Move Right",
+"move_up": "Move Up"
+}
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.gd
new file mode 100644
index 0000000..d186536
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.gd
@@ -0,0 +1,140 @@
+@tool
+class_name InputIconMapper
+extends FileLister
+
+signal joypad_device_changed
+
+const COMMON_REPLACE_STRINGS: Dictionary = {
+ "L 1": "Left Shoulder",
+ "R 1": "Right Shoulder",
+ "L 2": "Left Trigger",
+ "R 2": "Right Trigger",
+ "Lt": "Left Trigger",
+ "Rt": "Right Trigger",
+ "Lb": "Left Shoulder",
+ "Rb": "Right Shoulder",
+} # Dictionary[String, String]
+## Gives priority to icons with occurrences of the provided strings.
+@export var prioritized_strings : Array[String]
+## Replaces the first occurence in icon names of the key with the value.
+@export var replace_strings : Dictionary # Dictionary[String, String]
+## Filters the icon names of the provided strings.
+@export var filtered_strings : Array[String]
+## Adds entries for "Up", "Down", "Left", "Right" to icon names ending with "Stick".
+@export var add_stick_directions : bool = false
+@export var intial_joypad_device : String = InputEventHelper.DEVICE_GENERIC
+## Attempt to match the icon names to the input names based on the string rules.
+@export var _match_icons_to_inputs_action : bool = false :
+ set(value):
+ if value and Engine.is_editor_hint():
+ _match_icons_to_inputs()
+# For Godot 4.4
+# @export_tool_button("Match Icons to Inputs") var _match_icons_to_inputs_action = _match_icons_to_inputs
+@export var matching_icons : Dictionary # Dictionary[String, Texture]
+@export_group("Debug")
+@export var all_icons : Dictionary # Dictionary[String, Texture]
+
+@onready var last_joypad_device = intial_joypad_device
+
+func _is_end_of_word(full_string : String, what : String) -> bool:
+ var string_end_position = full_string.find(what) + what.length()
+ var end_of_word : bool
+ if string_end_position + 1 < full_string.length():
+ var next_character = full_string.substr(string_end_position, 1)
+ end_of_word = next_character == " "
+ return full_string.ends_with(what) or end_of_word
+
+func _get_standard_joy_name(joy_name : String) -> String:
+ var all_replace_strings := replace_strings.duplicate()
+ all_replace_strings.merge(COMMON_REPLACE_STRINGS)
+ for what in all_replace_strings:
+ if joy_name.contains(what) and _is_end_of_word(joy_name, what):
+ var position = joy_name.find(what)
+ joy_name = joy_name.erase(position, what.length())
+ joy_name = joy_name.insert(position, all_replace_strings[what])
+ var combined_joystick_name : Array[String] = []
+ for part in joy_name.split(" "):
+ if part.to_lower() in filtered_strings:
+ continue
+ if not part.is_empty():
+ combined_joystick_name.append(part)
+ joy_name = " ".join(combined_joystick_name)
+ joy_name = joy_name.strip_edges()
+ return joy_name
+
+func _match_icon_to_file(file : String) -> void:
+ var matching_string : String = file.get_file().get_basename()
+ var icon : Texture = load(file)
+ if not icon:
+ return
+ all_icons[matching_string] = icon
+ matching_string = matching_string.capitalize()
+ matching_string = _get_standard_joy_name(matching_string)
+ matching_string = matching_string.strip_edges()
+ if add_stick_directions and matching_string.ends_with("Stick"):
+ matching_icons[matching_string + " Up"] = icon
+ matching_icons[matching_string + " Down"] = icon
+ matching_icons[matching_string + " Left"] = icon
+ matching_icons[matching_string + " Right"] = icon
+ return
+ if matching_string in matching_icons:
+ return
+ matching_icons[matching_string] = icon
+
+func _prioritized_files() -> Array[String]:
+ var priority_levels : Dictionary # Dictionary[String, int]
+ var priortized_files : Array[String]
+ for prioritized_string in prioritized_strings:
+ for file in files:
+ if file.containsn(prioritized_string):
+ if file in priority_levels:
+ priority_levels[file] += 1
+ else:
+ priority_levels[file] = 1
+ var priority_file_map : Dictionary # Dictionary[int, Array]
+ var max_priority_level : int = 0
+ for file in priority_levels:
+ var priority_level = priority_levels[file]
+ max_priority_level = max(priority_level, max_priority_level)
+ if priority_level in priority_file_map:
+ priority_file_map[priority_level].append(file)
+ else:
+ priority_file_map[priority_level] = [file]
+ while max_priority_level > 0:
+ for priority_file in priority_file_map[max_priority_level]:
+ priortized_files.append(priority_file)
+ max_priority_level -= 1
+ return priortized_files
+
+func _match_icons_to_inputs() -> void:
+ matching_icons.clear()
+ all_icons.clear()
+ for prioritized_file in _prioritized_files():
+ _match_icon_to_file(prioritized_file)
+ for file in files:
+ _match_icon_to_file(file)
+
+func get_icon(input_event : InputEvent) -> Texture:
+ var specific_text = InputEventHelper.get_device_specific_text(input_event, last_joypad_device)
+ if specific_text in matching_icons:
+ return matching_icons[specific_text]
+ return null
+
+func _assign_joypad_0_to_last() -> void:
+ if last_joypad_device != intial_joypad_device : return
+ var connected_joypads := Input.get_connected_joypads()
+ if connected_joypads.is_empty(): return
+ last_joypad_device = InputEventHelper.get_device_name_by_id(connected_joypads[0])
+
+func _input(event : InputEvent) -> void:
+ var device_name = InputEventHelper.get_device_name(event)
+ if device_name != InputEventHelper.DEVICE_GENERIC and device_name != last_joypad_device:
+ last_joypad_device = device_name
+ joypad_device_changed.emit()
+
+func _ready() -> void:
+ _assign_joypad_0_to_last()
+ if files.size() == 0:
+ _refresh_files()
+ if matching_icons.size() == 0:
+ _match_icons_to_inputs()
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.gd.uid
new file mode 100644
index 0000000..4a8d67b
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.gd.uid
@@ -0,0 +1 @@
+uid://cqigj1uumknrp
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.tscn
new file mode 100644
index 0000000..e4fc58c
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://qoexj4ptqt8a"]
+
+[ext_resource type="Script" uid="uid://cqigj1uumknrp" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.gd" id="1_msrpt"]
+
+[node name="InputIconMapper" type="Node"]
+script = ExtResource("1_msrpt")
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.gd
new file mode 100644
index 0000000..249642c
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.gd
@@ -0,0 +1,102 @@
+@tool
+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}?"
+
+@export_enum("List", "Tree") var remapping_mode : int = 0 :
+ set(value):
+ remapping_mode = value
+ if is_inside_tree():
+ match(remapping_mode):
+ 0:
+ %InputActionsList.show()
+ %InputActionsTree.hide()
+ 1:
+ %InputActionsList.hide()
+ %InputActionsTree.show()
+
+@onready var assignment_placeholder_text = $KeyAssignmentDialog.dialog_text
+
+var last_input_readable_name
+
+func _horizontally_align_popup_labels() -> void:
+ $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
+ $ResetConfirmationDialog.get_label().horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+
+func _ready() -> void:
+ remapping_mode = remapping_mode
+ if Engine.is_editor_hint(): return
+ _horizontally_align_popup_labels()
+
+func _add_action_event() -> void:
+ var last_input_event = $KeyAssignmentDialog.last_input_event
+ last_input_readable_name = $KeyAssignmentDialog.last_input_text
+ match(remapping_mode):
+ 0:
+ %InputActionsList.add_action_event(last_input_readable_name, last_input_event)
+ 1:
+ %InputActionsTree.add_action_event(last_input_readable_name, last_input_event)
+
+func _remove_action_event(item : TreeItem) -> void:
+ %InputActionsTree.remove_action_event(item)
+
+func _on_reset_button_pressed() -> void:
+ $ResetConfirmationDialog.popup_centered()
+
+func _on_key_deletion_dialog_confirmed() -> void:
+ var editing_item = %InputActionsTree.editing_item
+ if is_instance_valid(editing_item):
+ _remove_action_event(editing_item)
+
+func _on_key_assignment_dialog_confirmed() -> void:
+ _add_action_event()
+
+func _open_key_assignment_dialog(action_name : String, readable_input_name : String = assignment_placeholder_text) -> void:
+ $KeyAssignmentDialog.title = tr("Assign Key for {action}").format({action = action_name})
+ $KeyAssignmentDialog.dialog_text = readable_input_name
+ $KeyAssignmentDialog.get_ok_button().disabled = true
+ $KeyAssignmentDialog.popup_centered()
+
+func _on_input_actions_tree_add_button_clicked(action_name) -> void:
+ _open_key_assignment_dialog(action_name)
+
+func _on_input_actions_tree_remove_button_clicked(action_name, input_name) -> void:
+ $KeyDeletionDialog.title = tr("Remove Key for {action}").format({action = action_name})
+ $KeyDeletionDialog.dialog_text = tr(KEY_DELETION_TEXT).format({key = input_name, action = action_name})
+ $KeyDeletionDialog.popup_centered()
+
+func _popup_already_assigned(action_name, input_name) -> void:
+ $AlreadyAssignedDialog.dialog_text = tr(ALREADY_ASSIGNED_TEXT).format({key = input_name, action = action_name})
+ $AlreadyAssignedDialog.popup_centered.call_deferred()
+
+func _popup_minimum_reached(action_name : String) -> void:
+ $OneInputMinimumDialog.dialog_text = ONE_INPUT_MINIMUM_TEXT % action_name
+ $OneInputMinimumDialog.popup_centered.call_deferred()
+
+func _on_input_actions_tree_already_assigned(action_name, input_name) -> void:
+ _popup_already_assigned(action_name, input_name)
+
+func _on_input_actions_tree_minimum_reached(action_name) -> void:
+ _popup_minimum_reached(action_name)
+
+func _on_input_actions_list_already_assigned(action_name, input_name) -> void:
+ _popup_already_assigned(action_name, input_name)
+
+func _on_input_actions_list_minimum_reached(action_name) -> void:
+ _popup_minimum_reached(action_name)
+
+func _on_input_actions_list_button_clicked(action_name, readable_input_name) -> void:
+ _open_key_assignment_dialog(action_name, readable_input_name)
+
+func _on_reset_confirmation_dialog_confirmed() -> void:
+ match(remapping_mode):
+ 0:
+ %InputActionsList.reset()
+ 1:
+ %InputActionsTree.reset()
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.gd.uid
new file mode 100644
index 0000000..72cfa78
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://eborw7q4b07h
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn
new file mode 100644
index 0000000..46d2a47
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn
@@ -0,0 +1,136 @@
+[gd_scene load_steps=7 format=3 uid="uid://dp3rgqaehb3xu"]
+
+[ext_resource type="Script" uid="uid://eborw7q4b07h" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.gd" id="1"]
+[ext_resource type="PackedScene" uid="uid://qoexj4ptqt8a" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_icon_mapper.tscn" id="2_627ul"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_wft4x"]
+[ext_resource type="Script" uid="uid://custha7r0uoic" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/key_assignment_dialog.gd" id="3_wsh2h"]
+[ext_resource type="PackedScene" uid="uid://bxp45814v6ydv" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_list.tscn" id="4_lf2nw"]
+[ext_resource type="PackedScene" uid="uid://ci6wgl2ngd35n" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_actions_tree.tscn" id="5_b2whh"]
+
+[node name="Controls" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/margin_left = 32
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 32
+theme_override_constants/margin_bottom = 8
+script = ExtResource("1")
+
+[node name="InputIconMapper" parent="." instance=ExtResource("2_627ul")]
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+script = ExtResource("2_wft4x")
+search_depth = 5
+
+[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="InputActionsList" parent="VBoxContainer/InputMappingContainer" node_paths=PackedStringArray("input_icon_mapper") instance=ExtResource("4_lf2nw")]
+unique_name_in_owner = true
+layout_mode = 2
+input_icon_mapper = NodePath("../../../InputIconMapper")
+
+[node name="InputActionsTree" parent="VBoxContainer/InputMappingContainer" node_paths=PackedStringArray("input_icon_mapper") instance=ExtResource("5_b2whh")]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+input_icon_mapper = NodePath("../../../InputIconMapper")
+
+[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.1
+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="ResetConfirmationDialog" type="ConfirmationDialog" parent="."]
+size = Vector2i(486, 100)
+dialog_text = "Are you sure you want to reset controls back to the defaults?"
+
+[connection signal="already_assigned" from="VBoxContainer/InputMappingContainer/InputActionsList" to="." method="_on_input_actions_list_already_assigned"]
+[connection signal="button_clicked" from="VBoxContainer/InputMappingContainer/InputActionsList" to="." method="_on_input_actions_list_button_clicked"]
+[connection signal="minimum_reached" from="VBoxContainer/InputMappingContainer/InputActionsList" to="." method="_on_input_actions_list_minimum_reached"]
+[connection signal="add_button_clicked" from="VBoxContainer/InputMappingContainer/InputActionsTree" to="." method="_on_input_actions_tree_add_button_clicked"]
+[connection signal="already_assigned" from="VBoxContainer/InputMappingContainer/InputActionsTree" to="." method="_on_input_actions_tree_already_assigned"]
+[connection signal="minimum_reached" from="VBoxContainer/InputMappingContainer/InputActionsTree" to="." method="_on_input_actions_tree_minimum_reached"]
+[connection signal="remove_button_clicked" from="VBoxContainer/InputMappingContainer/InputActionsTree" to="." method="_on_input_actions_tree_remove_button_clicked"]
+[connection signal="pressed" from="VBoxContainer/InputMappingContainer/HBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"]
+[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="ResetConfirmationDialog" to="." method="_on_reset_confirmation_dialog_confirmed"]
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/key_assignment_dialog.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/key_assignment_dialog.gd
new file mode 100644
index 0000000..471d9e0
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/key_assignment_dialog.gd
@@ -0,0 +1,104 @@
+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"
+
+enum InputConfirmation {
+ SINGLE,
+ DOUBLE,
+ OK_BUTTON
+}
+@export var input_confirmation : InputConfirmation = InputConfirmation.SINGLE
+
+var last_input_event : InputEvent
+var last_input_text : String
+var listening : bool = false
+var confirming : bool = false
+
+func _record_input_event(event : InputEvent) -> void:
+ 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) -> bool:
+ 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() -> void:
+ %InputTextEdit.placeholder_text = LISTENING_TEXT
+ listening = true
+ %DelayTimer.start()
+
+func _stop_listening() -> void:
+ %InputTextEdit.placeholder_text = FOCUS_HERE_TEXT
+ listening = false
+ confirming = false
+
+func _on_text_edit_focus_entered() -> void:
+ _start_listening.call_deferred()
+
+func _on_input_text_edit_focus_exited() -> void:
+ _stop_listening()
+
+func _focus_on_ok() -> void:
+ get_ok_button().grab_focus()
+
+func _ready() -> void:
+ 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 _confirm_choice() -> void:
+ confirmed.emit()
+ hide()
+
+func _process_input_event(event : InputEvent) -> void:
+ if not _should_process_input_event(event):
+ return
+ if _input_confirms_choice(event):
+ confirming = false
+ if input_confirmation == InputConfirmation.DOUBLE:
+ _confirm_choice()
+ else:
+ _focus_on_ok.call_deferred()
+ return
+ _record_input_event(event)
+ if input_confirmation == InputConfirmation.SINGLE:
+ _confirm_choice()
+ if _should_confirm_input_event(event):
+ confirming = true
+ %DelayTimer.start()
+ %InputTextEdit.placeholder_text = CONFIRM_INPUT_TEXT
+
+func _on_input_text_edit_gui_input(event) -> void:
+ %InputTextEdit.set_deferred("text", "")
+ _process_input_event(event)
+
+func _on_visibility_changed() -> void:
+ if visible:
+ %InputLabel.text = NO_INPUT_TEXT
+ %InputTextEdit.grab_focus()
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/input/key_assignment_dialog.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/key_assignment_dialog.gd.uid
new file mode 100644
index 0000000..b34806b
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/input/key_assignment_dialog.gd.uid
@@ -0,0 +1 @@
+uid://custha7r0uoic
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.gd
new file mode 100644
index 0000000..f74c76f
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.gd
@@ -0,0 +1,13 @@
+class_name MasterOptionsMenu
+extends Control
+
+func _unhandled_input(event : InputEvent) -> void:
+ 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
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.gd.uid
new file mode 100644
index 0000000..01c0cbd
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://c3mignmhuvvq4
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.tscn
new file mode 100644
index 0000000..c3c0415
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.tscn
@@ -0,0 +1,23 @@
+[gd_scene load_steps=2 format=3 uid="uid://bvwl11s2p0hd"]
+
+[ext_resource type="Script" uid="uid://c3mignmhuvvq4" path="res://addons/maaacks_game_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
+size_flags_horizontal = 3
+size_flags_vertical = 3
+script = ExtResource("1_u08d5")
+
+[node name="TabContainer" type="TabContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+tab_alignment = 1
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu_with_tabs.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu_with_tabs.tscn
new file mode 100644
index 0000000..ed0f43c
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu_with_tabs.tscn
@@ -0,0 +1,25 @@
+[gd_scene load_steps=5 format=3 uid="uid://hmx6o472ropw"]
+
+[ext_resource type="PackedScene" uid="uid://bvwl11s2p0hd" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.tscn" id="1_uaidt"]
+[ext_resource type="PackedScene" uid="uid://dp3rgqaehb3xu" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn" id="2_15wl6"]
+[ext_resource type="PackedScene" uid="uid://c8vnncjwqcpab" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.tscn" id="3_qg4me"]
+[ext_resource type="PackedScene" uid="uid://b2numvphf2kau" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.tscn" id="4_1t848"]
+
+[node name="MasterOptionsMenu" instance=ExtResource("1_uaidt")]
+
+[node name="TabContainer" parent="." index="0"]
+current_tab = 0
+
+[node name="Controls" parent="TabContainer" index="1" instance=ExtResource("2_15wl6")]
+layout_mode = 2
+metadata/_tab_index = 0
+
+[node name="Audio" parent="TabContainer" index="2" instance=ExtResource("3_qg4me")]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 1
+
+[node name="Video" parent="TabContainer" index="3" instance=ExtResource("4_1t848")]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 2
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.gd
new file mode 100644
index 0000000..5394c7e
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.gd
@@ -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) -> void:
+ AppSettings.set_bus_volume(bus_iter, bus_value)
+
+func _add_audio_control(bus_name : String, bus_value : float, bus_iter : int) -> void:
+ 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() -> void:
+ 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() -> void:
+ _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() -> void:
+ _sync_with_config()
+
+func _on_mute_control_setting_changed(value : bool) -> void:
+ AppSettings.set_mute(value)
+
+func _on_fullscreen_control_setting_changed(value : bool) -> void:
+ AppSettings.set_fullscreen_enabled(value, get_window())
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.gd.uid
new file mode 100644
index 0000000..877aa95
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://1c0iyo5djoxj
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.tscn
new file mode 100644
index 0000000..7f9f994
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.tscn
@@ -0,0 +1,51 @@
+[gd_scene load_steps=5 format=3 uid="uid://vh1ucj2rfbby"]
+
+[ext_resource type="Script" uid="uid://1c0iyo5djoxj" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.gd" id="1_32vm2"]
+[ext_resource type="PackedScene" uid="uid://cl416gdb1fgwr" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn" id="2_kpc65"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="3_7qt1o"]
+[ext_resource type="PackedScene" uid="uid://bsxh6v7j0257h" path="res://addons/maaacks_game_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"]
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.gd
new file mode 100644
index 0000000..55e2a15
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.gd
@@ -0,0 +1,81 @@
+@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() -> void:
+ if option_values.is_empty(): return
+ custom_option_values = option_values.duplicate()
+ var first_value = custom_option_values.front()
+ property_type = typeof(first_value)
+ _set_titles_from_values()
+
+func _on_setting_changed(value : Variant) -> void:
+ if value < custom_option_values.size() and value >= 0:
+ super._on_setting_changed(custom_option_values[value])
+
+func _set_titles_from_values() -> void:
+ 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) -> 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 and typeof(value) == property_type:
+ 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)
+ return super._set_value(value)
+
+func _set_option_list(option_titles_list : Array) -> void:
+ %OptionButton.clear()
+ for option_title in option_titles_list:
+ %OptionButton.add_item(option_title)
+
+func disable_option(option_index : int, disabled : bool = true) -> void:
+ %OptionButton.set_item_disabled(option_index, disabled)
+
+func _ready() -> void:
+ lock_titles = lock_titles
+ option_titles = option_titles
+ option_values = option_values
+ super._ready()
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.gd.uid
new file mode 100644
index 0000000..69ab4eb
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.gd.uid
@@ -0,0 +1 @@
+uid://b8xqufg4re3c2
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn
new file mode 100644
index 0000000..2fd3eac
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn
@@ -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_game_template/base/scenes/menus/options_menu/option_control/option_control.tscn" id="1_blo3b"]
+[ext_resource type="Script" uid="uid://b8xqufg4re3c2" path="res://addons/maaacks_game_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
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.gd
new file mode 100644
index 0000000..919eec5
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.gd
@@ -0,0 +1,140 @@
+@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) -> void:
+ if Engine.is_editor_hint(): return
+ 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) -> void:
+ 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) -> 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
+ return value
+
+func set_value(value : Variant) -> void:
+ value = _set_value(value)
+ _on_setting_changed(value)
+
+func set_editable(value : bool = true) -> void:
+ 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() -> void:
+ 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() -> Array[Dictionary]:
+ return [
+ { "name": "value", "type": property_type, "usage": PROPERTY_USAGE_NONE},
+ { "name": "default_value", "type": property_type}
+ ]
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.gd.uid
new file mode 100644
index 0000000..54d3316
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.gd.uid
@@ -0,0 +1 @@
+uid://cafqki2b08kwu
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.tscn
new file mode 100644
index 0000000..42b4bc9
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/option_control.tscn
@@ -0,0 +1,17 @@
+[gd_scene load_steps=2 format=3 uid="uid://d7te75il06t7"]
+
+[ext_resource type="Script" uid="uid://cafqki2b08kwu" path="res://addons/maaacks_game_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
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn
new file mode 100644
index 0000000..32d05df
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn
@@ -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_game_template/base/scenes/menus/options_menu/option_control/option_control.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
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/toggle_option_control.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/toggle_option_control.tscn
new file mode 100644
index 0000000..cb1353c
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/toggle_option_control.tscn
@@ -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_game_template/base/scenes/menus/options_menu/option_control/option_control.tscn" id="1_8rnmo"]
+
+[node name="OptionControl" instance=ExtResource("1_8rnmo")]
+
+[node name="CheckButton" type="CheckButton" parent="." index="1"]
+layout_mode = 2
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.gd
new file mode 100644
index 0000000..562d2e3
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.gd
@@ -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)
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.gd.uid
new file mode 100644
index 0000000..562da78
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.gd.uid
@@ -0,0 +1 @@
+uid://brntdgf3sv0s0
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.tscn
new file mode 100644
index 0000000..2d81426
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.tscn
@@ -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_game_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn" id="1_jqwiw"]
+[ext_resource type="Script" uid="uid://brntdgf3sv0s0" path="res://addons/maaacks_game_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")
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.gd b/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.gd
new file mode 100644
index 0000000..4e22563
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.gd
@@ -0,0 +1,38 @@
+class_name VideoOptionsMenu
+extends Control
+
+func _preselect_resolution(window : Window) -> void:
+ %ResolutionControl.value = window.size
+
+func _update_resolution_options_enabled(window : Window) -> void:
+ 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) -> void:
+ %FullscreenControl.value = AppSettings.is_fullscreen(window)
+ _preselect_resolution(window)
+ %VSyncControl.value = AppSettings.get_vsync(window)
+ _update_resolution_options_enabled(window)
+
+func _ready() -> void:
+ var window : Window = get_window()
+ _update_ui(window)
+ window.connect("size_changed", _preselect_resolution.bind(window))
+
+func _on_fullscreen_control_setting_changed(value) -> void:
+ var window : Window = get_window()
+ AppSettings.set_fullscreen_enabled(value, window)
+ _update_resolution_options_enabled(window)
+
+func _on_resolution_control_setting_changed(value) -> void:
+ AppSettings.set_resolution(value, get_window(), false)
+
+func _on_v_sync_control_setting_changed(value) -> void:
+ AppSettings.set_vsync(value, get_window())
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.gd.uid b/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.gd.uid
new file mode 100644
index 0000000..77495f0
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://cpe5r24151r5n
diff --git a/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.tscn b/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.tscn
new file mode 100644
index 0000000..9b80110
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.tscn
@@ -0,0 +1,60 @@
+[gd_scene load_steps=6 format=3 uid="uid://b2numvphf2kau"]
+
+[ext_resource type="Script" uid="uid://cpe5r24151r5n" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.gd" id="1"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_dgrai"]
+[ext_resource type="PackedScene" uid="uid://bsxh6v7j0257h" path="res://addons/maaacks_game_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_game_template/base/scenes/menus/options_menu/option_control/vector_2_list_option_control.tscn" id="4_gwtfq"]
+[ext_resource type="PackedScene" uid="uid://b6bl3n5mp3m1e" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn" id="5_881de"]
+
+[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"
+property_type = 6
+
+[node name="VSyncControl" parent="VBoxContainer" instance=ExtResource("5_881de")]
+unique_name_in_owner = true
+layout_mode = 2
+lock_titles = true
+option_values = [0, 1, 2, 3]
+option_titles = Array[String](["Disabled", "Enabled", "Adaptive", "Mailbox"])
+option_name = "V-Sync"
+option_section = 3
+key = "V-sync"
+section = "VideoSettings"
+property_type = 2
+default_value = 0
+
+[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"]
+[connection signal="setting_changed" from="VBoxContainer/VSyncControl" to="." method="_on_v_sync_control_setting_changed"]
diff --git a/addons/maaacks_game_template/base/scenes/music_players/background_music_player.tscn b/addons/maaacks_game_template/base/scenes/music_players/background_music_player.tscn
new file mode 100644
index 0000000..e9c3026
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/music_players/background_music_player.tscn
@@ -0,0 +1,6 @@
+[gd_scene format=3 uid="uid://bkcsjsk2ciff"]
+
+[node name="BackgroundMusicPlayer" type="AudioStreamPlayer"]
+process_mode = 3
+autoplay = true
+bus = &"Music"
diff --git a/addons/maaacks_game_template/base/scenes/opening/opening.gd b/addons/maaacks_game_template/base/scenes/opening/opening.gd
new file mode 100644
index 0000000..eef6d9a
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/opening/opening.gd
@@ -0,0 +1,100 @@
+extends Control
+
+@export_file("*.tscn") var next_scene : String
+@export var images : Array[Texture2D]
+@export_group("Animation")
+@export var fade_in_time : float = 0.2
+@export var fade_out_time : float = 0.2
+@export var visible_time : float = 1.6
+@export_group("Transition")
+@export var start_delay : float = 0.5
+@export var end_delay : float = 0.5
+@export var show_loading_screen : bool = false
+
+var tween : Tween
+var next_image_index : int = 0
+
+func _load_next_scene() -> void:
+ var status = SceneLoader.get_status()
+ if show_loading_screen or status != ResourceLoader.THREAD_LOAD_LOADED:
+ SceneLoader.change_scene_to_loading_screen()
+ else:
+ SceneLoader.change_scene_to_resource()
+
+func _add_textures_to_container(textures : Array[Texture2D]) -> void:
+ for texture in textures:
+ var texture_rect : TextureRect = TextureRect.new()
+ texture_rect.texture = texture
+ texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
+ texture_rect.modulate.a = 0.0
+ %ImagesContainer.call_deferred("add_child", texture_rect)
+
+func _event_skips_image(event : InputEvent) -> bool:
+ return event.is_action_released(&"ui_accept") or event.is_action_released(&"ui_select")
+
+func _event_skips_intro(event : InputEvent) -> bool:
+ return event.is_action_released(&"ui_cancel")
+
+func _event_is_mouse_button_released(event : InputEvent) -> bool:
+ return event is InputEventMouseButton and not event.is_pressed()
+
+func _unhandled_input(event : InputEvent) -> void:
+ if _event_skips_intro(event):
+ _load_next_scene()
+ elif _event_skips_image(event):
+ _show_next_image(false)
+
+func _gui_input(event : InputEvent) -> void:
+ if _event_is_mouse_button_released(event):
+ _show_next_image(false)
+
+func _transition_out() -> void:
+ await get_tree().create_timer(end_delay).timeout
+ _load_next_scene()
+
+func _transition_in() -> void:
+ await get_tree().create_timer(start_delay).timeout
+ if next_image_index == 0:
+ _show_next_image()
+
+func _wait_and_fade_out(texture_rect : TextureRect) -> void:
+ var _compare_next_index = next_image_index
+ await get_tree().create_timer(visible_time, false).timeout
+ if _compare_next_index != next_image_index : return
+ tween = create_tween()
+ tween.tween_property(texture_rect, "modulate:a", 0.0, fade_out_time)
+ await tween.finished
+ _show_next_image.call_deferred()
+
+func _hide_previous_image() -> void:
+ if tween and tween.is_running():
+ tween.stop()
+ if %ImagesContainer.get_child_count() == 0:
+ return
+ var current_image = %ImagesContainer.get_child(next_image_index - 1)
+ if current_image:
+ current_image.modulate.a = 0.0
+
+func _show_next_image(animated : bool = true) -> void:
+ _hide_previous_image()
+ if next_image_index >= %ImagesContainer.get_child_count():
+ if animated:
+ _transition_out()
+ else:
+ _load_next_scene()
+ return
+ var texture_rect = %ImagesContainer.get_child(next_image_index)
+ if animated:
+ tween = create_tween()
+ tween.tween_property(texture_rect, "modulate:a", 1.0, fade_in_time)
+ await tween.finished
+ else:
+ texture_rect.modulate.a = 1.0
+ next_image_index += 1
+ _wait_and_fade_out(texture_rect)
+
+func _ready() -> void:
+ SceneLoader.load_scene(next_scene, true)
+ _add_textures_to_container(images)
+ _transition_in()
diff --git a/addons/maaacks_game_template/base/scenes/opening/opening.gd.uid b/addons/maaacks_game_template/base/scenes/opening/opening.gd.uid
new file mode 100644
index 0000000..4b25152
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/opening/opening.gd.uid
@@ -0,0 +1 @@
+uid://dtco0s8byckx6
diff --git a/addons/maaacks_game_template/base/scenes/opening/opening.tscn b/addons/maaacks_game_template/base/scenes/opening/opening.tscn
new file mode 100644
index 0000000..273643f
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/opening/opening.tscn
@@ -0,0 +1,27 @@
+[gd_scene load_steps=2 format=3 uid="uid://sikc02ddepyt"]
+
+[ext_resource type="Script" uid="uid://dtco0s8byckx6" path="res://addons/maaacks_game_template/base/scenes/opening/opening.gd" id="1_fcjph"]
+
+[node name="Opening" 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_fcjph")
+next_scene = "res://addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn"
+
+[node name="BackgroundMusicPlayer" type="AudioStreamPlayer" parent="."]
+process_mode = 3
+autoplay = true
+bus = &"Music"
+
+[node name="ImagesContainer" 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
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/mini_options_overlaid_menu.tscn b/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/mini_options_overlaid_menu.tscn
new file mode 100644
index 0000000..5e78082
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/mini_options_overlaid_menu.tscn
@@ -0,0 +1,10 @@
+[gd_scene load_steps=3 format=3 uid="uid://cikf3o5omnunl"]
+
+[ext_resource type="PackedScene" uid="uid://bqqngki8bm3iq" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.tscn" id="1_kverk"]
+[ext_resource type="PackedScene" uid="uid://vh1ucj2rfbby" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.tscn" id="2_ihtu5"]
+
+[node name="OverlaidMenuContainer" instance=ExtResource("1_kverk")]
+menu_scene = ExtResource("2_ihtu5")
+
+[node name="TitleLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/TitleMargin" index="0"]
+text = "Options"
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.gd b/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.gd
new file mode 100644
index 0000000..56cde4b
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.gd
@@ -0,0 +1,81 @@
+class_name PauseMenu
+extends OverlaidMenu
+
+@export var options_packed_scene : PackedScene
+@export_file("*.tscn") var main_menu_scene : String
+
+var popup_open : Node
+
+func close_popup() -> void:
+ if popup_open != null:
+ popup_open.hide()
+ popup_open = null
+
+func _disable_focus() -> void:
+ for child in %MenuButtons.get_children():
+ if child is Control:
+ child.focus_mode = FOCUS_NONE
+
+func _enable_focus() -> void:
+ for child in %MenuButtons.get_children():
+ if child is Control:
+ child.focus_mode = FOCUS_ALL
+
+func _load_scene(scene_path: String) -> void:
+ _scene_tree.paused = false
+ SceneLoader.load_scene(scene_path)
+
+func open_options_menu() -> void:
+ var options_scene := options_packed_scene.instantiate()
+ add_child(options_scene)
+ _disable_focus.call_deferred()
+ await options_scene.tree_exiting
+ _enable_focus.call_deferred()
+
+func _handle_cancel_input() -> void:
+ if popup_open != null:
+ close_popup()
+ else:
+ super._handle_cancel_input()
+
+func _hide_exit_for_web() -> void:
+ if OS.has_feature("web"):
+ %ExitButton.hide()
+
+func _hide_options_if_unset() -> void:
+ if options_packed_scene == null:
+ %OptionsButton.hide()
+
+func _hide_main_menu_if_unset() -> void:
+ if main_menu_scene.is_empty():
+ %MainMenuButton.hide()
+
+func _ready() -> void:
+ _hide_exit_for_web()
+ _hide_options_if_unset()
+ _hide_main_menu_if_unset()
+
+func _on_restart_button_pressed() -> void:
+ %ConfirmRestart.popup_centered()
+ popup_open = %ConfirmRestart
+
+func _on_options_button_pressed() -> void:
+ open_options_menu()
+
+func _on_main_menu_button_pressed() -> void:
+ %ConfirmMainMenu.popup_centered()
+ popup_open = %ConfirmMainMenu
+
+func _on_exit_button_pressed() -> void:
+ %ConfirmExit.popup_centered()
+ popup_open = %ConfirmExit
+
+func _on_confirm_restart_confirmed() -> void:
+ SceneLoader.reload_current_scene()
+ close()
+
+func _on_confirm_main_menu_confirmed() -> void:
+ _load_scene(main_menu_scene)
+
+func _on_confirm_exit_confirmed() -> void:
+ get_tree().quit()
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.gd.uid b/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.gd.uid
new file mode 100644
index 0000000..2c8aacf
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.gd.uid
@@ -0,0 +1 @@
+uid://uidwhqh4fyhj
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.tscn b/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.tscn
new file mode 100644
index 0000000..ee6a450
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.tscn
@@ -0,0 +1,68 @@
+[gd_scene load_steps=4 format=3 uid="uid://b5cd6sa8qq4vc"]
+
+[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_gm3uv"]
+[ext_resource type="Script" uid="uid://uidwhqh4fyhj" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.gd" id="2_0ln3r"]
+[ext_resource type="PackedScene" uid="uid://cikf3o5omnunl" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/menus/mini_options_overlaid_menu.tscn" id="3_kv70e"]
+
+[node name="PauseMenu" instance=ExtResource("1_gm3uv")]
+process_mode = 3
+script = ExtResource("2_0ln3r")
+options_packed_scene = ExtResource("3_kv70e")
+main_menu_scene = "res://addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn"
+pauses_game = true
+
+[node name="MarginContainer" parent="MenuPanelContainer" index="0"]
+theme_override_constants/margin_left = 64
+theme_override_constants/margin_right = 64
+
+[node name="TitleLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/TitleMargin" index="0"]
+text = "Paused"
+
+[node name="MenuButtonsMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="2"]
+theme_override_constants/margin_top = 16
+theme_override_constants/margin_bottom = 16
+
+[node name="CloseButton" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0"]
+text = "Resume"
+
+[node name="RestartButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1"]
+layout_mode = 2
+text = "Restart"
+
+[node name="OptionsButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="2"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Options"
+
+[node name="MainMenuButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="3"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Main Menu"
+
+[node name="ExitButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="4"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Exit Game"
+
+[node name="ConfirmRestart" type="ConfirmationDialog" parent="." index="2"]
+unique_name_in_owner = true
+auto_translate_mode = 1
+dialog_text = "Restart the game?"
+
+[node name="ConfirmMainMenu" type="ConfirmationDialog" parent="." index="3"]
+unique_name_in_owner = true
+auto_translate_mode = 1
+dialog_text = "Go back to main menu?"
+
+[node name="ConfirmExit" type="ConfirmationDialog" parent="." index="4"]
+unique_name_in_owner = true
+auto_translate_mode = 1
+dialog_text = "Quit the game?"
+
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/RestartButton" to="." method="_on_restart_button_pressed"]
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/OptionsButton" to="." method="_on_options_button_pressed"]
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/MainMenuButton" to="." method="_on_main_menu_button_pressed"]
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/ExitButton" to="." method="_on_exit_button_pressed"]
+[connection signal="confirmed" from="ConfirmRestart" to="." method="_on_confirm_restart_confirmed"]
+[connection signal="confirmed" from="ConfirmMainMenu" to="." method="_on_confirm_main_menu_confirmed"]
+[connection signal="confirmed" from="ConfirmExit" to="." method="_on_confirm_exit_confirmed"]
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.gd b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.gd
new file mode 100644
index 0000000..92c47f2
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.gd
@@ -0,0 +1,49 @@
+@tool
+class_name OverlaidMenu
+extends Control
+
+@export var pauses_game : bool = false :
+ set(value):
+ pauses_game = value
+ if pauses_game:
+ process_mode = PROCESS_MODE_ALWAYS
+ else:
+ process_mode = PROCESS_MODE_INHERIT
+@export var makes_mouse_visible : bool = true
+
+var _initial_pause_state : bool = false
+var _initial_focus_mode : FocusMode = FOCUS_ALL
+var _initial_mouse_mode : Input.MouseMode
+var _initial_focus_control
+var _scene_tree : SceneTree
+
+func close() -> void:
+ _scene_tree.paused = _initial_pause_state
+ Input.set_mouse_mode(_initial_mouse_mode)
+ if is_instance_valid(_initial_focus_control) and _initial_focus_control.is_inside_tree():
+ _initial_focus_control.focus_mode = _initial_focus_mode
+ _initial_focus_control.grab_focus()
+ queue_free()
+
+func _handle_cancel_input() -> void:
+ close()
+
+func _unhandled_input(event : InputEvent) -> void:
+ if event.is_action_pressed("ui_cancel"):
+ _handle_cancel_input()
+ get_viewport().set_input_as_handled()
+
+func _on_close_button_pressed() -> void:
+ close()
+
+func _enter_tree() -> void:
+ _scene_tree = get_tree()
+ _initial_pause_state = _scene_tree.paused
+ _initial_mouse_mode = Input.get_mouse_mode()
+ _initial_focus_control = get_viewport().gui_get_focus_owner()
+ if _initial_focus_control:
+ _initial_focus_mode = _initial_focus_control.focus_mode
+ if Engine.is_editor_hint(): return
+ _scene_tree.paused = pauses_game or _initial_pause_state
+ if makes_mouse_visible:
+ Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.gd.uid b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.gd.uid
new file mode 100644
index 0000000..4a7fdb9
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.gd.uid
@@ -0,0 +1 @@
+uid://xfugmpspqbcc
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn
new file mode 100644
index 0000000..65c9d7f
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn
@@ -0,0 +1,88 @@
+[gd_scene load_steps=3 format=3 uid="uid://wny2d8dvp3ok"]
+
+[ext_resource type="Script" uid="uid://xfugmpspqbcc" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.gd" id="1_euyj1"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_6ani0"]
+
+[node name="OverlaidMenu" 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_euyj1")
+
+[node name="BackgroundColor" type="ColorRect" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+color = Color(0, 0, 0, 0.12549)
+
+[node name="MenuPanelContainer" type="PanelContainer" parent="."]
+unique_name_in_owner = true
+process_mode = 3
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -16.0
+offset_top = -16.0
+offset_right = 16.0
+offset_bottom = 16.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="MenuPanelContainer"]
+layout_mode = 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="BoxContainer" type="BoxContainer" parent="MenuPanelContainer/MarginContainer"]
+layout_mode = 2
+vertical = true
+
+[node name="TitleMargin" type="MarginContainer" parent="MenuPanelContainer/MarginContainer/BoxContainer"]
+layout_mode = 2
+
+[node name="TitleLabel" type="Label" parent="MenuPanelContainer/MarginContainer/BoxContainer/TitleMargin"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 24
+text = "Menu"
+horizontal_alignment = 1
+
+[node name="DescriptionMargin" type="MarginContainer" parent="MenuPanelContainer/MarginContainer/BoxContainer"]
+visible = false
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="DescriptionLabel" type="RichTextLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin"]
+layout_mode = 2
+bbcode_enabled = true
+
+[node name="MenuButtonsMargin" type="MarginContainer" parent="MenuPanelContainer/MarginContainer/BoxContainer"]
+layout_mode = 2
+
+[node name="MenuButtons" type="BoxContainer" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(128, 0)
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 3
+theme_override_constants/separation = 16
+alignment = 1
+vertical = true
+script = ExtResource("2_6ani0")
+
+[node name="CloseButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons"]
+layout_mode = 2
+text = "Close"
+
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/CloseButton" to="." method="_on_close_button_pressed"]
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.gd b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.gd
new file mode 100644
index 0000000..ec4f455
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.gd
@@ -0,0 +1,14 @@
+@tool
+class_name OverlaidMenuContainer
+extends OverlaidMenu
+
+@export var menu_scene : PackedScene :
+ set(value):
+ var _value_changed = menu_scene != value
+ menu_scene = value
+ if _value_changed:
+ for child in %MenuContainer.get_children():
+ child.queue_free()
+ if menu_scene:
+ var _instance = menu_scene.instantiate()
+ %MenuContainer.add_child(_instance)
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.gd.uid b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.gd.uid
new file mode 100644
index 0000000..c378b87
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.gd.uid
@@ -0,0 +1 @@
+uid://droejgtv8bu0s
diff --git a/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.tscn b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.tscn
new file mode 100644
index 0000000..5511e2c
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.tscn
@@ -0,0 +1,21 @@
+[gd_scene load_steps=3 format=3 uid="uid://bqqngki8bm3iq"]
+
+[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_xgkve"]
+[ext_resource type="Script" uid="uid://droejgtv8bu0s" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.gd" id="2_owcue"]
+
+[node name="OverlaidMenuContainer" instance=ExtResource("1_xgkve")]
+script = ExtResource("2_owcue")
+menu_scene = null
+
+[node name="MenuContainer" type="MarginContainer" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="2"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="MenuButtonsMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="3"]
+theme_override_constants/margin_top = 16
+theme_override_constants/margin_bottom = 16
+
+[node name="CloseButton" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0"]
+size_flags_horizontal = 4
+text = "Back"
diff --git a/addons/maaacks_game_template/base/scenes/utilities/api_client.gd b/addons/maaacks_game_template/base/scenes/utilities/api_client.gd
new file mode 100644
index 0000000..73f5b98
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/utilities/api_client.gd
@@ -0,0 +1,155 @@
+@tool
+class_name APIClient
+extends Node
+
+
+signal response_received(response_body)
+signal request_failed(error)
+
+const RESULT_CANT_CONNECT = "Failed to connect"
+const RESULT_CANT_RESOLVE = "Failed to resolve"
+const RESULT_CONNECTION_ERROR = "Connection error"
+const RESULT_TIMEOUT = "Connection timeout"
+const RESULT_SERVER_ERROR = "Server error"
+const REQUEST_FAILED = "Error in the request"
+const REQUEST_TIMEOUT = "Request timed out on the client side"
+const URL_NOT_SET = "URL parameter is not set"
+const PARSE_FAILED = "Parsing failed"
+
+## Location of the API endpoint.
+@export var api_url : String
+## HTTP request method to use. Typically GET or POST.
+@export var request_method : HTTPClient.Method = HTTPClient.METHOD_POST
+@export_group("Advanced")
+## Location of an API key file, if authorization is required by the endpoint.
+@export_file("*.txt") var api_key_file : String
+## Time in seconds before the request fails due to timeout.
+@export var request_timeout : float = 0.0
+@export var _send_request_action : bool = false :
+ set(value):
+ if value and Engine.is_editor_hint():
+ request()
+# For Godot 4.4
+# @export_tool_button("Send Request") var _send_request_action = request
+
+
+@onready var _http_request : HTTPRequest = $HTTPRequest
+@onready var _timeout_timer : Timer= $TimeoutTimer
+
+## State flag for whether the connection has timed out on the client-side.
+var timed_out : bool = false
+
+func get_http_request() -> HTTPRequest:
+ return _http_request
+
+func get_api_key() -> String:
+ if api_key_file.is_empty():
+ return ""
+ var file := FileAccess.open(api_key_file, FileAccess.READ)
+ var error := FileAccess.get_open_error()
+ if error != OK:
+ push_error("API Key reading error: %d" % error)
+ return ""
+ var content = file.get_as_text()
+ file.close()
+ return content
+
+func get_api_url() -> String:
+ return api_url
+
+func get_api_method() -> int:
+ return request_method
+
+func mock_empty_body() -> String:
+ var form : Dictionary = {}
+ return JSON.stringify(form)
+
+func mock_request(body : String):
+ await(get_tree().create_timer(10.0).timeout)
+ _on_request_completed(HTTPRequest.RESULT_SUCCESS, "200", [], body)
+
+func request(body : String = "", request_headers : Array = []) -> void:
+ var local_http_request : HTTPRequest = get_http_request()
+ var key : String = get_api_key()
+ var url : String = get_api_url()
+ var method : int = get_api_method()
+ if url.is_empty():
+ request_failed.emit(URL_NOT_SET)
+ push_error(URL_NOT_SET)
+ return
+ request_headers.append("Content-Type: application/json")
+ if key:
+ request_headers.append("x-api-key: %s" % key)
+ if request_timeout > 0.0:
+ local_http_request.timeout = request_timeout
+ var error = local_http_request.request(url, request_headers, method, body)
+ if error != OK:
+ request_failed.emit(REQUEST_FAILED)
+ push_error("HTTP Request error: %d" % error)
+ return
+ if request_timeout > 0.0:
+ _timeout_timer.start(request_timeout + 1.0)
+
+func request_raw(data : PackedByteArray = [], request_headers : Array = []) -> void:
+ var local_http_request : HTTPRequest = get_http_request()
+ var key : String = get_api_key()
+ var url : String = get_api_url()
+ var method : int = get_api_method()
+ if url.is_empty():
+ request_failed.emit(URL_NOT_SET)
+ push_error(URL_NOT_SET)
+ return
+ request_headers.append("Content-Type: application/json")
+ if key:
+ request_headers.append("x-api-key: %s" % key)
+ if request_timeout > 0.0:
+ local_http_request.timeout = request_timeout
+ var error = local_http_request.request_raw(url, request_headers, method, data)
+ if error != OK:
+ request_failed.emit(REQUEST_FAILED)
+ push_error("HTTP Request error: %d" % error)
+ return
+ if request_timeout > 0.0:
+ _timeout_timer.start(request_timeout + 1.0)
+
+func _on_request_completed(result, response_code, headers, body) -> void:
+ # If already timed out on client-side, then return.
+ if timed_out: return
+ _timeout_timer.stop()
+ if result == HTTPRequest.RESULT_SUCCESS:
+ var body_string : String
+ if body is PackedByteArray:
+ body_string = body.get_string_from_utf8()
+ elif body is String:
+ body_string = body
+ var json := JSON.new()
+ var error = json.parse(body_string)
+ if error != OK:
+ request_failed.emit(PARSE_FAILED)
+ push_error("Parse error: %d" % error)
+ return
+ var parsed_data = json.data
+ response_received.emit(json.data)
+ else:
+ var error_message : String
+ match(result):
+ HTTPRequest.RESULT_CANT_CONNECT:
+ error_message = RESULT_CANT_CONNECT
+ HTTPRequest.RESULT_CANT_RESOLVE:
+ error_message = RESULT_CANT_RESOLVE
+ HTTPRequest.RESULT_CONNECTION_ERROR:
+ error_message = RESULT_CONNECTION_ERROR
+ HTTPRequest.RESULT_TIMEOUT:
+ error_message = RESULT_TIMEOUT
+ _:
+ error_message = RESULT_SERVER_ERROR
+ request_failed.emit(error_message)
+ push_error("HTTP Result error: %d" % result)
+
+func _on_http_request_request_completed(result, response_code, headers, body) -> void:
+ _on_request_completed(result, response_code, headers, body)
+
+func _on_timeout_timer_timeout() -> void:
+ timed_out = true
+ request_failed.emit(REQUEST_TIMEOUT)
+ push_warning(REQUEST_TIMEOUT)
diff --git a/addons/maaacks_game_template/base/scenes/utilities/api_client.gd.uid b/addons/maaacks_game_template/base/scenes/utilities/api_client.gd.uid
new file mode 100644
index 0000000..335a8e4
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/utilities/api_client.gd.uid
@@ -0,0 +1 @@
+uid://s0j82xowl675
diff --git a/addons/maaacks_game_template/base/scenes/utilities/api_client.tscn b/addons/maaacks_game_template/base/scenes/utilities/api_client.tscn
new file mode 100644
index 0000000..b085508
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/utilities/api_client.tscn
@@ -0,0 +1,13 @@
+[gd_scene load_steps=2 format=3 uid="uid://drhhakm62vjsy"]
+
+[ext_resource type="Script" uid="uid://s0j82xowl675" path="res://addons/maaacks_game_template/base/scenes/utilities/api_client.gd" id="1_c5ofg"]
+
+[node name="APIClient" type="Node"]
+script = ExtResource("1_c5ofg")
+
+[node name="HTTPRequest" type="HTTPRequest" parent="."]
+
+[node name="TimeoutTimer" type="Timer" parent="."]
+
+[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
+[connection signal="timeout" from="TimeoutTimer" to="." method="_on_timeout_timer_timeout"]
diff --git a/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.gd b/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.gd
new file mode 100644
index 0000000..e1c7fbe
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.gd
@@ -0,0 +1,285 @@
+@tool
+## Utility node for downloading and unzipping a file from a URL to an extraction destination.
+class_name DownloadAndExtract
+extends Node
+
+## Sent when the run has completed.
+signal run_completed
+## Sent when a response is received from the server.
+signal response_received(response_body)
+## Sent when the run has failed or exited early for any reason.
+signal run_failed(error : String)
+## Sent when the zip file has finished saving.
+signal zip_saved
+
+const TEMPORARY_ZIP_PATH = "res://temp.zip"
+const RESULT_CANT_CONNECT = "Failed to connect"
+const RESULT_CANT_RESOLVE = "Failed to resolve"
+const RESULT_CONNECTION_ERROR = "Connection error"
+const RESULT_TIMEOUT = "Connection timeout"
+const RESULT_SERVER_ERROR = "Server error"
+const REQUEST_FAILED = "Error in the request"
+const REQUEST_TIMEOUT = "Request timed out on the client side"
+const DOWNLOAD_IN_PROGRESS = "Download already in progress"
+const EXTRACT_IN_PROGRESS = "Extract already in progress"
+const DELETE_IN_PROGRESS = "Delete already in progress"
+const FAILED_TO_SAVE_ZIP_FILE = "Failed to save the zip file"
+const FAILED_TO_MAKE_EXTRACT_DIR = "Failed to make extract directory"
+const FAILED_TO_READ_ZIP_FILE = "Failed to read the zip file"
+const DOWNLOADED_ZIP_FILE_DOESNT_EXIST = "The downloaded ZIP file doesn't exist"
+const URL_NOT_SET = "URL parameter is not set"
+
+enum Stage{
+ NONE,
+ DOWNLOAD,
+ SAVE,
+ EXTRACT,
+ DELETE,
+}
+
+## Location of the zip file to be downloaded.
+@export var zip_url : String
+## Path where the zipped files are to be extracted.
+@export_dir var extract_path : String
+@export_group("Advanced")
+## If not empty, zipped file paths that do not contain a match to the string will be ignored.
+@export var path_match_string : String = ""
+## Assuming zip file contains a single base directory, the flag copies all of the contents,
+## as if they were at the base of the zip file. It never makes the base directory locally.
+@export var skip_base_zip_dir : bool = false
+## Forces a download and extraction even if the files already exist.
+@export var force : bool = false
+## Path where the zip file will be stored.
+@export var zip_file_path : String = TEMPORARY_ZIP_PATH
+## Flag to delete a downloaded zip file after the contents are extracted.
+@export var delete_zip_file : bool = true
+## Ratio of processing time that should be spent on extracting files.
+@export_range(0.0, 1.0) var process_time_ratio : float = 0.75
+## Seconds of delay added between saving the zip file and extracting it.
+@export_range(0.0, 3.0) var extraction_delay : float = 0.25
+## Duration to wait before the request times out.
+@export var request_timeout : float = 0.0
+@export var _start_run_action : bool = false :
+ set(value):
+ if value and Engine.is_editor_hint():
+ run()
+# For Godot 4.4
+# @export_tool_button("Download & Extract") var _start_run_action = run
+
+
+@onready var _http_request : HTTPRequest = $HTTPRequest
+@onready var _timeout_timer : Timer= $TimeoutTimer
+
+## State flag for whether the connection has timed out on the client-side.
+var timed_out : bool = false
+## Current stage of the download and extract process.
+var stage : Stage = Stage.NONE
+var zip_reader : ZIPReader = ZIPReader.new()
+var zipped_file_paths : PackedStringArray = []
+var extracted_file_paths : Array[String] = []
+var skipped_file_paths : Array[String] = []
+var downloaded_zip_file : bool = false
+var base_zip_path : String = ""
+var _save_progress : float = 0.0
+
+func get_http_request() -> HTTPRequest:
+ return _http_request
+
+func get_zip_url() -> String:
+ return zip_url
+
+func _zip_exists() -> bool:
+ return FileAccess.file_exists(zip_file_path)
+
+func get_request_method() -> int:
+ return HTTPClient.METHOD_GET
+
+## Sends the request to download the target zip file, and then extracts the contents.
+func run(request_headers : Array = []) -> void:
+ if stage == Stage.DOWNLOAD:
+ run_failed.emit(DOWNLOAD_IN_PROGRESS)
+ push_warning(DOWNLOAD_IN_PROGRESS)
+ return
+ if _zip_exists() and not force:
+ _extract_files.call_deferred()
+ return
+ var local_http_request : HTTPRequest = get_http_request()
+ var url : String = get_zip_url()
+ var method : int = get_request_method()
+ if url.is_empty():
+ run_failed.emit(URL_NOT_SET)
+ push_error(URL_NOT_SET)
+ return
+ if request_timeout > 0.0:
+ local_http_request.timeout = request_timeout
+ var error = local_http_request.request(url, request_headers, method)
+ if error != OK:
+ run_failed.emit(REQUEST_FAILED)
+ push_error("HTTP Request error: %d" % error)
+ return
+ if request_timeout > 0.0:
+ _timeout_timer.start(request_timeout + 1.0)
+ stage = Stage.DOWNLOAD
+
+func _delete_zip_file() -> void:
+ if not delete_zip_file or not downloaded_zip_file: return
+ if stage == Stage.DELETE:
+ run_failed.emit(DELETE_IN_PROGRESS)
+ push_warning(DELETE_IN_PROGRESS)
+ return
+ stage = Stage.DELETE
+ DirAccess.remove_absolute(zip_file_path)
+ downloaded_zip_file = false
+
+func _save_zip_file(body : PackedByteArray) -> void:
+ stage = Stage.SAVE
+ var file = FileAccess.open(zip_file_path, FileAccess.WRITE)
+ if not file:
+ run_failed.emit(FAILED_TO_SAVE_ZIP_FILE)
+ push_error(FAILED_TO_SAVE_ZIP_FILE)
+ return
+ file.store_buffer(body)
+ file.close()
+ downloaded_zip_file = true
+ zip_saved.emit()
+
+func extract_path_exists() -> bool:
+ return DirAccess.dir_exists_absolute(extract_path)
+
+func _make_extract_path() -> void:
+ var err := DirAccess.make_dir_recursive_absolute(extract_path)
+ if err != OK:
+ run_failed.emit(FAILED_TO_MAKE_EXTRACT_DIR)
+ push_error(FAILED_TO_MAKE_EXTRACT_DIR)
+
+func _extract_files() -> void:
+ if stage == Stage.EXTRACT:
+ run_failed.emit(EXTRACT_IN_PROGRESS)
+ push_warning(EXTRACT_IN_PROGRESS)
+ return
+ stage = Stage.EXTRACT
+ if not _zip_exists():
+ run_failed.emit(DOWNLOADED_ZIP_FILE_DOESNT_EXIST)
+ push_error(DOWNLOADED_ZIP_FILE_DOESNT_EXIST)
+ return
+ if not extract_path_exists(): _make_extract_path()
+ var error = zip_reader.open(zip_file_path)
+ if error != OK:
+ run_failed.emit(FAILED_TO_READ_ZIP_FILE)
+ push_error("ZIP Reader error: %d" % error)
+ return
+ zipped_file_paths = zip_reader.get_files()
+ if skip_base_zip_dir:
+ base_zip_path = zipped_file_paths[0]
+ if not base_zip_path.ends_with("/"):
+ push_warning("Skipping extracting base path, but it is not a directory.")
+ zipped_file_paths.remove_at(0)
+
+func _on_request_completed(result, response_code, headers, body) -> void:
+ # If already timed out on client-side, then return.
+ if timed_out: return
+ _timeout_timer.stop()
+ if _zip_exists(): _delete_zip_file()
+ if result == HTTPRequest.RESULT_SUCCESS:
+ if body is PackedByteArray:
+ response_received.emit(body)
+ _save_zip_file(body)
+ _save_progress = 0.0
+ var tween = create_tween()
+ tween.tween_property(self, "_save_progress", 1.0, extraction_delay)
+ await tween.finished
+ _extract_files.call_deferred()
+ else:
+ var error_message : String
+ match(result):
+ HTTPRequest.RESULT_CANT_CONNECT:
+ error_message = RESULT_CANT_CONNECT
+ HTTPRequest.RESULT_CANT_RESOLVE:
+ error_message = RESULT_CANT_RESOLVE
+ HTTPRequest.RESULT_CONNECTION_ERROR:
+ error_message = RESULT_CONNECTION_ERROR
+ HTTPRequest.RESULT_TIMEOUT:
+ error_message = RESULT_TIMEOUT
+ _:
+ error_message = RESULT_SERVER_ERROR
+ run_failed.emit(error_message)
+ push_error("HTTP Result error: %d" % result)
+
+func _on_http_request_request_completed(result, response_code, headers, body) -> void:
+ _on_request_completed(result, response_code, headers, body)
+
+func _on_timeout_timer_timeout() -> void:
+ timed_out = true
+ run_failed.emit(REQUEST_TIMEOUT)
+ push_warning(REQUEST_TIMEOUT)
+
+func get_progress() -> float:
+ if stage == Stage.DOWNLOAD:
+ return get_download_progress()
+ elif stage == Stage.SAVE:
+ return get_save_progress()
+ elif stage == Stage.EXTRACT:
+ return get_extraction_progress()
+ return 0.0
+
+func get_save_progress() -> float:
+ return _save_progress
+
+func get_extraction_progress() -> float:
+ if zipped_file_paths.size() == 0:
+ return 0.0
+ return float(extracted_file_paths.size()) / float(zipped_file_paths.size())
+
+func get_download_progress() -> float:
+ var body_size := _http_request.get_body_size()
+ if body_size < 1: return 0.0
+ return float(_http_request.get_downloaded_bytes()) / float(body_size)
+
+func _zipped_files_remaining() -> int:
+ return zipped_file_paths.size() - (extracted_file_paths.size() + skipped_file_paths.size())
+
+func _extract_next_zipped_file() -> void:
+ var path_index = extracted_file_paths.size() + skipped_file_paths.size()
+ var zipped_file_path := zipped_file_paths.get(path_index)
+ if path_match_string and not zipped_file_path.contains(path_match_string):
+ skipped_file_paths.append(zipped_file_path)
+ return
+ var extract_path_dir := extract_path
+ if not extract_path_dir.ends_with("/"):
+ extract_path_dir += "/"
+ var full_path := extract_path_dir
+ if skip_base_zip_dir:
+ full_path += zipped_file_path.replace(base_zip_path, "")
+ else:
+ full_path += zipped_file_path
+ if full_path.ends_with("/"):
+ if not DirAccess.dir_exists_absolute(full_path):
+ DirAccess.make_dir_recursive_absolute(full_path)
+ else:
+ if not FileAccess.file_exists(full_path) or force:
+ var file_access := FileAccess.open(full_path, FileAccess.WRITE)
+ if file_access == null:
+ skipped_file_paths.append(zipped_file_path)
+ push_error("Failed to open file: %s" % full_path)
+ return
+ var file_contents = zip_reader.read_file(zipped_file_path)
+ file_access.store_buffer(file_contents)
+ file_access.close()
+ extracted_file_paths.append(full_path)
+
+func _finish_extraction() -> void:
+ zip_reader.close()
+ _delete_zip_file()
+ stage = Stage.NONE
+ run_completed.emit()
+
+func _process(delta : float) -> void:
+ if stage == Stage.EXTRACT:
+ var frame_start_time : float = Time.get_unix_time_from_system()
+ var frame_time : float = 0.0
+ while (frame_time < delta * process_time_ratio):
+ if _zipped_files_remaining() == 0:
+ _finish_extraction()
+ break
+ _extract_next_zipped_file()
+ frame_time = Time.get_unix_time_from_system() - frame_start_time
diff --git a/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.gd.uid b/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.gd.uid
new file mode 100644
index 0000000..d6b5bda
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.gd.uid
@@ -0,0 +1 @@
+uid://bqu3bc0tttrfk
diff --git a/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.tscn b/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.tscn
new file mode 100644
index 0000000..f9e8638
--- /dev/null
+++ b/addons/maaacks_game_template/base/scenes/utilities/download_and_extract.tscn
@@ -0,0 +1,14 @@
+[gd_scene load_steps=2 format=3 uid="uid://dlkmofxhavh10"]
+
+[ext_resource type="Script" uid="uid://bqu3bc0tttrfk" path="res://addons/maaacks_game_template/base/scenes/utilities/download_and_extract.gd" id="1_1few7"]
+
+[node name="DownloadAndExtract" type="Node"]
+script = ExtResource("1_1few7")
+
+[node name="HTTPRequest" type="HTTPRequest" parent="."]
+
+[node name="TimeoutTimer" type="Timer" parent="."]
+one_shot = true
+
+[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
+[connection signal="timeout" from="TimeoutTimer" to="." method="_on_timeout_timer_timeout"]
diff --git a/addons/maaacks_game_template/base/scripts/app_settings.gd b/addons/maaacks_game_template/base/scripts/app_settings.gd
new file mode 100644
index 0000000..4795711
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/app_settings.gd
@@ -0,0 +1,176 @@
+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() -> void:
+ Config.erase_section(INPUT_SECTION)
+
+static func remove_action_input_event(action_name : String, input_event : InputEvent) -> void:
+ 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) -> void:
+ 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_") or action_name.begins_with("spatial_editor"))
+ 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() -> void:
+ for bus_iter in AudioServer.bus_count:
+ var bus_key : String = get_audio_bus_name(bus_iter).to_pascal_case()
+ var bus_volume : float = get_bus_volume(bus_iter)
+ initial_bus_volumes.append(bus_volume)
+ bus_volume = Config.get_config(AUDIO_SECTION, bus_key, bus_volume)
+ if is_nan(bus_volume):
+ bus_volume = 1.0
+ Config.set_config(AUDIO_SECTION, bus_key, 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, update_config : bool = true) -> void:
+ if value.x == 0 or value.y == 0:
+ return
+ window.size = value
+ if update_config:
+ Config.set_config(VIDEO_SECTION, SCREEN_RESOLUTION, 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
+ return Config.get_config(VIDEO_SECTION, SCREEN_RESOLUTION, current_resolution)
+
+static func _on_window_size_changed(window: Window) -> void:
+ Config.set_config(VIDEO_SECTION, SCREEN_RESOLUTION, window.size)
+
+static func set_video_from_config(window : Window) -> void:
+ window.size_changed.connect(_on_window_size_changed.bind(window))
+ 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)
+
+static func set_vsync(vsync_mode : DisplayServer.VSyncMode, window : Window = null) -> void:
+ var window_id : int = 0
+ if window:
+ window_id = window.get_window_id()
+ DisplayServer.window_set_vsync_mode(vsync_mode, window_id)
+
+static func get_vsync(window : Window = null) -> DisplayServer.VSyncMode:
+ var window_id : int = 0
+ if window:
+ window_id = window.get_window_id()
+ var vsync_mode = DisplayServer.window_get_vsync_mode(window_id)
+ return vsync_mode
+
+# 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)
diff --git a/addons/maaacks_game_template/base/scripts/app_settings.gd.uid b/addons/maaacks_game_template/base/scripts/app_settings.gd.uid
new file mode 100644
index 0000000..ed60455
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/app_settings.gd.uid
@@ -0,0 +1 @@
+uid://dwflyh7g2rjxt
diff --git a/addons/maaacks_game_template/base/scripts/capture_focus.gd b/addons/maaacks_game_template/base/scripts/capture_focus.gd
new file mode 100644
index 0000000..915bd25
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/capture_focus.gd
@@ -0,0 +1,65 @@
+class_name CaptureFocus
+extends Control
+## Node that captures UI focus for games with a hidden mouse or joypad enabled.
+##
+## This script assists with capturing UI focus when
+## opening, closing, or switching between menus.
+## When attached to a node, it will check if it was changed to visible
+## and if it should grab focus. 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 enabled : bool = false
+@export var null_focus_enabled : bool = true
+@export var joypad_enabled : bool = true
+@export var mouse_hidden_enabled : bool = true
+
+## Locks focus
+@export var lock : bool = false :
+ set(value):
+ var value_changed : bool = lock != value
+ lock = value
+ if value_changed and not lock:
+ update_focus()
+
+func _focus_first_search(control_node : Control, levels : int = 1) -> bool:
+ if control_node == null or !control_node.is_visible_in_tree():
+ return false
+ if control_node.focus_mode == FOCUS_ALL:
+ control_node.grab_focus()
+ if control_node is ItemList:
+ control_node.select(0)
+ return true
+ if levels < 1:
+ return false
+ var children = control_node.get_children()
+ for child in children:
+ if _focus_first_search(child, levels - 1):
+ return true
+ return false
+
+func focus_first() -> void:
+ _focus_first_search(self, search_depth)
+
+func update_focus() -> void:
+ if lock : return
+ if _is_visible_and_should_capture():
+ focus_first()
+
+func _should_capture_focus() -> bool:
+ return enabled or \
+ (get_viewport().gui_get_focus_owner() == null and null_focus_enabled) or \
+ (Input.get_connected_joypads().size() > 0 and joypad_enabled) or \
+ (Input.mouse_mode not in [Input.MOUSE_MODE_VISIBLE, Input.MOUSE_MODE_CONFINED] and mouse_hidden_enabled)
+
+func _is_visible_and_should_capture() -> bool:
+ return is_visible_in_tree() and _should_capture_focus()
+
+func _on_visibility_changed() -> void:
+ call_deferred("update_focus")
+
+func _ready() -> void:
+ if is_inside_tree():
+ update_focus()
+ connect("visibility_changed", _on_visibility_changed)
diff --git a/addons/maaacks_game_template/base/scripts/capture_focus.gd.uid b/addons/maaacks_game_template/base/scripts/capture_focus.gd.uid
new file mode 100644
index 0000000..b0eccab
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/capture_focus.gd.uid
@@ -0,0 +1 @@
+uid://1nf36h0gms3q
diff --git a/addons/maaacks_game_template/base/scripts/config.gd b/addons/maaacks_game_template/base/scripts/config.gd
new file mode 100644
index 0000000..f1dbd0d
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/config.gd
@@ -0,0 +1,59 @@
+class_name Config
+extends Object
+
+## Interface for a single configuration file through [ConfigFile].
+
+const CONFIG_FILE_LOCATION := "user://config.cfg"
+
+static var config_file : ConfigFile
+
+static func _init() -> void:
+ load_config_file()
+
+static func _save_config_file() -> void:
+ var save_error : int = config_file.save(CONFIG_FILE_LOCATION)
+ if save_error:
+ push_error("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 save_error : int = config_file.save(CONFIG_FILE_LOCATION)
+ if save_error:
+ push_error("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) -> bool:
+ load_config_file()
+ return config_file.has_section(section)
+
+static func has_section_key(section: String, key: String) -> bool:
+ load_config_file()
+ return config_file.has_section_key(section, key)
+
+static func erase_section(section: String) -> void:
+ if has_section(section):
+ config_file.erase_section(section)
+ _save_config_file()
+
+static func erase_section_key(section: String, key: String) -> void:
+ if has_section_key(section, key):
+ config_file.erase_section_key(section, key)
+ _save_config_file()
+
+static func get_section_keys(section: String) -> PackedStringArray:
+ load_config_file()
+ if config_file.has_section(section):
+ return config_file.get_section_keys(section)
+ return PackedStringArray()
diff --git a/addons/maaacks_game_template/base/scripts/config.gd.uid b/addons/maaacks_game_template/base/scripts/config.gd.uid
new file mode 100644
index 0000000..37212e7
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/config.gd.uid
@@ -0,0 +1 @@
+uid://dxjk8pgi7yhtq
diff --git a/addons/maaacks_game_template/base/scripts/file_lister.gd b/addons/maaacks_game_template/base/scripts/file_lister.gd
new file mode 100644
index 0000000..cc12090
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/file_lister.gd
@@ -0,0 +1,51 @@
+@tool
+extends Node
+class_name FileLister
+## Helper class for listing all the scenes in a directory.
+
+## List of paths to scene files.
+@export var _refresh_files_action : bool = false :
+ set(value):
+ if value and Engine.is_editor_hint():
+ _refresh_files()
+# For Godot 4.4
+# @export_tool_button("Refresh Files") var _refresh_files_action = _refresh_files
+## Filled in the editor by selecting a directory.
+@export var files : Array[String]
+## Fills files with those discovered in directories, and matching constraints.
+@export_dir var directories : Array[String] :
+ set(value):
+ directories = value
+ _refresh_files()
+
+@export_group("Constraints")
+@export var search : String
+@export var filter : String
+
+@export_subgroup("Advanced Search")
+@export var begins_with : String
+@export var ends_with : String
+@export var not_begins_with : String
+@export var not_ends_with : String
+
+
+func _refresh_files():
+ if not is_inside_tree(): return
+ files.clear()
+ for directory in directories:
+ var dir_access = DirAccess.open(directory)
+ if dir_access:
+ for file in dir_access.get_files():
+ if (not search.is_empty()) and (not file.contains(search)):
+ continue
+ if (not filter.is_empty()) and (file.contains(filter)):
+ continue
+ if (not begins_with.is_empty()) and (not file.begins_with(begins_with)):
+ continue
+ if (not ends_with.is_empty()) and (not file.ends_with(ends_with)):
+ continue
+ if (not not_begins_with.is_empty()) and (file.begins_with(not_begins_with)):
+ continue
+ if (not not_ends_with.is_empty()) and (file.ends_with(not_ends_with)):
+ continue
+ files.append(directory + "/" + file)
diff --git a/addons/maaacks_game_template/base/scripts/file_lister.gd.uid b/addons/maaacks_game_template/base/scripts/file_lister.gd.uid
new file mode 100644
index 0000000..ae15737
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/file_lister.gd.uid
@@ -0,0 +1 @@
+uid://bij7wsh8d44gv
diff --git a/addons/maaacks_game_template/base/scripts/global_state.gd b/addons/maaacks_game_template/base/scripts/global_state.gd
new file mode 100644
index 0000000..e255bea
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/global_state.gd
@@ -0,0 +1,50 @@
+class_name GlobalState
+extends Node
+
+const SAVE_STATE_PATH = "user://global_state.tres"
+const NO_VERSION_NAME = "0.0.0"
+
+static var current : GlobalStateData
+static var current_version : String
+
+static func _log_opened() -> void:
+ if current is GlobalStateData:
+ current.last_unix_time_opened = int(Time.get_unix_time_from_system())
+
+static func _log_version() -> void:
+ if current is GlobalStateData:
+ current_version = ProjectSettings.get_setting("application/config/version", NO_VERSION_NAME)
+ if current_version.is_empty():
+ current_version = NO_VERSION_NAME
+ if not current.first_version_opened:
+ current.first_version_opened = current_version
+ current.last_version_opened = current_version
+
+static func _load_current_state() -> void:
+ if FileAccess.file_exists(SAVE_STATE_PATH):
+ current = ResourceLoader.load(SAVE_STATE_PATH)
+ if not current:
+ current = GlobalStateData.new()
+
+static func open() -> void:
+ _load_current_state()
+ _log_opened()
+ _log_version()
+ save()
+
+static func save() -> void:
+ if current is GlobalStateData:
+ ResourceSaver.save(current, SAVE_STATE_PATH)
+
+static func has_state(state_key : String) -> bool:
+ if current is not GlobalStateData: return false
+ return current.has_state(state_key)
+
+static func get_state(state_key : String, state_type_path : String) -> Resource:
+ if current is not GlobalStateData: return
+ return current.get_state(state_key, state_type_path)
+
+static func reset() -> void:
+ if current is not GlobalStateData: return
+ current.states.clear()
+ save()
diff --git a/addons/maaacks_game_template/base/scripts/global_state.gd.uid b/addons/maaacks_game_template/base/scripts/global_state.gd.uid
new file mode 100644
index 0000000..6c27059
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/global_state.gd.uid
@@ -0,0 +1 @@
+uid://34ojrqt1klav
diff --git a/addons/maaacks_game_template/base/scripts/global_state_data.gd b/addons/maaacks_game_template/base/scripts/global_state_data.gd
new file mode 100644
index 0000000..624002a
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/global_state_data.gd
@@ -0,0 +1,24 @@
+class_name GlobalStateData
+extends Resource
+
+@export var first_version_opened : String
+@export var last_version_opened : String
+@export var last_unix_time_opened : int
+@export var states : Dictionary
+
+func get_state(key_name : String, state_type_path : String) -> Resource:
+ var new_state : Resource
+ var new_state_script = load(state_type_path)
+ if new_state_script is GDScript:
+ new_state = new_state_script.new()
+ if key_name in states:
+ var saved_state : Resource = states[key_name]
+ var saved_script = saved_state.get_script()
+ var new_script = new_state.get_script()
+ if saved_script and new_script and saved_script == new_script:
+ return saved_state
+ states[key_name] = new_state
+ return new_state
+
+func has_state(key_name : String) -> bool:
+ return key_name in states
diff --git a/addons/maaacks_game_template/base/scripts/global_state_data.gd.uid b/addons/maaacks_game_template/base/scripts/global_state_data.gd.uid
new file mode 100644
index 0000000..533759a
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/global_state_data.gd.uid
@@ -0,0 +1 @@
+uid://bb3tb71vb6p8w
diff --git a/addons/maaacks_game_template/base/scripts/input_helper.gd b/addons/maaacks_game_template/base/scripts/input_helper.gd
new file mode 100644
index 0000000..affab91
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/input_helper.gd
@@ -0,0 +1,175 @@
+class_name InputEventHelper
+extends Node
+## Helper class for organizing constants related to [InputEvent].
+
+const DEVICE_KEYBOARD = "Keyboard"
+const DEVICE_MOUSE = "Mouse"
+const DEVICE_XBOX_CONTROLLER = "Xbox"
+const DEVICE_SWITCH_CONTROLLER = "Switch"
+const DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER = "Switch Left Joycon"
+const DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER = "Switch Right Joycon"
+const DEVICE_SWITCH_JOYCON_COMBINED_CONTROLLER = "Switch Combined Joycons"
+const DEVICE_PLAYSTATION_CONTROLLER = "Playstation"
+const DEVICE_STEAMDECK_CONTROLLER = "Steamdeck"
+const DEVICE_GENERIC = "Generic"
+
+const JOYSTICK_LEFT_NAME = "Left Stick"
+const JOYSTICK_RIGHT_NAME = "Right Stick"
+const D_PAD_NAME = "Dpad"
+
+const MOUSE_BUTTONS : Array = ["None", "Left", "Right", "Middle", "Scroll Up", "Scroll Down", "Wheel Left", "Wheel Right"]
+
+const JOYPAD_BUTTON_NAME_MAP : Dictionary = {
+ DEVICE_GENERIC : ["Trigger A", "Trigger B", "Trigger C", "", "", "", "", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right"],
+ DEVICE_XBOX_CONTROLLER : ["A", "B", "X", "Y", "View", "Home", "Menu", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Share"],
+ DEVICE_SWITCH_CONTROLLER : ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Capture"],
+ DEVICE_PLAYSTATION_CONTROLLER : ["Cross", "Circle", "Square", "Triangle", "Select", "PS", "Options", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Microphone"],
+ DEVICE_STEAMDECK_CONTROLLER : ["A", "B", "X", "Y", "View", "", "Options", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right"]
+} # Dictionary[String, Array]
+
+const SDL_DEVICE_NAMES: Dictionary = {
+ DEVICE_XBOX_CONTROLLER: ["XInput", "XBox"],
+ DEVICE_PLAYSTATION_CONTROLLER: ["Sony", "PS5", "PS4", "Nacon"],
+ DEVICE_STEAMDECK_CONTROLLER: ["Steam"],
+ DEVICE_SWITCH_CONTROLLER: ["Switch"],
+ DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER: ["Joy-Con (L)", "Left Joy-Con"],
+ DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER: ["Joy-Con (R)", "Right Joy-Con"],
+ DEVICE_SWITCH_JOYCON_COMBINED_CONTROLLER: ["Joy-Con (L/R)", "Combined Joy-Cons"],
+}
+
+const JOY_BUTTON_NAMES : Dictionary = {
+ JOY_BUTTON_A: "Button A",
+ JOY_BUTTON_B: "Button B",
+ JOY_BUTTON_X: "Button X",
+ JOY_BUTTON_Y: "Button Y",
+ JOY_BUTTON_LEFT_SHOULDER: "Left Shoulder",
+ JOY_BUTTON_RIGHT_SHOULDER: "Right Shoulder",
+ JOY_BUTTON_LEFT_STICK: "Left Stick",
+ JOY_BUTTON_RIGHT_STICK: "Right Stick",
+ JOY_BUTTON_START : "Button Start",
+ JOY_BUTTON_GUIDE : "Button Guide",
+ JOY_BUTTON_BACK : "Button Back",
+ 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",
+ JOY_BUTTON_MISC1 : "Misc",
+}
+
+const JOYPAD_DPAD_NAMES : Dictionary = {
+ 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",
+ JOY_AXIS_TRIGGER_RIGHT: "Right Trigger",
+}
+
+const 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",
+}
+
+static func has_joypad() -> bool:
+ return Input.get_connected_joypads().size() > 0
+
+static func is_joypad_event(event: InputEvent) -> bool:
+ return event is InputEventJoypadButton or event is InputEventJoypadMotion
+
+static func is_mouse_event(event: InputEvent) -> bool:
+ return event is InputEventMouseButton or event is InputEventMouseMotion
+
+static func get_device_name_by_id(device_id : int) -> String:
+ if device_id >= 0:
+ var device_name = Input.get_joy_name(device_id)
+ for device_key in SDL_DEVICE_NAMES:
+ for keyword in SDL_DEVICE_NAMES[device_key]:
+ if device_name.containsn(keyword):
+ return device_key
+ return DEVICE_GENERIC
+
+static func get_device_name(event: InputEvent) -> String:
+ if event is InputEventJoypadButton or event is InputEventJoypadMotion:
+ if event.device == -1:
+ return DEVICE_GENERIC
+ var device_id = event.device
+ return get_device_name_by_id(device_id)
+ return DEVICE_GENERIC
+
+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 == null:
+ return ""
+ 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()
+
+static func get_device_specific_text(event : InputEvent, device_name : String = "") -> String:
+ if device_name.is_empty():
+ device_name = get_device_name(event)
+ if event is InputEventJoypadButton:
+ var joypad_button : String = ""
+ if event.button_index in JOYPAD_DPAD_NAMES:
+ joypad_button = JOYPAD_DPAD_NAMES[event.button_index]
+ elif event.button_index < JOYPAD_BUTTON_NAME_MAP[device_name].size():
+ joypad_button = JOYPAD_BUTTON_NAME_MAP[device_name][event.button_index]
+ return "%s %s" % [device_name, joypad_button]
+ if event is InputEventJoypadMotion:
+ return "%s %s" % [device_name, get_text(event)]
+ if event is InputEventMouseButton:
+ if event.button_index < MOUSE_BUTTONS.size():
+ var mouse_button : String = MOUSE_BUTTONS[event.button_index]
+ return "%s %s" % [DEVICE_MOUSE, mouse_button]
+ return get_text(event).capitalize()
diff --git a/addons/maaacks_game_template/base/scripts/input_helper.gd.uid b/addons/maaacks_game_template/base/scripts/input_helper.gd.uid
new file mode 100644
index 0000000..7dbb6ea
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/input_helper.gd.uid
@@ -0,0 +1 @@
+uid://6xujceamar4h
diff --git a/addons/maaacks_game_template/base/scripts/music_controller.gd b/addons/maaacks_game_template/base/scripts/music_controller.gd
new file mode 100644
index 0000000..5aeef59
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/music_controller.gd
@@ -0,0 +1,184 @@
+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) -> Tween:
+ if is_zero_approx(duration): return
+ 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) -> void:
+ AudioServer.set_bus_volume_db(blend_audio_bus_idx, sub_volume_db)
+
+func fade_in(duration : float = 0.0) -> Tween:
+ if is_zero_approx(duration): return
+ 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) -> Tween:
+ 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
+ return
+
+func stop() -> void:
+ if not is_instance_valid(music_stream_player):
+ return
+ music_stream_player.stop()
+
+func play(playback_position : float = 0.0) -> void:
+ if not is_instance_valid(music_stream_player):
+ 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() -> void:
+ if not is_instance_valid(music_stream_player):
+ 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() -> void:
+ play()
+ fade_in( fade_in_duration )
+
+func _is_matching_stream(stream_player : AudioStreamPlayer) -> bool:
+ if stream_player.bus != audio_bus:
+ return false
+ if not is_instance_valid(music_stream_player):
+ return false
+ return music_stream_player.stream == stream_player.stream
+
+func _connect_stream_on_tree_exiting(stream_player : AudioStreamPlayer) -> void:
+ 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) -> void:
+ 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) -> void:
+ 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) -> void:
+ 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) -> void:
+ 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) -> void:
+ 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() -> void:
+ 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)
diff --git a/addons/maaacks_game_template/base/scripts/music_controller.gd.uid b/addons/maaacks_game_template/base/scripts/music_controller.gd.uid
new file mode 100644
index 0000000..ef37f81
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/music_controller.gd.uid
@@ -0,0 +1 @@
+uid://ctrh4qyxqncss
diff --git a/addons/maaacks_game_template/base/scripts/pause_menu_controller.gd b/addons/maaacks_game_template/base/scripts/pause_menu_controller.gd
new file mode 100644
index 0000000..cd61315
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/pause_menu_controller.gd
@@ -0,0 +1,18 @@
+class_name PauseMenuController
+extends Node
+
+## Node for opening a pause menu when detecting a 'ui_cancel' event.
+
+@export var pause_menu_packed : PackedScene
+@export var focused_viewport : Viewport
+
+func _unhandled_input(event : InputEvent) -> void:
+ if event.is_action_pressed("ui_cancel"):
+ if not focused_viewport:
+ focused_viewport = get_viewport()
+ var _initial_focus_control = focused_viewport.gui_get_focus_owner()
+ var current_menu = pause_menu_packed.instantiate()
+ get_tree().current_scene.call_deferred("add_child", current_menu)
+ await current_menu.tree_exited
+ if is_inside_tree() and _initial_focus_control:
+ _initial_focus_control.grab_focus()
diff --git a/addons/maaacks_game_template/base/scripts/pause_menu_controller.gd.uid b/addons/maaacks_game_template/base/scripts/pause_menu_controller.gd.uid
new file mode 100644
index 0000000..b1138e6
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/pause_menu_controller.gd.uid
@@ -0,0 +1 @@
+uid://cyh0d64pfygbl
diff --git a/addons/maaacks_game_template/base/scripts/ui_sound_controller.gd b/addons/maaacks_game_template/base/scripts/ui_sound_controller.gd
new file mode 100644
index 0000000..2020c62
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/ui_sound_controller.gd
@@ -0,0 +1,207 @@
+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
+
+@export_group("ItemList Sounds")
+@export var item_list_selected : AudioStream
+@export var item_list_activated : AudioStream
+
+@export_group("Tree Sounds")
+@export var tree_item_selected : AudioStream
+@export var tree_item_activated : AudioStream
+@export var tree_button_clicked : 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
+
+var item_list_activated_player : AudioStreamPlayer
+var item_list_selected_player : AudioStreamPlayer
+
+var tree_item_activated_player : AudioStreamPlayer
+var tree_item_selected_player : AudioStreamPlayer
+var tree_button_clicked_player : AudioStreamPlayer
+
+func _update_persistent_signals() -> void:
+ 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 = "") -> AudioStreamPlayer:
+ 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() -> void:
+ 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() -> void:
+ 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() -> void:
+ 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() -> void:
+ 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_item_list_stream_players() -> void:
+ item_list_activated_player = _build_stream_player(item_list_activated, "ItemActivated")
+ item_list_selected_player = _build_stream_player(item_list_selected, "ItemSelected")
+
+func _build_tree_stream_players() -> void:
+ tree_item_activated_player = _build_stream_player(tree_item_activated, "TreeItemActivated")
+ tree_item_selected_player = _build_stream_player(tree_item_selected, "TreeItemSelected")
+ tree_button_clicked_player = _build_stream_player(tree_button_clicked, "TreeButtonClicked")
+
+func _build_all_stream_players() -> void:
+ _build_button_stream_players()
+ _build_tab_stream_players()
+ _build_slider_stream_players()
+ _build_line_stream_players()
+ _build_item_list_stream_players()
+ _build_tree_stream_players()
+
+func _play_stream(stream_player : AudioStreamPlayer) -> void:
+ if not stream_player.is_inside_tree():
+ return
+ stream_player.play()
+
+func _tab_event_play_stream(_tab_idx : int, stream_player : AudioStreamPlayer) -> void:
+ _play_stream(stream_player)
+
+func _slider_drag_ended_play_stream(_value_changed : bool, stream_player : AudioStreamPlayer) -> void:
+ _play_stream(stream_player)
+
+func _line_event_play_stream(_new_text : String, stream_player : AudioStreamPlayer) -> void:
+ _play_stream(stream_player)
+
+func _item_list_play_stream(_index : int, stream_player : AudioStreamPlayer) -> void:
+ _play_stream(stream_player)
+
+func _tree_button_clicked_play_stream(_tree_item : TreeItem, _column : int, _id : int, _mouse_button_index : int, stream_player : AudioStreamPlayer) -> void:
+ _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)
+ elif node is ItemList:
+ _connect_stream_player(node, item_list_activated_player, &"item_activated", _item_list_play_stream)
+ _connect_stream_player(node, item_list_selected_player, &"item_selected", _item_list_play_stream)
+ elif node is Tree:
+ _connect_stream_player(node, tree_item_activated_player, &"item_activated", _play_stream)
+ _connect_stream_player(node, tree_item_selected_player, &"item_selected", _play_stream)
+ _connect_stream_player(node, tree_button_clicked_player, &"button_clicked", _tree_button_clicked_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() -> void:
+ var tree_node = get_tree()
+ if tree_node.node_added.is_connected(connect_ui_sounds):
+ tree_node.node_added.disconnect(connect_ui_sounds)
diff --git a/addons/maaacks_game_template/base/scripts/ui_sound_controller.gd.uid b/addons/maaacks_game_template/base/scripts/ui_sound_controller.gd.uid
new file mode 100644
index 0000000..cb78585
--- /dev/null
+++ b/addons/maaacks_game_template/base/scripts/ui_sound_controller.gd.uid
@@ -0,0 +1 @@
+uid://b5oej1q4h7jvh
diff --git a/addons/maaacks_game_template/base/translations/menus_translations.csv b/addons/maaacks_game_template/base/translations/menus_translations.csv
new file mode 100644
index 0000000..a9da299
--- /dev/null
+++ b/addons/maaacks_game_template/base/translations/menus_translations.csv
@@ -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
diff --git a/addons/maaacks_game_template/base/translations/menus_translations.csv.import b/addons/maaacks_game_template/base/translations/menus_translations.csv.import
new file mode 100644
index 0000000..d9f735f
--- /dev/null
+++ b/addons/maaacks_game_template/base/translations/menus_translations.csv.import
@@ -0,0 +1,17 @@
+[remap]
+
+importer="csv_translation"
+type="Translation"
+uid="uid://i6ihop1vp2ei"
+
+[deps]
+
+files=["res://addons/maaacks_game_template/base/translations/menus_translations.en.translation", "res://addons/maaacks_game_template/base/translations/menus_translations.fr.translation"]
+
+source_file="res://addons/maaacks_game_template/base/translations/menus_translations.csv"
+dest_files=["res://addons/maaacks_game_template/base/translations/menus_translations.en.translation", "res://addons/maaacks_game_template/base/translations/menus_translations.fr.translation"]
+
+[params]
+
+compress=true
+delimiter=0
diff --git a/addons/maaacks_game_template/docs/ExistingProject.md b/addons/maaacks_game_template/docs/ExistingProject.md
new file mode 100644
index 0000000..d592491
--- /dev/null
+++ b/addons/maaacks_game_template/docs/ExistingProject.md
@@ -0,0 +1,116 @@
+# Existing Project
+
+These instructions assume starting with just the contents of `addons/`. This will be the case when installing the *plugin* version in the Godot Asset Library.
+
+
+1. Update the project’s name in the main menu.
+
+
+ 1. Open `main_menu_with_animations.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.
+
+
+2. Link the main menu to the game scene.
+
+
+ 1. Open `main_menu_with_animations.tscn`.
+ 2. Select the `MainMenu` node.
+ 3. Update `Game Scene Path` to the path of the project's game scene.
+ 4. Save the scene.
+
+
+3. 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_with_animations.tscn`.
+ 3. Select the `BackgroundMusicPlayer` node.
+ 4. Assign the music asset to the `stream` property.
+ 5. Make sure that the `bus` property is set to `Music`.
+ 6. Save the scene.
+ 7. Optionally, repeat steps 3-5 for background music nodes in:
+ 1. `opening_with_logo.tscn`
+ 2. `game_ui.tscn`
+ 3. `end_credits.tscn`
+
+
+ 3. Add sound effects to UI elements.
+
+ 1. By scene.
+
+
+ 1. Open `main_menu_with_animations.tscn` and `pause_menu.tscn`.
+ 2. Select the `UISoundController` node.
+ 3. Add audio streams to the various UI node events.
+ 4. Save the scenes.
+
+
+ 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.
+
+
+4. Add readable names for input actions to the controls menu.
+
+
+ 1. Open `input_options_menu.tscn`.
+ 2. In the scene tree, select the `Controls` node.
+ 3. In the node inspector, select the desired input remapping mode (defaults to `List`).
+ 4. In the scene tree, select `InputActionsList` or `InputActionsTree`, depending on the choice of input remapping. The other node should be hidden.
+ 5. In the node inspector, update the `Input Action Names` and corresponding `Readable Action Names` to show user-friendly names for the project's input actions.
+ 6. Save the scene.
+
+5. 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 5.3.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.
+
+
+6. 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.
+
+
+7. Continue with:
+
+ 1. [Setting up the Main Menu.](/addons/maaacks_game_template/docs/MainMenuSetup.md)
+ 2. [Adding icons to the Input Options.](/addons/maaacks_game_template/docs/InputIconMapping.md)
+ 3. [Setting up a Game Scene.](/addons/maaacks_game_template/docs/GameSceneSetup.md)
+ 4. [Utilizing Game Saving](/addons/maaacks_game_template/docs/GameSaving.md)
diff --git a/addons/maaacks_game_template/docs/GameSaving.md b/addons/maaacks_game_template/docs/GameSaving.md
new file mode 100644
index 0000000..18026c4
--- /dev/null
+++ b/addons/maaacks_game_template/docs/GameSaving.md
@@ -0,0 +1,32 @@
+# Game Saving
+
+> [!IMPORTANT]
+> The save system doesn't follow the same conventions as other systems.
+> It is subject to change.
+
+> [!WARNING]
+> The save system relies on resource files, which are vulnerable to having malicious scripts injected into them.
+> Please discourage players from sharing their save files. Do not use this for cloud saving, either.
+> A safer save system is planned.
+
+
+The templates and plugin suite aim to keep most class definitions within the addon. These are not usually expected change. Unlike the other classes, the `GameState` and `LevelState` are defined for the developer to edit to their needs.
+
+## Usage
+
+The `GlobalState` static class keeps the state saved to a resource. The developer is responsible for making sure `GlobalState.save()` gets called when they want the state saved to the disk.
+
+### Game State
+
+The `GameState` class represents the state of a single playthrough of the game. It currently stores the current level, the max level reached, and the state of each level currently visited.
+
+It is currently expected to be used as a singleton, too.
+
+
+### Level State
+
+The `LevelState` class represents the state of a single level in a playthrough of the game. It currently stores whether the tutorial has been read, and a color, if the player has set one in the example levels. It can be used to store the states of many other level specific features.
+
+From within the `_ready()` method of a level scene, call `GameState.get_level_state(scene_file_path)` to get the last saved `LevelState`, or a new one, and then set the state of the level from that. When a state of the level changes that is intended to be preserved, save it into the level state, and call `GlobalState.save()`.
+
+Examples are provided allowing the player to save the level background color, and keeping the tutorial message from popping up more than once per playthrough.
\ No newline at end of file
diff --git a/addons/maaacks_game_template/docs/GameSceneSetup.md b/addons/maaacks_game_template/docs/GameSceneSetup.md
new file mode 100644
index 0000000..a2d9a12
--- /dev/null
+++ b/addons/maaacks_game_template/docs/GameSceneSetup.md
@@ -0,0 +1,87 @@
+# Game Scene Setup
+
+When setting up a game scene, it is useful to refer to the `game_scene/game_ui.tscn` included in the examples.
+
+There are a few parts to setting up a basic game scene, as done in the `GameUI` example used in the template.
+
+## Pausing
+The `PauseMenuController` node can be added to the tree, or the `pause_menu_controller.gd` script may be attached to an empty `Node`. Selecting the node should then allow for setting the `pause_menu_packed` value in the inspector. Set it to the `pause_menu.tscn` scene and save.
+
+This should be enough to capture when the `ui-cancel` input action is pressed in-game. On keyboards, this is commonly the `Esc` key.
+
+## Level Loading
+Some level loading scripts are provided with the examples. They load levels in order from a list, or dynamically by file paths. Levels can be added to the `LevelListLoader` by either selecting a directory to automatically read scene files from, or populating the files array manually.
+
+A `LevelListLoader` must be provided with a `level_container` in the scene. Levels will get added to and removed from this node. The example uses the `SubViewport`, but any leaf node (ie. node without children) in the scene should work.
+
+The level loader is called from a `LevelListManager` with `advance_and_load_level()`. An additional loading screen in the scene can show progress of loading levels, and is toggled by the `LevelListManager` with `reset()`.
+
+### Games without levels
+Level Loading is not required if the entire game takes place in one scene.
+
+In that case, the following nodes can be safely removed:
+* LevelListLoader
+* LevelLoadingScreen
+* LevelListManager
+
+The single level scene can then be added directly to the `SubViewport` or the root node.
+
+## Background Music
+`BackgroundMusicPlayer`'s are `AudioStreamPlayer`'s with `autoplay` set to `true` and `audio_bus` set to "Music". These will automatically be recognized by the `ProjectMusicController` with the default settings, and allow for blending between tracks.
+
+A `BackgroundMusicPlayer` can be added to the main game scene, but if using levels, the level scenes are typically a better place for them, as that allows for tracks to vary by level.
+
+## SubViewports
+The game example has the levels loaded into a `SubViewport` node, contained within a `SubViewportContainer`. This has a couple of advantages.
+
+- Separates elements intended to appear inside the game world from those intended to appear on a layer above it.
+- Allows setting a fixed resolution for the game, like pixel art games.
+- Allows setting rendering settings, like anti-aliasing, on the `SubViewport`.
+- Supports easily adding visual effects with shaders on the `SubViewportContainer`.
+- Visual effects can be added to the game world without hurting the readability of the UI.
+
+It has some disadvantages, as well.
+
+- Locks the viewport resolution if any scaling is enabled, which is not ideal for 3D games.
+- Requires enabling Audio Listeners to hear audio from the game world.
+- Extra processing overhead for the viewport layer.
+
+If a subviewport does not work well for the game, use any empty `Node` as the game world or level container, instead.
+
+### Pixel Art Games
+If working with a pixel art game, often the goal is that the number of art pixels on-screen is to remain the same regardless of screen resolution. As in, the art scales with the monitor, rather than bigger monitors showing more of a scene. This is done by setting the viewport size in the project settings, and setting the stretch mode to either `canvas_mode` or `viewport`.
+
+If a higher resolution is desired for the menus and UI than the game, then the project viewport size should be set to a multiple of the desired game window size. Then set the stretch shrink in `SubViewportContainer` to the multiple of the resolution. For example, if the game is at `640x360`, then the project viewport size can be set to `1280x720`, and the stretch shrink set to `2` (`1280x720 / 2 = 640x360`). Finally, set the texture filter on the `SubViewportContainer` to `Nearest`.
+
+### Mouse Interaction
+If trying to detect `mouse_enter` and `mouse_exit` events on areas inside the game world, enable physics object picking on the `SubViewport`.
+
+## Read Inputs
+Generally, any game is going to require reading some inputs from the player. Where in the scene hierarchy the reading occurs is best answered with simplicity.
+
+If the game involves moving a player character, then the inputs for movements could be read by a `player_character.gd` script overriding the `_process(delta)` or `_input(event)` methods.
+
+If the game involves sending commands to multiple units, then those inputs probably should be read by a `game_ui.gd` script, that then propagates those calls further down the chain.
+
+## Win & Lose Screens
+The example includes win and lose screens. These are triggered by the `LevelListManager` when a level is won or lost.
+
+```
+func _load_level_complete_screen_or_next_level():
+ if level_won_scene:
+ var instance = level_won_scene.instantiate()
+ get_tree().current_scene.add_child(instance)
+ ...
+ else:
+ _load_next_level()
+```
+Winning on the last level results in loading a win screen or ending for the game.
+
+```
+func _on_level_won():
+ if level_list_loader.is_on_last_level():
+ _load_win_screen_or_ending()
+ else:
+ _load_level_complete_screen_or_next_level()
+```
+ The `LevelListManager` will need to be linked to direct back to the main menu and forward to `end_credits.tscn`.
\ No newline at end of file
diff --git a/addons/maaacks_game_template/docs/GamesMade.md b/addons/maaacks_game_template/docs/GamesMade.md
new file mode 100644
index 0000000..acfd24e
--- /dev/null
+++ b/addons/maaacks_game_template/docs/GamesMade.md
@@ -0,0 +1,51 @@
+# Games
+This page features games using Maaack's Godot Game Template and/or plugins.
+
+If you have a game you'd like to share, join the [Discord server](https://discord.gg/AyZrJh5AMp ) and post a link to your game in #showcase.
+
+## Featured
+
+| Spud Customs | Rent Seek Kill | A Darkness Like Gravity |
+| :-------:| :-------: | :-------: |
+ |  |  |
+[Find on Steam](https://store.steampowered.com/app/3291880/Spud_Customs/) | [Play on itch.io](https://xandruher.itch.io/rent-seek-kill) | [Play on itch.io](https://maaack.itch.io/a-darkness-like-gravity) |
+
+
+## All Shared
+### 2025
+https://schinken.itch.io/low-ink
+https://maaack.itch.io/furnace-in-the-archive
+https://plexsoup.itch.io/factoriohno
+https://maaack.itch.io/dungeon-fantasy-fashion-show
+https://maaack.itch.io/absurd-herd
+https://maaack.itch.io/indys-expedition-2
+https://baconeggsrl.itch.io/sprouts-journey
+
+### 2024
+https://store.steampowered.com/app/3291880/Spud_Customs/ (Source: https://github.com/Lost-Rabbit-Digital/SpudCustoms)
+https://glockenberg.itch.io/icefire-temple
+https://maaack.itch.io/backroom-labyrinths
+https://maaack.itch.io/haunted-circuits
+https://maaack.itch.io/talk-up-the-tower
+https://marinaaaa.itch.io/meowntaineer
+https://maaack.itch.io/a-darkness-like-gravity
+https://maaack.itch.io/lore-of-the-wild-gwj-70
+https://maaack.itch.io/infinite-horizon
+https://elidef.itch.io/forge-ur-boss
+https://maaack.itch.io/forgeomino
+https://xandruher.itch.io/rent-seek-kill
+https://maaack.itch.io/blind-escape-gwj-66-edition
+https://justaguyjustaguy.itch.io/nannybot-overload
+https://maaack.itch.io/the-last-host-boss-rush
+https://kyveri.itch.io/riverking
+
+### 2023
+https://xandruher.itch.io/spectral-war
+https://maaack.itch.io/the-cat-with-eight-gwj-63-edition
+https://maaack.itch.io/harvest-hill-gwj-62-edition
+https://shoddygames.itch.io/once-summoned
+https://maaack.itch.io/the-last-host
+https://maaack.itch.io/do-androids-dream-gwj-55-edition
+https://maaack.itch.io/character-builder-gwj-53-edition
+https://maaack.itch.io/rit-dot-wav
+https://maaack.itch.io/supercritical-a-post-apocalyptic-bonsai
\ No newline at end of file
diff --git a/addons/maaacks_game_template/docs/HowPartsWork.md b/addons/maaacks_game_template/docs/HowPartsWork.md
new file mode 100644
index 0000000..6a9e747
--- /dev/null
+++ b/addons/maaacks_game_template/docs/HowPartsWork.md
@@ -0,0 +1,16 @@
+# How Parts Work
+
+This page features snippets of extra documentation on key pieces of the plugin. It was previously included in the README.
+
+- `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.
+- The `PauseMenuController` can be set to load `pause_menu.tscn` when triggering `ui-cancel`.
+- `pause_menu.tscn` is a type of `OverlaidMenu` with the `pauses_game` flag set to true. It will store the previously focused UI element, and return focus to it when closed.
+- `capture_focus.gd` is attached to container nodes throughout the UI. It focuses onto UI elements when they are shown, allowing for easier navigation without a mouse.
+- `game_ui.tscn` is a demo game scene that displays recognized action inputs, and features the `PauseMenuController` node, the `LevelListLoader` node to load levels from a directory, and `LevelListManager` to manage level progress and show menus in case of a win or loss.
\ No newline at end of file
diff --git a/addons/maaacks_game_template/docs/InputIconMapping.md b/addons/maaacks_game_template/docs/InputIconMapping.md
new file mode 100644
index 0000000..d6da4bb
--- /dev/null
+++ b/addons/maaacks_game_template/docs/InputIconMapping.md
@@ -0,0 +1,154 @@
+# Input Icon Mapping
+
+The `InputIconMapper` in `input_options_menu.tscn` is a generalized tool meant to be broadly compatible with freely licensed icon asset packs. Instructions on how to use it with a few of these packs are provided, with links to download them from their creator's page.
+
+## Kenney Input Prompts
+
+### Automatic
+
+With the project open, select `Project > Tools > Install Input Icons for Maaack's Game Template`.
+
+Select a style and then wait for the icons to download, extract, and setup.
+
+### Manual
+
+Available from [kenney.nl](https://kenney.nl/assets/input-prompts) and [itch.io](https://kenney-assets.itch.io/input-prompts).
+
+This pack is organized by `Device/IconType`. The `IconTypes` for each device are just `Default`, `Vector`, or `Double`. These instructions will assume using `Default`. In the inspector of `InputIconMapper`, set the `directories` to include the subdirectories of the asset pack.
+* `.../kenney_input-prompts/Keyboard & Mouse/Default`
+* `.../kenney_input-prompts/Generic/Default`
+* `.../kenney_input-prompts/Xbox Series/Default`
+* `.../kenney_input-prompts/PlayStation Series/Default`
+* `.../kenney_input-prompts/Nintendo Switch/Default`
+* `.../kenney_input-prompts/Steam Deck/Default`
+
+Set `filtered_strings` to:
+* `keyboard`
+* `color`
+* `button`
+* `arrow`
+
+Set `replace_strings` with the key pairs:
+* `"Capslock": "Caps Lock"`
+* `"Generic Stick": "Generic Left Stick"`
+* `"Guide": "Home"`
+* `"Slash Back": "Back Slash"`
+* `"Slash Forward": "Slash"`
+* `"Stick L": "Left Stick"`
+* `"Stick R": "Right Stick"`
+* `"Trigger L 1": "Left Shoulder"`
+* `"Trigger L 2": "Left Trigger"`
+* `"Trigger R 1": "Right Shoulder"`
+* `"Trigger R 2": "Right Trigger"`
+
+#### Filled Icons
+
+Under the `FileLister` properties of the `InputIconMapper`, expand the `Constraints` and `Advanced Search` tabs. Set `ends_with=".png"` and `not_ends_with="outline.png"`.
+
+Press `Refresh Files`.
+
+If you want to use colored icons, in `prioritized_strings` add `color`. Otherwise set `filter="color"`.
+
+Press `Match Icons to Inputs`.
+
+Validate the results by inspecting the `matching_icons` dictionary.
+
+#### Outlined Icons
+
+Not all icons have outlined versions, so we will end up including the filled icons as fallback, and prioritizing outlined.
+
+Under the `FileLister` properties of the `InputIconMapper`, expand the `Constraints` and `Advanced Search` export groups. Set `ends_with=".png"`.
+
+Press `Refresh Files`.
+
+Add to `filtered_strings`:
+* `outline`
+
+In `prioritized_strings` add `outline`. If you want to use colored icons, in `prioritized_strings` add `color`, too. Otherwise set `filter="color"`.
+
+Press `Match Icons to Inputs`.
+
+Validate the results by inspecting the `matching_icons` dictionary.
+
+## Kenny Input Prompts Pixel 16x
+
+Incompatible: File names not useable.
+
+## Xelu 's Free Controller & Key Prompts
+
+
+Available from [thoseawesomeguys.com](https://thoseawesomeguys.com/prompts/).
+
+This pack is organized by `Device`. In the inspector of `InputIconMapper`, set the `directories` to include the subdirectories of the asset pack. Assumes using the `Dark` icon set with the keyboard and mouse.
+* `.../Xelu_Free_Controller&Key_Prompts/Keyboard & Mouse/Dark`
+* `.../Xelu_Free_Controller&Key_Prompts/Xbox Series`
+* `.../Xelu_Free_Controller&Key_Prompts/PS5`
+* `.../Xelu_Free_Controller&Key_Prompts/Switch`
+* `.../Xelu_Free_Controller&Key_Prompts/Steam Deck`
+
+Under the `FileLister` properties of the `InputIconMapper`, expand the `Constraints` and `Advanced Search` tabs. Set `ends_with=".png"`.
+
+Press `Refresh Files`.
+
+Set `filtered_strings` to:
+* `dark`
+* `key`
+
+Set `replace_strings` with the key pairs:
+* `"Ps 5": "Playstation"`
+* `"Xbox Series X": "Xbox"`
+* `"Steam Deck": "Steamdeck"`
+* `"L 1": "Left Shoulder"`
+* `"R 1": "Right Shoulder"`
+* `"L 2": "Left Trigger"`
+* `"R 2": "Right Trigger"`
+* `"Click": "Press"`
+
+Set `add_stick_directions=true`.
+
+Press `Match Icons to Inputs`.
+
+Validate the results by inspecting the `matching_icons` dictionary.
+
+Since `Generic` device icons are not available, set `initial_joypad_device` to either `Xbox`, `Playstation`, `Switch`, or `Steamdeck`.
+
+## Free Icon Pack for Unity & Unreal – 1500+ Input Icons for Game UI
+
+
+Available from [itch.io](https://juliocacko.itch.io/free-input-prompts).
+
+This pack is organized by `Device/IconType`. In the inspector of `InputIconMapper`, set the `directories` to include the subdirectories of the asset pack. Assumes using the `Dark` icon set with the keyboard and mouse, and `Default` for the others.
+* `.../Source/Keyboard_Mouse/Dark`
+* `.../Source/P4Gamepad/Default`
+* `.../Source/XGamepad/Default`
+* `.../Source/SGamepad/Default`
+
+Under the `FileLister` properties of the `InputIconMapper`, expand the `Constraints` and `Advanced Search` tabs. Set `ends_with=".png"`.
+
+Press `Refresh Files`.
+
+In `prioritized_strings`, add either `color` or `white`, depending on what icons you prefer.
+
+Set `filtered_strings` to:
+* `dark`
+* `key`
+* `t`
+* `color`
+* `white`
+
+Set `replace_strings` with the key pairs:
+* `"P 4": "Playstation"`
+* `"X": "Xbox"`
+* `"S": "Switch"`
+* `"L": "Left Stick"`
+* `"R": "Right Stick"`
+* `"Left Stick 1": "Left Shoulder"`
+* `"Right Stick 1": "Right Shoulder"`
+* `"Left Stick 2": "Left Trigger"`
+* `"Right Stick 2": "Right Trigger"`
+
+Press `Match Icons to Inputs`.
+
+Validate the results by inspecting the `matching_icons` dictionary.
+
+Since `Generic` device icons are not available, set `initial_joypad_device` to either `Xbox`, `Playstation`, or `Switch`.
diff --git a/addons/maaacks_game_template/docs/JoypadInputs.md b/addons/maaacks_game_template/docs/JoypadInputs.md
new file mode 100644
index 0000000..0515080
--- /dev/null
+++ b/addons/maaacks_game_template/docs/JoypadInputs.md
@@ -0,0 +1,31 @@
+# Joypad Inputs
+
+This page covers topics related to working with joypads.
+
+## Recognized Devices
+
+- Xbox
+- Playstation 4
+- Playstation 5
+
+### Unconfirmed
+
+- Switch
+- Steam Deck
+
+## Added UI Inputs
+
+There is a `override.cfg` in the project root directory that adds a few additional inputs to the project's built-in UI actions.
+
+These additional inputs are for joypads and include the following:
+
+- `UI Accept`: A Button (Xbox A / Sony X)
+- `UI Cancel`: Back Button (Xbox Back / Sony Select)
+- `UI Page Up`: Left Shoulder (Xbox LB / Sony L1)
+- `UI Page Down`: Right Shoulder (Xbox RB / Sony R2)
+
+However, for these to work in exported versions of the project, the inputs need to either be added manually to the project's built-in actions, or `override.cfg` will need to be included in the exports. The latter can be done by including the pattern (`*.cfg`) in **Filters to export non-resource files/folders** under the *Resources* tab of the *Export* window.
+
+## Web Builds
+
+Godot (or the template) currently does not support joypad device detection on the web. If icons are being used for input remapping, the joypad icons will *not* update automatically to match a new detected controller.
\ No newline at end of file
diff --git a/addons/maaacks_game_template/docs/MainMenuSetup.md b/addons/maaacks_game_template/docs/MainMenuSetup.md
new file mode 100644
index 0000000..b6a120a
--- /dev/null
+++ b/addons/maaacks_game_template/docs/MainMenuSetup.md
@@ -0,0 +1,34 @@
+# Main Menu Setup
+
+These are instructions for further editing the menus. Basic instructions are available in the [README](/addons/maaacks_game_template/README.md#usage).
+
+## Inheritance
+
+Most example scenes in the template inherit from scenes in `addons`. This is useful for developing of the plugin, but often less useful for those using it. When editing the example scenes, any nodes inherited from a parent scene are highlighted in yellow in the scene tree. Inherited nodes cannot be edited like native nodes. Therefore, it is recommended to first right-click on the root node, and select `Clear Inheritance`. You'll get a warning that this cannot be undone, but it's okay. You probably won't need to undo it, and if you do, there are solutions.
+
+## Visual Placement
+
+The positions and anchor presets of the UI elements can be adjusted to match most designs with ease. Buttons can be centered, right or left justfied, or arranged horizontally. Most visual UI elements are contained within `MarginContainer` and `Control` nodes that allow for fine-tuning of placement.
+
+## Scene Structure
+Some designs may require rearranging the nodes in the scene tree. This is easier once the inheritance to the parent scene is cleared. However, if editing `main_menu_with_animations.tscn`, keep in mind that there are animations, and moving elements outside of the animated containers may have undesired effects.
+
+## 3D Background
+When adding a 3D background to the menu, it is recommended to use a `SubViewportContainer` in place of or right above the `BackgroundTextureRect`. Then add a `SubViewport` to it, and finally the 3D world node to that. This structure gives fine-tune control of scaling, allows for layering 3D views when they have transparency, and makes it easy to add a texture shader to the whole background.
+
+## Level Select
+
+A basic level select scene is available to add to the menu. In `main_menu_with_animations.tscn`, click the root `MainMenu` mode and set `Level Select Packed Scene` to `level_select_menu.tscn`. The button will appear on the main menu when the player has reached the second level.
+
+Levels can be added to the menu by inspecting the `SceneLister` and either selecting a directory to automatically read scene files from, or populating the files array manually.
+
+## Theming
+It is recommended to have a custom theme for a project. Create a theme resource file or use one of the ones provided with the template and set it as the custom theme in the project settings. Any changes made to the theme file will then apply automatically to the whole project.
+
+The main UI elements that are used throughout the project that require theming for customization are:
+- Button
+- Label
+- PanelContainer
+- ProgressBar
+- TabContainer
+- Tree
\ No newline at end of file
diff --git a/addons/maaacks_game_template/docs/NewProject.md b/addons/maaacks_game_template/docs/NewProject.md
new file mode 100644
index 0000000..ee91992
--- /dev/null
+++ b/addons/maaacks_game_template/docs/NewProject.md
@@ -0,0 +1,126 @@
+# New Projects
+
+These instructions assume starting with the entire contents of the project folder. This will be the case when cloning the repo, or starting from the *template* version in the Godot Asset Library.
+
+
+1. Finish setup and remove duplicate example files.
+
+
+ 1. Go to `Project > Tools > Copy Maaack's Game Template Examples`.
+ 2. Click `Cancel` in the first window asking to copy the examples. It's already done.
+ 3. Select a theme in the next window if desired.
+ 4. Go to `Project > Tools > Delete Maaack's Game Template Examples`.
+ 5. Click `Yes` in the first window.
+
+
+2. Update the project’s name.
+
+
+ 1. Go to `Project > Project Settings… > General > Application > Config`.
+ 2. Update `Name` to `"Game Name"`.
+ 3. Close the window.
+ 4. Open `main_menu_with_animations.tscn`.
+ 5. The `Title` node should automatically update with the project's title. Customize the `Text` property if desired.
+ 7. Select the `Subtitle` node and customize the `Text` property if desired.
+ 9. Save the scene.
+
+
+3. Add background music and sound effects to the UI.
+
+
+ 1. Verify the `Music` and `SFX` audio busses.
+
+ 1. Open the Audio bus editor.
+ 2. Make sure there is a bus for `Music` and another for `SFX`.
+ 3. Add the busses if they do not exist.
+
+ 2. Add background music to the Main Menu.
+
+ 1. Import the music asset into the project.
+ 2. Open `main_menu_with_animations.tscn`.
+ 3. Select the `BackgroundMusicPlayer` node.
+ 4. Assign the music asset to the `stream` property.
+ 5. Make sure that the `bus` property is set to `Music`.
+ 6. Save the scene.
+ 7. Optionally, repeat steps 3-5 for background music nodes in:
+ 1. `opening_with_logo.tscn`
+ 2. `game_ui.tscn`
+ 3. `end_credits.tscn`
+
+
+ 3. Add sound effects to UI elements.
+
+
+ 1. By scene.
+
+
+ 1. Open `main_menu_with_animations.tscn` and `pause_menu.tscn`.
+ 2. Select the `UISoundController` node.
+ 3. Add audio streams to the various UI node events.
+ 4. Save the scenes.
+
+
+ 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.
+
+
+4. Add readable names for input actions to the controls menu.
+
+
+ 1. Open `input_options_menu.tscn`.
+ 2. In the scene tree, select the `Controls` node.
+ 3. In the node inspector, select the desired input remapping mode (defaults to `List`).
+ 4. In the scene tree, select `InputActionsList` or `InputActionsTree`, depending on the choice of input remapping. The other node should be hidden.
+ 5. In the node inspector, update the `Input Action Names` and corresponding `Readable Action Names` to show user-friendly names for the project's input actions.
+ 6. Save the scene.
+
+
+5. 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 5.3.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.
+
+
+6. 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.
+
+
+7. Keep, update, or remove `res://LICENSE.txt`.
+
+
+8. Optionally, if using Git for version control, update `.gitignore` to include `addons/`.
+
+
+9. Continue with:
+
+ 1. [Setting up the Main Menu.](/addons/maaacks_game_template/docs/MainMenuSetup.md)
+ 2. [Adding icons to the Input Options.](/addons/maaacks_game_template/docs/InputIconMapping.md)
+ 3. [Setting up a Game Scene.](/addons/maaacks_game_template/docs/GameSceneSetup.md)
+ 4. [Utilizing Game Saving](/addons/maaacks_game_template/docs/GameSaving.md)
diff --git a/addons/maaacks_game_template/docs/PluginSuite.md b/addons/maaacks_game_template/docs/PluginSuite.md
new file mode 100644
index 0000000..5555e5f
--- /dev/null
+++ b/addons/maaacks_game_template/docs/PluginSuite.md
@@ -0,0 +1,27 @@
+# Plugin Suite
+
+
+
+Maaack's Game Template is a culmination of a suite of plugins, that can be downloaded individually, if desired.
+
+## GitHub
+
+- [Game Template](https://github.com/Maaack/Godot-Game-Template)
+ - [Menus Template](https://github.com/Maaack/Godot-Menus-Template)
+ - [Options Menus](https://github.com/Maaack/Godot-Options-Menus)
+ - [Input Remapping](https://github.com/Maaack/Godot-Input-Remapping)
+ - [Scene Loader](https://github.com/Maaack/Godot-Scene-Loader)
+ - [Credits Scene](https://github.com/Maaack/Godot-Credits-Scene)
+ - [UI Sound Controller](https://github.com/Maaack/Godot-UI-Sound-Controller)
+ - [Music Controller](https://github.com/Maaack/Godot-Music-Controller)
+
+## Godot Asset Library
+
+- [Game Template](https://godotengine.org/asset-library/asset/2709)
+ - [Menus Template](https://godotengine.org/asset-library/asset/2899)
+ - [Options Menus](https://godotengine.org/asset-library/asset/3058)
+ - [Input Remapping](https://godotengine.org/asset-library/asset/4051)
+ - [Scene Loader](https://godotengine.org/asset-library/asset/2896)
+ - [Credits Scene](https://godotengine.org/asset-library/asset/2932)
+ - [UI Sound Controller](https://godotengine.org/asset-library/asset/2897)
+ - [Music Controller](https://godotengine.org/asset-library/asset/2898)
diff --git a/addons/maaacks_game_template/docs/Screenshots.md b/addons/maaacks_game_template/docs/Screenshots.md
new file mode 100644
index 0000000..9f3202e
--- /dev/null
+++ b/addons/maaacks_game_template/docs/Screenshots.md
@@ -0,0 +1,57 @@
+# Screenshots
+Screenshots organized by included themes with a variety of the features shown.
+
+The template is presented here in a 640x360 resolution, but up to 4k resolutions are supported.
+
+## Default (No Theme)
+
+
+
+
+
+
+
+
+
+
+## Gravity
+
+
+
+
+
+
+
+
+
+
+
+## Lore
+
+
+
+
+
+
+
+
+## Steal This Theme
+
+
+
+
+
+
+
+
+
+
+
+
+## Tower
+
+
+
+
+
+
diff --git a/addons/maaacks_game_template/docs/UploadingToItchIo.md b/addons/maaacks_game_template/docs/UploadingToItchIo.md
new file mode 100644
index 0000000..9678f4c
--- /dev/null
+++ b/addons/maaacks_game_template/docs/UploadingToItchIo.md
@@ -0,0 +1,37 @@
+# Uploading to itch.io
+
+This is a guide on using *Butler* along with a *Butler Manager* helper script to rapidly upload and deploy your builds to itch.io. It's useful for game jams!
+
+## Butler
+
+*Butler* is a command-line tool provided by itch.io to upload content to project pages on itch.io.
+
+Get it here: https://itchio.itch.io/butler
+
+After installing it, run `butler login` and go through the login flow. You should only have to do this once.
+
+*Butler* automatically compares builds and only uploads what has changed, so the first upload will take the longest, but every upload after should be faster.
+
+## Exporting
+
+It is recommended to create an `exports/` directory for your builds, add the directory to your `.gitignore` file (if applicable), and also add a `.gdignore` file to the directory to avoid having Godot add `*.import` files to it as well.
+
+
+## Butler Manager
+
+This script provided at `addons/maaacks_game_template/extras/scripts/butler_manager.sh` can be used to rapidly deploy 4 different builds to your project page. Make sure you can run `bash` shell scripts on your OS. Copy the script into your `exports/` directory and mark it as an executable, if required.
+
+Run the script with `./butler_manager.sh`. On the first run, it will ask for the destination for uploads. This is a combination of the page owner and the project's URL.
+
+The Butler Manager will look for directories named the following:
+
+* HTML5
+* Linux
+* Windows
+* MacOS
+
+Matching directories will be uploaded by *Butler* to their corresponding channels on itch.io. They will then be processed by itch.io servers and eventually appear on the page (usually within 2 minutes).
+
+The owner of the project page will also get a notification when the builds have finished processing.
+
+You can re-run `./butler_manager.sh` right after an export from Godot to keep your builds synced.
\ No newline at end of file
diff --git a/addons/maaacks_game_template/docs/Videos.md b/addons/maaacks_game_template/docs/Videos.md
new file mode 100644
index 0000000..5250464
--- /dev/null
+++ b/addons/maaacks_game_template/docs/Videos.md
@@ -0,0 +1,6 @@
+# Videos
+
+[](https://youtu.be/U9CB3vKINVw)
+[](https://youtu.be/-QWJnZ8bVdk)
+[](https://youtu.be/SBE4icfXYRA)
+[](https://youtu.be/wCc2QUnaBKo)
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.gd b/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.gd
new file mode 100644
index 0000000..9ceee50
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.gd
@@ -0,0 +1,34 @@
+class_name GameWonMenu
+extends OverlaidMenu
+
+signal continue_pressed
+signal main_menu_pressed
+
+func _handle_cancel_input():
+ if $ConfirmExit.visible:
+ $ConfirmExit.hide()
+ elif $ConfirmMainMenu.visible:
+ $ConfirmMainMenu.hide()
+ else:
+ super._handle_cancel_input()
+
+func _ready():
+ if OS.has_feature("web"):
+ %ExitButton.hide()
+
+func _on_exit_button_pressed():
+ $ConfirmExit.popup_centered()
+
+func _on_main_menu_button_pressed():
+ $ConfirmMainMenu.popup_centered()
+
+func _on_confirm_main_menu_confirmed():
+ main_menu_pressed.emit()
+ close()
+
+func _on_confirm_exit_confirmed():
+ get_tree().quit()
+
+func _on_close_button_pressed():
+ continue_pressed.emit()
+ close()
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.gd.uid b/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.gd.uid
new file mode 100644
index 0000000..652953e
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.gd.uid
@@ -0,0 +1 @@
+uid://gi8tms6wsj1n
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.tscn b/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.tscn
new file mode 100644
index 0000000..17b2180
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.tscn
@@ -0,0 +1,61 @@
+[gd_scene load_steps=3 format=3 uid="uid://4brssbq1ghsw"]
+
+[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_87sd7"]
+[ext_resource type="Script" uid="uid://gi8tms6wsj1n" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.gd" id="2_hi7oy"]
+
+[node name="GameWonMenu" instance=ExtResource("1_87sd7")]
+process_mode = 3
+script = ExtResource("2_hi7oy")
+pauses_game = true
+
+[node name="MenuPanelContainer" parent="." index="1"]
+custom_minimum_size = Vector2(432, 240)
+
+[node name="TitleMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="0"]
+visible = false
+
+[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
+visible = true
+theme_override_constants/margin_top = 64
+theme_override_constants/margin_bottom = 64
+
+[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
+text = "[center]You won![/center]"
+scroll_active = false
+
+[node name="MenuButtons" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin" index="0"]
+custom_minimum_size = Vector2(400, 0)
+vertical = false
+
+[node name="ExitButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Exit"
+
+[node name="MainMenuButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Main Menu"
+
+[node name="CloseButton" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="2"]
+size_flags_horizontal = 3
+text = "Continue"
+
+[node name="ConfirmMainMenu" type="ConfirmationDialog" parent="." index="2"]
+auto_translate_mode = 1
+initial_position = 2
+dialog_text = "Go back to main menu?"
+
+[node name="ConfirmExit" type="ConfirmationDialog" parent="." index="3"]
+auto_translate_mode = 1
+initial_position = 2
+dialog_text = "Quit the game?"
+
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/ExitButton" to="." method="_on_exit_button_pressed"]
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/MainMenuButton" to="." method="_on_main_menu_button_pressed"]
+[connection signal="confirmed" from="ConfirmMainMenu" to="." method="_on_confirm_main_menu_confirmed"]
+[connection signal="confirmed" from="ConfirmExit" to="." method="_on_confirm_exit_confirmed"]
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.gd b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.gd
new file mode 100644
index 0000000..976357a
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.gd
@@ -0,0 +1,34 @@
+class_name LevelLostMenu
+extends OverlaidMenu
+
+signal restart_pressed
+signal main_menu_pressed
+
+func _handle_cancel_input():
+ if $ConfirmExit.visible:
+ $ConfirmExit.hide()
+ elif $ConfirmMainMenu.visible:
+ $ConfirmMainMenu.hide()
+ else:
+ super._handle_cancel_input()
+
+func _ready():
+ if OS.has_feature("web"):
+ %ExitButton.hide()
+
+func _on_exit_button_pressed():
+ $ConfirmExit.popup_centered()
+
+func _on_main_menu_button_pressed():
+ $ConfirmMainMenu.popup_centered()
+
+func _on_confirm_main_menu_confirmed():
+ main_menu_pressed.emit()
+ close()
+
+func _on_confirm_exit_confirmed():
+ get_tree().quit()
+
+func _on_close_button_pressed():
+ restart_pressed.emit()
+ close()
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.gd.uid b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.gd.uid
new file mode 100644
index 0000000..9c4f54f
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.gd.uid
@@ -0,0 +1 @@
+uid://ckh3w3xa6qjk0
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.tscn b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.tscn
new file mode 100644
index 0000000..1f8be8b
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.tscn
@@ -0,0 +1,59 @@
+[gd_scene load_steps=3 format=3 uid="uid://dkq3nhkmhu4je"]
+
+[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_ok347"]
+[ext_resource type="Script" uid="uid://ckh3w3xa6qjk0" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.gd" id="2_6r1n8"]
+
+[node name="LevelLostMenu" instance=ExtResource("1_ok347")]
+process_mode = 3
+script = ExtResource("2_6r1n8")
+pauses_game = true
+
+[node name="MenuPanelContainer" parent="." index="1"]
+custom_minimum_size = Vector2(432, 240)
+
+[node name="TitleMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="0"]
+visible = false
+
+[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
+visible = true
+theme_override_constants/margin_top = 64
+theme_override_constants/margin_bottom = 64
+
+[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
+text = "[center]You lost...[/center]"
+scroll_active = false
+
+[node name="MenuButtons" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin" index="0"]
+custom_minimum_size = Vector2(400, 0)
+vertical = false
+
+[node name="ExitButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Exit"
+
+[node name="MainMenuButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Main Menu"
+
+[node name="CloseButton" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="2"]
+size_flags_horizontal = 3
+text = "Restart"
+
+[node name="ConfirmMainMenu" type="ConfirmationDialog" parent="." index="2"]
+auto_translate_mode = 1
+initial_position = 2
+dialog_text = "Go back to main menu?"
+
+[node name="ConfirmExit" type="ConfirmationDialog" parent="." index="3"]
+auto_translate_mode = 1
+initial_position = 2
+dialog_text = "Quit the game?"
+
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/ExitButton" to="." method="_on_exit_button_pressed"]
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/MainMenuButton" to="." method="_on_main_menu_button_pressed"]
+[connection signal="confirmed" from="ConfirmMainMenu" to="." method="_on_confirm_main_menu_confirmed"]
+[connection signal="confirmed" from="ConfirmExit" to="." method="_on_confirm_exit_confirmed"]
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.gd b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.gd
new file mode 100644
index 0000000..d80b1fc
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.gd
@@ -0,0 +1,27 @@
+class_name LevelWonMenu
+extends OverlaidMenu
+
+signal continue_pressed
+signal restart_pressed
+signal main_menu_pressed
+
+func _input(event):
+ if event.is_action_pressed("ui_cancel"):
+ if $ConfirmMainMenu.visible:
+ $ConfirmMainMenu.hide()
+ get_viewport().set_input_as_handled()
+
+func _on_main_menu_button_pressed():
+ $ConfirmMainMenu.popup_centered()
+
+func _on_confirm_main_menu_confirmed():
+ main_menu_pressed.emit()
+ close()
+
+func _on_restart_button_pressed():
+ restart_pressed.emit()
+ close()
+
+func _on_close_button_pressed():
+ continue_pressed.emit()
+ close()
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.gd.uid b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.gd.uid
new file mode 100644
index 0000000..09d2a97
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.gd.uid
@@ -0,0 +1 @@
+uid://bdic7jebf0y7a
diff --git a/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.tscn b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.tscn
new file mode 100644
index 0000000..45a8205
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.tscn
@@ -0,0 +1,53 @@
+[gd_scene load_steps=3 format=3 uid="uid://y3vtx0e0shv4"]
+
+[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_nknag"]
+[ext_resource type="Script" uid="uid://bdic7jebf0y7a" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.gd" id="2_klq7f"]
+
+[node name="LevelWonMenu" instance=ExtResource("1_nknag")]
+process_mode = 3
+script = ExtResource("2_klq7f")
+pauses_game = true
+
+[node name="MenuPanelContainer" parent="." index="1"]
+custom_minimum_size = Vector2(432, 240)
+
+[node name="TitleMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="0"]
+visible = false
+
+[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
+visible = true
+theme_override_constants/margin_top = 64
+theme_override_constants/margin_bottom = 64
+
+[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
+text = "[center]Level complete![/center]"
+scroll_active = false
+
+[node name="MenuButtons" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin" index="0"]
+custom_minimum_size = Vector2(400, 0)
+vertical = false
+
+[node name="MainMenuButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Main Menu"
+
+[node name="RestartButton" type="Button" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Restart"
+
+[node name="CloseButton" parent="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="2"]
+size_flags_horizontal = 3
+text = "Continue"
+
+[node name="ConfirmMainMenu" type="ConfirmationDialog" parent="." index="2"]
+auto_translate_mode = 1
+initial_position = 2
+dialog_text = "Go back to main menu?"
+
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/MainMenuButton" to="." method="_on_main_menu_button_pressed"]
+[connection signal="pressed" from="MenuPanelContainer/MarginContainer/BoxContainer/MenuButtonsMargin/MenuButtons/RestartButton" to="." method="_on_restart_button_pressed"]
+[connection signal="confirmed" from="ConfirmMainMenu" to="." method="_on_confirm_main_menu_confirmed"]
diff --git a/addons/maaacks_game_template/extras/scripts/asset_checker.sh b/addons/maaacks_game_template/extras/scripts/asset_checker.sh
new file mode 100644
index 0000000..4174191
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/asset_checker.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+# asset checker command
+# Used for quickly checking that assets (like audio files) are being used where expected.
+#
+# Recursively searches through scene files (.tscn, .scn, .res)
+# for occurrences of asset types (default: AudioStream).
+# It then outputs the paths of assets discovered,
+# along with the file names that use them.
+
+short_flag=false
+asset_type="AudioStream"
+
+print_usage() {
+ printf "Usage: -sa %s\n" "$asset_type"
+}
+
+while getopts 'a:s' flag; do
+ case "${flag}" in
+ a)
+ asset_type="${OPTARG}"
+ ;;
+ s)
+ short_flag=true
+ ;;
+ *)
+ print_usage
+ exit 1
+ ;;
+ esac
+done
+
+# Initialize an associative array to store paths and corresponding files
+declare -A path_files
+
+while IFS=: read -r file line; do
+ path=$(echo "$line" | grep -o 'path="[^"]*' | cut -d'"' -f2)
+ if [ -n "$path" ]; then
+ # Append the current file to the string of files for this path
+ # Note: Bash does not support having arrays as values of associative array.
+ # Using a pipe `|` separator instead, and then splitting on output
+ if [ -z "${path_files["$path"]}" ]; then
+ path_files["$path"]=$file
+ else
+ path_files["$path"]+="|$file"
+ fi
+ fi
+done < <(egrep -ir --include=*.{tscn,scn,res} "type=\"$asset_type\"")
+
+# Get the paths and sort them
+sorted_paths=()
+for key in "${!path_files[@]}"; do
+ sorted_paths+=("$key")
+done
+IFS=$'\n' sorted_paths=($(sort <<< "${sorted_paths[*]}"))
+unset IFS
+
+# Print out the results
+for path in "${sorted_paths[@]}"; do
+ # Note: Bash does not support having arrays as values of associative array.
+ # Splitting the concatenated files string on the pipe `|` separator.
+ IFS='|' read -r -a files_array <<< "${path_files[$path]}"
+ files_count=${#files_array[@]}
+ printf "%-80s | Uses: %s\n" "$path" "$files_count"
+ if ! $short_flag ; then
+ for file in "${files_array[@]}"; do
+ printf "\t%82s\n" "$file"
+ done
+ echo
+ fi
+done
diff --git a/addons/maaacks_game_template/extras/scripts/butler_manager.sh b/addons/maaacks_game_template/extras/scripts/butler_manager.sh
new file mode 100644
index 0000000..b576487
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/butler_manager.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# butler manager command
+# Uploads directories as builds to matching itch.io channels.
+# HTML5 => html5
+# Linux => linux
+# Windows => win
+# MacOS => osx
+
+file=upload_destination.txt
+directories=("HTML5" "Linux" "Windows" "MacOS")
+channels=("html5" "linux" "win" "osx")
+
+# Check if the file exists
+if [ ! -e $file ]; then
+ # File doesn't exist, create an empty one
+ touch $file
+fi
+
+# File exists, read the first line into a variable
+read -r destination < $file
+
+if [ -z "$destination" ]; then
+ # File is empty, prompt the user for input
+ echo "Please enter the build destination (username/project-url-after-slash)."
+ read -r user_input
+
+ # Save user input to the file
+ echo "$user_input" > "$file"
+ echo "Destination saved to $file."
+ destination="$user_input"
+fi
+
+# Check for the existence of directories and upload contents
+for ((i=0; i<${#directories[@]}; i++)); do
+ dir="${directories[i]}"
+ channel="${channels[i]}"
+
+ if [ -d "$dir" ]; then
+ echo butler push ./$dir/ $destination:$channel
+ butler push ./$dir/ $destination:$channel
+ else
+ echo "Directory '$dir' does not exist."
+ fi
+done
\ No newline at end of file
diff --git a/addons/maaacks_game_template/extras/scripts/capture_mouse.gd b/addons/maaacks_game_template/extras/scripts/capture_mouse.gd
new file mode 100644
index 0000000..115769a
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/capture_mouse.gd
@@ -0,0 +1,9 @@
+class_name CaptureMouse
+extends Control
+## Control node that captures the mouse for games that require it.
+##
+## Used for games that use the mouse to move the camera (ex. FPS or third-person shooters).
+
+func _gui_input(event):
+ if event is InputEventMouseButton and Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
+ Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
diff --git a/addons/maaacks_game_template/extras/scripts/capture_mouse.gd.uid b/addons/maaacks_game_template/extras/scripts/capture_mouse.gd.uid
new file mode 100644
index 0000000..e3b0ff7
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/capture_mouse.gd.uid
@@ -0,0 +1 @@
+uid://dqdyrkm3jily6
diff --git a/addons/maaacks_game_template/extras/scripts/level_list_loader.gd b/addons/maaacks_game_template/extras/scripts/level_list_loader.gd
new file mode 100644
index 0000000..509ef14
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/level_list_loader.gd
@@ -0,0 +1,48 @@
+@tool
+class_name LevelListLoader
+extends SceneLister
+## Extends [SceneLister] to manage level advancement through [GameStateExample].
+
+signal level_load_started
+signal level_loaded
+signal levels_finished
+
+## Container where the level instance will be added.
+@export var level_container : Node
+
+var current_level : Node
+
+func get_level_file(level_id : int):
+ if files.is_empty():
+ push_error("levels list is empty")
+ return
+ if level_id >= files.size():
+ push_error("level_id is out of bounds of the levels list")
+ level_id = files.size() - 1
+ return files[level_id]
+
+func _attach_level(level_resource : Resource):
+ assert(level_container != null, "level_container is null")
+ var instance = level_resource.instantiate()
+ level_container.call_deferred("add_child", instance)
+ return instance
+
+func load_level(level_id : int):
+ if is_instance_valid(current_level):
+ current_level.queue_free()
+ await current_level.tree_exited
+ current_level = null
+ var level_file = get_level_file(level_id)
+ if level_file == null:
+ levels_finished.emit()
+ return
+ SceneLoader.load_scene(level_file, true)
+ level_load_started.emit()
+ await SceneLoader.scene_loaded
+ current_level = _attach_level(SceneLoader.get_resource())
+ level_loaded.emit()
+
+func _ready():
+ if Engine.is_editor_hint():
+ # Text files get a `.remap` extension added on export.
+ _refresh_files()
diff --git a/addons/maaacks_game_template/extras/scripts/level_list_loader.gd.uid b/addons/maaacks_game_template/extras/scripts/level_list_loader.gd.uid
new file mode 100644
index 0000000..9f012db
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/level_list_loader.gd.uid
@@ -0,0 +1 @@
+uid://crbo2e4ndbyvk
diff --git a/addons/maaacks_game_template/extras/scripts/level_list_manager.gd b/addons/maaacks_game_template/extras/scripts/level_list_manager.gd
new file mode 100644
index 0000000..8787523
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/level_list_manager.gd
@@ -0,0 +1,143 @@
+class_name LevelListManager
+extends Node
+## Manager of level progress and the result screens between them.
+##
+## A helper script to assign to a node in a scene.
+## It works with a level list loader and a loading screen
+## to advance levels and open menus when players win or lose.
+
+## Required reference to a level list loader in the scene.
+@export var level_list_loader : LevelListLoader
+## Required path to a main menu scene.
+@export_file("*.tscn") var main_menu_scene : String
+## Optional path to an ending scene.
+@export_file("*.tscn") var ending_scene : String
+@export var auto_load : bool = true
+@export_group("Screens")
+## Optional reference to a loading screen in the scene.
+@export var level_loading_screen : LoadingScreen
+## Optional win screen to be shown after the last level is won.
+@export var game_won_scene : PackedScene
+## Optional lose screen to be shown after the level is lost.
+@export var level_lost_scene : PackedScene
+## Optional level compete screen to be shown after the level is won.
+@export var level_won_scene : PackedScene
+## Loads a level on start.
+@export_group("Debugging")
+@export var force_level : int = -1
+
+## Reference to the current level node.
+var current_level
+var current_level_id : int :
+ set = set_current_level_id
+
+func set_current_level_id(value : int) -> void:
+ current_level_id = value
+
+func _try_connecting_signal_to_node(node : Node, signal_name : String, callable : Callable) -> void:
+ if node.has_signal(signal_name) and not node.is_connected(signal_name, callable):
+ node.connect(signal_name, callable)
+
+func _try_connecting_signal_to_level(signal_name : String, callable : Callable) -> void:
+ _try_connecting_signal_to_node(current_level, signal_name, callable)
+
+func _load_main_menu() -> void:
+ SceneLoader.load_scene(main_menu_scene)
+
+func _advance_level() -> bool:
+ if is_on_last_level(): return false
+ current_level_id += 1
+ return true
+
+func _advance_and_load_main_menu() -> void:
+ _advance_level()
+ _load_main_menu()
+
+func _load_ending() -> void:
+ if ending_scene:
+ SceneLoader.load_scene(ending_scene)
+ else:
+ _load_main_menu()
+
+func _on_level_lost() -> void:
+ if level_lost_scene:
+ var instance = level_lost_scene.instantiate()
+ get_tree().current_scene.add_child(instance)
+ _try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
+ _try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
+ else:
+ _reload_level()
+
+func get_current_level_id() -> int:
+ return current_level_id if force_level == -1 else force_level
+
+func load_current_level() -> void:
+ level_list_loader.load_level(get_current_level_id())
+
+func _advance_and_reload() -> void:
+ var _prior_level_id = get_current_level_id()
+ _advance_level()
+ current_level_id = _prior_level_id
+ load_current_level()
+
+func _load_next_level() -> void:
+ _advance_level()
+ load_current_level()
+
+func _reload_level() -> void:
+ load_current_level()
+
+func _load_win_screen_or_ending() -> void:
+ if game_won_scene:
+ var instance = game_won_scene.instantiate()
+ get_tree().current_scene.add_child(instance)
+ _try_connecting_signal_to_node(instance, &"continue_pressed", _load_ending)
+ _try_connecting_signal_to_node(instance, &"restart_pressed", _reload_level)
+ _try_connecting_signal_to_node(instance, &"main_menu_pressed", _load_main_menu)
+ else:
+ _load_ending()
+
+func _load_level_complete_screen_or_next_level() -> void:
+ if level_won_scene:
+ var instance = level_won_scene.instantiate()
+ get_tree().current_scene.add_child(instance)
+ _try_connecting_signal_to_node(instance, &"continue_pressed", _load_next_level)
+ _try_connecting_signal_to_node(instance, &"restart_pressed", _advance_and_reload)
+ _try_connecting_signal_to_node(instance, &"main_menu_pressed", _advance_and_load_main_menu)
+ else:
+ _load_next_level()
+
+func is_on_last_level() -> bool:
+ return get_current_level_id() + 1 >= level_list_loader.files.size()
+
+func _on_level_won():
+ if is_on_last_level():
+ _load_win_screen_or_ending()
+ else:
+ _load_level_complete_screen_or_next_level()
+
+func _connect_level_signals() -> void:
+ _try_connecting_signal_to_level(&"level_won", _on_level_won)
+ _try_connecting_signal_to_level(&"level_lost", _on_level_lost)
+ _try_connecting_signal_to_level(&"level_skipped", _load_next_level)
+
+func _on_level_loader_level_loaded() -> void:
+ current_level = level_list_loader.current_level
+ await current_level.ready
+ _connect_level_signals()
+ if level_loading_screen:
+ level_loading_screen.close()
+
+func _on_level_loader_levels_finished() -> void:
+ _load_win_screen_or_ending()
+
+func _on_level_loader_level_load_started() -> void:
+ if level_loading_screen:
+ level_loading_screen.reset()
+
+func _ready() -> void:
+ level_list_loader.level_loaded.connect(_on_level_loader_level_loaded)
+ level_list_loader.levels_finished.connect(_on_level_loader_levels_finished)
+ level_list_loader.level_load_started.connect(_on_level_loader_level_load_started)
+ if auto_load:
+ load_current_level()
diff --git a/addons/maaacks_game_template/extras/scripts/level_list_manager.gd.uid b/addons/maaacks_game_template/extras/scripts/level_list_manager.gd.uid
new file mode 100644
index 0000000..c7c8080
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/level_list_manager.gd.uid
@@ -0,0 +1 @@
+uid://yakisxcmflgk
diff --git a/addons/maaacks_game_template/extras/scripts/level_loader.gd b/addons/maaacks_game_template/extras/scripts/level_loader.gd
new file mode 100644
index 0000000..0e3bc9f
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/level_loader.gd
@@ -0,0 +1,52 @@
+@tool
+class_name LevelLoader
+extends Node
+## Loads scenes into a container.
+
+signal level_load_started
+signal level_loaded
+signal level_ready
+
+## Container where the level instance will be added.
+@export var level_container : Node
+## Loads a level on start.
+@export var auto_load : bool = false
+@export var current_level_path : String
+@export_group("Debugging")
+@export var force_level : String
+@export var current_level : Node
+
+var is_loading : bool = false
+
+func get_current_level_path() -> String:
+ return current_level_path if force_level.is_empty() else force_level
+
+func _attach_level(level_resource : Resource):
+ assert(level_container != null, "level_container is null")
+ var instance = level_resource.instantiate()
+ level_container.call_deferred("add_child", instance)
+ return instance
+
+func load_level(level_path : String = get_current_level_path()):
+ if is_loading : return
+ if is_instance_valid(current_level):
+ current_level.queue_free()
+ await current_level.tree_exited
+ current_level = null
+ is_loading = true
+ current_level_path = level_path
+ SceneLoader.load_scene(level_path, true)
+ level_load_started.emit()
+ await SceneLoader.scene_loaded
+ is_loading = false
+ current_level = _attach_level(SceneLoader.get_resource())
+ level_loaded.emit()
+ await current_level.ready
+ level_ready.emit()
+
+func reload_level():
+ load_level()
+
+func _ready():
+ if auto_load:
+ load_level()
diff --git a/addons/maaacks_game_template/extras/scripts/level_loader.gd.uid b/addons/maaacks_game_template/extras/scripts/level_loader.gd.uid
new file mode 100644
index 0000000..d3c169c
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/level_loader.gd.uid
@@ -0,0 +1 @@
+uid://bbymrin0cm704
diff --git a/addons/maaacks_game_template/extras/scripts/scene_lister.gd b/addons/maaacks_game_template/extras/scripts/scene_lister.gd
new file mode 100644
index 0000000..c2bb4c4
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/scene_lister.gd
@@ -0,0 +1,23 @@
+@tool
+extends Node
+class_name SceneLister
+## Helper class for listing all the scenes in a directory.
+
+## List of paths to scene files.
+## Prefilled in the editor by selecting a directory.
+@export var files : Array[String]
+## Prefill files with any scenes in the directory.
+@export_dir var directory : String :
+ set(value):
+ directory = value
+ _refresh_files()
+
+func _refresh_files():
+ if not is_inside_tree() or directory.is_empty(): return
+ var dir_access = DirAccess.open(directory)
+ if dir_access:
+ files.clear()
+ for file in dir_access.get_files():
+ if not file.ends_with(".tscn"):
+ continue
+ files.append(directory + "/" + file)
diff --git a/addons/maaacks_game_template/extras/scripts/scene_lister.gd.uid b/addons/maaacks_game_template/extras/scripts/scene_lister.gd.uid
new file mode 100644
index 0000000..ae7cd7f
--- /dev/null
+++ b/addons/maaacks_game_template/extras/scripts/scene_lister.gd.uid
@@ -0,0 +1 @@
+uid://wjq7li836lwj
diff --git a/addons/maaacks_game_template/installer/check_plugin_version.gd b/addons/maaacks_game_template/installer/check_plugin_version.gd
new file mode 100644
index 0000000..7bc603f
--- /dev/null
+++ b/addons/maaacks_game_template/installer/check_plugin_version.gd
@@ -0,0 +1,76 @@
+@tool
+extends Node
+
+signal new_version_detected(version: String)
+signal versions_matched
+signal failed
+
+const API_RELEASES_URL := "https://api.github.com/repos/%s/%s/releases"
+
+@export var plugin_directory : String
+@export var plugin_github_url : String :
+ set(value):
+ plugin_github_url = value
+ _update_urls()
+@export_group("Advanced")
+@export var auto_start : bool = false
+@export var default_version : String = "0.0.0"
+@export var replace_tag_name : String = "v"
+@export var _test_action : bool = false :
+ set(value):
+ if value and Engine.is_editor_hint():
+ compare_versions()
+
+@onready var _api_client : APIClient = $APIClient
+
+var _zipball_url : String
+
+func get_plugin_version() -> String :
+ if not plugin_directory.is_empty():
+ for enabled_plugin in ProjectSettings.get_setting("editor_plugins/enabled"):
+ if enabled_plugin.contains(plugin_directory):
+ var config := ConfigFile.new()
+ var error = config.load(enabled_plugin)
+ if error != OK:
+ return default_version
+ return config.get_value("plugin", "version", default_version)
+ return default_version
+
+func _update_urls() -> void:
+ if plugin_github_url.is_empty(): return
+ if _api_client == null: return
+ var regex := RegEx.create_from_string("https:\\/\\/github\\.com\\/([\\w-]+)\\/([\\w-]+)\\/*")
+ var regex_match := regex.search(plugin_github_url)
+ if regex_match == null: return
+ var username := regex_match.get_string(1)
+ var repository := regex_match.get_string(2)
+ _api_client.api_url = API_RELEASES_URL % [username, repository]
+
+func _on_api_client_request_failed(error) -> void:
+ failed.emit()
+ queue_free()
+
+func _on_api_client_response_received(response_body) -> void:
+ if response_body is not Array:
+ failed.emit()
+ queue_free()
+ return
+ var latest_release : Dictionary = response_body.front()
+ var tag_name := default_version
+ if latest_release.has("tag_name"):
+ tag_name = latest_release["tag_name"]
+ if replace_tag_name:
+ tag_name = tag_name.replacen(replace_tag_name, "")
+ var current_tag_name = get_plugin_version()
+ if tag_name != current_tag_name:
+ new_version_detected.emit(tag_name)
+ else:
+ versions_matched.emit()
+ queue_free()
+
+func compare_versions() -> void:
+ _api_client.request()
+
+func _ready() -> void:
+ if auto_start:
+ compare_versions()
diff --git a/addons/maaacks_game_template/installer/check_plugin_version.gd.uid b/addons/maaacks_game_template/installer/check_plugin_version.gd.uid
new file mode 100644
index 0000000..6211f05
--- /dev/null
+++ b/addons/maaacks_game_template/installer/check_plugin_version.gd.uid
@@ -0,0 +1 @@
+uid://ye1geusqp1gd
diff --git a/addons/maaacks_game_template/installer/check_plugin_version.tscn b/addons/maaacks_game_template/installer/check_plugin_version.tscn
new file mode 100644
index 0000000..01c7933
--- /dev/null
+++ b/addons/maaacks_game_template/installer/check_plugin_version.tscn
@@ -0,0 +1,16 @@
+[gd_scene load_steps=3 format=3 uid="uid://b5m61gpvjy1ao"]
+
+[ext_resource type="Script" uid="uid://ye1geusqp1gd" path="res://addons/maaacks_game_template/installer/check_plugin_version.gd" id="1_aqelj"]
+[ext_resource type="PackedScene" uid="uid://drhhakm62vjsy" path="res://addons/maaacks_game_template/base/scenes/utilities/api_client.tscn" id="2_5myc0"]
+
+[node name="CheckPluginVersion" type="Node"]
+script = ExtResource("1_aqelj")
+plugin_directory = "maaacks_game_template"
+plugin_github_url = "https://github.com/Maaack/Godot-Game-Template"
+
+[node name="APIClient" parent="." instance=ExtResource("2_5myc0")]
+api_url = "https://api.github.com/repos/Maaack/Godot-Game-Template/releases"
+request_method = 0
+
+[connection signal="request_failed" from="APIClient" to="." method="_on_api_client_request_failed"]
+[connection signal="response_received" from="APIClient" to="." method="_on_api_client_response_received"]
diff --git a/addons/maaacks_game_template/installer/copy_confirmation_dialog.tscn b/addons/maaacks_game_template/installer/copy_confirmation_dialog.tscn
new file mode 100644
index 0000000..909e871
--- /dev/null
+++ b/addons/maaacks_game_template/installer/copy_confirmation_dialog.tscn
@@ -0,0 +1,14 @@
+[gd_scene format=3 uid="uid://cyx4i4v30bw4o"]
+
+[node name="CopyConfirmationDialog" type="ConfirmationDialog"]
+title = "Copy Examples"
+initial_position = 2
+size = Vector2i(1024, 148)
+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"
diff --git a/addons/maaacks_game_template/installer/delete_examples_confirmation_dialog.tscn b/addons/maaacks_game_template/installer/delete_examples_confirmation_dialog.tscn
new file mode 100644
index 0000000..2cc1c83
--- /dev/null
+++ b/addons/maaacks_game_template/installer/delete_examples_confirmation_dialog.tscn
@@ -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(1024, 256)
+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"
diff --git a/addons/maaacks_game_template/installer/delete_examples_short_confirmation_dialog.tscn b/addons/maaacks_game_template/installer/delete_examples_short_confirmation_dialog.tscn
new file mode 100644
index 0000000..ea79a51
--- /dev/null
+++ b/addons/maaacks_game_template/installer/delete_examples_short_confirmation_dialog.tscn
@@ -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(1024, 128)
+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"
diff --git a/addons/maaacks_game_template/installer/destination_dialog.tscn b/addons/maaacks_game_template/installer/destination_dialog.tscn
new file mode 100644
index 0000000..9399873
--- /dev/null
+++ b/addons/maaacks_game_template/installer/destination_dialog.tscn
@@ -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(1024, 640)
+visible = true
+exclusive = false
+ok_button_text = "Select Current Folder"
+mode_overrides_title = false
+file_mode = 2
diff --git a/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.gd b/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.gd
new file mode 100644
index 0000000..4d79818
--- /dev/null
+++ b/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.gd
@@ -0,0 +1,12 @@
+@tool
+extends ConfirmationDialog
+
+const SHORT_DESCRIPTION : String = "Choose a style for icons in the input remapping menu. This style can be changed later."
+
+signal configuration_selected(index : int)
+
+func _on_item_list_item_selected(index) -> void:
+ configuration_selected.emit(index)
+
+func set_short_description() -> void:
+ %Label.text = SHORT_DESCRIPTION
diff --git a/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.gd.uid b/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.gd.uid
new file mode 100644
index 0000000..ec63fee
--- /dev/null
+++ b/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.gd.uid
@@ -0,0 +1 @@
+uid://bduy6qihnm0qo
diff --git a/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.tscn b/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.tscn
new file mode 100644
index 0000000..3a8cfca
--- /dev/null
+++ b/addons/maaacks_game_template/installer/kenney_input_prompts_dialog.tscn
@@ -0,0 +1,83 @@
+[gd_scene load_steps=14 format=3 uid="uid://d3x8m40qtdrj"]
+
+[ext_resource type="Script" uid="uid://bduy6qihnm0qo" path="res://addons/maaacks_game_template/installer/kenney_input_prompts_dialog.gd" id="1_nf1bc"]
+[ext_resource type="Texture2D" uid="uid://cmni5hv40bfaa" path="res://addons/maaacks_game_template/assets/input-icons/icons-filled-colored.png" id="2_0nqam"]
+[ext_resource type="Texture2D" uid="uid://deskx061vlcgx" path="res://addons/maaacks_game_template/assets/input-icons/icons-filled-white.png" id="3_ynuxh"]
+[ext_resource type="Texture2D" uid="uid://bohem6w6kcl3x" path="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-colored.png" id="4_dqbfh"]
+[ext_resource type="Texture2D" uid="uid://bq211jkfnm7k7" path="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-white.png" id="5_1tkva"]
+[ext_resource type="Texture2D" uid="uid://bt1yqttw3d5xn" path="res://addons/maaacks_game_template/assets/input-icons/icons-filled-colored-2x.png" id="6_r3yyh"]
+[ext_resource type="Texture2D" uid="uid://bit8o3p506th6" path="res://addons/maaacks_game_template/assets/input-icons/icons-filled-white-2x.png" id="7_xgp8o"]
+[ext_resource type="Texture2D" uid="uid://cqb86gp1gh3y8" path="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-2x.png" id="8_ag5dy"]
+[ext_resource type="Texture2D" uid="uid://d3bsc6o2ae88q" path="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-white-2x.png" id="9_3b8mx"]
+[ext_resource type="Texture2D" uid="uid://ix1d2e62f233" path="res://addons/maaacks_game_template/assets/input-icons/icons-filled-colored-vector.svg" id="10_ag5dy"]
+[ext_resource type="Texture2D" uid="uid://c37gofthe2bh3" path="res://addons/maaacks_game_template/assets/input-icons/icons-filled-white-vector.svg" id="11_3b8mx"]
+[ext_resource type="Texture2D" uid="uid://bsgf78aysgdnd" path="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-colored-vector.svg" id="12_rrkvx"]
+[ext_resource type="Texture2D" uid="uid://c1lpc33fpmd4p" path="res://addons/maaacks_game_template/assets/input-icons/icons-outlined-white-vector.svg" id="13_bkfjd"]
+
+[node name="KenneyInputPromptsDialog" type="ConfirmationDialog"]
+title = "Add Kenney Input Prompts Pack"
+initial_position = 2
+size = Vector2i(1024, 640)
+visible = true
+ok_button_text = "Yes"
+dialog_autowrap = true
+cancel_button_text = "No"
+script = ExtResource("1_nf1bc")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+custom_minimum_size = Vector2(560, 443)
+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
+theme_override_constants/separation = 16
+
+[node name="Label" type="Label" parent="VBoxContainer"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(384, 0)
+layout_mode = 2
+text = "Would you like to install Kenney's Input Prompts?
+
+This adds icons for a majority of input keys and devices in the input remapping menu. They are Creative Commons Zero (CC0) licensed, about 3.9 MB in size (7.6 MB with *.import files), and get installed into the assets folder.
+
+Choose a style for icons in the input remapping menu. The style can be changed later."
+autowrap_mode = 3
+
+[node name="ItemList" type="ItemList" parent="VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+item_count = 12
+same_column_width = true
+item_0/text = "Filled and Colored Vector"
+item_0/icon = ExtResource("10_ag5dy")
+item_1/text = "Filled and White Vector"
+item_1/icon = ExtResource("11_3b8mx")
+item_2/text = "Outlined and Colored Vector"
+item_2/icon = ExtResource("12_rrkvx")
+item_3/text = "Outlined and White Vector"
+item_3/icon = ExtResource("13_bkfjd")
+item_4/text = "Filled and Colored 64x64"
+item_4/icon = ExtResource("2_0nqam")
+item_5/text = "Filled and White 64x64"
+item_5/icon = ExtResource("3_ynuxh")
+item_6/text = "Outlined and Colored 64x64"
+item_6/icon = ExtResource("4_dqbfh")
+item_7/text = "Outlined and White 64x64"
+item_7/icon = ExtResource("5_1tkva")
+item_8/text = "Filled and Colored 128x128"
+item_8/icon = ExtResource("6_r3yyh")
+item_9/text = "Filled and White 128x128"
+item_9/icon = ExtResource("7_xgp8o")
+item_10/text = "Outlined and Colored 128x128"
+item_10/icon = ExtResource("8_ag5dy")
+item_11/text = "Outlined and White 128x128"
+item_11/icon = ExtResource("9_3b8mx")
+
+[connection signal="item_selected" from="VBoxContainer/ItemList" to="." method="_on_item_list_item_selected"]
diff --git a/addons/maaacks_game_template/installer/kenney_input_prompts_installer.gd b/addons/maaacks_game_template/installer/kenney_input_prompts_installer.gd
new file mode 100644
index 0000000..161d9f9
--- /dev/null
+++ b/addons/maaacks_game_template/installer/kenney_input_prompts_installer.gd
@@ -0,0 +1,342 @@
+@tool
+## Tool for installing icons and setting up the configuration of the input icon mapper.
+extends Node
+
+## Sent when the user selects to cancel the installation process.
+signal canceled
+## Sent when the installation process has completed.
+signal completed
+
+const REIMPORT_CHECK_DELAY : float = 0.5
+const OPEN_SCENE_DELAY : float = 0.5
+const REGEX_PREFIX = """\\[node name="InputIconMapper" parent="." index="0"\\][\\s\\S]*"""
+
+const FILLED_WHITE_CONFIGURATION = """
+[node name="InputIconMapper" parent="." index="0"]
+replace_strings = {
+"Capslock": "Caps Lock",
+"Generic Stick": "Generic Left Stick",
+"Guide": "Home",
+"Slash Back": "Back Slash",
+"Slash Forward": "Slash",
+"Stick L": "Left Stick",
+"Stick R": "Right Stick",
+"Trigger L 1": "Left Shoulder",
+"Trigger L 2": "Left Trigger",
+"Trigger R 1": "Right Shoulder",
+"Trigger R 2": "Right Trigger"
+}
+filtered_strings = Array[String](["keyboard", "color", "button", "arrow"])
+directories = Array[String](["res://assets/kenney_input-prompts/Keyboard & Mouse/Default", "res://assets/kenney_input-prompts/Generic/Default", "res://assets/kenney_input-prompts/Xbox Series/Default", "res://assets/kenney_input-prompts/PlayStation Series/Default", "res://assets/kenney_input-prompts/Nintendo Switch/Default", "res://assets/kenney_input-prompts/Steam Deck/Default"])
+filter = "color"
+ends_with = ".png"
+not_ends_with = "outline.png"
+"""
+const FILLED_COLOR_CONFIGURATION = """
+[node name="InputIconMapper" parent="." index="0"]
+prioritized_strings = Array[String](["color"])
+replace_strings = {
+"Capslock": "Caps Lock",
+"Generic Stick": "Generic Left Stick",
+"Guide": "Home",
+"Slash Back": "Back Slash",
+"Slash Forward": "Slash",
+"Stick L": "Left Stick",
+"Stick R": "Right Stick",
+"Trigger L 1": "Left Shoulder",
+"Trigger L 2": "Left Trigger",
+"Trigger R 1": "Right Shoulder",
+"Trigger R 2": "Right Trigger"
+}
+filtered_strings = Array[String](["keyboard", "color", "button", "arrow"])
+directories = Array[String](["res://assets/kenney_input-prompts/Keyboard & Mouse/Default", "res://assets/kenney_input-prompts/Generic/Default", "res://assets/kenney_input-prompts/Xbox Series/Default", "res://assets/kenney_input-prompts/PlayStation Series/Default", "res://assets/kenney_input-prompts/Nintendo Switch/Default", "res://assets/kenney_input-prompts/Steam Deck/Default"])
+ends_with = ".png"
+not_ends_with = "outline.png"
+"""
+const OUTLINED_WHITE_CONFIGURATION = """
+[node name="InputIconMapper" parent="." index="0"]
+prioritized_strings = Array[String](["outline"])
+replace_strings = {
+"Capslock": "Caps Lock",
+"Generic Stick": "Generic Left Stick",
+"Guide": "Home",
+"Slash Back": "Back Slash",
+"Slash Forward": "Slash",
+"Stick L": "Left Stick",
+"Stick R": "Right Stick",
+"Trigger L 1": "Left Shoulder",
+"Trigger L 2": "Left Trigger",
+"Trigger R 1": "Right Shoulder",
+"Trigger R 2": "Right Trigger"
+}
+filtered_strings = Array[String](["keyboard", "color", "button", "arrow", "outline"])
+directories = Array[String](["res://assets/kenney_input-prompts/Keyboard & Mouse/Default", "res://assets/kenney_input-prompts/Generic/Default", "res://assets/kenney_input-prompts/Xbox Series/Default", "res://assets/kenney_input-prompts/PlayStation Series/Default", "res://assets/kenney_input-prompts/Nintendo Switch/Default", "res://assets/kenney_input-prompts/Steam Deck/Default"])
+filter = "color"
+ends_with = ".png"
+"""
+const OUTLINED_COLOR_CONFIGURATION = """
+[node name="InputIconMapper" parent="." index="0"]
+prioritized_strings = Array[String](["outline", "color"])
+replace_strings = {
+"Capslock": "Caps Lock",
+"Generic Stick": "Generic Left Stick",
+"Guide": "Home",
+"Slash Back": "Back Slash",
+"Slash Forward": "Slash",
+"Stick L": "Left Stick",
+"Stick R": "Right Stick",
+"Trigger L 1": "Left Shoulder",
+"Trigger L 2": "Left Trigger",
+"Trigger R 1": "Right Shoulder",
+"Trigger R 2": "Right Trigger"
+}
+filtered_strings = Array[String](["keyboard", "color", "button", "arrow", "outline"])
+directories = Array[String](["res://assets/kenney_input-prompts/Keyboard & Mouse/Default", "res://assets/kenney_input-prompts/Generic/Default", "res://assets/kenney_input-prompts/Xbox Series/Default", "res://assets/kenney_input-prompts/PlayStation Series/Default", "res://assets/kenney_input-prompts/Nintendo Switch/Default", "res://assets/kenney_input-prompts/Steam Deck/Default"])
+ends_with = ".png"
+"""
+
+const PACKAGE_EXTRA_DIRECTORIES := [
+ "Flairs",
+ "Nintendo Gamecube",
+ "Nintendo Switch 2",
+ "Nintendo Wii",
+ "Nintendo WiiU",
+ "Playdate",
+ "Steam Controller",
+ "Touch",
+]
+
+const PACKAGE_EXTRA_FILES := [
+ "Preview",
+]
+
+## Path start where the project examples have been copied.
+@export_dir var copy_dir_path : String
+## Path end where the zipped files are to be extracted.
+@export var extract_extension : String
+
+@onready var _download_and_extract_node : DownloadAndExtract = $DownloadAndExtract
+@onready var _skip_installation_dialog : ConfirmationDialog = $SkipInstallationDialog
+@onready var _kenney_input_prompts_dialog : ConfirmationDialog = $KenneyInputPromptsDialog
+@onready var _installing_dialog : AcceptDialog = $InstallingDialog
+@onready var _clean_up_dialog : ConfirmationDialog = $CleanUpDialog
+@onready var _error_dialog : AcceptDialog = $ErrorDialog
+@onready var _stage_label : Label = %StageLabel
+@onready var _progress_bar : ProgressBar = %ProgressBar
+
+var _configuration_index : int = -1
+## State flag of whether the tool is waiting for the filesystem to finish scanning.
+var scanning : bool = false
+## State flag for whether the tool is waiting for the filesystem to finish reimporting.
+var reimporting : bool = false
+## Flag for whether the tool will force a download and extraction, even if the contents exist.
+var force : bool = false
+
+func _download_and_extract() -> void:
+ _installing_dialog.show()
+ _download_and_extract_node.run.call_deferred()
+
+func _run_complete() -> void:
+ completed.emit()
+ queue_free()
+
+func _clean_up_or_complete() -> void:
+ if _has_extras():
+ _clean_up_dialog.show()
+ else:
+ _run_complete()
+
+func _process(_delta : float) -> void:
+ if _installing_dialog.visible:
+ _progress_bar.value = _download_and_extract_node.get_progress()
+ match _download_and_extract_node.stage:
+ DownloadAndExtract.Stage.DOWNLOAD:
+ _stage_label.text = "Downloading..."
+ DownloadAndExtract.Stage.SAVE:
+ _stage_label.text = "Saving..."
+ DownloadAndExtract.Stage.EXTRACT:
+ _stage_label.text = "Extracting..."
+ DownloadAndExtract.Stage.DELETE:
+ _stage_label.text = "Cleaning up..."
+ DownloadAndExtract.Stage.NONE:
+ _installing_dialog.hide()
+ elif scanning:
+ var file_system := EditorInterface.get_resource_filesystem()
+ if not file_system.is_scanning():
+ scanning = false
+ await get_tree().create_timer(REIMPORT_CHECK_DELAY).timeout
+ if reimporting:
+ await file_system.resources_reimported
+ reimporting = false
+ _configure_and_complete()
+
+func _delete_recursive(path : String) -> void:
+ if not path.ends_with("/"):
+ path += "/"
+ var dir_access := DirAccess.open(path)
+ if dir_access == null:
+ return
+ var directories := dir_access.get_directories()
+ for directory in directories:
+ _delete_recursive(path + directory)
+ DirAccess.remove_absolute(path + directory)
+ var files := dir_access.get_files()
+ for file in files:
+ DirAccess.remove_absolute(path + file)
+
+func get_full_path() -> String:
+ var full_path := copy_dir_path
+ if not full_path.ends_with("/"):
+ full_path += "/"
+ full_path += extract_extension
+ if not full_path.ends_with("/"):
+ full_path += "/"
+ return full_path
+
+func _has_extras() -> bool:
+ var full_path := get_full_path()
+ var directories := DirAccess.get_directories_at(full_path)
+ for directory in directories:
+ for key in PACKAGE_EXTRA_DIRECTORIES:
+ if directory.contains(key):
+ return true
+ var files := DirAccess.get_files_at(full_path)
+ for file in files:
+ for key in PACKAGE_EXTRA_FILES:
+ if file.contains(key):
+ return true
+ return false
+
+func _delete_extras() -> void:
+ var full_path := get_full_path()
+ var directories := DirAccess.get_directories_at(full_path)
+ for directory in directories:
+ for key in PACKAGE_EXTRA_DIRECTORIES:
+ if directory.contains(key):
+ _delete_recursive(full_path + directory)
+ DirAccess.remove_absolute(full_path + directory)
+ continue
+ var files := DirAccess.get_files_at(full_path)
+ for file in files:
+ for key in PACKAGE_EXTRA_FILES:
+ if file.contains(key):
+ DirAccess.remove_absolute(full_path + file)
+ continue
+ EditorInterface.get_resource_filesystem().scan()
+
+func _configure_icons() -> void:
+ var input_options_menu_path := copy_dir_path + "scenes/menus/options_menu/input/input_options_menu.tscn"
+ var input_options_menu := FileAccess.get_file_as_string(input_options_menu_path)
+ var regex := RegEx.new()
+ regex.compile(REGEX_PREFIX + """\\[node""")
+ var result = regex.sub(input_options_menu, "[node")
+ if result == input_options_menu:
+ regex.clear()
+ regex.compile(REGEX_PREFIX)
+ result = regex.sub(input_options_menu, "")
+ input_options_menu = result
+ match(_configuration_index % 4):
+ 0:
+ input_options_menu += FILLED_COLOR_CONFIGURATION
+ 1:
+ input_options_menu += FILLED_WHITE_CONFIGURATION
+ 2:
+ input_options_menu += OUTLINED_COLOR_CONFIGURATION
+ 3:
+ input_options_menu += OUTLINED_WHITE_CONFIGURATION
+ match(_configuration_index / 4):
+ 0:
+ input_options_menu = input_options_menu.replace("Default", "Vector").replace(".png", ".svg")
+ 1:
+ pass
+ 2:
+ input_options_menu = input_options_menu.replace("Default", "Double")
+ var file_rewrite := FileAccess.open(input_options_menu_path, FileAccess.WRITE)
+ file_rewrite.store_string(input_options_menu)
+ file_rewrite.close()
+ if input_options_menu_path in EditorInterface.get_open_scenes():
+ EditorInterface.reload_scene_from_path(input_options_menu_path)
+ else:
+ EditorInterface.open_scene_from_path(input_options_menu_path)
+ await get_tree().create_timer(OPEN_SCENE_DELAY).timeout
+ EditorInterface.save_scene()
+ await get_tree().create_timer(REIMPORT_CHECK_DELAY).timeout
+ _clean_up_or_complete()
+
+func _configure_and_complete() -> void:
+ if _configuration_index >= 0:
+ _configure_icons()
+ return
+ _clean_up_or_complete()
+
+func _scan_filesystem_and_reimport() -> void:
+ var file_system := EditorInterface.get_resource_filesystem()
+ file_system.scan()
+ scanning = true
+ await file_system.resources_reimporting
+ reimporting = true
+
+func _enable_forced_install() -> void:
+ force = true
+ _download_and_extract_node.force = true
+ _kenney_input_prompts_dialog.show.call_deferred()
+
+func _enable_skipped_install() -> void:
+ _kenney_input_prompts_dialog.set_short_description()
+ _kenney_input_prompts_dialog.show.call_deferred()
+
+func _show_error_dialog(error : String) -> void:
+ _installing_dialog.hide()
+ _error_dialog.show()
+ _error_dialog.dialog_text = "%s!" % error
+
+func _ready() -> void:
+ _skip_installation_dialog.hide()
+ _kenney_input_prompts_dialog.hide()
+ _installing_dialog.hide()
+ _installing_dialog.get_ok_button().hide()
+ _clean_up_dialog.hide()
+ _error_dialog.hide()
+ _download_and_extract_node.extract_path = get_full_path()
+ if _download_and_extract_node.extract_path_exists():
+ _skip_installation_dialog.show()
+ else:
+ _kenney_input_prompts_dialog.show()
+
+func _on_kenney_input_prompts_dialog_canceled() -> void:
+ canceled.emit()
+ queue_free()
+
+func _on_kenney_input_prompts_dialog_configuration_selected(index: int) -> void:
+ _configuration_index = index
+
+func _on_kenney_input_prompts_dialog_confirmed() -> void:
+ if _download_and_extract_node.extract_path_exists() and not force:
+ _configure_and_complete()
+ return
+ _download_and_extract()
+
+func _on_skip_installation_dialog_canceled() -> void:
+ _enable_forced_install()
+
+func _on_skip_installation_dialog_confirmed() -> void:
+ _enable_skipped_install()
+
+func _on_error_dialog_confirmed() -> void:
+ queue_free()
+
+func _on_error_dialog_canceled() -> void:
+ queue_free()
+
+func _on_download_and_extract_run_completed() -> void:
+ _scan_filesystem_and_reimport()
+
+func _on_download_and_extract_run_failed(error : String) -> void:
+ _show_error_dialog(error)
+
+func _on_clean_up_dialog_confirmed() -> void:
+ _delete_extras()
+ _run_complete()
+
+func _on_clean_up_dialog_canceled() -> void:
+ _run_complete()
diff --git a/addons/maaacks_game_template/installer/kenney_input_prompts_installer.gd.uid b/addons/maaacks_game_template/installer/kenney_input_prompts_installer.gd.uid
new file mode 100644
index 0000000..c6fb9c6
--- /dev/null
+++ b/addons/maaacks_game_template/installer/kenney_input_prompts_installer.gd.uid
@@ -0,0 +1 @@
+uid://ca36dy2vkk46q
diff --git a/addons/maaacks_game_template/installer/kenney_input_prompts_installer.tscn b/addons/maaacks_game_template/installer/kenney_input_prompts_installer.tscn
new file mode 100644
index 0000000..cb88122
--- /dev/null
+++ b/addons/maaacks_game_template/installer/kenney_input_prompts_installer.tscn
@@ -0,0 +1,92 @@
+[gd_scene load_steps=4 format=3 uid="uid://d27twrcwmfxrs"]
+
+[ext_resource type="Script" uid="uid://ca36dy2vkk46q" path="res://addons/maaacks_game_template/installer/kenney_input_prompts_installer.gd" id="1_ebstj"]
+[ext_resource type="PackedScene" uid="uid://d3x8m40qtdrj" path="res://addons/maaacks_game_template/installer/kenney_input_prompts_dialog.tscn" id="1_pslk0"]
+[ext_resource type="PackedScene" uid="uid://dlkmofxhavh10" path="res://addons/maaacks_game_template/base/scenes/utilities/download_and_extract.tscn" id="3_ebstj"]
+
+[node name="KenneyInputPromptsInstaller" type="Node"]
+script = ExtResource("1_ebstj")
+copy_dir_path = "res://"
+extract_extension = "assets/kenney_input-prompts"
+
+[node name="DownloadAndExtract" parent="." instance=ExtResource("3_ebstj")]
+zip_url = "https://github.com/Maaack/Kenney-Input-Prompts/archive/refs/tags/1.3.zip"
+extract_path = "res://assets/kenney_input-prompts/"
+skip_base_zip_dir = true
+zip_file_path = "res://kenney_input-prompts.zip"
+metadata/_custom_type_script = "uid://bqu3bc0tttrfk"
+
+[node name="SkipInstallationDialog" type="ConfirmationDialog" parent="."]
+title = "Skip Installation?"
+initial_position = 2
+size = Vector2i(682, 160)
+ok_button_text = "Skip"
+dialog_text = "The input prompts pack appears to already be installed.
+
+Do you want to force a reinstall of the pack, or skip to picking a style?"
+cancel_button_text = "Reinstall"
+
+[node name="KenneyInputPromptsDialog" parent="." instance=ExtResource("1_pslk0")]
+visible = false
+
+[node name="InstallingDialog" type="AcceptDialog" parent="."]
+title = "Installing..."
+initial_position = 2
+size = Vector2i(400, 100)
+
+[node name="MarginContainer" type="MarginContainer" parent="InstallingDialog"]
+offset_left = 4.0
+offset_top = 4.0
+offset_right = 396.0
+offset_bottom = 96.0
+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="VBoxContainer" type="VBoxContainer" parent="InstallingDialog/MarginContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="StageLabel" type="Label" parent="InstallingDialog/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="ProgressBar" type="ProgressBar" parent="InstallingDialog/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+max_value = 1.0
+step = 0.001
+
+[node name="CleanUpDialog" type="ConfirmationDialog" parent="."]
+auto_translate_mode = 1
+title = "Clean Up Extra Content?"
+initial_position = 2
+size = Vector2i(1024, 210)
+ok_button_text = "Yes"
+dialog_text = "Kenney's Input Prompts contains extra content not used by the input remapping menu.
+
+This includes icons for devices not currently detected and preview images. Removing the extras cuts the total size of extracted assets by almost 50%. The option to change input icon styles will remain available after the clean up, too.
+
+Would you like to have the extra content removed?"
+dialog_autowrap = true
+cancel_button_text = "No"
+
+[node name="ErrorDialog" type="AcceptDialog" parent="."]
+title = "Error!"
+initial_position = 2
+size = Vector2i(400, 128)
+
+[connection signal="run_completed" from="DownloadAndExtract" to="." method="_on_download_and_extract_run_completed"]
+[connection signal="run_failed" from="DownloadAndExtract" to="." method="_on_download_and_extract_run_failed"]
+[connection signal="canceled" from="SkipInstallationDialog" to="." method="_on_skip_installation_dialog_canceled"]
+[connection signal="confirmed" from="SkipInstallationDialog" to="." method="_on_skip_installation_dialog_confirmed"]
+[connection signal="canceled" from="KenneyInputPromptsDialog" to="." method="_on_kenney_input_prompts_dialog_canceled"]
+[connection signal="configuration_selected" from="KenneyInputPromptsDialog" to="." method="_on_kenney_input_prompts_dialog_configuration_selected"]
+[connection signal="confirmed" from="KenneyInputPromptsDialog" to="." method="_on_kenney_input_prompts_dialog_confirmed"]
+[connection signal="canceled" from="CleanUpDialog" to="." method="_on_clean_up_dialog_canceled"]
+[connection signal="confirmed" from="CleanUpDialog" to="." method="_on_clean_up_dialog_confirmed"]
+[connection signal="canceled" from="ErrorDialog" to="." method="_on_error_dialog_canceled"]
+[connection signal="confirmed" from="ErrorDialog" to="." method="_on_error_dialog_confirmed"]
diff --git a/addons/maaacks_game_template/installer/main_scene_confirmation_dialog.tscn b/addons/maaacks_game_template/installer/main_scene_confirmation_dialog.tscn
new file mode 100644
index 0000000..a195f68
--- /dev/null
+++ b/addons/maaacks_game_template/installer/main_scene_confirmation_dialog.tscn
@@ -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(1024, 192)
+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"
diff --git a/addons/maaacks_game_template/installer/override.cfg b/addons/maaacks_game_template/installer/override.cfg
new file mode 100644
index 0000000..e7ecedc
--- /dev/null
+++ b/addons/maaacks_game_template/installer/override.cfg
@@ -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":4,"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)
+]
+}
diff --git a/addons/maaacks_game_template/installer/play_opening_confirmation_dialog.tscn b/addons/maaacks_game_template/installer/play_opening_confirmation_dialog.tscn
new file mode 100644
index 0000000..b7bea2b
--- /dev/null
+++ b/addons/maaacks_game_template/installer/play_opening_confirmation_dialog.tscn
@@ -0,0 +1,13 @@
+[gd_scene format=3 uid="uid://b8808yj7a0ghj"]
+
+[node name="PlayOpeningConfirmationDialog" type="ConfirmationDialog"]
+title = "Run & Test"
+initial_position = 2
+size = Vector2i(1024, 148)
+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"
diff --git a/addons/maaacks_game_template/installer/theme_selection_dialog.gd b/addons/maaacks_game_template/installer/theme_selection_dialog.gd
new file mode 100644
index 0000000..db6beb7
--- /dev/null
+++ b/addons/maaacks_game_template/installer/theme_selection_dialog.gd
@@ -0,0 +1,33 @@
+@tool
+extends ConfirmationDialog
+
+signal theme_selected(theme_file: String)
+
+@export_dir var theme_directories : Array[String] :
+ set(value):
+ theme_directories = value
+ if is_inside_tree():
+ %FileLister.directories = theme_directories
+ _fill_with_themes()
+
+func _fill_with_themes() -> void:
+ %ItemList.clear()
+ for file in %FileLister.files:
+ if file is String:
+ var readable_name = file.get_file().get_basename().capitalize()
+ %ItemList.add_item(readable_name)
+
+func _ready() -> void:
+ get_ok_button().disabled = true
+
+func _preview_theme(theme_file: String) -> void:
+ var theme_resource : Theme = load(theme_file)
+ if theme_resource == null: return
+ %ThemePreviewContainer.theme = theme_resource
+
+func _on_item_list_item_selected(index) -> void:
+ get_ok_button().disabled = false
+ if index < %FileLister.files.size():
+ var file = %FileLister.files[index]
+ _preview_theme(file)
+ theme_selected.emit(file)
diff --git a/addons/maaacks_game_template/installer/theme_selection_dialog.gd.uid b/addons/maaacks_game_template/installer/theme_selection_dialog.gd.uid
new file mode 100644
index 0000000..61f64df
--- /dev/null
+++ b/addons/maaacks_game_template/installer/theme_selection_dialog.gd.uid
@@ -0,0 +1 @@
+uid://c6p8xjvlrgsfk
diff --git a/addons/maaacks_game_template/installer/theme_selection_dialog.tscn b/addons/maaacks_game_template/installer/theme_selection_dialog.tscn
new file mode 100644
index 0000000..9e3e2dd
--- /dev/null
+++ b/addons/maaacks_game_template/installer/theme_selection_dialog.tscn
@@ -0,0 +1,178 @@
+[gd_scene load_steps=3 format=3 uid="uid://d25mdvmlbn35"]
+
+[ext_resource type="Script" uid="uid://c6p8xjvlrgsfk" path="res://addons/maaacks_game_template/installer/theme_selection_dialog.gd" id="1_5u0gx"]
+[ext_resource type="Script" uid="uid://bij7wsh8d44gv" path="res://addons/maaacks_game_template/base/scripts/file_lister.gd" id="2_luhgx"]
+
+[node name="ThemeSelectionDialog" type="ConfirmationDialog"]
+title = "Use a Starter Theme"
+initial_position = 2
+size = Vector2i(1024, 704)
+visible = true
+ok_button_text = "Yes"
+dialog_autowrap = true
+cancel_button_text = "No"
+script = ExtResource("1_5u0gx")
+
+[node name="FileLister" type="Node" parent="."]
+unique_name_in_owner = true
+script = ExtResource("2_luhgx")
+ends_with = ".tres"
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+custom_minimum_size = Vector2(560, 443)
+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
+theme_override_constants/separation = 16
+
+[node name="Label" type="Label" parent="VBoxContainer"]
+custom_minimum_size = Vector2(384, 0)
+layout_mode = 2
+text = "A custom theme was not detected for the project. Starter options are provided below. These can be customized as needed.
+
+Requires restarting the editor to take full effect."
+autowrap_mode = 3
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="ItemList" type="ItemList" parent="VBoxContainer/HBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="VSeparator" type="VSeparator" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 16
+theme_override_constants/margin_right = 16
+
+[node name="ThemePreviewContainer" type="TabContainer" parent="VBoxContainer/HBoxContainer/MarginContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+current_tab = 0
+
+[node name="Tab1" type="Control" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer"]
+layout_mode = 2
+metadata/_tab_index = 0
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 8
+theme_override_constants/margin_bottom = 8
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "Label"
+horizontal_alignment = 1
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="Button" type="Button" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Button"
+
+[node name="Button2" type="Button" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+toggle_mode = true
+button_pressed = true
+text = "Button"
+
+[node name="Button3" type="Button" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+disabled = true
+text = "Button"
+
+[node name="CheckButton" type="CheckButton" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "CheckButton"
+
+[node name="CheckBox" type="CheckBox" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "CheckBox"
+
+[node name="MenuButton" type="MenuButton" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "MenuButton"
+
+[node name="OptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab1/MarginContainer/VBoxContainer"]
+layout_mode = 2
+selected = 0
+item_count = 2
+popup/item_0/text = "OptionButton"
+popup/item_0/id = 0
+popup/item_1/text = "OptionButton2"
+popup/item_1/id = 1
+
+[node name="Tab2" type="Control" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer"]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 1
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_top = 8
+theme_override_constants/margin_right = 8
+theme_override_constants/margin_bottom = 8
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2/MarginContainer"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+text = "Another label"
+horizontal_alignment = 1
+
+[node name="LineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="TextEdit" type="TextEdit" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="HSlider" type="HSlider" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="HScrollBar" type="HScrollBar" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="HSeparator" type="HSeparator" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer/HBoxContainer/MarginContainer/ThemePreviewContainer/Tab2/MarginContainer/VBoxContainer"]
+layout_mode = 2
+value = 50.0
+
+[connection signal="item_selected" from="VBoxContainer/HBoxContainer/ItemList" to="." method="_on_item_list_item_selected"]
diff --git a/addons/maaacks_game_template/installer/update_plugin.gd b/addons/maaacks_game_template/installer/update_plugin.gd
new file mode 100644
index 0000000..4f656c4
--- /dev/null
+++ b/addons/maaacks_game_template/installer/update_plugin.gd
@@ -0,0 +1,153 @@
+@tool
+extends Node
+
+signal update_completed
+
+const API_RELEASES_URL := "https://api.github.com/repos/%s/%s/releases"
+const UPDATE_CONFIRMATION_MESSAGE := "This will update the contents of the plugin folder (addons/%s/).\nFiles outside of the plugin folder will not be affected.\n\nUpdate %s to v%s?"
+const PLUGIN_EXTRACT_PATH := "res://addons/%s/"
+const PLUGIN_TEMP_ZIP_PATH := "res://%s_%s_update.zip"
+
+@export var plugin_directory : String
+@export var plugin_github_url : String :
+ set(value):
+ plugin_github_url = value
+ _update_urls()
+@export_group("Advanced")
+@export var auto_start : bool = false
+@export var default_version : String = "0.0.0"
+@export var replace_tag_name : String = "v"
+@export var _test_action : bool = false :
+ set(value):
+ if value and Engine.is_editor_hint():
+ get_newest_version()
+
+@onready var _api_client : APIClient = $APIClient
+@onready var _download_and_extract_node : DownloadAndExtract = $DownloadAndExtract
+@onready var _update_confirmation_dialog : ConfirmationDialog = $UpdateConfirmationDialog
+@onready var _installing_dialog : AcceptDialog = $InstallingDialog
+@onready var _error_dialog : AcceptDialog = $ErrorDialog
+@onready var _success_dialog : AcceptDialog = $SuccessDialog
+@onready var _release_label : RichTextLabel = %ReleaseLabel
+@onready var _update_label : Label = %UpdateLabel
+@onready var _release_notes_button : LinkButton = %ReleaseNotesButton
+@onready var _release_notes_panel : Panel = %ReleaseNotesPanel
+@onready var _stage_label : Label = %StageLabel
+@onready var _progress_bar : ProgressBar = %ProgressBar
+
+var _zipball_url : String
+var _newest_version : String
+var _plugin_name : String
+var _current_plugin_version : String
+
+func _load_plugin_details() -> void:
+ if plugin_directory.is_empty(): return
+ for enabled_plugin in ProjectSettings.get_setting("editor_plugins/enabled"):
+ if enabled_plugin.contains(plugin_directory):
+ var config := ConfigFile.new()
+ var error = config.load(enabled_plugin)
+ if error != OK:
+ return
+ _current_plugin_version = config.get_value("plugin", "version", default_version)
+ _plugin_name = config.get_value("plugin", "name", "Plugin")
+
+func _update_urls() -> void:
+ if plugin_github_url.is_empty(): return
+ if _api_client == null: return
+ var regex := RegEx.create_from_string("https:\\/\\/github\\.com\\/([\\w-]+)\\/([\\w-]+)\\/*")
+ var regex_match := regex.search(plugin_github_url)
+ if regex_match == null: return
+ var username := regex_match.get_string(1)
+ var repository := regex_match.get_string(2)
+ _api_client.api_url = API_RELEASES_URL % [username, repository]
+
+func _show_error_dialog(error : String) -> void:
+ _error_dialog.show()
+ _error_dialog.dialog_text = "%s!" % error
+
+func _show_success_dialog() -> void:
+ _success_dialog.show()
+ _success_dialog.dialog_text = "%s updated to v%s." % [_plugin_name, _newest_version]
+
+func _on_api_client_request_failed(error : String) -> void:
+ _show_error_dialog(error)
+
+func _on_api_client_response_received(response_body : Variant) -> void:
+ if response_body is not Array:
+ push_error("Response was not an array")
+ return
+ var latest_release : Dictionary = response_body.front()
+ _newest_version = default_version
+ if latest_release.has("tag_name"):
+ var tag_name = latest_release["tag_name"]
+ if replace_tag_name:
+ tag_name = tag_name.replacen(replace_tag_name, "")
+ _newest_version = tag_name
+ if latest_release.has("zipball_url"):
+ _zipball_url = latest_release["zipball_url"]
+ _download_and_extract_node.zip_url = _zipball_url
+ _download_and_extract_node.zip_file_path = PLUGIN_TEMP_ZIP_PATH % [plugin_directory, _newest_version]
+ _update_label.text = UPDATE_CONFIRMATION_MESSAGE % [plugin_directory, _plugin_name, _newest_version]
+ if latest_release.has("body"):
+ _release_label.text = latest_release["body"]
+ _update_confirmation_dialog.show()
+
+func _on_download_and_extract_zip_saved() -> void:
+ OS.move_to_trash(ProjectSettings.globalize_path(PLUGIN_EXTRACT_PATH % plugin_directory))
+
+func _on_download_and_extract_run_failed(error : String) -> void:
+ _show_error_dialog(error)
+
+func _on_download_and_extract_run_completed() -> void:
+ update_completed.emit()
+ _show_success_dialog()
+
+func _on_error_dialog_canceled() -> void:
+ queue_free()
+
+func _on_error_dialog_confirmed() -> void:
+ queue_free()
+
+func _on_success_dialog_canceled() -> void:
+ queue_free()
+
+func _on_success_dialog_confirmed() -> void:
+ queue_free()
+
+func _on_update_confirmation_dialog_canceled() -> void:
+ queue_free()
+
+func _on_update_confirmation_dialog_confirmed() -> void:
+ _download_and_extract_node.run()
+ _installing_dialog.show()
+
+func _on_release_notes_button_pressed() -> void:
+ _release_notes_panel.show()
+ _release_notes_button.hide()
+
+func get_newest_version() -> void:
+ _api_client.request()
+
+func _ready() -> void:
+ _load_plugin_details()
+ _update_confirmation_dialog.hide()
+ _installing_dialog.hide()
+ _error_dialog.hide()
+ _success_dialog.hide()
+ if auto_start:
+ get_newest_version()
+
+func _process(_delta : float) -> void:
+ if _installing_dialog.visible:
+ _progress_bar.value = _download_and_extract_node.get_progress()
+ match _download_and_extract_node.stage:
+ DownloadAndExtract.Stage.DOWNLOAD:
+ _stage_label.text = "Downloading..."
+ DownloadAndExtract.Stage.SAVE:
+ _stage_label.text = "Saving..."
+ DownloadAndExtract.Stage.EXTRACT:
+ _stage_label.text = "Extracting..."
+ DownloadAndExtract.Stage.DELETE:
+ _stage_label.text = "Cleaning up..."
+ DownloadAndExtract.Stage.NONE:
+ _installing_dialog.hide()
diff --git a/addons/maaacks_game_template/installer/update_plugin.gd.uid b/addons/maaacks_game_template/installer/update_plugin.gd.uid
new file mode 100644
index 0000000..fc72e0a
--- /dev/null
+++ b/addons/maaacks_game_template/installer/update_plugin.gd.uid
@@ -0,0 +1 @@
+uid://cwj8dpqveao6o
diff --git a/addons/maaacks_game_template/installer/update_plugin.tscn b/addons/maaacks_game_template/installer/update_plugin.tscn
new file mode 100644
index 0000000..b330c71
--- /dev/null
+++ b/addons/maaacks_game_template/installer/update_plugin.tscn
@@ -0,0 +1,128 @@
+[gd_scene load_steps=4 format=3 uid="uid://gynblau0ojia"]
+
+[ext_resource type="Script" uid="uid://cwj8dpqveao6o" path="res://addons/maaacks_game_template/installer/update_plugin.gd" id="1_s6qpc"]
+[ext_resource type="PackedScene" uid="uid://drhhakm62vjsy" path="res://addons/maaacks_game_template/base/scenes/utilities/api_client.tscn" id="2_s6pdq"]
+[ext_resource type="PackedScene" uid="uid://dlkmofxhavh10" path="res://addons/maaacks_game_template/base/scenes/utilities/download_and_extract.tscn" id="3_s6pdq"]
+
+[node name="UpdatePlugin" type="Node"]
+script = ExtResource("1_s6qpc")
+plugin_directory = "maaacks_game_template"
+plugin_github_url = "https://github.com/Maaack/Godot-Game-Template"
+
+[node name="APIClient" parent="." instance=ExtResource("2_s6pdq")]
+api_url = "https://api.github.com/repos/Maaack/Godot-Game-Template/releases"
+request_method = 0
+
+[node name="DownloadAndExtract" parent="." instance=ExtResource("3_s6pdq")]
+extract_path = "res://"
+path_match_string = "addons/"
+skip_base_zip_dir = true
+force = true
+
+[node name="UpdateConfirmationDialog" type="ConfirmationDialog" parent="."]
+auto_translate_mode = 1
+title = "Update Plugin?"
+initial_position = 2
+size = Vector2i(640, 360)
+dialog_autowrap = true
+
+[node name="MarginContainer" type="MarginContainer" parent="UpdateConfirmationDialog"]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 632.0
+offset_bottom = 311.0
+theme_override_constants/margin_bottom = 16
+
+[node name="VBoxContainer" type="VBoxContainer" parent="UpdateConfirmationDialog/MarginContainer"]
+layout_mode = 2
+
+[node name="UpdateLabel" type="Label" parent="UpdateConfirmationDialog/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "This will update the contents of the plugin folder (addons/plugin_directory/).
+Files outside of the plugin folder will not be affected.
+
+Update to v0.0.0?"
+
+[node name="HSeparator" type="HSeparator" parent="UpdateConfirmationDialog/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="ReleaseNotesButton" type="LinkButton" parent="UpdateConfirmationDialog/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Show Release Notes"
+
+[node name="ReleaseNotesPanel" type="Panel" parent="UpdateConfirmationDialog/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+visible = false
+custom_minimum_size = Vector2(0, 420)
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="ReleaseLabel" type="RichTextLabel" parent="UpdateConfirmationDialog/MarginContainer/VBoxContainer/ReleaseNotesPanel"]
+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
+size_flags_vertical = 3
+
+[node name="InstallingDialog" type="AcceptDialog" parent="."]
+auto_translate_mode = 1
+title = "Installing..."
+initial_position = 2
+size = Vector2i(400, 111)
+
+[node name="MarginContainer" type="MarginContainer" parent="InstallingDialog"]
+offset_left = 4.0
+offset_top = 4.0
+offset_right = 396.0
+offset_bottom = 96.0
+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="VBoxContainer" type="VBoxContainer" parent="InstallingDialog/MarginContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="StageLabel" type="Label" parent="InstallingDialog/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="ProgressBar" type="ProgressBar" parent="InstallingDialog/MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+max_value = 1.0
+step = 0.001
+
+[node name="ErrorDialog" type="AcceptDialog" parent="."]
+auto_translate_mode = 1
+title = "Error!"
+initial_position = 2
+size = Vector2i(400, 128)
+
+[node name="SuccessDialog" type="AcceptDialog" parent="."]
+auto_translate_mode = 1
+title = "Update Complete"
+initial_position = 2
+size = Vector2i(400, 128)
+dialog_text = "%s updated to v%s."
+
+[connection signal="request_failed" from="APIClient" to="." method="_on_api_client_request_failed"]
+[connection signal="response_received" from="APIClient" to="." method="_on_api_client_response_received"]
+[connection signal="run_completed" from="DownloadAndExtract" to="." method="_on_download_and_extract_run_completed"]
+[connection signal="run_failed" from="DownloadAndExtract" to="." method="_on_download_and_extract_run_failed"]
+[connection signal="zip_saved" from="DownloadAndExtract" to="." method="_on_download_and_extract_zip_saved"]
+[connection signal="canceled" from="UpdateConfirmationDialog" to="." method="_on_update_confirmation_dialog_canceled"]
+[connection signal="confirmed" from="UpdateConfirmationDialog" to="." method="_on_update_confirmation_dialog_confirmed"]
+[connection signal="pressed" from="UpdateConfirmationDialog/MarginContainer/VBoxContainer/ReleaseNotesButton" to="." method="_on_release_notes_button_pressed"]
+[connection signal="canceled" from="ErrorDialog" to="." method="_on_error_dialog_canceled"]
+[connection signal="confirmed" from="ErrorDialog" to="." method="_on_error_dialog_confirmed"]
+[connection signal="canceled" from="SuccessDialog" to="." method="_on_success_dialog_canceled"]
+[connection signal="confirmed" from="SuccessDialog" to="." method="_on_success_dialog_confirmed"]
diff --git a/addons/maaacks_game_template/maaacks_game_template.gd b/addons/maaacks_game_template/maaacks_game_template.gd
new file mode 100644
index 0000000..1e3e8da
--- /dev/null
+++ b/addons/maaacks_game_template/maaacks_game_template.gd
@@ -0,0 +1,426 @@
+@tool
+extends EditorPlugin
+
+const PLUGIN_NAME = "Maaack's Game Template"
+const PROJECT_SETTINGS_PATH = "maaacks_game_template/"
+
+const EXAMPLES_RELATIVE_PATH = "examples/"
+const MAIN_SCENE_RELATIVE_PATH = "scenes/opening/opening_with_logo.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 THEMES_DIRECTORY_RELATIVE_PATH = "resources/themes"
+const UID_PREG_MATCH = r'uid="uid:\/\/[0-9a-z]+" '
+const WINDOW_OPEN_DELAY : float = 0.5
+const RUNNING_CHECK_DELAY : float = 0.25
+const RESAVING_DELAY : float = 1.0
+const OPEN_EDITOR_DELAY : float = 0.1
+const MAX_PHYSICS_FRAMES_FROM_START : int = 20
+const AVAILABLE_TRANSLATIONS : Array = ["en", "fr"]
+const RAW_COPY_EXTENSIONS : Array = ["gd", "md", "txt"]
+const OMIT_COPY_EXTENSIONS : Array = ["uid"]
+const REPLACE_CONTENT_EXTENSIONS : Array = ["gd", "tscn", "tres", "md"]
+
+var selected_theme : String
+var update_plugin_tool_string : String
+
+func _get_plugin_name() -> String:
+ 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 get_copy_path() -> String:
+ var copy_path = ProjectSettings.get_setting(PROJECT_SETTINGS_PATH + "copy_path", get_plugin_examples_path())
+ if not copy_path.ends_with("/"):
+ copy_path += "/"
+ return copy_path
+
+func _on_theme_selected(theme_resource_path: String) -> void:
+ selected_theme = theme_resource_path
+
+func _update_gui_theme() -> void:
+ if selected_theme.is_empty(): return
+ ProjectSettings.set_setting("gui/theme/custom", selected_theme)
+ ProjectSettings.save()
+
+func _open_theme_selection_dialog(target_path : String) -> void:
+ selected_theme = ""
+ var theme_selection_scene : PackedScene = load(get_plugin_path() + "installer/theme_selection_dialog.tscn")
+ var theme_selection_instance = theme_selection_scene.instantiate()
+ theme_selection_instance.confirmed.connect(_update_gui_theme)
+ theme_selection_instance.theme_selected.connect(_on_theme_selected)
+ add_child(theme_selection_instance)
+ var theme_directores : Array[String]
+ theme_directores.append(target_path + THEMES_DIRECTORY_RELATIVE_PATH)
+ theme_selection_instance.theme_directories = theme_directores
+
+func _delayed_open_theme_selection_dialog(target_path : String) -> void:
+ var timer: Timer = Timer.new()
+ var callable := func():
+ timer.stop()
+ _open_theme_selection_dialog(target_path)
+ timer.queue_free()
+ timer.timeout.connect(callable)
+ add_child(timer)
+ timer.start(WINDOW_OPEN_DELAY)
+
+func _check_theme_needs_updating(target_path : String) -> void:
+ var current_theme_resource_path = ProjectSettings.get_setting("gui/theme/custom", "")
+ if current_theme_resource_path != "":
+ return
+ var new_theme_resource_path = target_path + MAIN_SCENE_RELATIVE_PATH
+ if new_theme_resource_path == current_theme_resource_path:
+ return
+ _delayed_open_theme_selection_dialog(target_path)
+
+func _update_main_scene(target_path : String, main_scene_path : String) -> void:
+ ProjectSettings.set_setting("application/run/main_scene", main_scene_path)
+ ProjectSettings.save()
+ _check_theme_needs_updating(target_path)
+
+func _check_main_scene_needs_updating(target_path : String) -> void:
+ 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:
+ _open_main_scene_confirmation_dialog(target_path, current_main_scene_path, new_main_scene_path)
+ return
+ _check_theme_needs_updating(target_path)
+
+func _open_main_scene_confirmation_dialog(target_path : String, current_main_scene : String, new_main_scene : String) -> void:
+ 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(target_path, new_main_scene))
+ main_confirmation_instance.canceled.connect(_check_theme_needs_updating.bind(target_path))
+ add_child(main_confirmation_instance)
+
+func _open_play_opening_confirmation_dialog(target_path : String) -> void:
+ 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) -> void:
+ 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() -> void:
+ 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) -> void:
+ 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() -> void:
+ 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(RUNNING_CHECK_DELAY)
+
+func _delete_directory_recursive(dir_path : String) -> void:
+ 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 = "") -> void:
+ 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) -> void:
+ var extension : String = file_path.get_extension()
+ if extension not in REPLACE_CONTENT_EXTENSIONS:
+ return
+ 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().trim_prefix("res://"), target_path.trim_prefix("res://"))
+ # Replace game state example class names.
+ replaced_content = replaced_content.replace("StateExample", "State")
+ 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 _raw_copy_file_path(file_path : String, destination_path : String) -> Error:
+ var dir := DirAccess.open("res://")
+ var error := dir.copy(file_path, destination_path)
+ return error
+
+func _copy_override_file() -> void:
+ 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) -> Error:
+ var error : Error
+ if file_path.get_extension() in OMIT_COPY_EXTENSIONS:
+ return error
+ if file_path.get_extension() in RAW_COPY_EXTENSIONS:
+ error = _raw_copy_file_path(file_path, destination_path)
+ else:
+ error = _save_resource(file_path, destination_path)
+ if error == ERR_FILE_UNRECOGNIZED:
+ error = _raw_copy_file_path(file_path, destination_path)
+ if not error:
+ _replace_file_contents(destination_path, target_path)
+ return error
+
+func _copy_directory_path(dir_path : String, target_path : String) -> void:
+ 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)
+ else:
+ error = _copy_file_path(full_file_path, destination_path, target_path)
+ 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) -> void:
+ 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_play_opening_confirmation_dialog(target_path : String) -> void:
+ var timer: Timer = Timer.new()
+ var callable := func():
+ timer.stop()
+ _open_play_opening_confirmation_dialog(target_path)
+ timer.queue_free()
+ timer.timeout.connect(callable)
+ add_child(timer)
+ timer.start(WINDOW_OPEN_DELAY)
+
+func _wait_for_scan_and_delay_next_prompt(target_path : String) -> void:
+ var timer: Timer = Timer.new()
+ var callable := func():
+ if EditorInterface.get_resource_filesystem().is_scanning(): return
+ timer.stop()
+ _delayed_play_opening_confirmation_dialog(target_path)
+ timer.queue_free()
+ timer.timeout.connect(callable)
+ add_child(timer)
+ timer.start(RUNNING_CHECK_DELAY)
+
+func _delayed_saving_and_next_prompt(target_path : String) -> void:
+ var timer: Timer = Timer.new()
+ var callable := func():
+ timer.stop()
+ EditorInterface.save_all_scenes()
+ EditorInterface.get_resource_filesystem().scan()
+ _wait_for_scan_and_delay_next_prompt(target_path)
+ timer.queue_free()
+ timer.timeout.connect(callable)
+ add_child(timer)
+ timer.start(RESAVING_DELAY)
+
+func _add_translations() -> void:
+ var dir := DirAccess.open("res://")
+ var translations : PackedStringArray = ProjectSettings.get_setting("internationalization/locale/translations", [])
+ for available_translation in AVAILABLE_TRANSLATIONS:
+ var translation_path = get_plugin_path() + ("base/translations/menus_translations.%s.translation" % available_translation)
+ if dir.file_exists(translation_path) and translation_path not in translations:
+ translations.append(translation_path)
+ ProjectSettings.set_setting("internationalization/locale/translations", translations)
+
+func _copy_to_directory(target_path : String) -> void:
+ 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)
+ _update_scene_loader_path(target_path)
+ _copy_override_file()
+ _delayed_saving_and_next_prompt(target_path)
+
+func _open_input_icons_dialog() -> void:
+ var input_icons_scene : PackedScene = load(get_plugin_path() + "installer/kenney_input_prompts_installer.tscn")
+ var input_icons_instance = input_icons_scene.instantiate()
+ input_icons_instance.copy_dir_path = get_copy_path()
+ add_child(input_icons_instance)
+
+func _open_path_dialog() -> void:
+ 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_copy_path()))
+ add_child(destination_instance)
+
+func _open_confirmation_dialog() -> void:
+ 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_copy_path()))
+ add_child(confirmation_instance)
+
+func _open_check_plugin_version() -> void:
+ if ProjectSettings.has_setting(PROJECT_SETTINGS_PATH + "disable_update_check"):
+ if ProjectSettings.get_setting(PROJECT_SETTINGS_PATH + "disable_update_check"):
+ return
+ else:
+ ProjectSettings.set_setting(PROJECT_SETTINGS_PATH + "disable_update_check", false)
+ ProjectSettings.save()
+ var check_version_scene : PackedScene = load(get_plugin_path() + "installer/check_plugin_version.tscn")
+ var check_version_instance : Node = check_version_scene.instantiate()
+ check_version_instance.auto_start = true
+ check_version_instance.new_version_detected.connect(_add_update_plugin_tool_option)
+ add_child(check_version_instance)
+
+func _open_update_plugin() -> void:
+ var update_plugin_scene : PackedScene = load(get_plugin_path() + "installer/update_plugin.tscn")
+ var update_plugin_instance : Node = update_plugin_scene.instantiate()
+ update_plugin_instance.auto_start = true
+ update_plugin_instance.update_completed.connect(_remove_update_plugin_tool_option)
+ add_child(update_plugin_instance)
+
+func _add_update_plugin_tool_option(new_version : String) -> void:
+ update_plugin_tool_string = "Update %s to v%s..." % [_get_plugin_name(), new_version]
+ add_tool_menu_item(update_plugin_tool_string, _open_update_plugin)
+
+func _remove_update_plugin_tool_option() -> void:
+ if update_plugin_tool_string.is_empty(): return
+ remove_tool_menu_item(update_plugin_tool_string)
+ update_plugin_tool_string = ""
+
+func _deprecate_old_setting_name() -> void:
+ if not ProjectSettings.has_setting(PROJECT_SETTINGS_PATH + "disable_plugin_dialogues"): return
+ var prior_setting : bool = ProjectSettings.get_setting(PROJECT_SETTINGS_PATH + "disable_plugin_dialogues", false)
+ ProjectSettings.set_setting(PROJECT_SETTINGS_PATH + "disable_install_wizard", prior_setting)
+ ProjectSettings.set_setting(PROJECT_SETTINGS_PATH + "disable_plugin_dialogues", null)
+
+func _show_plugin_dialogues() -> void:
+ _deprecate_old_setting_name()
+ if ProjectSettings.has_setting(PROJECT_SETTINGS_PATH + "disable_install_wizard") :
+ if ProjectSettings.get_setting(PROJECT_SETTINGS_PATH + "disable_install_wizard") :
+ return
+ _open_confirmation_dialog()
+ ProjectSettings.set_setting(PROJECT_SETTINGS_PATH + "disable_install_wizard", true)
+ ProjectSettings.save()
+
+func _resave_if_recently_opened() -> void:
+ 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_tool_options() -> void:
+ 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)
+ add_tool_menu_item("Use Input Icons for " + _get_plugin_name() + "...", _open_input_icons_dialog)
+ _open_check_plugin_version()
+
+func _remove_tool_options() -> void:
+ 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...")
+ remove_tool_menu_item("Use Input Icons for " + _get_plugin_name() + "...")
+ _remove_update_plugin_tool_option()
+
+func _enter_tree() -> void:
+ 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_tool_options()
+ _add_translations()
+ _show_plugin_dialogues()
+ _resave_if_recently_opened()
+
+func _exit_tree() -> void:
+ remove_autoload_singleton("AppConfig")
+ remove_autoload_singleton("SceneLoader")
+ remove_autoload_singleton("ProjectMusicController")
+ remove_autoload_singleton("ProjectUISoundController")
+ _remove_tool_options()
diff --git a/addons/maaacks_game_template/maaacks_game_template.gd.uid b/addons/maaacks_game_template/maaacks_game_template.gd.uid
new file mode 100644
index 0000000..2b07b65
--- /dev/null
+++ b/addons/maaacks_game_template/maaacks_game_template.gd.uid
@@ -0,0 +1 @@
+uid://bndaaa5when2r
diff --git a/addons/maaacks_game_template/media/.gdignore b/addons/maaacks_game_template/media/.gdignore
new file mode 100644
index 0000000..e69de29
diff --git a/addons/maaacks_game_template/media/credits_scene-icon-black-transparent-256x256.png b/addons/maaacks_game_template/media/credits_scene-icon-black-transparent-256x256.png
new file mode 100644
index 0000000..4711e01
Binary files /dev/null and b/addons/maaacks_game_template/media/credits_scene-icon-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/media/game-icon-black-transparent-256x256.png b/addons/maaacks_game_template/media/game-icon-black-transparent-256x256.png
new file mode 100644
index 0000000..cbf0047
Binary files /dev/null and b/addons/maaacks_game_template/media/game-icon-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/media/input_remapping-icon-black-transparent-256x256.png b/addons/maaacks_game_template/media/input_remapping-icon-black-transparent-256x256.png
new file mode 100644
index 0000000..065b89c
Binary files /dev/null and b/addons/maaacks_game_template/media/input_remapping-icon-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/media/maaack-black-transparent-256x256.png b/addons/maaacks_game_template/media/maaack-black-transparent-256x256.png
new file mode 100644
index 0000000..23523f6
Binary files /dev/null and b/addons/maaacks_game_template/media/maaack-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/media/maaacks-plugin-suite-256x256.gif b/addons/maaacks_game_template/media/maaacks-plugin-suite-256x256.gif
new file mode 100644
index 0000000..04dcaee
Binary files /dev/null and b/addons/maaacks_game_template/media/maaacks-plugin-suite-256x256.gif differ
diff --git a/addons/maaacks_game_template/media/menus-icon-black-transparent-256x256.png b/addons/maaacks_game_template/media/menus-icon-black-transparent-256x256.png
new file mode 100644
index 0000000..e409952
Binary files /dev/null and b/addons/maaacks_game_template/media/menus-icon-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/media/music_controller-icon-black-transparent-256x256.png b/addons/maaacks_game_template/media/music_controller-icon-black-transparent-256x256.png
new file mode 100644
index 0000000..060295c
Binary files /dev/null and b/addons/maaacks_game_template/media/music_controller-icon-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/media/options-icon-black-transparent-256x256.png b/addons/maaacks_game_template/media/options-icon-black-transparent-256x256.png
new file mode 100644
index 0000000..28bf4c5
Binary files /dev/null and b/addons/maaacks_game_template/media/options-icon-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/media/scene_loader-icon-black-transparent-256x256.png b/addons/maaacks_game_template/media/scene_loader-icon-black-transparent-256x256.png
new file mode 100644
index 0000000..f3e37bc
Binary files /dev/null and b/addons/maaacks_game_template/media/scene_loader-icon-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-5-juliocacko-1.png b/addons/maaacks_game_template/media/screenshot-5-juliocacko-1.png
new file mode 100644
index 0000000..f9b609d
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-5-juliocacko-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-5-juliocacko-2.png b/addons/maaacks_game_template/media/screenshot-5-juliocacko-2.png
new file mode 100644
index 0000000..51da311
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-5-juliocacko-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-5-kenney-1.png b/addons/maaacks_game_template/media/screenshot-5-kenney-1.png
new file mode 100644
index 0000000..8286e7f
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-5-kenney-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-5-kenney-2.png b/addons/maaacks_game_template/media/screenshot-5-kenney-2.png
new file mode 100644
index 0000000..ba8455c
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-5-kenney-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-5-kenney-3.png b/addons/maaacks_game_template/media/screenshot-5-kenney-3.png
new file mode 100644
index 0000000..ad1eebc
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-5-kenney-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-5-kenney-4.png b/addons/maaacks_game_template/media/screenshot-5-kenney-4.png
new file mode 100644
index 0000000..19c2343
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-5-kenney-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-5-xelu-1.png b/addons/maaacks_game_template/media/screenshot-5-xelu-1.png
new file mode 100644
index 0000000..157614c
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-5-xelu-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-5-xelu-2.png b/addons/maaacks_game_template/media/screenshot-5-xelu-2.png
new file mode 100644
index 0000000..acb0964
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-5-xelu-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-audio-options-1.png b/addons/maaacks_game_template/media/screenshot-6-audio-options-1.png
new file mode 100644
index 0000000..d5d504c
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-audio-options-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-audio-options-2.png b/addons/maaacks_game_template/media/screenshot-6-audio-options-2.png
new file mode 100644
index 0000000..0020cd7
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-audio-options-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-audio-options-3.png b/addons/maaacks_game_template/media/screenshot-6-audio-options-3.png
new file mode 100644
index 0000000..cb10028
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-audio-options-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-audio-options-4.png b/addons/maaacks_game_template/media/screenshot-6-audio-options-4.png
new file mode 100644
index 0000000..02ccf47
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-audio-options-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-audio-options-5.png b/addons/maaacks_game_template/media/screenshot-6-audio-options-5.png
new file mode 100644
index 0000000..afae5fa
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-audio-options-5.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-audio-options-6.png b/addons/maaacks_game_template/media/screenshot-6-audio-options-6.png
new file mode 100644
index 0000000..a2a0161
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-audio-options-6.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-1.png b/addons/maaacks_game_template/media/screenshot-6-input-list-1.png
new file mode 100644
index 0000000..13b9478
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-2.png b/addons/maaacks_game_template/media/screenshot-6-input-list-2.png
new file mode 100644
index 0000000..c9c6936
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-3.png b/addons/maaacks_game_template/media/screenshot-6-input-list-3.png
new file mode 100644
index 0000000..d1652b0
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-4.png b/addons/maaacks_game_template/media/screenshot-6-input-list-4.png
new file mode 100644
index 0000000..8e649cd
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-5.png b/addons/maaacks_game_template/media/screenshot-6-input-list-5.png
new file mode 100644
index 0000000..f882582
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-5.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-6.png b/addons/maaacks_game_template/media/screenshot-6-input-list-6.png
new file mode 100644
index 0000000..89d9cad
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-6.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-7.png b/addons/maaacks_game_template/media/screenshot-6-input-list-7.png
new file mode 100644
index 0000000..37dc102
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-7.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-8.png b/addons/maaacks_game_template/media/screenshot-6-input-list-8.png
new file mode 100644
index 0000000..35253e0
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-8.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-list-9.png b/addons/maaacks_game_template/media/screenshot-6-input-list-9.png
new file mode 100644
index 0000000..c863208
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-list-9.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-1.png b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-1.png
new file mode 100644
index 0000000..e308813
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-2.png b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-2.png
new file mode 100644
index 0000000..6236ced
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-3.png b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-3.png
new file mode 100644
index 0000000..6e42860
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-4.png b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-4.png
new file mode 100644
index 0000000..e4f1377
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-5.png b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-5.png
new file mode 100644
index 0000000..9a6c9cb
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-sensitivity-5.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-tree-1.png b/addons/maaacks_game_template/media/screenshot-6-input-tree-1.png
new file mode 100644
index 0000000..cb187d8
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-tree-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-tree-2.png b/addons/maaacks_game_template/media/screenshot-6-input-tree-2.png
new file mode 100644
index 0000000..32e03ff
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-tree-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-tree-3.png b/addons/maaacks_game_template/media/screenshot-6-input-tree-3.png
new file mode 100644
index 0000000..b7c0305
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-tree-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-tree-4.png b/addons/maaacks_game_template/media/screenshot-6-input-tree-4.png
new file mode 100644
index 0000000..062d982
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-tree-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-input-tree-5.png b/addons/maaacks_game_template/media/screenshot-6-input-tree-5.png
new file mode 100644
index 0000000..af86c6c
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-input-tree-5.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-lost-1.png b/addons/maaacks_game_template/media/screenshot-6-level-lost-1.png
new file mode 100644
index 0000000..eb1e243
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-lost-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-lost-2.png b/addons/maaacks_game_template/media/screenshot-6-level-lost-2.png
new file mode 100644
index 0000000..5c8ecdb
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-lost-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-lost-3.png b/addons/maaacks_game_template/media/screenshot-6-level-lost-3.png
new file mode 100644
index 0000000..61b3583
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-lost-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-select-1.png b/addons/maaacks_game_template/media/screenshot-6-level-select-1.png
new file mode 100644
index 0000000..1bc5dd8
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-select-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-state-1.png b/addons/maaacks_game_template/media/screenshot-6-level-state-1.png
new file mode 100644
index 0000000..69e11a1
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-state-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-state-2.png b/addons/maaacks_game_template/media/screenshot-6-level-state-2.png
new file mode 100644
index 0000000..69fab0b
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-state-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-state-3.png b/addons/maaacks_game_template/media/screenshot-6-level-state-3.png
new file mode 100644
index 0000000..f90392c
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-state-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-won-1.png b/addons/maaacks_game_template/media/screenshot-6-level-won-1.png
new file mode 100644
index 0000000..b1ce24f
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-won-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-won-2.png b/addons/maaacks_game_template/media/screenshot-6-level-won-2.png
new file mode 100644
index 0000000..d9042d1
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-won-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-level-won-3.png b/addons/maaacks_game_template/media/screenshot-6-level-won-3.png
new file mode 100644
index 0000000..d0a02f0
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-level-won-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-loading-screen-1.png b/addons/maaacks_game_template/media/screenshot-6-loading-screen-1.png
new file mode 100644
index 0000000..789ed03
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-loading-screen-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-loading-screen-2.png b/addons/maaacks_game_template/media/screenshot-6-loading-screen-2.png
new file mode 100644
index 0000000..7aefce7
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-loading-screen-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-loading-screen-3.png b/addons/maaacks_game_template/media/screenshot-6-loading-screen-3.png
new file mode 100644
index 0000000..ecaa3ed
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-loading-screen-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-loading-screen-4.png b/addons/maaacks_game_template/media/screenshot-6-loading-screen-4.png
new file mode 100644
index 0000000..56e40c0
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-loading-screen-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-main-menu-1.png b/addons/maaacks_game_template/media/screenshot-6-main-menu-1.png
new file mode 100644
index 0000000..ebbfeff
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-main-menu-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-main-menu-2.png b/addons/maaacks_game_template/media/screenshot-6-main-menu-2.png
new file mode 100644
index 0000000..6aa9637
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-main-menu-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-main-menu-3.png b/addons/maaacks_game_template/media/screenshot-6-main-menu-3.png
new file mode 100644
index 0000000..b7f6caa
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-main-menu-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-main-menu-4.png b/addons/maaacks_game_template/media/screenshot-6-main-menu-4.png
new file mode 100644
index 0000000..e5285ab
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-main-menu-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-main-menu-5.png b/addons/maaacks_game_template/media/screenshot-6-main-menu-5.png
new file mode 100644
index 0000000..871aeb7
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-main-menu-5.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-mini-options-1.png b/addons/maaacks_game_template/media/screenshot-6-mini-options-1.png
new file mode 100644
index 0000000..57e1e58
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-mini-options-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-mini-options-2.png b/addons/maaacks_game_template/media/screenshot-6-mini-options-2.png
new file mode 100644
index 0000000..22c5133
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-mini-options-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-pause-menu-1.png b/addons/maaacks_game_template/media/screenshot-6-pause-menu-1.png
new file mode 100644
index 0000000..2e2a30b
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-pause-menu-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-pause-menu-2.png b/addons/maaacks_game_template/media/screenshot-6-pause-menu-2.png
new file mode 100644
index 0000000..5e30fa2
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-pause-menu-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-pause-menu-3.png b/addons/maaacks_game_template/media/screenshot-6-pause-menu-3.png
new file mode 100644
index 0000000..795eac5
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-pause-menu-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-pause-menu-4.png b/addons/maaacks_game_template/media/screenshot-6-pause-menu-4.png
new file mode 100644
index 0000000..c248cf1
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-pause-menu-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-pause-menu-5.png b/addons/maaacks_game_template/media/screenshot-6-pause-menu-5.png
new file mode 100644
index 0000000..5cb7c36
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-pause-menu-5.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-1.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-1.png
new file mode 100644
index 0000000..cac8c8e
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-10.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-10.png
new file mode 100644
index 0000000..989ebcf
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-10.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-11.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-11.png
new file mode 100644
index 0000000..c970f2e
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-11.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-12.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-12.png
new file mode 100644
index 0000000..50d2728
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-12.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-2.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-2.png
new file mode 100644
index 0000000..ccbcf1a
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-3.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-3.png
new file mode 100644
index 0000000..6c696fa
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-4.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-4.png
new file mode 100644
index 0000000..63bc601
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-5.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-5.png
new file mode 100644
index 0000000..a5f85a3
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-5.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-6.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-6.png
new file mode 100644
index 0000000..b9367a8
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-6.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-7.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-7.png
new file mode 100644
index 0000000..3108e07
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-7.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-8.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-8.png
new file mode 100644
index 0000000..2f3838a
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-8.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-theme-selector-9.png b/addons/maaacks_game_template/media/screenshot-6-theme-selector-9.png
new file mode 100644
index 0000000..ebdb9be
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-theme-selector-9.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-video-options-1.png b/addons/maaacks_game_template/media/screenshot-6-video-options-1.png
new file mode 100644
index 0000000..6cc7c52
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-video-options-1.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-video-options-2.png b/addons/maaacks_game_template/media/screenshot-6-video-options-2.png
new file mode 100644
index 0000000..efbdd68
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-video-options-2.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-video-options-3.png b/addons/maaacks_game_template/media/screenshot-6-video-options-3.png
new file mode 100644
index 0000000..928a2ee
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-video-options-3.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-video-options-4.png b/addons/maaacks_game_template/media/screenshot-6-video-options-4.png
new file mode 100644
index 0000000..16ff796
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-video-options-4.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-video-options-5.png b/addons/maaacks_game_template/media/screenshot-6-video-options-5.png
new file mode 100644
index 0000000..f11b1e1
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-video-options-5.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-6-video-options-6.png b/addons/maaacks_game_template/media/screenshot-6-video-options-6.png
new file mode 100644
index 0000000..54a5228
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-6-video-options-6.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-game-a-darkness-like-gravity.png b/addons/maaacks_game_template/media/screenshot-game-a-darkness-like-gravity.png
new file mode 100644
index 0000000..880e5cb
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-game-a-darkness-like-gravity.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-game-harvest-hill.png b/addons/maaacks_game_template/media/screenshot-game-harvest-hill.png
new file mode 100644
index 0000000..4554020
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-game-harvest-hill.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-game-nannybot-overload.png b/addons/maaacks_game_template/media/screenshot-game-nannybot-overload.png
new file mode 100644
index 0000000..7df9100
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-game-nannybot-overload.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-game-rent-seek-kill.png b/addons/maaacks_game_template/media/screenshot-game-rent-seek-kill.png
new file mode 100644
index 0000000..9b67b6d
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-game-rent-seek-kill.png differ
diff --git a/addons/maaacks_game_template/media/screenshot-game-spud-customs.png b/addons/maaacks_game_template/media/screenshot-game-spud-customs.png
new file mode 100644
index 0000000..59a2d7e
Binary files /dev/null and b/addons/maaacks_game_template/media/screenshot-game-spud-customs.png differ
diff --git a/addons/maaacks_game_template/media/ui_sound_controller-icon-black-transparent-256x256.png b/addons/maaacks_game_template/media/ui_sound_controller-icon-black-transparent-256x256.png
new file mode 100644
index 0000000..44abb19
Binary files /dev/null and b/addons/maaacks_game_template/media/ui_sound_controller-icon-black-transparent-256x256.png differ
diff --git a/addons/maaacks_game_template/plugin.cfg b/addons/maaacks_game_template/plugin.cfg
new file mode 100644
index 0000000..eb05506
--- /dev/null
+++ b/addons/maaacks_game_template/plugin.cfg
@@ -0,0 +1,9 @@
+[plugin]
+
+name="Maaack's Game Template"
+description="Template with a main menu, options menus, pause menu, credits, scene loader, extra tools, and an example game scene.
+
+Created in collaboration with members of the Godot Wild Jam community."
+author="Marek Belski"
+version="0.23.0"
+script="maaacks_game_template.gd"
diff --git a/menus/ATTRIBUTION.md b/menus/ATTRIBUTION.md
new file mode 100644
index 0000000..2cd9d1c
--- /dev/null
+++ b/menus/ATTRIBUTION.md
@@ -0,0 +1,38 @@
+# Attribution
+## Collaborators
+
+### Role
+Person 1
+Person 2
+[Person w/ Link]()
+
+## Sourced
+### Asset Type
+#### Use Case
+Author: [Name]()
+Source: [Domain : webpage.html]()
+License: [License]()
+
+#### Godot Engine Logo
+Author: Andrea Calabró
+Source: [godotengine.org : press](https://godotengine.org/press/)
+License: [CC BY 4.0 International](https://github.com/godotengine/godot/blob/master/LOGO_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)
+
+#### Godot Game Template
+
+Author: [Marek Belski and contributors](https://github.com/Maaack/Godot-Game-Template/graphs/contributors)
+Source: [github: Godot-Game-Template](https://github.com/Maaack/Godot-Game-Template)
+License: [MIT License](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)
diff --git a/menus/assets/git_logo/Git-Logo-2Color.png b/menus/assets/git_logo/Git-Logo-2Color.png
new file mode 100644
index 0000000..18c5b29
Binary files /dev/null and b/menus/assets/git_logo/Git-Logo-2Color.png differ
diff --git a/menus/assets/git_logo/Git-Logo-2Color.png.import b/menus/assets/git_logo/Git-Logo-2Color.png.import
new file mode 100644
index 0000000..cd56d76
--- /dev/null
+++ b/menus/assets/git_logo/Git-Logo-2Color.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bgu2uludk87k0"
+path="res://.godot/imported/Git-Logo-2Color.png-8d60985b349b90d009d8e0fa064f2f30.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://menus/assets/git_logo/Git-Logo-2Color.png"
+dest_files=["res://.godot/imported/Git-Logo-2Color.png-8d60985b349b90d009d8e0fa064f2f30.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
diff --git a/menus/assets/git_logo/LICENSE.txt b/menus/assets/git_logo/LICENSE.txt
new file mode 100644
index 0000000..2d17b1d
--- /dev/null
+++ b/menus/assets/git_logo/LICENSE.txt
@@ -0,0 +1,6 @@
+Git Logo
+Copyright (c) Jason Long
+
+This work is licensed under the Creative Commons Attribution 3.0 Unported
+license (CC BY 3.0): https://creativecommons.org/licenses/by/3.0/
+
diff --git a/menus/assets/godot_engine_logo/LICENSE.txt b/menus/assets/godot_engine_logo/LICENSE.txt
new file mode 100644
index 0000000..a081c9e
--- /dev/null
+++ b/menus/assets/godot_engine_logo/LICENSE.txt
@@ -0,0 +1,5 @@
+Godot Engine Logo
+Copyright (c) 2017 Andrea Calabró
+
+This work is licensed under the Creative Commons Attribution 4.0 International
+license (CC BY 4.0 International): https://creativecommons.org/licenses/by/4.0/
\ No newline at end of file
diff --git a/menus/assets/godot_engine_logo/logo_vertical_color_dark.png b/menus/assets/godot_engine_logo/logo_vertical_color_dark.png
new file mode 100644
index 0000000..2c38732
Binary files /dev/null and b/menus/assets/godot_engine_logo/logo_vertical_color_dark.png differ
diff --git a/menus/assets/godot_engine_logo/logo_vertical_color_dark.png.import b/menus/assets/godot_engine_logo/logo_vertical_color_dark.png.import
new file mode 100644
index 0000000..2508eae
--- /dev/null
+++ b/menus/assets/godot_engine_logo/logo_vertical_color_dark.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://6vwgmbr7eylm"
+path="res://.godot/imported/logo_vertical_color_dark.png-384a2af4f2358a96a64d672d7da75a7a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://menus/assets/godot_engine_logo/logo_vertical_color_dark.png"
+dest_files=["res://.godot/imported/logo_vertical_color_dark.png-384a2af4f2358a96a64d672d7da75a7a.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
diff --git a/menus/assets/icon.png b/menus/assets/icon.png
new file mode 100644
index 0000000..cbf0047
Binary files /dev/null and b/menus/assets/icon.png differ
diff --git a/menus/assets/icon.png.import b/menus/assets/icon.png.import
new file mode 100644
index 0000000..b5f5642
--- /dev/null
+++ b/menus/assets/icon.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cokgo26lwtfvw"
+path="res://.godot/imported/icon.png-416da93d6e3577a1ba611493576a9a19.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://menus/assets/icon.png"
+dest_files=["res://.godot/imported/icon.png-416da93d6e3577a1ba611493576a9a19.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
diff --git a/menus/resources/themes/expedition.tres b/menus/resources/themes/expedition.tres
new file mode 100644
index 0000000..444aff6
--- /dev/null
+++ b/menus/resources/themes/expedition.tres
@@ -0,0 +1,98 @@
+[gd_resource type="Theme" load_steps=8 format=3 uid="uid://bwfja23avgm8f"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g0sbc"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.823636, 0.744991, 0.659007, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.662913, 0.549096, 0.478248, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_krvwn"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.662913, 0.549096, 0.478248, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.466529, 0.360525, 0.333165, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gfyr3"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.466529, 0.360525, 0.333165, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.282353, 0.231067, 0.227161, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tytr8"]
+bg_color = Color(0.282353, 0.231067, 0.227161, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.133364, 0.133364, 0.133364, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wsakr"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.823636, 0.744991, 0.659007, 1)
+border_width_right = 2
+border_color = Color(0.662913, 0.549096, 0.478248, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1ngrn"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.662913, 0.549096, 0.478248, 1)
+border_width_right = 2
+border_color = Color(0.466529, 0.360525, 0.333165, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q0g5m"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.466529, 0.360525, 0.333165, 1)
+border_width_top = 2
+border_width_right = 2
+border_color = Color(0.282353, 0.231067, 0.227161, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+
+[resource]
+Button/styles/hover = SubResource("StyleBoxFlat_g0sbc")
+Button/styles/normal = SubResource("StyleBoxFlat_krvwn")
+Button/styles/pressed = SubResource("StyleBoxFlat_gfyr3")
+Panel/styles/panel = SubResource("StyleBoxFlat_tytr8")
+PanelContainer/styles/panel = SubResource("StyleBoxFlat_tytr8")
+TabContainer/styles/panel = SubResource("StyleBoxFlat_tytr8")
+TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_wsakr")
+TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_1ngrn")
+TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_q0g5m")
diff --git a/menus/resources/themes/gravity.tres b/menus/resources/themes/gravity.tres
new file mode 100644
index 0000000..d261afd
--- /dev/null
+++ b/menus/resources/themes/gravity.tres
@@ -0,0 +1,116 @@
+[gd_resource type="Theme" load_steps=8 format=3 uid="uid://dyjbfmjgx8t8f"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_w50h3"]
+content_margin_left = 16.0
+content_margin_top = 4.0
+content_margin_right = 16.0
+content_margin_bottom = 4.0
+bg_color = Color(0.125911, 0.125911, 0.125911, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.95, 0.95, 0.95, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+corner_detail = 1
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6tkof"]
+content_margin_left = 16.0
+content_margin_top = 4.0
+content_margin_right = 16.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.95, 0.95, 0.95, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+corner_detail = 1
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ftays"]
+content_margin_left = 16.0
+content_margin_top = 4.0
+content_margin_right = 16.0
+content_margin_bottom = 4.0
+bg_color = Color(0.95, 0.95, 0.95, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.125911, 0.125911, 0.125911, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+corner_detail = 1
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sucf2"]
+bg_color = Color(0.0619267, 0.0619267, 0.0619266, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+corner_detail = 1
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u30hj"]
+content_margin_left = 8.0
+content_margin_right = 8.0
+bg_color = Color(0.125536, 0.125536, 0.125536, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_color = Color(0.945281, 0.945281, 0.945281, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_detail = 1
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h5y6c"]
+content_margin_left = 8.0
+content_margin_right = 8.0
+bg_color = Color(0.945281, 0.945281, 0.945281, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_color = Color(0.0195315, 0.0195315, 0.0195315, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_detail = 1
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g0qsc"]
+content_margin_left = 8.0
+content_margin_right = 8.0
+bg_color = Color(0, 0, 0, 0.933333)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_color = Color(0.945281, 0.945281, 0.945281, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_detail = 1
+
+[resource]
+Button/colors/font_color = Color(0.95, 0.95, 0.95, 1)
+Button/colors/font_focus_color = Color(0.95, 0.95, 0.95, 1)
+Button/colors/font_hover_color = Color(0.95, 0.95, 0.95, 1)
+Button/colors/font_pressed_color = Color(0.125911, 0.125911, 0.125911, 1)
+Button/styles/disabled = null
+Button/styles/focus = null
+Button/styles/hover = SubResource("StyleBoxFlat_w50h3")
+Button/styles/normal = SubResource("StyleBoxFlat_6tkof")
+Button/styles/pressed = SubResource("StyleBoxFlat_ftays")
+Panel/styles/panel = SubResource("StyleBoxFlat_sucf2")
+PanelContainer/styles/panel = SubResource("StyleBoxFlat_sucf2")
+TabContainer/colors/font_hovered_color = Color(0.945281, 0.945281, 0.945281, 1)
+TabContainer/colors/font_selected_color = Color(0.0195315, 0.0195315, 0.0195315, 1)
+TabContainer/colors/font_unselected_color = Color(0.945281, 0.945281, 0.945281, 1)
+TabContainer/styles/panel = SubResource("StyleBoxFlat_sucf2")
+TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_u30hj")
+TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_h5y6c")
+TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_g0qsc")
diff --git a/menus/resources/themes/grow.tres b/menus/resources/themes/grow.tres
new file mode 100644
index 0000000..4563a8f
--- /dev/null
+++ b/menus/resources/themes/grow.tres
@@ -0,0 +1,98 @@
+[gd_resource type="Theme" load_steps=8 format=3 uid="uid://b71j4vgc6rwfx"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g0sbc"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.428961, 0.730226, 0.50528, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.298858, 0.546296, 0.363635, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_krvwn"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.298858, 0.546296, 0.363635, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.234842, 0.443383, 0.289887, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gfyr3"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.234842, 0.443383, 0.289887, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.234842, 0.443383, 0.289887, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tytr8"]
+bg_color = Color(0.191138, 0.370484, 0.238651, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.115392, 0.241196, 0.148848, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wsakr"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.428961, 0.730226, 0.50528, 1)
+border_width_right = 2
+border_color = Color(0.298858, 0.546296, 0.363635, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1ngrn"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.298858, 0.546296, 0.363635, 1)
+border_width_right = 2
+border_color = Color(0.234842, 0.443383, 0.289887, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q0g5m"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.234842, 0.443383, 0.289887, 1)
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.234842, 0.443383, 0.289887, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+
+[resource]
+Button/styles/hover = SubResource("StyleBoxFlat_g0sbc")
+Button/styles/normal = SubResource("StyleBoxFlat_krvwn")
+Button/styles/pressed = SubResource("StyleBoxFlat_gfyr3")
+Panel/styles/panel = SubResource("StyleBoxFlat_tytr8")
+PanelContainer/styles/panel = SubResource("StyleBoxFlat_tytr8")
+TabContainer/styles/panel = SubResource("StyleBoxFlat_tytr8")
+TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_wsakr")
+TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_1ngrn")
+TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_q0g5m")
diff --git a/menus/resources/themes/lab.tres b/menus/resources/themes/lab.tres
new file mode 100644
index 0000000..1a6ecdd
--- /dev/null
+++ b/menus/resources/themes/lab.tres
@@ -0,0 +1,185 @@
+[gd_resource type="Theme" load_steps=16 format=3 uid="uid://dm6tkxt65tnxi"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4b4gg"]
+content_margin_left = 12.0
+content_margin_right = 12.0
+content_margin_bottom = 14.0
+bg_color = Color(0.305882, 0.454902, 0.6, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_width_bottom = 12
+border_color = Color(0.141176, 0.321569, 0.45098, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_l51yu"]
+content_margin_left = 12.0
+content_margin_right = 12.0
+content_margin_bottom = 14.0
+bg_color = Color(0.243137, 0.25098, 0.333333, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_width_bottom = 12
+border_color = Color(0.219608, 0.176471, 0.207843, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e7ejt"]
+content_margin_left = 12.0
+content_margin_right = 12.0
+content_margin_bottom = 14.0
+bg_color = Color(0.121569, 0.176471, 0.211765, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_width_bottom = 12
+border_color = Color(0.0784314, 0.121569, 0.145098, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ywrjj"]
+bg_color = Color(0.6, 0.6, 0.6, 0)
+border_width_left = 3
+border_width_top = 3
+border_width_bottom = 3
+corner_radius_top_left = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tyerd"]
+bg_color = Color(0.8, 0.8, 0.8, 1)
+border_width_left = 2
+border_width_top = 3
+border_width_bottom = 3
+border_color = Color(0.8, 0.8, 0.8, 0)
+corner_radius_top_left = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ulcur"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(0.0784314, 0.121569, 0.145098, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_width_bottom = 4
+border_color = Color(0.0431373, 0.0627451, 0.0862745, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_aliwb"]
+bg_color = Color(0.6, 0.6, 0.6, 0)
+border_width_top = 3
+border_width_right = 3
+border_width_bottom = 3
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tnl2j"]
+content_margin_bottom = 3.0
+bg_color = Color(0.8, 0.8, 0.8, 1)
+border_width_top = 3
+border_width_right = 3
+border_width_bottom = 3
+border_color = Color(0.8, 0.8, 0.8, 0)
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k5faf"]
+bg_color = Color(0.6, 0.6, 0.6, 0)
+border_width_left = 3
+border_width_top = 3
+border_width_right = 3
+border_width_bottom = 3
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5j0a2"]
+bg_color = Color(0.8, 0.8, 0.8, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_odiue"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(0.0784314, 0.121569, 0.145098, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_width_bottom = 4
+border_color = Color(0.219608, 0.176471, 0.207843, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+corner_radius_bottom_right = 8
+corner_radius_bottom_left = 8
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ghjya"]
+content_margin_left = 12.0
+content_margin_right = 12.0
+bg_color = Color(0.305882, 0.454902, 0.6, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_color = Color(0.141176, 0.321569, 0.45098, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_m6y06"]
+content_margin_left = 12.0
+content_margin_right = 12.0
+bg_color = Color(0.243137, 0.25098, 0.333333, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_color = Color(0.219608, 0.176471, 0.207843, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5icga"]
+content_margin_left = 12.0
+content_margin_right = 12.0
+bg_color = Color(0.0784314, 0.121569, 0.145098, 1)
+border_width_left = 4
+border_width_top = 4
+border_width_right = 4
+border_color = Color(0.121569, 0.176471, 0.211765, 1)
+corner_radius_top_left = 8
+corner_radius_top_right = 8
+
+[sub_resource type="FontVariation" id="FontVariation_i860b"]
+spacing_top = 4
+spacing_bottom = 2
+
+[resource]
+default_font = SubResource("FontVariation_i860b")
+Button/styles/hover = SubResource("StyleBoxFlat_4b4gg")
+Button/styles/normal = SubResource("StyleBoxFlat_l51yu")
+Button/styles/pressed = SubResource("StyleBoxFlat_e7ejt")
+LeftStaminaBar/base_type = &"ProgressBar"
+LeftStaminaBar/styles/background = SubResource("StyleBoxFlat_ywrjj")
+LeftStaminaBar/styles/fill = SubResource("StyleBoxFlat_tyerd")
+Panel/styles/panel = SubResource("StyleBoxFlat_ulcur")
+PanelContainer/styles/panel = SubResource("StyleBoxFlat_ulcur")
+RightStaminaBar/base_type = &"ProgressBar"
+RightStaminaBar/styles/background = SubResource("StyleBoxFlat_aliwb")
+RightStaminaBar/styles/fill = SubResource("StyleBoxFlat_tnl2j")
+StaminaBar/base_type = &"ProgressBar"
+StaminaBar/styles/background = SubResource("StyleBoxFlat_k5faf")
+StaminaBar/styles/fill = SubResource("StyleBoxFlat_5j0a2")
+TabContainer/styles/panel = SubResource("StyleBoxFlat_odiue")
+TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_ghjya")
+TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_m6y06")
+TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_5icga")
diff --git a/menus/resources/themes/lore.tres b/menus/resources/themes/lore.tres
new file mode 100644
index 0000000..212993a
--- /dev/null
+++ b/menus/resources/themes/lore.tres
@@ -0,0 +1,190 @@
+[gd_resource type="Theme" load_steps=15 format=3 uid="uid://bcopca4uvqrrw"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8hxc2"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(0.631373, 0.52549, 0.619608, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.192157, 0.239216, 0.352941, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_daw1f"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.839216, 0.933333, 1, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_r1yu6"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wnp2l"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 8.0
+bg_color = Color(0.192157, 0.239216, 0.352941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2ymfe"]
+bg_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.8, 0.8, 0.8, 0)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pq3iw"]
+bg_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hvab5"]
+bg_color = Color(0.192157, 0.239216, 0.352941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f05by"]
+bg_color = Color(0.192157, 0.239216, 0.352941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t1x62"]
+bg_color = Color(0.929412, 0.921569, 0.627451, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.8, 0.8, 0.8, 0)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_w2bse"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_color = Color(0.839216, 0.933333, 1, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7eahf"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.192157, 0.239216, 0.352941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5sk2t"]
+content_margin_left = 8.0
+content_margin_top = 8.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_00w47"]
+bg_color = Color(0.839216, 0.933333, 1, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.8, 0.8, 0.8, 0)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gh53c"]
+bg_color = Color(0.631373, 0.52549, 0.619608, 1)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.8, 0.8, 0.8, 0)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[resource]
+Button/colors/font_color = Color(0.839216, 0.933333, 1, 1)
+Button/colors/font_disabled_color = Color(0.192157, 0.239216, 0.352941, 1)
+Button/colors/font_focus_color = Color(0.839216, 0.933333, 1, 1)
+Button/colors/font_hover_color = Color(0.839216, 0.933333, 1, 1)
+Button/colors/font_hover_pressed_color = Color(0.839216, 0.933333, 1, 1)
+Button/colors/font_pressed_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+Button/styles/disabled = SubResource("StyleBoxFlat_8hxc2")
+Button/styles/hover = SubResource("StyleBoxFlat_daw1f")
+Button/styles/normal = SubResource("StyleBoxFlat_r1yu6")
+Button/styles/pressed = SubResource("StyleBoxFlat_wnp2l")
+DelayProgressBar/base_type = &"ProgressBar"
+DelayProgressBar/styles/fill = SubResource("StyleBoxFlat_2ymfe")
+Label/colors/font_color = Color(0.839216, 0.933333, 1, 1)
+Label/colors/font_outline_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+Label/constants/outline_size = 8
+LineEdit/colors/caret_color = Color(0.839216, 0.933333, 1, 1)
+LineEdit/colors/font_color = Color(0.929412, 0.921569, 0.627451, 1)
+LineEdit/colors/font_uneditable_color = Color(0.192157, 0.239216, 0.352941, 1)
+LineEdit/styles/normal = SubResource("StyleBoxFlat_pq3iw")
+LineEdit/styles/read_only = SubResource("StyleBoxFlat_pq3iw")
+Panel/styles/panel = SubResource("StyleBoxFlat_hvab5")
+PanelContainer/styles/panel = SubResource("StyleBoxFlat_hvab5")
+ProgressBar/styles/background = SubResource("StyleBoxFlat_f05by")
+ProgressBar/styles/fill = SubResource("StyleBoxFlat_t1x62")
+RichTextLabel/colors/default_color = Color(0.839216, 0.933333, 1, 1)
+RichTextLabel/colors/font_outline_color = Color(0.00392157, 0.0862745, 0.152941, 1)
+RichTextLabel/constants/outline_size = 8
+TabContainer/styles/panel = SubResource("StyleBoxFlat_hvab5")
+TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_w2bse")
+TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_7eahf")
+TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_5sk2t")
+TimerProgressBar/base_type = &"ProgressBar"
+TimerProgressBar/styles/fill = SubResource("StyleBoxFlat_00w47")
+WarningProgressBar/base_type = &"ProgressBar"
+WarningProgressBar/styles/fill = SubResource("StyleBoxFlat_gh53c")
diff --git a/menus/resources/themes/steal_this_theme.tres b/menus/resources/themes/steal_this_theme.tres
new file mode 100644
index 0000000..695ae46
--- /dev/null
+++ b/menus/resources/themes/steal_this_theme.tres
@@ -0,0 +1,560 @@
+[gd_resource type="Theme" load_steps=44 format=3 uid="uid://bg5t434wh7tmh"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7rtxy"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.698039, 0.133333, 0.203922, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hofdy"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 0)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.235294, 0.231373, 0.431373, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+expand_margin_left = 2.0
+expand_margin_top = 2.0
+expand_margin_right = 2.0
+expand_margin_bottom = 2.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0ahyh"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_83bj2"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.973535, 0.973535, 0.973535, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bjb6u"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.235294, 0.231373, 0.431373, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wv8md"]
+bg_color = Color(0.698039, 0.133333, 0.203922, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s1fdf"]
+bg_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tieq2"]
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rrxf3"]
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.235294, 0.231373, 0.431373, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2e0dr"]
+content_margin_top = 3.0
+content_margin_bottom = 3.0
+bg_color = Color(0.0392157, 0.0392157, 0.0392157, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_fgisk"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fgisk"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5e2ta"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.0383972, 0.0383972, 0.0383972, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7e08u"]
+bg_color = Color(0, 0, 0, 1)
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_whago"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.235294, 0.231373, 0.431373, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_d8x3d"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.698039, 0.133333, 0.203922, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_hofdy"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lmfyq"]
+bg_color = Color(0.698039, 0.133333, 0.203922, 1)
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0, 0, 0, 0)
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_wv8md"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c4ulf"]
+bg_color = Color(0, 0, 0, 1)
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0, 0, 0, 0)
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8723n"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.235294, 0.231373, 0.431373, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_opsya"]
+bg_color = Color(0.235294, 0.231373, 0.431373, 1)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8g14u"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.0383972, 0.0383972, 0.0383972, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qnvbk"]
+content_margin_left = 8.0
+content_margin_top = 4.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_top = 2
+border_color = Color(0.698039, 0.133333, 0.203922, 1)
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_d8x3d"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4bfjk"]
+content_margin_left = 8.0
+content_margin_top = 4.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_top = 2
+border_color = Color(0.235294, 0.231373, 0.431373, 1)
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ojvr3"]
+content_margin_left = 8.0
+content_margin_top = 4.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0.0392157, 0.0392157, 0.0392157, 1)
+border_width_top = 2
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lop2v"]
+content_margin_left = 8.0
+content_margin_top = 4.0
+content_margin_right = 8.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_top = 2
+border_color = Color(0.0392157, 0.0392157, 0.0392157, 1)
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_0ahyh"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_83bj2"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rr4b1"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 0)
+border_width_left = 2
+border_width_top = 2
+border_width_right = 2
+border_width_bottom = 2
+border_color = Color(0.235294, 0.231373, 0.431373, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+expand_margin_left = 2.0
+expand_margin_top = 2.0
+expand_margin_right = 2.0
+expand_margin_bottom = 2.0
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bcw1c"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.0383972, 0.0383972, 0.0383972, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xi1kj"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rrcvo"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.235294, 0.231373, 0.431373, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wvge0"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.235294, 0.231373, 0.431373, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v0ggg"]
+bg_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_kvvmu"]
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.973535, 0.973535, 0.973535, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rum38"]
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.235294, 0.231373, 0.431373, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5wbgk"]
+content_margin_left = 3.0
+content_margin_right = 3.0
+bg_color = Color(0.0392157, 0.0392157, 0.0392157, 1)
+corner_radius_top_left = 2
+corner_radius_top_right = 2
+corner_radius_bottom_right = 2
+corner_radius_bottom_left = 2
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_s1fdf"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_x127s"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.698039, 0.133333, 0.203922, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0, 0, 0, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qjhx0"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.698039, 0.133333, 0.203922, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u427p"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0, 0, 0, 1)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(0.698039, 0.133333, 0.203922, 1)
+corner_radius_top_left = 4
+corner_radius_top_right = 4
+corner_radius_bottom_right = 4
+corner_radius_bottom_left = 4
+
+[resource]
+BoldLabel/base_type = &"Label"
+BoldLabel/fonts/font = null
+Button/colors/font_color = Color(0, 0, 0, 1)
+Button/colors/font_disabled_color = Color(0.698039, 0.133333, 0.203922, 1)
+Button/colors/font_focus_color = Color(0, 0, 0, 1)
+Button/colors/font_hover_color = Color(0.973535, 0.973535, 0.973535, 1)
+Button/colors/font_hover_pressed_color = Color(0.973535, 0.973535, 0.973535, 1)
+Button/colors/font_pressed_color = Color(0.235294, 0.231373, 0.431373, 1)
+Button/styles/disabled = SubResource("StyleBoxFlat_7rtxy")
+Button/styles/focus = SubResource("StyleBoxFlat_hofdy")
+Button/styles/hover = SubResource("StyleBoxFlat_0ahyh")
+Button/styles/normal = SubResource("StyleBoxFlat_83bj2")
+Button/styles/pressed = SubResource("StyleBoxFlat_bjb6u")
+DisabledLabel/base_type = &"Label"
+DisabledLabel/colors/font_color = Color(0, 0, 0, 1)
+DisabledLabel/font_sizes/font_size = 24
+DisabledLabel/fonts/font = null
+DisabledLabel/styles/normal = SubResource("StyleBoxFlat_wv8md")
+HScrollBar/styles/grabber = SubResource("StyleBoxFlat_s1fdf")
+HScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_tieq2")
+HScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_rrxf3")
+HScrollBar/styles/scroll = SubResource("StyleBoxFlat_2e0dr")
+HScrollBar/styles/scroll_focus = SubResource("StyleBoxEmpty_fgisk")
+LocationName/base_type = &"Label"
+LocationName/colors/font_color = Color(0.973535, 0.973535, 0.973535, 1)
+LocationName/font_sizes/font_size = 22
+LocationName/fonts/font = null
+LocationType/base_type = &"Label"
+LocationType/colors/font_color = Color(0.235294, 0.231373, 0.431373, 1)
+LocationType/fonts/font = null
+Panel/styles/panel = SubResource("StyleBoxFlat_fgisk")
+PanelContainer/styles/panel = SubResource("StyleBoxFlat_5e2ta")
+ProgressBar/styles/background = SubResource("StyleBoxFlat_7e08u")
+ProgressBar/styles/fill = SubResource("StyleBoxFlat_whago")
+ProgressBarBad/base_type = &"ProgressBar"
+ProgressBarBad/styles/fill = SubResource("StyleBoxFlat_d8x3d")
+ProgressBarDelay/base_type = &"ProgressBar"
+ProgressBarDelay/styles/background = SubResource("StyleBoxEmpty_hofdy")
+ProgressBarDelay/styles/fill = SubResource("StyleBoxFlat_lmfyq")
+ProgressBarDelayHovered/base_type = &"ProgressBar"
+ProgressBarDelayHovered/styles/background = SubResource("StyleBoxEmpty_wv8md")
+ProgressBarDelayHovered/styles/fill = SubResource("StyleBoxFlat_c4ulf")
+ProgressBarGood/base_type = &"ProgressBar"
+ProgressBarGood/styles/fill = SubResource("StyleBoxFlat_8723n")
+RichTextLabel/fonts/bold_font = null
+RichTextLabel/fonts/bold_italics_font = null
+RichTextLabel/fonts/italics_font = null
+SelectedLabel/base_type = &"Label"
+SelectedLabel/colors/font_color = Color(0.973535, 0.973535, 0.973535, 1)
+SelectedLabel/font_sizes/font_size = 24
+SelectedLabel/fonts/font = null
+SelectedLabel/styles/normal = SubResource("StyleBoxFlat_opsya")
+TabContainer/colors/font_disabled_color = Color(0.698039, 0.133333, 0.203922, 1)
+TabContainer/colors/font_hovered_color = Color(0.235294, 0.231373, 0.431373, 1)
+TabContainer/colors/font_selected_color = Color(0.973535, 0.973535, 0.973535, 1)
+TabContainer/colors/font_unselected_color = Color(0.973535, 0.973535, 0.973535, 1)
+TabContainer/styles/panel = SubResource("StyleBoxFlat_8g14u")
+TabContainer/styles/tab_disabled = SubResource("StyleBoxFlat_qnvbk")
+TabContainer/styles/tab_focus = SubResource("StyleBoxEmpty_d8x3d")
+TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_4bfjk")
+TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_ojvr3")
+TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_lop2v")
+Tree/colors/children_hl_line_color = Color(0.0392157, 0.0392157, 0.0392157, 1)
+Tree/colors/font_color = Color(0.973535, 0.973535, 0.973535, 1)
+Tree/colors/font_disabled_color = Color(0.698039, 0.133333, 0.203922, 1)
+Tree/colors/font_hovered_color = Color(0.235294, 0.231373, 0.431373, 1)
+Tree/colors/font_selected_color = Color(0.973535, 0.973535, 0.973535, 1)
+Tree/colors/guide_color = Color(0, 0, 0, 0)
+Tree/colors/parent_hl_line_color = Color(0.0392157, 0.0392157, 0.0392157, 1)
+Tree/colors/relationship_line_color = Color(0.0392157, 0.0392157, 0.0392157, 1)
+Tree/constants/inner_item_margin_left = 4
+Tree/constants/inner_item_margin_right = 4
+Tree/constants/item_margin = 0
+Tree/styles/button_hover = SubResource("StyleBoxEmpty_0ahyh")
+Tree/styles/button_pressed = SubResource("StyleBoxEmpty_83bj2")
+Tree/styles/focus = SubResource("StyleBoxFlat_rr4b1")
+Tree/styles/hovered = SubResource("StyleBoxFlat_bcw1c")
+Tree/styles/panel = SubResource("StyleBoxFlat_xi1kj")
+Tree/styles/selected = SubResource("StyleBoxFlat_rrcvo")
+Tree/styles/selected_focus = SubResource("StyleBoxFlat_wvge0")
+VScrollBar/styles/grabber = SubResource("StyleBoxFlat_v0ggg")
+VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_kvvmu")
+VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_rum38")
+VScrollBar/styles/scroll = SubResource("StyleBoxFlat_5wbgk")
+VScrollBar/styles/scroll_focus = SubResource("StyleBoxEmpty_s1fdf")
+WaitingButton/base_type = &"Button"
+WaitingButton/colors/font_color = Color(0.698039, 0.133333, 0.203922, 1)
+WaitingButton/colors/font_focus_color = Color(0.698039, 0.133333, 0.203922, 1)
+WaitingButton/colors/font_hover_color = Color(0, 0, 0, 1)
+WaitingButton/colors/font_hover_pressed_color = Color(0.698039, 0.133333, 0.203922, 1)
+WaitingButton/colors/font_pressed_color = Color(0.698039, 0.133333, 0.203922, 1)
+WaitingButton/styles/hover = SubResource("StyleBoxFlat_x127s")
+WaitingButton/styles/normal = SubResource("StyleBoxFlat_qjhx0")
+WaitingButton/styles/pressed = SubResource("StyleBoxFlat_u427p")
diff --git a/menus/scenes/credits/scrollable_credits.gd b/menus/scenes/credits/scrollable_credits.gd
new file mode 100644
index 0000000..46aa323
--- /dev/null
+++ b/menus/scenes/credits/scrollable_credits.gd
@@ -0,0 +1,2 @@
+@tool
+extends ScrollableCredits
diff --git a/menus/scenes/credits/scrollable_credits.gd.uid b/menus/scenes/credits/scrollable_credits.gd.uid
new file mode 100644
index 0000000..63ec90c
--- /dev/null
+++ b/menus/scenes/credits/scrollable_credits.gd.uid
@@ -0,0 +1 @@
+uid://6x1vpinoeg7h
diff --git a/menus/scenes/credits/scrollable_credits.tscn b/menus/scenes/credits/scrollable_credits.tscn
new file mode 100644
index 0000000..9c8848a
--- /dev/null
+++ b/menus/scenes/credits/scrollable_credits.tscn
@@ -0,0 +1,48 @@
+[gd_scene load_steps=3 format=3 uid="uid://ct0yseu6qy88d"]
+
+[ext_resource type="PackedScene" uid="uid://osxulxw2oas3" path="res://addons/maaacks_game_template/base/scenes/credits/scrollable_credits.tscn" id="1_xoaw6"]
+[ext_resource type="Script" uid="uid://6x1vpinoeg7h" path="res://menus/scenes/credits/scrollable_credits.gd" id="2_s1bbb"]
+
+[node name="ScrollableCredits" instance=ExtResource("1_xoaw6")]
+script = ExtResource("2_s1bbb")
+
+[node name="CreditsLabel" parent="." index="0"]
+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[/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=24]Godot Engine Logo[/font_size]
+Author: Andrea Calabró
+Source: [url=https://godotengine.org/press/]godotengine.org : press[/url]
+License: [url=https://github.com/godotengine/godot/blob/master/LOGO_LICENSE.txt]CC BY 4.0 International[/url]
+
+[font_size=48]Tools[/font_size]
+[font_size=24]Godot[/font_size]
+[img=80]res:///menus/assets/godot_engine_logo/logo_vertical_color_dark.png[/img]
+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]Godot Game Template[/font_size]
+[img=80]res:///menus/assets/icon.png[/img]
+Author: [url=https://github.com/Maaack/Godot-Game-Template/graphs/contributors]Marek Belski and contributors[/url]
+Source: [url=https://github.com/Maaack/Godot-Game-Template]github: Godot-Game-Template[/url]
+License: [url=LICENSE.txt]MIT License[/url]
+
+[font_size=24]Git[/font_size]
+[img=80]res:///menus/assets/git_logo/Git-Logo-2Color.png[/img]
+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]
+[/center]"
+attribution_file_path = "res://menus/ATTRIBUTION.md"
diff --git a/menus/scenes/credits/scrolling_credits.gd b/menus/scenes/credits/scrolling_credits.gd
new file mode 100644
index 0000000..3da8829
--- /dev/null
+++ b/menus/scenes/credits/scrolling_credits.gd
@@ -0,0 +1,2 @@
+@tool
+extends ScrollingCredits
diff --git a/menus/scenes/credits/scrolling_credits.gd.uid b/menus/scenes/credits/scrolling_credits.gd.uid
new file mode 100644
index 0000000..bc98c8e
--- /dev/null
+++ b/menus/scenes/credits/scrolling_credits.gd.uid
@@ -0,0 +1 @@
+uid://hkt3gnjndeoa
diff --git a/menus/scenes/credits/scrolling_credits.tscn b/menus/scenes/credits/scrolling_credits.tscn
new file mode 100644
index 0000000..ae32757
--- /dev/null
+++ b/menus/scenes/credits/scrolling_credits.tscn
@@ -0,0 +1,48 @@
+[gd_scene load_steps=3 format=3 uid="uid://degp6cr65aj0l"]
+
+[ext_resource type="PackedScene" uid="uid://t2dui8ppm3a4" path="res://addons/maaacks_game_template/base/scenes/credits/scrolling_credits.tscn" id="1_o3677"]
+[ext_resource type="Script" uid="uid://hkt3gnjndeoa" path="res://menus/scenes/credits/scrolling_credits.gd" id="2_6tdhy"]
+
+[node name="ScrollingCredits" instance=ExtResource("1_o3677")]
+script = ExtResource("2_6tdhy")
+
+[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[/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=24]Godot Engine Logo[/font_size]
+Author: Andrea Calabró
+Source: [url=https://godotengine.org/press/]godotengine.org : press[/url]
+License: [url=https://github.com/godotengine/godot/blob/master/LOGO_LICENSE.txt]CC BY 4.0 International[/url]
+
+[font_size=48]Tools[/font_size]
+[font_size=24]Godot[/font_size]
+[img=80]res:///menus/assets/godot_engine_logo/logo_vertical_color_dark.png[/img]
+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]Godot Game Template[/font_size]
+[img=80]res:///menus/assets/icon.png[/img]
+Author: [url=https://github.com/Maaack/Godot-Game-Template/graphs/contributors]Marek Belski and contributors[/url]
+Source: [url=https://github.com/Maaack/Godot-Game-Template]github: Godot-Game-Template[/url]
+License: [url=LICENSE.txt]MIT License[/url]
+
+[font_size=24]Git[/font_size]
+[img=80]res:///menus/assets/git_logo/Git-Logo-2Color.png[/img]
+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]
+[/center]"
+attribution_file_path = "res://menus/ATTRIBUTION.md"
diff --git a/menus/scenes/end_credits/end_credits.gd b/menus/scenes/end_credits/end_credits.gd
new file mode 100644
index 0000000..526c157
--- /dev/null
+++ b/menus/scenes/end_credits/end_credits.gd
@@ -0,0 +1,34 @@
+extends ScrollingCredits
+
+@export_file("*.tscn") var main_menu_scene : String
+@onready var init_mouse_filter : MouseFilter = mouse_filter
+
+func _on_scroll_container_end_reached() -> void:
+ %EndMessagePanel.show()
+ mouse_filter = Control.MOUSE_FILTER_STOP
+ super._on_scroll_container_end_reached()
+
+func _on_MenuButton_pressed() -> void:
+ SceneLoader.load_scene(main_menu_scene)
+
+func _on_ExitButton_pressed() -> void:
+ get_tree().quit()
+
+func _on_visibility_changed() -> void:
+ if visible:
+ %EndMessagePanel.hide()
+ mouse_filter = init_mouse_filter
+
+func _ready() -> void:
+ visibility_changed.connect(_on_visibility_changed)
+ if main_menu_scene.is_empty():
+ %MenuButton.hide()
+ if OS.has_feature("web"):
+ %ExitButton.hide()
+
+func _unhandled_input(event : InputEvent) -> void:
+ if event.is_action_pressed("ui_cancel"):
+ if not %EndMessagePanel.visible:
+ _on_scroll_container_end_reached()
+ else:
+ get_tree().quit()
diff --git a/menus/scenes/end_credits/end_credits.gd.uid b/menus/scenes/end_credits/end_credits.gd.uid
new file mode 100644
index 0000000..b81c34c
--- /dev/null
+++ b/menus/scenes/end_credits/end_credits.gd.uid
@@ -0,0 +1 @@
+uid://nn5qa7y0gpb6
diff --git a/menus/scenes/end_credits/end_credits.tscn b/menus/scenes/end_credits/end_credits.tscn
new file mode 100644
index 0000000..864dee6
--- /dev/null
+++ b/menus/scenes/end_credits/end_credits.tscn
@@ -0,0 +1,90 @@
+[gd_scene load_steps=5 format=3 uid="uid://dobyufvvon4i4"]
+
+[ext_resource type="PackedScene" uid="uid://degp6cr65aj0l" path="res://menus/scenes/credits/scrolling_credits.tscn" id="1_glhfm"]
+[ext_resource type="Script" uid="uid://nn5qa7y0gpb6" path="res://menus/scenes/end_credits/end_credits.gd" id="2_3mj4t"]
+[ext_resource type="PackedScene" uid="uid://bkcsjsk2ciff" path="res://addons/maaacks_game_template/base/scenes/music_players/background_music_player.tscn" id="3_yl3jd"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="4_fhtnl"]
+
+[node name="EndCredits" instance=ExtResource("1_glhfm")]
+script = ExtResource("2_3mj4t")
+main_menu_scene = "res://menus/scenes/menus/main_menu/main_menu_with_animations.tscn"
+
+[node name="BackgroundMusicPlayer" parent="." index="0" instance=ExtResource("3_yl3jd")]
+
+[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"]
+custom_minimum_size = Vector2(256, 0)
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/separation = 16
+script = ExtResource("4_fhtnl")
+
+[node name="ExitButton" type="Button" parent="CenterContainer/EndMessagePanel/VBoxContainer/CenterContainer/HBoxContainer" index="0"]
+unique_name_in_owner = true
+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
+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"]
diff --git a/menus/scenes/game_scene/configurable_sub_viewport.gd b/menus/scenes/game_scene/configurable_sub_viewport.gd
new file mode 100644
index 0000000..be48ec7
--- /dev/null
+++ b/menus/scenes/game_scene/configurable_sub_viewport.gd
@@ -0,0 +1,9 @@
+extends SubViewport
+
+@export var anti_aliasing_key : StringName = "Anti-aliasing"
+@export var video_section : StringName = AppSettings.VIDEO_SECTION
+
+func _ready() -> void:
+ var anti_aliasing : int = Config.get_config(video_section, anti_aliasing_key, Viewport.MSAA_DISABLED)
+ msaa_2d = anti_aliasing as MSAA
+ msaa_3d = anti_aliasing as MSAA
diff --git a/menus/scenes/game_scene/configurable_sub_viewport.gd.uid b/menus/scenes/game_scene/configurable_sub_viewport.gd.uid
new file mode 100644
index 0000000..84f734a
--- /dev/null
+++ b/menus/scenes/game_scene/configurable_sub_viewport.gd.uid
@@ -0,0 +1 @@
+uid://setdprunjids
diff --git a/menus/scenes/game_scene/game_ui.tscn b/menus/scenes/game_scene/game_ui.tscn
new file mode 100644
index 0000000..bc3098c
--- /dev/null
+++ b/menus/scenes/game_scene/game_ui.tscn
@@ -0,0 +1,63 @@
+[gd_scene load_steps=11 format=3 uid="uid://cn7ialakmhaeq"]
+
+[ext_resource type="Script" uid="uid://cyh0d64pfygbl" path="res://addons/maaacks_game_template/base/scripts/pause_menu_controller.gd" id="1_wm2gk"]
+[ext_resource type="PackedScene" uid="uid://ccqajqchiw4xu" path="res://menus/scenes/overlaid_menus/pause_menu.tscn" id="2_0bqsg"]
+[ext_resource type="PackedScene" uid="uid://bkcsjsk2ciff" path="res://addons/maaacks_game_template/base/scenes/music_players/background_music_player.tscn" id="3_aryyu"]
+[ext_resource type="Script" uid="uid://crbo2e4ndbyvk" path="res://addons/maaacks_game_template/extras/scripts/level_list_loader.gd" id="4_q70eh"]
+[ext_resource type="Script" uid="uid://3yfyhcjuxm0t" path="res://menus/scripts/level_list_and_state_manager.gd" id="5_cm6at"]
+[ext_resource type="PackedScene" uid="uid://dmq0tpdodtomh" path="res://menus/scenes/overlaid_menus/game_won_menu.tscn" id="6_72q1f"]
+[ext_resource type="PackedScene" uid="uid://ciyq8eiv1mtie" path="res://menus/scenes/overlaid_menus/level_lost_menu.tscn" id="7_mkrcq"]
+[ext_resource type="PackedScene" uid="uid://b46jlduh4lllk" path="res://menus/scenes/overlaid_menus/level_won_menu.tscn" id="8_sqpeu"]
+[ext_resource type="PackedScene" uid="uid://c63l6okbko3xp" path="res://menus/scenes/loading_screen/level_loading_screen.tscn" id="9_u7rb7"]
+[ext_resource type="Script" uid="uid://setdprunjids" path="res://menus/scenes/game_scene/configurable_sub_viewport.gd" id="10_kh6hf"]
+
+[node name="GameUI" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="PauseMenuController" type="Node" parent="." node_paths=PackedStringArray("focused_viewport")]
+script = ExtResource("1_wm2gk")
+pause_menu_packed = ExtResource("2_0bqsg")
+focused_viewport = NodePath("../ViewportContainer/ConfigurableSubViewport")
+
+[node name="BackgroundMusicPlayer" parent="." instance=ExtResource("3_aryyu")]
+
+[node name="LevelListLoader" type="Node" parent="." node_paths=PackedStringArray("level_container")]
+script = ExtResource("4_q70eh")
+level_container = NodePath("../ViewportContainer/ConfigurableSubViewport")
+files = Array[String](["res://menus/scenes/game_scene/levels/level_1.tscn", "res://menus/scenes/game_scene/levels/level_2.tscn", "res://menus/scenes/game_scene/levels/level_3.tscn"])
+directory = "res://menus/scenes/game_scene/levels"
+
+[node name="LevelListManager" type="Node" parent="." node_paths=PackedStringArray("level_list_loader", "level_loading_screen")]
+script = ExtResource("5_cm6at")
+level_list_loader = NodePath("../LevelListLoader")
+main_menu_scene = "res://menus/scenes/menus/main_menu/main_menu_with_animations.tscn"
+ending_scene = "res://menus/scenes/end_credits/end_credits.tscn"
+level_loading_screen = NodePath("../LevelLoadingScreen")
+game_won_scene = ExtResource("6_72q1f")
+level_lost_scene = ExtResource("7_mkrcq")
+level_won_scene = ExtResource("8_sqpeu")
+
+[node name="LevelLoadingScreen" parent="." instance=ExtResource("9_u7rb7")]
+visible = false
+
+[node name="ViewportContainer" type="SubViewportContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+stretch = true
+
+[node name="ConfigurableSubViewport" type="SubViewport" parent="ViewportContainer"]
+handle_input_locally = false
+audio_listener_enable_2d = true
+audio_listener_enable_3d = true
+size = Vector2i(1280, 720)
+render_target_update_mode = 4
+script = ExtResource("10_kh6hf")
diff --git a/menus/scenes/game_scene/input_display_label.gd b/menus/scenes/game_scene/input_display_label.gd
new file mode 100644
index 0000000..a57c4d2
--- /dev/null
+++ b/menus/scenes/game_scene/input_display_label.gd
@@ -0,0 +1,21 @@
+extends Label
+
+@onready var action_names := AppSettings.get_action_names()
+
+func _get_inputs_as_string() -> String:
+ var all_inputs : String = ""
+ var is_first : bool = true
+ for action_name in action_names:
+ if Input.is_action_pressed(action_name):
+ if is_first:
+ is_first = false
+ all_inputs += action_name
+ else:
+ all_inputs += " + " + action_name
+ return all_inputs
+
+func _process(_delta : float) -> void:
+ if Input.is_anything_pressed():
+ text = _get_inputs_as_string()
+ else:
+ text = ""
diff --git a/menus/scenes/game_scene/input_display_label.gd.uid b/menus/scenes/game_scene/input_display_label.gd.uid
new file mode 100644
index 0000000..8bd0716
--- /dev/null
+++ b/menus/scenes/game_scene/input_display_label.gd.uid
@@ -0,0 +1 @@
+uid://nvm4qcl05gmc
diff --git a/menus/scenes/game_scene/levels/level.gd b/menus/scenes/game_scene/levels/level.gd
new file mode 100644
index 0000000..88c20fb
--- /dev/null
+++ b/menus/scenes/game_scene/levels/level.gd
@@ -0,0 +1,31 @@
+extends Node
+
+signal level_won
+signal level_lost
+
+var level_state : LevelState
+
+func _on_lose_button_pressed() -> void:
+ level_lost.emit()
+
+func _on_win_button_pressed() -> void:
+ level_won.emit()
+
+func open_tutorials() -> void:
+ %TutorialManager.open_tutorials()
+ level_state.tutorial_read = true
+
+func _ready() -> void:
+ level_state = GameState.get_level_state(scene_file_path)
+ %ColorPickerButton.color = level_state.color
+ %BackgroundColor.color = level_state.color
+ if not level_state.tutorial_read:
+ open_tutorials()
+
+func _on_color_picker_button_color_changed(color : Color) -> void:
+ %BackgroundColor.color = color
+ level_state.color = color
+ GlobalState.save()
+
+func _on_tutorial_button_pressed() -> void:
+ open_tutorials()
diff --git a/menus/scenes/game_scene/levels/level.gd.uid b/menus/scenes/game_scene/levels/level.gd.uid
new file mode 100644
index 0000000..c5cbc4e
--- /dev/null
+++ b/menus/scenes/game_scene/levels/level.gd.uid
@@ -0,0 +1 @@
+uid://oqr1470sqa04
diff --git a/menus/scenes/game_scene/levels/level_1.tscn b/menus/scenes/game_scene/levels/level_1.tscn
new file mode 100644
index 0000000..e2b8a32
--- /dev/null
+++ b/menus/scenes/game_scene/levels/level_1.tscn
@@ -0,0 +1,90 @@
+[gd_scene load_steps=6 format=3 uid="uid://b0qcy25u3w436"]
+
+[ext_resource type="Script" uid="uid://oqr1470sqa04" path="res://menus/scenes/game_scene/levels/level.gd" id="1_gurrc"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_ou215"]
+[ext_resource type="Script" uid="uid://nvm4qcl05gmc" path="res://menus/scenes/game_scene/input_display_label.gd" id="3_c34o5"]
+[ext_resource type="Script" uid="uid://qf7h5xkvdgqw" path="res://menus/scenes/game_scene/tutorial_manager.gd" id="4_f47b8"]
+[ext_resource type="PackedScene" uid="uid://cy58hst85hxsj" path="res://menus/scenes/game_scene/tutorials/tutorial_1.tscn" id="5_4spcs"]
+
+[node name="Level1" 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_gurrc")
+
+[node name="BackgroundColor" type="ColorRect" 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
+color = Color(0, 0, 0, 1)
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 32
+theme_override_constants/margin_top = 32
+theme_override_constants/margin_right = 32
+theme_override_constants/margin_bottom = 32
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 16
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/separation = 32
+script = ExtResource("2_ou215")
+
+[node name="LoseButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Lose"
+
+[node name="WinButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Win"
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="TutorialButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+text = "Tutorial"
+
+[node name="InputDisplayLabel" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+size_flags_horizontal = 3
+horizontal_alignment = 1
+script = ExtResource("3_c34o5")
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+text = "Change Level State: "
+
+[node name="ColorPickerButton" type="ColorPickerButton" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Change Color"
+
+[node name="TutorialManager" type="Node" parent="."]
+unique_name_in_owner = true
+script = ExtResource("4_f47b8")
+tutorial_scenes = Array[PackedScene]([ExtResource("5_4spcs")])
+
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/LoseButton" to="." method="_on_lose_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/WinButton" to="." method="_on_win_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer2/TutorialButton" to="." method="_on_tutorial_button_pressed"]
+[connection signal="color_changed" from="MarginContainer/VBoxContainer/HBoxContainer2/ColorPickerButton" to="." method="_on_color_picker_button_color_changed"]
diff --git a/menus/scenes/game_scene/levels/level_2.tscn b/menus/scenes/game_scene/levels/level_2.tscn
new file mode 100644
index 0000000..6ebfa2f
--- /dev/null
+++ b/menus/scenes/game_scene/levels/level_2.tscn
@@ -0,0 +1,96 @@
+[gd_scene load_steps=6 format=3 uid="uid://cnywew70g84u8"]
+
+[ext_resource type="Script" uid="uid://oqr1470sqa04" path="res://menus/scenes/game_scene/levels/level.gd" id="1_3qleh"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_n4y0n"]
+[ext_resource type="Script" uid="uid://nvm4qcl05gmc" path="res://menus/scenes/game_scene/input_display_label.gd" id="3_fxw66"]
+[ext_resource type="Script" uid="uid://qf7h5xkvdgqw" path="res://menus/scenes/game_scene/tutorial_manager.gd" id="4_hrqtj"]
+[ext_resource type="PackedScene" uid="uid://dxrk0tt7ciipu" path="res://menus/scenes/game_scene/tutorials/tutorial_2.tscn" id="5_318fc"]
+
+[node name="Level2" 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_3qleh")
+
+[node name="BackgroundColor" type="ColorRect" 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
+color = Color(0, 0, 0, 1)
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 32
+theme_override_constants/margin_top = 32
+theme_override_constants/margin_right = 32
+theme_override_constants/margin_bottom = 32
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 16
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/separation = 32
+script = ExtResource("2_n4y0n")
+
+[node name="LoseButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Lose"
+
+[node name="WinButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Win"
+
+[node name="LoseButton2" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Lose"
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="TutorialButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+text = "Tutorial"
+
+[node name="InputDisplayLabel" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+size_flags_horizontal = 3
+horizontal_alignment = 1
+script = ExtResource("3_fxw66")
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+text = "Change Level State: "
+
+[node name="ColorPickerButton" type="ColorPickerButton" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Change Color"
+
+[node name="TutorialManager" type="Node" parent="."]
+unique_name_in_owner = true
+script = ExtResource("4_hrqtj")
+tutorial_scenes = Array[PackedScene]([ExtResource("5_318fc")])
+
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/LoseButton" to="." method="_on_lose_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/WinButton" to="." method="_on_win_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/LoseButton2" to="." method="_on_lose_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer2/TutorialButton" to="." method="_on_tutorial_button_pressed"]
+[connection signal="color_changed" from="MarginContainer/VBoxContainer/HBoxContainer2/ColorPickerButton" to="." method="_on_color_picker_button_color_changed"]
diff --git a/menus/scenes/game_scene/levels/level_3.tscn b/menus/scenes/game_scene/levels/level_3.tscn
new file mode 100644
index 0000000..698ca0e
--- /dev/null
+++ b/menus/scenes/game_scene/levels/level_3.tscn
@@ -0,0 +1,102 @@
+[gd_scene load_steps=6 format=3 uid="uid://5k7yo8y0x1th"]
+
+[ext_resource type="Script" uid="uid://oqr1470sqa04" path="res://menus/scenes/game_scene/levels/level.gd" id="1_x17wb"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_fxhbp"]
+[ext_resource type="Script" uid="uid://nvm4qcl05gmc" path="res://menus/scenes/game_scene/input_display_label.gd" id="3_5rvw0"]
+[ext_resource type="Script" uid="uid://qf7h5xkvdgqw" path="res://menus/scenes/game_scene/tutorial_manager.gd" id="4_eriqx"]
+[ext_resource type="PackedScene" uid="uid://belanf70yj2sq" path="res://menus/scenes/game_scene/tutorials/tutorial_3.tscn" id="5_fejky"]
+
+[node name="Level3" 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_x17wb")
+
+[node name="BackgroundColor" type="ColorRect" 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
+color = Color(0, 0, 0, 1)
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 32
+theme_override_constants/margin_top = 32
+theme_override_constants/margin_right = 32
+theme_override_constants/margin_bottom = 32
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+theme_override_constants/separation = 16
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/separation = 32
+script = ExtResource("2_fxhbp")
+
+[node name="WinButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Win"
+
+[node name="LoseButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Lose"
+
+[node name="LoseButton2" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Lose"
+
+[node name="LoseButton3" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Lose"
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="TutorialButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+text = "Tutorial"
+
+[node name="InputDisplayLabel" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+size_flags_horizontal = 3
+horizontal_alignment = 1
+script = ExtResource("3_5rvw0")
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+text = "Change Level State: "
+
+[node name="ColorPickerButton" type="ColorPickerButton" parent="MarginContainer/VBoxContainer/HBoxContainer2"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Change Color"
+
+[node name="TutorialManager" type="Node" parent="."]
+unique_name_in_owner = true
+script = ExtResource("4_eriqx")
+tutorial_scenes = Array[PackedScene]([ExtResource("5_fejky")])
+
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/WinButton" to="." method="_on_win_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/LoseButton" to="." method="_on_lose_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/LoseButton2" to="." method="_on_lose_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/LoseButton3" to="." method="_on_lose_button_pressed"]
+[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer2/TutorialButton" to="." method="_on_tutorial_button_pressed"]
+[connection signal="color_changed" from="MarginContainer/VBoxContainer/HBoxContainer2/ColorPickerButton" to="." method="_on_color_picker_button_color_changed"]
diff --git a/menus/scenes/game_scene/tutorial_manager.gd b/menus/scenes/game_scene/tutorial_manager.gd
new file mode 100644
index 0000000..d3f7b90
--- /dev/null
+++ b/menus/scenes/game_scene/tutorial_manager.gd
@@ -0,0 +1,19 @@
+extends Node
+@export var tutorial_scenes : Array[PackedScene]
+@export var open_delay : float = 0.25
+@export var auto_open : bool = false
+
+func open_tutorials() -> void:
+ if open_delay > 0.0:
+ await get_tree().create_timer(open_delay, false).timeout
+ for tutorial_scene in tutorial_scenes:
+ var tutorial_menu : OverlaidMenu = tutorial_scene.instantiate()
+ if tutorial_menu == null:
+ push_warning("tutorial failed to open %s" % tutorial_scene)
+ return
+ get_tree().current_scene.call_deferred("add_child", tutorial_menu)
+ await tutorial_menu.tree_exited
+
+func _ready() -> void:
+ if auto_open:
+ open_tutorials()
diff --git a/menus/scenes/game_scene/tutorial_manager.gd.uid b/menus/scenes/game_scene/tutorial_manager.gd.uid
new file mode 100644
index 0000000..4f48d3e
--- /dev/null
+++ b/menus/scenes/game_scene/tutorial_manager.gd.uid
@@ -0,0 +1 @@
+uid://qf7h5xkvdgqw
diff --git a/menus/scenes/game_scene/tutorials/tutorial_1.tscn b/menus/scenes/game_scene/tutorials/tutorial_1.tscn
new file mode 100644
index 0000000..ea7583c
--- /dev/null
+++ b/menus/scenes/game_scene/tutorials/tutorial_1.tscn
@@ -0,0 +1,25 @@
+[gd_scene load_steps=2 format=3 uid="uid://cy58hst85hxsj"]
+
+[ext_resource type="PackedScene" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_apclv"]
+
+[node name="Tutorial1" instance=ExtResource("1_apclv")]
+
+[node name="MenuPanelContainer" parent="." index="1"]
+offset_left = -206.0
+offset_top = -75.0
+offset_right = 215.0
+offset_bottom = 76.0
+
+[node name="BoxContainer" parent="MenuPanelContainer/MarginContainer" index="0"]
+theme_override_constants/separation = 16
+
+[node name="TitleLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/TitleMargin" index="0"]
+text = "Tutorial"
+
+[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
+visible = true
+
+[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
+text = "[center]Click the Win button to progress.
+Click the Lose button to try again.[/center]"
+fit_content = true
diff --git a/menus/scenes/game_scene/tutorials/tutorial_2.tscn b/menus/scenes/game_scene/tutorials/tutorial_2.tscn
new file mode 100644
index 0000000..0e78f30
--- /dev/null
+++ b/menus/scenes/game_scene/tutorials/tutorial_2.tscn
@@ -0,0 +1,25 @@
+[gd_scene load_steps=2 format=3 uid="uid://dxrk0tt7ciipu"]
+
+[ext_resource type="PackedScene" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_cg7og"]
+
+[node name="Tutorial2" instance=ExtResource("1_cg7og")]
+
+[node name="MenuPanelContainer" parent="." index="1"]
+offset_left = -206.0
+offset_top = -75.0
+offset_right = 215.0
+offset_bottom = 76.0
+
+[node name="BoxContainer" parent="MenuPanelContainer/MarginContainer" index="0"]
+theme_override_constants/separation = 16
+
+[node name="TitleLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/TitleMargin" index="0"]
+text = "Tutorial"
+
+[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
+visible = true
+
+[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
+text = "[center]Progress is saved.
+Pressing Continue from the main menu will load the last level played.[/center]"
+fit_content = true
diff --git a/menus/scenes/game_scene/tutorials/tutorial_3.tscn b/menus/scenes/game_scene/tutorials/tutorial_3.tscn
new file mode 100644
index 0000000..ecfa901
--- /dev/null
+++ b/menus/scenes/game_scene/tutorials/tutorial_3.tscn
@@ -0,0 +1,26 @@
+[gd_scene load_steps=2 format=3 uid="uid://belanf70yj2sq"]
+
+[ext_resource type="PackedScene" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_w71gn"]
+
+[node name="Tutorial2" instance=ExtResource("1_w71gn")]
+
+[node name="MenuPanelContainer" parent="." index="1"]
+offset_left = -206.0
+offset_top = -75.0
+offset_right = 215.0
+offset_bottom = 76.0
+
+[node name="BoxContainer" parent="MenuPanelContainer/MarginContainer" index="0"]
+theme_override_constants/separation = 16
+
+[node name="TitleLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/TitleMargin" index="0"]
+text = "Tutorial"
+
+[node name="DescriptionMargin" parent="MenuPanelContainer/MarginContainer/BoxContainer" index="1"]
+visible = true
+
+[node name="DescriptionLabel" parent="MenuPanelContainer/MarginContainer/BoxContainer/DescriptionMargin" index="0"]
+text = "[center]The color picker at the bottom-right updates the level state. This change persists until the game is reset.
+
+The label at the bottom-center displays the current input action detected, if any are setup for the project.[/center]"
+fit_content = true
diff --git a/menus/scenes/loading_screen/level_loading_screen.tscn b/menus/scenes/loading_screen/level_loading_screen.tscn
new file mode 100644
index 0000000..1b78001
--- /dev/null
+++ b/menus/scenes/loading_screen/level_loading_screen.tscn
@@ -0,0 +1,11 @@
+[gd_scene load_steps=3 format=3 uid="uid://c63l6okbko3xp"]
+
+[ext_resource type="PackedScene" uid="uid://cd0jbh4metflb" path="res://addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.tscn" id="1_8i8ik"]
+[ext_resource type="Script" uid="uid://1j5xbugtakig" path="res://menus/scenes/loading_screen/loading_screen.gd" id="2_3hi1w"]
+
+[node name="LevelLoadingScreen" instance=ExtResource("1_8i8ik")]
+script = ExtResource("2_3hi1w")
+_in_progress = "Loading Level..."
+_in_progress_waiting = "Still Loading Level..."
+_in_progress_still_waiting = "Still Loading Level... (%d seconds)"
+_complete = "Loading Level Complete!"
diff --git a/menus/scenes/loading_screen/loading_screen.gd b/menus/scenes/loading_screen/loading_screen.gd
new file mode 100644
index 0000000..c454123
--- /dev/null
+++ b/menus/scenes/loading_screen/loading_screen.gd
@@ -0,0 +1 @@
+extends LoadingScreen
diff --git a/menus/scenes/loading_screen/loading_screen.gd.uid b/menus/scenes/loading_screen/loading_screen.gd.uid
new file mode 100644
index 0000000..ff6a895
--- /dev/null
+++ b/menus/scenes/loading_screen/loading_screen.gd.uid
@@ -0,0 +1 @@
+uid://1j5xbugtakig
diff --git a/menus/scenes/loading_screen/loading_screen.tscn b/menus/scenes/loading_screen/loading_screen.tscn
new file mode 100644
index 0000000..4c70f90
--- /dev/null
+++ b/menus/scenes/loading_screen/loading_screen.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://dshcs2ioahnvg"]
+
+[ext_resource type="PackedScene" uid="uid://cd0jbh4metflb" path="res://addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.tscn" id="1_rqt5e"]
+[ext_resource type="Script" uid="uid://1j5xbugtakig" path="res://menus/scenes/loading_screen/loading_screen.gd" id="2_t84qw"]
+
+[node name="LoadingScreen" instance=ExtResource("1_rqt5e")]
+script = ExtResource("2_t84qw")
diff --git a/menus/scenes/loading_screen/loading_screen_with_shader_caching.gd b/menus/scenes/loading_screen/loading_screen_with_shader_caching.gd
new file mode 100644
index 0000000..32929ab
--- /dev/null
+++ b/menus/scenes/loading_screen/loading_screen_with_shader_caching.gd
@@ -0,0 +1,91 @@
+extends LoadingScreen
+
+@export_dir var _spatial_shader_material_dir : String
+@export_file("*.tscn") var _cache_shaders_scene : String
+@export var _mesh : Mesh
+@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() -> bool:
+ 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() -> void:
+ var partial_total := _scene_loading_progress
+ if can_load_shader_cache():
+ partial_total += _caching_progress
+ partial_total /= 2
+ _total_loading_progress = partial_total
+
+func _set_scene_loading_complete() -> void:
+ super._set_scene_loading_complete()
+ 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
+ SceneLoader._background_loading = false
+ SceneLoader.set_process(true)
+
+func _show_all_draw_passes_once() -> void:
+ 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) -> void:
+ var material_shower := MeshInstance3D.new()
+ material_shower.mesh = _mesh
+ var material := ResourceLoader.load(path) as Material
+ material_shower.set_surface_override_material(0, material)
+ %SpatialShaderTypeCaches.add_child(material_shower)
+
+func _ready() -> void:
+ SceneLoader._background_loading = true
diff --git a/menus/scenes/loading_screen/loading_screen_with_shader_caching.gd.uid b/menus/scenes/loading_screen/loading_screen_with_shader_caching.gd.uid
new file mode 100644
index 0000000..4fdabb1
--- /dev/null
+++ b/menus/scenes/loading_screen/loading_screen_with_shader_caching.gd.uid
@@ -0,0 +1 @@
+uid://cit747gi4v6jd
diff --git a/menus/scenes/loading_screen/loading_screen_with_shader_caching.tscn b/menus/scenes/loading_screen/loading_screen_with_shader_caching.tscn
new file mode 100644
index 0000000..f70b9b9
--- /dev/null
+++ b/menus/scenes/loading_screen/loading_screen_with_shader_caching.tscn
@@ -0,0 +1,22 @@
+[gd_scene load_steps=4 format=3 uid="uid://b8sfwmevux8s1"]
+
+[ext_resource type="PackedScene" uid="uid://cd0jbh4metflb" path="res://addons/maaacks_game_template/base/scenes/loading_screen/loading_screen.tscn" id="1_b2x1j"]
+[ext_resource type="Script" uid="uid://cit747gi4v6jd" path="res://menus/scenes/loading_screen/loading_screen_with_shader_caching.gd" id="2_n73ts"]
+
+[sub_resource type="QuadMesh" id="QuadMesh_klnwy"]
+
+[node name="LoadingScreen" instance=ExtResource("1_b2x1j")]
+script = ExtResource("2_n73ts")
+_spatial_shader_material_dir = ""
+_cache_shaders_scene = "res://menus/scenes/game_scene/game_ui.tscn"
+_mesh = SubResource("QuadMesh_klnwy")
+_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
diff --git a/menus/scenes/menus/level_select_menu/level_select_menu.gd b/menus/scenes/menus/level_select_menu/level_select_menu.gd
new file mode 100644
index 0000000..b747c4f
--- /dev/null
+++ b/menus/scenes/menus/level_select_menu/level_select_menu.gd
@@ -0,0 +1,33 @@
+extends Control
+
+## Loads a simple ItemList node within a margin container. SceneLister updates
+## the available scenes in the directory provided. Activating a level will update
+## the GameState's current_level, and emit a signal. The main menu node will trigger
+## a load action from that signal.
+
+@onready var level_buttons_container: ItemList = %LevelButtonsContainer
+@onready var scene_lister: SceneLister = $SceneLister
+
+signal level_selected
+
+func _ready() -> void:
+ add_levels_to_container()
+
+## A fresh level list is propgated into the ItemList, and the file names are cleaned
+func add_levels_to_container() -> void:
+ level_buttons_container.clear()
+ var max_level_reached := GameState.get_max_level_reached()
+ var level_iter := 0
+ for file_path in scene_lister.files:
+ if level_iter > max_level_reached : break
+ level_iter += 1
+ var file_name := file_path.get_file() # e.g., "level_1.tscn"
+ file_name = file_name.trim_suffix(".tscn") # Remove the ".tscn" extension
+ file_name = file_name.replace("_", " ") # Replace underscores with spaces
+ file_name = file_name.capitalize() # Convert to proper case
+ var button_name := str(file_name)
+ level_buttons_container.add_item(button_name)
+
+func _on_level_buttons_container_item_activated(index: int) -> void:
+ GameState.set_current_level(index)
+ level_selected.emit()
diff --git a/menus/scenes/menus/level_select_menu/level_select_menu.gd.uid b/menus/scenes/menus/level_select_menu/level_select_menu.gd.uid
new file mode 100644
index 0000000..bed9bd1
--- /dev/null
+++ b/menus/scenes/menus/level_select_menu/level_select_menu.gd.uid
@@ -0,0 +1 @@
+uid://x734txqgcn84
diff --git a/menus/scenes/menus/level_select_menu/level_select_menu.tscn b/menus/scenes/menus/level_select_menu/level_select_menu.tscn
new file mode 100644
index 0000000..08a0f6b
--- /dev/null
+++ b/menus/scenes/menus/level_select_menu/level_select_menu.tscn
@@ -0,0 +1,49 @@
+[gd_scene load_steps=4 format=3 uid="uid://fybssggvts5q"]
+
+[ext_resource type="Script" uid="uid://x734txqgcn84" path="res://menus/scenes/menus/level_select_menu/level_select_menu.gd" id="1_wxb4a"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_oixp6"]
+[ext_resource type="Script" uid="uid://wjq7li836lwj" path="res://addons/maaacks_game_template/extras/scripts/scene_lister.gd" id="3_stdqw"]
+
+[node name="LevelSelectMenu" type="Control"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_wxb4a")
+
+[node name="Control" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("2_oixp6")
+
+[node name="LevelButtonsContainer" type="ItemList" parent="Control"]
+unique_name_in_owner = true
+custom_minimum_size = Vector2(400, 0)
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -200.0
+offset_top = -17.5
+offset_right = 200.0
+offset_bottom = 17.5
+grow_horizontal = 2
+grow_vertical = 2
+auto_height = true
+item_count = 1
+item_0/text = "1 - ExampleLevel"
+
+[node name="SceneLister" type="Node" parent="."]
+script = ExtResource("3_stdqw")
+files = Array[String](["res://menus/scenes/game_scene/levels/level_1.tscn", "res://menus/scenes/game_scene/levels/level_2.tscn", "res://menus/scenes/game_scene/levels/level_3.tscn"])
+directory = "res://menus/scenes/game_scene/levels"
+
+[connection signal="item_activated" from="Control/LevelButtonsContainer" to="." method="_on_level_buttons_container_item_activated"]
diff --git a/menus/scenes/menus/main_menu/main_menu.gd b/menus/scenes/menus/main_menu/main_menu.gd
new file mode 100644
index 0000000..4c83b4f
--- /dev/null
+++ b/menus/scenes/menus/main_menu/main_menu.gd
@@ -0,0 +1 @@
+extends MainMenu
diff --git a/menus/scenes/menus/main_menu/main_menu.gd.uid b/menus/scenes/menus/main_menu/main_menu.gd.uid
new file mode 100644
index 0000000..22f7e3c
--- /dev/null
+++ b/menus/scenes/menus/main_menu/main_menu.gd.uid
@@ -0,0 +1 @@
+uid://cns18c1ywt06b
diff --git a/menus/scenes/menus/main_menu/main_menu.tscn b/menus/scenes/menus/main_menu/main_menu.tscn
new file mode 100644
index 0000000..8e01822
--- /dev/null
+++ b/menus/scenes/menus/main_menu/main_menu.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=5 format=3 uid="uid://1on734grcvxa"]
+
+[ext_resource type="PackedScene" uid="uid://c6k5nnpbypshi" path="res://addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn" id="1_557p1"]
+[ext_resource type="Script" uid="uid://cns18c1ywt06b" path="res://menus/scenes/menus/main_menu/main_menu.gd" id="2_wwoqg"]
+[ext_resource type="PackedScene" uid="uid://bq2ti3hrjlgdl" path="res://menus/scenes/menus/options_menu/master_options_menu_with_tabs.tscn" id="3_n1pnu"]
+[ext_resource type="PackedScene" uid="uid://ct0yseu6qy88d" path="res://menus/scenes/credits/scrollable_credits.tscn" id="4_0emlv"]
+
+[node name="MainMenu" instance=ExtResource("1_557p1")]
+script = ExtResource("2_wwoqg")
+game_scene_path = "res://menus/scenes/game_scene/game_ui.tscn"
+options_packed_scene = ExtResource("3_n1pnu")
+credits_packed_scene = ExtResource("4_0emlv")
diff --git a/menus/scenes/menus/main_menu/main_menu_with_animations.gd b/menus/scenes/menus/main_menu/main_menu_with_animations.gd
new file mode 100644
index 0000000..7eb12ad
--- /dev/null
+++ b/menus/scenes/menus/main_menu/main_menu_with_animations.gd
@@ -0,0 +1,69 @@
+extends MainMenu
+
+@export var level_select_packed_scene: PackedScene
+
+var level_select_scene : Node
+var animation_state_machine : AnimationNodeStateMachinePlayback
+
+func load_game_scene() -> void:
+ GameState.start_game()
+ super.load_game_scene()
+
+func new_game() -> void:
+ GlobalState.reset()
+ load_game_scene()
+
+func intro_done() -> void:
+ animation_state_machine.travel("OpenMainMenu")
+
+func _is_in_intro() -> bool:
+ return animation_state_machine.get_current_node() == "Intro"
+
+func _event_is_mouse_button_released(event : InputEvent) -> bool:
+ return event is InputEventMouseButton and not event.is_pressed()
+
+func _event_skips_intro(event : InputEvent) -> bool:
+ 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 : Node) -> void:
+ super._open_sub_menu(menu)
+ animation_state_machine.travel("OpenSubMenu")
+
+func _close_sub_menu() -> void:
+ super._close_sub_menu()
+ animation_state_machine.travel("OpenMainMenu")
+
+func _input(event : InputEvent) -> void:
+ if _is_in_intro() and _event_skips_intro(event):
+ intro_done()
+ return
+ super._input(event)
+
+func _add_level_select_if_set() -> void:
+ if level_select_packed_scene == null: return
+ if GameState.get_max_level_reached() <= 0 : return
+ level_select_scene = level_select_packed_scene.instantiate()
+ level_select_scene.hide()
+ %LevelSelectContainer.call_deferred("add_child", level_select_scene)
+ if level_select_scene.has_signal("level_selected"):
+ level_select_scene.connect("level_selected", load_game_scene)
+ %LevelSelectButton.show()
+
+func _show_continue_if_set() -> void:
+ if GameState.has_game_state():
+ %ContinueGameButton.show()
+
+func _ready() -> void:
+ super._ready()
+ _add_level_select_if_set()
+ _show_continue_if_set()
+ animation_state_machine = $MenuAnimationTree.get("parameters/playback")
+
+func _on_continue_game_button_pressed() -> void:
+ load_game_scene()
+
+func _on_level_select_button_pressed() -> void:
+ _open_sub_menu(level_select_scene)
diff --git a/menus/scenes/menus/main_menu/main_menu_with_animations.gd.uid b/menus/scenes/menus/main_menu/main_menu_with_animations.gd.uid
new file mode 100644
index 0000000..1e51644
--- /dev/null
+++ b/menus/scenes/menus/main_menu/main_menu_with_animations.gd.uid
@@ -0,0 +1 @@
+uid://c0ntfmiuw4ttg
diff --git a/menus/scenes/menus/main_menu/main_menu_with_animations.tscn b/menus/scenes/menus/main_menu/main_menu_with_animations.tscn
new file mode 100644
index 0000000..eaf0e0f
--- /dev/null
+++ b/menus/scenes/menus/main_menu/main_menu_with_animations.tscn
@@ -0,0 +1,417 @@
+[gd_scene load_steps=18 format=3 uid="uid://vm22i5sv3p3s"]
+
+[ext_resource type="PackedScene" uid="uid://c6k5nnpbypshi" path="res://addons/maaacks_game_template/base/scenes/menus/main_menu/main_menu.tscn" id="1_my8ii"]
+[ext_resource type="Script" uid="uid://c0ntfmiuw4ttg" path="res://menus/scenes/menus/main_menu/main_menu_with_animations.gd" id="2_nyjay"]
+[ext_resource type="PackedScene" uid="uid://bq2ti3hrjlgdl" path="res://menus/scenes/menus/options_menu/master_options_menu_with_tabs.tscn" id="3_f368h"]
+[ext_resource type="PackedScene" uid="uid://ct0yseu6qy88d" path="res://menus/scenes/credits/scrollable_credits.tscn" id="4_85otm"]
+
+[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/TitleMargin/TitleContainer: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/SubTitleMargin/SubTitleContainer: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/MenuButtonsMargin/MenuButtonsContainer: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("VersionMargin/VersionContainer: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/TitleMargin/TitleContainer: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/SubTitleMargin/SubTitleContainer: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/MenuButtonsMargin/MenuButtonsContainer: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("VersionMargin/VersionContainer: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/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer: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/TitleMargin/TitleContainer: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/SubTitleMargin/SubTitleContainer: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/MenuButtonsMargin/MenuButtonsContainer: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("VersionMargin/VersionContainer: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/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer: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_my8ii")]
+script = ExtResource("2_nyjay")
+level_select_packed_scene = null
+game_scene_path = "uid://cxbskue0lj2gv"
+options_packed_scene = ExtResource("3_f368h")
+credits_packed_scene = ExtResource("4_85otm")
+
+[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="BackgroundMusicPlayer" parent="." index="3"]
+bus = &"Master"
+
+[node name="VersionContainer" parent="VersionMargin" index="0"]
+modulate = Color(1, 1, 1, 0)
+
+[node name="TitleContainer" parent="MenuContainer/TitleMargin" index="0"]
+modulate = Color(1, 1, 1, 0)
+
+[node name="TitleLabel" parent="MenuContainer/TitleMargin/TitleContainer" index="0"]
+text = "Movement tests"
+
+[node name="SubTitleContainer" parent="MenuContainer/SubTitleMargin" index="0"]
+modulate = Color(1, 1, 1, 0)
+
+[node name="SubTitleLabel" parent="MenuContainer/SubTitleMargin/SubTitleContainer" index="0"]
+text = "A prototype"
+
+[node name="MenuButtonsContainer" parent="MenuContainer/MenuButtonsMargin" index="0"]
+modulate = Color(1, 1, 1, 0)
+
+[node name="MenuButtonsBoxContainer" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer" index="0"]
+lock = true
+
+[node name="ContinueGameButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer" index="1"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+text = "Continue"
+
+[node name="LevelSelectButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer" index="2"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+text = "Level Select"
+
+[node name="LevelSelectContainer" type="MarginContainer" parent="." index="9"]
+unique_name_in_owner = true
+layout_mode = 0
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 2
+theme_override_constants/margin_left = 16
+theme_override_constants/margin_top = 32
+theme_override_constants/margin_right = 16
+theme_override_constants/margin_bottom = 32
+
+[connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/ContinueGameButton" to="." method="_on_continue_game_button_pressed"]
+[connection signal="pressed" from="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer/LevelSelectButton" to="." method="_on_level_select_button_pressed"]
diff --git a/menus/scenes/menus/options_menu/audio/audio_input_option_control.gd b/menus/scenes/menus/options_menu/audio/audio_input_option_control.gd
new file mode 100644
index 0000000..da3199a
--- /dev/null
+++ b/menus/scenes/menus/options_menu/audio/audio_input_option_control.gd
@@ -0,0 +1,38 @@
+@tool
+extends ListOptionControl
+
+func _set_input_device() -> void:
+ var current_setting : Variant = _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() -> void:
+ 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 : Variant) -> void:
+ 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)
diff --git a/menus/scenes/menus/options_menu/audio/audio_input_option_control.gd.uid b/menus/scenes/menus/options_menu/audio/audio_input_option_control.gd.uid
new file mode 100644
index 0000000..36a8007
--- /dev/null
+++ b/menus/scenes/menus/options_menu/audio/audio_input_option_control.gd.uid
@@ -0,0 +1 @@
+uid://cx21fockjcehv
diff --git a/menus/scenes/menus/options_menu/audio/audio_input_option_control.tscn b/menus/scenes/menus/options_menu/audio/audio_input_option_control.tscn
new file mode 100644
index 0000000..c954a27
--- /dev/null
+++ b/menus/scenes/menus/options_menu/audio/audio_input_option_control.tscn
@@ -0,0 +1,20 @@
+[gd_scene load_steps=3 format=3 uid="uid://cimjrqopk7trl"]
+
+[ext_resource type="PackedScene" uid="uid://b6bl3n5mp3m1e" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn" id="1_xywkw"]
+[ext_resource type="Script" uid="uid://cx21fockjcehv" path="res://menus/scenes/menus/options_menu/audio/audio_input_option_control.gd" id="2_cgbct"]
+
+[node name="AudioInputOptionControl" instance=ExtResource("1_xywkw")]
+script = ExtResource("2_cgbct")
+option_name = "Input Device"
+option_section = 2
+key = "InputDevice"
+section = "AudioSettings"
+property_type = 4
+
+[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
diff --git a/menus/scenes/menus/options_menu/audio/audio_options_menu.gd b/menus/scenes/menus/options_menu/audio/audio_options_menu.gd
new file mode 100644
index 0000000..d125ea0
--- /dev/null
+++ b/menus/scenes/menus/options_menu/audio/audio_options_menu.gd
@@ -0,0 +1 @@
+extends AudioOptionsMenu
diff --git a/menus/scenes/menus/options_menu/audio/audio_options_menu.gd.uid b/menus/scenes/menus/options_menu/audio/audio_options_menu.gd.uid
new file mode 100644
index 0000000..d07bced
--- /dev/null
+++ b/menus/scenes/menus/options_menu/audio/audio_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://ccd8k37glks2h
diff --git a/menus/scenes/menus/options_menu/audio/audio_options_menu.tscn b/menus/scenes/menus/options_menu/audio/audio_options_menu.tscn
new file mode 100644
index 0000000..be05650
--- /dev/null
+++ b/menus/scenes/menus/options_menu/audio/audio_options_menu.tscn
@@ -0,0 +1,11 @@
+[gd_scene load_steps=4 format=3 uid="uid://c7ulxshxe636e"]
+
+[ext_resource type="PackedScene" uid="uid://c8vnncjwqcpab" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/audio/audio_options_menu.tscn" id="1_8yusf"]
+[ext_resource type="Script" uid="uid://ccd8k37glks2h" path="res://menus/scenes/menus/options_menu/audio/audio_options_menu.gd" id="2_85b82"]
+[ext_resource type="PackedScene" uid="uid://cimjrqopk7trl" path="res://menus/scenes/menus/options_menu/audio/audio_input_option_control.tscn" id="3_3g00r"]
+
+[node name="Audio" instance=ExtResource("1_8yusf")]
+script = ExtResource("2_85b82")
+
+[node name="AudioInputOptionControl" parent="VBoxContainer" index="2" instance=ExtResource("3_3g00r")]
+layout_mode = 2
diff --git a/menus/scenes/menus/options_menu/game/game_options_menu.gd b/menus/scenes/menus/options_menu/game/game_options_menu.gd
new file mode 100644
index 0000000..a6b10dc
--- /dev/null
+++ b/menus/scenes/menus/options_menu/game/game_options_menu.gd
@@ -0,0 +1,4 @@
+extends Control
+
+func _on_ResetGameControl_reset_confirmed() -> void:
+ GlobalState.reset()
diff --git a/menus/scenes/menus/options_menu/game/game_options_menu.gd.uid b/menus/scenes/menus/options_menu/game/game_options_menu.gd.uid
new file mode 100644
index 0000000..c6b56c6
--- /dev/null
+++ b/menus/scenes/menus/options_menu/game/game_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://do8gkcahgjpj2
diff --git a/menus/scenes/menus/options_menu/game/game_options_menu.tscn b/menus/scenes/menus/options_menu/game/game_options_menu.tscn
new file mode 100644
index 0000000..fc7a0ff
--- /dev/null
+++ b/menus/scenes/menus/options_menu/game/game_options_menu.tscn
@@ -0,0 +1,26 @@
+[gd_scene load_steps=4 format=3 uid="uid://csyeb55n3u6bs"]
+
+[ext_resource type="Script" uid="uid://do8gkcahgjpj2" path="res://menus/scenes/menus/options_menu/game/game_options_menu.gd" id="1_u06ei"]
+[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="2_vwave"]
+[ext_resource type="PackedScene" uid="uid://dfjkn4yl3q1am" path="res://menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.tscn" id="3_hu6k8"]
+
+[node name="Game" 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_u06ei")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+custom_minimum_size = Vector2(400, 0)
+layout_mode = 2
+size_flags_horizontal = 4
+alignment = 1
+script = ExtResource("2_vwave")
+search_depth = 2
+
+[node name="ResetGameControl" parent="VBoxContainer" instance=ExtResource("3_hu6k8")]
+layout_mode = 2
+
+[connection signal="reset_confirmed" from="VBoxContainer/ResetGameControl" to="." method="_on_ResetGameControl_reset_confirmed"]
diff --git a/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.gd b/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.gd
new file mode 100644
index 0000000..0e2fab7
--- /dev/null
+++ b/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.gd
@@ -0,0 +1,13 @@
+extends HBoxContainer
+
+signal reset_confirmed
+
+func _on_ResetButton_pressed() -> void:
+ $ConfirmResetDialog.popup_centered()
+ $ResetButton.disabled = true
+
+func _on_ConfirmResetDialog_confirmed() -> void:
+ reset_confirmed.emit()
+
+func _on_confirm_reset_dialog_canceled() -> void:
+ $ResetButton.disabled = false
diff --git a/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.gd.uid b/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.gd.uid
new file mode 100644
index 0000000..fe34957
--- /dev/null
+++ b/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.gd.uid
@@ -0,0 +1 @@
+uid://bijygd7ul853x
diff --git a/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.tscn b/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.tscn
new file mode 100644
index 0000000..7f192ea
--- /dev/null
+++ b/menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.tscn
@@ -0,0 +1,27 @@
+[gd_scene load_steps=2 format=3 uid="uid://dfjkn4yl3q1am"]
+
+[ext_resource type="Script" uid="uid://bijygd7ul853x" path="res://menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.gd" id="1_feltj"]
+
+[node name="ResetGameControl" type="HBoxContainer"]
+custom_minimum_size = Vector2(0, 32)
+offset_top = 210.0
+offset_right = 305.0
+offset_bottom = 242.0
+script = ExtResource("1_feltj")
+
+[node name="ResetLabel" type="Label" parent="."]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Reset Game :"
+
+[node name="ResetButton" type="Button" parent="."]
+custom_minimum_size = Vector2(72, 32)
+layout_mode = 2
+text = "Reset"
+
+[node name="ConfirmResetDialog" type="ConfirmationDialog" parent="."]
+dialog_text = "Do you want to reset your game data?"
+
+[connection signal="pressed" from="ResetButton" to="." method="_on_ResetButton_pressed"]
+[connection signal="canceled" from="ConfirmResetDialog" to="." method="_on_confirm_reset_dialog_canceled"]
+[connection signal="confirmed" from="ConfirmResetDialog" to="." method="_on_ConfirmResetDialog_confirmed"]
diff --git a/menus/scenes/menus/options_menu/input/input_extras_menu.tscn b/menus/scenes/menus/options_menu/input/input_extras_menu.tscn
new file mode 100644
index 0000000..7857b4f
--- /dev/null
+++ b/menus/scenes/menus/options_menu/input/input_extras_menu.tscn
@@ -0,0 +1,65 @@
+[gd_scene load_steps=3 format=3 uid="uid://ceehkbh6emrdm"]
+
+[ext_resource type="Script" path="res://addons/maaacks_game_template/base/scripts/capture_focus.gd" id="1_goga1"]
+[ext_resource type="PackedScene" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn" id="2_iyvrj"]
+
+[node name="Inputs" type="MarginContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+size_flags_horizontal = 4
+size_flags_vertical = 4
+theme_override_constants/separation = 8
+script = ExtResource("1_goga1")
+search_depth = 5
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
+layout_mode = 2
+theme_override_constants/margin_top = 32
+theme_override_constants/margin_bottom = 32
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/MarginContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/separation = 8
+alignment = 1
+
+[node name="MouseSensitivityControl" parent="VBoxContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_iyvrj")]
+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.25
+max_value = 2.0
+tick_count = 8
+
+[node name="JoypadSensitivityControl" parent="VBoxContainer/MarginContainer/VBoxContainer" instance=ExtResource("2_iyvrj")]
+layout_mode = 2
+option_name = "Joypad Sensitivity"
+option_section = 1
+key = "JoypadSensitivity"
+section = "InputSettings"
+
+[node name="OptionLabel" parent="VBoxContainer/MarginContainer/VBoxContainer/JoypadSensitivityControl" index="0"]
+text = "Joypad Sensitivity :"
+
+[node name="HSlider" parent="VBoxContainer/MarginContainer/VBoxContainer/JoypadSensitivityControl" index="1"]
+min_value = 0.25
+max_value = 2.0
+tick_count = 8
+
+[editable path="VBoxContainer/MarginContainer/VBoxContainer/MouseSensitivityControl"]
+[editable path="VBoxContainer/MarginContainer/VBoxContainer/JoypadSensitivityControl"]
diff --git a/menus/scenes/menus/options_menu/input/input_options_menu.gd b/menus/scenes/menus/options_menu/input/input_options_menu.gd
new file mode 100644
index 0000000..32b012c
--- /dev/null
+++ b/menus/scenes/menus/options_menu/input/input_options_menu.gd
@@ -0,0 +1,2 @@
+@tool
+extends InputOptionsMenu
diff --git a/menus/scenes/menus/options_menu/input/input_options_menu.gd.uid b/menus/scenes/menus/options_menu/input/input_options_menu.gd.uid
new file mode 100644
index 0000000..40406b7
--- /dev/null
+++ b/menus/scenes/menus/options_menu/input/input_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://crmckg7rtbwok
diff --git a/menus/scenes/menus/options_menu/input/input_options_menu.tscn b/menus/scenes/menus/options_menu/input/input_options_menu.tscn
new file mode 100644
index 0000000..9443870
--- /dev/null
+++ b/menus/scenes/menus/options_menu/input/input_options_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://cmoeybw5t652d"]
+
+[ext_resource type="PackedScene" uid="uid://dp3rgqaehb3xu" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn" id="1_7pp6d"]
+[ext_resource type="Script" uid="uid://crmckg7rtbwok" path="res://menus/scenes/menus/options_menu/input/input_options_menu.gd" id="2_0afnv"]
+
+[node name="Controls" instance=ExtResource("1_7pp6d")]
+script = ExtResource("2_0afnv")
diff --git a/menus/scenes/menus/options_menu/input/input_options_menu_with_mouse_sensitivity.tscn b/menus/scenes/menus/options_menu/input/input_options_menu_with_mouse_sensitivity.tscn
new file mode 100644
index 0000000..da142d6
--- /dev/null
+++ b/menus/scenes/menus/options_menu/input/input_options_menu_with_mouse_sensitivity.tscn
@@ -0,0 +1,41 @@
+[gd_scene load_steps=4 format=3 uid="uid://6ykdvf4srto5"]
+
+[ext_resource type="PackedScene" uid="uid://dp3rgqaehb3xu" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/input/input_options_menu.tscn" id="1_mva3c"]
+[ext_resource type="Script" uid="uid://crmckg7rtbwok" path="res://menus/scenes/menus/options_menu/input/input_options_menu.gd" id="2_2ceeu"]
+[ext_resource type="PackedScene" uid="uid://cl416gdb1fgwr" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/slider_option_control.tscn" id="3_pecdw"]
+
+[node name="Controls" instance=ExtResource("1_mva3c")]
+script = ExtResource("2_2ceeu")
+
+[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("3_pecdw")]
+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.25
+max_value = 2.0
+tick_count = 8
+
+[node name="HSeparator" type="HSeparator" parent="VBoxContainer" index="1"]
+layout_mode = 2
+
+[editable path="VBoxContainer/MarginContainer/VBoxContainer/MouseSensitivityControl"]
diff --git a/menus/scenes/menus/options_menu/master_options_menu.gd b/menus/scenes/menus/options_menu/master_options_menu.gd
new file mode 100644
index 0000000..f2f2718
--- /dev/null
+++ b/menus/scenes/menus/options_menu/master_options_menu.gd
@@ -0,0 +1 @@
+extends MasterOptionsMenu
diff --git a/menus/scenes/menus/options_menu/master_options_menu.gd.uid b/menus/scenes/menus/options_menu/master_options_menu.gd.uid
new file mode 100644
index 0000000..105fc2a
--- /dev/null
+++ b/menus/scenes/menus/options_menu/master_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://bml584x67xpj2
diff --git a/menus/scenes/menus/options_menu/master_options_menu.tscn b/menus/scenes/menus/options_menu/master_options_menu.tscn
new file mode 100644
index 0000000..5904c43
--- /dev/null
+++ b/menus/scenes/menus/options_menu/master_options_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://bh2anuw21xdwu"]
+
+[ext_resource type="PackedScene" uid="uid://bvwl11s2p0hd" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.tscn" id="1_ma1jo"]
+[ext_resource type="Script" uid="uid://bml584x67xpj2" path="res://menus/scenes/menus/options_menu/master_options_menu.gd" id="2_mk4ln"]
+
+[node name="MasterOptionsMenu" instance=ExtResource("1_ma1jo")]
+script = ExtResource("2_mk4ln")
diff --git a/menus/scenes/menus/options_menu/master_options_menu_with_tabs.tscn b/menus/scenes/menus/options_menu/master_options_menu_with_tabs.tscn
new file mode 100644
index 0000000..e848fba
--- /dev/null
+++ b/menus/scenes/menus/options_menu/master_options_menu_with_tabs.tscn
@@ -0,0 +1,39 @@
+[gd_scene load_steps=8 format=3 uid="uid://bq2ti3hrjlgdl"]
+
+[ext_resource type="PackedScene" uid="uid://bvwl11s2p0hd" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/master_options_menu.tscn" id="1_iwcoa"]
+[ext_resource type="Script" uid="uid://bml584x67xpj2" path="res://menus/scenes/menus/options_menu/master_options_menu.gd" id="2_36oo7"]
+[ext_resource type="PackedScene" uid="uid://cmoeybw5t652d" path="res://menus/scenes/menus/options_menu/input/input_options_menu.tscn" id="3_l1ye5"]
+[ext_resource type="PackedScene" uid="uid://ceehkbh6emrdm" path="res://menus/scenes/menus/options_menu/input/input_extras_menu.tscn" id="4_w27xj"]
+[ext_resource type="PackedScene" uid="uid://c7ulxshxe636e" path="res://menus/scenes/menus/options_menu/audio/audio_options_menu.tscn" id="5_f0yn1"]
+[ext_resource type="PackedScene" uid="uid://ct0i2rcp47pm7" path="res://menus/scenes/menus/options_menu/video/video_options_menu_with_extras.tscn" id="6_7ovpx"]
+[ext_resource type="PackedScene" uid="uid://csyeb55n3u6bs" path="res://menus/scenes/menus/options_menu/game/game_options_menu.tscn" id="7_q1wy3"]
+
+[node name="MasterOptionsMenu" instance=ExtResource("1_iwcoa")]
+script = ExtResource("2_36oo7")
+
+[node name="TabContainer" parent="." index="0"]
+current_tab = 0
+
+[node name="Controls" parent="TabContainer" index="1" instance=ExtResource("3_l1ye5")]
+layout_mode = 2
+metadata/_tab_index = 0
+
+[node name="Inputs" parent="TabContainer" index="2" instance=ExtResource("4_w27xj")]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 1
+
+[node name="Audio" parent="TabContainer" index="3" instance=ExtResource("5_f0yn1")]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 2
+
+[node name="Video" parent="TabContainer" index="4" instance=ExtResource("6_7ovpx")]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 3
+
+[node name="Game" parent="TabContainer" index="5" instance=ExtResource("7_q1wy3")]
+visible = false
+layout_mode = 2
+metadata/_tab_index = 4
diff --git a/menus/scenes/menus/options_menu/mini_options_menu.gd b/menus/scenes/menus/options_menu/mini_options_menu.gd
new file mode 100644
index 0000000..62b8641
--- /dev/null
+++ b/menus/scenes/menus/options_menu/mini_options_menu.gd
@@ -0,0 +1 @@
+extends MiniOptionsMenu
diff --git a/menus/scenes/menus/options_menu/mini_options_menu.gd.uid b/menus/scenes/menus/options_menu/mini_options_menu.gd.uid
new file mode 100644
index 0000000..ccf7bc2
--- /dev/null
+++ b/menus/scenes/menus/options_menu/mini_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://d230xsd8s1bgc
diff --git a/menus/scenes/menus/options_menu/mini_options_menu.tscn b/menus/scenes/menus/options_menu/mini_options_menu.tscn
new file mode 100644
index 0000000..59664d5
--- /dev/null
+++ b/menus/scenes/menus/options_menu/mini_options_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://bia3medhlsg3j"]
+
+[ext_resource type="PackedScene" uid="uid://vh1ucj2rfbby" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.tscn" id="1_8lxe7"]
+[ext_resource type="Script" uid="uid://d230xsd8s1bgc" path="res://menus/scenes/menus/options_menu/mini_options_menu.gd" id="2_ptjhm"]
+
+[node name="MiniOptionsMenu" instance=ExtResource("1_8lxe7")]
+script = ExtResource("2_ptjhm")
diff --git a/menus/scenes/menus/options_menu/mini_options_menu_with_reset.gd b/menus/scenes/menus/options_menu/mini_options_menu_with_reset.gd
new file mode 100644
index 0000000..966a008
--- /dev/null
+++ b/menus/scenes/menus/options_menu/mini_options_menu_with_reset.gd
@@ -0,0 +1,4 @@
+extends MiniOptionsMenu
+
+func _on_reset_game_control_reset_confirmed() -> void:
+ GlobalState.reset()
diff --git a/menus/scenes/menus/options_menu/mini_options_menu_with_reset.gd.uid b/menus/scenes/menus/options_menu/mini_options_menu_with_reset.gd.uid
new file mode 100644
index 0000000..bb92687
--- /dev/null
+++ b/menus/scenes/menus/options_menu/mini_options_menu_with_reset.gd.uid
@@ -0,0 +1 @@
+uid://ceg0gjjapg77k
diff --git a/menus/scenes/menus/options_menu/mini_options_menu_with_reset.tscn b/menus/scenes/menus/options_menu/mini_options_menu_with_reset.tscn
new file mode 100644
index 0000000..fe1bb4d
--- /dev/null
+++ b/menus/scenes/menus/options_menu/mini_options_menu_with_reset.tscn
@@ -0,0 +1,13 @@
+[gd_scene load_steps=4 format=3 uid="uid://foajscu2dqkk"]
+
+[ext_resource type="PackedScene" uid="uid://vh1ucj2rfbby" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/mini_options_menu.tscn" id="1_gcivb"]
+[ext_resource type="Script" uid="uid://ceg0gjjapg77k" path="res://menus/scenes/menus/options_menu/mini_options_menu_with_reset.gd" id="2_oof6u"]
+[ext_resource type="PackedScene" uid="uid://dfjkn4yl3q1am" path="res://menus/scenes/menus/options_menu/game/reset_game_control/reset_game_control.tscn" id="3_gss4d"]
+
+[node name="MiniOptionsMenu" instance=ExtResource("1_gcivb")]
+script = ExtResource("2_oof6u")
+
+[node name="ResetGameControl" parent="." index="3" instance=ExtResource("3_gss4d")]
+layout_mode = 2
+
+[connection signal="reset_confirmed" from="ResetGameControl" to="." method="_on_reset_game_control_reset_confirmed"]
diff --git a/menus/scenes/menus/options_menu/video/video_options_menu.gd b/menus/scenes/menus/options_menu/video/video_options_menu.gd
new file mode 100644
index 0000000..f6bae15
--- /dev/null
+++ b/menus/scenes/menus/options_menu/video/video_options_menu.gd
@@ -0,0 +1 @@
+extends VideoOptionsMenu
diff --git a/menus/scenes/menus/options_menu/video/video_options_menu.gd.uid b/menus/scenes/menus/options_menu/video/video_options_menu.gd.uid
new file mode 100644
index 0000000..ec96a03
--- /dev/null
+++ b/menus/scenes/menus/options_menu/video/video_options_menu.gd.uid
@@ -0,0 +1 @@
+uid://dihvdjypwmb1w
diff --git a/menus/scenes/menus/options_menu/video/video_options_menu.tscn b/menus/scenes/menus/options_menu/video/video_options_menu.tscn
new file mode 100644
index 0000000..7997207
--- /dev/null
+++ b/menus/scenes/menus/options_menu/video/video_options_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://df8nj4p82ic65"]
+
+[ext_resource type="PackedScene" uid="uid://b2numvphf2kau" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.tscn" id="1_kdy5u"]
+[ext_resource type="Script" uid="uid://dihvdjypwmb1w" path="res://menus/scenes/menus/options_menu/video/video_options_menu.gd" id="2_2gjg5"]
+
+[node name="Video" instance=ExtResource("1_kdy5u")]
+script = ExtResource("2_2gjg5")
diff --git a/menus/scenes/menus/options_menu/video/video_options_menu_with_extras.tscn b/menus/scenes/menus/options_menu/video/video_options_menu_with_extras.tscn
new file mode 100644
index 0000000..d1aa230
--- /dev/null
+++ b/menus/scenes/menus/options_menu/video/video_options_menu_with_extras.tscn
@@ -0,0 +1,33 @@
+[gd_scene load_steps=4 format=3 uid="uid://ct0i2rcp47pm7"]
+
+[ext_resource type="PackedScene" uid="uid://b2numvphf2kau" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/video/video_options_menu.tscn" id="1_iqoga"]
+[ext_resource type="Script" uid="uid://dihvdjypwmb1w" path="res://menus/scenes/menus/options_menu/video/video_options_menu.gd" id="2_ti4vq"]
+[ext_resource type="PackedScene" uid="uid://b6bl3n5mp3m1e" path="res://addons/maaacks_game_template/base/scenes/menus/options_menu/option_control/list_option_control.tscn" id="3_nxs4g"]
+
+[node name="Video" instance=ExtResource("1_iqoga")]
+script = ExtResource("2_ti4vq")
+
+[node name="AntiAliasingControl" parent="VBoxContainer" index="3" instance=ExtResource("3_nxs4g")]
+layout_mode = 2
+lock_titles = true
+option_values = [0, 1, 2, 3]
+option_titles = Array[String](["Disabled (Fastest)", "2x", "4x", "8x (Slowest)"])
+option_name = "Anti-Aliasing"
+option_section = 3
+key = "Anti-aliasing"
+section = "VideoSettings"
+property_type = 2
+default_value = 0
+
+[node name="CameraShakeControl" parent="VBoxContainer" index="4" instance=ExtResource("3_nxs4g")]
+visible = false
+layout_mode = 2
+lock_titles = true
+option_values = [1.0, 0.75, 0.5, 0.0]
+option_titles = Array[String](["Normal", "Reduced", "Minimal", "None"])
+option_name = "Camera Shake"
+option_section = 3
+key = "CameraShake"
+section = "VideoSettings"
+property_type = 3
+default_value = 1.0
diff --git a/menus/scenes/opening/opening.gd b/menus/scenes/opening/opening.gd
new file mode 100644
index 0000000..770176b
--- /dev/null
+++ b/menus/scenes/opening/opening.gd
@@ -0,0 +1 @@
+extends "res://addons/maaacks_game_template/base/scenes/opening/opening.gd"
diff --git a/menus/scenes/opening/opening.gd.uid b/menus/scenes/opening/opening.gd.uid
new file mode 100644
index 0000000..2790664
--- /dev/null
+++ b/menus/scenes/opening/opening.gd.uid
@@ -0,0 +1 @@
+uid://656mrvlybxv8
diff --git a/menus/scenes/opening/opening.tscn b/menus/scenes/opening/opening.tscn
new file mode 100644
index 0000000..32bf960
--- /dev/null
+++ b/menus/scenes/opening/opening.tscn
@@ -0,0 +1,8 @@
+[gd_scene load_steps=3 format=3 uid="uid://dygcj05sb15ld"]
+
+[ext_resource type="PackedScene" uid="uid://sikc02ddepyt" path="res://addons/maaacks_game_template/base/scenes/opening/opening.tscn" id="1_dit3p"]
+[ext_resource type="Script" uid="uid://656mrvlybxv8" path="res://menus/scenes/opening/opening.gd" id="2_tudjo"]
+
+[node name="Opening" instance=ExtResource("1_dit3p")]
+script = ExtResource("2_tudjo")
+next_scene = "res://menus/scenes/menus/main_menu/main_menu_with_animations.tscn"
diff --git a/menus/scenes/opening/opening_with_logo.tscn b/menus/scenes/opening/opening_with_logo.tscn
new file mode 100644
index 0000000..0366395
--- /dev/null
+++ b/menus/scenes/opening/opening_with_logo.tscn
@@ -0,0 +1,13 @@
+[gd_scene load_steps=4 format=3 uid="uid://dwo50456dv6va"]
+
+[ext_resource type="PackedScene" uid="uid://sikc02ddepyt" path="res://addons/maaacks_game_template/base/scenes/opening/opening.tscn" id="1_e0yng"]
+[ext_resource type="Script" uid="uid://656mrvlybxv8" path="res://menus/scenes/opening/opening.gd" id="2_qjsl3"]
+[ext_resource type="Texture2D" uid="uid://6vwgmbr7eylm" path="res://menus/assets/godot_engine_logo/logo_vertical_color_dark.png" id="3_voviu"]
+
+[node name="Opening" instance=ExtResource("1_e0yng")]
+script = ExtResource("2_qjsl3")
+next_scene = "res://menus/scenes/menus/main_menu/main_menu_with_animations.tscn"
+images = Array[Texture2D]([ExtResource("3_voviu")])
+
+[node name="BackgroundMusicPlayer" parent="." index="0"]
+bus = &"Master"
diff --git a/menus/scenes/overlaid_menus/game_won_menu.gd b/menus/scenes/overlaid_menus/game_won_menu.gd
new file mode 100644
index 0000000..9b4037d
--- /dev/null
+++ b/menus/scenes/overlaid_menus/game_won_menu.gd
@@ -0,0 +1 @@
+extends GameWonMenu
diff --git a/menus/scenes/overlaid_menus/game_won_menu.gd.uid b/menus/scenes/overlaid_menus/game_won_menu.gd.uid
new file mode 100644
index 0000000..9bd3ee6
--- /dev/null
+++ b/menus/scenes/overlaid_menus/game_won_menu.gd.uid
@@ -0,0 +1 @@
+uid://cb5f86vwhucv1
diff --git a/menus/scenes/overlaid_menus/game_won_menu.tscn b/menus/scenes/overlaid_menus/game_won_menu.tscn
new file mode 100644
index 0000000..a57f7a6
--- /dev/null
+++ b/menus/scenes/overlaid_menus/game_won_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://dmq0tpdodtomh"]
+
+[ext_resource type="PackedScene" uid="uid://4brssbq1ghsw" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/game_won_menu.tscn" id="1_qj15s"]
+[ext_resource type="Script" uid="uid://cb5f86vwhucv1" path="res://menus/scenes/overlaid_menus/game_won_menu.gd" id="2_5yetg"]
+
+[node name="GameWonMenu" instance=ExtResource("1_qj15s")]
+script = ExtResource("2_5yetg")
diff --git a/menus/scenes/overlaid_menus/level_lost_menu.gd b/menus/scenes/overlaid_menus/level_lost_menu.gd
new file mode 100644
index 0000000..cfe6091
--- /dev/null
+++ b/menus/scenes/overlaid_menus/level_lost_menu.gd
@@ -0,0 +1 @@
+extends LevelLostMenu
diff --git a/menus/scenes/overlaid_menus/level_lost_menu.gd.uid b/menus/scenes/overlaid_menus/level_lost_menu.gd.uid
new file mode 100644
index 0000000..15ca6b1
--- /dev/null
+++ b/menus/scenes/overlaid_menus/level_lost_menu.gd.uid
@@ -0,0 +1 @@
+uid://rhnj8gy1tp6x
diff --git a/menus/scenes/overlaid_menus/level_lost_menu.tscn b/menus/scenes/overlaid_menus/level_lost_menu.tscn
new file mode 100644
index 0000000..9eeeb05
--- /dev/null
+++ b/menus/scenes/overlaid_menus/level_lost_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://ciyq8eiv1mtie"]
+
+[ext_resource type="PackedScene" uid="uid://dkq3nhkmhu4je" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/level_lost_menu.tscn" id="1_ql4ov"]
+[ext_resource type="Script" uid="uid://rhnj8gy1tp6x" path="res://menus/scenes/overlaid_menus/level_lost_menu.gd" id="2_swrtm"]
+
+[node name="LevelLostMenu" instance=ExtResource("1_ql4ov")]
+script = ExtResource("2_swrtm")
diff --git a/menus/scenes/overlaid_menus/level_won_menu.gd b/menus/scenes/overlaid_menus/level_won_menu.gd
new file mode 100644
index 0000000..ecbc0c9
--- /dev/null
+++ b/menus/scenes/overlaid_menus/level_won_menu.gd
@@ -0,0 +1 @@
+extends LevelWonMenu
diff --git a/menus/scenes/overlaid_menus/level_won_menu.gd.uid b/menus/scenes/overlaid_menus/level_won_menu.gd.uid
new file mode 100644
index 0000000..e31e58c
--- /dev/null
+++ b/menus/scenes/overlaid_menus/level_won_menu.gd.uid
@@ -0,0 +1 @@
+uid://c4uqtcx5p2yip
diff --git a/menus/scenes/overlaid_menus/level_won_menu.tscn b/menus/scenes/overlaid_menus/level_won_menu.tscn
new file mode 100644
index 0000000..bf85c24
--- /dev/null
+++ b/menus/scenes/overlaid_menus/level_won_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://b46jlduh4lllk"]
+
+[ext_resource type="PackedScene" uid="uid://y3vtx0e0shv4" path="res://addons/maaacks_game_template/extras/scenes/overlaid_menus/level_won_menu.tscn" id="1_sf5pg"]
+[ext_resource type="Script" uid="uid://c4uqtcx5p2yip" path="res://menus/scenes/overlaid_menus/level_won_menu.gd" id="2_vajr3"]
+
+[node name="LevelWonMenu" instance=ExtResource("1_sf5pg")]
+script = ExtResource("2_vajr3")
diff --git a/menus/scenes/overlaid_menus/mini_options_overlaid_menu.tscn b/menus/scenes/overlaid_menus/mini_options_overlaid_menu.tscn
new file mode 100644
index 0000000..ebc628e
--- /dev/null
+++ b/menus/scenes/overlaid_menus/mini_options_overlaid_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://dim045la3mijk"]
+
+[ext_resource type="PackedScene" uid="uid://cikf3o5omnunl" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/menus/mini_options_overlaid_menu.tscn" id="1_5piya"]
+[ext_resource type="PackedScene" uid="uid://foajscu2dqkk" path="res://menus/scenes/menus/options_menu/mini_options_menu_with_reset.tscn" id="2_3u5po"]
+
+[node name="MiniOptionsOverlaidMenu" instance=ExtResource("1_5piya")]
+menu_scene = ExtResource("2_3u5po")
diff --git a/menus/scenes/overlaid_menus/overlaid_menu.gd b/menus/scenes/overlaid_menus/overlaid_menu.gd
new file mode 100644
index 0000000..3b612cf
--- /dev/null
+++ b/menus/scenes/overlaid_menus/overlaid_menu.gd
@@ -0,0 +1 @@
+extends OverlaidMenu
diff --git a/menus/scenes/overlaid_menus/overlaid_menu.gd.uid b/menus/scenes/overlaid_menus/overlaid_menu.gd.uid
new file mode 100644
index 0000000..c1b8fb1
--- /dev/null
+++ b/menus/scenes/overlaid_menus/overlaid_menu.gd.uid
@@ -0,0 +1 @@
+uid://bis5jkwil0a8t
diff --git a/menus/scenes/overlaid_menus/overlaid_menu.tscn b/menus/scenes/overlaid_menus/overlaid_menu.tscn
new file mode 100644
index 0000000..e8827cf
--- /dev/null
+++ b/menus/scenes/overlaid_menus/overlaid_menu.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://cg1fsb8a8s5bx"]
+
+[ext_resource type="PackedScene" uid="uid://wny2d8dvp3ok" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu.tscn" id="1_cu881"]
+[ext_resource type="Script" uid="uid://bis5jkwil0a8t" path="res://menus/scenes/overlaid_menus/overlaid_menu.gd" id="2_at5jq"]
+
+[node name="OverlaidMenu" instance=ExtResource("1_cu881")]
+script = ExtResource("2_at5jq")
diff --git a/menus/scenes/overlaid_menus/overlaid_menu_container.gd b/menus/scenes/overlaid_menus/overlaid_menu_container.gd
new file mode 100644
index 0000000..b451bbd
--- /dev/null
+++ b/menus/scenes/overlaid_menus/overlaid_menu_container.gd
@@ -0,0 +1 @@
+extends OverlaidMenuContainer
diff --git a/menus/scenes/overlaid_menus/overlaid_menu_container.gd.uid b/menus/scenes/overlaid_menus/overlaid_menu_container.gd.uid
new file mode 100644
index 0000000..5ab6f18
--- /dev/null
+++ b/menus/scenes/overlaid_menus/overlaid_menu_container.gd.uid
@@ -0,0 +1 @@
+uid://4vyebfu30j4b
diff --git a/menus/scenes/overlaid_menus/overlaid_menu_container.tscn b/menus/scenes/overlaid_menus/overlaid_menu_container.tscn
new file mode 100644
index 0000000..8ee39b7
--- /dev/null
+++ b/menus/scenes/overlaid_menus/overlaid_menu_container.tscn
@@ -0,0 +1,7 @@
+[gd_scene load_steps=3 format=3 uid="uid://dcb1m1sh36xgt"]
+
+[ext_resource type="PackedScene" uid="uid://bqqngki8bm3iq" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/overlaid_menu_container.tscn" id="1_bb3ic"]
+[ext_resource type="Script" uid="uid://4vyebfu30j4b" path="res://menus/scenes/overlaid_menus/overlaid_menu_container.gd" id="2_urhek"]
+
+[node name="OverlaidMenuContainer" instance=ExtResource("1_bb3ic")]
+script = ExtResource("2_urhek")
diff --git a/menus/scenes/overlaid_menus/pause_menu.gd b/menus/scenes/overlaid_menus/pause_menu.gd
new file mode 100644
index 0000000..bee76c0
--- /dev/null
+++ b/menus/scenes/overlaid_menus/pause_menu.gd
@@ -0,0 +1 @@
+extends PauseMenu
diff --git a/menus/scenes/overlaid_menus/pause_menu.gd.uid b/menus/scenes/overlaid_menus/pause_menu.gd.uid
new file mode 100644
index 0000000..b7efbce
--- /dev/null
+++ b/menus/scenes/overlaid_menus/pause_menu.gd.uid
@@ -0,0 +1 @@
+uid://bwvomv4eww4fg
diff --git a/menus/scenes/overlaid_menus/pause_menu.tscn b/menus/scenes/overlaid_menus/pause_menu.tscn
new file mode 100644
index 0000000..c721513
--- /dev/null
+++ b/menus/scenes/overlaid_menus/pause_menu.tscn
@@ -0,0 +1,10 @@
+[gd_scene load_steps=4 format=3 uid="uid://ccqajqchiw4xu"]
+
+[ext_resource type="PackedScene" uid="uid://b5cd6sa8qq4vc" path="res://addons/maaacks_game_template/base/scenes/overlaid_menu/menus/pause_menu.tscn" id="1_0paax"]
+[ext_resource type="Script" uid="uid://bwvomv4eww4fg" path="res://menus/scenes/overlaid_menus/pause_menu.gd" id="2_ek1hy"]
+[ext_resource type="PackedScene" uid="uid://dim045la3mijk" path="res://menus/scenes/overlaid_menus/mini_options_overlaid_menu.tscn" id="3_e0t2r"]
+
+[node name="PauseMenu" instance=ExtResource("1_0paax")]
+script = ExtResource("2_ek1hy")
+options_packed_scene = ExtResource("3_e0t2r")
+main_menu_scene = "res://menus/scenes/menus/main_menu/main_menu_with_animations.tscn"
diff --git a/menus/scripts/game_state.gd b/menus/scripts/game_state.gd
new file mode 100644
index 0000000..abfd8d7
--- /dev/null
+++ b/menus/scripts/game_state.gd
@@ -0,0 +1,60 @@
+class_name GameState
+extends Resource
+
+const STATE_NAME : String = "GameState"
+const FILE_PATH = "res://menus/scripts/game_state.gd"
+
+@export var level_states : Dictionary = {}
+@export var max_level_reached : int
+@export var current_level : int
+@export var times_played : int
+
+static func get_level_state(level_state_key : String) -> LevelState:
+ var game_state := get_game_state()
+ if level_state_key.is_empty() : return
+ if level_state_key in game_state.level_states:
+ return game_state.level_states[level_state_key]
+ else:
+ var new_level_state := LevelState.new()
+ game_state.level_states[level_state_key] = new_level_state
+ return new_level_state
+
+static func has_game_state() -> bool:
+ return GlobalState.has_state(STATE_NAME)
+
+static func get_game_state() -> GameState:
+ return GlobalState.get_state(STATE_NAME, FILE_PATH)
+
+static func get_current_level() -> int:
+ var game_state := get_game_state()
+ if not game_state:
+ return 0
+ return game_state.current_level
+
+static func get_max_level_reached() -> int:
+ var game_state := get_game_state()
+ if not game_state:
+ return 0
+ return game_state.max_level_reached
+
+static func level_reached(level_number : int) -> void:
+ var game_state := get_game_state()
+ if not game_state:
+ return
+ game_state.max_level_reached = max(level_number, game_state.max_level_reached)
+ game_state.current_level = level_number
+ GlobalState.save()
+
+static func set_current_level(level_number : int) -> void:
+ var game_state := get_game_state()
+ if not game_state:
+ return
+ game_state.current_level = level_number
+ GlobalState.save()
+
+static func start_game() -> void:
+ var game_state := get_game_state()
+ if not game_state:
+ return
+ game_state.times_played += 1
+ GlobalState.save()
diff --git a/menus/scripts/game_state.gd.uid b/menus/scripts/game_state.gd.uid
new file mode 100644
index 0000000..c50595e
--- /dev/null
+++ b/menus/scripts/game_state.gd.uid
@@ -0,0 +1 @@
+uid://yelhbgm3q31p
diff --git a/menus/scripts/level_list_and_state_manager.gd b/menus/scripts/level_list_and_state_manager.gd
new file mode 100644
index 0000000..4fd94ab
--- /dev/null
+++ b/menus/scripts/level_list_and_state_manager.gd
@@ -0,0 +1,14 @@
+extends LevelListManager
+
+func set_current_level_id(value : int) -> void:
+ super.set_current_level_id(value)
+ GameState.level_reached(value)
+
+func get_current_level_id() -> int:
+ current_level_id = GameState.get_current_level() if force_level == -1 else force_level
+ return current_level_id
+
+func _advance_level() -> bool:
+ var _advanced := super._advance_level()
+ GameState.set_current_level(current_level_id)
+ return _advanced
diff --git a/menus/scripts/level_list_and_state_manager.gd.uid b/menus/scripts/level_list_and_state_manager.gd.uid
new file mode 100644
index 0000000..68feff8
--- /dev/null
+++ b/menus/scripts/level_list_and_state_manager.gd.uid
@@ -0,0 +1 @@
+uid://3yfyhcjuxm0t
diff --git a/menus/scripts/level_state.gd b/menus/scripts/level_state.gd
new file mode 100644
index 0000000..168aa2a
--- /dev/null
+++ b/menus/scripts/level_state.gd
@@ -0,0 +1,5 @@
+class_name LevelState
+extends Resource
+
+@export var color : Color
+@export var tutorial_read : bool = false
diff --git a/menus/scripts/level_state.gd.uid b/menus/scripts/level_state.gd.uid
new file mode 100644
index 0000000..2edfc39
--- /dev/null
+++ b/menus/scripts/level_state.gd.uid
@@ -0,0 +1 @@
+uid://d1dccbxlxbleg
diff --git a/override.cfg b/override.cfg
new file mode 100644
index 0000000..e7ecedc
--- /dev/null
+++ b/override.cfg
@@ -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":4,"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)
+]
+}
diff --git a/project.godot b/project.godot
index 2fb8723..cfa2bb3 100644
--- a/project.godot
+++ b/project.godot
@@ -15,13 +15,17 @@ warnings/check_invalid_track_paths=false
[application]
config/name="Movement tests"
-run/main_scene="uid://cxbskue0lj2gv"
+run/main_scene="res://menus/scenes/opening/opening_with_logo.tscn"
config/features=PackedStringArray("4.4", "C#", "Forward Plus")
config/icon="res://icon.svg"
[autoload]
GUIDE="*res://addons/guide/guide.gd"
+AppConfig="*res://addons/maaacks_game_template/base/scenes/autoloads/app_config.tscn"
+SceneLoader="*res://addons/maaacks_game_template/base/scenes/autoloads/scene_loader.tscn"
+ProjectMusicController="*res://addons/maaacks_game_template/base/scenes/autoloads/project_music_controller.tscn"
+ProjectUISoundController="*res://addons/maaacks_game_template/base/scenes/autoloads/project_ui_sound_controller.tscn"
[display]
@@ -34,7 +38,11 @@ project/assembly_name="Movement tests"
[editor_plugins]
-enabled=PackedStringArray("res://addons/godot_state_charts/plugin.cfg", "res://addons/guide/plugin.cfg")
+enabled=PackedStringArray("res://addons/godot_state_charts/plugin.cfg", "res://addons/guide/plugin.cfg", "res://addons/maaacks_game_template/plugin.cfg")
+
+[gui]
+
+theme/custom="res://menus/resources/themes/expedition.tres"
[input]
@@ -108,6 +116,16 @@ aim_dash={
]
}
+[internationalization]
+
+locale/translations=PackedStringArray("res://addons/maaacks_game_template/base/translations/menus_translations.en.translation", "res://addons/maaacks_game_template/base/translations/menus_translations.fr.translation")
+
+[maaacks_game_template]
+
+disable_update_check=false
+disable_install_wizard=true
+copy_path="res://scenes"
+
[physics]
3d/physics_engine="Jolt Physics"