imported main menu scene addon

This commit is contained in:
2026-05-10 15:48:56 +02:00
parent 86d9f75161
commit 71c2850a34
458 changed files with 14881 additions and 0 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,40 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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,40 @@
[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/uastc_level=0
compress/rdo_quality_loss=0.0
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/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
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,10 @@
extends Node
@export_group("Scenes")
@export_file("*.tscn") var main_menu_scene_path : String
@export_file("*.tscn") var game_scene_path : String
@export_file("*.tscn") var ending_scene_path : String
func _ready() -> void:
GlobalState.open()
AppSettings.set_from_config_and_window(get_window())

View File

@@ -0,0 +1,9 @@
[gd_scene format=3 uid="uid://cjke6crjg14a0"]
[ext_resource type="Script" uid="uid://cno5ujal5t3kf" path="res://addons/maaacks_game_template/base/nodes/autoloads/app_config/app_config.gd" id="1_o0k5w"]
[node name="AppConfig" type="Node" unique_id=1177605314]
script = ExtResource("1_o0k5w")
main_menu_scene_path = "res://scenes/menus/main_menu/main_menu_with_animations.tscn"
game_scene_path = "res://scenes/game_scene/game_ui.tscn"
ending_scene_path = "res://scenes/end_credits/end_credits.tscn"

View File

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

View File

@@ -0,0 +1,7 @@
[gd_scene format=3 uid="uid://r5t485lr3p7t"]
[ext_resource type="Script" uid="uid://ctrh4qyxqncss" path="res://addons/maaacks_game_template/base/nodes/autoloads/music_controller/music_controller.gd" id="1_wbudo"]
[node name="ProjectMusicController" type="Node" unique_id=1628865114]
process_mode = 3
script = ExtResource("1_wbudo")

View File

@@ -0,0 +1,127 @@
class_name SceneLoaderClass
extends Node
## Autoload class for loading scenes with an optional loading screen.
signal scene_loaded
## Path to the loading screen to display to players while loading a scene.
@export_file("*.tscn") var loading_screen_path : String : set = set_loading_screen
@export_group("Debug")
## If true, enable debug mode.
@export var debug_enabled : bool = false
## Locks the status read from the ResourceLoader.
@export var debug_lock_status : ResourceLoader.ThreadLoadStatus
## Locks the progress read from the ResourceLoader.
@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.load(_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:
_background_loading = false
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,7 @@
[gd_scene format=3 uid="uid://cbwmrnp0af35y"]
[ext_resource type="Script" uid="uid://cxrcy0evb0j3l" path="res://addons/maaacks_game_template/base/nodes/autoloads/scene_loader/scene_loader.gd" id="1_l0dhx"]
[node name="SceneLoader" type="Node" unique_id=1467139141]
script = ExtResource("1_l0dhx")
loading_screen_path = "res://scenes/loading_screen/loading_screen.tscn"

View File

@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://cc37235kj4384"]
[ext_resource type="Script" uid="uid://b5oej1q4h7jvh" path="res://addons/maaacks_game_template/base/nodes/autoloads/ui_sound_controller/ui_sound_controller.gd" id="1_dmagn"]
[node name="ProjectUISoundController" type="Node" unique_id=1525696179]
script = ExtResource("1_dmagn")

View File

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

View File

@@ -0,0 +1,188 @@
class_name AppSettings
extends Node
## Interface to read/write general application settings through [PlayerConfig].
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 = &'Fullscreen'
const SCREEN_RESOLUTION = &'ScreenResolution'
const V_SYNC = &'V-Sync'
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 PlayerConfig.get_config(INPUT_SECTION, action_name, default)
static func set_config_input_events(action_name : String, inputs : Array) -> void:
PlayerConfig.set_config(INPUT_SECTION, action_name, inputs)
static func _clear_config_input_events() -> void:
PlayerConfig.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():
PlayerConfig.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 = PlayerConfig.get_config(AUDIO_SECTION, bus_key, bus_volume)
if is_nan(bus_volume):
bus_volume = 1.0
PlayerConfig.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 = PlayerConfig.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:
PlayerConfig.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 PlayerConfig.get_config(VIDEO_SECTION, SCREEN_RESOLUTION, current_resolution)
static func _on_window_size_changed(window: Window) -> void:
PlayerConfig.set_config(VIDEO_SECTION, SCREEN_RESOLUTION, window.size)
static func _set_fullscreen_from_config(window: Window) -> bool:
var fullscreen_enabled : bool = is_fullscreen(window)
fullscreen_enabled = PlayerConfig.get_config(VIDEO_SECTION, FULLSCREEN, fullscreen_enabled)
set_fullscreen_enabled(fullscreen_enabled, window)
return fullscreen_enabled
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
static func _set_v_sync_from_config(window: Window) -> DisplayServer.VSyncMode:
var vsync := get_vsync(window)
vsync = PlayerConfig.get_config(VIDEO_SECTION, V_SYNC, vsync)
set_vsync(vsync)
return vsync
static func set_video_from_config(window : Window) -> void:
window.size_changed.connect(_on_window_size_changed.bind(window))
var fullscreen_enabled := _set_fullscreen_from_config(window)
if not (fullscreen_enabled or OS.has_feature("web")):
var current_resolution : Vector2i = get_resolution(window)
set_resolution(current_resolution, window)
_set_v_sync_from_config(window)
# All
static func set_from_config() -> void:
set_default_inputs()
set_inputs_from_config()
set_audio_from_config()
static func set_from_config_and_window(window : Window) -> void:
set_from_config()
set_video_from_config(window)

View File

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

View File

@@ -0,0 +1,56 @@
class_name PlayerConfig
extends Object
## Interface for a single configuration file through [ConfigFile].
const CONFIG_FILE_LOCATION := "user://player_config.cfg"
static var config_file : ConfigFile
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,18 @@
@tool
extends Label
## Displays the value of `application/config/name`, set in project settings.
const NO_NAME_STRING : String = "Title"
## If true, update the title when ready.
@export var auto_update : bool = true
func update_name_label():
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():
if auto_update:
update_name_label()

View File

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

View File

@@ -0,0 +1,17 @@
@tool
extends Label
## 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,114 @@
@tool
extends RichTextLabel
## Script for parsing an attribution file in markdown format.
const HEADING_STRING_REPLACEMENT = "$1[font_size=%d]$2[/font_size]"
const BOLD_HEADING_STRING_REPLACEMENT = "$1[b][font_size=%d]$2[/font_size][/b]"
## The path to the attribution file in markdown format.
@export_file("*.md") var attribution_file_path: String
## If true, update the content of the text label from the attribution file when ready.
@export var auto_update : bool = true
@export_group("Font Sizes")
## The size to give text that was formatted as h1 header.
@export var h1_font_size: int
## The size to give text that was formatted as h2 header.
@export var h2_font_size: int
## The size to give text that was formatted as h3 header.
@export var h3_font_size: int
## The size to give text that was formatted as h4 header.
@export var h4_font_size: int
## The size to give text that was formatted as h5 header.
@export var h5_font_size: int
## The size to give text that was formatted as h6 header.
@export var h6_font_size: int
## If true, bold any headers (h1-h6).
@export var bold_headings : bool
@export_group("Image Sizes")
## The maximum width in pixels of any images loaded from the attibution file.
@export var max_image_width: int
## The maximum height in pixels of any images loaded from the attibution file.
@export var max_image_height : int
@export_group("Extra Options")
## If true, disable reading images from the attribution file.
@export var disable_images : bool = false
## If true, disable reading URLs from the attribution file.
@export var disable_urls : bool = false
## If true, disable opening links. For platforms that don't permit linking to other domains.
@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)
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,
h5_font_size,
h6_font_size]
for heading_font_size in heading_font_sizes:
iter += 1
var regex = RegEx.new()
var match_string : String = "([^#]|^)#{%d}\\s([^\n]*)" % iter
var replace_string := HEADING_STRING_REPLACEMENT % [heading_font_size]
if bold_headings:
replace_string = BOLD_HEADING_STRING_REPLACEMENT % [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 = 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:
meta_clicked.connect(_on_meta_clicked)
if not auto_update: return
set_file_path(attribution_file_path)

View File

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

View File

@@ -0,0 +1,28 @@
@tool
extends Label
## Displays the value of `version` from the config file of the specified plugin.
const NO_VERSION_STRING : String = "0.0.0"
@export var plugin_directory : String
@export var version_prefix : String = "v"
func _get_plugin_version() -> String:
if not plugin_directory.is_empty():
for enabled_plugin in ProjectSettings.get_setting("editor_plugins/enabled"):
if enabled_plugin.contains(plugin_directory):
var config := ConfigFile.new()
var error = config.load(enabled_plugin)
if error != OK:
break
return config.get_value("plugin", "version", NO_VERSION_STRING)
return ""
func update_version_label() -> void:
var plugin_version = _get_plugin_version()
if plugin_version.is_empty():
plugin_version = NO_VERSION_STRING
text = version_prefix + plugin_version
func _ready() -> void:
update_version_label()

View File

@@ -0,0 +1,176 @@
class_name LoadingScreen
extends CanvasLayer
## Scene for displaying the progress of a loading scene to the player.
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}
## Delay between updating the message in the window during stalled periods.
@export_range(5, 60, 0.5, "or_greater") var state_change_delay : float = 15.0
@export_group("State Messages")
@export_subgroup("In Progress")
## Default text to show when loading.
@export var _in_progress : String = "Loading..."
## Next text to show when loading has stalled.
@export var _in_progress_waiting : String = "Still Loading..."
## Last text to show when loading has stalled.
@export var _in_progress_still_waiting : String = "Still Loading... (%d seconds)"
@export_subgroup("Completed")
## Default text to show when loading has completed.
@export var _complete : String = "Loading Complete!"
## Next text to show if opening the scene has stalled.
@export var _complete_waiting : String = "Any Moment Now..."
## Last text to show if opening the scene has stalled.
@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,87 @@
[gd_scene format=3 uid="uid://cd0jbh4metflb"]
[ext_resource type="Script" uid="uid://dgeewyjjpk4qn" path="res://addons/maaacks_game_template/base/nodes/loading_screen/loading_screen.gd" id="1_gbk34"]
[node name="LoadingScreen" type="CanvasLayer" unique_id=788661482]
process_mode = 3
layer = 20
script = ExtResource("1_gbk34")
[node name="Control" type="Control" parent="." unique_id=1999015458]
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" unique_id=604252196]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="BackgroundColor" type="ColorRect" parent="Control" unique_id=144482895]
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" unique_id=630614432]
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" unique_id=10456653]
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_id=1503629466]
unique_name_in_owner = true
layout_mode = 2
text = "Loading..."
horizontal_alignment = 1
[node name="ProgressBar" type="ProgressBar" parent="Control/VBoxContainer" unique_id=1483901811]
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_id=1421740495]
unique_name_in_owner = true
title = "Loading Error"
initial_position = 2
size = Vector2i(360, 100)
[node name="StalledMessage" type="ConfirmationDialog" parent="Control" unique_id=1733180994]
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_id=1602373273]
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,127 @@
class_name MainMenu
extends Control
## Base menu scene that links to a game scene, an options menu, and credits.
signal sub_menu_opened
signal sub_menu_closed
signal game_started
signal game_exited
## Defines the path to the game scene. Hides the play button if empty.
## Will attempt to read from AppConfig if left empty.
@export_file("*.tscn") var game_scene_path : String
## The scene to open when a player clicks the 'Options' button.
@export var options_packed_scene : PackedScene
## The scene to open when a player clicks the 'Credits' button.
@export var credits_packed_scene : PackedScene
@export var confirm_exit : bool = true
@export_group("Extra Settings")
## If true, signals that the game has started loading in the background, instead of directly loading it.
@export var signal_game_start : bool = false
## If true, signals that the player clicked the 'Exit' button, instead of immediately exiting.
@export var signal_game_exit : bool = false
var sub_menu : Control
@onready var menu_container = %MenuContainer
@onready var menu_buttons_box_container = %MenuButtonsBoxContainer
@onready var new_game_button = %NewGameButton
@onready var options_button = %OptionsButton
@onready var credits_button = %CreditsButton
@onready var exit_button = %ExitButton
@onready var exit_confirmation = %ExitConfirmation
func get_game_scene_path() -> String:
if game_scene_path.is_empty():
return AppConfig.game_scene_path
return game_scene_path
func load_game_scene() -> void:
if signal_game_start:
SceneLoader.load_scene(get_game_scene_path(), true)
game_started.emit()
else:
SceneLoader.load_scene(get_game_scene_path())
func new_game() -> void:
load_game_scene()
func try_exit_game() -> void:
if confirm_exit and (not exit_confirmation.visible):
exit_confirmation.show()
else:
exit_game()
func exit_game() -> void:
if OS.has_feature("web"):
return
if signal_game_exit:
game_exited.emit()
else:
get_tree().quit()
func _open_sub_menu(menu : PackedScene) -> Node:
sub_menu = menu.instantiate()
add_child(sub_menu)
menu_container.hide()
sub_menu.hidden.connect(_close_sub_menu, CONNECT_ONE_SHOT)
sub_menu.tree_exiting.connect(_close_sub_menu, CONNECT_ONE_SHOT)
sub_menu_opened.emit()
return sub_menu
func _close_sub_menu() -> void:
if sub_menu == null:
return
sub_menu.queue_free()
sub_menu = null
menu_container.show()
sub_menu_closed.emit()
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:
try_exit_game()
if event.is_action_released("ui_accept") and get_viewport().gui_get_focus_owner() == null:
menu_buttons_box_container.focus_first()
func _hide_exit_for_web() -> void:
if OS.has_feature("web"):
exit_button.hide()
func _hide_new_game_if_unset() -> void:
if get_game_scene_path().is_empty():
new_game_button.hide()
func _hide_options_if_unset() -> void:
if options_packed_scene == null:
options_button.hide()
func _hide_credits_if_unset() -> void:
if credits_packed_scene == null:
credits_button.hide()
func _ready() -> void:
_hide_exit_for_web()
_hide_options_if_unset()
_hide_credits_if_unset()
_hide_new_game_if_unset()
func _on_new_game_button_pressed() -> void:
new_game()
func _on_options_button_pressed() -> void:
_open_sub_menu(options_packed_scene)
func _on_credits_button_pressed() -> void:
_open_sub_menu(credits_packed_scene)
func _on_exit_button_pressed() -> void:
try_exit_game()
func _on_exit_confirmation_confirmed():
exit_game()

