extends Node # Автолоад «PauseMenu»: меню паузы по Esc / Start. Игра идёт дальше (это мультиплеер), но игрок стоит # (Net.input_blocked, как при вводе ника). Страницы: главная, настройки, управление. # # В браузере Esc при захваченной мыши не доходит до игры — браузер просто снимает захват. Поэтому меню # открывается и по самому факту «мышь была захвачена и перестала быть» (см. _process). const TEXT := Color(0.92, 0.88, 0.8) const DIM := Color(0.62, 0.6, 0.56) const MINE := Color(0.96, 0.76, 0.44) const HUB_SCENE := "res://maps/hub/hub.tscn" const REOPEN_GUARD := 0.6 # после закрытия столько секунд снятие захвата не считается нажатием Esc const CAPTURED_MIN := 0.5 # мышь должна быть захвачена хотя бы столько, чтобы потеря захвата считалась Esc var open := false var _layer: CanvasLayer var _pages := {} # имя -> Control var _page := "main" var _title: Label var _subtitle: Label var _hub_btn: Button var _lb_btn: Button var _captured_time := 0.0 var _guard := 0.0 var _sliders := {} # имя настройки -> {slider, value} var _invert: CheckButton func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS _build() _layer.visible = false # ---------- открытие / закрытие ---------- func open_menu() -> void: if open: return open = true Net.input_blocked = true Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _show_page("main") _layer.visible = true func close_menu() -> void: if not open: return open = false _guard = REOPEN_GUARD Settings.save() _layer.visible = false get_viewport().gui_release_focus() if not Overlay.nick_open: Net.input_blocked = false Input.mouse_mode = Input.MOUSE_MODE_CAPTURED func _process(delta: float) -> void: _guard = maxf(_guard - delta, 0.0) if open: _captured_time = 0.0 return if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: _captured_time += delta return # Захват пропал. Если до этого мышь побыла захвачена и мы сами её не отпускали — это Esc в браузере. var was := _captured_time >= CAPTURED_MIN _captured_time = 0.0 if was and _guard <= 0.0 and _can_open(): open_menu() func _can_open() -> bool: return not Overlay.nick_open and not MapLoader._busy and get_tree().current_scene != null func _unhandled_input(event: InputEvent) -> void: if open: if event.is_action_pressed("pause") or event.is_action_pressed("ui_cancel"): if _page == "main": close_menu() else: _show_page("main") # Игровые клавиши (R, N, Tab, G…) за меню не работают if not (event is InputEventMouseMotion): get_viewport().set_input_as_handled() return if event.is_action_pressed("pause") and not event.is_echo(): if Overlay.nick_open or Overlay.lb_open or not _can_open(): return # Esc закрывает панель ника / таблицу — это делает Overlay open_menu() get_viewport().set_input_as_handled() # ---------- страницы ---------- func _show_page(name: String) -> void: _page = name for k in _pages: _pages[k].visible = (k == name) match name: "main": _title.text = "ПАУЗА" var map_title: String = Net.MAP_TITLES.get(Net.current_map, "") _subtitle.text = "Карта: %s" % map_title if map_title != "" else "" _hub_btn.visible = Net.current_map != "hub" _lb_btn.visible = true _focus_first.call_deferred(_pages.main) "settings": _title.text = "НАСТРОЙКИ" _subtitle.text = "Сохраняются автоматически" _refresh_settings() _focus_first.call_deferred(_pages.settings) "controls": _title.text = "УПРАВЛЕНИЕ" _subtitle.text = "" _focus_first.call_deferred(_pages.controls) func _focus_first(page: Control) -> void: for b in page.find_children("*", "Button", true, false): if b.visible and not b.disabled: b.grab_focus() return func _build() -> void: _layer = CanvasLayer.new() _layer.layer = 30 add_child(_layer) var theme := _make_theme() var root := Control.new() root.theme = theme root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _layer.add_child(root) var dim := ColorRect.new() dim.color = Color(0.0, 0.0, 0.0, 0.62) dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.add_child(dim) var panel := PanelContainer.new() panel.add_theme_stylebox_override("panel", LbView.panel_style(28)) root.add_child(panel) panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER) panel.grow_horizontal = Control.GROW_DIRECTION_BOTH panel.grow_vertical = Control.GROW_DIRECTION_BOTH var box := VBoxContainer.new() box.add_theme_constant_override("separation", 12) box.custom_minimum_size = Vector2(460, 0) panel.add_child(box) _title = LbView.make_label("ПАУЗА", 34, TEXT) _title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_title) _subtitle = LbView.make_label("", 16, DIM) _subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_subtitle) box.add_child(HSeparator.new()) _pages["main"] = _build_main() _pages["settings"] = _build_settings() _pages["controls"] = _build_controls() for k in _pages: box.add_child(_pages[k]) var hint := LbView.make_label("Esc / Start — назад", 14, DIM) hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(hint) func _make_theme() -> Theme: var t := Theme.new() t.default_font_size = 20 var normal := _btn_style(Color(0.07, 0.075, 0.08, 0.95), Color(0.30, 0.22, 0.14)) var hover := _btn_style(Color(0.13, 0.11, 0.09, 0.98), Color(0.75, 0.52, 0.26)) var pressed := _btn_style(Color(0.20, 0.15, 0.10, 1.0), MINE) var focus := _btn_style(Color(0.13, 0.11, 0.09, 0.98), MINE) for cls in ["Button", "CheckButton"]: t.set_stylebox("normal", cls, normal) t.set_stylebox("hover", cls, hover) t.set_stylebox("pressed", cls, pressed) t.set_stylebox("focus", cls, focus) t.set_color("font_color", cls, TEXT) t.set_color("font_hover_color", cls, Color.WHITE) t.set_color("font_focus_color", cls, Color.WHITE) t.set_color("font_pressed_color", cls, MINE) t.set_font_size("font_size", cls, 20) t.set_stylebox("disabled", "Button", normal) # Слайдер: тёмная полоса и латунный заполнитель var track := StyleBoxFlat.new() track.bg_color = Color(0.1, 0.1, 0.11) track.set_corner_radius_all(3) track.content_margin_top = 4 track.content_margin_bottom = 4 var fill := StyleBoxFlat.new() fill.bg_color = Color(0.62, 0.42, 0.2) fill.set_corner_radius_all(3) t.set_stylebox("slider", "HSlider", track) t.set_stylebox("grabber_area", "HSlider", fill) t.set_stylebox("grabber_area_highlight", "HSlider", fill) return t func _btn_style(bg: Color, border: Color) -> StyleBoxFlat: var sb := StyleBoxFlat.new() sb.bg_color = bg sb.border_color = border sb.set_border_width_all(2) sb.content_margin_left = 16 sb.content_margin_right = 16 sb.content_margin_top = 9 sb.content_margin_bottom = 9 return sb func _button(text: String, cb: Callable) -> Button: var b := Button.new() b.text = text b.focus_mode = Control.FOCUS_ALL b.pressed.connect(cb) return b func _vbox() -> VBoxContainer: var v := VBoxContainer.new() v.add_theme_constant_override("separation", 10) return v func _build_main() -> Control: var v := _vbox() v.add_child(_button("Продолжить", close_menu)) _hub_btn = _button("Выйти в хаб", _go_hub) v.add_child(_hub_btn) _lb_btn = _button("Таблица лидеров", _open_lb) v.add_child(_lb_btn) v.add_child(_button("Сменить ник", _open_nick)) v.add_child(_button("Настройки", func(): _show_page("settings"))) v.add_child(_button("Управление", func(): _show_page("controls"))) return v func _go_hub() -> void: close_menu() MapLoader.enter("hub", HUB_SCENE, "", "Хаб") func _open_lb() -> void: close_menu() Overlay.open_lb() func _open_nick() -> void: open = false _guard = REOPEN_GUARD _layer.visible = false get_viewport().gui_release_focus() Overlay.open_nick(false) # сама держит Net.input_blocked и мышь # ---------- настройки ---------- func _build_settings() -> Control: var v := _vbox() _add_slider(v, "mouse", "Чувствительность мыши", 0.2, 3.0, 0.05, func(x): Settings.mouse_sens = x, func(x): return "×%.2f" % x) _add_slider(v, "stick", "Чувствительность стика", 0.3, 2.5, 0.05, func(x): Settings.stick_sens = x, func(x): return "×%.2f" % x) _add_slider(v, "fov", "Поле зрения", 60.0, 100.0, 1.0, func(x): Settings.fov = x, func(x): return "%d°" % int(x)) _add_slider(v, "volume", "Громкость", 0.0, 1.0, 0.01, func(x): Settings.volume = x, func(x): return "%d%%" % int(round(x * 100.0))) _invert = CheckButton.new() _invert.text = "Инвертировать вертикаль" _invert.focus_mode = Control.FOCUS_ALL _invert.toggled.connect(func(on: bool): Settings.invert_y = on) v.add_child(_invert) v.add_child(_button("По умолчанию", func(): Settings.reset_defaults() _refresh_settings())) v.add_child(_button("Назад", func(): _show_page("main"))) return v func _add_slider(parent: Control, key: String, caption: String, lo: float, hi: float, step: float, setter: Callable, fmt: Callable) -> void: var row := VBoxContainer.new() row.add_theme_constant_override("separation", 2) var head := HBoxContainer.new() var name_l := LbView.make_label(caption, 18, TEXT) name_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL head.add_child(name_l) var val_l := LbView.make_label("", 18, MINE) head.add_child(val_l) row.add_child(head) var s := HSlider.new() s.min_value = lo s.max_value = hi s.step = step s.focus_mode = Control.FOCUS_ALL s.custom_minimum_size = Vector2(0, 26) s.value_changed.connect(func(x: float): setter.call(x) val_l.text = fmt.call(x) Settings.apply()) row.add_child(s) parent.add_child(row) _sliders[key] = {"slider": s, "label": val_l, "fmt": fmt} func _refresh_settings() -> void: var values := {"mouse": Settings.mouse_sens, "stick": Settings.stick_sens, "fov": Settings.fov, "volume": Settings.volume} for k in values: var e: Dictionary = _sliders[k] e.slider.set_value_no_signal(values[k]) e.label.text = e.fmt.call(values[k]) _invert.set_pressed_no_signal(Settings.invert_y) # ---------- справка по управлению ---------- func _build_controls() -> Control: var v := _vbox() var grid := GridContainer.new() grid.columns = 3 grid.add_theme_constant_override("h_separation", 22) grid.add_theme_constant_override("v_separation", 5) var rows := [ ["", "Клавиатура", "Геймпад Xbox"], ["Движение / обзор", "WASD / мышь", "левый / правый стик"], ["Прыжок, подтягивание", "Пробел", "A или LB"], ["Спринт", "Shift", "RT или L3"], ["Рывок", "Q", "X или RB"], ["Присед (держать)", "C", "B"], ["Подкат (на бегу)", "Shift + C", "RT + B"], ["Зум", "ЛКМ", "LT"], ["«Готов» / забег заново", "R", "Y"], ["Сдаться в гонке (держать)", "G", "крестовина вниз"], ["Таблица лидеров", "Tab", "Back"], ["Ник", "N", "в меню паузы"], ["Пауза", "Esc", "Start"], ] for r in rows.size(): for c in 3: var l := LbView.make_label(rows[r][c], 16, MINE if r == 0 else (TEXT if c == 0 else DIM)) grid.add_child(l) v.add_child(grid) var tip := LbView.make_label("Стены: прыжок в воздухе у стены с зажатым «вперёд» — бег по стене.", 14, DIM) v.add_child(tip) v.add_child(_button("Назад", func(): _show_page("main"))) return v