Skip to content

Enable keyboard & dpad navigation for advanced settings - #401

Merged
ikostan merged 24 commits into
mainfrom
enable-keyboard-&-dpad-navigation-for-advanced-settings
Feb 15, 2026
Merged

ikostan merged 24 commits into
mainfrom
enable-keyboard-&-dpad-navigation-for-advanced-settings

Conversation

@ikostan

@ikostan ikostan commented Feb 15, 2026

Copy link
Copy Markdown
Owner

Add full keyboard navigation support to the Advanced Settings menu in SkyLockAssault, allowing users to navigate, modify, and apply advanced configuration options using only the keyboard. This includes all advanced toggles, selectors, sliders, and sub-sections within the Advanced Settings menu.

Enable full D-Pad (controller) navigation support within the Advanced Settings menu to ensure consistent gamepad-only usability across SkyLockAssault.

Perform a comprehensive review of Milestone 11 upon its completion, and update the project's README.md file to reflect the delivered content.


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

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")

Summary by Sourcery

Improve focus handling when navigating to and from the Advanced Settings menu to better support keyboard and D-pad navigation.

Enhancements:

  • Automatically focus the log level control when the Advanced Settings menu is opened.
  • Restore focus to the Advanced Settings button in the previous menu after exiting Advanced Settings, when available.

Summary by Sourcery

Standardize and centralize UI focus handling across menus to improve keyboard and controller navigation, particularly around the Advanced Settings flow.

Enhancements:

  • Introduce a reusable helper in Globals for safely setting initial keyboard/controller focus within menus based on current focus state.
  • Adopt the centralized focus helper in main, pause, options, and advanced settings menus to provide consistent focus behavior when opening or returning to these screens.
  • Improve Advanced Settings back navigation to restore visibility and focus to the originating options menu when available.
  • Refactor pause menu tests to stub Globals methods instead of replacing the autoload, simplifying setup and avoiding side effects.
  • Document project architecture, DevOps/automation, testing practices, and current development status in the README, including input remapping and options behavior highlights.

Documentation:

  • Expand README with architecture highlights, testing strategy, DevOps/automation details, and updated development status including input remapping and options menu behavior.

Tests:

  • Simplify and harden pause menu GUT tests by stubbing global behaviors while preserving the real Globals autoload in the scene tree.

Summary by CodeRabbit

  • User Interface

    • Advanced Settings visuals refreshed: updated backgrounds, borders, shadows, corner radii, and focus highlight colors for clearer appearance.
  • Improvements

    • Smarter keyboard focus and navigation across menus: safer initial-focus handling, improved focus return when navigating back, checks to avoid stealing focus, and added diagnostic logging for focus actions.

[FEATURE] Enable D-Pad Navigation for Advanced Settings #395
[FEATURE] Enable Keyboard Navigation for Advanced Settings #388
@sourcery-ai

sourcery-ai Bot commented Feb 15, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Centralizes menu focus handling and updates main, options, pause, and advanced settings menus to use a safer, logging-backed initial-focus helper, while ensuring focus is restored correctly when exiting Advanced Settings.

Sequence diagram for focus handling when entering and exiting Advanced Settings

sequenceDiagram
    actor Player
    participant MainMenu
    participant OptionsMenu
    participant AdvancedSettings
    participant Globals
    participant Viewport

    Player->>MainMenu: Open game
    MainMenu->>Globals: ensure_initial_focus(start_button, [start_button, options_button, quit_button], Main_Menu)
    Globals->>Viewport: gui_get_focus_owner()
    alt No menu control has focus
        Globals-->>MainMenu: call_deferred(grab_focus on start_button)
    else Menu control already has focus
        Globals-->>MainMenu: Skip initial focus grab
    end

    Player->>MainMenu: Press options_button
    MainMenu->>OptionsMenu: Show OptionsMenu
    OptionsMenu->>OptionsMenu: _grab_first_button_focus()
    OptionsMenu->>Globals: ensure_initial_focus(candidate_button, [advanced_settings_button, audio_settings_button, key_mapping_button, gameplay_settings_button, options_back_button], Options_Menu)
    Globals->>Viewport: gui_get_focus_owner()
    alt No options control has focus
        Globals-->>OptionsMenu: call_deferred(grab_focus on candidate_button)
    else Options control already has focus
        Globals-->>OptionsMenu: Skip initial focus grab
    end

    Player->>OptionsMenu: Activate advanced_settings_button
    OptionsMenu->>AdvancedSettings: Show AdvancedSettings
    AdvancedSettings->>Globals: ensure_initial_focus(log_lvl_option, [log_lvl_option, advanced_back_button, advanced_reset_button], Advanced_Settings)
    Globals->>Viewport: gui_get_focus_owner()
    alt No advanced control has focus
        Globals-->>AdvancedSettings: call_deferred(grab_focus on log_lvl_option)
    else Advanced control already has focus
        Globals-->>AdvancedSettings: Skip initial focus grab
    end

    Player->>AdvancedSettings: Press advanced_back_button
    AdvancedSettings->>OptionsMenu: Show prev_menu (OptionsMenu)
    AdvancedSettings->>OptionsMenu: Get advanced_settings_button
    alt advanced_settings_button is valid
        AdvancedSettings->>OptionsMenu: call_deferred(grab_focus on advanced_settings_button)
    else advanced_settings_button invalid
        AdvancedSettings->>AdvancedSettings: Log warning and keep current focus
    end
Loading

Class diagram for menu focus handling with Globals.ensure_initial_focus

