VanguardReMaster/client/Assets/Vanguard/Runtime/UI/LobbyView.cs

732 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using Vanguard.Api;
using Vanguard.App;
using Vanguard.Auth;
using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>
/// Главный экран после входа: профиль и балансы, вход в бой/колоды/магазин,
/// заглушки разделов без серверной части и гамбургер-меню.
/// </summary>
public sealed class LobbyView : IDisposable
{
public const string RootName = "LobbyScreen";
private const float Margin = 24f;
private const float LeftWidth = 560f;
private const float RightWidth = 336f;
private const float Gutter = 16f;
private const float ContentTop = 112f;
private const float BottomMargin = 24f;
private const float BalanceHeight = 88f;
private static readonly Color PanelFill = new Color(0.024f, 0.058f, 0.15f, 0.92f);
private static readonly Color PanelFillSoft = new Color(0.035f, 0.08f, 0.19f, 0.55f);
private static readonly Color Border = new Color(0.18f, 0.49f, 1f, 0.55f);
private static readonly Color BorderStrong = new Color(0.34f, 0.72f, 1f, 0.9f);
private static readonly Color TextPrimary = new Color(0.88f, 0.93f, 1f);
private static readonly Color TextMuted = new Color(0.58f, 0.69f, 0.88f);
private static readonly Color TextDim = new Color(0.44f, 0.55f, 0.75f);
private static readonly Color Accent = new Color(0.37f, 0.69f, 1f);
private readonly AuthService _auth;
private readonly PlayerState _player;
private readonly Action _onLoggedOut;
private readonly Action _onStore;
private readonly PlayerTopBar _topBar;
private readonly StatusTicker _ticker;
private readonly RectTransform _root;
private Text _gold;
private Text _dust;
private Text _tokens;
private Text _status;
private bool _disposed;
private bool _refreshing;
public LobbyView(
Transform parent,
AppConfig config,
AuthService auth,
PlayerState player,
Action onLoggedOut,
Action onBattle,
Action onDecks,
Action onStore,
Action onHistory,
Action onRating,
Action onCommunity)
{
_auth = auth;
_player = player;
_onLoggedOut = onLoggedOut;
_onStore = onStore;
_root = UiKit.CreatePanel(parent, RootName, new Color(0f, 0f, 0f, 0f));
_root.GetComponent<Image>().raycastTarget = false;
_topBar = new PlayerTopBar(
_root,
config,
player,
onLoggedOut,
onHistory,
onRating,
onCommunity);
BuildActions(onBattle, onDecks, onStore);
BuildMissions(out _status);
BuildBalances(out _gold, out _dust, out _tokens);
BuildChat();
BuildFriends();
_ticker = _root.gameObject.AddComponent<StatusTicker>();
_ticker.Hide = () => SetStatus(string.Empty);
_player.Changed += UpdateTexts;
Loc.LanguageChanged += UpdateTexts;
UpdateTexts();
_root.gameObject.SetActive(false);
}
public bool IsMenuOpen => _topBar != null && _topBar.IsMenuOpen;
public void CloseMenu()
{
if (_topBar != null)
_topBar.CloseMenu();
}
public void Show()
{
UiKit.SetActive(_root, true);
UpdateTexts();
_ = RefreshAsync();
}
public void Hide() => UiKit.SetActive(_root, false);
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_player.Changed -= UpdateTexts;
Loc.LanguageChanged -= UpdateTexts;
_topBar?.Dispose();
}
// ------------------------------------------------------------ левая колонка
private void BuildActions(Action onBattle, Action onDecks, Action onStore)
{
var actions = NewRect(_root, "Actions");
actions.anchorMin = new Vector2(0f, 1f);
actions.anchorMax = new Vector2(0f, 1f);
actions.pivot = new Vector2(0f, 1f);
actions.sizeDelta = new Vector2(LeftWidth, 428f);
actions.anchoredPosition = new Vector2(Margin, -ContentTop);
ActionCard(
actions,
"BattleCard",
"BATTLE",
"Найдите соперника и сразитесь",
LobbyIcon.Swords,
0f,
true,
onBattle);
ActionCard(
actions,
"DecksCard",
"DECK BUILDER",
"Соберите и настройте колоду",
LobbyIcon.Deck,
-148f,
false,
onDecks);
ActionCard(
actions,
"ShopCard",
"SHOP",
"Бустеры, триал-деки и токены",
LobbyIcon.Shop,
-296f,
false,
onStore);
}
private static void ActionCard(
RectTransform parent,
string name,
string title,
string subtitle,
Action<RectTransform, Color> icon,
float y,
bool primary,
Action onClick)
{
var card = Surface(
parent,
name,
PanelFill,
primary ? BorderStrong : Border,
primary ? 2 : 1,
18);
card.anchorMin = new Vector2(0f, 1f);
card.anchorMax = new Vector2(0f, 1f);
card.pivot = new Vector2(0f, 1f);
card.sizeDelta = new Vector2(LeftWidth, 132f);
card.anchoredPosition = new Vector2(0f, y);
var image = card.GetComponent<Image>();
image.raycastTarget = true;
var button = card.gameObject.AddComponent<Button>();
button.targetGraphic = image;
button.transition = Selectable.Transition.ColorTint;
button.colors = HoverColors();
button.onClick.AddListener(() => onClick());
var iconRect = Icon(card, new Vector2(64f, 64f), new Vector2(-LeftWidth * 0.5f + 56f, 0f), 1f);
icon(iconRect, primary ? BorderStrong : Accent);
var titleText = UiKit.CreateText(
card,
"Title",
title,
27,
TextPrimary,
TextAnchor.MiddleLeft);
titleText.fontStyle = FontStyle.Bold;
var titleRect = titleText.rectTransform;
titleRect.anchorMin = new Vector2(0f, 1f);
titleRect.anchorMax = new Vector2(0f, 1f);
titleRect.pivot = new Vector2(0f, 1f);
titleRect.sizeDelta = new Vector2(380f, 34f);
titleRect.anchoredPosition = new Vector2(104f, -30f);
var subtitleText = UiKit.CreateText(
card,
"Subtitle",
subtitle,
15,
TextMuted,
TextAnchor.MiddleLeft);
var subtitleRect = subtitleText.rectTransform;
subtitleRect.anchorMin = new Vector2(0f, 1f);
subtitleRect.anchorMax = new Vector2(0f, 1f);
subtitleRect.pivot = new Vector2(0f, 1f);
subtitleRect.sizeDelta = new Vector2(420f, 24f);
subtitleRect.anchoredPosition = new Vector2(104f, -72f);
var chevron = Icon(card, new Vector2(18f, 18f), new Vector2(LeftWidth * 0.5f - 34f, 0f), 0.35f);
LobbyIcon.Chevron(chevron, TextMuted);
}
// ------------------------------------------------------------ миссии
private void BuildMissions(out Text status)
{
var panel = Surface(_root, "MissionsPanel", PanelFill, Border, 1, 16);
panel.anchorMin = new Vector2(0f, 1f);
panel.anchorMax = new Vector2(0f, 1f);
panel.pivot = new Vector2(0f, 1f);
panel.sizeDelta = new Vector2(LeftWidth, 396f);
panel.anchoredPosition = new Vector2(Margin, -(ContentTop + 428f + Gutter));
var iconRect = Icon(panel, new Vector2(28f, 28f), new Vector2(-LeftWidth * 0.5f + 30f, 182f), 0.5f);
LobbyIcon.Shield(iconRect, Accent);
var title = UiKit.CreateText(
panel,
"Title",
"Ежедневные / еженедельные задания",
17,
TextPrimary,
TextAnchor.MiddleLeft);
title.fontStyle = FontStyle.Bold;
var titleRect = title.rectTransform;
titleRect.anchorMin = new Vector2(0f, 1f);
titleRect.anchorMax = new Vector2(0f, 1f);
titleRect.pivot = new Vector2(0f, 1f);
titleRect.sizeDelta = new Vector2(380f, 28f);
titleRect.anchoredPosition = new Vector2(50f, -20f);
StubUi.Badge(panel, new Vector2(-16f, -16f));
for (var i = 0; i < 3; i++)
{
var row = Surface(
panel,
"MissionRow" + i,
PanelFillSoft,
new Color(0.16f, 0.42f, 0.85f, 0.3f),
1,
10);
row.anchorMin = new Vector2(0.5f, 1f);
row.anchorMax = new Vector2(0.5f, 1f);
row.pivot = new Vector2(0.5f, 1f);
row.sizeDelta = new Vector2(528f, 88f);
row.anchoredPosition = new Vector2(0f, -62f - i * 96f);
var lockRect = Icon(row, new Vector2(34f, 34f), new Vector2(-264f + 32f, 0f), 0.6f);
LobbyIcon.Lock(lockRect, TextDim);
var rowTitle = UiKit.CreateText(
row,
"Title",
"Задание появится позже",
15,
TextMuted,
TextAnchor.MiddleLeft);
var rowTitleRect = rowTitle.rectTransform;
rowTitleRect.anchorMin = new Vector2(0f, 0.5f);
rowTitleRect.anchorMax = new Vector2(1f, 0.5f);
rowTitleRect.offsetMin = new Vector2(64f, 8f);
rowTitleRect.offsetMax = new Vector2(-20f, 26f);
var rowNote = UiKit.CreateText(
row,
"Note",
"Награды и прогресс — после появления серверных заданий.",
12,
TextDim,
TextAnchor.MiddleLeft);
var rowNoteRect = rowNote.rectTransform;
rowNoteRect.anchorMin = new Vector2(0f, 0.5f);
rowNoteRect.anchorMax = new Vector2(1f, 0.5f);
rowNoteRect.offsetMin = new Vector2(64f, -30f);
rowNoteRect.offsetMax = new Vector2(-20f, -8f);
}
status = UiKit.CreateText(
_root,
"LobbyStatus",
string.Empty,
14,
new Color(1f, 0.62f, 0.5f),
TextAnchor.MiddleCenter);
var statusRect = status.rectTransform;
statusRect.anchorMin = new Vector2(0.5f, 0f);
statusRect.anchorMax = new Vector2(0.5f, 0f);
statusRect.pivot = new Vector2(0.5f, 0f);
statusRect.sizeDelta = new Vector2(900f, 20f);
statusRect.anchoredPosition = new Vector2(0f, 2f);
}
// ------------------------------------------------------------ балансы
private void BuildBalances(out Text gold, out Text dust, out Text tokens)
{
var panel = Surface(
_root,
"Balances",
new Color(0.02f, 0.05f, 0.13f, 0.94f),
Border,
1,
14);
panel.anchorMin = new Vector2(0f, 0f);
panel.anchorMax = new Vector2(0f, 0f);
panel.pivot = new Vector2(0f, 0f);
panel.sizeDelta = new Vector2(LeftWidth, BalanceHeight);
panel.anchoredPosition = new Vector2(Margin, BottomMargin);
var image = panel.GetComponent<Image>();
image.raycastTarget = true;
var button = panel.gameObject.AddComponent<Button>();
button.targetGraphic = image;
button.transition = Selectable.Transition.ColorTint;
button.colors = HoverColors();
button.onClick.AddListener(() => _onStore());
gold = BalanceGroup(panel, "Gold", "Золото", -LeftWidth / 3f, new Color(1f, 0.78f, 0.32f), 0);
dust = BalanceGroup(panel, "Dust", "Пыль", 0f, new Color(0.44f, 0.78f, 1f), 1);
tokens = BalanceGroup(panel, "Tokens", "Токены", LeftWidth / 3f, new Color(0.72f, 0.52f, 1f), 2);
}
private static Text BalanceGroup(
RectTransform parent,
string name,
string caption,
float x,
Color color,
int kind)
{
var group = NewRect(parent, name);
group.anchorMin = new Vector2(0.5f, 0.5f);
group.anchorMax = new Vector2(0.5f, 0.5f);
group.pivot = new Vector2(0.5f, 0.5f);
group.sizeDelta = new Vector2(176f, 64f);
group.anchoredPosition = new Vector2(x, 0f);
if (kind == 0)
{
NeonUi.AddImage(group, "Icon", NeonUi.Circle(22, color), new Vector2(22f, 22f), new Vector2(-64f, 10f));
}
else if (kind == 1)
{
var diamond = NeonUi.AddImage(
group,
"Icon",
NeonUi.Body(16, 16, 3, color, color, 1),
new Vector2(16f, 16f),
new Vector2(-64f, 10f));
diamond.rectTransform.localRotation = Quaternion.Euler(0f, 0f, 45f);
}
else
{
NeonUi.AddImage(group, "Icon", NeonUi.Ring(20, 3f, color), new Vector2(20f, 20f), new Vector2(-64f, 10f));
}
var value = UiKit.CreateText(group, "Value", "0", 22, TextPrimary);
value.fontStyle = FontStyle.Bold;
var valueRect = value.rectTransform;
valueRect.anchorMin = new Vector2(0f, 0.5f);
valueRect.anchorMax = new Vector2(0f, 0.5f);
valueRect.pivot = new Vector2(0f, 0.5f);
valueRect.sizeDelta = new Vector2(120f, 28f);
valueRect.anchoredPosition = new Vector2(-46f, 10f);
UiKit.MakeLiteral(value);
var label = UiKit.CreateText(
group,
"Caption",
caption,
12,
TextDim,
TextAnchor.MiddleLeft);
var labelRect = label.rectTransform;
labelRect.anchorMin = new Vector2(0f, 0.5f);
labelRect.anchorMax = new Vector2(0f, 0.5f);
labelRect.pivot = new Vector2(0f, 0.5f);
labelRect.sizeDelta = new Vector2(140f, 18f);
labelRect.anchoredPosition = new Vector2(-46f, -12f);
return value;
}
// ------------------------------------------------------------ чат и друзья
private void BuildChat()
{
var panel = Surface(
_root,
"ChatPanel",
new Color(0.018f, 0.045f, 0.12f, 0.9f),
Border,
1,
16);
panel.anchorMin = new Vector2(600f / 1920f, 0f);
panel.anchorMax = new Vector2(1560f / 1920f, 1f);
panel.offsetMin = new Vector2(0f, BottomMargin);
panel.offsetMax = new Vector2(0f, -ContentTop);
var headerIcon = Icon(panel, new Vector2(28f, 28f), new Vector2(-450f, 440f), 0.5f);
LobbyIcon.Chat(headerIcon, Accent);
var header = UiKit.CreateText(
panel,
"Title",
"Чат",
20,
TextPrimary,
TextAnchor.MiddleLeft);
header.fontStyle = FontStyle.Bold;
var headerRect = header.rectTransform;
headerRect.anchorMin = new Vector2(0f, 1f);
headerRect.anchorMax = new Vector2(0f, 1f);
headerRect.pivot = new Vector2(0f, 1f);
headerRect.sizeDelta = new Vector2(240f, 30f);
headerRect.anchoredPosition = new Vector2(56f, -16f);
StubUi.Badge(panel, new Vector2(-16f, -14f));
StubUi.Panel(
panel,
"ChatStub",
"Чат появится позже",
"Здесь будет общий чат лобби и личные сообщения. Пока серверного канала нет.");
var input = Surface(
panel,
"ChatInput",
new Color(0.01f, 0.03f, 0.08f, 0.9f),
new Color(0.16f, 0.42f, 0.85f, 0.35f),
1,
10);
input.anchorMin = new Vector2(0f, 0f);
input.anchorMax = new Vector2(1f, 0f);
input.pivot = new Vector2(0.5f, 0f);
input.offsetMin = new Vector2(18f, 18f);
input.offsetMax = new Vector2(-80f, 70f);
var placeholder = UiKit.CreateText(
input,
"Placeholder",
"Введите сообщение…",
16,
TextDim,
TextAnchor.MiddleLeft);
UiKit.Stretch(placeholder.rectTransform);
placeholder.rectTransform.offsetMin = new Vector2(18f, 0f);
placeholder.rectTransform.offsetMax = new Vector2(-18f, 0f);
var send = Surface(
panel,
"ChatSend",
new Color(0.06f, 0.16f, 0.36f, 0.7f),
new Color(0.16f, 0.42f, 0.85f, 0.35f),
1,
10);
send.anchorMin = new Vector2(1f, 0f);
send.anchorMax = new Vector2(1f, 0f);
send.pivot = new Vector2(1f, 0f);
send.sizeDelta = new Vector2(48f, 52f);
send.anchoredPosition = new Vector2(-18f, 18f);
var chevron = Icon(send, new Vector2(18f, 18f), Vector2.zero, 0.35f);
LobbyIcon.Chevron(chevron, TextDim);
}
private void BuildFriends()
{
var panel = Surface(
_root,
"FriendsPanel",
new Color(0.018f, 0.045f, 0.12f, 0.9f),
Border,
1,
16);
panel.anchorMin = new Vector2(1560f / 1920f, 0f);
panel.anchorMax = new Vector2(1896f / 1920f, 1f);
panel.offsetMin = new Vector2(0f, BottomMargin);
panel.offsetMax = new Vector2(0f, -ContentTop);
var iconRect = Icon(panel, new Vector2(28f, 28f), new Vector2(-RightWidth * 0.5f + 30f, 440f), 0.5f);
LobbyIcon.Friends(iconRect, Accent);
var header = UiKit.CreateText(
panel,
"Title",
"Друзья онлайн",
17,
TextPrimary,
TextAnchor.MiddleLeft);
header.fontStyle = FontStyle.Bold;
var headerRect = header.rectTransform;
headerRect.anchorMin = new Vector2(0f, 1f);
headerRect.anchorMax = new Vector2(0f, 1f);
headerRect.pivot = new Vector2(0f, 1f);
headerRect.sizeDelta = new Vector2(240f, 28f);
headerRect.anchoredPosition = new Vector2(52f, -16f);
StubUi.Badge(panel, new Vector2(-14f, -14f));
for (var i = 0; i < 5; i++)
{
var row = Surface(
panel,
"FriendRow" + i,
PanelFillSoft,
new Color(0.16f, 0.42f, 0.85f, 0.22f),
1,
10);
row.anchorMin = new Vector2(0.5f, 1f);
row.anchorMax = new Vector2(0.5f, 1f);
row.pivot = new Vector2(0.5f, 1f);
row.sizeDelta = new Vector2(RightWidth - 24f, 62f);
row.anchoredPosition = new Vector2(0f, -62f - i * 72f);
var avatarRect = Surface(
row,
"Avatar",
new Color(0.06f, 0.14f, 0.3f, 0.9f),
new Color(0.2f, 0.45f, 0.85f, 0.4f),
1,
16);
avatarRect.anchorMin = new Vector2(0f, 0.5f);
avatarRect.anchorMax = new Vector2(0f, 0.5f);
avatarRect.pivot = new Vector2(0f, 0.5f);
avatarRect.sizeDelta = new Vector2(36f, 36f);
avatarRect.anchoredPosition = new Vector2(12f, 0f);
var name = UiKit.CreateText(
row,
"Name",
"—",
14,
TextMuted,
TextAnchor.MiddleLeft);
var nameRect = name.rectTransform;
nameRect.anchorMin = new Vector2(0f, 0.5f);
nameRect.anchorMax = new Vector2(1f, 0.5f);
nameRect.offsetMin = new Vector2(58f, 4f);
nameRect.offsetMax = new Vector2(-12f, 24f);
var state = UiKit.CreateText(
row,
"State",
"скоро",
12,
TextDim,
TextAnchor.MiddleLeft);
var stateRect = state.rectTransform;
stateRect.anchorMin = new Vector2(0f, 0.5f);
stateRect.anchorMax = new Vector2(1f, 0.5f);
stateRect.offsetMin = new Vector2(58f, -26f);
stateRect.offsetMax = new Vector2(-12f, -6f);
}
var note = UiKit.CreateText(
panel,
"Note",
"Список друзей появится вместе с сервером.",
12,
TextDim,
TextAnchor.LowerCenter);
var noteRect = note.rectTransform;
noteRect.anchorMin = new Vector2(0f, 0f);
noteRect.anchorMax = new Vector2(1f, 0f);
noteRect.pivot = new Vector2(0.5f, 0f);
noteRect.offsetMin = new Vector2(12f, 16f);
noteRect.offsetMax = new Vector2(-12f, 16f);
noteRect.sizeDelta = new Vector2(noteRect.sizeDelta.x, 32f);
}
// ------------------------------------------------------------ состояние
private async Task RefreshAsync()
{
if (_refreshing || _disposed)
return;
_refreshing = true;
try
{
await _auth.RefreshMeAsync();
if (!_disposed)
SetStatus(string.Empty);
}
catch (ApiException exception) when (exception.StatusCode == 401)
{
if (!_disposed)
_onLoggedOut();
}
catch (Exception exception)
{
Debug.LogException(exception);
if (!_disposed)
SetStatus("Не удалось обновить профиль. Сервер недоступен?");
}
finally
{
_refreshing = false;
}
}
private void UpdateTexts()
{
if (_disposed)
return;
_gold.text = _player.Gold.ToString();
_dust.text = _player.Dust.ToString();
_tokens.text = _player.Tokens.ToString();
}
private void SetStatus(string text)
{
if (_status == null)
return;
_status.text = text ?? string.Empty;
_ticker.HideAt = string.IsNullOrEmpty(text) ? 0f : Time.unscaledTime + 6f;
}
// ------------------------------------------------------------ утилиты
private static RectTransform NewRect(Transform parent, string name)
{
var go = new GameObject(name, typeof(RectTransform));
var rect = (RectTransform)go.transform;
rect.SetParent(parent, false);
return rect;
}
private static RectTransform Surface(
Transform parent,
string name,
Color fill,
Color border,
int borderWidth,
int radius)
{
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
var rect = (RectTransform)go.transform;
rect.SetParent(parent, false);
var image = go.GetComponent<Image>();
image.sprite = radius > 0
? NeonUi.BodySliced(radius, fill, border, borderWidth)
: NeonUi.BodySliced(0, fill, border, borderWidth);
image.type = Image.Type.Sliced;
image.color = Color.white;
image.raycastTarget = false;
return rect;
}
private static RectTransform Icon(
Transform parent,
Vector2 size,
Vector2 position,
float scale)
{
var rect = NewRect(parent, "Icon");
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.sizeDelta = size;
rect.anchoredPosition = position;
rect.localScale = Vector3.one * scale;
return rect;
}
private static ColorBlock HoverColors()
{
return new ColorBlock
{
normalColor = Color.white,
highlightedColor = new Color(1.16f, 1.16f, 1.16f, 1f),
pressedColor = new Color(0.9f, 0.9f, 0.9f, 1f),
selectedColor = Color.white,
disabledColor = new Color(0.55f, 0.55f, 0.55f, 0.6f),
colorMultiplier = 1f,
fadeDuration = 0.08f,
};
}
/// <summary>Скрывает статус через несколько секунд.</summary>
private sealed class StatusTicker : MonoBehaviour
{
public float HideAt;
public Action Hide;
private void Update()
{
if (HideAt <= 0f || Time.unscaledTime < HideAt)
return;
HideAt = 0f;
Hide?.Invoke();
}
}
}
}