View File

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

View File

@@ -0,0 +1,182 @@
[gd_scene format=3 uid="uid://c6k5nnpbypshi"]
[ext_resource type="Script" uid="uid://bhgs1upaahk3y" path="res://addons/maaacks_game_template/base/nodes/menus/main_menu/main_menu.gd" id="1"]
[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/nodes/utilities/capture_focus.gd" id="4_l1ebe"]
[ext_resource type="PackedScene" uid="uid://bkcsjsk2ciff" path="res://addons/maaacks_game_template/base/nodes/music_players/background_music_player.tscn" id="4_w8sbm"]
[ext_resource type="Script" uid="uid://b5oej1q4h7jvh" path="res://addons/maaacks_game_template/base/nodes/autoloads/ui_sound_controller/ui_sound_controller.gd" id="6_bs342"]
[ext_resource type="Script" uid="uid://dmkubt2nsnsbn" path="res://addons/maaacks_game_template/base/nodes/labels/config_version_label.gd" id="6_pdiij"]
[ext_resource type="PackedScene" uid="uid://cwt4p3bufkke5" path="res://addons/maaacks_game_template/base/nodes/windows/confirmation_overlaid_window.tscn" id="7_im16j"]
[ext_resource type="Script" uid="uid://bkwlopi4qn32o" path="res://addons/maaacks_game_template/base/nodes/labels/config_name_label.gd" id="7_j7612"]
[node name="MainMenu" type="Control" unique_id=781937481]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
[node name="UISoundController" type="Node" parent="." unique_id=1619985599]
script = ExtResource("6_bs342")
[node name="BackgroundMusicPlayer" parent="." unique_id=1141822549 instance=ExtResource("4_w8sbm")]
[node name="BackgroundTextureRect" type="TextureRect" parent="." unique_id=1940176448]
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="MenuContainer" type="MarginContainer" parent="." unique_id=396616752]
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" unique_id=2112060331]
layout_mode = 2
theme_override_constants/margin_top = 24
[node name="TitleContainer" type="Control" parent="MenuContainer/TitleMargin" unique_id=556427956]
layout_mode = 2
mouse_filter = 2
[node name="TitleLabel" type="Label" parent="MenuContainer/TitleMargin/TitleContainer" unique_id=359406872]
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 = "Title"
horizontal_alignment = 1
vertical_alignment = 1
script = ExtResource("7_j7612")
[node name="SubTitleMargin" type="MarginContainer" parent="MenuContainer" unique_id=1510335937]
layout_mode = 2
theme_override_constants/margin_top = 92
[node name="SubTitleContainer" type="Control" parent="MenuContainer/SubTitleMargin" unique_id=930520368]
layout_mode = 2
mouse_filter = 2
[node name="SubTitleLabel" type="Label" parent="MenuContainer/SubTitleMargin/SubTitleContainer" unique_id=1037003361]
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" unique_id=210553886]
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" unique_id=116448484]
layout_mode = 2
mouse_filter = 2
[node name="MenuButtonsBoxContainer" type="BoxContainer" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer" unique_id=2017477786]
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_id=2044537792]
unique_name_in_owner = true
layout_mode = 2
text = "New Game"
[node name="OptionsButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer" unique_id=1486236076]
unique_name_in_owner = true
layout_mode = 2
text = "Options"
[node name="CreditsButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer" unique_id=301223169]
unique_name_in_owner = true
layout_mode = 2
text = "Credits"
[node name="ExitButton" type="Button" parent="MenuContainer/MenuButtonsMargin/MenuButtonsContainer/MenuButtonsBoxContainer" unique_id=1750454379]
unique_name_in_owner = true
layout_mode = 2
text = "Exit"
[node name="VersionMargin" type="MarginContainer" parent="." unique_id=1732528471]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 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" unique_id=1603969262]
layout_mode = 2
mouse_filter = 2
[node name="VersionLabel" type="Label" parent="VersionMargin/VersionContainer" unique_id=793266470]
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="ExitConfirmation" parent="." unique_id=912502155 instance=ExtResource("7_im16j")]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(300, 160)
layout_mode = 1
offset_left = -150.0
offset_top = -80.0
offset_right = 150.0
offset_bottom = 80.0
ui_cancel_closes = false
text = "Really exit the game?"
title_visible = false
[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="confirmed" from="ExitConfirmation" to="." method="_on_exit_confirmation_confirmed"]

View File

