Skip to content

Add Menu SFX bus, AudioWebBridge, and UI navigation sounds - #500

Merged
ikostan merged 101 commits into
mainfrom
SFX
Mar 31, 2026
Merged

Add Menu SFX bus, AudioWebBridge, and UI navigation sounds#500
ikostan merged 101 commits into
mainfrom
SFX

Conversation

@ikostan

@ikostan ikostan commented Mar 22, 2026

Copy link
Copy Markdown
Owner

name: Default Pull Request Template
about: Suggesting changes to SkyLockAssault
title: ''
labels: ''
assignees: ''

image image image

Description

What does this PR do? (e.g., "Fixes player jump physics in level 2" or "Adds
new enemy AI script")

Related Issue

Closes #ISSUE_NUMBER (if applicable)

Changes

  • List key changes here (e.g., "Updated Jump.gd to use Godot 4.4's new Tween
    system")
  • Any breaking changes? (e.g., "Deprecated old signal; migrate to new one")

Testing

  • Ran the game in Godot v4.5 editor—describe what you tested (e.g., "Jump
    works on Win10 with 60 FPS")
  • Any new unit tests added? (Link to test scene if yes)
  • Screenshots/GIFs if UI-related: (Attach below)

Checklist

  • Code follows Godot style guide (e.g., snake_case for variables)
  • No console errors in editor/output
  • Ready for review!

Additional Notes

Anything else? (e.g., "Tested on Win10 64-bit; needs Linux validation")

🤖 Bot & AI Contributions

This pull request utilized several AI-driven tools to ensure code quality, automate documentation, and provide actionable feedback during the development of the audio system refactor.

Summary of Activity

  • Static Analysis & Security: Automated code scanning was performed to identify potential vulnerabilities and performance bottlenecks. This ensured that the GDScript implementation for the new audio bus adhered to best practices before merging.
  • Code Review & Feedback: AI-driven systems provided real-time analysis. Specifically, potential issues with deferred calls and unit test assertions were identified and addressed to ensure optimal performance in the audio_settings.gd and test_audio_settings.gd files.
  • Automated Summarization: AI tools were used to generate a high-level overview of the 101 commits included in this PR, categorizing the changes into features (Menu SFX bus), refactoring (AudioWebBridge), and testing infrastructure.
  • Workflow Automation: Bots managed the transition of related tasks within the project boards, ensuring progress was tracked accurately from "In Progress" to "Done."

AI Contributors

The following automated entities contributed to the successful completion of this PR:

  • @deepsource-io – Performed static analysis to detect bug risks and maintain code health across the repository.
  • @coderabbitai – Provided deep code analysis, identified technical debt (such as nested call_deferred chains), and suggested specific GDScript fixes.
  • @sourcery-ai – Assisted in generating the reviewer's guide and pull request summaries to streamline the manual review process.
  • @github-project-automation – Managed the project management workflow and synchronized the status of linked issues.

Summary by Sourcery

Add a dedicated Menu SFX audio bus and UI controls, and hook up global UI navigation sounds to that bus.

New Features:

  • Introduce a separate Menu SFX volume and mute channel alongside existing audio buses.
  • Play a global UI navigation sound on focus and directional UI input, routed through the Menu SFX bus.

Enhancements:

  • Extend in‑game and web audio settings UIs to control Menu SFX volume/mute, including DOM bridge callbacks and syncing logic.

Summary by Sourcery

Add a dedicated menu SFX bus and global UI navigation sound, while decoupling web audio bridging into a new autoload and updating audio settings UI and tests accordingly.

New Features:

  • Introduce a separate SFX_Menu audio bus with corresponding volume and mute controls in the audio settings UI and HTML shell.
  • Add a global UI navigation sound that plays on menu navigation inputs via a persistent audio player on the new menu SFX bus.
  • Provide an AudioWebBridge autoload to handle all web/JavaScript audio integration and DOM syncing independently of the audio settings scene.

Enhancements:

  • Refactor audio_settings.gd to centralize bus UI wiring, enforce hierarchical mute/lock behavior, and sync with AudioManager via new volume/mute signals.
  • Improve back navigation handling from the audio settings menu, including DOM visibility restoration and focus management across menus.
  • Extend AudioManager and AudioConstants to support the new menu bus and emit signals for volume and mute changes so UI and web layers stay in sync.

Tests:

  • Update existing GdUnit and Playwright tests to cover the new menu SFX bus, UI reset behavior, back-navigation logging, and revised logging messages.
  • Add a new GUT test suite for the AudioWebBridge to validate initialization paths, DOM synchronization, and JS-to-Godot signal routing.
  • Adjust audio save/load tests to account for the additional menu volume and mute fields in the config.

Summary by CodeRabbit

  • New Features

    • Menu SFX controls: added a menu effects volume slider and mute toggle in Audio Settings.
    • Browser overlay: audio controls can show/hide in-browser and sync bidirectionally with the app.
    • Menu navigation SFX: dedicated UI navigation sound and player for menu interactions.
  • Audio

    • Dedicated menu SFX channel for UI/menu effects.
    • Settings now emit volume/mute events so external overlays stay in sync.
  • UI

    • Improved focus/navigation flow and centralized interactivity locking.
  • Tests

    • Added and updated tests for web bridge, DOM sync, sliders/mutes, and back navigation.

@ikostan ikostan self-assigned this Mar 22, 2026
@ikostan ikostan added enhancement New feature or request menu GUI audio Sound/Audio related issue labels Mar 22, 2026
@ikostan ikostan moved this to In Progress in Sky Lock Assault Project Mar 22, 2026
@sourcery-ai

sourcery-ai Bot commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a dedicated Menu SFX bus and global UI navigation sound, and refactors web audio integration into an AudioWebBridge autoload while updating audio settings UI, Globals, and tests to support the new bus and DOM-sync signaling.

Sequence diagram for bidirectional audio settings sync between web DOM, AudioWebBridge, AudioManager, and UI

sequenceDiagram
    actor WebUser
    participant DOM as AudioDOM_elements
    participant JSWin as window_JS
    participant Bridge as AudioWebBridge
    participant AM as AudioManager
    participant UI as AudioSettings

    rect rgb(240,240,255)
        WebUser->>DOM: Drag menu-slider (HTML range)
        DOM-->>JSWin: onchange calls changeMenuVolume([value])
        JSWin->>Bridge: changeMenuVolume([value])
        Bridge->>Bridge: _on_change_menu_volume_js(args)
        Bridge->>Bridge: _on_change_volume_js(args BUS_SFX_MENU)
        Bridge->>AM: set_volume(BUS_SFX_MENU value)
        Bridge->>AM: apply_volume_to_bus(BUS_SFX_MENU value menu_muted)
        Bridge->>AM: save_volumes()
    end

    rect rgb(240,255,240)
        AM-->>Bridge: volume_changed(BUS_SFX_MENU value)
        Bridge->>DOM: set menu-slider.value = value
    end

    rect rgb(255,240,240)
        AM-->>UI: volume_changed(BUS_SFX_MENU value)
        UI->>UI: _on_global_volume_changed(BUS_SFX_MENU value)
        UI->>UI: menu_slider.set_value_no_signal(value)
        UI->>UI: _update_ui_interactivity()
    end

    rect rgb(240,240,255)
        WebUser->>DOM: Click mute-menu checkbox
        DOM-->>JSWin: onchange calls toggleMuteMenu([checked])
        JSWin->>Bridge: toggleMuteMenu([checked])
        Bridge->>Bridge: _on_toggle_mute_menu_js(args)
        Bridge->>Bridge: _on_toggle_mute_js(args BUS_SFX_MENU)
        Bridge->>AM: set_muted(BUS_SFX_MENU is_muted)
        Bridge->>AM: apply_volume_to_bus(BUS_SFX_MENU menu_volume is_muted)
        Bridge->>AM: save_volumes()
    end

    rect rgb(240,255,240)
        AM-->>Bridge: mute_toggled(BUS_SFX_MENU is_muted)
        Bridge->>DOM: set mute-menu.checked = not is_muted
    end

    rect rgb(255,240,240)
        AM-->>UI: mute_toggled(BUS_SFX_MENU is_muted)
        UI->>UI: _on_global_mute_toggled(BUS_SFX_MENU is_muted)
        UI->>UI: mute_menu.set_pressed_no_signal(not is_muted)
        UI->>UI: _update_ui_interactivity()
    end
Loading

Class diagram for AudioManager, AudioWebBridge, Globals, AudioSettings, and AudioConstants

classDiagram
    class AudioManager {
        <<Node>>
        +signal volume_changed(bus_name String volume float)
        +signal mute_toggled(bus_name String is_muted bool)
        +master_volume float
        +master_muted bool
        +music_volume float
        +music_muted bool
        +sfx_volume float
        +sfx_muted bool
        +weapon_volume float
        +weapon_muted bool
        +rotors_volume float
        +rotors_muted bool
        +menu_volume float
        +menu_muted bool
        +current_config_path String
        +set_bus_state(bus_name String volume float muted bool) void
        +get_volume(bus_name String) float
        +set_volume(bus_name String vol float) void
        +get_muted(bus_name String) bool
        +set_muted(bus_name String muted bool) void
        +get_bus_state(bus_name String) Dictionary
        +apply_volume_to_bus(bus_name String vol float muted bool) void
        +load_volumes() void
        +save_volumes() void
        +reset_volumes() void
    }

    class AudioConstants {
        <<Node or Script>>
        +BUS_MASTER String
        +BUS_MUSIC String
        +BUS_SFX String
        +BUS_SFX_ROTORS String
        +BUS_SFX_WEAPON String
        +BUS_SFX_MENU String
        +BUS_CONFIG Dictionary
    }

    class AudioWebBridge {
        <<Node(Auotload)>>
        +signal web_back_requested
        +signal web_reset_requested
        +os_wrapper OSWrapper
        +js_bridge_wrapper JavaScriptBridgeWrapper
        +js_window Variant
        +toggle_dom_visibility(show bool) void
        +sync_all_to_dom() void
        +restore_options_menu_dom() void
        +_ready() void
        +_on_godot_volume_changed(bus_name String value float) void
        +_on_godot_mute_toggled(bus_name String is_muted bool) void
        +_on_change_volume_js(args Array bus_name String) void
        +_on_toggle_mute_js(args Array bus_name String) void
        +_on_change_master_volume_js(args Array) void
        +_on_change_music_volume_js(args Array) void
        +_on_change_sfx_volume_js(args Array) void
        +_on_change_weapon_volume_js(args Array) void
        +_on_change_rotors_volume_js(args Array) void
        +_on_change_menu_volume_js(args Array) void
        +_on_toggle_mute_master_js(args Array) void
        +_on_toggle_mute_music_js(args Array) void
        +_on_toggle_mute_sfx_js(args Array) void
        +_on_toggle_mute_weapon_js(args Array) void
        +_on_toggle_mute_rotors_js(args Array) void
        +_on_toggle_mute_menu_js(args Array) void
        +_on_audio_back_button_pressed_js(args Array) void
        +_on_audio_reset_js(args Array) void
        +_register_all_callbacks() void
        +_register_js_callback(callback_method String window_property String) Variant
        +_validate_volume_args(args Array) float
        +_validate_mute_args(args Array) Variant
        +_get_slider_id_for_bus(bus_name String) String
        +_get_mute_id_for_bus(bus_name String) String
        +_sync_all_dom_values() void
    }

    class AudioSettings {
        <<Control>>
        +master_warning_shown bool
        +sfx_warning_shown bool
        +_intentional_exit bool
        +master_slider HSlider
        +mute_master CheckButton
        +music_slider HSlider
        +mute_music CheckButton
        +sfx_slider HSlider
        +mute_sfx CheckButton
        +weapon_slider HSlider
        +mute_weapon CheckButton
        +rotor_slider HSlider
        +mute_rotor CheckButton
        +menu_slider HSlider
        +mute_menu CheckButton
        +audio_back_button Button
        +audio_reset_button Button
        +_ready() void
        +_on_back_button_pressed() void
        +_on_audio_reset_button_pressed() void
        +_sync_ui_from_manager() void
        +_sync_all_sliders_and_mutes() void
        +_on_global_volume_changed(bus_name String volume float) void
        +_on_global_mute_toggled(bus_name String is_muted bool) void
        +_update_ui_interactivity() void
        +_connect_bus_ui(mute_btn CheckButton slider HSlider mute_callback Callable gui_callback Callable is_muted bool) void
        +_on_master_volume_control_gui_input(event InputEvent) void
        +_on_music_volume_control_gui_input(event InputEvent) void
        +_on_sfx_volume_control_gui_input(event InputEvent) void
        +_on_weapon_volume_control_gui_input(event InputEvent) void
        +_on_rotor_volume_control_gui_input(event InputEvent) void
        +_on_menu_volume_control_gui_input(event InputEvent) void
        +_on_master_mute_toggled(toggled_on bool) void
        +_on_music_mute_toggled(toggled_on bool) void
        +_on_sfx_mute_toggled(toggled_on bool) void
        +_on_weapon_mute_toggled(toggled_on bool) void
        +_on_rotor_mute_toggled(toggled_on bool) void
        +_on_menu_mute_toggled(toggled_on bool) void
        +_on_music_mute_gui_input(event InputEvent) void
        +_on_sfx_mute_gui_input(event InputEvent) void
        +_on_weapon_mute_gui_input(event InputEvent) void
        +_on_rotor_mute_gui_input(event InputEvent) void
        +_on_menu_mute_gui_input(event InputEvent) void
        +_handle_slider_gui_input(event InputEvent master_muted bool sfx_muted bool bus_muted bool mute_button CheckButton master_dialog AcceptDialog sfx_dialog AcceptDialog) void
        +_handle_mute_gui_input(event InputEvent master_muted bool sfx_muted bool master_dialog AcceptDialog sfx_dialog AcceptDialog) void
        +_update_label_colors() void
        +_reset_master_warning_shown() void
        +_reset_sfx_warning_shown() void
        +_on_tree_exited() void
    }

    class Globals {
        <<Node(Auotload)>>
        +UI_NAV_SOUND_PATH String
        +settings GameSettingsResource
        +previous_scene String
        +next_scene String
        +options_open bool
        +hidden_menus Array
        +_ui_nav_stream AudioStream
        +_nav_sfx_player AudioStreamPlayer
        +_nav_actions Array~String~
        +_ready() void
        +_on_setting_changed(setting_name String new_value Variant) void
        +_load_settings() void
        +_save_settings() void
        +log_message(message String level LogLevel) void
        +_input(event InputEvent) void
        +_play_ui_navigation_sfx() void
        +ensure_initial_focus(first Control controls Array~Control~ context String) void
        +get_game_version() String
        +set_game_version_for_tests(value String) void
    }

    AudioSettings ..> AudioManager : uses
    AudioSettings ..> AudioConstants : uses
    AudioSettings ..> Globals : uses
    AudioSettings ..> AudioWebBridge : optional reference via /root

    AudioWebBridge ..> AudioManager : uses signals and setters
    AudioWebBridge ..> AudioConstants : uses bus names

    AudioManager ..> AudioConstants : uses BUS_* and BUS_CONFIG

    Globals ..> AudioConstants : sets AudioStreamPlayer bus

    AudioWebBridge ..> OSWrapper : composition
    AudioWebBridge ..> JavaScriptBridgeWrapper : composition
Loading

File-Level Changes

Change Details Files
Refactor audio settings UI logic to support a hierarchical menu SFX bus, centralize mute/volume wiring, and delegate web/DOM responsibilities to a bridge autoload.
  • Replace per-bus wiring in audio_settings.gd with a reusable _connect_bus_ui helper and a single _update_ui_interactivity source of truth for enabling/disabling sliders and mutes based on master/SFX/menu mute hierarchy.
  • Add Menu (SFX_Menu) slider and mute controls to the audio settings scene and script, including label color updates and warning-dialog handling for master/SFX lockouts.
  • Connect AudioManager.volume_changed and AudioManager.mute_toggled signals so changes from Playwright/web update Godot sliders/mutes, with helpers to sync all UI from the manager on load and reset.
  • Rework back/reset flows to use a unified _on_back_button_pressed handler that restores previous menus, DOM visibility, and focus, and to call into the web bridge for DOM visibility and reset syncing.
  • Simplify unexpected-exit handling by disconnecting from AudioManager and AudioWebBridge signals in _on_tree_exited and restoring the last hidden menu without direct JS access.
scripts/audio_settings.gd
scenes/audio_settings.tscn
test/gdunit4/test_audio_settings.gd
test/gdunit4/test_audio_unexpected_exit.gd
tests/back_flow_test.py
Add a dedicated SFX_Menu audio bus, persistent UI navigation sound, and expose audio changes via signals for UI and web layers.
  • Extend AudioConstants and AudioManager with a BUS_SFX_MENU configuration, menu_volume/menu_muted fields, and updated get/set helpers that emit volume_changed and mute_toggled signals on successful updates.
  • Instantiate a persistent AudioStreamPlayer in Globals that plays a preloaded ui_navigation.wav on UI navigation actions, routed through the SFX_Menu bus and processed even when the game is paused.
  • Update basic save/load and AudioServer setup tests to register the SFX_Menu bus and assert the extra menu volume/mute keys in the config.
  • Adjust volume slider tests to initialize AudioManager and AudioServer consistently before assertions.
scripts/audio_constants.gd
scripts/audio_manager.gd
scripts/globals.gd
default_bus_layout.tres
test/gut/test_basic_save_load_without_other_settings.gd
test/gdunit4/test_volume_slider.gd
files/sounds/sfx/ui_navigation.wav.import
Introduce an AudioWebBridge autoload to own all web/JavaScript audio callbacks and DOM synchronization for sliders, mutes, and menu visibility.
  • Create audio_web_bridge.gd as a web-only singleton that registers all JS callbacks (volume, mute, reset, back) on window, validates payloads, enforces master/SFX parent-mute constraints, and routes changes into AudioManager while emitting web_back_requested and web_reset_requested signals.
  • Provide DOM-sync helpers that map audio buses to slider/mute element IDs, update their values/checked states based on AudioManager, toggle audio overlay visibility, and restore options menu DOM as a fallback.
  • Add a GUT test suite for AudioWebBridge covering non-web self-destruction, missing window behavior, DOM toggle semantics, Godot→JS signal propagation, JS→Godot volume/mute validation, and back/reset signal emission.
  • Register UID files for the new script and tests.
scripts/audio_web_bridge.gd
scripts/audio_web_bridge.gd.uid
test/gut/test_audio_web_bridge.gd
test/gut/test_audio_web_bridge.gd.uid
Extend the HTML shell and Playwright/E2E tests to cover the new menu bus and updated logging and back-flow semantics.
  • Add menu-slider and mute-menu controls to custom_shell.html and wire them to new window.changeMenuVolume and window.toggleMuteMenu callbacks.
  • Update volume_sliders_mutes_test.py to match new AudioManager log messages, exercise menu volume/mute paths, and assert correct DOM state for the new controls.
  • Adjust back_flow_test.py to assert on the new back-navigation debug log string and maintain mid-drag SFX slider behavior.
custom_shell.html
tests/volume_sliders_mutes_test.py
tests/back_flow_test.py

Assessment against linked issues

Issue Objective Addressed Explanation
#497 Add a UI audio bus constant and configuration in audio_constants.gd, including const BUS_UI: String = "UI" and a BUS_UI entry in BUS_CONFIG with volume_var: "ui_volume" and muted_var: "ui_muted". The PR modifies audio_constants.gd to add BUS_SFX_MENU and a BUS_CONFIG entry using menu_volume/menu_muted, but it does not add a BUS_UI constant or a BUS_UI entry with ui_volume/ui_muted as specified in the issue.
#497 Add a new audio bus named UI to the default_bus_layout.tres bus layout. Although default_bus_layout.tres is modified, the PR’s changes and other code consistently use a bus named SFX_Menu rather than a bus named UI. There is no evidence that a bus explicitly named UI was added to the bus layout as required by the issue.
#498 Extend the audio settings UI scene (audio_settings.tscn) to add a new UI/Menu volume row consisting of a label, a VolumeSlider-based slider, and a mute toggle button.
#498 Update audio_settings.gd to declare @onready variables for the new UI/Menu volume controls and implement handlers for slider input and mute toggling for the new bus.
#498 Update audio settings synchronization logic so that the new UI/Menu audio bus state (volume and mute) is kept in sync between AudioManager, the Godot UI, and the web/DOM bridge.
#503 Add volume_changed and mute_toggled signals to AudioManager and emit them whenever a bus volume or mute state changes so external layers (UI/web) can stay in sync.
#503 Create an audio_web_bridge.gd Autoload (AudioWebBridge) that owns all web/JavaScript audio integration, including JS callback registration, DOM visibility and value syncing, and routing JS events directly to AudioManager instead of UI scripts.
#503 Refactor audio_settings.gd to remove all web-specific variables and logic (JavaScriptBridgeWrapper, OSWrapper, JS callbacks, DOM evals) so it only manages Godot UI, connects to AudioManager via signals, and interacts with web behavior only through the AudioWebBridge API/signals.
#504 Add a test_audio_web_bridge.gd unit test suite for the AudioWebBridge Autoload that implements the 14 specified test cases (TC-AWB-01 through TC-AWB-14), covering initialization, public API visibility toggling, Godot→JS DOM sync, and JS→Godot routing for volume/mute/back/reset.
#504 Within the test_audio_web_bridge.gd suite, use mocking of OSWrapper and JavaScriptBridgeWrapper (including a mock js_window) to simulate the web/browser environment and spy on JS eval/callback behavior as described.

Possibly linked issues

  • #FEATURE: PR implements the requested dedicated UI/Menu audio bus, constants, manager fields, and new Audio Settings row with controls.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@ikostan has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 54 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 13 minutes and 54 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cac038d3-6cf8-4456-9a11-e24f0a037205

📥 Commits

Reviewing files that changed from the base of the PR and between ce589c4 and a8e1479.

📒 Files selected for processing (3)
  • scripts/audio_settings.gd
  • scripts/audio_web_bridge.gd
  • scripts/globals.gd
📝 Walkthrough

Walkthrough

Adds an AudioWebBridge autoload for browser DOM ⇄ Godot audio sync, introduces a dedicated SFX_Menu audio bus and UI controls (slider + mute), moves JS bridge logic out of audio_settings.gd, adds UI navigation SFX playback, and includes tests for the web bridge and menu volume/mute behavior.

Changes

Cohort / File(s) Summary
Web bridge & HTML wiring
custom_shell.html, scripts/audio_web_bridge.gd, scripts/audio_web_bridge.gd.uid, project.godot
Added HTML slider/checkbox callbacks and a new AudioWebBridge autoload that registers JS callbacks, validates/clamps inputs, updates AudioManager, syncs Godot→DOM, exposes toggle_dom_visibility()/sync_all_to_dom(), and emits web_back_requested/web_reset_requested.
Audio bus & constants
default_bus_layout.tres, scripts/audio_constants.gd
Changed bus/3 send target to Master, added a new exported audio bus SFX_Menu, and added BUS_SFX_MENU constant plus BUS_CONFIG entry mapping menu volume/mute defaults.
Audio manager
scripts/audio_manager.gd
Added menu_volume/menu_muted fields, volume_changed/mute_toggled signals, and getter/setter support for BUS_SFX_MENU with conditional signal emission and updated match fallbacks.
Audio settings UI & scene
scenes/audio_settings.tscn, scenes/volume_controls/sfx_menu_volume_control.tscn, custom_shell.html
Inserted SFXMenu control (label, HSlider, CheckButton), updated scene dependencies and offsets, rewired focus/navigation to include menu controls, and added corresponding DOM inputs in custom_shell.html.
Audio settings logic
scripts/audio_settings.gd
Removed embedded JS bridge plumbing; integrated AudioWebBridge autoload calls; added menu_slider/mute_menu wiring; centralized mute-hierarchy interactivity; connected to AudioManager signals and updated reset/back flows.
UI navigation SFX
scripts/globals.gd, files/sounds/sfx/ui_navigation.wav.import
Preloaded UI navigation SFX, added a persistent _nav_sfx_player, input handler to play navigation sounds in menu context, and added import metadata for the wav asset.
Tests & expectations
test/gdunit4/*, test/gut/test_audio_web_bridge.gd, test/gut/test_audio_web_bridge.gd.uid, tests/*, test/gut/test_basic_save_load_without_other_settings.gd
Removed many in-file web mocks, added GUT tests for AudioWebBridge (init, API, Godot→JS, JS→Godot), updated tests and browser-driven expectations for the new menu bus and log text, and adjusted headless test setup to ensure SFX_Menu presence.

Sequence Diagram

sequenceDiagram
    participant Web as JavaScript (Browser)
    participant Bridge as AudioWebBridge
    participant Manager as AudioManager
    participant Audio as AudioServer
    participant UI as AudioSettingsUI

    Web->>Bridge: window.changeMenuVolume([0.9])
    activate Bridge
    Bridge->>Manager: set_volume("SFX_Menu", 0.9)
    activate Manager
    Manager->>Manager: menu_volume = 0.9
    Manager->>Audio: apply volume to "SFX_Menu"
    Manager->>Manager: emit volume_changed("SFX_Menu", 0.9)
    Manager-->>Bridge: (signal)
    deactivate Manager
    Bridge->>Web: eval(...) to update DOM slider
    deactivate Bridge

    Web->>Bridge: window.toggleMuteMenu([true])
    activate Bridge
    Bridge->>Manager: set_muted("SFX_Menu", true)
    activate Manager
    Manager->>Manager: menu_muted = true
    Manager->>Audio: apply mute to "SFX_Menu"
    Manager->>Manager: emit mute_toggled("SFX_Menu", true)
    Manager-->>Bridge: (signal)
    deactivate Manager
    Bridge->>Web: eval(...) to update DOM checkbox
    deactivate Bridge

    Manager-->>UI: volume_changed / mute_toggled
    UI->>UI: update slider/checkbox visuals
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Suggested labels

web, controls, testing

Poem

🐇 I hopped from node to DOM with care,
I wired the slider and hushed the air.
Signals chimed, the bridge replied,
Menu SFX and mute now tied.
A tiny hop — audio bound!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Menu SFX bus, AudioWebBridge, and UI navigation sounds' accurately and specifically summarizes the three main features added in the PR.
Description check ✅ Passed The PR description includes images, a detailed 'Summary by Sourcery' section covering new features and enhancements, testing information, and a comprehensive overview. However, the templated sections (Description, Related Issue, Changes, Testing, Checklist) are incomplete or only checkmarked without substantive content.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SFX

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@deepsource-io

deepsource-io Bot commented Mar 22, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 9ddba69...a8e1479 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Mar 31, 2026 4:50a.m. Review ↗
JavaScript Mar 31, 2026 4:50a.m. Review ↗

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In _on_menu_volume_control_gui_input you pass AudioManager.rotors_muted into _handle_slider_gui_input while controlling the Menu slider; this looks like a copy‑paste issue and probably should use the menu mute state instead to avoid incorrect lock/enable behavior.
  • In globals.gd, _ui_nav_stream is preloaded using UI_NAV_SOUND_PATH before that constant is defined, which can cause a load-time error in GDScript; consider moving the const UI_NAV_SOUND_PATH definition above the preload or moving the preload into _ready().
  • The global _unhandled_input handler in globals.gd will play the navigation SFX for any ui_* movement anywhere in the game; if this is intended only for menus, consider guarding it based on the active scene or focused UI to avoid SFX triggering during gameplay.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_on_menu_volume_control_gui_input` you pass `AudioManager.rotors_muted` into `_handle_slider_gui_input` while controlling the Menu slider; this looks like a copy‑paste issue and probably should use the menu mute state instead to avoid incorrect lock/enable behavior.
- In `globals.gd`, `_ui_nav_stream` is preloaded using `UI_NAV_SOUND_PATH` before that constant is defined, which can cause a load-time error in GDScript; consider moving the `const UI_NAV_SOUND_PATH` definition above the preload or moving the preload into `_ready()`.
- The global `_unhandled_input` handler in `globals.gd` will play the navigation SFX for any `ui_*` movement anywhere in the game; if this is intended only for menus, consider guarding it based on the active scene or focused UI to avoid SFX triggering during gameplay.

## Individual Comments

### Comment 1
<location path="scripts/audio_settings.gd" line_range="726" />
<code_context>
+		event,
+		AudioManager.master_muted,
+		AudioManager.sfx_muted,
+		AudioManager.rotors_muted,
+		mute_menu,
+		master_warning_dialog,
</code_context>
<issue_to_address>
**issue (bug_risk):** Menu slider mute state should likely use `menu_muted` instead of `rotors_muted`.

In `_on_menu_volume_control_gui_input`, the 4th argument to `_handle_slider_gui_input` is `AudioManager.rotors_muted`. To keep the menu slider independent of the rotors bus and consistent with the other sub-buses, this should be `AudioManager.menu_muted` instead.
</issue_to_address>

### Comment 2
<location path="scripts/globals.gd" line_range="39-42" />
<code_context>
 var _is_loading_settings: bool = false  # Guard flag

+## Preloaded stream to prevent disk I/O lag during fast menu navigation.
+var _ui_nav_stream: AudioStream = preload(UI_NAV_SOUND_PATH)
+
+## Path to the navigation sound file
+const UI_NAV_SOUND_PATH: String = "res://files/sounds/sfx/ui_navigation.wav"
+

</code_context>
<issue_to_address>
**issue (bug_risk):** Preload uses `UI_NAV_SOUND_PATH` before the constant is defined, which will break in GDScript.

Because GDScript evaluates top-to-bottom, `preload(UI_NAV_SOUND_PATH)` runs before `UI_NAV_SOUND_PATH` exists and will error. Fix by moving `const UI_NAV_SOUND_PATH` above `_ui_nav_stream`, or by inlining the path string directly in `preload()`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/audio_settings.gd Outdated
Comment thread scripts/globals.gd Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/audio_manager.gd (1)

72-88: ⚠️ Potential issue | 🟠 Major

Refactor the bus getters before adding another bus case.

Adding SFX_Menu pushes both functions past gdlint’s max-returns limit, and CI is already red. AudioConstants.BUS_CONFIG already carries the backing property names, so both lookups can be reduced to a single guarded return path.

♻️ One way to collapse both functions
 func get_volume(bus_name: String) -> float:
-	match bus_name:
-		AudioConstants.BUS_MASTER:
-			return master_volume
-		AudioConstants.BUS_MUSIC:
-			return music_volume
-		AudioConstants.BUS_SFX:
-			return sfx_volume
-		AudioConstants.BUS_SFX_WEAPON:
-			return weapon_volume
-		AudioConstants.BUS_SFX_ROTORS:
-			return rotors_volume
-		AudioConstants.BUS_SFX_MENU:
-			return menu_volume
-		_:
-			Globals.log_message("Unknown bus for get_volume: " + bus_name, Globals.LogLevel.WARNING)
-			return 0.0
+	if not AudioConstants.BUS_CONFIG.has(bus_name):
+		Globals.log_message("Unknown bus for get_volume: " + bus_name, Globals.LogLevel.WARNING)
+		return 0.0
+	return float(get(AudioConstants.BUS_CONFIG[bus_name]["volume_var"]))
@@
 func get_muted(bus_name: String) -> bool:
-	match bus_name:
-		AudioConstants.BUS_MASTER:
-			return master_muted
-		AudioConstants.BUS_MUSIC:
-			return music_muted
-		AudioConstants.BUS_SFX:
-			return sfx_muted
-		AudioConstants.BUS_SFX_WEAPON:
-			return weapon_muted
-		AudioConstants.BUS_SFX_ROTORS:
-			return rotors_muted
-		AudioConstants.BUS_SFX_MENU:
-			return menu_muted
-		_:
-			Globals.log_message("Unknown bus for get_muted: " + bus_name, Globals.LogLevel.WARNING)
-			return false
+	if not AudioConstants.BUS_CONFIG.has(bus_name):
+		Globals.log_message("Unknown bus for get_muted: " + bus_name, Globals.LogLevel.WARNING)
+		return false
+	return bool(get(AudioConstants.BUS_CONFIG[bus_name]["muted_var"]))

Also applies to: 143-159

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/audio_manager.gd` around lines 72 - 88, Refactor get_volume (and the
sibling setter at lines 143-159) to avoid multiple return points by using
AudioConstants.BUS_CONFIG (which maps bus names to backing property names) to
resolve the target property, then perform a single guarded return: look up
AudioConstants.BUS_CONFIG[bus_name], if missing log the warning via
Globals.log_message and return 0.0 (or appropriate default), otherwise read the
resolved property (master_volume, music_volume, etc.) dynamically and return it;
apply the same collapse pattern to the corresponding setter so both functions
have a single exit and comply with gdlint max-returns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scenes/audio_settings.tscn`:
- Around line 156-158: The focus chain is skipping the new SFXMenu row; update
focus properties so keyboard/gamepad can reach it: in the SFXRotors node change
focus_neighbor_bottom and focus_next to "SFXMenu" instead of "AudioBackButton";
add focus_neighbor_top="SFXRotors", focus_neighbor_bottom="AudioBackButton",
focus_next="AudioBackButton" and focus_previous="SFXRotors" to the SFXMenu node;
and update AudioBackButton to set focus_neighbor_top="SFXMenu" (was
"SFXRotors"). Ensure the node names match exactly: SFXRotors, SFXMenu,
AudioBackButton.

In `@scenes/volume_controls/sfx_menu_volume_control.tscn`:
- Line 21: Two tooltip_text properties in sfx_menu_volume_control.tscn still say
"SFX Rotors Volume Control"; update both occurrences of tooltip_text (the ones
currently set to "SFX Rotors Volume Control") to the correct label, e.g. "SFX
Menu Volume Control" (or simply "SFX Volume Control") so the menu row
tooltip/accessibility text matches this control.

In `@scripts/audio_settings.gd`:
- Line 80: The _update_label_colors() function currently updates labels up to
rotor_label but omits the new menu_label; update that helper so it also sets
menu_label's modulate/color based on the same focus logic (same branch used for
rotor_label), referencing the onready var menu_label and the
_update_label_colors() function to locate and add the assignment so the Menu row
gets the same highlight behavior as the other volume rows.
- Around line 719-796: The file exceeds max-file-lines due to duplicated per-bus
blocks (see _on_menu_volume_control_gui_input, _on_menu_mute_toggled,
_on_menu_mute_gui_input, _on_change_menu_volume_js, _on_toggle_mute_menu_js and
variables mute_menu/menu_slider), so refactor by extracting a shared helper and
a small bus-config data structure: create a reusable
register_bus_handlers(bus_id, ui_nodes, warning_dialogs, slider_props) that
wraps calls to _handle_slider_gui_input/_handle_mute_gui_input, sets/get
toggles, calls AudioManager.apply_volume_to_bus, AudioManager.save_volumes,
menu_slider.set_value_no_signal, and uses
_validate_volume_args/_validate_mute_args; then replace the menu-specific blocks
with a single invocation of register_bus_handlers using
AudioConstants.BUS_SFX_MENU and the existing UI node references to remove the
near-duplicate code.
- Around line 721-730: In _on_menu_volume_control_gui_input replace the
incorrect rotor mute flag with the menu mute flag: call
_handle_slider_gui_input(event, AudioManager.master_muted,
AudioManager.sfx_muted, AudioManager.menu_muted, mute_menu,
master_warning_dialog, sfx_warning_dialog) so the Menu slider uses
AudioManager.menu_muted instead of AudioManager.rotors_muted to determine its
mute state.

In `@scripts/globals.gd`:
- Around line 346-358: The _unhandled_input handler currently plays navigation
SFX for ui_* actions even during gameplay; modify Globals._unhandled_input to
first check whether there is a visible GUI focus owner before calling
_play_ui_navigation_sfx (e.g., verify get_tree().get_gui_focus_owner() is
non-null and is_visible_in_tree or otherwise confirm a control has
focus/visibility). Update the guard logic in _unhandled_input so
_play_ui_navigation_sfx() is only invoked when a focused/visible UI control
exists.

---

Outside diff comments:
In `@scripts/audio_manager.gd`:
- Around line 72-88: Refactor get_volume (and the sibling setter at lines
143-159) to avoid multiple return points by using AudioConstants.BUS_CONFIG
(which maps bus names to backing property names) to resolve the target property,
then perform a single guarded return: look up
AudioConstants.BUS_CONFIG[bus_name], if missing log the warning via
Globals.log_message and return 0.0 (or appropriate default), otherwise read the
resolved property (master_volume, music_volume, etc.) dynamically and return it;
apply the same collapse pattern to the corresponding setter so both functions
have a single exit and comply with gdlint max-returns.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6b461682-4a6f-442b-99f5-f87ce39dab0f

📥 Commits

Reviewing files that changed from the base of the PR and between 9ddba69 and 2c86f40.

📒 Files selected for processing (8)
  • custom_shell.html
  • default_bus_layout.tres
  • scenes/audio_settings.tscn
  • scenes/volume_controls/sfx_menu_volume_control.tscn
  • scripts/audio_constants.gd
  • scripts/audio_manager.gd
  • scripts/audio_settings.gd
  • scripts/globals.gd
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: Pull Request Pipeline
scripts/audio_manager.gd

[error] 88-88: gdlint: Function "get_volume" has more than 6 return statements (max-returns)


[error] 159-159: gdlint: Function "get_muted" has more than 6 return statements (max-returns)

scripts/globals.gd

[error] 42-42: gdlint: Definition out of order in global scope (class-definitions-order)

scripts/audio_settings.gd

[error] 1123-1123: gdlint: Max allowed file lines num (1000) exceeded (max-file-lines)

🔇 Additional comments (1)
scripts/globals.gd (1)

38-42: This review comment is incorrect. The code is already in the correct order.

The UI_NAV_SOUND_PATH constant is declared on line 12, and the _ui_nav_stream variable that uses it is declared on line 32. The const-before-var ordering is already correct, which is why the preload(UI_NAV_SOUND_PATH) call on line 32 works without issue. There is no class-definitions-order lint failure in the current code.

			> Likely an incorrect or invalid review comment.

Comment thread scenes/audio_settings.tscn
Comment thread scenes/volume_controls/sfx_menu_volume_control.tscn Outdated
Comment thread scripts/audio_settings.gd
Comment thread scripts/audio_settings.gd Outdated
Comment thread scripts/audio_settings.gd
Comment thread scripts/globals.gd Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
scripts/audio_settings.gd (1)

768-769: Fix misleading comment.

The comment on line 768 says "sets rotors_volume" but this function sets menu volume.

Proposed fix
-	# Update AudioManager (sets rotors_volume)
+	# Update AudioManager (sets menu_volume)
 	AudioManager.set_volume(AudioConstants.BUS_SFX_MENU, value)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/audio_settings.gd` around lines 768 - 769, The inline comment above
the call to AudioManager.set_volume with AudioConstants.BUS_SFX_MENU is
incorrect ("sets rotors_volume"); update that comment to accurately describe the
call (e.g., "sets menu volume" or "set SFX menu bus volume") or remove it if
redundant so the comment matches the function's behavior. Ensure the modified
comment sits immediately above the
AudioManager.set_volume(AudioConstants.BUS_SFX_MENU, value) call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@scripts/audio_settings.gd`:
- Around line 768-769: The inline comment above the call to
AudioManager.set_volume with AudioConstants.BUS_SFX_MENU is incorrect ("sets
rotors_volume"); update that comment to accurately describe the call (e.g.,
"sets menu volume" or "set SFX menu bus volume") or remove it if redundant so
the comment matches the function's behavior. Ensure the modified comment sits
immediately above the AudioManager.set_volume(AudioConstants.BUS_SFX_MENU,
value) call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d21d138a-2116-405c-99b7-86d6c6c9bdeb

📥 Commits

Reviewing files that changed from the base of the PR and between 2c86f40 and ff20858.

📒 Files selected for processing (3)
  • scripts/audio_manager.gd
  • scripts/audio_settings.gd
  • scripts/globals.gd
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/audio_manager.gd
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: Pull Request Pipeline
scripts/audio_settings.gd

[error] 1118-1118: gdlint error: Max allowed file lines num (1000) exceeded (max-file-lines). Failure: 1 problem found.

🔇 Additional comments (10)
scripts/globals.gd (3)

11-12: LGTM!

Preloading the navigation sound at startup is the correct approach to avoid disk I/O lag during fast menu navigation.

Also applies to: 31-33


336-348: Guard SFX playback to prevent menu navigation sounds during gameplay.

The _unhandled_input handler will receive ui_* actions during gameplay because neither player.gd nor main_scene.gd consume these actions. Navigation SFX will leak outside menus when no control has focus.

Proposed fix
 func _unhandled_input(event: InputEvent) -> void:
+	var focus_owner: Control = get_viewport().gui_get_focus_owner()
+	if not is_instance_valid(focus_owner) or not focus_owner.is_visible_in_tree():
+		return
+
 	# Target Actions: ui_up, ui_down, ui_left, ui_right, ui_focus_next, ui_focus_prev.
 	if (
 		event.is_action_pressed("ui_up")

[raise_major_issue, duplicate_comment]


351-365: LGTM!

The one-shot sound pattern with automatic cleanup via finished.connect(sfx_player.queue_free) is appropriate. Routing through BUS_SFX_MENU correctly respects user's menu volume settings.

scripts/audio_settings.gd (7)

35-35: LGTM!

Menu control declarations follow the established pattern for other SFX sub-buses.

Also applies to: 42-42, 61-63, 75-75


150-158: LGTM!

Menu control initialization follows the established pattern with proper signal connection guards and JS callback registration.

Also applies to: 197-199, 215-217, 234-235


246-259: Include menu_label in _update_label_colors().

The menu_label is declared at line 75 but is not included in this function, so the Menu row will never receive the same focus highlight as other volume rows.

Proposed fix
 	rotor_label.modulate = yellow if (rotor_slider.has_focus() or mute_rotor.has_focus()) else white
+	menu_label.modulate = yellow if (menu_slider.has_focus() or mute_menu.has_focus()) else white

[raise_minor_issue, duplicate_comment]


280-282: LGTM!

DOM synchronization for menu slider and mute checkbox follows the established pattern.

Also applies to: 310-312


716-725: Use menu_muted here, not rotors_muted.

Line 721 passes AudioManager.rotors_muted as the bus_muted parameter. This causes the Menu slider to auto-unmute based on the Rotor mute state instead of its own mute flag.

Proposed fix
 func _on_menu_volume_control_gui_input(event: InputEvent) -> void:
 	_handle_slider_gui_input(
 		event,
 		AudioManager.master_muted,
 		AudioManager.sfx_muted,
-		AudioManager.rotors_muted,
+		AudioManager.menu_muted,
 		mute_menu,
 		master_warning_dialog,
 		sfx_warning_dialog
 	)

[raise_major_issue, duplicate_comment]


831-833: LGTM!

Menu controls correctly follow the SFX-dependent enable/disable pattern, matching weapon and rotor behavior.


711-795: File exceeds max-file-lines lint limit (1000 lines).

The pipeline is failing due to this file having 1118 lines. The menu-specific handlers add another near-identical block that increases code duplication.

Consider extracting shared per-bus registration/handler logic into a reusable helper and a small bus-config data structure. This would consolidate the repetitive patterns for master, music, SFX, weapon, rotors, and menu into a single parameterized implementation.

As a short-term fix to unblock CI, you could extract the new menu handlers into a separate partial class or helper script.
[raise_major_issue, duplicate_comment]

ikostan added 3 commits March 21, 2026 22:29
… instead of rotors_muted.

In _on_menu_volume_control_gui_input, the 4th argument to _handle_slider_gui_input is AudioManager.rotors_muted. To keep the menu slider independent of the rotors bus and consistent with the other sub-buses, this should be AudioManager.menu_muted instead.
These still announce “Rotors”, so the new control exposes the wrong label in tooltip/accessibility text.
The new row is wired here, but the color-update helper still stops at rotor_label, so Menu never gets the same focus highlight as the other volume rows.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
scripts/audio_settings.gd (1)

150-157: 🛠️ Refactor suggestion | 🟠 Major

Please extract the new Menu bus plumbing behind shared helpers before merge.

This adds another full copy of the init/sync/JS-handler/cleanup path, and CI is still failing gdlint on max-file-lines for this file. A small per-bus config plus shared helpers would remove most of these new hunks and clear the lint gate.

Also applies to: 197-216, 281-313, 715-792, 1045-1047, 1075-1081, 1111-1117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/audio_settings.gd` around lines 150 - 157, The Menu bus plumbing
duplicates init/sync/JS-handler/cleanup logic (connecting toggled/gui_input and
setting button_pressed) and should be refactored into shared helpers; create a
generic helper (e.g., _init_bus_controls or _setup_bus_plumbing) that takes the
bus identifier and relevant nodes/handlers (mute_menu, menu_slider,
_on_menu_mute_toggled, _on_menu_volume_control_gui_input,
_on_menu_mute_gui_input) and performs the connect checks and initial state
assignment (using AudioManager.menu_muted for initial button_pressed), then
replace the repeated blocks (including the other ranges noted) to call that
helper and implement corresponding teardown/sync helpers for cleanup and
JS-handler wiring.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/audio_settings.gd`:
- Around line 759-792: The web Menu callbacks don't mirror the native
parent-mute guard: in _on_change_menu_volume_js, when returning early due to
Master or SFX being muted, call _sync_dom_ui() (and leave UI slider unchanged)
before returning so the DOM stays in sync; in _on_toggle_mute_menu_js, check
AudioManager.get_muted(AudioConstants.BUS_MASTER) and
AudioManager.get_muted(AudioConstants.BUS_SFX) and if either is true, reset
mute_menu.button_pressed to the actual Menu bus state (do not apply the toggle),
call _sync_dom_ui(), and return early so the web overlay cannot drift out of
sync with the native gui_input path.

---

Duplicate comments:
In `@scripts/audio_settings.gd`:
- Around line 150-157: The Menu bus plumbing duplicates
init/sync/JS-handler/cleanup logic (connecting toggled/gui_input and setting
button_pressed) and should be refactored into shared helpers; create a generic
helper (e.g., _init_bus_controls or _setup_bus_plumbing) that takes the bus
identifier and relevant nodes/handlers (mute_menu, menu_slider,
_on_menu_mute_toggled, _on_menu_volume_control_gui_input,
_on_menu_mute_gui_input) and performs the connect checks and initial state
assignment (using AudioManager.menu_muted for initial button_pressed), then
replace the repeated blocks (including the other ranges noted) to call that
helper and implement corresponding teardown/sync helpers for cleanup and
JS-handler wiring.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 99402d20-f116-466a-bcb4-6681a1daa1db

📥 Commits

Reviewing files that changed from the base of the PR and between ff20858 and f9ae750.

📒 Files selected for processing (2)
  • scenes/volume_controls/sfx_menu_volume_control.tscn
  • scripts/audio_settings.gd
✅ Files skipped from review due to trivial changes (1)
  • scenes/volume_controls/sfx_menu_volume_control.tscn
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: Pull Request Pipeline
scripts/audio_settings.gd

[error] 1119-1119: gdlint failed: Max allowed file lines num (1000) exceeded (max-file-lines). Failure: 1 problem found.

Comment thread scripts/audio_settings.gd Outdated
ikostan and others added 4 commits March 30, 2026 20:54
In `@scripts/audio_settings.gd` around lines 190 - 193, The code is incorrectly
chaining three call_deferred calls on start_button which delays grab_focus by
three frames; replace the triple-nested call_deferred with a single deferred
invocation that calls grab_focus once (e.g., call_deferred("grab_focus") on the
start_button) while keeping the is_instance_valid(start_button) check and the
prev_menu/Panel logic intact so focus is set on the StartButton in the next idle
frame.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Mute toggle handler lacks parent-mute guard.

Unlike volume changes which block when parent buses are muted, _on_toggle_mute_js allows mute toggles on child buses even when Master or SFX is muted. This creates an inconsistency with the native UI path in audio_settings.gd where _handle_mute_gui_input blocks such interactions and shows a warning dialog.
@ikostan

ikostan commented Mar 31, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai resolve

@ikostan

ikostan commented Mar 31, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In Globals._play_ui_navigation_sfx() you log every navigation sound play; given this is triggered on high‑frequency UI input, consider downgrading or gating that log (e.g., behind a debug flag) to avoid noisy logs in normal gameplay.
  • In audio_settings.gd you repeatedly call get_node_or_null("/root/AudioWebBridge") in several methods (_ready, _on_back_button_pressed, _on_audio_reset_button_pressed); caching this reference once in _ready (with a nullable member) would simplify the code and avoid repeated tree lookups.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Globals._play_ui_navigation_sfx()` you log every navigation sound play; given this is triggered on high‑frequency UI input, consider downgrading or gating that log (e.g., behind a debug flag) to avoid noisy logs in normal gameplay.
- In `audio_settings.gd` you repeatedly call `get_node_or_null("/root/AudioWebBridge")` in several methods (`_ready`, `_on_back_button_pressed`, `_on_audio_reset_button_pressed`); caching this reference once in `_ready` (with a nullable member) would simplify the code and avoid repeated tree lookups.

## Individual Comments

### Comment 1
<location path="scripts/audio_settings.gd" line_range="111-116" />
<code_context>
+	)
+
+	# Connect specific mute button warning interceptors
+	mute_music.gui_input.connect(_on_music_mute_gui_input)
+	mute_sfx.gui_input.connect(_on_sfx_mute_gui_input)
+	mute_weapon.gui_input.connect(_on_weapon_mute_gui_input)
+	mute_rotor.gui_input.connect(_on_rotor_mute_gui_input)
+	mute_menu.gui_input.connect(_on_menu_mute_gui_input)
+
+	# Buttons
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Guard the new gui_input connections to avoid duplicate signal hookups on re-entrance.

The new `gui_input` connections for the mute buttons aren’t guarded with `if not X.is_connected(...)` like the other `_ready` connections. If the scene is removed/re-added or `_ready` runs multiple times, these handlers may be connected repeatedly and fire multiple times. Please wrap these `gui_input.connect(...)` calls in the same `is_connected` guard for consistency and to avoid duplicate signal handling.

```suggestion
	# Connect specific mute button warning interceptors
	if not mute_music.gui_input.is_connected(_on_music_mute_gui_input):
		mute_music.gui_input.connect(_on_music_mute_gui_input)
	if not mute_sfx.gui_input.is_connected(_on_sfx_mute_gui_input):
		mute_sfx.gui_input.connect(_on_sfx_mute_gui_input)
	if not mute_weapon.gui_input.is_connected(_on_weapon_mute_gui_input):
		mute_weapon.gui_input.connect(_on_weapon_mute_gui_input)
	if not mute_rotor.gui_input.is_connected(_on_rotor_mute_gui_input):
		mute_rotor.gui_input.connect(_on_rotor_mute_gui_input)
	if not mute_menu.gui_input.is_connected(_on_menu_mute_gui_input):
		mute_menu.gui_input.connect(_on_menu_mute_gui_input)
```
</issue_to_address>

### Comment 2
<location path="scripts/audio_settings.gd" line_range="151-152" />
<code_context>
 	Globals.ensure_initial_focus(master_slider, menu_controls, "Audio Settings")

+	# 1. Listen for changes coming from Playwright/Web
+	AudioManager.volume_changed.connect(_on_global_volume_changed)
+	AudioManager.mute_toggled.connect(_on_global_mute_toggled)
+
+	# Apply the hierarchy locks immediately when the menu opens
</code_context>
<issue_to_address>
**issue (bug_risk):** Consider disconnecting AudioManager signals on exit to avoid callbacks into freed nodes.

These connections in `_ready` have no matching disconnect in `_on_tree_exited` or before `queue_free()`. If this node is freed while `AudioManager` remains, later signal emissions will still try to call into it (Godot will warn and drop the call). Please explicitly `disconnect` (guarded with `is_connected`) during teardown to avoid noisy callbacks into freed objects.
</issue_to_address>

### Comment 3
<location path="scripts/globals.gd" line_range="382-383" />
<code_context>
+
+	# If the sound is already playing (e.g., from rapid button presses),
+	# restart it from the beginning to feel responsive.
+	_nav_sfx_player.play()
+	log_message("Playing UI nav sound...", LogLevel.DEBUG)
</code_context>
<issue_to_address>
**suggestion (performance):** The per-navigation DEBUG log can be very noisy; consider gating or removing it.

This now logs on every navigation event, which will flood DEBUG output in fast keyboard/controller navigation and can slow CI logs. If you still need this visibility, consider logging only on error/exception paths (e.g., invalid player) or adding a simple rate limit/boolean flag so it doesn’t emit on every single nav event.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/audio_settings.gd Outdated
Comment thread scripts/audio_settings.gd
Comment thread scripts/globals.gd Outdated
ikostan and others added 3 commits March 30, 2026 21:10
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
… to avoid callbacks into freed nodes.

issue (bug_risk): Consider disconnecting AudioManager signals on exit to avoid callbacks into freed nodes.

These connections in _ready have no matching disconnect in _on_tree_exited or before queue_free(). If this node is freed while AudioManager remains, later signal emissions will still try to call into it (Godot will warn and drop the call). Please explicitly disconnect (guarded with is_connected) during teardown to avoid noisy callbacks into freed objects.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
scripts/audio_settings.gd (1)

513-517: Reset path does redundant full-state sync.

_sync_ui_from_manager() already updates sliders/mutes + interactivity. Calling _sync_all_sliders_and_mutes() immediately after duplicates the same work and repeatedly re-runs interactivity updates.

Also applies to: 525-550

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/audio_settings.gd` around lines 513 - 517, The reset routine calls
_sync_ui_from_manager() which already updates sliders, mutes, and interactivity,
so remove the redundant _sync_all_sliders_and_mutes() and any duplicate
_update_ui_interactivity() calls after it; update the reset handler(s) that
currently call the sequence (_sync_ui_from_manager(),
_sync_all_sliders_and_mutes(), _update_ui_interactivity())—including the similar
block around the 525-550 region—to only call _sync_ui_from_manager(), ensuring
no duplicate UI-sync or interactivity updates remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/audio_settings.gd`:
- Around line 296-299: In _connect_bus_ui(), avoid assigning
mute_btn.button_pressed directly because it emits the toggled signal and invokes
mute_callback during initialization; instead call
mute_btn.set_pressed_no_signal(not is_muted) to set the button state without
emitting signals, and keep the existing toggled.connect(mute_callback) logic;
ensure this change is made near the current
mute_btn.toggled.connect(mute_callback) line and leave the slider.gui_input/
gui_callback connections unchanged.

---

Nitpick comments:
In `@scripts/audio_settings.gd`:
- Around line 513-517: The reset routine calls _sync_ui_from_manager() which
already updates sliders, mutes, and interactivity, so remove the redundant
_sync_all_sliders_and_mutes() and any duplicate _update_ui_interactivity() calls
after it; update the reset handler(s) that currently call the sequence
(_sync_ui_from_manager(), _sync_all_sliders_and_mutes(),
_update_ui_interactivity())—including the similar block around the 525-550
region—to only call _sync_ui_from_manager(), ensuring no duplicate UI-sync or
interactivity updates remain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ab9863c7-f4a3-4e1d-a7a8-7a71f9c730a8

📥 Commits

Reviewing files that changed from the base of the PR and between ce589c4 and 7ff62e7.

📒 Files selected for processing (3)
  • scripts/audio_settings.gd
  • scripts/audio_web_bridge.gd
  • scripts/globals.gd
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Browser Functional Tests / test
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-02-25T05:50:14.510Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 429
File: scripts/input_remap_button.gd:158-162
Timestamp: 2026-02-25T05:50:14.510Z
Learning: In `scripts/input_remap_button.gd`, the keyboard event copying logic (around lines 158-162) intentionally omits `meta_pressed` when transferring modifier states to new_event. Only `shift_pressed`, `ctrl_pressed`, and `alt_pressed` are copied. This is by design.

Applied to files:

  • scripts/globals.gd
📚 Learning: 2026-02-25T05:46:55.056Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 429
File: scripts/settings.gd:593-601
Timestamp: 2026-02-25T05:46:55.056Z
Learning: In `scripts/settings.gd`, the `events_match` function intentionally omits `meta_pressed` comparison for InputEventKey events. It only compares `physical_keycode`, `shift_pressed`, `ctrl_pressed`, and `alt_pressed`. This is by design.

Applied to files:

  • scripts/globals.gd
🔇 Additional comments (8)
scripts/globals.gd (5)

11-12: LGTM!

Clean constant definition for the navigation sound resource path.


31-40: LGTM!

Preloading the audio stream and using a persistent player are good practices that avoid disk I/O lag and node churn during rapid menu navigation.


43-51: LGTM!

The PROCESS_MODE_ALWAYS correctly addresses the autoload sleep bug mentioned in the PR objectives, ensuring input handling continues during paused states. The SFX player initialization is clean and properly routes audio through the dedicated SFX_Menu bus.


353-372: LGTM!

The implementation correctly:

  • Uses _input to catch navigation events before UI control focus traversal consumes them
  • Guards playback with a comprehensive menu context check covering pause state, options visibility, hidden menu stack, and focus ownership
  • Applies Input.is_action_just_pressed() for proper debouncing across keyboard and joypad inputs

This addresses the prior feedback about gameplay sound leakage and joypad echo handling.


375-383: LGTM!

The helper method has appropriate defensive null-checking and the restart behavior on rapid presses provides responsive feedback. DEBUG-level logging is suitable for this high-frequency event.

scripts/audio_web_bridge.gd (2)

37-58: Web-only autoload bootstrap is correctly guarded.

Good use of early queue_free() on non-web and explicit window interface validation before callback wiring.


185-214: Parent-mute guards now keep DOM and engine state consistent.

The early-return branches correctly revert slider/checkbox DOM state to current AudioManager values when changes are blocked.

Also applies to: 228-260

scripts/audio_settings.gd (1)

177-216: Unified back-navigation flow looks consistent now.

Nice consolidation: DOM hide, previous menu restore, focus restoration, and scene fallback are handled in one path.

Comment thread scripts/audio_settings.gd
ikostan and others added 4 commits March 30, 2026 21:20
…isy; consider gating or removing it.

suggestion (performance): The per-navigation DEBUG log can be very noisy; consider gating or removing it.

This now logs on every navigation event, which will flood DEBUG output in fast keyboard/controller navigation and can slow CI logs. If you still need this visibility, consider logging only on error/exception paths (e.g., invalid player) or adding a simple rate limit/boolean flag so it doesn’t emit on every single nav event.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@ikostan

ikostan commented Mar 31, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai guide

@ikostan

ikostan commented Mar 31, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai resolve

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

audio Sound/Audio related issue enhancement New feature or request GUI menu

Projects

Status: Done

2 participants