Skip to content

Debug Settings Panel — Design

A pause-menu screen exposing individual on/off toggles for graphics effects and audio (music/SFX), so a specific effect or sound category can be isolated during investigation (e.g. bisecting the cause of a performance hitch) without guessing or writing one-off code each time. Built as a dev tool for now — gated off in every real device build — but architected so the underlying toggle plumbing can be reused when this becomes a real player-facing settings screen later. The UI itself stays minimal/functional; it is not being polished for players yet.

  • No player-facing polish (NeonTheme styling, animations) — functional checkboxes only.
  • No persistence — every toggle resets to “everything on” each new process launch.
  • No reconciliation between this panel and the existing F4 tier-cycling override (see “Known interaction with F4” below) — accepted as a documented quirk, not engineered around.
  • No toggle for the “UI” audio bus (nav/select/buy blips) — only Music and SFX, per Chris’s ask. Trivial to add later if wanted.

A new “Debug Settings” button on ui/pause_menu.gd, gated by BuildConfig.dev_tools() (OS.has_feature("editor")) — the exact same gate already used for “Arm Remote Control” on the same menu. This guarantees the panel is invisible in every exported build (device dev-installs and App Store submissions alike), matching BuildConfig’s stated contract.

main.gd wires the button’s signal (debug_settings_requested, mirroring the existing arm_remote_requested pattern) to show a new DebugSettingsPanel CanvasLayer, passing it a reference to the already-existing quality_manager instance (no new wiring needed beyond that single reference — AudioServer is a global singleton, needs no reference).

class_name DebugSettingsPanel extends CanvasLayer. Follows the same tvOS-safe explicit debounced-nav pattern already established by PauseMenu/LevelUpPanel/StartMenu (dpad via MenuNav.is_down/is_up + JOY_BUTTON_A confirm to toggle the focused row) — plain default-focus Godot CheckBox/Button nodes are NOT reliable on the Apple TV’s Siri Remote per this project’s established gotcha, so toggling must go through the same manual focus/confirm loop as every other tvOS-safe menu here, not a native CheckBox.

Each row is a label + on/off indicator (reusing the neon glow / text-color-flip convention already used elsewhere for showing “on” vs “off” state, e.g. the drone-ready pop or the arm button’s “armed” state swap) rather than a literal CheckBox node.

Graphics toggles (8) — direct pass-through to existing QualityManager-bound nodes

Section titled “Graphics toggles (8) — direct pass-through to existing QualityManager-bound nodes”

QualityManager already exposes null-safe references to every render node its own tier system (_apply()) drives: world_env, halo_targets: Array, fx_layer, damage_numbers, zone_renderer, screen_fx, arena_bg, player_visual. The panel calls these same setters directly — no new render-side code is needed since QualityManager’s own tier system already proved each one is an independently safe, null-guarded knob:

Toggle label Underlying call Note
Bloom world_env.environment.glow_enabled = v direct bool
Halos for h in halo_targets: h.set_halo_visible(v) direct bool, iterates array
Arena Grid Detail arena_bg.set_low(!v) inverted — set_low(true) means LOW detail
Damage Numbers damage_numbers.enabled = v direct bool
Screen FX (vignette/shake) screen_fx.enabled = v direct bool
Zone Detail zone_renderer.low_detail = !v inverted — low_detail=true means LOW detail
Juice FX (particles/deaths/reactions) fx_layer.enabled = v direct bool
Player Visual Detail player_visual.low_detail = !v inverted — low_detail=true means LOW detail

The panel presents every checkbox with the natural sense (“ON” = full/normal quality), doing the inversion internally for the three fields whose own name is phrased as “low detail” — so the UI never shows an inverted-feeling toggle.

Each call is wrapped in a null-check (if quality_manager.world_env: ... etc.) matching the existing null-safe-knob convention QualityManager itself follows, since the panel could technically be opened before a run has fully bound (defensive, not expected to trigger live).

Audio toggles (2) — direct bus mute, no AudioManager changes

Section titled “Audio toggles (2) — direct bus mute, no AudioManager changes”

Confirmed via audio/bus_layout.tres: the project already has separate "Music" and "SFX" buses (plus "UI", out of scope here). The panel toggles these directly:

AudioServer.set_bus_mute(AudioServer.get_bus_index("Music"), !v)
AudioServer.set_bus_mute(AudioServer.get_bus_index("SFX"), !v)

On open, each checkbox reflects the CURRENT mute state via AudioServer.is_bus_mute(idx), so re-opening the panel mid-session shows accurate state rather than always defaulting to “on.” No changes to audio/audio_manager.gd are needed — bus routing there is unaffected by a bus-level mute.

The first time ANY graphics checkbox is touched, the panel sets quality_manager.auto = false — the same field QualityManager.cycle_override() (bound to F4) already flips when pinning a fixed tier. With auto == false, QualityManager.tick()’s adaptive tier-driven _apply() stops re-asserting a bundled tier every few seconds, so the panel’s individual toggles remain the sole source of truth for the rest of the session — nothing fights them.

This flip happens once and is never reset back to true by this panel (matching how F4’s own override behaves — it’s a session-long pin, not a momentary one). A label on the panel reflects current state: “Adaptive Quality: ON” before any toggle is touched, “Adaptive Quality: OFF (manual)” afterward.

Known interaction with F4 (accepted, not engineered around): F4 independently cycles QualityManager through fixed tiers via cycle_override(), which calls _apply() and re-asserts an entire tier’s bundle of settings in one shot. If F4 is pressed after the debug panel has set individual toggles, F4’s tier-apply will overwrite them (and vice versa — using the panel after F4 overwrites F4’s tier). Whichever is touched last wins. This is acceptable for a dev-only investigation tool; no reconciliation logic is being built for it.

None. Every toggle (graphics and audio) resets to its default (everything on, adaptive quality enabled) on the next process launch. This is a deliberate scope cut for the dev-tool pass — when this becomes a real player-facing settings screen, persistence should route through MetaState/MetaStore (the existing save-file mechanism), and the UI likely simplifies to a single graphics-quality picker rather than 8 raw checkboxes. Flagging this here as the first thing to revisit in that future pass.

  • The toggle-name → node-mutation dispatch is a pure, unit-testable method (e.g. DebugSettingsPanel.apply_graphics_toggle(quality_manager, effect_name, enabled)), tested with plain stub script objects exposing only the needed methods/properties — the same stubbing style tests/test_quality_manager.gd already uses (no real render nodes bound).
  • A test confirms quality_manager.auto flips to false on the first graphics toggle and stays false on subsequent toggles (doesn’t get reset).
  • Audio bus mute/unmute is exercised directly against the real (headless) AudioServer — GUT tests run inside a real Godot engine instance, so AudioServer.set_bus_mute/is_bus_mute work directly in a test, no mocking needed.
  • No determinism-baseline impact: this is entirely render/audio-side, never touches /sim.