@@ -0,0 +1,38 @@
extends Control
## Scene for adjusting the volume of the audio busses.
@export var audio_control_scene : PackedScene
## Optional names of audio busses that should be ignored.
@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 format=3 uid="uid://c8vnncjwqcpab"]
[ext_resource type="Script" uid="uid://bwugqn2cjr41e" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/audio/audio_options_menu.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://cl416gdb1fgwr" path="res://addons/maaacks_game_template/base/nodes/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/nodes/utilities/capture_focus.gd" id="3_dtraq"]
[ext_resource type="PackedScene" uid="uid://bsxh6v7j0257h" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/option_control/toggle_option_control.tscn" id="4_ojfec"]
[node name="Audio" type="MarginContainer" unique_id=2801527]
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="." unique_id=1265848630]
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_id=2086522930]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
[node name="MuteControl" parent="VBoxContainer" unique_id=372694560 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,349 @@
@tool
class_name InputActionsList
extends Container
## Scene to list the input actions out as buttons in a grid format.
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"
## If true, lists action names on the vertical axis.
@export var vertical : bool = true :
set(value):
vertical = value
if is_inside_tree():
%ParentBoxContainer.vertical = vertical
## The number of inputs to make editable per action name.
@export_range(1, 5) var action_groups : int = 2
## The header to each input action group.
@export var action_group_names : Array[String]
## The names of the action names that should be listed for editing.
@export var input_action_names : Array[StringName] :
set(value):
var _value_changed = input_action_names != value
input_action_names = value
if _value_changed:
_refresh_readable_action_names()
## The readable names of the action names that should be listed for editing.
@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
## If true, capitalizes action names in order to make them readable.
@export var capitalize_action_names : bool = true :
set(value):
capitalize_action_names = value
_refresh_readable_action_names()
## If true, show action names that are not explicitely listed in an input action name map.
@export var show_all_actions : bool = true
## Optional minimum size to add to all edit buttons.
@export var button_minimum_size : Vector2
@export_group("Icons")
## Optional link to an input icon mapper to replace the text with icons.
@export var input_icon_mapper : InputIconMapper
## If true, expand the icons to fill the buttons.
@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 _refresh_readable_action_names():
var _new_readable_action_names : Array[String]
for action_name in input_action_names:
if capitalize_action_names:
action_name = action_name.capitalize()
_new_readable_action_names.append(action_name)
readable_action_names = _new_readable_action_names
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
var button = _get_button_by_action(action_name, action_group)
var readable_input_name : String
if button and button in button_readable_input_map:
readable_input_name = button_readable_input_map[button]
_replace_action(action_name, readable_input_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()
if button_minimum_size.x > 0:
new_label.custom_minimum_size.x = button_minimum_size.x
new_label.size_flags_horizontal = SIZE_SHRINK_CENTER
else:
new_label.size_flags_horizontal = SIZE_EXPAND_FILL
if button_minimum_size.y > 0:
new_label.custom_minimum_size.y = button_minimum_size.y
new_label.size_flags_vertical = SIZE_SHRINK_CENTER
else:
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, disabled: bool = false) -> void:
var button = _get_button_by_action(action_name, action_group + 1)
if button:
button.disabled = disabled
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()
if button_minimum_size.x > 0:
new_button.custom_minimum_size.x = button_minimum_size.x
new_button.size_flags_horizontal = SIZE_SHRINK_CENTER
else:
new_button.size_flags_horizontal = SIZE_EXPAND_FILL
if button_minimum_size.y > 0:
new_button.custom_minimum_size.y = button_minimum_size.y
new_button.size_flags_vertical = SIZE_SHRINK_CENTER
else:
new_button.size_flags_vertical = SIZE_EXPAND_FILL
new_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
new_button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
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(action_name : StringName) -> String:
var readable_name : String
if action_name in action_name_map:
readable_name = action_name_map[action_name]
elif action_name in built_in_action_name_map:
readable_name = built_in_action_name_map[action_name]
else:
readable_name = action_name
if capitalize_action_names:
readable_name = readable_name.capitalize()
action_name_map[action_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)
_update_next_button_disabled_state(action_name, group_iter)
group_iter += 1
while group_iter < action_groups:
_clear_button(action_name, group_iter)
_update_next_button_disabled_state(action_name, group_iter, true)
group_iter += 1
func _set_action_box_container_size() -> void:
if button_minimum_size.x > 0:
%ActionBoxContainer.size_flags_horizontal = SIZE_SHRINK_CENTER
else:
%ActionBoxContainer.size_flags_horizontal = SIZE_EXPAND_FILL
if button_minimum_size.y > 0:
%ActionBoxContainer.size_flags_vertical = SIZE_SHRINK_CENTER
else:
%ActionBoxContainer.size_flags_vertical = SIZE_EXPAND_FILL
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
_set_action_box_container_size()
_build_assigned_input_events()
_build_ui_list.call_deferred()
if input_icon_mapper:
input_icon_mapper.joypad_device_changed.connect(_refresh_ui_list_button_content)

View File

@@ -0,0 +1,43 @@
[gd_scene format=3 uid="uid://bxp45814v6ydv"]
[ext_resource type="Script" uid="uid://b3q5fgjev8gyo" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/input/input_actions_list.gd" id="1_cxorh"]
[node name="InputActionsList" type="ScrollContainer" unique_id=1135275701]
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_id=1544918220]
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_id=694881838]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="ActionNameLabel" type="Label" parent="ParentBoxContainer/ActionBoxContainer" unique_id=1115508057]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2

View File

