Add UiArt.add_screen_backdrop() to place the reference art behind an opaque panel, then wire the pause overlay and settings screen to it. Pause rows get larger, rounded styles; settings widens to fit its expanded controls. Headless 226/226 verified in a clean HEAD worktree (the realism WIP currently editing materials.gd remains unstaged).
244 lines
7.5 KiB
GDScript
244 lines
7.5 KiB
GDScript
class_name SettingsPanel
|
|
extends CanvasLayer
|
|
## Settings: mouse look sensitivity and HUD font scale, persisted through the
|
|
## `Save` autoload into `user://settings.json`.
|
|
##
|
|
## Emits `applied(settings)` so the game manager can push values into the guard
|
|
## and HUD as soon as the player confirms.
|
|
|
|
const InputBindings := preload("res://src/core/input_bindings.gd")
|
|
const UiArt := preload("res://src/game/ui/art.gd")
|
|
|
|
signal applied(settings: Dictionary)
|
|
signal closed()
|
|
|
|
var _root: Control = null
|
|
var _sensitivity: HSlider = null
|
|
var _font_scale: HSlider = null
|
|
var _sensitivity_value: Label = null
|
|
var _font_scale_value: Label = null
|
|
var _invert_y: CheckButton = null
|
|
var _head_bob: CheckButton = null
|
|
var _bind_buttons := {}
|
|
var _listening_action := ""
|
|
var settings: Dictionary = {}
|
|
|
|
func _ready() -> void:
|
|
layer = 22
|
|
_build()
|
|
hide()
|
|
|
|
func _init() -> void:
|
|
layer = 22
|
|
_build()
|
|
hide()
|
|
|
|
func _build() -> void:
|
|
_root = Control.new()
|
|
_root.name = "SettingsRoot"
|
|
_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
_root.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
add_child(_root)
|
|
|
|
var art := UiArt.new()
|
|
if not art.add_screen_backdrop(_root, "settings_panel",
|
|
Rect2(0.117, 0.049, 0.703, 0.895)):
|
|
var bg := ColorRect.new()
|
|
bg.color = Color(0.02, 0.03, 0.05, 0.9)
|
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_root.add_child(bg)
|
|
|
|
var center := CenterContainer.new()
|
|
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
_root.add_child(center)
|
|
|
|
var column := VBoxContainer.new()
|
|
column.custom_minimum_size = Vector2(640, 0)
|
|
column.add_theme_constant_override("separation", 12)
|
|
center.add_child(column)
|
|
|
|
var title := Label.new()
|
|
title.text = "Settings"
|
|
title.add_theme_font_size_override("font_size", 32)
|
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
column.add_child(title)
|
|
|
|
var sens_row := VBoxContainer.new()
|
|
column.add_child(sens_row)
|
|
_sensitivity_value = _add_label_row(sens_row, "Mouse look sensitivity")
|
|
_sensitivity = _add_slider(sens_row, 0.0005, 0.0060, 0.0001)
|
|
_sensitivity.value_changed.connect(_on_sensitivity_changed)
|
|
|
|
var font_row := VBoxContainer.new()
|
|
column.add_child(font_row)
|
|
_font_scale_value = _add_label_row(font_row, "HUD font scale")
|
|
_font_scale = _add_slider(font_row, 0.75, 1.75, 0.05)
|
|
_font_scale.value_changed.connect(_on_font_scale_changed)
|
|
|
|
_invert_y = CheckButton.new()
|
|
_invert_y.text = "Invert vertical look"
|
|
_invert_y.toggled.connect(_on_invert_y_toggled)
|
|
column.add_child(_invert_y)
|
|
|
|
_head_bob = CheckButton.new()
|
|
_head_bob.text = "Head bob and camera motion"
|
|
_head_bob.toggled.connect(_on_head_bob_toggled)
|
|
column.add_child(_head_bob)
|
|
|
|
var controls_title := Label.new()
|
|
controls_title.text = "Controls (click a binding, then press a key or pad button)"
|
|
controls_title.add_theme_font_size_override("font_size", 13)
|
|
controls_title.modulate = Color(0.75, 0.8, 0.9)
|
|
column.add_child(controls_title)
|
|
var grid := GridContainer.new()
|
|
grid.columns = 4
|
|
grid.add_theme_constant_override("h_separation", 10)
|
|
grid.add_theme_constant_override("v_separation", 6)
|
|
column.add_child(grid)
|
|
for action in InputBindings.REMAPPABLE:
|
|
var label := Label.new()
|
|
label.text = str(action).replace("_", " ").capitalize()
|
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
grid.add_child(label)
|
|
var button := Button.new()
|
|
button.custom_minimum_size = Vector2(130, 0)
|
|
button.pressed.connect(_on_rebind.bind(action))
|
|
grid.add_child(button)
|
|
_bind_buttons[action] = button
|
|
|
|
var row := HBoxContainer.new()
|
|
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
row.add_theme_constant_override("separation", 12)
|
|
column.add_child(row)
|
|
|
|
var save_button := Button.new()
|
|
save_button.name = "ApplyButton"
|
|
save_button.text = "Apply"
|
|
save_button.pressed.connect(_on_apply)
|
|
row.add_child(save_button)
|
|
|
|
var cancel_button := Button.new()
|
|
cancel_button.name = "CancelButton"
|
|
cancel_button.text = "Cancel"
|
|
cancel_button.pressed.connect(_on_cancel)
|
|
row.add_child(cancel_button)
|
|
|
|
func _add_label_row(parent: Control, text: String) -> Label:
|
|
var row := HBoxContainer.new()
|
|
parent.add_child(row)
|
|
var label := Label.new()
|
|
label.text = text
|
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
row.add_child(label)
|
|
var value := Label.new()
|
|
value.add_theme_color_override("font_color", Color(0.75, 0.8, 0.9))
|
|
row.add_child(value)
|
|
return value
|
|
|
|
func _add_slider(parent: Control, min_v: float, max_v: float, step: float) -> HSlider:
|
|
var slider := HSlider.new()
|
|
slider.min_value = min_v
|
|
slider.max_value = max_v
|
|
slider.step = step
|
|
slider.custom_minimum_size = Vector2(420, 24)
|
|
parent.add_child(slider)
|
|
return slider
|
|
|
|
func open(current: Dictionary) -> void:
|
|
settings = current.duplicate(true)
|
|
var sens: float = float(settings.get("look_sensitivity", 0.0022))
|
|
var scale: float = float(settings.get("hud_font_scale", 1.0))
|
|
if _sensitivity:
|
|
_sensitivity.set_value_no_signal(clampf(sens, _sensitivity.min_value, _sensitivity.max_value))
|
|
if _font_scale:
|
|
_font_scale.set_value_no_signal(clampf(scale, _font_scale.min_value, _font_scale.max_value))
|
|
if _invert_y:
|
|
_invert_y.set_pressed_no_signal(bool(settings.get("invert_y", false)))
|
|
if _head_bob:
|
|
_head_bob.set_pressed_no_signal(bool(settings.get("head_bob", true)))
|
|
_listening_action = ""
|
|
_refresh_bind_buttons()
|
|
_sync_labels()
|
|
visible = true
|
|
_root.visible = true
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
|
|
func close() -> void:
|
|
visible = false
|
|
_root.visible = false
|
|
|
|
func _sync_labels() -> void:
|
|
if _sensitivity_value and _sensitivity:
|
|
_sensitivity_value.text = "%.4f" % _sensitivity.value
|
|
if _font_scale_value and _font_scale:
|
|
_font_scale_value.text = "%.2fx" % _font_scale.value
|
|
|
|
func _on_sensitivity_changed(value: float) -> void:
|
|
settings["look_sensitivity"] = value
|
|
_sync_labels()
|
|
|
|
func _on_font_scale_changed(value: float) -> void:
|
|
settings["hud_font_scale"] = value
|
|
_sync_labels()
|
|
|
|
func _on_invert_y_toggled(pressed: bool) -> void:
|
|
settings["invert_y"] = pressed
|
|
|
|
func _on_head_bob_toggled(pressed: bool) -> void:
|
|
settings["head_bob"] = pressed
|
|
|
|
func _refresh_bind_buttons() -> void:
|
|
var remaps: Dictionary = settings.get("input_bindings", {})
|
|
for action in _bind_buttons.keys():
|
|
var button: Button = _bind_buttons[action]
|
|
var descriptor: Dictionary = remaps.get(action, {})
|
|
if descriptor.is_empty():
|
|
descriptor = InputBindings.current_descriptor(action)
|
|
button.text = InputBindings.label_for(descriptor)
|
|
|
|
func _on_rebind(action: String) -> void:
|
|
_listening_action = action
|
|
var button: Button = _bind_buttons.get(action)
|
|
if button:
|
|
button.text = "Press…"
|
|
if button.is_inside_tree():
|
|
button.grab_focus()
|
|
|
|
func _apply_remap(event: InputEvent) -> bool:
|
|
if _listening_action == "":
|
|
return false
|
|
var descriptor := InputBindings.descriptor_from_event(event)
|
|
if descriptor.is_empty():
|
|
return false
|
|
var remaps: Dictionary = settings.get("input_bindings", {})
|
|
remaps[_listening_action] = descriptor
|
|
settings["input_bindings"] = remaps
|
|
_listening_action = ""
|
|
_refresh_bind_buttons()
|
|
return true
|
|
|
|
func _on_apply() -> void:
|
|
if Save:
|
|
Save.save_settings(settings)
|
|
applied.emit(settings)
|
|
close()
|
|
closed.emit()
|
|
|
|
func _on_cancel() -> void:
|
|
close()
|
|
closed.emit()
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if not visible:
|
|
return
|
|
if _listening_action != "":
|
|
var pressed: bool = (event is InputEventKey and event.pressed and not event.echo) \
|
|
or (event is InputEventJoypadButton and event.pressed)
|
|
if pressed and _apply_remap(event):
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
|
|
_on_cancel()
|
|
get_viewport().set_input_as_handled()
|