extends CanvasLayer ## Volumes and key bindings. ## ## Reachable from the main menu and from the in-game menu, and the same screen ## both times: settings are local to this client, so there is nothing about the ## game's state that should change what it can do. signal closed signal credits_requested var _rows: Dictionary[String, Button] = {} var _status: Label var _master: HSlider var _sfx: HSlider var _master_value: Label var _sfx_value: Label ## The action currently waiting for a key press, or empty. var _listening: String = "" func _ready() -> void: layer = 32 visible = false var root := UiTheme.themed_root() add_child(root) var scrim := ColorRect.new() scrim.color = Color(0.03, 0.03, 0.06, 0.94) scrim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) scrim.mouse_filter = Control.MOUSE_FILTER_STOP root.add_child(scrim) var centre := CenterContainer.new() centre.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.add_child(centre) # A panel, so a dialog reads as a thing laid over the world rather # than as text floating on a dimmed one. var frame := UiTheme.dialog_panel() centre.add_child(frame) var panel := VBoxContainer.new() panel.custom_minimum_size = Vector2(520.0, 0.0) panel.add_theme_constant_override("separation", 8) frame.add_child(panel) var title := Label.new() title.text = "SETTINGS" title.add_theme_font_size_override("font_size", 26) panel.add_child(title) panel.add_child(_heading("SOUND")) var master_row := _slider_row(panel, "Master") _master = master_row[0] _master_value = master_row[1] var sfx_row := _slider_row(panel, "Effects") _sfx = sfx_row[0] _sfx_value = sfx_row[1] panel.add_child(_heading("CONTROLS")) _status = Label.new() _status.add_theme_font_size_override("font_size", 12) _status.add_theme_color_override("font_color", Color(1.0, 0.7, 0.5)) panel.add_child(_status) # Scrolled: thirteen rows is more than fits on a short window, and a list # that runs off the bottom of the screen is a list with unreachable entries. var scroll := ScrollContainer.new() scroll.custom_minimum_size = Vector2(520.0, 300.0) panel.add_child(scroll) var list := VBoxContainer.new() list.custom_minimum_size = Vector2(500.0, 0.0) list.add_theme_constant_override("separation", 3) scroll.add_child(list) for entry in Settings.REBINDABLE: _rows[String(entry[0])] = _binding_row(list, String(entry[0]), String(entry[1])) var buttons := HBoxContainer.new() buttons.add_theme_constant_override("separation", 8) panel.add_child(buttons) var reset := Button.new() reset.text = "Reset controls" reset.custom_minimum_size = Vector2(160.0, 34.0) reset.pressed.connect(func() -> void: Settings.reset_bindings() _status.text = "controls reset" refresh()) buttons.add_child(reset) var credits := Button.new() credits.text = "Credits" credits.custom_minimum_size = Vector2(160.0, 34.0) credits.pressed.connect(func() -> void: credits_requested.emit()) buttons.add_child(credits) var close := Button.new() close.text = "Close" close.custom_minimum_size = Vector2(160.0, 34.0) close.pressed.connect(func() -> void: closed.emit()) buttons.add_child(close) ## A section heading on the pack's banner ribbon, so the two halves of this ## screen are visibly two sections rather than one long list. ## A section heading on the pack's banner ribbon. ## ## Deliberately narrow and centred rather than stretched across the dialog: at ## full width the ribbon stops reading as a label and becomes a bright divider ## competing with everything under it. func _heading(text: String) -> Control: var row := CenterContainer.new() var holder := MarginContainer.new() holder.custom_minimum_size = Vector2(210.0, 34.0) row.add_child(holder) var banner := NinePatchRect.new() banner.texture = UiTheme.texture("banner") # The ribbon's tails hang off both ends; stretching through them would # smear the shape, so they are the patch margins and only the body grows. banner.patch_margin_left = 14 banner.patch_margin_right = 14 banner.patch_margin_top = 8 banner.patch_margin_bottom = 8 holder.add_child(banner) var l := Label.new() l.text = text l.add_theme_font_size_override("font_size", 13) l.add_theme_color_override("font_color", UiTheme.INK) l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER l.vertical_alignment = VERTICAL_ALIGNMENT_CENTER holder.add_child(l) return row func _slider_row(parent: Control, label: String) -> Array: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 10) parent.add_child(row) var name_label := Label.new() name_label.text = label name_label.custom_minimum_size = Vector2(90.0, 0.0) row.add_child(name_label) var slider := HSlider.new() slider.min_value = 0.0 slider.max_value = 1.0 slider.step = 0.05 slider.custom_minimum_size = Vector2(340.0, 20.0) row.add_child(slider) var value := Label.new() value.custom_minimum_size = Vector2(50.0, 0.0) row.add_child(value) return [slider, value] func _binding_row(parent: Control, action: String, label: String) -> Button: var row := HBoxContainer.new() row.add_theme_constant_override("separation", 10) parent.add_child(row) var name_label := Label.new() name_label.text = label name_label.custom_minimum_size = Vector2(260.0, 0.0) row.add_child(name_label) var button := Button.new() button.custom_minimum_size = Vector2(200.0, 30.0) button.pressed.connect(func() -> void: _listen_for(action)) row.add_child(button) return button func open() -> void: visible = true _listening = "" _status.text = "" refresh() func refresh() -> void: _master.value = Settings.master_volume _sfx.value = Settings.sfx_volume _master_value.text = "%d%%" % roundi(Settings.master_volume * 100.0) _sfx_value.text = "%d%%" % roundi(Settings.sfx_volume * 100.0) # Connected here rather than in _ready so setting the values above cannot # fire the handler and write the file back on every open. if not _master.value_changed.is_connected(_on_master): _master.value_changed.connect(_on_master) _sfx.value_changed.connect(_on_sfx) for action in _rows: _rows[action].text = "listening..." if action == _listening \ else Settings.binding_label(action) func _on_master(value: float) -> void: Settings.master_volume = value Settings.apply_audio() Settings.save() _master_value.text = "%d%%" % roundi(value * 100.0) func _on_sfx(value: float) -> void: Settings.sfx_volume = value Settings.apply_audio() Settings.save() _sfx_value.text = "%d%%" % roundi(value * 100.0) func _listen_for(action: String) -> void: _listening = action _status.text = "press a key or mouse button (Escape cancels)" refresh() ## Captures the next key or mouse button while a row is listening. ## ## _input rather than _unhandled_input on purpose: the buttons in this screen ## consume clicks, and a rebind that could never see a mouse button would be a ## rebind that cannot bind Fire. func _input(event: InputEvent) -> void: if not visible or _listening.is_empty(): return if event is InputEventKey and not (event as InputEventKey).pressed: return if event is InputEventMouseButton and not (event as InputEventMouseButton).pressed: return if not Settings.is_bindable(event): return get_viewport().set_input_as_handled() var action := _listening _listening = "" # Escape is the way out of a listening row, so it cannot also be captured # by one -- binding the cancel key would leave no way to cancel. if event is InputEventKey \ and (event as InputEventKey).physical_keycode == KEY_ESCAPE: _status.text = "cancelled" refresh() return var clash := Settings.rebind(action, event) if clash.is_empty(): _status.text = "" else: _status.text = "%s is already %s" % [ Settings.binding_label(clash), _label_of(clash)] refresh() func _label_of(action: String) -> String: for entry in Settings.REBINDABLE: if String(entry[0]) == action: return String(entry[1]) return action