Files
settled-reach/client/ui/settings_dialog.gd
T
jpmschweitzerandClaude Opus 4.6 4eb54a6f7a fix(client): resolve all gdlint warnings — zero warnings policy
Fix 354 gdlint warnings across 65 files: 194 class-definitions-order
(reorder declarations), 138 max-line-length (split long lines),
22 code issues (unused args, no-else-return, naming). Update .gdlintrc
to exclude addons/ and raise max-public-methods for test files.
No logic changes — declaration order, whitespace, and naming only.

Ticket: #783

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:59:56 +02:00

381 lines
13 KiB
GDScript

extends Control
## #528: Audio settings dialog — 5-bus volume sliders.
## #646: AI-Enhanced Dialogue toggle + hardware detection status (D-138).
## Opens on OPEN_MENU (ESC) from main.gd. Closes on OPEN_MENU again or CLOSE button.
## Volumes persist via AudioManager._save_prefs() on each slider change.
## AI Dialogue toggle persists via ConfigFile (client-local) + ChangeSettings IPC (server SQLite).
signal closed
signal debug_console_toggled(enabled: bool) # #581: debug console enabled/disabled
signal ai_dialogue_toggled(enabled: bool) # #646: AI-Enhanced Dialogue enabled/disabled
const BG_COLOR := Color(0.05, 0.05, 0.08, 0.90)
const BORDER_COLOR := Color("#4a9ebb")
const TEXT_COLOR := Color(0.878, 0.969, 0.98, 1)
const TITLE_COLOR := Color("#4a9ebb")
const STATUS_GREEN := Color("#6bc9a6")
const STATUS_YELLOW := Color("#e8c547")
const STATUS_RED := Color("#c84040")
const FONT_SIZE := 14
const FONT_SIZE_SMALL := 11
## Shared client preferences file — avoids compile-time dependency on DebugConsole for path.
const SETTINGS_CFG_PATH := "user://settings.cfg"
const BOX_WIDTH := 460
const BOX_HEIGHT := 500 # +36 debug console, +88 AI dialogue section
const PADDING := 20
const ROW_HEIGHT := 36
# Bus display labels → bus name strings (must match AudioManager BUS_* constants)
const BUS_ROWS: Array = [
["Music", "Music"],
["Ambient", "Ambient"],
["World SFX", "WorldSFX"],
["Player Actions", "PlayerActions"],
["UI Sounds", "UISounds"],
]
var _active: bool = false
var _container: VBoxContainer = null
# #646: AI Dialogue hardware status and toggle node ref — used by testable API methods
var _ai_hw_status: String = "" # "pass" | "marginal" | "fail" | "" (not yet checked)
var _ai_check_node: CheckButton = null
var _ai_inference_suspended: bool = false # D-138 §8 Layer 3 battery auto-suspend state
var _ai_battery_warning_label: Label = null # shown when on battery; toggle stays enabled
func _ready() -> void:
visible = false
mouse_filter = Control.MOUSE_FILTER_STOP
func open() -> void:
if _active:
return
_active = true
visible = true
_build_ui()
queue_redraw()
func close() -> void:
if not _active:
return
_active = false
visible = false
_destroy_ui()
queue_redraw()
closed.emit()
func is_open() -> bool:
return _active
func _build_ui() -> void:
var vp_size := get_viewport_rect().size
var box_pos := Vector2(
(vp_size.x - BOX_WIDTH) / 2.0,
(vp_size.y - BOX_HEIGHT) / 2.0
)
_container = VBoxContainer.new()
_container.position = box_pos + Vector2(PADDING, PADDING + 28)
_container.custom_minimum_size = Vector2(BOX_WIDTH - PADDING * 2, 0)
_container.add_theme_constant_override("separation", 4)
add_child(_container)
for row_data in BUS_ROWS:
var label_text: String = row_data[0]
var bus_name: String = row_data[1]
var hbox := HBoxContainer.new()
hbox.custom_minimum_size = Vector2(0, ROW_HEIGHT)
_container.add_child(hbox)
var label := Label.new()
label.text = label_text
label.custom_minimum_size = Vector2(150, 0)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", FONT_SIZE)
label.add_theme_color_override("font_color", TEXT_COLOR)
hbox.add_child(label)
var slider := HSlider.new()
slider.min_value = -40.0
slider.max_value = 0.0
slider.step = 0.5
slider.value = AudioManager.get_volume(bus_name)
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.add_child(slider)
var db_label := Label.new()
db_label.text = _format_db(slider.value)
db_label.custom_minimum_size = Vector2(70, 0)
db_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
db_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
db_label.add_theme_font_size_override("font_size", FONT_SIZE)
db_label.add_theme_color_override("font_color", TEXT_COLOR)
hbox.add_child(db_label)
slider.value_changed.connect(func(value: float) -> void:
AudioManager.set_volume(bus_name, value)
db_label.text = _format_db(value)
)
# #581: Debug Console toggle
var debug_hbox := HBoxContainer.new()
debug_hbox.custom_minimum_size = Vector2(0, ROW_HEIGHT)
_container.add_child(debug_hbox)
var debug_label := Label.new()
debug_label.text = "Debug Console"
debug_label.custom_minimum_size = Vector2(150, 0)
debug_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
debug_label.add_theme_font_size_override("font_size", FONT_SIZE)
debug_label.add_theme_color_override("font_color", TEXT_COLOR)
debug_hbox.add_child(debug_label)
var debug_check := CheckButton.new()
# Query live DebugConsole node if available; fall back to prefs file
var console_node := get_node_or_null("/root/Main/ModalLayer/DebugConsole")
if console_node and console_node.has_method("is_enabled"):
debug_check.button_pressed = console_node.is_enabled()
else:
var cfg := ConfigFile.new()
debug_check.button_pressed = true
if cfg.load(DebugConsole.PREFS_PATH) == OK:
debug_check.button_pressed = cfg.get_value(DebugConsole.PREFS_SECTION, DebugConsole.PREFS_KEY_ENABLED, true)
debug_check.toggled.connect(func(enabled: bool) -> void:
debug_console_toggled.emit(enabled)
)
debug_hbox.add_child(debug_check)
# #646: AI Dialogue section divider
var ai_divider := Control.new()
ai_divider.custom_minimum_size = Vector2(0, 8)
_container.add_child(ai_divider)
var ai_section_label := Label.new()
ai_section_label.text = "AI DIALOGUE"
ai_section_label.add_theme_font_size_override("font_size", FONT_SIZE - 2)
ai_section_label.add_theme_color_override("font_color", TITLE_COLOR)
_container.add_child(ai_section_label)
# #646: Detect hardware and set status
var ram_result := HardwareDetector.check_ram()
var tpt_cache: Variant = HardwareDetector.read_benchmark_cache()
var ram_class: String = ram_result["classification"]
var tpt_class: String = "green"
var tpt_tps: float = 0.0
if tpt_cache != null:
tpt_class = tpt_cache["classification"]
tpt_tps = tpt_cache["tps"]
# Composite status: RAM fail overrides TPT; RAM marginal keeps its own category
var hw_status: String
if ram_class == "fail":
hw_status = "fail"
elif ram_class == "marginal":
hw_status = "marginal"
else:
# RAM passes — status follows TPT if benchmarked, else "pass"
hw_status = "pass" if tpt_cache == null else tpt_class
set_ai_dialogue_hardware_status(hw_status)
# D-138 §8 Layer 3: reflect current battery-suspension state from HardwareDetector.
_ai_inference_suspended = HardwareDetector.inference_suspended
var ai_hbox := HBoxContainer.new()
ai_hbox.custom_minimum_size = Vector2(0, ROW_HEIGHT)
_container.add_child(ai_hbox)
var ai_label := Label.new()
ai_label.text = get_ai_dialogue_label_text()
ai_label.custom_minimum_size = Vector2(200, 0)
ai_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
ai_label.add_theme_font_size_override("font_size", FONT_SIZE)
ai_label.add_theme_color_override("font_color", TEXT_COLOR)
ai_hbox.add_child(ai_label)
# Status dot — colored square indicating hardware classification
var status_dot := ColorRect.new()
status_dot.custom_minimum_size = Vector2(10, 10)
status_dot.size_flags_vertical = Control.SIZE_SHRINK_CENTER
match _ai_hw_status:
"pass", "green":
status_dot.color = STATUS_GREEN
"marginal", "yellow":
status_dot.color = STATUS_YELLOW
_: # "red", "fail"
status_dot.color = STATUS_RED
ai_hbox.add_child(status_dot)
var dot_spacer := Control.new()
dot_spacer.custom_minimum_size = Vector2(8, 0)
ai_hbox.add_child(dot_spacer)
_ai_check_node = CheckButton.new()
_ai_check_node.button_pressed = GameState.ai_enhanced_dialogue_enabled
_ai_check_node.disabled = (_ai_hw_status == "fail")
ai_hbox.add_child(_ai_check_node)
# Status message label — only shown when non-empty
var status_msg: String = HardwareDetector.status_message(
ram_class, tpt_class, ram_result["free_mb"], tpt_tps)
var ai_status_label := Label.new()
ai_status_label.text = status_msg
ai_status_label.add_theme_font_size_override("font_size", FONT_SIZE_SMALL)
ai_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
ai_status_label.custom_minimum_size = Vector2(BOX_WIDTH - PADDING * 2, 0)
match _ai_hw_status:
"marginal", "yellow":
ai_status_label.add_theme_color_override("font_color", STATUS_YELLOW)
"red", "fail":
ai_status_label.add_theme_color_override("font_color", STATUS_RED)
_:
ai_status_label.add_theme_color_override("font_color", TEXT_COLOR)
ai_status_label.visible = not status_msg.is_empty()
_container.add_child(ai_status_label)
# Battery warning label — visible when on battery power; toggle stays enabled per D-138.
_ai_battery_warning_label = Label.new()
_ai_battery_warning_label.text = UIStrings.get_text("settings.ai_battery_warning")
_ai_battery_warning_label.add_theme_font_size_override("font_size", FONT_SIZE_SMALL)
_ai_battery_warning_label.add_theme_color_override("font_color", STATUS_YELLOW)
_ai_battery_warning_label.visible = _ai_inference_suspended
_container.add_child(_ai_battery_warning_label)
_ai_check_node.toggled.connect(func(enabled: bool) -> void:
GameState.ai_enhanced_dialogue_enabled = enabled
_save_ai_pref(enabled)
if SimBridge.state == SimBridge.ConnectionState.CONNECTED:
SimBridge.send_input({
"action": InputMapper.Action.CHANGE_SETTINGS,
"action_data": {"ai_enhanced_dialogue": enabled},
"timestamp_msec": Time.get_ticks_msec(),
})
ai_dialogue_toggled.emit(enabled)
)
# Spacer
var spacer := Control.new()
spacer.custom_minimum_size = Vector2(0, 8)
_container.add_child(spacer)
# Close button
var close_btn := Button.new()
close_btn.text = "CLOSE"
close_btn.add_theme_font_size_override("font_size", FONT_SIZE)
close_btn.pressed.connect(close)
_container.add_child(close_btn)
# Quit to Menu button (#258: D-085 session management)
var quit_btn := Button.new()
quit_btn.text = UIStrings.get_text("menu.quit_to_menu")
quit_btn.add_theme_font_size_override("font_size", FONT_SIZE)
quit_btn.add_theme_color_override("font_color", Color("#c87040"))
quit_btn.pressed.connect(_on_quit_to_menu)
_container.add_child(quit_btn)
func _destroy_ui() -> void:
if _container:
_container.queue_free()
_container = null
_ai_check_node = null # freed with _container
_ai_battery_warning_label = null
# -- #646: AI Dialogue testable API -------------------------------------------
## Returns the canonical label text for the AI-Enhanced Dialogue toggle (D-138).
## Reads from UIStrings yaml (settings.ai_dialogue_toggle) for localization support.
func get_ai_dialogue_label_text() -> String:
return UIStrings.get_text("settings.ai_dialogue_toggle")
## Set the hardware detection status — drives toggle enabled/disabled state.
## Accepts: "pass" | "marginal" | "fail" (RAM) or "green" | "yellow" | "red" (TPT).
## Only "fail" (insufficient RAM) disables the toggle. All others leave it enabled —
## D-138: no hard minimum spec floor; player can always override recommendations.
func set_ai_dialogue_hardware_status(status: String) -> void:
_ai_hw_status = status
if _ai_check_node != null:
_ai_check_node.disabled = (status == "fail")
## Returns true if the AI-Enhanced Dialogue toggle is currently enabled (not greyed out).
## Only disabled by RAM "fail" — battery suspension shows a warning but never greys the toggle.
func is_ai_dialogue_toggle_enabled() -> bool:
return _ai_hw_status != "fail"
## Returns true if AI inference is currently auto-suspended due to battery power (D-138 §8 Layer 3).
## Set by set_ai_inference_suspended() when HardwareDetector detects battery profile.
func is_ai_inference_suspended() -> bool:
return _ai_inference_suspended
## Returns true if the battery warning label is currently visible.
func get_battery_warning_visible() -> bool:
if _ai_battery_warning_label == null:
return false
return _ai_battery_warning_label.visible
## Update battery suspension display state.
## Shows/hides the battery warning label; toggle stays enabled — player autonomy wins (D-138).
## Called when PlatformInfo.power_profile_changed fires (via main.gd or HardwareDetector).
## Tests can inject suspended=true to verify the warning label appears.
func set_ai_inference_suspended(suspended: bool) -> void:
_ai_inference_suspended = suspended
if _ai_battery_warning_label != null:
_ai_battery_warning_label.visible = suspended
## Persist the AI Dialogue enabled state to the local prefs file.
## Uses the same settings.cfg as DebugConsole — different section ("ai_dialogue").
func _save_ai_pref(enabled: bool) -> void:
var cfg := ConfigFile.new()
cfg.load(SETTINGS_CFG_PATH)
cfg.set_value("ai_dialogue", "enabled", enabled)
cfg.save(SETTINGS_CFG_PATH)
func _draw() -> void:
if not _active:
return
var viewport_size := get_viewport_rect().size
# Dim overlay
draw_rect(Rect2(Vector2.ZERO, viewport_size), BG_COLOR)
# Dialog box
var box_pos := Vector2(
(viewport_size.x - BOX_WIDTH) / 2.0,
(viewport_size.y - BOX_HEIGHT) / 2.0
)
var box_rect := Rect2(box_pos, Vector2(BOX_WIDTH, BOX_HEIGHT))
draw_rect(box_rect, Color(0.08, 0.08, 0.12, 0.95))
draw_rect(box_rect, BORDER_COLOR, false, 1.0)
# Title
var font := ThemeDB.fallback_font
draw_string(font,
box_pos + Vector2(PADDING, PADDING + 18),
"SETTINGS",
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 2, TITLE_COLOR)
func _on_quit_to_menu() -> void:
close()
SessionManager.quit_to_menu()
static func _format_db(db: float) -> String:
if db <= -40.0:
return "mute"
return "%d dB" % int(db)