gd: added menu template

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

View File

@ -0,0 +1 @@
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/

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

View File

@ -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

View File

@ -0,0 +1,5 @@
extends Node
func _ready() -> void:
GlobalState.open()
AppSettings.set_from_config_and_window(get_window())

View File

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

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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()

View File

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

View File

@ -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"

View File

@ -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)

View File

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

View File

@ -0,0 +1,4 @@
class_name Credits
extends Control
signal end_reached

View File

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

View File

@ -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)

View File

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

View File

@ -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)

View File

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

View File

@ -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

View File

@ -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)

View File

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

View File

@ -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"]

View File

@ -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()

View File

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

View File

@ -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"]

View File

@ -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()

View File

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

View File

@ -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()

View File

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

View File

@ -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()

View File

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

View File

@ -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"]

View File

@ -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)

View File

@ -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"]

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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"
}

View File

@ -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()

View File

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

View File

@ -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")

View File

@ -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()

View File

@ -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"]

View File

@ -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()

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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())

View File

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

View File

@ -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"]

View File

@ -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()

View File

@ -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

View File

@ -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}
]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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())

View File

@ -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"]

View File

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

View File

@ -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()

View File

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

View File

@ -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

View File

@ -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"

View File

@ -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()

View File

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

View File

@ -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"]

View File

@ -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)

View File

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

View File

@ -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"]

View File

@ -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)

View File

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

View File

@ -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"

View File

@ -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)

View File

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

View File

@ -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"]

View File

@ -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

View File

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

View File

@ -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"]

View File

@ -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)

View File

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

View File

@ -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)

View File

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

View File

@ -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()

View File

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

View File

@ -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)

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