feat: base
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 6s
Create tag and build when new code gets to main / Export (push) Successful in 2m5s

This commit is contained in:
2025-06-27 23:11:54 +02:00
parent 9a79715e47
commit 00037d0270
394 changed files with 11399 additions and 11 deletions

View File

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

View File

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

View File

@ -0,0 +1,65 @@
class_name CaptureFocus
extends Control
## Node that captures UI focus for games with a hidden mouse or joypad enabled.
##
## This script assists with capturing UI focus when
## opening, closing, or switching between menus.
## When attached to a node, it will check if it was changed to visible
## and if it should grab focus. If both are true, it will capture focus
## on the first eligible node in its scene tree.
## Hierarchical depth to search in the scene tree.
@export var search_depth : int = 1
@export var enabled : bool = false
@export var null_focus_enabled : bool = true
@export var joypad_enabled : bool = true
@export var mouse_hidden_enabled : bool = true
## Locks focus
@export var lock : bool = false :
set(value):
var value_changed : bool = lock != value
lock = value
if value_changed and not lock:
update_focus()
func _focus_first_search(control_node : Control, levels : int = 1) -> bool:
if control_node == null or !control_node.is_visible_in_tree():
return false
if control_node.focus_mode == FOCUS_ALL:
control_node.grab_focus()
if control_node is ItemList:
control_node.select(0)
return true
if levels < 1:
return false
var children = control_node.get_children()
for child in children:
if _focus_first_search(child, levels - 1):
return true
return false
func focus_first() -> void:
_focus_first_search(self, search_depth)
func update_focus() -> void:
if lock : return
if _is_visible_and_should_capture():
focus_first()
func _should_capture_focus() -> bool:
return enabled or \
(get_viewport().gui_get_focus_owner() == null and null_focus_enabled) or \
(Input.get_connected_joypads().size() > 0 and joypad_enabled) or \
(Input.mouse_mode not in [Input.MOUSE_MODE_VISIBLE, Input.MOUSE_MODE_CONFINED] and mouse_hidden_enabled)
func _is_visible_and_should_capture() -> bool:
return is_visible_in_tree() and _should_capture_focus()
func _on_visibility_changed() -> void:
call_deferred("update_focus")
func _ready() -> void:
if is_inside_tree():
update_focus()
connect("visibility_changed", _on_visibility_changed)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,51 @@
@tool
extends Node
class_name FileLister
## Helper class for listing all the scenes in a directory.
## List of paths to scene files.
@export var _refresh_files_action : bool = false :
set(value):
if value and Engine.is_editor_hint():
_refresh_files()
# For Godot 4.4
# @export_tool_button("Refresh Files") var _refresh_files_action = _refresh_files
## Filled in the editor by selecting a directory.
@export var files : Array[String]
## Fills files with those discovered in directories, and matching constraints.
@export_dir var directories : Array[String] :
set(value):
directories = value
_refresh_files()
@export_group("Constraints")
@export var search : String
@export var filter : String
@export_subgroup("Advanced Search")
@export var begins_with : String
@export var ends_with : String
@export var not_begins_with : String
@export var not_ends_with : String
func _refresh_files():
if not is_inside_tree(): return
files.clear()
for directory in directories:
var dir_access = DirAccess.open(directory)
if dir_access:
for file in dir_access.get_files():
if (not search.is_empty()) and (not file.contains(search)):
continue
if (not filter.is_empty()) and (file.contains(filter)):
continue
if (not begins_with.is_empty()) and (not file.begins_with(begins_with)):
continue
if (not ends_with.is_empty()) and (not file.ends_with(ends_with)):
continue
if (not not_begins_with.is_empty()) and (file.begins_with(not_begins_with)):
continue
if (not not_ends_with.is_empty()) and (file.ends_with(not_ends_with)):
continue
files.append(directory + "/" + file)

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,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 @@
uid://ctrh4qyxqncss

View File

@ -0,0 +1,18 @@
class_name PauseMenuController
extends Node
## Node for opening a pause menu when detecting a 'ui_cancel' event.
@export var pause_menu_packed : PackedScene
@export var focused_viewport : Viewport
func _unhandled_input(event : InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
if not focused_viewport:
focused_viewport = get_viewport()
var _initial_focus_control = focused_viewport.gui_get_focus_owner()
var current_menu = pause_menu_packed.instantiate()
get_tree().current_scene.call_deferred("add_child", current_menu)
await current_menu.tree_exited
if is_inside_tree() and _initial_focus_control:
_initial_focus_control.grab_focus()

View File

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

View File

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

View File

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