classDiagram
    class Globals {
        <<singleton>>
        +LogLevel DEBUG
        +LogLevel WARNING
        +ensure_initial_focus(candidate Control, allowed_controls Array~Control~, context String) void
        +log_message(message String, level LogLevel) void
    }

    class MainMenu {
        +start_button Button
        +options_button Button
        +quit_button Button
        +_ready() void
        +_on_start_pressed() void
    }

    class OptionsMenu {
        <<CanvasLayer>>
        +advanced_settings_button Button
        +audio_settings_button Button
        +key_mapping_button Button
        +gameplay_settings_button Button
        +options_back_button Button
        +options_vbox VBoxContainer
        +_ready() void
        +_grab_first_button_focus() void
    }

    class AdvancedSettings {
        <<CanvasLayer>>
        +log_lvl_option Control
        +advanced_back_button Button
        +advanced_reset_button Button
        +_ready() void
        +_on_advanced_back_button_pressed() void
        +_on_advanced_reset_js() void
    }

    class PauseMenu {
        <<CanvasLayer>>
        +resume_button Button
        +options_button Button
        +back_to_main_button Button
        +toggle_pause() void
        +_on_resume_button_pressed() void
    }

    class Viewport {
        +gui_get_focus_owner() Control
    }

    Globals ..> Viewport : uses
    MainMenu ..> Globals : calls_ensure_initial_focus
    OptionsMenu ..> Globals : calls_ensure_initial_focus
    AdvancedSettings ..> Globals : calls_ensure_initial_focus
    PauseMenu ..> Globals : calls_ensure_initial_focus

    AdvancedSettings ..> OptionsMenu : restores_focus_to_advanced_settings_button

    MainMenu o-- Button : owns
    OptionsMenu o-- Button : owns
    OptionsMenu o-- VBoxContainer : owns
    AdvancedSettings o-- Button : owns
    AdvancedSettings o-- Control : owns
    PauseMenu o-- Button : owns
Loading

File-Level Changes

Change Details Files
Introduce a centralized helper to safely apply initial keyboard/D‑Pad focus within a menu without stealing focus if a menu control is already focused.
  • Add ensure_initial_focus(candidate, allowed_controls, context) to validate the candidate, inspect current gui focus, and only defer grab_focus when focus is not already on an allowed menu control.
  • Log both focus-grab and focus-skip paths for easier debugging of focus issues.
  • Guard against invalid candidate controls and log a warning when the candidate is null or freed.
scripts/globals.gd
Update the Options menu to use the centralized focus helper and expose it as a named class for type-aware focus restoration.
  • Declare class_name OptionsMenu so it can be referenced by type elsewhere (e.g., Advanced Settings).
  • Refactor _grab_first_button_focus to locate the first visible, enabled button and pass it to Globals.ensure_initial_focus instead of directly grabbing focus.
  • Provide the list of main menu buttons as allowed_controls for the focus helper and log when no suitable button is found.
scripts/options_menu.gd
Improve Advanced Settings focus behavior so it sets a sensible initial control and restores focus to the originating Advanced Settings button when closing.
  • On ready, call Globals.ensure_initial_focus with the log level option as the candidate and the key Advanced Settings controls as allowed_controls to establish initial focus when entering the menu.
  • When the Back button is pressed, detect if the previous menu is an OptionsMenu and, if so, defer focus to its advanced_settings_button, with logging and validity checks.
  • Keep existing visibility and JS callback cleanup behavior intact while layering focus restoration on top.
scripts/advanced_settings.gd
Align Pause and Main menus with the new focus helper to avoid stealing focus and to make keyboard/controller navigation consistent.
  • Replace direct deferred grab_focus on the pause menu resume_button with a call to Globals.ensure_initial_focus using the primary pause menu buttons as allowed_controls.
  • Replace direct deferred grab_focus on the main menu start_button with Globals.ensure_initial_focus using the main menu buttons as allowed_controls.
  • Preserve existing visibility/tween behavior while routing all initial focus to the new helper for consistency and logging.
scripts/pause_menu.gd
scripts/main_menu.gd

Assessment against linked issues

Issue Objective Addressed Explanation
#388 Enable full keyboard (and D‑Pad) navigation for the Advanced Settings menu so that all interactive controls (toggles, sliders, selectors, buttons) can be reached and used without a mouse.
#388 Implement robust focus management for Advanced Settings: set a sensible initial focus when the menu opens, ensure clear focus ownership across its controls, and prevent focus loss when exiting back to parent menus.
#388 Maintain compatibility and usability across platforms and other menus (Main, Pause, Options), ensuring keyboard navigation remains stable and does not interfere with mouse input.
#395 Enable full D‑Pad/controller navigation for all Advanced Settings controls (all elements reachable and adjustable without mouse/analog input). The PR adds initial focus to the log level option and improves focus management, but it does not modify focus traversal within the Advanced Settings UI (e.g., focus neighbors, control properties, or scene structure). There are no changes ensuring that all toggles, sliders, and dropdowns that previously required analog stick or mouse input are now fully reachable and adjustable via D‑Pad/controller alone.
#395 Improve focus handling when entering and exiting Advanced Settings so keyboard and D‑Pad navigation works reliably across menus.

Possibly linked issues


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

@ikostan ikostan moved this to In Progress in Sky Lock Assault Project Feb 15, 2026
@ikostan ikostan linked an issue Feb 15, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Feb 15, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds centralized initial-focus logic and guarded/deferred focus grabs across menus; wires explicit focus neighbors in Advanced Settings scene and replaces several StyleBox resources with StyleBoxFlat variants. No public/exported API signature removals.

Changes

Cohort / File(s) Summary
Advanced Settings scene
scenes/advanced_settings.tscn
Replaced/updated StyleBox resources with StyleBoxFlat variants (bg_color, borders, corners, shadows); updated theme focus style refs and explicit focus_neighbor/focus_next/focus_previous on controls (LogLevelOptionButton, BackButton, ResetButton).
Advanced Settings script
scripts/advanced_settings.gd
In _ready, defer focus grab for the log level control via centralized helper when no focus owner; on Back pressed, after revealing previous menu, attempt deferred grab_focus() on originating OptionsMenu button with checks and logging.
Main menu script
scripts/main_menu.gd
Replaced unconditional Start-button grab_focus with guarded initialization using Globals.ensure_initial_focus and a candidate list to avoid stealing existing focus.
Options menu script
scripts/options_menu.gd
Added class_name OptionsMenu; _grab_first_button_focus now finds first visible/enabled Button and delegates focus decisions to Globals.ensure_initial_focus, logging when none found.
Pause menu script
scripts/pause_menu.gd
When toggling pause, delegates initial-focus decision to Globals.ensure_initial_focus (resume_button target and candidate list) instead of direct deferred grab_focus.
Globals helper
scripts/globals.gd
Added ensure_initial_focus(candidate, allowed_controls, context) to centralize validation, check existing focus among allowed controls, defer grab_focus on candidate, and emit contextual logs.
Tests
test/gut/test_pause_menu.gd
Removed Globals autoload mock; replaced with stubs for specific Globals functions; added input-action helpers (ensure_action, restore_action, create_action_event) and adjusted setup/teardown to manage input actions and paused state.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (keyboard/controller)
    participant AdvScene as AdvancedSettings (scene)
    participant AdvScript as AdvancedSettings.gd
    participant Globals as Globals.ensure_initial_focus
    participant PrevMenu as OptionsMenu
    participant Engine as GodotEngine

    User->>AdvScene: open Advanced Settings
    AdvScene->>AdvScript: _ready()
    AdvScript->>Globals: ensure_initial_focus(LogLevelControl, [Back,Reset], "Advanced Settings")
    Globals->>Engine: if no allowed control has focus -> defer candidate.grab_focus()
    Engine-->>Globals: grab scheduled / ignored

    User->>AdvScene: press BackButton
    AdvScript->>PrevMenu: reveal_previous_menu()
    AdvScript->>PrevMenu: find AdvancedSettingsButton
    alt AdvancedSettingsButton found
        AdvScript->>Globals: ensure_initial_focus(AdvancedSettingsButton, [Start,Options,Quit], "Options Menu")
        Globals->>Engine: defer AdvancedSettingsButton.grab_focus()
    else not found
        AdvScript->>Engine: log warning
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Suggested labels