@@ -0,0 +1,232 @@
@tool
class_name InputActionsTree
extends Tree
## Scene to list the input actions out in a tree format.
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)
## The names of the action names that should be listed for editing.
@export var input_action_names : Array[StringName] :
set(value):
var _value_changed = input_action_names != value
input_action_names = value
if _value_changed:
_refresh_readable_action_names()
## The readable names of the action names that should be listed for editing.
@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
## If true, capitalizes action names in order to make them readable.
@export var capitalize_action_names : bool = true :
set(value):
capitalize_action_names = value
_refresh_readable_action_names()
## Show action names that are not explicitely listed in an action name map.
@export var show_all_actions : bool = true
@export_group("Icons")
## Icon for the button that adds a new input to an action name.
@export var add_button_texture : Texture2D
## Icon for the button that removes an input to an action name.
@export var remove_button_texture : Texture2D
## Optional link to an input icon mapper to replace the text with icons.
@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 _refresh_readable_action_names():
var _new_readable_action_names : Array[String]
for action_name in input_action_names:
if capitalize_action_names:
action_name = action_name.capitalize()
_new_readable_action_names.append(action_name)
readable_action_names = _new_readable_action_names
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(action_name : StringName) -> String:
var readable_name : String
if action_name in action_name_map:
readable_name = action_name_map[action_name]
elif action_name in built_in_action_name_map:
readable_name = built_in_action_name_map[action_name]
else:
readable_name = action_name
if capitalize_action_names:
readable_name = readable_name.capitalize()
action_name_map[action_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.call_deferred()
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,29 @@
[gd_scene format=3 uid="uid://ci6wgl2ngd35n"]
[ext_resource type="Script" uid="uid://bp7d2e5djo2tp" path="res://addons/maaacks_game_template/base/nodes/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" unique_id=1082851938]
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
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,6 @@
[gd_scene format=3 uid="uid://qoexj4ptqt8a"]
[ext_resource type="Script" uid="uid://cqigj1uumknrp" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/input/input_icon_mapper.gd" id="1_msrpt"]
[node name="InputIconMapper" type="Node" unique_id=338050523]
script = ExtResource("1_msrpt")

View File

@@ -0,0 +1,92 @@
@tool
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 = $KeyAssignmentWindow.text
var last_input_readable_name
func _ready() -> void:
remapping_mode = remapping_mode
func _add_action_event() -> void:
var last_input_event = $KeyAssignmentWindow.last_input_event
last_input_readable_name = $KeyAssignmentWindow.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:
$ResetConfirmation.show()
func _on_key_deletion_confirmation_confirmed() -> void:
var editing_item = %InputActionsTree.editing_item
if is_instance_valid(editing_item):
_remove_action_event(editing_item)
func _on_key_assignment_window_confirmed() -> void:
_add_action_event()
func _open_key_assignment_window(action_name : String, readable_input_name : String = assignment_placeholder_text) -> void:
$KeyAssignmentWindow.title = tr("Assign Key for {action}").format({action = action_name})
$KeyAssignmentWindow.text = readable_input_name
$KeyAssignmentWindow.confirm_button.disabled = true
$KeyAssignmentWindow.show()
func _on_input_actions_tree_add_button_clicked(action_name) -> void:
_open_key_assignment_window(action_name)
func _on_input_actions_tree_remove_button_clicked(action_name, input_name) -> void:
$KeyDeletionConfirmation.title = tr("Remove Key for {action}").format({action = action_name})
$KeyDeletionConfirmation.text = tr(KEY_DELETION_TEXT).format({key = input_name, action = action_name})
$KeyDeletionConfirmation.show()
func _popup_already_assigned(action_name, input_name) -> void:
$AlreadyAssignedMessage.text = tr(ALREADY_ASSIGNED_TEXT).format({key = input_name, action = action_name})
$AlreadyAssignedMessage.show()
func _popup_minimum_reached(action_name : String) -> void:
$OneInputMinimumMessage.text = ONE_INPUT_MINIMUM_TEXT % action_name
$OneInputMinimumMessage.show()
func _on_input_actions_tree_already_assigned(action_name, input_name) -> void:
_popup_already_assigned.call_deferred(action_name, input_name)
func _on_input_actions_tree_minimum_reached(action_name) -> void:
_popup_minimum_reached.call_deferred(action_name)
func _on_input_actions_list_already_assigned(action_name, input_name) -> void:
_popup_already_assigned.call_deferred(action_name, input_name)
func _on_input_actions_list_minimum_reached(action_name) -> void:
_popup_minimum_reached.call_deferred(action_name)
func _on_input_actions_list_button_clicked(action_name, readable_input_name) -> void:
_open_key_assignment_window(action_name, readable_input_name)
func _on_reset_confirmation_confirmed() -> void:
match(remapping_mode):
0:
%InputActionsList.reset()
1:
%InputActionsTree.reset()

View File

@@ -0,0 +1,100 @@
[gd_scene format=3 uid="uid://dp3rgqaehb3xu"]
[ext_resource type="Script" uid="uid://eborw7q4b07h" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/input/input_options_menu.gd" id="1"]
[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/nodes/utilities/capture_focus.gd" id="2_wft4x"]
[ext_resource type="PackedScene" uid="uid://bxp45814v6ydv" path="res://addons/maaacks_game_template/base/nodes/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/nodes/menus/options_menu/input/input_actions_tree.tscn" id="5_b2whh"]
[ext_resource type="PackedScene" uid="uid://cwt4p3bufkke5" path="res://addons/maaacks_game_template/base/nodes/windows/confirmation_overlaid_window.tscn" id="7_5j1ya"]
[ext_resource type="PackedScene" uid="uid://dgravx3vt5g3i" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/input/key_assignment_window.tscn" id="7_r3r3g"]
[ext_resource type="PackedScene" uid="uid://6gdbfi0172ji" path="res://addons/maaacks_game_template/base/nodes/windows/overlaid_window.tscn" id="8_jtpjy"]
[node name="Controls" type="MarginContainer" unique_id=1417083348]
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="VBoxContainer" type="VBoxContainer" parent="." unique_id=539297323]
layout_mode = 2
script = ExtResource("2_wft4x")
search_depth = 5
[node name="InputMappingContainer" type="VBoxContainer" parent="VBoxContainer" unique_id=1004992485]
layout_mode = 2
size_flags_vertical = 3
alignment = 1
[node name="Label" type="Label" parent="VBoxContainer/InputMappingContainer" unique_id=1142261578]
layout_mode = 2
text = "Actions & Inputs"
horizontal_alignment = 1
[node name="InputActionsList" parent="VBoxContainer/InputMappingContainer" unique_id=2089976301 instance=ExtResource("4_lf2nw")]
unique_name_in_owner = true
layout_mode = 2
[node name="InputActionsTree" parent="VBoxContainer/InputMappingContainer" unique_id=780755880 instance=ExtResource("5_b2whh")]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/InputMappingContainer" unique_id=668442458]
layout_mode = 2
alignment = 1
[node name="ResetButton" type="Button" parent="VBoxContainer/InputMappingContainer/HBoxContainer" unique_id=14998312]
layout_mode = 2
text = "Reset"
[node name="KeyDeletionConfirmation" parent="." unique_id=1120617858 instance=ExtResource("7_5j1ya")]
visible = false
custom_minimum_size = Vector2(420, 200)
layout_mode = 2
text = "Are you sure you want to remove KEY from ACTION?"
title = "Remove Key"
[node name="ResetConfirmation" parent="." unique_id=1967398625 instance=ExtResource("7_5j1ya")]
visible = false
custom_minimum_size = Vector2(420, 200)
layout_mode = 2
text = "Are you sure you want to reset controls back to the defaults?"
title = "Reset to Default"
[node name="OneInputMinimumMessage" parent="." unique_id=1110523665 instance=ExtResource("8_jtpjy")]
visible = false
custom_minimum_size = Vector2(420, 200)
layout_mode = 2
update_content = true
title = "One Input Minimum"
[node name="AlreadyAssignedMessage" parent="." unique_id=574954440 instance=ExtResource("8_jtpjy")]
visible = false
custom_minimum_size = Vector2(420, 200)
layout_mode = 2
update_content = true
title = "Already Assigned"
[node name="KeyAssignmentWindow" parent="." unique_id=751125317 instance=ExtResource("7_r3r3g")]
visible = false
custom_minimum_size = Vector2(420, 200)
layout_mode = 2
[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="KeyDeletionConfirmation" to="." method="_on_key_deletion_confirmation_confirmed"]
[connection signal="confirmed" from="ResetConfirmation" to="." method="_on_reset_confirmation_confirmed"]
[connection signal="confirmed" from="KeyAssignmentWindow" to="." method="_on_key_assignment_window_confirmed"]

View File

@@ -0,0 +1,112 @@
@tool
extends ConfirmationOverlaidWindow
## Scene to confirm a new input for an action name.
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
}
## Confirmations required before a new input is accepted for an aciton.
@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
confirm_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_input_text_edit_focus_entered() -> void:
_start_listening.call_deferred()
func _on_input_text_edit_focus_exited() -> void:
_stop_listening()
func _focus_on_ok() -> void:
confirm_button.grab_focus()
func _ready() -> void:
confirm_button.focus_neighbor_top = ^"../../../BodyMargin/VBoxContainer/InputTextEdit"
close_button.focus_neighbor_top = ^"../../../BodyMargin/VBoxContainer/InputTextEdit"
super._ready()
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()
close()
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:
super._on_visibility_changed()
if visible:
if not text.strip_edges().is_empty():
%InputLabel.text = text
else:
%InputLabel.text = NO_INPUT_TEXT
%InputTextEdit.grab_focus()

View File

@@ -0,0 +1,64 @@
[gd_scene format=3 uid="uid://dgravx3vt5g3i"]
[ext_resource type="PackedScene" uid="uid://cwt4p3bufkke5" path="res://addons/maaacks_game_template/base/nodes/windows/confirmation_overlaid_window.tscn" id="1_6c67a"]
[ext_resource type="Script" uid="uid://custha7r0uoic" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/input/key_assignment_window.gd" id="2_oif0q"]
[node name="KeyAssignmentWindow" unique_id=1194922714 instance=ExtResource("1_6c67a")]
offset_left = -210.0
offset_top = -100.0
offset_right = 210.0
offset_bottom = 100.0
script = ExtResource("2_oif0q")
input_confirmation = 0
close_button_text = "Close"
title = "Set Input"
[node name="TitleLabel" parent="ContentContainer/BoxContainer/TitleMargin/BoxContainer" parent_id_path=PackedInt32Array(1788474031) index="0" unique_id=1049966061]
text = "Set Input"
[node name="DescriptionLabel" parent="ContentContainer/BoxContainer/BodyMargin" parent_id_path=PackedInt32Array(590613964) index="0" unique_id=617407155]
visible = false
[node name="VBoxContainer" type="VBoxContainer" parent="ContentContainer/BoxContainer/BodyMargin" parent_id_path=PackedInt32Array(590613964) index="1" unique_id=1136766756]
layout_mode = 2
size_flags_vertical = 3
[node name="InputLabel" type="Label" parent="ContentContainer/BoxContainer/BodyMargin/VBoxContainer" index="0" unique_id=1464176867]
unique_name_in_owner = true
layout_mode = 2
text = "None"
horizontal_alignment = 1
[node name="InputTextEdit" type="TextEdit" parent="ContentContainer/BoxContainer/BodyMargin/VBoxContainer" index="1" unique_id=1475588538]
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="MenuButtons" parent="ContentContainer/BoxContainer/MenuButtonsMargin" parent_id_path=PackedInt32Array(1413292752) index="0" unique_id=1371114575]
null_focus_enabled = false
joypad_enabled = false
mouse_hidden_enabled = false
[node name="CloseButton" parent="ContentContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0" unique_id=314526102]
focus_neighbor_top = NodePath("../../../BodyMargin/VBoxContainer/InputTextEdit")
text = "Close"
[node name="ConfirmButton" parent="ContentContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1" unique_id=1052970550]
focus_neighbor_top = NodePath("../../../BodyMargin/VBoxContainer/InputTextEdit")
[node name="DelayTimer" type="Timer" parent="." index="1" unique_id=1825912432]
unique_name_in_owner = true
wait_time = 0.1
one_shot = true
[connection signal="focus_entered" from="ContentContainer/BoxContainer/BodyMargin/VBoxContainer/InputTextEdit" to="." method="_on_input_text_edit_focus_entered"]
[connection signal="focus_exited" from="ContentContainer/BoxContainer/BodyMargin/VBoxContainer/InputTextEdit" to="." method="_on_input_text_edit_focus_exited"]
[connection signal="gui_input" from="ContentContainer/BoxContainer/BodyMargin/VBoxContainer/InputTextEdit" to="." method="_on_input_text_edit_gui_input"]

View File

@@ -0,0 +1,46 @@
extends Control
@onready var mute_control = %MuteControl
@onready var fullscreen_control = %FullscreenControl
## Scene for adjusting the volume of the audio busses.
@export var audio_control_scene : PackedScene
## Optional names of audio busses that should be ignored.
@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,84 @@
@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 _refresh_option_values(value : Variant) -> void:
if option_values.is_empty(): return
if value == null:
return
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))
func set_value(value : Variant) -> void:
_refresh_option_values(value)
value = custom_option_values.find(value)
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 format=3 uid="uid://b6bl3n5mp3m1e"]
[ext_resource type="PackedScene" uid="uid://d7te75il06t7" path="res://addons/maaacks_game_template/base/nodes/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/nodes/menus/options_menu/option_control/list_option_control.gd" id="2_kt4vl"]
[node name="OptionControl" unique_id=16284350 instance=ExtResource("1_blo3b")]
script = ExtResource("2_kt4vl")
lock_titles = false
option_values = []
option_titles = []
[node name="OptionButton" type="OptionButton" parent="." index="1" unique_id=264509485]
unique_name_in_owner = true
layout_mode = 2

