186 lines
5.4 KiB
GDScript
186 lines
5.4 KiB
GDScript
extends Node
|
|
## Central Audio Manager singleton (autoload name: "AudioManager").
|
|
##
|
|
## Pre-warms an AudioStreamPlayer pool for UI and game SFX.
|
|
## Provides convenience methods for standard sound effects and
|
|
## automatically hooks UI buttons for tactile click audio feedback.
|
|
|
|
const STREAM_CLICK: AudioStream = preload("res://assets/audio/click.wav")
|
|
const STREAM_COIN: AudioStream = preload("res://assets/audio/coin.wav")
|
|
const STREAM_CHIME: AudioStream = preload("res://assets/audio/chime.wav")
|
|
const STREAM_FANFARE: AudioStream = preload("res://assets/audio/fanfare.wav")
|
|
const STREAM_BELL: AudioStream = preload("res://assets/audio/bell.wav")
|
|
const STREAM_DOOR: AudioStream = preload("res://assets/audio/door_open.wav")
|
|
|
|
const POOL_SIZE: int = 16
|
|
const SETTINGS_FILE := "user://audio_settings.json"
|
|
|
|
signal sound_toggled(enabled: bool)
|
|
|
|
var _players: Array[AudioStreamPlayer] = []
|
|
var _player_index: int = 0
|
|
|
|
## Whether sound effects and audio output are enabled.
|
|
var sound_enabled: bool = true
|
|
|
|
## Number of active AudioStreamPlayers in the pool.
|
|
var pool_size: int:
|
|
get:
|
|
return _players.size()
|
|
|
|
|
|
func _ready() -> void:
|
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
|
_init_pool()
|
|
_load_settings()
|
|
var tree := get_tree()
|
|
if tree != null:
|
|
tree.node_added.connect(_on_node_added)
|
|
_hook_existing_buttons(tree.root)
|
|
|
|
|
|
func _load_settings() -> void:
|
|
if FileAccess.file_exists(SETTINGS_FILE):
|
|
var file := FileAccess.open(SETTINGS_FILE, FileAccess.READ)
|
|
if file != null:
|
|
var text := file.get_as_text()
|
|
var parsed = JSON.parse_string(text)
|
|
if parsed is Dictionary and parsed.has("sound_enabled"):
|
|
set_sound_enabled(bool(parsed["sound_enabled"]), false)
|
|
return
|
|
set_sound_enabled(true, false)
|
|
|
|
|
|
func _save_settings() -> void:
|
|
var file := FileAccess.open(SETTINGS_FILE, FileAccess.WRITE)
|
|
if file != null:
|
|
file.store_string(JSON.stringify({"sound_enabled": sound_enabled}))
|
|
|
|
|
|
## Check whether sound is currently enabled.
|
|
func is_sound_enabled() -> bool:
|
|
return sound_enabled
|
|
|
|
|
|
## Enable or disable sound output.
|
|
func set_sound_enabled(enabled: bool, save: bool = true) -> void:
|
|
sound_enabled = enabled
|
|
if not sound_enabled:
|
|
for player in _players:
|
|
if player != null and is_instance_valid(player) and player.playing:
|
|
player.stop()
|
|
var master_idx := AudioServer.get_bus_index("Master")
|
|
if master_idx >= 0:
|
|
AudioServer.set_bus_mute(master_idx, not sound_enabled)
|
|
if save:
|
|
_save_settings()
|
|
sound_toggled.emit(sound_enabled)
|
|
|
|
|
|
## Toggle sound between ON and OFF. Returns new state.
|
|
func toggle_sound() -> bool:
|
|
set_sound_enabled(not sound_enabled)
|
|
if sound_enabled:
|
|
play_click()
|
|
return sound_enabled
|
|
|
|
|
|
func _init_pool() -> void:
|
|
if not _players.is_empty():
|
|
return
|
|
for i in range(POOL_SIZE):
|
|
var player := AudioStreamPlayer.new()
|
|
player.name = "AudioPlayer_%d" % i
|
|
player.bus = "Master"
|
|
add_child(player)
|
|
_players.append(player)
|
|
|
|
|
|
## Play an audio stream on the specified bus with volume and pitch settings.
|
|
func play_sound(stream: AudioStream, bus: String = "Master", volume_db: float = 0.0, pitch_scale: float = 1.0) -> AudioStreamPlayer:
|
|
if not sound_enabled:
|
|
return null
|
|
if stream == null:
|
|
return null
|
|
if _players.is_empty():
|
|
_init_pool()
|
|
|
|
var player: AudioStreamPlayer = _players[_player_index]
|
|
_player_index = (_player_index + 1) % POOL_SIZE
|
|
|
|
if player.playing:
|
|
player.stop()
|
|
|
|
player.stream = stream
|
|
player.bus = bus
|
|
player.volume_db = volume_db
|
|
player.pitch_scale = pitch_scale
|
|
player.play()
|
|
return player
|
|
|
|
|
|
## Play UI button click with subtle random pitch variance.
|
|
func play_click(volume_db: float = 0.0, pitch_variance: float = 0.05) -> AudioStreamPlayer:
|
|
var pitch := 1.0
|
|
if pitch_variance > 0.0:
|
|
pitch += randf_range(-pitch_variance, pitch_variance)
|
|
return play_sound(STREAM_CLICK, "Master", volume_db, pitch)
|
|
|
|
|
|
## Play coin transaction sound (credits / marketplace / payouts).
|
|
func play_coin(volume_db: float = 0.0) -> AudioStreamPlayer:
|
|
return play_sound(STREAM_COIN, "Master", volume_db, 1.0)
|
|
|
|
|
|
## Play chime sound (successful world / chunk save).
|
|
func play_chime(volume_db: float = 0.0) -> AudioStreamPlayer:
|
|
return play_sound(STREAM_CHIME, "Master", volume_db, 1.0)
|
|
|
|
|
|
## Play victory / achievement fanfare.
|
|
func play_fanfare(volume_db: float = 0.0) -> AudioStreamPlayer:
|
|
return play_sound(STREAM_FANFARE, "Master", volume_db, 1.0)
|
|
|
|
|
|
## Play alert bell sound.
|
|
func play_bell(volume_db: float = 0.0) -> AudioStreamPlayer:
|
|
return play_sound(STREAM_BELL, "Master", volume_db, 1.0)
|
|
|
|
|
|
## Play door open transition sound.
|
|
func play_door(volume_db: float = 0.0) -> AudioStreamPlayer:
|
|
return play_sound(STREAM_DOOR, "Master", volume_db, 1.0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Universal Tactile UI Click Wiring
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _on_node_added(node: Node) -> void:
|
|
if node is BaseButton:
|
|
_hook_button(node as BaseButton)
|
|
|
|
|
|
func _hook_existing_buttons(node: Node) -> void:
|
|
if node is BaseButton:
|
|
_hook_button(node as BaseButton)
|
|
for child in node.get_children():
|
|
_hook_existing_buttons(child)
|
|
|
|
|
|
func _hook_button(btn: BaseButton) -> void:
|
|
var callable := Callable(self, "_on_button_pressed").bind(btn)
|
|
if not btn.pressed.is_connected(callable):
|
|
btn.pressed.connect(callable)
|
|
|
|
|
|
func _on_button_pressed(btn: BaseButton) -> void:
|
|
if not sound_enabled:
|
|
return
|
|
if not is_instance_valid(btn):
|
|
return
|
|
if btn.is_in_group("no_click_sound"):
|
|
return
|
|
if btn.is_visible_in_tree() and not btn.disabled:
|
|
play_click()
|