Геймпад Xbox, меню паузы с настройками, новый хаб с музыкой
- Геймпад: раскладка под паркур и гонки, обзор правым стиком, навигация по меню (A/B/крестовина) - Меню паузы (Esc/Start): выход в хаб, настройки (мышь, стик, FOV, громкость, инверсия), справка по управлению - Хаб 48x32x10 м: колонны, окна с городом, голограмма, лавки; музыка «Local Forecast - Elevator» (Kevin MacLeod, CC-BY) - Тесты: pause_test, pad_menu_test, hub_tour; README обновлён Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
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
|
||||
Reference in New Issue
Block a user