View File

@@ -0,0 +1,136 @@
@tool
class_name OptionControl
extends Control
## Generic scene for editing a value of the [PlayerConfig].
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
PlayerConfig.set_config(section, key, value)
setting_changed.emit(value)
func _get_setting(default : Variant = null) -> Variant:
return PlayerConfig.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) -> void:
if value == null:
return
for node in get_children():
if node is Button:
if node is OptionButton:
node.select(value as int)
elif node is ColorPickerButton:
node.color = value as Color
else:
node.button_pressed = value as bool
if node is Range:
node.value = value as float
if node is LineEdit or node is TextEdit:
node.text = "%s" % value
func set_editable(value : bool = true) -> 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 format=3 uid="uid://d7te75il06t7"]
[ext_resource type="Script" uid="uid://cafqki2b08kwu" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/option_control/option_control.gd" id="1_jvl5q"]
[node name="OptionControl" type="HBoxContainer" unique_id=364203356]
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_id=1854788461]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = " :"
vertical_alignment = 1

View File

@@ -0,0 +1,19 @@
[gd_scene format=3 uid="uid://cl416gdb1fgwr"]
[ext_resource type="PackedScene" uid="uid://d7te75il06t7" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/option_control/option_control.tscn" id="1_16hlr"]
[node name="OptionControl" unique_id=1557081957 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" unique_id=424108384]
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 format=3 uid="uid://bsxh6v7j0257h"]
[ext_resource type="PackedScene" uid="uid://d7te75il06t7" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/option_control/option_control.tscn" id="1_8rnmo"]
[node name="OptionControl" unique_id=1563851297 instance=ExtResource("1_8rnmo")]
[node name="CheckButton" type="CheckButton" parent="." index="1" unique_id=1452268147]
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 format=3 uid="uid://c01ayjblhcg1t"]
[ext_resource type="PackedScene" uid="uid://b6bl3n5mp3m1e" path="res://addons/maaacks_game_template/base/nodes/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/nodes/menus/options_menu/option_control/vector_2_list_option_control.gd" id="2_w33vs"]
[node name="OptionControl" unique_id=21057374 instance=ExtResource("1_jqwiw")]
script = ExtResource("2_w33vs")

View File

@@ -0,0 +1,13 @@
extends TabContainer
## Applies UI page up and page down inputs to tab switching.
func _unhandled_input(event : InputEvent) -> void:
if not is_visible_in_tree():
return
if event.is_action_pressed("ui_page_down"):
current_tab = (current_tab+1) % get_tab_count()
elif event.is_action_pressed("ui_page_up"):
if current_tab == 0:
current_tab = get_tab_count()-1
else:
current_tab = current_tab-1

View File

@@ -0,0 +1,37 @@
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 format=3 uid="uid://b2numvphf2kau"]
[ext_resource type="Script" uid="uid://cpe5r24151r5n" path="res://addons/maaacks_game_template/base/nodes/menus/options_menu/video/video_options_menu.gd" id="1"]
[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/nodes/utilities/capture_focus.gd" id="2_dgrai"]
[ext_resource type="PackedScene" uid="uid://bsxh6v7j0257h" path="res://addons/maaacks_game_template/base/nodes/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/nodes/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/nodes/menus/options_menu/option_control/list_option_control.tscn" id="5_881de"]
[node name="Video" type="MarginContainer" unique_id=1604782575]
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="." unique_id=1251699420]
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" unique_id=1477535498 instance=ExtResource("3_uded6")]
unique_name_in_owner = true
layout_mode = 2
option_name = "Fullscreen"
option_section = 3
key = "Fullscreen"
section = "VideoSettings"
[node name="ResolutionControl" parent="VBoxContainer" unique_id=646319285 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" unique_id=2127470729 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" unique_id=1446051342]
process_mode = 3
autoplay = true
bus = &"Music"

View File

@@ -0,0 +1,120 @@
extends Control
## Scene for displaying opening logos, placards, or other images before a game.
## Defines the path to the next scene.
## Will attempt to read from AppConfig if left empty.
@export_file("*.tscn") var next_scene_path : String
## The list of images to show in the opening sequence.
@export var images : Array[Texture2D]
@export_group("Animation")
## The time to fade-in the next image.
@export var fade_in_time : float = 0.2
## The time to fade-out the previous image.
@export var fade_out_time : float = 0.2
## The time to keep an image visible after fade-in and before fade-out.
@export var visible_time : float = 1.6
@export_group("Transition")
## The delay before starting the first fade-in animation once ready.
@export var start_delay : float = 0.5
## The delay after ending the last fade-in animation before loading the next scene.
@export var end_delay : float = 0.5
## If true, show a loading screen if the next scene is not yet ready.
@export var show_loading_screen : bool = false
var tween : Tween
var next_image_index : int = 0
func get_next_scene_path() -> String:
if next_scene_path.is_empty():
return AppConfig.main_menu_scene_path
return next_scene_path
func _on_scene_loaded() -> void:
SceneLoader.change_scene_to_resource()
func _load_next_scene() -> void:
var status = SceneLoader.get_status()
if status == ResourceLoader.THREAD_LOAD_LOADED:
_on_scene_loaded()
elif show_loading_screen:
SceneLoader.change_scene_to_loading_screen()
elif not SceneLoader.scene_loaded.is_connected(_on_scene_loaded):
SceneLoader.scene_loaded.connect(_on_scene_loaded, CONNECT_ONE_SHOT)
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(get_next_scene_path(), true)
_add_textures_to_container(images)
_transition_in()

View File

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

View File

@@ -0,0 +1,26 @@
[gd_scene format=3 uid="uid://sikc02ddepyt"]
[ext_resource type="Script" uid="uid://dtco0s8byckx6" path="res://addons/maaacks_game_template/base/nodes/opening/opening.gd" id="1_fcjph"]
[node name="Opening" type="Control" unique_id=331014594]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_fcjph")
[node name="BackgroundMusicPlayer" type="AudioStreamPlayer" parent="." unique_id=1317385298]
process_mode = 3
autoplay = true
bus = &"Music"
[node name="ImagesContainer" type="MarginContainer" parent="." unique_id=1471559661]
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,50 @@
class_name GlobalState
extends Node
const SAVE_STATE_PATH = "user://global_state.tres"
const NO_VERSION_NAME = "0.0.0"
static var current : GlobalStateData
static var current_version : String
static func _log_opened() -> void:
if current is GlobalStateData:
current.last_unix_time_opened = int(Time.get_unix_time_from_system())
static func _log_version() -> void:
if current is GlobalStateData:
current_version = ProjectSettings.get_setting("application/config/version", NO_VERSION_NAME)
if current_version.is_empty():
current_version = NO_VERSION_NAME
if not current.first_version_opened:
current.first_version_opened = current_version
current.last_version_opened = current_version
static func _load_current_state() -> void:
if FileAccess.file_exists(SAVE_STATE_PATH):
current = ResourceLoader.load(SAVE_STATE_PATH)
if not current:
current = GlobalStateData.new()
static func open() -> void:
_load_current_state()
_log_opened()
_log_version()
save()
static func save() -> void:
if current is GlobalStateData:
ResourceSaver.save(current, SAVE_STATE_PATH)
static func has_state(state_key : String) -> bool:
if current is not GlobalStateData: return false
return current.has_state(state_key)
static func get_or_create_state(state_key : String, state_type_path : String) -> Resource:
if current is not GlobalStateData: return
return current.get_or_create_state(state_key, state_type_path)
static func reset() -> void:
if current is not GlobalStateData: return
current.states.clear()
save()

View File

@@ -0,0 +1 @@
uid://34ojrqt1klav

View File

@@ -0,0 +1,24 @@
class_name GlobalStateData
extends Resource
@export var first_version_opened : String
@export var last_version_opened : String
@export var last_unix_time_opened : int
@export var states : Dictionary
func get_or_create_state(key_name : String, state_type_path : String) -> Resource:
var new_state : Resource
var new_state_script = load(state_type_path)
if new_state_script is GDScript:
new_state = new_state_script.new()
if key_name in states:
var saved_state : Resource = states[key_name]
var saved_script = saved_state.get_script()
var new_script = new_state.get_script()
if saved_script and new_script and saved_script == new_script:
return saved_state
states[key_name] = new_state
return new_state
func has_state(key_name : String) -> bool:
return key_name in states

View File

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

View File

@@ -0,0 +1,68 @@
extends Control
## Node that captures UI focus when switching menus.
##
## 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 for a focusable control node.
@export var search_depth : int = 1
## If true, always capture focus when made visible.
@export var enabled : bool = false
## If true, capture focus if nothing currently is in focus.
@export var null_focus_enabled : bool = true
## If true, capture focus if there is a joypad detected.
@export var joypad_enabled : bool = true
## If true, capture focus if the mouse is hidden.
@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,55 @@
@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")
## Include any results that match the string.
@export var search : String
## Exclude any results that match the string.
@export var filter : String
@export_subgroup("Advanced Search")
## Include any results that begin with the string.
@export var begins_with : String
## Include any results that end with the string.
@export var ends_with : String
## Exclude any results that begin with the string.
@export var not_begins_with : String
## Exclude any results that end with the 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)

View File

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

View File

