Debug Settings Panel — Design
Debug Settings Panel — Design
Section titled “Debug Settings Panel — Design”Purpose
Section titled “Purpose”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.
Non-goals (for this pass)
Section titled “Non-goals (for this pass)”- 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.
Architecture
Section titled “Architecture”Reachability
Section titled “Reachability”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).
New file: ui/debug_settings_panel.gd
Section titled “New file: ui/debug_settings_panel.gd”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.
Manual override mode (graphics only)
Section titled “Manual override mode (graphics only)”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.
Persistence
Section titled “Persistence”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.
Testing
Section titled “Testing”- 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 styletests/test_quality_manager.gdalready uses (no real render nodes bound). - A test confirms
quality_manager.autoflips tofalseon the first graphics toggle and staysfalseon 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, soAudioServer.set_bus_mute/is_bus_mutework directly in a test, no mocking needed. - No determinism-baseline impact: this is entirely render/audio-side, never touches
/sim.