Files

298 lines
11 KiB
GDScript

class_name MainMenu
extends Control
## Главное меню: больше воздуха, бумага, один акцентный блок действий.
## Логотип, слоган, четыре кнопки и короткое «как идёт смена» — без
## служебных подписей, модемов и мелких блоков.
##
## Только UI. Наличие сохранения передаёт интеграция:
## menu.set_has_save(SaveManager.has_save())
signal new_run
signal continue_run
signal settings
signal about
const W := 1280.0
const H := 720.0
## Preload вместо глобального имени: работает и без прогретого class cache.
const VHS_OVERLAY := preload("res://scripts/ui/vhs_overlay.gd")
const WIN_RECT := Rect2(120, 80, 1040, 560)
var has_save := false
## Тонкий VHS-слой поверх меню. Игра и модалки не фильтруются.
var vhs = null
var _btn_continue: Button90
var _btn_new: Button90
var _btn_settings: Button90
var _btn_about: Button90
static var _card_style: StyleBoxFlat = null
## Накопитель для перерисовки VHS на 12 Гц.
var _vhs_clock := 0.0
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
mouse_filter = Control.MOUSE_FILTER_STOP
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_build()
_build_vhs()
resized.connect(_layout)
_layout()
queue_redraw()
func set_has_save(value: bool) -> void:
has_save = value
if _btn_continue != null:
_btn_continue.disabled = not has_save
_btn_continue.tooltip_text = "" if has_save else "Сохранение не найдено"
queue_redraw()
## Включить/выключить VHS-эффект меню (кодом или из настроек).
func set_vhs_enabled(value: bool) -> void:
if vhs != null:
vhs.set_enabled(value)
queue_redraw()
func is_vhs_enabled() -> bool:
return vhs != null and vhs.enabled
## Медленная анимация: полоса и зерно. Перерисовываем меню на 12 Гц —
## чаще для такого эффекта не нужно, а текст остаётся тем же.
func _process(delta: float) -> void:
if vhs == null or not vhs.enabled or not vhs.is_available() or not visible:
return
vhs.advance(delta)
_vhs_clock += delta
if _vhs_clock >= 1.0 / 12.0:
_vhs_clock = 0.0
queue_redraw()
func _build() -> void:
_btn_continue = _make_button("Продолжить")
_btn_continue.disabled = not has_save
_btn_continue.pressed.connect(func() -> void: continue_run.emit())
_btn_new = _make_button("Новый забег")
_btn_new.accent = true
_btn_new.pressed.connect(func() -> void: new_run.emit())
_btn_settings = _make_button(_tr("btn.settings", "Настройки"))
# визуально «активная» кнопка: логику подключит main.gd
_btn_settings.active = true
_btn_settings.pressed.connect(func() -> void: settings.emit())
_btn_about = _make_button("Об игре")
_btn_about.pressed.connect(func() -> void: about.emit())
## VHS-слой — процедурный помощник (RefCounted, без Node/Control).
## Меню рисует его само в _draw(), поэтому ничто не перекрывает меню
## и не перехватывает мышь. Модалки — отдельные соседи в modal_root.
func _build_vhs() -> void:
vhs = VHS_OVERLAY.new()
func _tr(key: String, fallback: String) -> String:
var data := get_node_or_null("/root/Data")
if data != null and data.has_method("has_string") and data.has_string(key):
var value := str(data.tr(key))
if not value.is_empty() and value != key:
return value
return fallback
func _make_button(caption: String) -> Button90:
var b := Button90.new()
b.caption = caption
b.caption_size = 14
add_child(b)
return b
func _window() -> Rect2:
var s := size
if s.x <= 0.0 or s.y <= 0.0:
s = Vector2(W, H)
return Rect2(WIN_RECT.position + ((s - Vector2(W, H)) * 0.5).round(), WIN_RECT.size)
func _layout() -> void:
var w := _window()
var px := w.position.x + 660.0
var py := w.position.y + 112.0
var buttons: Array[Button90] = [_btn_continue, _btn_new, _btn_settings, _btn_about]
for b in buttons:
if b != null:
b.position = Vector2(px, py)
b.size = Vector2(300, 38)
py += 52.0
# --- отрисовка -------------------------------------------------------------
func _draw() -> void:
var full := Rect2(Vector2.ZERO, size)
if size.x <= 0.0 or size.y <= 0.0:
full = Rect2(0, 0, W, H)
# 1. Бумага.
draw_texture_rect(Theme90.dither_texture(), full, true)
# очень мягкие пастельные пятна — цвет в фоне, не в интерфейсе
draw_circle(Vector2(full.size.x * 0.93, full.size.y * 0.10), 250.0,
Color(Theme90.ACCENT_LIGHT.r, Theme90.ACCENT_LIGHT.g, Theme90.ACCENT_LIGHT.b, 0.10))
draw_circle(Vector2(full.size.x * 0.06, full.size.y * 0.96), 300.0,
Color(0.96, 0.80, 0.72, 0.09))
# 2. VHS на бумаге.
if vhs != null:
vhs.draw_layer(self, full)
var w := _window()
var panel := _panel_rect(w)
# 3. Поверхности: карточка, панель, декоративные фигуры.
_draw_card(w)
_draw_actions_surface(w)
_draw_hero_shapes(w)
# 4. Мягкий VHS-проход по карточке и панели — ДО текста, поэтому буквы
# и кнопки остаются резкими.
if vhs != null:
vhs.draw_layer(self, w, true)
vhs.draw_layer(self, panel, true)
# 5. Текст поверх эффекта.
_draw_hero_text(w)
_draw_actions_text(w)
func _panel_rect(w: Rect2) -> Rect2:
return Rect2(w.position.x + 636.0, w.position.y + 44.0, 348, w.size.y - 88.0)
func _draw_card(w: Rect2) -> void:
if _card_style == null:
_card_style = Theme90.box(Theme90.WHITE, Theme90.BORDER, 6, 1,
Color(0, 0, 0, 0.13))
_card_style.shadow_size = 10
_card_style.shadow_offset = Vector2(0, 5)
draw_style_box(_card_style, w)
# тонкая цветная полоска в верхнем левом углу
draw_rect(Rect2(w.position.x + 1, w.position.y + 1, 112, 3), Theme90.ACCENT)
func _draw_hero_shapes(w: Rect2) -> void:
var x := w.position.x + 56.0
# акцентная линейка под логотипом
draw_rect(Rect2(x, w.position.y + 116, 72, 3), Theme90.ACCENT)
draw_rect(Rect2(x + 76, w.position.y + 116, 6, 3), Theme90.ERROR)
# шаги смены: круги, волосяные разделители
var ry := w.position.y + 258.0
for i in 3:
draw_circle(Vector2(x + 12, ry + 12), 12.0, Theme90.ACCENT_PALE)
if i < 2:
draw_rect(Rect2(x + 34, ry + 35, 478, 1), Theme90.HAIRLINE)
ry += 48.0
# стартовая сводка — одна спокойная полоса
var strip := Rect2(x, w.position.y + 412, 512, 28)
draw_style_box(Theme90.box(Theme90.PAPER_HI, Theme90.BORDER_SOFT, 4, 1), strip)
draw_circle(Vector2(strip.position.x + 14, strip.get_center().y), 3.0, Theme90.SUCCESS)
func _draw_hero_text(w: Rect2) -> void:
var f := Theme90.ui()
var x := w.position.x + 56.0
Theme90.text(self, f, Vector2(x, w.position.y + 42),
"ДИСПЕТЧЕР ФАЙЛОВОГО ОБМЕНА · 1997", 10, Theme90.ACCENT)
Theme90.text(self, Theme90.logo(), Vector2(x - 2, w.position.y + 62),
"PEAK HOUR 97", 32, Theme90.NAVY)
var sub := _tr("ui.subtitle", "Час пик. Одна смена. Ни одного свободного места.")
var lines := Theme90.wrap(f, sub, 16, 520, 2)
var sy := w.position.y + 132.0
for line in lines:
Theme90.text(self, f, Vector2(x, sy), line, 16, Theme90.DARK)
sy += 21.0
# короткий блок «как идёт смена»
var ty := w.position.y + 232.0
Theme90.text(self, f, Vector2(x, ty), "КАК ИДЁТ СМЕНА", 10, Theme90.SHADE)
var rows := [
"Принимайте пакеты и раскладывайте файлы по папкам",
"Собирайте запросы клиентов и отправляйте в канал",
"Держите SLA и отражайте атаки на узел",
]
var ry := w.position.y + 258.0
for i in rows.size():
var circ := Vector2(x + 12, ry + 12)
var num := str(i + 1)
var nw := f.get_string_size(num, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
Theme90.text(self, f, Vector2(circ.x - nw * 0.5, circ.y - 9), num, 12, Theme90.NAVY)
Theme90.text(self, f, Vector2(x + 34, ry + 3),
Theme90.fit(f, rows[i], 15, 478), 15, Theme90.INK)
ry += 48.0
var strip := Rect2(x, w.position.y + 412, 512, 28)
Theme90.text_vcenter(self, f, Rect2(strip.position.x + 26, strip.position.y,
strip.size.x - 36, strip.size.y),
"9 часов · $150 на старте · 128 SLA · 4 типа файлов", 11, Theme90.DARK)
# нижняя строка — только один тихий кредит
Theme90.text(self, f, Vector2(x, w.position.y + w.size.y - 28),
"© 1997 «ВЕКТОР-СОФТ» · СБОРКА 12.10.1997", 10, Theme90.SHADE)
func _draw_actions_surface(w: Rect2) -> void:
var panel := _panel_rect(w)
draw_style_box(Theme90.box(Theme90.PAPER_HI, Theme90.BORDER_SOFT, 5, 1), panel)
# едва заметный водяной знак в нижней части панели
Theme90.text(self, Theme90.logo(), Vector2(panel.get_center().x - 44,
panel.end.y - 126), "97", 44, Color(Theme90.NAVY.r, Theme90.NAVY.g,
Theme90.NAVY.b, 0.055))
# вертикальная тонкая линия раздела колонок
draw_rect(Rect2(w.position.x + 612.0, w.position.y + 44.0, 1,
w.size.y - 88.0), Theme90.HAIRLINE)
func _draw_actions_text(w: Rect2) -> void:
var panel := _panel_rect(w)
var f := Theme90.ui()
Theme90.text(self, f, Vector2(panel.position.x + 24, panel.position.y + 22),
"НОЧНАЯ СМЕНА · УЗЕЛ 7", 10, Theme90.ACCENT)
var save_text := "СОХРАНЕНИЕ НАЙДЕНО" if has_save else "СОХРАНЕНИЯ ПОКА НЕТ"
Theme90.text(self, f, Vector2(panel.position.x + 24, panel.position.y + 300),
save_text, 10, Theme90.SUCCESS if has_save else Theme90.SHADE)
# подсказка-слоган внизу панели
var tip := "Начните смену — и держите канал."
var tw := f.get_string_size(tip, HORIZONTAL_ALIGNMENT_LEFT, -1, 11).x
Theme90.text(self, f, Vector2(panel.get_center().x - tw * 0.5,
panel.end.y - 34), tip, 11, Theme90.SHADE)
func _draw_step_icon(index: int, box: Rect2) -> void:
var x := box.position.x + 5.0
var y := box.position.y + 5.0
match index:
0:
draw_style_box(Theme90.box(Theme90.WHITE, Theme90.ACCENT_LIGHT, 2, 1),
Rect2(x, y + 6, 16, 10))
Theme90.draw_arrow(self, Rect2(x + 4, y, 8, 8), 1, Theme90.ACCENT)
1:
Theme90.draw_packet_icon(self, Vector2(x + 1, y + 3), Theme90.ACCENT)
draw_rect(Rect2(x + 12, y + 8, 3, 5), Theme90.WARN)
_:
draw_circle(Vector2(x + 8, y + 8), 8.0, Theme90.SUCCESS)
draw_rect(Rect2(x + 7, y + 4, 2, 6), Theme90.WHITE)
draw_rect(Rect2(x + 7, y + 12, 2, 2), Theme90.WHITE)