feat(audio): Sprint 13 — AudioManager, zone crossfade, dip profiles, murmur wiring (#528, #529, #530, #533)
AudioManager: volume slider persistence (ConfigFile), settings UI with 5 teal-bordered sliders, default_bus_layout.tres for editor. Zone crossfade: defensive zone_id read from snapshot tiles, 1.5-2s ambient tween, auto-activates when server ships OQ-09. Dip profiles: dialogue dip in show/hide_dialogue, ListeningFocus 30-tick gate via stationary_ticks in game_state.gd. NPC murmur: client plumbing for event-driven World SFX playback, no-ops until audio asset arrives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,10 @@ func show_dialogue(npc_name: String, speech: String, options: Array = []) -> voi
|
||||
_is_showing = true
|
||||
GameState.dialogue_active = true # D-064: block movement while dialogue visible/fading
|
||||
|
||||
# D-069: Dialogue dip — reduce ambient noise to foreground conversation.
|
||||
# Confrontation options override this with apply_dip("confrontation") in _start_confrontation_beat().
|
||||
AudioManager.apply_dip("dialogue")
|
||||
|
||||
# D-061: auto-pause in single-player when dialogue opens
|
||||
SimBridge.send_input({"action": InputMapper.Action.PAUSE, "timestamp_msec": Time.get_ticks_msec()})
|
||||
|
||||
@@ -156,6 +160,10 @@ func hide_dialogue() -> void:
|
||||
_is_showing = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# D-069: Clear dialogue/confrontation dip — restore bus volumes to player slider settings.
|
||||
# Safe to call even if confrontation beat already cleared the dip (no-op when empty).
|
||||
AudioManager.clear_dip()
|
||||
|
||||
# D-061: unpause when dialogue closes
|
||||
SimBridge.send_input({"action": InputMapper.Action.UNPAUSE, "timestamp_msec": Time.get_ticks_msec()})
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
extends Control
|
||||
|
||||
## #528: Audio settings dialog — 5-bus volume sliders.
|
||||
## Opens on OPEN_MENU (ESC) from main.gd. Closes on OPEN_MENU again or CLOSE button.
|
||||
## Volumes persist via AudioManager._save_prefs() on each slider change.
|
||||
|
||||
const BG_COLOR := Color(0.05, 0.05, 0.08, 0.90)
|
||||
const BORDER_COLOR := Color("#4a9ebb")
|
||||
const TEXT_COLOR := Color(0.878, 0.969, 0.98, 1)
|
||||
const TITLE_COLOR := Color("#4a9ebb")
|
||||
const FONT_SIZE := 14
|
||||
|
||||
const BOX_WIDTH := 460
|
||||
const BOX_HEIGHT := 340
|
||||
const PADDING := 20
|
||||
const ROW_HEIGHT := 36
|
||||
|
||||
# Bus display labels → bus name strings (must match AudioManager BUS_* constants)
|
||||
const BUS_ROWS: Array = [
|
||||
["Music", "Music"],
|
||||
["Ambient", "Ambient"],
|
||||
["World SFX", "WorldSFX"],
|
||||
["Player Actions", "PlayerActions"],
|
||||
["UI Sounds", "UISounds"],
|
||||
]
|
||||
|
||||
var _active: bool = false
|
||||
var _container: VBoxContainer = null
|
||||
|
||||
signal closed
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
|
||||
func open() -> void:
|
||||
if _active:
|
||||
return
|
||||
_active = true
|
||||
visible = true
|
||||
_build_ui()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func close() -> void:
|
||||
if not _active:
|
||||
return
|
||||
_active = false
|
||||
visible = false
|
||||
_destroy_ui()
|
||||
queue_redraw()
|
||||
closed.emit()
|
||||
|
||||
|
||||
func is_open() -> bool:
|
||||
return _active
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
var vp_size := get_viewport_rect().size
|
||||
var box_pos := Vector2(
|
||||
(vp_size.x - BOX_WIDTH) / 2.0,
|
||||
(vp_size.y - BOX_HEIGHT) / 2.0
|
||||
)
|
||||
|
||||
_container = VBoxContainer.new()
|
||||
_container.position = box_pos + Vector2(PADDING, PADDING + 28)
|
||||
_container.custom_minimum_size = Vector2(BOX_WIDTH - PADDING * 2, 0)
|
||||
_container.add_theme_constant_override("separation", 4)
|
||||
add_child(_container)
|
||||
|
||||
for row_data in BUS_ROWS:
|
||||
var label_text: String = row_data[0]
|
||||
var bus_name: String = row_data[1]
|
||||
|
||||
var hbox := HBoxContainer.new()
|
||||
hbox.custom_minimum_size = Vector2(0, ROW_HEIGHT)
|
||||
_container.add_child(hbox)
|
||||
|
||||
var label := Label.new()
|
||||
label.text = label_text
|
||||
label.custom_minimum_size = Vector2(150, 0)
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
label.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
hbox.add_child(label)
|
||||
|
||||
var slider := HSlider.new()
|
||||
slider.min_value = -40.0
|
||||
slider.max_value = 0.0
|
||||
slider.step = 0.5
|
||||
slider.value = AudioManager.get_volume(bus_name)
|
||||
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
hbox.add_child(slider)
|
||||
|
||||
var db_label := Label.new()
|
||||
db_label.text = _format_db(slider.value)
|
||||
db_label.custom_minimum_size = Vector2(70, 0)
|
||||
db_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
db_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
db_label.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
db_label.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
hbox.add_child(db_label)
|
||||
|
||||
slider.value_changed.connect(func(value: float) -> void:
|
||||
AudioManager.set_volume(bus_name, value)
|
||||
db_label.text = _format_db(value)
|
||||
)
|
||||
|
||||
# Spacer
|
||||
var spacer := Control.new()
|
||||
spacer.custom_minimum_size = Vector2(0, 8)
|
||||
_container.add_child(spacer)
|
||||
|
||||
# Close button
|
||||
var close_btn := Button.new()
|
||||
close_btn.text = "CLOSE"
|
||||
close_btn.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
close_btn.pressed.connect(close)
|
||||
_container.add_child(close_btn)
|
||||
|
||||
|
||||
func _destroy_ui() -> void:
|
||||
if _container:
|
||||
_container.queue_free()
|
||||
_container = null
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not _active:
|
||||
return
|
||||
var viewport_size := get_viewport_rect().size
|
||||
|
||||
# Dim overlay
|
||||
draw_rect(Rect2(Vector2.ZERO, viewport_size), BG_COLOR)
|
||||
|
||||
# Dialog box
|
||||
var box_pos := Vector2(
|
||||
(viewport_size.x - BOX_WIDTH) / 2.0,
|
||||
(viewport_size.y - BOX_HEIGHT) / 2.0
|
||||
)
|
||||
var box_rect := Rect2(box_pos, Vector2(BOX_WIDTH, BOX_HEIGHT))
|
||||
draw_rect(box_rect, Color(0.08, 0.08, 0.12, 0.95))
|
||||
draw_rect(box_rect, BORDER_COLOR, false, 1.0)
|
||||
|
||||
# Title
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_string(font,
|
||||
box_pos + Vector2(PADDING, PADDING + 18),
|
||||
"AUDIO SETTINGS",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 2, TITLE_COLOR)
|
||||
|
||||
|
||||
static func _format_db(db: float) -> String:
|
||||
if db <= -40.0:
|
||||
return "mute"
|
||||
return "%d dB" % int(db)
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/settings_dialog.gd" id="1_settings"]
|
||||
|
||||
; #528: Audio settings dialog — 5-bus volume sliders, OPEN_MENU (ESC) to toggle
|
||||
[node name="SettingsDialog" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_settings")
|
||||
Reference in New Issue
Block a user