@@ -0,0 +1,175 @@
class_name InputEventHelper
extends Node
## Helper class for organizing constants related to [InputEvent].
const DEVICE_KEYBOARD = "Keyboard"
const DEVICE_MOUSE = "Mouse"
const DEVICE_XBOX_CONTROLLER = "Xbox"
const DEVICE_SWITCH_CONTROLLER = "Switch"
const DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER = "Switch Left Joycon"
const DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER = "Switch Right Joycon"
const DEVICE_SWITCH_JOYCON_COMBINED_CONTROLLER = "Switch Combined Joycons"
const DEVICE_PLAYSTATION_CONTROLLER = "Playstation"
const DEVICE_STEAMDECK_CONTROLLER = "Steamdeck"
const DEVICE_GENERIC = "Generic"
const JOYSTICK_LEFT_NAME = "Left Stick"
const JOYSTICK_RIGHT_NAME = "Right Stick"
const D_PAD_NAME = "Dpad"
const MOUSE_BUTTONS : Array = ["None", "Left", "Right", "Middle", "Scroll Up", "Scroll Down", "Wheel Left", "Wheel Right"]
const JOYPAD_BUTTON_NAME_MAP : Dictionary = {
DEVICE_GENERIC : ["Trigger A", "Trigger B", "Trigger C", "", "", "", "", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right"],
DEVICE_XBOX_CONTROLLER : ["A", "B", "X", "Y", "View", "Home", "Menu", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Share"],
DEVICE_SWITCH_CONTROLLER : ["B", "A", "Y", "X", "Minus", "", "Plus", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Capture"],
DEVICE_PLAYSTATION_CONTROLLER : ["Cross", "Circle", "Square", "Triangle", "Select", "PS", "Options", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right", "Microphone"],
DEVICE_STEAMDECK_CONTROLLER : ["A", "B", "X", "Y", "View", "", "Options", "Left Stick Press", "Right Stick Press", "Left Shoulder", "Right Shoulder", "Up", "Down", "Left", "Right"]
} # Dictionary[String, Array]
const SDL_DEVICE_NAMES: Dictionary = {
DEVICE_XBOX_CONTROLLER: ["XInput", "XBox"],
DEVICE_PLAYSTATION_CONTROLLER: ["Sony", "PS5", "PS4", "Nacon"],
DEVICE_STEAMDECK_CONTROLLER: ["Steam"],
DEVICE_SWITCH_CONTROLLER: ["Switch"],
DEVICE_SWITCH_JOYCON_LEFT_CONTROLLER: ["Joy-Con (L)", "Left Joy-Con"],
DEVICE_SWITCH_JOYCON_RIGHT_CONTROLLER: ["Joy-Con (R)", "Right Joy-Con"],
DEVICE_SWITCH_JOYCON_COMBINED_CONTROLLER: ["Joy-Con (L/R)", "Combined Joy-Cons"],
}
const JOY_BUTTON_NAMES : Dictionary = {
JOY_BUTTON_A: "Button A",
JOY_BUTTON_B: "Button B",
JOY_BUTTON_X: "Button X",
JOY_BUTTON_Y: "Button Y",
JOY_BUTTON_LEFT_SHOULDER: "Left Shoulder",
JOY_BUTTON_RIGHT_SHOULDER: "Right Shoulder",
JOY_BUTTON_LEFT_STICK: "Left Stick",
JOY_BUTTON_RIGHT_STICK: "Right Stick",
JOY_BUTTON_START : "Button Start",
JOY_BUTTON_GUIDE : "Button Guide",
JOY_BUTTON_BACK : "Button Back",
JOY_BUTTON_DPAD_UP : D_PAD_NAME + " Up",
JOY_BUTTON_DPAD_DOWN : D_PAD_NAME + " Down",
JOY_BUTTON_DPAD_LEFT : D_PAD_NAME + " Left",
JOY_BUTTON_DPAD_RIGHT : D_PAD_NAME + " Right",
JOY_BUTTON_MISC1 : "Misc",
}
const JOYPAD_DPAD_NAMES : Dictionary = {
JOY_BUTTON_DPAD_UP : D_PAD_NAME + " Up",
JOY_BUTTON_DPAD_DOWN : D_PAD_NAME + " Down",
JOY_BUTTON_DPAD_LEFT : D_PAD_NAME + " Left",
JOY_BUTTON_DPAD_RIGHT : D_PAD_NAME + " Right",
}
const JOY_AXIS_NAMES : Dictionary = {
JOY_AXIS_TRIGGER_LEFT: "Left Trigger",
JOY_AXIS_TRIGGER_RIGHT: "Right Trigger",
}
const BUILT_IN_ACTION_NAME_MAP : Dictionary = {
"ui_accept" : "Accept",
"ui_select" : "Select",
"ui_cancel" : "Cancel",
"ui_focus_next" : "Focus Next",
"ui_focus_prev" : "Focus Prev",
"ui_left" : "Left (UI)",
"ui_right" : "Right (UI)",
"ui_up" : "Up (UI)",
"ui_down" : "Down (UI)",
"ui_page_up" : "Page Up",
"ui_page_down" : "Page Down",
"ui_home" : "Home",
"ui_end" : "End",
"ui_cut" : "Cut",
"ui_copy" : "Copy",
"ui_paste" : "Paste",
"ui_undo" : "Undo",
"ui_redo" : "Redo",
}
static func has_joypad() -> bool:
return Input.get_connected_joypads().size() > 0
static func is_joypad_event(event: InputEvent) -> bool:
return event is InputEventJoypadButton or event is InputEventJoypadMotion
static func is_mouse_event(event: InputEvent) -> bool:
return event is InputEventMouseButton or event is InputEventMouseMotion
static func get_device_name_by_id(device_id : int) -> String:
if device_id >= 0:
var device_name = Input.get_joy_name(device_id)
for device_key in SDL_DEVICE_NAMES:
for keyword in SDL_DEVICE_NAMES[device_key]:
if device_name.containsn(keyword):
return device_key
return DEVICE_GENERIC
static func get_device_name(event: InputEvent) -> String:
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
if event.device == -1:
return DEVICE_GENERIC
var device_id = event.device
return get_device_name_by_id(device_id)
return DEVICE_GENERIC
static func _display_server_supports_keycode_from_physical():
return OS.has_feature("windows") or OS.has_feature("macos") or OS.has_feature("linux")
static func get_text(event : InputEvent) -> String:
if event == null:
return ""
if event is InputEventJoypadButton:
if event.button_index in JOY_BUTTON_NAMES:
return JOY_BUTTON_NAMES[event.button_index]
elif event is InputEventJoypadMotion:
var full_string := ""
var direction_string := ""
var is_right_or_down : bool = event.axis_value > 0.0
if event.axis in JOY_AXIS_NAMES:
return JOY_AXIS_NAMES[event.axis]
match(event.axis):
JOY_AXIS_LEFT_X:
full_string = JOYSTICK_LEFT_NAME
direction_string = "Right" if is_right_or_down else "Left"
JOY_AXIS_LEFT_Y:
full_string = JOYSTICK_LEFT_NAME
direction_string = "Down" if is_right_or_down else "Up"
JOY_AXIS_RIGHT_X:
full_string = JOYSTICK_RIGHT_NAME
direction_string = "Right" if is_right_or_down else "Left"
JOY_AXIS_RIGHT_Y:
full_string = JOYSTICK_RIGHT_NAME
direction_string = "Down" if is_right_or_down else "Up"
full_string += " " + direction_string
return full_string
elif event is InputEventKey:
var keycode : Key = event.get_physical_keycode()
if keycode:
keycode = event.get_physical_keycode_with_modifiers()
else:
keycode = event.get_keycode_with_modifiers()
if _display_server_supports_keycode_from_physical():
keycode = DisplayServer.keyboard_get_keycode_from_physical(keycode)
return OS.get_keycode_string(keycode)
return event.as_text()
static func get_device_specific_text(event : InputEvent, device_name : String = "") -> String:
if device_name.is_empty():
device_name = get_device_name(event)
if event is InputEventJoypadButton:
var joypad_button : String = ""
if event.button_index in JOYPAD_DPAD_NAMES:
joypad_button = JOYPAD_DPAD_NAMES[event.button_index]
elif event.button_index < JOYPAD_BUTTON_NAME_MAP[device_name].size():
joypad_button = JOYPAD_BUTTON_NAME_MAP[device_name][event.button_index]
return "%s %s" % [device_name, joypad_button]
if event is InputEventJoypadMotion:
return "%s %s" % [device_name, get_text(event)]
if event is InputEventMouseButton:
if event.button_index < MOUSE_BUTTONS.size():
var mouse_button : String = MOUSE_BUTTONS[event.button_index]
return "%s %s" % [DEVICE_MOUSE, mouse_button]
return get_text(event).capitalize()

View File

@@ -0,0 +1 @@
uid://6xujceamar4h

View File

@@ -0,0 +1,31 @@
extends Node
## Node for opening a pause menu when detecting a 'ui_cancel' event.
@export var pause_menu_packed : PackedScene
@export var focused_viewport : Viewport
var pause_menu : Node
func pause() -> void:
if pause_menu.visible: return
if not focused_viewport:
focused_viewport = get_viewport()
var _initial_focus_control = focused_viewport.gui_get_focus_owner()
pause_menu.show()
if pause_menu is CanvasLayer:
await pause_menu.visibility_changed
else:
await pause_menu.hidden
if is_inside_tree() and _initial_focus_control:
_initial_focus_control.grab_focus()
# If pause menu should take precedence, override _input() instead.
func _unhandled_input(event : InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
pause()
func _ready() -> void:
pause_menu = pause_menu_packed.instantiate()
pause_menu.hide()
get_tree().current_scene.call_deferred("add_child", pause_menu)

View File

@@ -0,0 +1,20 @@
@tool
class_name ConfirmationOverlaidWindow
extends OverlaidWindow
signal confirmed
@onready var confirm_button : Button = %ConfirmButton
@export var confirm_button_text : String = "Confirm" :
set(value):
confirm_button_text = value
if update_content and is_inside_tree():
confirm_button.text = confirm_button_text
func confirm():
confirmed.emit()
close()
func _on_confirm_button_pressed():
confirm()

View File

@@ -0,0 +1,23 @@
[gd_scene format=3 uid="uid://cwt4p3bufkke5"]
[ext_resource type="PackedScene" uid="uid://6gdbfi0172ji" path="res://addons/maaacks_game_template/base/nodes/windows/overlaid_window.tscn" id="1_vfkm2"]
[ext_resource type="Script" uid="uid://bgthh72eu0du" path="res://addons/maaacks_game_template/base/nodes/windows/confirmation_overlaid_window.gd" id="2_sw7p1"]
[node name="ConfirmationOverlaidWindow" unique_id=1482267070 instance=ExtResource("1_vfkm2")]
script = ExtResource("2_sw7p1")
confirm_button_text = "Confirm"
update_content = true
close_button_text = "Cancel"
[node name="MenuButtons" parent="ContentContainer/BoxContainer/MenuButtonsMargin" parent_id_path=PackedInt32Array(1413292752) index="0" unique_id=1371114575]
vertical = false
[node name="CloseButton" parent="ContentContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="0" unique_id=314526102]
text = "Cancel"
[node name="ConfirmButton" type="Button" parent="ContentContainer/BoxContainer/MenuButtonsMargin/MenuButtons" index="1" unique_id=1052970550]
unique_name_in_owner = true
layout_mode = 2
text = "Confirm"
[connection signal="pressed" from="ContentContainer/BoxContainer/MenuButtonsMargin/MenuButtons/ConfirmButton" to="." method="_on_confirm_button_pressed"]

View File

@@ -0,0 +1,77 @@
@tool
class_name OverlaidWindow
extends WindowContainer
@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
@export var exclusive : bool = true
@export var exclusive_background_color : Color
var _initial_pause_state : bool = false
var _initial_mouse_mode : Input.MouseMode
var _initial_focus_control
var _initial_node_focus_modes : Dictionary
var _scene_tree : SceneTree
var _exclusive_control_node : ColorRect
func _set_focus_none(node : Node) -> void:
for child in node.get_children():
if child == self: continue
if child is Control:
_initial_node_focus_modes[child] = child.focus_mode
child.focus_mode = Control.FOCUS_NONE
_set_focus_none(child)
func _set_focus_initial() -> void:
for node in _initial_node_focus_modes:
if is_instance_valid(node) and node is Control:
node.focus_mode = _initial_node_focus_modes[node]
_initial_node_focus_modes.clear()
func close() -> void:
if not visible: return
_scene_tree.paused = _initial_pause_state
Input.set_mouse_mode(_initial_mouse_mode)
_set_focus_initial()
if is_instance_valid(_initial_focus_control) and _initial_focus_control.is_inside_tree():
_initial_focus_control.grab_focus()
if _exclusive_control_node:
_exclusive_control_node.queue_free()
super.close()
func _overlaid_window_setup():
if _scene_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_control.release_focus()
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)
if exclusive:
_set_focus_none(get_tree().current_scene)
_exclusive_control_node = ColorRect.new()
_exclusive_control_node.name = self.name + "ExclusiveControl"
_exclusive_control_node.color = exclusive_background_color
_exclusive_control_node.set_anchors_preset(PRESET_FULL_RECT)
add_sibling.call_deferred(_exclusive_control_node)
await _exclusive_control_node.draw
get_parent().move_child(_exclusive_control_node, get_index())
func _on_visibility_changed() -> void:
if is_visible_in_tree():
_overlaid_window_setup()
func _enter_tree() -> void:
_scene_tree = get_tree()
if not visibility_changed.is_connected(_on_visibility_changed):
visibility_changed.connect(_on_visibility_changed)
_on_visibility_changed()

View File

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

View File

@@ -0,0 +1,12 @@
[gd_scene format=3 uid="uid://6gdbfi0172ji"]
[ext_resource type="Script" uid="uid://xfugmpspqbcc" path="res://addons/maaacks_game_template/base/nodes/windows/overlaid_window.gd" id="1_euyj1"]
[ext_resource type="PackedScene" uid="uid://b2s0kvrx8r2kq" path="res://addons/maaacks_game_template/base/nodes/windows/window_container.tscn" id="2_pmk27"]
[node name="OverlaidWindow" unique_id=343706911 instance=ExtResource("2_pmk27")]
process_mode = 0
script = ExtResource("1_euyj1")
pauses_game = false
makes_mouse_visible = true
exclusive = true
exclusive_background_color = Color(0, 0, 0, 0.5)

View File

@@ -0,0 +1,19 @@
@tool
class_name OverlaidWindowContainer
extends OverlaidWindow
var instance : Node
@onready var scene_container : Container = %SceneContainer
@export var packed_scene : PackedScene :
set(value):
packed_scene = value
if is_inside_tree():
for child in scene_container.get_children():
child.queue_free()
if packed_scene:
instance = packed_scene.instantiate()
scene_container.add_child(instance)
func _ready() -> void:
packed_scene = packed_scene

View File

@@ -0,0 +1,13 @@
[gd_scene format=3 uid="uid://crndfbb22ri4s"]
[ext_resource type="PackedScene" uid="uid://6gdbfi0172ji" path="res://addons/maaacks_game_template/base/nodes/windows/overlaid_window.tscn" id="1_07348"]
[ext_resource type="Script" uid="uid://c6pmyo50c1tqy" path="res://addons/maaacks_game_template/base/nodes/windows/overlaid_window_scene_container.gd" id="2_p673y"]
[node name="OverlaidWindowSceneContainer" unique_id=232665413 instance=ExtResource("1_07348")]
script = ExtResource("2_p673y")
packed_scene = null
[node name="SceneContainer" type="MarginContainer" parent="ContentContainer/BoxContainer/BodyMargin" parent_id_path=PackedInt32Array(590613964) index="1" unique_id=1464754606]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3

View File

@@ -0,0 +1,80 @@
@tool
class_name WindowContainer
extends PanelContainer
signal closed
signal opened
@export var ui_cancel_closes : bool = true
@export_group("Content")
@export var update_content : bool = false
@export_multiline var text : String :
set(value):
text = value
if update_content and is_inside_tree():
description_label.text = text
@export var close_button_text : String = "Close" :
set(value):
close_button_text = value
if update_content and is_inside_tree():
close_button.text = close_button_text
@export_subgroup("Title")
@export var title : String = "Menu" :
set(value):
title = value
if update_content and is_inside_tree():
title_label.text = title
@export_range(0, 1000, 1) var title_font_size : int = 16 :
set(value):
title_font_size = value
if update_content and is_inside_tree():
title_label.set("theme_override_font_sizes/font_size", title_font_size)
@export var title_visible : bool = true :
set(value):
title_visible = value
if update_content and is_inside_tree():
title_margin.visible = title_visible
@onready var content_container : Container = %ContentContainer
@onready var title_label : Label = %TitleLabel
@onready var title_margin : MarginContainer = %TitleMargin
@onready var description_label : RichTextLabel = %DescriptionLabel
@onready var close_button : Button = %CloseButton
@onready var menu_buttons : BoxContainer = %MenuButtons
func _ready() -> void:
update_content = update_content
text = text
close_button_text = close_button_text
title = title
title_font_size = title_font_size
title_visible = title_visible
func close() -> void:
if not visible: return
hide()
closed.emit()
func _handle_cancel_input() -> void:
close()
func _unhandled_input(event : InputEvent) -> void:
if visible and event.is_action_released("ui_cancel") and ui_cancel_closes:
_handle_cancel_input()
get_viewport().set_input_as_handled()
func _on_close_button_pressed() -> void:
close()
func show() -> void:
super.show()
opened.emit()
func _exit_tree():
if Engine.is_editor_hint(): return
close()

View File

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

View File

@@ -0,0 +1,90 @@
[gd_scene format=3 uid="uid://b2s0kvrx8r2kq"]
[ext_resource type="Script" uid="uid://b3onujul5qho1" path="res://addons/maaacks_game_template/base/nodes/windows/window_container.gd" id="1_te2s1"]
[ext_resource type="Script" uid="uid://1nf36h0gms3q" path="res://addons/maaacks_game_template/base/nodes/utilities/capture_focus.gd" id="2_xihbi"]
[node name="WindowContainer" type="PanelContainer" unique_id=2023947717]
process_mode = 3
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -80.0
offset_top = -50.0
offset_right = 80.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 4
size_flags_vertical = 4
script = ExtResource("1_te2s1")
[node name="ContentContainer" type="MarginContainer" parent="." unique_id=1755486830]
unique_name_in_owner = true
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="ContentContainer" unique_id=394030069]
layout_mode = 2
vertical = true
[node name="TitleMargin" type="MarginContainer" parent="ContentContainer/BoxContainer" unique_id=1262022916]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/margin_left = -14
theme_override_constants/margin_top = -14
theme_override_constants/margin_right = -14
theme_override_constants/margin_bottom = 8
[node name="BoxContainer" type="BoxContainer" parent="ContentContainer/BoxContainer/TitleMargin" unique_id=1788474031]
layout_mode = 2
theme_override_constants/separation = 0
vertical = true
[node name="TitleLabel" type="Label" parent="ContentContainer/BoxContainer/TitleMargin/BoxContainer" unique_id=1049966061]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 16
text = "Menu"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="ContentContainer/BoxContainer/TitleMargin/BoxContainer" unique_id=33373596]
layout_mode = 2
[node name="BodyMargin" type="MarginContainer" parent="ContentContainer/BoxContainer" unique_id=590613964]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="DescriptionLabel" type="RichTextLabel" parent="ContentContainer/BoxContainer/BodyMargin" unique_id=617407155]
unique_name_in_owner = true
layout_mode = 2
bbcode_enabled = true
fit_content = true
horizontal_alignment = 1
vertical_alignment = 1
[node name="MenuButtonsMargin" type="MarginContainer" parent="ContentContainer/BoxContainer" unique_id=1413292752]
layout_mode = 2
theme_override_constants/margin_top = 8
[node name="MenuButtons" type="BoxContainer" parent="ContentContainer/BoxContainer/MenuButtonsMargin" unique_id=1371114575]
unique_name_in_owner = true
custom_minimum_size = Vector2(128, 0)
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 16
alignment = 1
vertical = true
script = ExtResource("2_xihbi")
[node name="CloseButton" type="Button" parent="ContentContainer/BoxContainer/MenuButtonsMargin/MenuButtons" unique_id=314526102]
unique_name_in_owner = true
layout_mode = 2
text = "Close"
[connection signal="pressed" from="ContentContainer/BoxContainer/MenuButtonsMargin/MenuButtons/CloseButton" to="." method="_on_close_button_pressed"]

View File

@@ -0,0 +1,104 @@
keys,en,fr
___ MAIN MENU,,
Title,Title,Titre
Subtitle,Subtitle,Sous-titre
New Game,New Game,
Continue,Continue,
Play,Play,Jouer
Level Select,Level Select,
Options,Options,Options
Credits,Credits,Crédits
Exit,Exit,Quitter
"Are you sure you want to start a new game?
All progress in the current game will be lost.","Are you sure you want to start a new game?
All progress in the current game will be lost.",
___ LOADING SCREEN,,
Loading...,Loading...,Chargement...
Still Loading...,Still Loading...,
Loading Complete!,Loading Complete!,
Any Moment Now...,Any Moment Now...,
___ DIALOGS IN GAME,,
Win,Win,
Lose,Lose,
Tutorial,Tutorial,
Change Level Color: ,Change Level Color: ,
Close,Close,
Level complete!,Level complete!,
You lost...,You lost...,Vous avez perdu...
You won!,You won!,Vous avez gagné !
Thanks for playing!,Thanks for playing!,Merci d'avoir joué !
Exit Game,Exit Game,Quitter le jeu
Main Menu,Main Menu,Menu principal
Restart,Restart,Recommencer
Continue,Continue,Continuer
Menu,Menu,Menu
Please Confirm...,Please Confirm...,Veuillez confirmer...
Go back to main menu?,Go back to main menu?,Retourner au menu principal ?
Quit the game?,Quit the game?,Quitter le jeu ?
Cancel,Cancel,Annuler
OK,OK,OK
__ TUTORIALS,,
"Click the Win button to progress.
Click the Lose button to try again.","Click the Win button to progress.
Click the Lose button to try again.",
"Progress is saved.
Pressing Continue from the main menu will load the last level played.","Progress is saved.
Pressing Continue from the main menu will load the last level played.",
"The color picker at the bottom-right updates the level state. This change persists until the game is reset.
The label at the bottom-center displays the current input action detected, if any are setup for the project.","The color picker at the bottom-right updates the level state. This change persists until the game is reset.
The label at the bottom-center displays the current input action detected, if any are setup for the project.",
___ OPTIONS MENU,,
Controls,Controls,Contrôles
Mouse Sensitivity :,Mouse Sensitivity :,Sensibilité souris :
Actions & Inputs,Actions & Inputs,Actions et contrôles
Add,Add,Ajouter
Remove,Remove,Enlever
Assign Key for {action},Assign Key for {action},Choisir le contrôle pour {action}
Listening for input...,Listening for input...,Appuyez sur un bouton...
Press again to confirm...,Press again to confirm...,Appuyez encore pour confirmer...
Focus here to assign inputs.,Focus here to assign inputs.,Mettez le focus ici pour choisir le contrôle.
Already Assigned,Already Assigned,Déjà utilisé
{key} already assigned to {action}.,{key} already assigned to {action}.,{key} est déjà utilisé pour {action}.
Remove Key for {action},Remove Key for {action},Supprimer le contrôle pour {action}
Are you sure you want to remove {key} from {action}?,Are you sure you want to remove {key} from {action}?,Êtes-vous sûr de vouloir supprimer {key} pour {action} ?
Reset,Reset,Réinitialiser
Audio,Audio,Audio
Master :,Master :,Principal :
Music :,Music :,Musique :
SFX :,SFX :,Effets :
Mute :,Mute :,Silencieux :
Video,Video,Vidéo
Fullscreen :,Fullscreen :,Plein écran :
Resolution :,Resolution :,Résolution :
Anti-Aliasing :,Anti-Aliasing :,Anticrénelage :
Disabled (Fastest),Disabled (Fastest),Désactivé (Plus rapide)
8x (Slowest),8x (Slowest),8x (Plus lent)
Camera Shake :,Camera Shake :,Secousse Caméra :
Normal,Normal,Normale
Reduced,Reduced,Réduite
Minimal,Minimal,Minimum
None,None,Aucune
Game,Game,Jeu
Reset Game :,Reset Game :,Réinitialiser le jeu :
Do you want to reset your game data?,Do you want to reset your game data?,Voulez-vous réinitialiser votre partie ?
Back,Back,Retour
1 keys en fr
2 ___ MAIN MENU
3 Title Title Titre
4 Subtitle Subtitle Sous-titre
5 New Game New Game
6 Continue Continue
7 Play Play Jouer
8 Level Select Level Select
9 Options Options Options
10 Credits Credits Crédits
11 Exit Exit Quitter
12 Are you sure you want to start a new game? All progress in the current game will be lost. Are you sure you want to start a new game? All progress in the current game will be lost.
13 ___ LOADING SCREEN
14 Loading... Loading... Chargement...
15 Still Loading... Still Loading...
16 Loading Complete! Loading Complete!
17 Any Moment Now... Any Moment Now...
18 ___ DIALOGS IN GAME
19 Win Win
20 Lose Lose
21 Tutorial Tutorial
22 Change Level Color: Change Level Color:
23 Close Close
24 Level complete! Level complete!
25 You lost... You lost... Vous avez perdu...
26 You won! You won! Vous avez gagné !
27 Thanks for playing! Thanks for playing! Merci d'avoir joué !
28 Exit Game Exit Game Quitter le jeu
29 Main Menu Main Menu Menu principal
30 Restart Restart Recommencer
31 Continue Continue Continuer
32 Menu Menu Menu
33 Please Confirm... Please Confirm... Veuillez confirmer...
34 Go back to main menu? Go back to main menu? Retourner au menu principal ?
35 Quit the game? Quit the game? Quitter le jeu ?
36 Cancel Cancel Annuler
37 OK OK OK
38 __ TUTORIALS
39 Click the Win button to progress. Click the Lose button to try again. Click the Win button to progress. Click the Lose button to try again.
40 Progress is saved. Pressing Continue from the main menu will load the last level played. Progress is saved. Pressing Continue from the main menu will load the last level played.
41 The color picker at the bottom-right updates the level state. This change persists until the game is reset. The label at the bottom-center displays the current input action detected, if any are setup for the project. The color picker at the bottom-right updates the level state. This change persists until the game is reset. The label at the bottom-center displays the current input action detected, if any are setup for the project.
42 ___ OPTIONS MENU
43 Controls Controls Contrôles
44 Mouse Sensitivity : Mouse Sensitivity : Sensibilité souris :
45 Actions & Inputs Actions & Inputs Actions et contrôles
46 Add Add Ajouter
47 Remove Remove Enlever
48 Assign Key for {action} Assign Key for {action} Choisir le contrôle pour {action}
49 Listening for input... Listening for input... Appuyez sur un bouton...
50 Press again to confirm... Press again to confirm... Appuyez encore pour confirmer...
51 Focus here to assign inputs. Focus here to assign inputs. Mettez le focus ici pour choisir le contrôle.
52 Already Assigned Already Assigned Déjà utilisé
53 {key} already assigned to {action}. {key} already assigned to {action}. {key} est déjà utilisé pour {action}.
54 Remove Key for {action} Remove Key for {action} Supprimer le contrôle pour {action}
55 Are you sure you want to remove {key} from {action}? Are you sure you want to remove {key} from {action}? Êtes-vous sûr de vouloir supprimer {key} pour {action} ?
56 Reset Reset Réinitialiser
57 Audio Audio Audio
58 Master : Master : Principal :
59 Music : Music : Musique :
60 SFX : SFX : Effets :
61 Mute : Mute : Silencieux :
62 Video Video Vidéo
63 Fullscreen : Fullscreen : Plein écran :
64 Resolution : Resolution : Résolution :
65 Anti-Aliasing : Anti-Aliasing : Anticrénelage :
66 Disabled (Fastest) Disabled (Fastest) Désactivé (Plus rapide)
67 8x (Slowest) 8x (Slowest) 8x (Plus lent)
68 Camera Shake : Camera Shake : Secousse Caméra :
69 Normal Normal Normale
70 Reduced Reduced Réduite
71 Minimal Minimal Minimum
72 None None Aucune
73 Game Game Jeu
74 Reset Game : Reset Game : Réinitialiser le jeu :
75 Do you want to reset your game data? Do you want to reset your game data? Voulez-vous réinitialiser votre partie ?
76 Back Back Retour

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