testing

Poem

🐇 I hopped through menus, soft and spry,

I nudged the focus, let sliders try.
Shadows trimmed and borders bright,
Keys now find their way at night.
The rabbit pressed "Back"—all feels right.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Invalid branch name format
Description check ❓ Inconclusive PR description provided but lacks structured compliance with template; initial narrative is clear but template sections are incomplete or unchecked. Complete the template sections: provide clear issue reference in Related Issue, verify all Testing checkboxes have proper descriptions, and ensure Additional Notes section documents testing platform and any known limitations.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main objective of the PR: enabling keyboard and D-pad navigation for the advanced settings menu, which aligns with the primary changes across multiple menu scripts and the advanced settings scene.
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 docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch enable-keyboard-&-dpad-navigation-for-advanced-settings
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch enable-keyboard-&-dpad-navigation-for-advanced-settings
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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.

@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 1 issue, and left some high level feedback:

  • Consider guarding the log_lvl_option.call_deferred("grab_focus") call with a null/validity check to avoid potential runtime errors if the node is not found or renamed.
  • The hard-coded node path "Panel/OptionsVBoxContainer/AdvancedSettingsButton" couples focus logic tightly to the scene hierarchy; consider exposing this button via an exported NodePath or onready variable so hierarchy changes don’t silently break focus restoration.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider guarding the `log_lvl_option.call_deferred("grab_focus")` call with a null/validity check to avoid potential runtime errors if the node is not found or renamed.
- The hard-coded node path `"Panel/OptionsVBoxContainer/AdvancedSettingsButton"` couples focus logic tightly to the scene hierarchy; consider exposing this button via an exported NodePath or onready variable so hierarchy changes don’t silently break focus restoration.

## Individual Comments

### Comment 1
<location> `scripts/advanced_settings.gd:186-187` </location>
<code_context>
 			Globals.log_message("Showing menu: " + prev_menu.name, Globals.LogLevel.DEBUG)
 			hidden_menu_found = true
+
+			# Focus on AdvancedSettingsButton
+			var advanced_btn: Button = prev_menu.get_node(
+				"Panel/OptionsVBoxContainer/AdvancedSettingsButton"
+			)
</code_context>

<issue_to_address>
**issue (bug_risk):** Using `get_node` here can throw if the button path changes or is missing; consider a safer lookup.

Since `get_node` will error if `Panel/OptionsVBoxContainer/AdvancedSettingsButton` is missing, this can fail before `is_instance_valid` runs. To harden this, use `get_node_or_null` and check for null (or otherwise guard the lookup) so the handler still works if the path changes or the button isn’t present.
</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/advanced_settings.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: 1

🤖 Fix all issues with AI agents
In `@scenes/advanced_settings.tscn`:
- Around line 167-168: The controls currently only set focus_next and
focus_previous (e.g., focus_next = NodePath("../../BtnContainer/BackButton" and
focus_previous = NodePath("../../BtnContainer/ResetButton")) which only affects
Tab navigation; add explicit D‑Pad/arrow navigation properties by setting
focus_neighbor_top, focus_neighbor_bottom, focus_neighbor_left, and
focus_neighbor_right for the same UI controls so the engine uses deterministic
neighbors instead of spatial auto-detection—use the same NodePath targets
(BtnContainer/BackButton, BtnContainer/ResetButton, and any adjacent buttons
referenced at the other locations called out in the comment) when assigning each
focus_neighbor_* property.
🧹 Nitpick comments (2)
scenes/advanced_settings.tscn (1)

72-86: Duplicate StyleBoxFlat resources: StyleBoxFlat_cfdt5 and StyleBoxFlat_itopb are identical.

Both sub-resources have the same bg_color, corner_radius_*, and shadow_size values. They can be consolidated into a single resource referenced by both BackButton and ResetButton, reducing scene bloat.

scripts/advanced_settings.gd (1)

185-194: Use get_node_or_null instead of get_node to avoid error noise when the path is missing.

If prev_menu doesn't have the expected node hierarchy, get_node() will print an error to the console even though is_instance_valid handles null gracefully on the next line. get_node_or_null() avoids the spurious error output.

Suggested fix
 			# Focus on AdvancedSettingsButton
-			var advanced_btn: Button = prev_menu.get_node(
+			var advanced_btn: Button = prev_menu.get_node_or_null(
 				"Panel/OptionsVBoxContainer/AdvancedSettingsButton"
 			)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf3379 and 966662a.

📒 Files selected for processing (2)
  • scenes/advanced_settings.tscn
  • scripts/advanced_settings.gd
⏰ 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 comments (1)
scripts/advanced_settings.gd (1)

88-89: LGTM — deferred focus grab ensures the UI is ready before focusing.

Using call_deferred is the correct approach for grabbing focus during _ready.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread scenes/advanced_settings.tscn
focus_next/focus_previous only control Tab/Shift+Tab navigation. D-Pad and arrow key navigation in Godot 4 requires explicit focus_neighbor_top, focus_neighbor_bottom, focus_neighbor_left, and focus_neighbor_right properties. Without them, the engine falls back to spatial auto-detection, which may not produce the intended focus cycle.
…anges or is missing; consider a safer lookup.

Since get_node will error if Panel/OptionsVBoxContainer/AdvancedSettingsButton is missing, this can fail before is_instance_valid runs. To harden this, use get_node_or_null and check for null (or otherwise guard the lookup) so the handler still works if the path changes or the button isn’t present.
Consider guarding the log_lvl_option.call_deferred("grab_focus") call with a null/validity check to avoid potential runtime errors if the node is not found or renamed.
@ikostan

ikostan commented Feb 15, 2026

Copy link
Copy Markdown
Owner Author

Hey - I've found 1 issue, and left some high level feedback:

  • Consider guarding the log_lvl_option.call_deferred("grab_focus") call with a null/validity check to avoid potential runtime errors if the node is not found or renamed.
  • The hard-coded node path "Panel/OptionsVBoxContainer/AdvancedSettingsButton" couples focus logic tightly to the scene hierarchy; consider exposing this button via an exported NodePath or onready variable so hierarchy changes don’t silently break focus restoration.

Prompt for AI Agents

Please address the comments from this code review:

## Overall Comments
- Consider guarding the `log_lvl_option.call_deferred("grab_focus")` call with a null/validity check to avoid potential runtime errors if the node is not found or renamed.
- The hard-coded node path `"Panel/OptionsVBoxContainer/AdvancedSettingsButton"` couples focus logic tightly to the scene hierarchy; consider exposing this button via an exported NodePath or onready variable so hierarchy changes don’t silently break focus restoration.

## Individual Comments

### Comment 1
<location> `scripts/advanced_settings.gd:186-187` </location>
<code_context>
 			Globals.log_message("Showing menu: " + prev_menu.name, Globals.LogLevel.DEBUG)
 			hidden_menu_found = true
+
+			# Focus on AdvancedSettingsButton
+			var advanced_btn: Button = prev_menu.get_node(
+				"Panel/OptionsVBoxContainer/AdvancedSettingsButton"
+			)
</code_context>

<issue_to_address>
**issue (bug_risk):** Using `get_node` here can throw if the button path changes or is missing; consider a safer lookup.

Since `get_node` will error if `Panel/OptionsVBoxContainer/AdvancedSettingsButton` is missing, this can fail before `is_instance_valid` runs. To harden this, use `get_node_or_null` and check for null (or otherwise guard the lookup) so the handler still works if the path changes or the button isn’t present.
</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.

All addressed

@ikostan

ikostan commented Feb 15, 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 1 issue, and left some high level feedback:

  • The focus management for advanced_btn relies on a hard-coded node path ("Panel/OptionsVBoxContainer/AdvancedSettingsButton"); consider using an exported NodePath or a more robust lookup mechanism so UI refactors don’t silently break focus restoration.
  • The new warning logs for missing log_lvl_option and AdvancedSettingsButton might be noisy in normal play; if these are non-critical, consider downgrading them to DEBUG or gating them behind a verbose flag.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The focus management for `advanced_btn` relies on a hard-coded node path (`"Panel/OptionsVBoxContainer/AdvancedSettingsButton"`); consider using an exported `NodePath` or a more robust lookup mechanism so UI refactors don’t silently break focus restoration.
- The new warning logs for missing `log_lvl_option` and `AdvancedSettingsButton` might be noisy in normal play; if these are non-critical, consider downgrading them to DEBUG or gating them behind a verbose flag.

## Individual Comments

### Comment 1
<location> `scripts/advanced_settings.gd:191` </location>
<code_context>
 			Globals.log_message("Showing menu: " + prev_menu.name, Globals.LogLevel.DEBUG)
 			hidden_menu_found = true
+
+			# Focus on AdvancedSettingsButton
+			var advanced_btn: Button = prev_menu.get_node_or_null(
+				"Panel/OptionsVBoxContainer/AdvancedSettingsButton"
</code_context>

<issue_to_address>
**suggestion:** Hard-coded node path for the AdvancedSettingsButton may be brittle across scene/layout changes.

This hard-coded path couples the script to the exact scene hierarchy, so any UI restructure (renames, reparenting) will break focus without errors. Prefer wiring this via an exported NodePath, a group, or an onready var set from the scene to keep it stable across layout changes.

Suggested implementation:

```
			prev_menu.visible = true
			Globals.log_message("Showing menu: " + prev_menu.name, Globals.LogLevel.DEBUG)
			hidden_menu_found = true

			# Focus on AdvancedSettingsButton
			var advanced_btn: Button = null

			# Prefer a configurable NodePath (exported/onready) if available, to avoid coupling
			# to a specific scene hierarchy. Fall back to the legacy hard-coded path if needed.
			if typeof(advanced_settings_button_path) == TYPE_NODE_PATH \
			and String(advanced_settings_button_path) != "" \
			and prev_menu.has_node(advanced_settings_button_path):
				advanced_btn = prev_menu.get_node(advanced_settings_button_path)
			else:
				advanced_btn = prev_menu.get_node_or_null(
					"Panel/OptionsVBoxContainer/AdvancedSettingsButton"
				)

			if is_instance_valid(advanced_btn):
				advanced_btn.call_deferred("grab_focus")
				Globals.log_message(
					"Focused on AdvancedSettingsButton after back.", Globals.LogLevel.DEBUG
				)
			else:
				Globals.log_message(
					"AdvancedSettingsButton not found—skipping focus.", Globals.LogLevel.WARNING
				)

```

To fully implement the suggestion and make this robust, you should also:

1. Declare a configurable `NodePath` in `scripts/advanced_settings.gd` (near the top of the script):
   - For Godot 4.x:
     - `@export var advanced_settings_button_path: NodePath`
   - For Godot 3.x:
     - `export(NodePath) var advanced_settings_button_path`

2. In the scene(s) using this script, assign `advanced_settings_button_path` to point to the AdvancedSettingsButton node **relative to `prev_menu`** (i.e., the menu you are returning to).  
   For example, if the button is now under `Buttons/AdvancedSettingsButton`, set the property to `Buttons/AdvancedSettingsButton`.

3. Once all scenes are updated to use the exported path, you may optionally remove the fallback hard-coded path block to fully decouple the script from the old layout.
</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/advanced_settings.gd Outdated
…e brittle across scene/layout changes.

This hard-coded path couples the script to the exact scene hierarchy, so any UI restructure (renames, reparenting) will break focus without errors. Prefer wiring this via an exported NodePath, a group, or an onready var set from the scene to keep it stable across layout changes.
@ikostan

ikostan commented Feb 15, 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 left some high level feedback:

  • In _on_advanced_back_button_pressed, the warning "AdvancedSettingsButton not found—skipping focus." will also trigger when prev_menu is not an OptionsMenu; consider only logging a warning when prev_menu is OptionsMenu to avoid noisy logs in other navigation flows.
  • The initial focus on log_lvl_option in _ready() is deferred correctly, but you might want to guard this behavior behind a flag or check whether another control already has meaningful focus to avoid unexpectedly overriding user-driven focus when the menu is reopened.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_on_advanced_back_button_pressed`, the warning `"AdvancedSettingsButton not found—skipping focus."` will also trigger when `prev_menu` is not an `OptionsMenu`; consider only logging a warning when `prev_menu is OptionsMenu` to avoid noisy logs in other navigation flows.
- The initial focus on `log_lvl_option` in `_ready()` is deferred correctly, but you might want to guard this behavior behind a flag or check whether another control already has meaningful focus to avoid unexpectedly overriding user-driven focus when the menu is reopened.

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.

In _on_advanced_back_button_pressed, the warning "AdvancedSettingsButton not found—skipping focus." will also trigger when prev_menu is not an OptionsMenu; consider only logging a warning when prev_menu is OptionsMenu to avoid noisy logs in other navigation flows.
Check for an existing GUI focus owner before calling grab_focus in multiple menus (advanced_settings.gd, main_menu.gd, options_menu.gd, pause_menu.gd). Add debug/warning logs when focus is already set or when target controls are missing, and return early where appropriate to avoid overriding user or system focus. Minor comment/whitespace cleanups included.

The initial focus on log_lvl_option in _ready() is deferred correctly, but you might want to guard this behavior behind a flag or check whether another control already has meaningful focus to avoid unexpectedly overriding user-driven focus when the menu is reopened.

@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

🤖 Fix all issues with AI agents
In `@scripts/advanced_settings.gd`:
- Around line 88-97: The comment "Give keyboard focus to the log level slider"
is misleading because the control is an OptionButton; update the comment near
the block that references log_lvl_option (and related calls
get_viewport().gui_get_focus_owner() and
log_lvl_option.call_deferred("grab_focus")) to say something like "Give keyboard
focus to the log level OptionButton" or similar to accurately reflect the
control type.
🧹 Nitpick comments (2)
scripts/pause_menu.gd (1)

69-74: Unnecessary return on line 74.

The else branch's return on line 74 has no effect — there is no code after this if/else block in toggle_pause(). It's harmless but slightly misleading, as it suggests there's subsequent logic being skipped.

Suggested cleanup
 	if visible and is_instance_valid(resume_button):
 		if get_viewport().gui_get_focus_owner() == null:
 			resume_button.call_deferred("grab_focus")
 			Globals.log_message("Grabbed initial focus on: resume_button" , Globals.LogLevel.DEBUG)
 		else:
 			Globals.log_message("Focus already set—skipping initial grab.", Globals.LogLevel.DEBUG)
-			return
scripts/options_menu.gd (1)

135-143: Hoist the focus-owner check out of the loop.

gui_get_focus_owner() is evaluated on every loop iteration but its result won't change within this synchronous function. Moving the check before the for loop avoids redundant viewport queries and makes the intent clearer.

Suggested refactor
 func _grab_first_button_focus() -> void:
+	if get_viewport().gui_get_focus_owner() != null:
+		Globals.log_message("Focus already set—skipping initial grab.", Globals.LogLevel.DEBUG)
+		return
 	for child in options_vbox.get_children():
-		if get_viewport().gui_get_focus_owner() == null:
-			if child is Button and child.visible and not child.disabled:
-				child.grab_focus()
-				Globals.log_message("Grabbed initial focus on: " + child.name, Globals.LogLevel.DEBUG)
-				return
-		else:
-			Globals.log_message("Focus already set—skipping initial grab.", Globals.LogLevel.DEBUG)
+		if child is Button and child.visible and not child.disabled:
+			child.grab_focus()
+			Globals.log_message("Grabbed initial focus on: " + child.name, Globals.LogLevel.DEBUG)
 			return
 	Globals.log_message(
 		"No Button found in OptionsVBoxContainer for initial focus!", Globals.LogLevel.WARNING
 	)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc6887b and 73d64a3.

📒 Files selected for processing (4)
  • scripts/advanced_settings.gd
  • scripts/main_menu.gd
  • scripts/options_menu.gd
  • scripts/pause_menu.gd
🧰 Additional context used
🪛 GitHub Actions: Pull Request Pipeline
scripts/pause_menu.gd

[error] 1-1: gdformat formatting check failed. Code would be reformatted by 'gdformat --diff --check'. Run 'gdformat --write' to fix.

scripts/options_menu.gd

[error] 1-1: gdformat formatting check failed. Code would be reformatted by 'gdformat --diff --check'. Run 'gdformat --write' to fix.

scripts/main_menu.gd

[error] 1-1: gdformat formatting check failed. Code would be reformatted by 'gdformat --diff --check'. Run 'gdformat --write' to fix.

🔇 Additional comments (3)
scripts/main_menu.gd (1)

75-83: LGTM — guarded focus logic is consistent with the project-wide pattern.

The deferred grab_focus with null-check on gui_get_focus_owner() and the is_instance_valid guard are well-structured. The warning log when start_button is missing is helpful for debugging.

scripts/options_menu.gd (1)

15-15: LGTM — class_name OptionsMenu enables type-safe is checks in other scripts.

This is used by advanced_settings.gd (prev_menu is OptionsMenu) to safely access advanced_settings_button, which is a clean approach.

scripts/advanced_settings.gd (1)

194-207: LGTM — well-guarded back-navigation focus logic.

The is OptionsMenu type check, direct member access to advanced_settings_button, validity guard, and deferred grab_focus with logging all look solid. This properly addresses the reviewer's concern about hard-coded node paths.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread scripts/advanced_settings.gd Outdated
ikostan and others added 2 commits February 14, 2026 22:02
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Refine initial focus logic in multiple menu scripts (advanced_settings.gd, main_menu.gd, options_menu.gd, pause_menu.gd). Replace direct null checks with a guarded focus_owner inspection (is_instance_valid) and per-menu whitelists so we only grab focus when the viewport owner isn't already one of this menu's interactive controls. Add debug log messages when focus is grabbed or skipped and tidy up some log text/formatting.
@ikostan

ikostan commented Feb 15, 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 1 issue, and left some high level feedback:

  • The focus-initialization logic (checking gui_get_focus_owner, already_has_focus, and logging) is duplicated across advanced_settings.gd, options_menu.gd, main_menu.gd, and pause_menu.gd; consider extracting this into a small shared helper function to keep the behavior consistent and easier to maintain.
  • In _grab_first_button_focus you recompute focus_owner and potentially early-return inside the loop for every child; it would be clearer and less error-prone to determine whether focus is already on one of the menu buttons once before iterating, then either immediately return or run a straightforward loop to find the first focusable button.
  • In options_menu.gd the key_mapping_button is listed twice in the focus-owner checks; clean this up to avoid confusion and make it clearer which controls are considered part of the focus group.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The focus-initialization logic (checking `gui_get_focus_owner`, `already_has_focus`, and logging) is duplicated across `advanced_settings.gd`, `options_menu.gd`, `main_menu.gd`, and `pause_menu.gd`; consider extracting this into a small shared helper function to keep the behavior consistent and easier to maintain.
- In `_grab_first_button_focus` you recompute `focus_owner` and potentially early-return inside the loop for every child; it would be clearer and less error-prone to determine whether focus is already on one of the menu buttons once before iterating, then either immediately return or run a straightforward loop to find the first focusable button.
- In `options_menu.gd` the `key_mapping_button` is listed twice in the focus-owner checks; clean this up to avoid confusion and make it clearer which controls are considered part of the focus group.

## Individual Comments

### Comment 1
<location> `scripts/main_menu.gd:75-84` </location>
<code_context>
 	# Fallback: Grab focus immediately if tween isn't running (e.g., error or instant)
 	# Give keyboard focus to the first button after the fade-in completes
-	start_button.call_deferred("grab_focus")
+	if is_instance_valid(start_button):
+		var focus_owner := get_viewport().gui_get_focus_owner()
+
+		var already_has_focus := false
+		if is_instance_valid(focus_owner):
+			# Only skip if focus is already on one of our own interactive controls
+			if (
+				focus_owner == start_button
+				or focus_owner == options_button
+				or focus_owner == quit_button
+			):
+				already_has_focus = true
+
+		if not already_has_focus:
+			start_button.call_deferred("grab_focus")
+			Globals.log_message("Grabbed initial focus on start_button", Globals.LogLevel.DEBUG)
+		else:
+			Globals.log_message("Focus already set—skipping initial grab.", Globals.LogLevel.DEBUG)
+	else:
+		Globals.log_message(
</code_context>

<issue_to_address>
**suggestion:** The focus-handling pattern is duplicated across multiple menus and could be centralized.

Very similar focus-guard logic now appears in `main_menu.gd`, `pause_menu.gd`, `advanced_settings.gd`, and `options_menu.gd`. This duplication will make it harder to keep focus behavior consistent and to adjust it later.

Consider extracting a shared helper (e.g., in `Globals` or a UI utility) that:
- accepts the candidate control, the allowed focus-owner controls, and optional log context,
- checks whether focus is already on one of those controls, and
- otherwise calls `call_deferred("grab_focus")` and logs.

Centralizing this logic would reduce duplication and make future focus-policy changes safer.

Suggested implementation:

```
		Globals.log_message("Invalid tween—grabbing focus immediately.", Globals.LogLevel.WARNING)
	# Fallback: Grab focus immediately if tween isn't running (e.g., error or instant)
	# Give keyboard focus to the first button after the fade-in completes
	Globals.ensure_initial_focus(
		start_button,
		[start_button, options_button, quit_button],
		"main_menu"
	)

	# Connect START button signal
	@warning_ignore("return_value_discarded")
	start_button.pressed.connect(_on_start_pressed)

```

To fully implement the refactor:

1. Add a shared helper (e.g. in `Globals.gd` or a dedicated UI utility), something like:
   ```gdscript
   static func ensure_initial_focus(candidate: Control, allowed_controls: Array, context: String = "") -> void:
       if not is_instance_valid(candidate):
           Globals.log_message(
               "Button %s not found—skipping focus. (%s)".sprintf([str(candidate), context]),
               Globals.LogLevel.WARNING
           )
           return

       var focus_owner := candidate.get_viewport().gui_get_focus_owner()
       var already_has_focus := false

       if is_instance_valid(focus_owner) and focus_owner in allowed_controls:
           already_has_focus = true

       if not already_has_focus:
           candidate.call_deferred("grab_focus")
           Globals.log_message(
               "Grabbed initial focus on %s (%s)".sprintf([candidate.name, context]),
               Globals.LogLevel.DEBUG
           )
       else:
           Globals.log_message(
               "Focus already set—skipping initial grab. (%s)".sprintf([context]),
               Globals.LogLevel.DEBUG
           )
   ```
   Adjust exact logging text/levels to match your conventions.

2. Update the other scripts you mentioned (`pause_menu.gd`, `advanced_settings.gd`, `options_menu.gd`, etc.) to replace their duplicated focus-guard blocks with `Globals.ensure_initial_focus(...)`, passing the appropriate candidate button and allowed-controls list for each menu.
</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/main_menu.gd Outdated
In options_menu.gd the key_mapping_button is listed twice in the focus-owner checks; clean this up to avoid confusion and make it clearer which controls are considered part of the focus group.
In _grab_first_button_focus you recompute focus_owner and potentially early-return inside the loop for every child; it would be clearer and less error-prone to determine whether focus is already on one of the menu buttons once before iterating, then either immediately return or run a straightforward loop to find the first focusable button.
…menus and could be centralized.

Very similar focus-guard logic now appears in main_menu.gd, pause_menu.gd, advanced_settings.gd, and options_menu.gd. This duplication will make it harder to keep focus behavior consistent and to adjust it later.

Consider extracting a shared helper (e.g., in Globals or a UI utility) that:

accepts the candidate control, the allowed focus-owner controls, and optional log context,
checks whether focus is already on one of those controls, and
otherwise calls call_deferred("grab_focus") and logs.
Centralizing this logic would reduce duplication and make future focus-policy changes safer.
@ikostan

ikostan commented Feb 15, 2026

Copy link
Copy Markdown
Owner Author

Hey - I've found 1 issue, and left some high level feedback:

  • The focus-initialization logic (checking gui_get_focus_owner, already_has_focus, and logging) is duplicated across advanced_settings.gd, options_menu.gd, main_menu.gd, and pause_menu.gd; consider extracting this into a small shared helper function to keep the behavior consistent and easier to maintain.
  • In _grab_first_button_focus you recompute focus_owner and potentially early-return inside the loop for every child; it would be clearer and less error-prone to determine whether focus is already on one of the menu buttons once before iterating, then either immediately return or run a straightforward loop to find the first focusable button.
  • In options_menu.gd the key_mapping_button is listed twice in the focus-owner checks; clean this up to avoid confusion and make it clearer which controls are considered part of the focus group.

Prompt for AI Agents

Please address the comments from this code review:

## Overall Comments
- The focus-initialization logic (checking `gui_get_focus_owner`, `already_has_focus`, and logging) is duplicated across `advanced_settings.gd`, `options_menu.gd`, `main_menu.gd`, and `pause_menu.gd`; consider extracting this into a small shared helper function to keep the behavior consistent and easier to maintain.
- In `_grab_first_button_focus` you recompute `focus_owner` and potentially early-return inside the loop for every child; it would be clearer and less error-prone to determine whether focus is already on one of the menu buttons once before iterating, then either immediately return or run a straightforward loop to find the first focusable button.
- In `options_menu.gd` the `key_mapping_button` is listed twice in the focus-owner checks; clean this up to avoid confusion and make it clearer which controls are considered part of the focus group.

## Individual Comments

### Comment 1
<location> `scripts/main_menu.gd:75-84` </location>
<code_context>
 	# Fallback: Grab focus immediately if tween isn't running (e.g., error or instant)
 	# Give keyboard focus to the first button after the fade-in completes
-	start_button.call_deferred("grab_focus")
+	if is_instance_valid(start_button):
+		var focus_owner := get_viewport().gui_get_focus_owner()
+
+		var already_has_focus := false
+		if is_instance_valid(focus_owner):
+			# Only skip if focus is already on one of our own interactive controls
+			if (
+				focus_owner == start_button
+				or focus_owner == options_button
+				or focus_owner == quit_button
+			):
+				already_has_focus = true
+
+		if not already_has_focus:
+			start_button.call_deferred("grab_focus")
+			Globals.log_message("Grabbed initial focus on start_button", Globals.LogLevel.DEBUG)
+		else:
+			Globals.log_message("Focus already set—skipping initial grab.", Globals.LogLevel.DEBUG)
+	else:
+		Globals.log_message(
</code_context>

<issue_to_address>
**suggestion:** The focus-handling pattern is duplicated across multiple menus and could be centralized.

Very similar focus-guard logic now appears in `main_menu.gd`, `pause_menu.gd`, `advanced_settings.gd`, and `options_menu.gd`. This duplication will make it harder to keep focus behavior consistent and to adjust it later.

Consider extracting a shared helper (e.g., in `Globals` or a UI utility) that:
- accepts the candidate control, the allowed focus-owner controls, and optional log context,
- checks whether focus is already on one of those controls, and
- otherwise calls `call_deferred("grab_focus")` and logs.

Centralizing this logic would reduce duplication and make future focus-policy changes safer.

Suggested implementation:

  Globals.log_message("Invalid tween—grabbing focus immediately.", Globals.LogLevel.WARNING)

Fallback: Grab focus immediately if tween isn't running (e.g., error or instant)

Give keyboard focus to the first button after the fade-in completes

Globals.ensure_initial_focus(
start_button,
[start_button, options_button, quit_button],
"main_menu"
)

Connect START button signal

@warning_ignore("return_value_discarded")
start_button.pressed.connect(_on_start_pressed)


To fully implement the refactor:

1. Add a shared helper (e.g. in `Globals.gd` or a dedicated UI utility), something like:
   ```gdscript
   static func ensure_initial_focus(candidate: Control, allowed_controls: Array, context: String = "") -> void:
       if not is_instance_valid(candidate):
           Globals.log_message(
               "Button %s not found—skipping focus. (%s)".sprintf([str(candidate), context]),
               Globals.LogLevel.WARNING
           )
           return

       var focus_owner := candidate.get_viewport().gui_get_focus_owner()
       var already_has_focus := false

       if is_instance_valid(focus_owner) and focus_owner in allowed_controls:
           already_has_focus = true

       if not already_has_focus:
           candidate.call_deferred("grab_focus")
           Globals.log_message(
               "Grabbed initial focus on %s (%s)".sprintf([candidate.name, context]),
               Globals.LogLevel.DEBUG
           )
       else:
           Globals.log_message(
               "Focus already set—skipping initial grab. (%s)".sprintf([context]),
               Globals.LogLevel.DEBUG
           )

Adjust exact logging text/levels to match your conventions.

  1. Update the other scripts you mentioned (pause_menu.gd, advanced_settings.gd, options_menu.gd, etc.) to replace their duplicated focus-guard blocks with Globals.ensure_initial_focus(...), passing the appropriate candidate button and allowed-controls list for each menu.
    </issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
* [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
* [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
* [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
* [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

All addressed

@ikostan

ikostan commented Feb 15, 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 1 issue, and left some high level feedback:

  • The ensure_initial_focus helper only considers focus already being on one of the explicit allowed_controls nodes; if focus is on a child/descendant (e.g., a button inside a container that isn’t listed), it will still steal focus, so consider taking a parent container as input or checking is_ancestor_of(focus_owner) to avoid overriding valid in-menu focus.
  • In _on_advanced_back_button_pressed you directly grab_focus on advanced_settings_button instead of going through ensure_initial_focus, which means this path can behave differently from the other menus’ focus logic; consider reusing the helper for consistency or documenting why this path should always force focus.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `ensure_initial_focus` helper only considers focus already being on one of the explicit `allowed_controls` nodes; if focus is on a child/descendant (e.g., a button inside a container that isn’t listed), it will still steal focus, so consider taking a parent container as input or checking `is_ancestor_of(focus_owner)` to avoid overriding valid in-menu focus.
- In `_on_advanced_back_button_pressed` you directly `grab_focus` on `advanced_settings_button` instead of going through `ensure_initial_focus`, which means this path can behave differently from the other menus’ focus logic; consider reusing the helper for consistency or documenting why this path should always force focus.

## Individual Comments

### Comment 1
<location> `scripts/pause_menu.gd:69-70` </location>
<code_context>
 				"_on_advanced_reset_js", "advancedResetPressed"
 			)
+	# Give keyboard focus to the log level slider (only if nothing in this menu already has focus)
+	Globals.ensure_initial_focus(
+		log_lvl_option,
+		[log_lvl_option, advanced_back_button, advanced_reset_button],
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid calling the focus helper when the pause menu is being hidden.

This block used to run only when `visible` was true, but now `ensure_initial_focus` is called on every `toggle_pause()`, including when hiding the menu. That can wrongly move focus to `resume_button` while unpausing, interfering with gameplay input. Please guard this call with `if visible:` (after toggling) or pass `visible` into the helper so it no-ops when the menu is hidden.
</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/pause_menu.gd Outdated
Replace the MockGlobals autoload node with method stubs to prevent side effects in pause_menu tests. Removed MockGlobals class and original_globals handling; added stub calls for Globals.log_message, Globals.load_scene_with_loading, and Globals.load_options in before_each. Updated comments and simplified after_each cleanup to no longer manage the mock Globals node.
…is being hidden.

This block used to run only when visible was true, but now ensure_initial_focus is called on every toggle_pause(), including when hiding the menu. That can wrongly move focus to resume_button while unpausing, interfering with gameplay input. Please guard this call with if visible: (after toggling) or pass visible into the helper so it no-ops when the menu is hidden.

@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

🤖 Fix all issues with AI agents
In `@scripts/pause_menu.gd`:
- Around line 66-71: The toggle_pause flow currently flips visible and always
calls Globals.ensure_initial_focus which runs even when hiding the menu; change
toggle_pause so that after flipping visible and setting get_tree().paused you
only call Globals.ensure_initial_focus(resume_button, [back_to_main_button,
options_button, resume_button], "Pause Menu") when visible is true (i.e., the
menu was just shown), leaving focus untouched when the menu is being
hidden/unpaused.
🧹 Nitpick comments (3)
scripts/globals.gd (1)

36-72: Well-structured centralized focus helper.

The implementation cleanly validates the candidate, checks existing focus ownership, and defers the grab. Good use of contextual logging.

One minor simplification: on Line 54, since get_tree().root (a Window) already extends Viewport, calling .get_viewport() on it is redundant. You could use get_viewport().gui_get_focus_owner() directly.

Simplify viewport access
-	var focus_owner: Control = get_tree().root.get_viewport().gui_get_focus_owner()
+	var focus_owner: Control = get_viewport().gui_get_focus_owner()
scripts/options_menu.gd (1)

129-155: Clean delegation to centralized focus helper.

The logic correctly finds the first visible/enabled Button and delegates to ensure_initial_focus. One small observation: the allowed_controls list on Lines 143-148 is hardcoded separately from the dynamic loop on Line 135. If a button is added to the container but not to this list, ensure_initial_focus would override existing focus on it. Consider building allowed_controls dynamically from the same loop to stay in sync, though this is low risk given the current structure.

test/gut/test_pause_menu.gd (1)

62-77: String-based dynamic variable access is fragile.

ensure_action uses set(added_var_name, ...) and set(events_var_name, ...) with string-based variable names. A typo in the caller (e.g., "added_pausee") would silently create a new property via set() rather than failing, making bugs hard to track.

This is acceptable for a small test helper, but consider using a Dictionary keyed by action name instead if the pattern grows.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f036bac and 0c13fb4.

📒 Files selected for processing (6)
  • scripts/advanced_settings.gd
  • scripts/globals.gd
  • scripts/main_menu.gd
  • scripts/options_menu.gd
  • scripts/pause_menu.gd
  • test/gut/test_pause_menu.gd
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/main_menu.gd
⏰ 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 comments (3)
scripts/advanced_settings.gd (1)

190-203: Clean focus restoration with proper type guard.

Using prev_menu is OptionsMenu before accessing advanced_settings_button is safe and avoids noisy warnings when navigating back to non-Options menus. Good improvement over hard-coded node paths.

scripts/options_menu.gd (1)

15-15: Good: Exposing class_name OptionsMenu enables safe type checks.

This allows advanced_settings.gd to use prev_menu is OptionsMenu instead of brittle string/path-based checks.

test/gut/test_pause_menu.gd (1)

38-40: GUT's stub() successfully intercepts calls on autoload singletons — verified by working tests. The stubs on Globals.log_message, Globals.load_scene_with_loading, and Globals.load_options are effective in preventing side effects.

Note: Globals.ensure_initial_focus does run live during toggle_pause() calls (PM-03/04/05), but its logging output is already suppressed by the log_message() stub, and its other operations (grab_focus, gui_get_focus_owner) are safe for tests. This is intentional integration-style testing, not an oversight.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread scripts/pause_menu.gd Outdated
In _on_advanced_back_button_pressed you directly grab_focus on advanced_settings_button instead of going through ensure_initial_focus, which means this path can behave differently from the other menus’ focus logic; consider reusing the helper for consistency or documenting why this path should always force focus.
Perform a comprehensive review of Milestone 11 upon its completion, and update the project's README.md file to reflect the delivered content. This ensures the documentation accurately captures the progress in this Godot 4.5 learning project on Windows 10 64-bit, highlighting new features, refactors, and improvements for better readability and onboarding.
@ikostan ikostan linked an issue Feb 15, 2026 that may be closed by this pull request
@ikostan

ikostan commented Feb 15, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai summary

@ikostan
ikostan merged commit 787c699 into main Feb 15, 2026
11 checks passed
@ikostan
ikostan deleted the enable-keyboard-&-dpad-navigation-for-advanced-settings branch February 15, 2026 07:55
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Sky Lock Assault Project Feb 15, 2026
@ikostan
ikostan restored the enable-keyboard-&-dpad-navigation-for-advanced-settings branch February 15, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment