diff --git a/client/Assets/Vanguard/Editor/SceneBuilder.cs b/client/Assets/Vanguard/Editor/SceneBuilder.cs index db98946..22792f4 100644 --- a/client/Assets/Vanguard/Editor/SceneBuilder.cs +++ b/client/Assets/Vanguard/Editor/SceneBuilder.cs @@ -36,6 +36,7 @@ namespace Vanguard.Editor Create("Login"); Create("Lobby"); + Create("Community"); Create("DeckBuilder"); Create("Battle"); EnsurePlaymatConfig(); @@ -46,6 +47,7 @@ namespace Vanguard.Editor Add(scenes, BootPath); Add(scenes, SceneDir + "/Login.unity"); Add(scenes, SceneDir + "/Lobby.unity"); + Add(scenes, SceneDir + "/Community.unity"); Add(scenes, SceneDir + "/DeckBuilder.unity"); Add(scenes, SceneDir + "/Battle.unity"); diff --git a/client/Assets/Vanguard/Editor/SceneSmoke.cs b/client/Assets/Vanguard/Editor/SceneSmoke.cs index 06930f0..ee413fc 100644 --- a/client/Assets/Vanguard/Editor/SceneSmoke.cs +++ b/client/Assets/Vanguard/Editor/SceneSmoke.cs @@ -5,6 +5,7 @@ using UnityEngine; using UnityEngine.SceneManagement; using Vanguard.App; using Vanguard.Auth; +using Vanguard.UI; namespace Vanguard.Editor { @@ -116,20 +117,22 @@ namespace Vanguard.Editor var active = SceneManager.GetActiveScene().name; var environment = GameObject.Find("Vanguard 3D Environment"); var backdrop = GameObject.Find("LoginBackdrop"); + var lobbyBackdrop = GameObject.Find(LobbyBackdrop.RootName); var login = GameObject.Find("LoginScreen"); - var lobby = GameObject.Find("MetaScreen"); + var lobby = GameObject.Find(LobbyView.RootName); var onboarding = GameObject.Find("OnboardingScreen"); var isLogin = active == SceneFlow.Login; var isLobby = active == SceneFlow.Lobby; var success = (isLogin || isLobby) && (login != null || lobby != null || onboarding != null) - && (isLogin ? backdrop != null : environment != null); + && (isLogin ? backdrop != null : lobbyBackdrop != null); Debug.Log( "SCENE_SMOKE success=" + success + " active=" + active + " env=" + (environment != null) + " backdrop=" + (backdrop != null) + + " lobbyBackdrop=" + (lobbyBackdrop != null) + " login=" + (login != null) + " lobby=" + (lobby != null) + " onboarding=" + (onboarding != null) diff --git a/client/Assets/Vanguard/Runtime/App/CommunitySceneController.cs b/client/Assets/Vanguard/Runtime/App/CommunitySceneController.cs new file mode 100644 index 0000000..8a380ad --- /dev/null +++ b/client/Assets/Vanguard/Runtime/App/CommunitySceneController.cs @@ -0,0 +1,21 @@ +using UnityEngine; +using Vanguard.UI; + +namespace Vanguard.App +{ + /// Сцена Community: гайды, публичные колоды и тикеты. + public sealed class CommunitySceneController : SceneControllerBase + { + private CommunityView _view; + + private void Start() + { + var canvas = CreateCanvas("Community Canvas"); + LobbyBackdrop.Create(canvas.transform); + _view = new CommunityView( + canvas.transform, + () => _ = Flow.LoadLobbyAsync()); + _view.Show(); + } + } +} diff --git a/client/Assets/Vanguard/Runtime/App/CommunitySceneController.cs.meta b/client/Assets/Vanguard/Runtime/App/CommunitySceneController.cs.meta new file mode 100644 index 0000000..d5aae18 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/App/CommunitySceneController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 001ba3cec4018cb44b121859d9b38d08 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs b/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs index ae8fc70..95a7ce9 100644 --- a/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs +++ b/client/Assets/Vanguard/Runtime/App/LobbySceneController.cs @@ -13,7 +13,10 @@ namespace Vanguard.App private AuthService _auth; private MetaService _meta; private PlayerState _player; - private MetaView _metaView; + private AppConfig _config; + private LobbyView _lobby; + private BattleEntryOverlay _battleEntry; + private ComingSoonOverlay _rating; private OnboardingView _onboarding; private QueueView _queue; private StoreView _store; @@ -21,23 +24,41 @@ namespace Vanguard.App private void Start() { - SceneEnvironment.Build(SceneTheme.Lobby); - _auth = Resolve(); _meta = Resolve(); _player = Resolve(); - var canvas = CreateCanvas("Lobby Canvas"); + _config = Resolve(); - _metaView = new MetaView( + var canvas = CreateCanvas("Lobby Canvas"); + LobbyBackdrop.Create(canvas.transform); + + _lobby = new LobbyView( canvas.transform, + _config, _auth, _player, OnLogout, + ShowBattleEntry, () => _ = Flow.LoadDeckBuilderAsync(), ShowStore, ShowHistory, - () => _ = Flow.LoadDeckBuilderAsync(), - ShowOnboarding); + ShowRating, + ShowCommunity); + + _battleEntry = new BattleEntryOverlay( + canvas.transform, + _meta, + _auth, + OnRanked, + OnPracticeStarted, + () => _ = Flow.LoadDeckBuilderAsync()); + + _rating = new ComingSoonOverlay( + canvas.transform, + "RatingOverlay", + "Рейтинг", + "Топ игроков по рейтингу появится позже: нужен серверный лидерборд."); + _onboarding = new OnboardingView( canvas.transform, _auth, _meta, OnOnboarded, OnLogout); _queue = new QueueView( @@ -47,6 +68,9 @@ namespace Vanguard.App _history = new HistoryView( canvas.transform, _meta, _auth, OnHistoryBack); + var hotkeys = canvas.gameObject.AddComponent(); + hotkeys.Cancel = CloseTopmost; + ShowDefault(); } @@ -62,15 +86,24 @@ namespace Vanguard.App HideAll(); if (_player.OnboardingCompleted) - _metaView.Show(); + _lobby.Show(); else _onboarding.Show(); } - private void ShowOnboarding() + private void ShowBattleEntry() { - HideAll(); - _onboarding.Show(); + _battleEntry.Show(); + } + + private void ShowRating() + { + _rating.Show(); + } + + private void ShowCommunity() + { + _ = Flow.LoadCommunityAsync(); } private void ShowStore() @@ -82,7 +115,7 @@ namespace Vanguard.App private void OnStoreBack() { _store.Hide(); - _metaView.Show(); + _lobby.Show(); } private void ShowHistory() @@ -94,19 +127,30 @@ namespace Vanguard.App private void OnHistoryBack() { _history.Hide(); - _metaView.Show(); + _lobby.Show(); } private void OnOnboarded() { HideAll(); - _metaView.Show(); + _lobby.Show(); } private void OnQueueBack() { _queue.Hide(); - _metaView.Show(); + _lobby.Show(); + } + + private void OnRanked(DeckDto deck) + { + HideAll(); + _queue.Show(deck); + } + + private void OnPracticeStarted(MatchConnectionInfo info) + { + _ = Flow.LoadBattleAsync(info); } private void OnMatched(MatchConnectionInfo info) @@ -119,18 +163,38 @@ namespace Vanguard.App _ = Flow.LoadLoginAsync(); } + private void CloseTopmost() + { + if (_lobby != null && _lobby.IsMenuOpen) + { + _lobby.CloseMenu(); + return; + } + + if (_battleEntry != null && _battleEntry.IsOpen) + { + _battleEntry.Close(); + return; + } + + if (_rating != null && _rating.IsOpen) + _rating.Close(); + } + private void HideAll() { - _metaView.Hide(); + _lobby.Hide(); _onboarding.Hide(); _queue.Hide(); _store.Hide(); _history.Hide(); + _battleEntry.Close(); + _rating.Close(); } private void OnDestroy() { - _metaView?.Dispose(); + _lobby?.Dispose(); _store?.Dispose(); } } diff --git a/client/Assets/Vanguard/Runtime/App/SceneFlow.cs b/client/Assets/Vanguard/Runtime/App/SceneFlow.cs index 9c0c996..491da7f 100644 --- a/client/Assets/Vanguard/Runtime/App/SceneFlow.cs +++ b/client/Assets/Vanguard/Runtime/App/SceneFlow.cs @@ -16,6 +16,7 @@ namespace Vanguard.App public const string Boot = "Boot"; public const string Login = "Login"; public const string Lobby = "Lobby"; + public const string Community = "Community"; public const string DeckBuilder = "DeckBuilder"; public const string Battle = "Battle"; public const string PlaymatSandbox = "GameBoard3D"; @@ -32,6 +33,8 @@ namespace Vanguard.App public Task LoadLobbyAsync() => LoadAsync(Lobby); + public Task LoadCommunityAsync() => LoadAsync(Community); + public Task LoadDeckBuilderAsync() => LoadAsync(DeckBuilder); public Task OpenLobbyWithQueueAsync(DeckDto deck) diff --git a/client/Assets/Vanguard/Runtime/UI/BattleEntryOverlay.cs b/client/Assets/Vanguard/Runtime/UI/BattleEntryOverlay.cs new file mode 100644 index 0000000..b6844bc --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/BattleEntryOverlay.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.UI; +using Vanguard.Api; +using Vanguard.Api.Contracts; +using Vanguard.Auth; +using Vanguard.Battle; +using Vanguard.Meta; + +namespace Vanguard.UI +{ + /// + /// Оверлей входа в бой: выбор колоды и режима (рейтинговый или с ботом). + /// Колода проверяется сервером до запуска. + /// + public sealed class BattleEntryOverlay + { + public const string RootName = "BattleEntryOverlay"; + + private static readonly Color PanelFill = new Color(0.024f, 0.058f, 0.15f, 0.98f); + private static readonly Color Border = new Color(0.18f, 0.49f, 1f, 0.7f); + 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 readonly MetaService _meta; + private readonly AuthService _auth; + private readonly Action _onRanked; + private readonly Action _onPractice; + private readonly Action _onBuildDeck; + + private readonly RectTransform _root; + private readonly RectTransform _deckContent; + private readonly Text _status; + private readonly Button _play; + private readonly Text _playLabel; + private readonly Button _rankedMode; + private readonly Button _botMode; + + private readonly List _decks = new List(); + private DeckDto _selected; + private bool? _valid; + private bool _ranked = true; + private bool _busy; + private bool _loading; + private int _validationVersion; + + public BattleEntryOverlay( + Transform parent, + MetaService meta, + AuthService auth, + Action onRanked, + Action onPractice, + Action onBuildDeck) + { + _meta = meta; + _auth = auth; + _onRanked = onRanked; + _onPractice = onPractice; + _onBuildDeck = onBuildDeck; + + _root = UiKit.CreatePanel(parent, RootName, new Color(0f, 0f, 0f, 0.001f)); + _root.GetComponent().raycastTarget = true; + + var panel = UiKit.CreateCenteredCard( + _root, + "Panel", + new Vector2(820f, 720f), + Color.white); + var panelImage = panel.GetComponent(); + panelImage.sprite = NeonUi.BodySliced(20, PanelFill, Border, 2); + panelImage.type = Image.Type.Sliced; + panelImage.raycastTarget = true; + + var title = UiKit.CreateText(panel, "Title", "В бой", 34, TextPrimary); + title.fontStyle = FontStyle.Bold; + PlaceTop(title.rectTransform, new Vector2(0f, -26f), 48f); + + var subtitle = UiKit.CreateText( + panel, + "Subtitle", + "Выберите колоду и режим", + 15, + TextMuted); + PlaceTop(subtitle.rectTransform, new Vector2(0f, -72f), 24f); + + var close = UiKit.CreateButton(panel, "Close", "×", () => Close(), 22); + var closeRect = close.GetComponent(); + closeRect.anchorMin = new Vector2(1f, 1f); + closeRect.anchorMax = new Vector2(1f, 1f); + closeRect.pivot = new Vector2(1f, 1f); + closeRect.sizeDelta = new Vector2(40f, 40f); + closeRect.anchoredPosition = new Vector2(-20f, -20f); + var closeImage = close.GetComponent(); + if (closeImage != null) + { + closeImage.sprite = NeonUi.BodySliced( + 10, + new Color(0.08f, 0.14f, 0.26f, 0.9f), + Border, + 1); + closeImage.type = Image.Type.Sliced; + closeImage.color = Color.white; + } + var closeLabel = close.GetComponentInChildren(); + if (closeLabel != null) + UiKit.MakeLiteral(closeLabel); + + _rankedMode = ModeButton(panel, "ModeRanked", "Рейтинговый", -160f, true); + _botMode = ModeButton(panel, "ModeBot", "С ботом", 160f, false); + _rankedMode.onClick.AddListener(() => SetMode(true)); + _botMode.onClick.AddListener(() => SetMode(false)); + + var scroll = UiKit.CreateScrollView(panel, "DeckScroll", out _deckContent, 8f); + scroll.anchorMin = new Vector2(0f, 0f); + scroll.anchorMax = new Vector2(1f, 1f); + scroll.offsetMin = new Vector2(30f, 196f); + scroll.offsetMax = new Vector2(-30f, -170f); + + _status = UiKit.CreateText( + panel, + "Status", + string.Empty, + 16, + TextMuted, + TextAnchor.MiddleCenter); + var statusRect = _status.rectTransform; + statusRect.anchorMin = new Vector2(0f, 0f); + statusRect.anchorMax = new Vector2(1f, 0f); + statusRect.pivot = new Vector2(0.5f, 0f); + statusRect.offsetMin = new Vector2(30f, 140f); + statusRect.offsetMax = new Vector2(-30f, 172f); + + var build = UiKit.CreateButton( + panel, + "BuildDeck", + "Собрать колоду", + () => + { + Close(); + _onBuildDeck(); + }, + 16); + var buildRect = build.GetComponent(); + buildRect.anchorMin = new Vector2(0.5f, 0f); + buildRect.anchorMax = new Vector2(0.5f, 0f); + buildRect.pivot = new Vector2(0.5f, 0f); + buildRect.sizeDelta = new Vector2(240f, 48f); + buildRect.anchoredPosition = new Vector2(-200f, 36f); + var buildImage = build.GetComponent(); + if (buildImage != null) + ApplyFill(buildImage, new Color(0.06f, 0.12f, 0.24f, 0.95f), Border); + + _play = UiKit.CreateButton(panel, "Play", "В бой", Play, 20); + var playRect = _play.GetComponent(); + playRect.anchorMin = new Vector2(0.5f, 0f); + playRect.anchorMax = new Vector2(0.5f, 0f); + playRect.pivot = new Vector2(0.5f, 0f); + playRect.sizeDelta = new Vector2(300f, 56f); + playRect.anchoredPosition = new Vector2(130f, 30f); + var playImage = _play.GetComponent(); + if (playImage != null) + ApplyFill(playImage, new Color(0.10f, 0.37f, 0.79f, 1f), Border); + _playLabel = _play.GetComponentInChildren(); + + UpdateButtons(); + _root.gameObject.SetActive(false); + } + + public bool IsOpen => _root.gameObject.activeSelf; + + public void Show() + { + UiKit.SetActive(_root, true); + _selected = null; + _valid = null; + SetStatus(string.Empty); + UpdateButtons(); + _ = LoadDecksAsync(); + } + + public void Close() => UiKit.SetActive(_root, false); + + // ------------------------------------------------------------ колоды + + private async Task LoadDecksAsync() + { + if (_loading) + return; + + _loading = true; + SetStatus("Загружаем колоды…"); + try + { + await _auth.EnsureAccessAsync(); + var decks = await _meta.GetDecksAsync(); + _decks.Clear(); + if (decks != null) + _decks.AddRange(decks); + + RebuildDeckList(); + + if (_decks.Count == 0) + SetStatus("Нет колод. Соберите колоду."); + else + SetStatus(string.Empty); + } + catch (ApiException exception) + { + SetStatus(exception.Message); + } + catch (Exception exception) + { + Debug.LogException(exception); + SetStatus("Не удалось загрузить колоды."); + } + finally + { + _loading = false; + } + } + + private void RebuildDeckList() + { + for (var i = _deckContent.childCount - 1; i >= 0; i--) + UnityEngine.Object.Destroy(_deckContent.GetChild(i).gameObject); + + foreach (var deck in _decks) + { + var item = UiKit.CreateButton( + _deckContent, + "Deck_" + deck.Id, + string.Empty, + () => Select(deck), + 15); + var image = item.GetComponent(); + ApplyFill(image, DeckFill(false), Border); + + var element = item.gameObject.AddComponent(); + element.preferredHeight = 64f; + + var name = UiKit.CreateText( + item.transform, + "Name", + deck.Name, + 17, + TextPrimary, + TextAnchor.MiddleLeft); + var nameRect = name.rectTransform; + nameRect.anchorMin = new Vector2(0f, 0.5f); + nameRect.anchorMax = new Vector2(0.7f, 0.5f); + nameRect.offsetMin = new Vector2(18f, 4f); + nameRect.offsetMax = new Vector2(0f, 24f); + UiKit.MakeLiteral(name); + + var count = UiKit.CreateText( + item.transform, + "Count", + deck.Cards.Count + Loc.Translate(" карт"), + 13, + TextDim, + TextAnchor.MiddleRight); + var countRect = count.rectTransform; + countRect.anchorMin = new Vector2(0.7f, 0.5f); + countRect.anchorMax = new Vector2(1f, 0.5f); + countRect.offsetMin = new Vector2(0f, 4f); + countRect.offsetMax = new Vector2(-18f, 24f); + UiKit.MakeLiteral(count); + } + } + + private async void Select(DeckDto deck) + { + if (_busy) + return; + + _selected = deck; + _valid = null; + UpdateButtons(); + HighlightSelection(); + + var version = ++_validationVersion; + SetStatus("Проверяем колоду…"); + try + { + var result = await _meta.ValidateDeckAsync(deck.Id, deck.Cards); + if (version != _validationVersion) + return; + + _valid = result != null && result.Valid; + SetStatus(_valid == true + ? "Колода готова." + : FirstViolation(result)); + } + catch (ApiException exception) + { + _valid = false; + SetStatus(exception.Message); + } + catch (Exception exception) + { + Debug.LogException(exception); + _valid = false; + SetStatus("Не удалось проверить колоду."); + } + + UpdateButtons(); + } + + private static string FirstViolation(DeckValidationResponse result) + { + if (result == null || result.Violations == null || result.Violations.Count == 0) + return "Колода не прошла проверку."; + + return result.Violations[0].Message; + } + + private void HighlightSelection() + { + foreach (Transform child in _deckContent) + { + var image = child.GetComponent(); + if (image == null) + continue; + + var selected = _selected != null + && child.name == "Deck_" + _selected.Id; + ApplyFill(image, DeckFill(selected), selected ? BorderStrong : Border); + } + } + + private static Color DeckFill(bool selected) => selected + ? new Color(0.16f, 0.40f, 0.80f, 0.95f) + : new Color(0.05f, 0.10f, 0.22f, 0.9f); + + private static readonly Color BorderStrong = new Color(0.34f, 0.72f, 1f, 0.9f); + + private static void ApplyFill(Image image, Color fill, Color border) + { + image.sprite = NeonUi.BodySliced(12, fill, border, 1); + image.type = Image.Type.Sliced; + image.color = Color.white; + } + + // ------------------------------------------------------------ режим и запуск + + private Button ModeButton( + Transform parent, + string name, + string label, + float x, + bool active) + { + var button = UiKit.CreateButton(parent, name, label, null, 16); + var rect = button.GetComponent(); + rect.anchorMin = new Vector2(0.5f, 1f); + rect.anchorMax = new Vector2(0.5f, 1f); + rect.pivot = new Vector2(0.5f, 1f); + rect.sizeDelta = new Vector2(300f, 52f); + rect.anchoredPosition = new Vector2(x, -108f); + + var image = button.GetComponent(); + ApplyFill(image, DeckFill(active), active ? BorderStrong : Border); + return button; + } + + private void SetMode(bool ranked) + { + _ranked = ranked; + ApplyFill(_rankedMode.GetComponent(), DeckFill(ranked), ranked ? BorderStrong : Border); + ApplyFill(_botMode.GetComponent(), DeckFill(!ranked), !ranked ? BorderStrong : Border); + } + + private async void Play() + { + if (_busy || _selected == null || _valid != true) + return; + + if (_ranked) + { + var deck = _selected; + Close(); + _onRanked(deck); + return; + } + + _busy = true; + SetStatus("Готовим тренировочный матч…"); + UpdateButtons(); + try + { + await _auth.EnsureAccessAsync(); + var response = await _meta.StartPracticeAsync(_selected.Id); + var deckName = _selected.Name; + Close(); + _onPractice(new MatchConnectionInfo + { + MatchId = response.MatchId, + Seat = response.Seat, + Ticket = response.Ticket, + WsUrl = response.WsUrl, + DeckName = deckName, + }); + } + catch (ApiException exception) + { + SetStatus(exception.Message); + } + catch (Exception exception) + { + Debug.LogException(exception); + SetStatus("Не удалось начать матч с ботом."); + } + finally + { + _busy = false; + UpdateButtons(); + } + } + + private void UpdateButtons() + { + var ready = !_busy && _selected != null && _valid == true; + _play.interactable = ready; + if (_playLabel != null) + _playLabel.color = ready + ? Color.white + : new Color(1f, 1f, 1f, 0.45f); + } + + private void SetStatus(string text) + { + if (_status != null) + _status.text = text ?? string.Empty; + } + + private static void PlaceTop(RectTransform rect, Vector2 position, float height) + { + rect.anchorMin = new Vector2(0f, 1f); + rect.anchorMax = new Vector2(1f, 1f); + rect.pivot = new Vector2(0.5f, 1f); + rect.sizeDelta = new Vector2(-60f, height); + rect.anchoredPosition = position; + } + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/BattleEntryOverlay.cs.meta b/client/Assets/Vanguard/Runtime/UI/BattleEntryOverlay.cs.meta new file mode 100644 index 0000000..d5e5398 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/BattleEntryOverlay.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fb9a2911fede84d419e8ca7de4164fad \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/ComingSoonOverlay.cs b/client/Assets/Vanguard/Runtime/UI/ComingSoonOverlay.cs new file mode 100644 index 0000000..0cedc02 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/ComingSoonOverlay.cs @@ -0,0 +1,85 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace Vanguard.UI +{ + /// Модальная заглушка раздела: заголовок, пояснение и «скоро». + public sealed class ComingSoonOverlay + { + private readonly RectTransform _root; + + public ComingSoonOverlay( + Transform parent, + string name, + string title, + string note, + Action onClose = null) + { + _root = UiKit.CreatePanel(parent, name, new Color(0f, 0f, 0f, 0.001f)); + _root.GetComponent().raycastTarget = true; + + var panel = UiKit.CreateCenteredCard( + _root, + "Panel", + new Vector2(720f, 420f), + Color.white); + panel.GetComponent().sprite = NeonUi.Body( + 720, + 420, + 20, + new Color(0.024f, 0.058f, 0.15f, 0.97f), + new Color(0.18f, 0.49f, 1f, 0.85f), + 2); + panel.GetComponent().raycastTarget = true; + + var titleText = UiKit.CreateText( + panel, + "Title", + title, + 34, + new Color(0.88f, 0.94f, 1f)); + titleText.fontStyle = FontStyle.Bold; + var titleRect = titleText.rectTransform; + titleRect.anchorMin = new Vector2(0f, 1f); + titleRect.anchorMax = new Vector2(1f, 1f); + titleRect.pivot = new Vector2(0.5f, 1f); + titleRect.sizeDelta = new Vector2(-64f, 56f); + titleRect.anchoredPosition = new Vector2(0f, -34f); + + var noteText = UiKit.CreateText( + panel, + "Note", + note, + 18, + new Color(0.60f, 0.70f, 0.88f), + TextAnchor.UpperCenter); + var noteRect = noteText.rectTransform; + noteRect.anchorMin = new Vector2(0.5f, 0.5f); + noteRect.anchorMax = new Vector2(0.5f, 0.5f); + noteRect.pivot = new Vector2(0.5f, 0.5f); + noteRect.sizeDelta = new Vector2(600f, 120f); + noteRect.anchoredPosition = new Vector2(0f, 10f); + + StubUi.Badge(panel, new Vector2(-18f, -18f)); + + var close = UiKit.CreateButton(panel, "Close", "Закрыть", () => Close(), 18); + var closeRect = close.GetComponent(); + closeRect.anchorMin = new Vector2(0.5f, 0f); + closeRect.anchorMax = new Vector2(0.5f, 0f); + closeRect.pivot = new Vector2(0.5f, 0f); + closeRect.sizeDelta = new Vector2(240f, 48f); + closeRect.anchoredPosition = new Vector2(0f, 28f); + if (onClose != null) + close.onClick.AddListener(() => onClose()); + + _root.gameObject.SetActive(false); + } + + public bool IsOpen => _root.gameObject.activeSelf; + + public void Show() => UiKit.SetActive(_root, true); + + public void Close() => UiKit.SetActive(_root, false); + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/ComingSoonOverlay.cs.meta b/client/Assets/Vanguard/Runtime/UI/ComingSoonOverlay.cs.meta new file mode 100644 index 0000000..47ad869 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/ComingSoonOverlay.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b6e3834537fd38641aebc38d141e1c64 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/CommunityView.cs b/client/Assets/Vanguard/Runtime/UI/CommunityView.cs new file mode 100644 index 0000000..7360fa4 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/CommunityView.cs @@ -0,0 +1,202 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace Vanguard.UI +{ + public enum CommunityTab + { + Guides, + Decks, + Tickets, + } + + /// + /// Сцена Community: вкладки гайдов, публичных колод и тикетов. Пока это + /// каркас — серверных данных для разделов нет. + /// + public sealed class CommunityView + { + public const string RootName = "CommunityScreen"; + + private static readonly Color PanelFill = new Color(0.024f, 0.058f, 0.15f, 0.92f); + 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 TextDim = new Color(0.44f, 0.55f, 0.75f); + + private readonly RectTransform _root; + private readonly RectTransform[] _contents; + private readonly Button[] _tabs; + + public CommunityView(Transform parent, Action onBack) + { + _root = UiKit.CreatePanel(parent, RootName, new Color(0f, 0f, 0f, 0f)); + _root.GetComponent().raycastTarget = false; + + var top = Surface(_root, "TopBar", new Color(0.012f, 0.03f, 0.08f, 0.94f), 0); + top.anchorMin = new Vector2(0f, 1f); + top.anchorMax = new Vector2(1f, 1f); + top.pivot = new Vector2(0.5f, 1f); + top.sizeDelta = new Vector2(0f, 88f); + top.anchoredPosition = Vector2.zero; + + var back = UiKit.CreateButton(top, "Back", "← Назад", () => onBack(), 18); + var backRect = back.GetComponent(); + backRect.anchorMin = new Vector2(0f, 0.5f); + backRect.anchorMax = new Vector2(0f, 0.5f); + backRect.pivot = new Vector2(0f, 0.5f); + backRect.sizeDelta = new Vector2(150f, 44f); + backRect.anchoredPosition = new Vector2(24f, 0f); + ApplyFill(back.GetComponent(), new Color(0.06f, 0.12f, 0.24f, 0.95f), Border); + + var title = UiKit.CreateText( + top, + "Title", + "Сообщество", + 30, + TextPrimary, + TextAnchor.MiddleLeft); + title.fontStyle = FontStyle.Bold; + var titleRect = title.rectTransform; + titleRect.anchorMin = new Vector2(0f, 0.5f); + titleRect.anchorMax = new Vector2(0f, 0.5f); + titleRect.pivot = new Vector2(0f, 0.5f); + titleRect.sizeDelta = new Vector2(420f, 44f); + titleRect.anchoredPosition = new Vector2(196f, 0f); + + var hint = UiKit.CreateText( + top, + "Hint", + "Гайды, колоды и обратная связь", + 15, + TextDim, + TextAnchor.MiddleRight); + var hintRect = hint.rectTransform; + hintRect.anchorMin = new Vector2(1f, 0.5f); + hintRect.anchorMax = new Vector2(1f, 0.5f); + hintRect.pivot = new Vector2(1f, 0.5f); + hintRect.sizeDelta = new Vector2(420f, 30f); + hintRect.anchoredPosition = new Vector2(-24f, 0f); + + var tabs = NewRect(_root, "Tabs"); + tabs.anchorMin = new Vector2(0f, 1f); + tabs.anchorMax = new Vector2(0f, 1f); + tabs.pivot = new Vector2(0f, 1f); + tabs.sizeDelta = new Vector2(900f, 56f); + tabs.anchoredPosition = new Vector2(24f, -104f); + + _tabs = new Button[3]; + _contents = new RectTransform[3]; + for (var i = 0; i < 3; i++) + { + var index = i; + var label = TabTitle((CommunityTab)i); + var tab = UiKit.CreateButton(tabs, "Tab" + i, label, () => SelectTab((CommunityTab)index), 17); + var rect = tab.GetComponent(); + rect.anchorMin = new Vector2(0f, 0.5f); + rect.anchorMax = new Vector2(0f, 0.5f); + rect.pivot = new Vector2(0f, 0.5f); + rect.sizeDelta = new Vector2(250f, 52f); + rect.anchoredPosition = new Vector2(i * 266f, 0f); + _tabs[i] = tab; + } + + var body = Surface(_root, "Body", PanelFill, 16); + body.anchorMin = new Vector2(0f, 0f); + body.anchorMax = new Vector2(1f, 1f); + body.offsetMin = new Vector2(24f, 24f); + body.offsetMax = new Vector2(-24f, -180f); + + for (var i = 0; i < 3; i++) + { + var tab = (CommunityTab)i; + _contents[i] = StubUi.Panel( + body, + "Content" + i, + TabTitle(tab), + TabNote(tab)); + } + + SelectTab(CommunityTab.Guides); + _root.gameObject.SetActive(false); + } + + public CommunityTab Current { get; private set; } + + public void Show() => UiKit.SetActive(_root, true); + + public void Hide() => UiKit.SetActive(_root, false); + + public void SelectTab(CommunityTab tab) + { + Current = tab; + for (var i = 0; i < 3; i++) + { + var active = (CommunityTab)i == tab; + _contents[i].gameObject.SetActive(active); + ApplyFill( + _tabs[i].GetComponent(), + active + ? new Color(0.16f, 0.40f, 0.80f, 0.95f) + : new Color(0.05f, 0.10f, 0.22f, 0.9f), + active ? BorderStrong : Border); + } + } + + private static string TabTitle(CommunityTab tab) + { + switch (tab) + { + case CommunityTab.Decks: + return "Колоды"; + case CommunityTab.Tickets: + return "Тикеты"; + default: + return "Гайды"; + } + } + + private static string TabNote(CommunityTab tab) + { + switch (tab) + { + case CommunityTab.Decks: + return "Публичные колоды появятся позже. Сейчас колодой можно поделиться по коду в редакторе."; + case CommunityTab.Tickets: + return "Отправка сообщений о багах и улучшениях появится позже: нужен серверный раздел тикетов."; + default: + return "Разборы карт, колод и механик появятся позже. Материалы будут открываться прямо здесь."; + } + } + + 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, 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.sprite = NeonUi.BodySliced(radius, fill, Border, 1); + image.type = Image.Type.Sliced; + image.color = Color.white; + image.raycastTarget = false; + return rect; + } + + private static void ApplyFill(Image image, Color fill, Color border) + { + image.sprite = NeonUi.BodySliced(12, fill, border, 1); + image.type = Image.Type.Sliced; + image.color = Color.white; + } + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/CommunityView.cs.meta b/client/Assets/Vanguard/Runtime/UI/CommunityView.cs.meta new file mode 100644 index 0000000..58c8209 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/CommunityView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 61a538c7871f1924ea1f9671cf287c69 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/LoginMenu.cs b/client/Assets/Vanguard/Runtime/UI/HamburgerMenu.cs similarity index 72% rename from client/Assets/Vanguard/Runtime/UI/LoginMenu.cs rename to client/Assets/Vanguard/Runtime/UI/HamburgerMenu.cs index 74cb88e..166440b 100644 --- a/client/Assets/Vanguard/Runtime/UI/LoginMenu.cs +++ b/client/Assets/Vanguard/Runtime/UI/HamburgerMenu.cs @@ -1,19 +1,32 @@ +using System; using UnityEngine; using UnityEngine.UI; using Vanguard.App; namespace Vanguard.UI { - /// Гамбургер-меню логина: язык, версия и адрес сервера. - public sealed class LoginMenu + /// + /// Гамбургер-меню экрана: язык, версия и адрес сервера. В Lobby добавляется + /// «Выйти». Закрывается кнопкой, кликом вне панели и по Esc (через драйвер + /// экрана). + /// + public sealed class HamburgerMenu { private readonly GameObject _blocker; private readonly RectTransform _panel; + private readonly Action _onLogout; + private readonly Action _onHistory; - public bool IsOpen => _panel != null && _panel.gameObject.activeSelf; - - public LoginMenu(Transform parent, AppConfig config) + public HamburgerMenu( + Transform parent, + AppConfig config, + Action onLogout = null, + Action onHistory = null, + string title = "Меню") { + _onLogout = onLogout; + _onHistory = onHistory; + _blocker = new GameObject( "MenuBlocker", typeof(RectTransform), @@ -76,6 +89,9 @@ namespace Vanguard.UI new Vector2(0f, i * 7f)); } + var height = 300f + + (onLogout != null ? 56f : 0f) + + (onHistory != null ? 46f : 0f); var panelGo = new GameObject("MenuPanel", typeof(RectTransform), typeof(Image)); _panel = (RectTransform)panelGo.transform; _panel.SetParent(parent, false); @@ -83,12 +99,12 @@ namespace Vanguard.UI _panel.anchorMax = new Vector2(1f, 1f); _panel.pivot = new Vector2(1f, 1f); _panel.anchoredPosition = new Vector2(-28f, -96f); - _panel.sizeDelta = new Vector2(340f, 300f); + _panel.sizeDelta = new Vector2(340f, height); var panelImage = panelGo.GetComponent(); panelImage.sprite = NeonUi.Body( 340, - 300, + Mathf.RoundToInt(height), 16, new Color(0.027f, 0.066f, 0.17f, 0.96f), new Color(0.18f, 0.49f, 1f, 0.85f), @@ -96,22 +112,45 @@ namespace Vanguard.UI panelImage.type = Image.Type.Simple; panelImage.raycastTarget = true; - BuildContent(config); + BuildContent(config, title); _panel.gameObject.SetActive(false); } - private void BuildContent(AppConfig config) + public bool IsOpen => _panel != null && _panel.gameObject.activeSelf; + + public void Toggle() + { + if (IsOpen) + Close(); + else + Open(); + } + + public void Open() + { + _blocker.SetActive(true); + _panel.gameObject.SetActive(true); + _panel.SetAsLastSibling(); + } + + public void Close() + { + _panel.gameObject.SetActive(false); + _blocker.SetActive(false); + } + + private void BuildContent(AppConfig config, string title) { UiKit.AddVerticalLayout(_panel, 16, 10); - var title = UiKit.CreateText( + var header = UiKit.CreateText( _panel, "Title", - "Меню", + title, 18, new Color(0.82f, 0.9f, 1f), TextAnchor.MiddleLeft); - UiKit.SetPreferredHeight(title, 24f); + UiKit.SetPreferredHeight(header, 24f); var row = new GameObject("LanguageRow", typeof(RectTransform)); row.transform.SetParent(_panel, false); @@ -167,32 +206,47 @@ namespace Vanguard.UI server.text = config.ServerBaseUrl; }); + if (_onHistory != null) + { + var history = UiKit.CreateButton( + _panel, + "History", + "История матчей", + () => + { + Close(); + _onHistory(); + }, + 15); + var historyImage = history.GetComponent(); + if (historyImage != null) + historyImage.color = new Color(0.06f, 0.12f, 0.24f, 0.95f); + UiKit.SetPreferredHeight(history, 36f); + } + + if (_onLogout != null) + { + var logout = UiKit.CreateButton( + _panel, + "Logout", + "Выйти", + () => + { + Close(); + _onLogout(); + }, + 15); + var logoutImage = logout.GetComponent(); + if (logoutImage != null) + logoutImage.color = new Color(0.36f, 0.09f, 0.12f, 0.95f); + UiKit.SetPreferredHeight(logout, 36f); + } + var close = UiKit.CreateButton(_panel, "Close", "Закрыть", Close, 15); var closeImage = close.GetComponent(); if (closeImage != null) closeImage.color = new Color(0.06f, 0.12f, 0.24f, 0.95f); UiKit.SetPreferredHeight(close, 36f); } - - private void Toggle() - { - if (_panel.gameObject.activeSelf) - Close(); - else - Open(); - } - - private void Open() - { - _blocker.SetActive(true); - _panel.gameObject.SetActive(true); - _panel.SetAsLastSibling(); - } - - public void Close() - { - _panel.gameObject.SetActive(false); - _blocker.SetActive(false); - } } } diff --git a/client/Assets/Vanguard/Runtime/UI/HamburgerMenu.cs.meta b/client/Assets/Vanguard/Runtime/UI/HamburgerMenu.cs.meta new file mode 100644 index 0000000..b3bb66f --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/HamburgerMenu.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7bfdadaad5565034fbda5660dc99fd78 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/LobbyBackdrop.cs b/client/Assets/Vanguard/Runtime/UI/LobbyBackdrop.cs new file mode 100644 index 0000000..984e2b0 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/LobbyBackdrop.cs @@ -0,0 +1,104 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace Vanguard.UI +{ + /// + /// Фон Lobby: процедурный тёмно-синий градиент с сеткой, центральным + /// свечением и виньеткой. Кешируется как один спрайт; когда появится + /// key art, достаточно заменить источник изображения. + /// + public static class LobbyBackdrop + { + public const string RootName = "LobbyBackdrop"; + + private static Sprite _sprite; + + public static RectTransform Create(Transform parent) + { + var go = new GameObject(RootName, typeof(RectTransform), typeof(Image)); + var rect = (RectTransform)go.transform; + rect.SetParent(parent, false); + UiKit.Stretch(rect); + rect.SetAsFirstSibling(); + + var image = go.GetComponent(); + image.sprite = Sprite(); + image.type = Image.Type.Simple; + image.preserveAspect = false; + image.raycastTarget = false; + + return rect; + } + + private static Sprite Sprite() + { + if (_sprite != null) + return _sprite; + + const int width = 512; + const int height = 288; + var pixels = new Color32[width * height]; + + var top = new Color(0.010f, 0.026f, 0.070f); + var middle = new Color(0.017f, 0.055f, 0.135f); + var bottom = new Color(0.006f, 0.016f, 0.045f); + var glow = new Color(0.06f, 0.26f, 0.62f); + var grid = new Color(0.16f, 0.46f, 0.92f); + + for (var y = 0; y < height; y++) + { + var v = y / (float)(height - 1); + var baseColor = v < 0.55f + ? Color.Lerp(bottom, middle, v / 0.55f) + : Color.Lerp(middle, top, (v - 0.55f) / 0.45f); + + for (var x = 0; x < width; x++) + { + var u = x / (float)(width - 1); + var color = baseColor; + + // Мягкое пятно света над центром сцены. + var dx = (u - 0.5f) * 2f; + var dy = (v - 0.62f) * 2.2f; + var distance = Mathf.Sqrt(dx * dx + dy * dy); + var light = Mathf.Clamp01(1f - distance / 1.15f); + color += glow * (light * light * 0.16f); + + // Сетка с шагом 32 px по текстуре. + if (x % 32 == 0 || y % 32 == 0) + color += grid * 0.035f; + + // Виньетка по краям. + var vignette = Mathf.Clamp01( + (Mathf.Abs(dx) * 0.55f + Mathf.Abs(v - 0.5f) * 1.1f - 0.35f) / 0.65f); + color = Color.Lerp(color, new Color(0f, 0f, 0f), vignette * 0.55f); + color.a = 1f; + + pixels[y * width + x] = color; + } + } + + var texture = new Texture2D(width, height, TextureFormat.RGBA32, false) + { + hideFlags = HideFlags.DontSave, + wrapMode = TextureWrapMode.Clamp, + filterMode = FilterMode.Bilinear, + name = "LobbyBackdrop", + }; + texture.SetPixels32(pixels); + texture.Apply(false, false); + + _sprite = UnityEngine.Sprite.Create( + texture, + new Rect(0f, 0f, width, height), + new Vector2(0.5f, 0.5f), + 100f, + 0, + SpriteMeshType.FullRect); + _sprite.name = "LobbyBackdrop"; + _sprite.hideFlags = HideFlags.DontSave; + return _sprite; + } + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/LobbyBackdrop.cs.meta b/client/Assets/Vanguard/Runtime/UI/LobbyBackdrop.cs.meta new file mode 100644 index 0000000..14fa2c1 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/LobbyBackdrop.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 07d4138d68deb1043a10c6c4f88a9266 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/LobbyIcon.cs b/client/Assets/Vanguard/Runtime/UI/LobbyIcon.cs new file mode 100644 index 0000000..49dedbe --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/LobbyIcon.cs @@ -0,0 +1,214 @@ +using UnityEngine; + +namespace Vanguard.UI +{ + /// + /// Иконки Lobby собраны из примитивов NeonUi: без атласа и без внешних + /// ассетов. Контейнер задаёт размер, иконка центрируется внутри. + /// + public static class LobbyIcon + { + public static readonly Color Default = new Color(0.64f, 0.84f, 1f, 0.92f); + + public static void Swords(RectTransform parent, Color color) + { + Bar(parent, 46f, 5f, 45f, Vector2.zero, color); + Bar(parent, 46f, 5f, -45f, Vector2.zero, color); + Bar(parent, 14f, 4f, 45f, new Vector2(-13f, -13f), color); + Bar(parent, 14f, 4f, -45f, new Vector2(-13f, 13f), color); + } + + public static void Deck(RectTransform parent, Color color) + { + Card(parent, new Vector2(-10f, 0f), -8f, color * 0.75f); + Card(parent, new Vector2(10f, 0f), 8f, color * 0.75f); + Card(parent, Vector2.zero, 0f, color); + } + + public static void Shop(RectTransform parent, Color color) + { + NeonUi.AddImage( + parent, + "Basket", + NeonUi.Body( + 32, + 20, + 4, + new Color(color.r, color.g, color.b, 0.18f), + color, + 2), + new Vector2(32f, 20f), + new Vector2(0f, 2f)); + Bar(parent, 14f, 4f, -28f, new Vector2(-18f, 12f), color); + NeonUi.AddImage( + parent, "WheelL", NeonUi.Circle(9, color), new Vector2(9f, 9f), + new Vector2(-9f, -14f)); + NeonUi.AddImage( + parent, "WheelR", NeonUi.Circle(9, color), new Vector2(9f, 9f), + new Vector2(9f, -14f)); + } + + public static void Shield(RectTransform parent, Color color) + { + NeonUi.AddImage( + parent, + "Shield", + NeonUi.Body( + 34, + 40, + 10, + new Color(color.r, color.g, color.b, 0.18f), + color, + 2), + new Vector2(34f, 40f), + Vector2.zero); + Bar(parent, 18f, 4f, 0f, Vector2.zero, color); + Bar(parent, 4f, 14f, 0f, Vector2.zero, color); + } + + public static void Chat(RectTransform parent, Color color) + { + NeonUi.AddImage( + parent, + "Bubble", + NeonUi.Body( + 46, + 32, + 8, + new Color(color.r, color.g, color.b, 0.14f), + color, + 2), + new Vector2(46f, 32f), + new Vector2(0f, 4f)); + NeonUi.AddImage( + parent, + "Tail", + NeonUi.Body(12, 12, 2, new Color(color.r, color.g, color.b, 0.14f), color, 2), + new Vector2(12f, 12f), + new Vector2(-14f, -14f)).rectTransform.localRotation = + Quaternion.Euler(0f, 0f, 45f); + + for (var i = -1; i <= 1; i++) + { + NeonUi.AddImage( + parent, + "Dot" + i, + NeonUi.Circle(4, color), + new Vector2(4f, 4f), + new Vector2(i * 9f, 4f)); + } + } + + public static void Friends(RectTransform parent, Color color) + { + NeonUi.AddImage( + parent, "Head", NeonUi.Circle(14, color), new Vector2(14f, 14f), + new Vector2(-9f, 10f)); + NeonUi.AddImage( + parent, + "Body", + NeonUi.Body( + 28, + 14, + 7, + new Color(color.r, color.g, color.b, 0.25f), + color, + 2), + new Vector2(28f, 14f), + new Vector2(-9f, -8f)); + NeonUi.AddImage( + parent, + "Head2", + NeonUi.Circle(11, color * 0.7f), + new Vector2(11f, 11f), + new Vector2(13f, 9f)); + NeonUi.AddImage( + parent, + "Body2", + NeonUi.Body( + 22, + 12, + 6, + new Color(color.r, color.g, color.b, 0.18f), + color * 0.7f, + 2), + new Vector2(22f, 12f), + new Vector2(13f, -8f)); + } + + public static void Lock(RectTransform parent, Color color) + { + NeonUi.AddImage( + parent, + "Shackle", + NeonUi.Ring(34, 3f, color), + new Vector2(34f, 34f), + new Vector2(0f, 10f)); + NeonUi.AddImage( + parent, + "Body", + NeonUi.Body( + 40, + 26, + 6, + new Color(color.r, color.g, color.b, 0.2f), + color, + 2), + new Vector2(40f, 26f), + new Vector2(0f, -8f)); + NeonUi.AddImage( + parent, + "Keyhole", + NeonUi.Circle(6, color), + new Vector2(6f, 6f), + new Vector2(0f, -8f)); + } + + public static void Chevron(RectTransform parent, Color color) + { + Bar(parent, 13f, 3f, -45f, new Vector2(-4f, 4f), color); + Bar(parent, 13f, 3f, 45f, new Vector2(-4f, -4f), color); + } + + public static void Chart(RectTransform parent, Color color) + { + Bar(parent, 12f, 6f, 90f, new Vector2(-10f, -6f), color); + Bar(parent, 20f, 6f, 90f, new Vector2(0f, 0f), color); + Bar(parent, 28f, 6f, 90f, new Vector2(10f, 4f), color); + } + + private static void Card(RectTransform parent, Vector2 position, float angle, Color color) + { + var image = NeonUi.AddImage( + parent, + "Card", + NeonUi.Body( + 28, + 38, + 5, + new Color(0.10f, 0.24f, 0.48f, 0.55f), + color, + 2), + new Vector2(28f, 38f), + position); + image.rectTransform.localRotation = Quaternion.Euler(0f, 0f, angle); + } + + private static void Bar( + RectTransform parent, + float width, + float height, + float angle, + Vector2 position, + Color color) + { + var image = NeonUi.AddImage( + parent, + "Bar", + NeonUi.HorizontalBar(Mathf.RoundToInt(width), Mathf.RoundToInt(height), color), + new Vector2(width, height), + position); + image.rectTransform.localRotation = Quaternion.Euler(0f, 0f, angle); + } + } +} diff --git a/client/Assets/Vanguard/Runtime/UI/LobbyIcon.cs.meta b/client/Assets/Vanguard/Runtime/UI/LobbyIcon.cs.meta new file mode 100644 index 0000000..d5695b2 --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/LobbyIcon.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 61932e95c3c9b0043a331d7e3bfcd832 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/UI/LobbyView.cs b/client/Assets/Vanguard/Runtime/UI/LobbyView.cs new file mode 100644 index 0000000..330ccff --- /dev/null +++ b/client/Assets/Vanguard/Runtime/UI/LobbyView.cs @@ -0,0 +1,869 @@ +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 +{ + /// + /// Главный экран после входа: профиль и балансы, вход в бой/колоды/магазин, + /// заглушки разделов без серверной части и гамбургер-меню. + /// + public sealed class LobbyView : IDisposable + { + public const string RootName = "LobbyScreen"; + + private const float Margin = 24f; + private const float TopHeight = 96f; + 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 HamburgerMenu _menu; + private readonly StatusTicker _ticker; + + private readonly RectTransform _root; + private Text _avatar; + private Text _playerName; + private Text _mmr; + 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().raycastTarget = false; + + var top = TopBar(out _avatar, out _playerName, out _mmr); + AddTopAction(top, "Rating", "Рейтинг", -312f, 180f, () => onRating(), LobbyIcon.Chart); + AddTopAction(top, "Community", "Сообщество", -96f, 200f, () => onCommunity(), LobbyIcon.Friends); + + BuildActions(onBattle, onDecks, onStore); + BuildMissions(out _status); + BuildBalances(out _gold, out _dust, out _tokens); + BuildChat(); + BuildFriends(); + + _menu = new HamburgerMenu(_root, config, onLoggedOut, onHistory); + _ticker = _root.gameObject.AddComponent(); + _ticker.Hide = () => SetStatus(string.Empty); + + _player.Changed += UpdateTexts; + Loc.LanguageChanged += UpdateTexts; + UpdateTexts(); + _root.gameObject.SetActive(false); + } + + public bool IsMenuOpen => _menu != null && _menu.IsOpen; + + public void CloseMenu() + { + if (_menu != null) + _menu.Close(); + } + + 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; + } + + // ------------------------------------------------------------ топбар + + private RectTransform TopBar(out Text avatar, out Text playerName, out Text mmr) + { + var top = Surface(_root, "TopBar", new Color(0.012f, 0.03f, 0.08f, 0.94f), Border, 1, 0); + top.anchorMin = new Vector2(0f, 1f); + top.anchorMax = new Vector2(1f, 1f); + top.pivot = new Vector2(0.5f, 1f); + top.offsetMin = new Vector2(Margin, -TopHeight); + top.offsetMax = new Vector2(-Margin, 0f); + + var avatarRect = Surface( + top, + "Avatar", + new Color(0.05f, 0.13f, 0.3f, 0.95f), + BorderStrong, + 2, + 14); + 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(64f, 64f); + avatarRect.anchoredPosition = new Vector2(20f, 0f); + + avatar = UiKit.CreateText(avatarRect, "Initials", "?", 26, TextPrimary); + avatar.fontStyle = FontStyle.Bold; + UiKit.Stretch(avatar.rectTransform); + UiKit.MakeLiteral(avatar); + + playerName = UiKit.CreateText( + top, + "PlayerName", + "…", + 22, + TextPrimary, + TextAnchor.MiddleLeft); + playerName.fontStyle = FontStyle.Bold; + var nameRect = playerName.rectTransform; + nameRect.anchorMin = new Vector2(0f, 0.5f); + nameRect.anchorMax = new Vector2(0f, 0.5f); + nameRect.pivot = new Vector2(0f, 0.5f); + nameRect.sizeDelta = new Vector2(420f, 30f); + nameRect.anchoredPosition = new Vector2(104f, 14f); + UiKit.MakeLiteral(playerName); + + mmr = UiKit.CreateText( + top, + "Mmr", + string.Empty, + 14, + TextMuted, + TextAnchor.MiddleLeft); + var mmrRect = mmr.rectTransform; + mmrRect.anchorMin = new Vector2(0f, 0.5f); + mmrRect.anchorMax = new Vector2(0f, 0.5f); + mmrRect.pivot = new Vector2(0f, 0.5f); + mmrRect.sizeDelta = new Vector2(460f, 22f); + mmrRect.anchoredPosition = new Vector2(104f, -16f); + UiKit.MakeLiteral(mmr); + + var season = UiKit.CreateText( + top, + "Season", + "Сезон — скоро", + 14, + TextDim, + TextAnchor.MiddleCenter); + var seasonRect = season.rectTransform; + seasonRect.anchorMin = new Vector2(0.5f, 0.5f); + seasonRect.anchorMax = new Vector2(0.5f, 0.5f); + seasonRect.sizeDelta = new Vector2(280f, 30f); + seasonRect.anchoredPosition = Vector2.zero; + + return top; + } + + private void AddTopAction( + RectTransform parent, + string name, + string label, + float rightOffset, + float width, + Action onClick, + Action icon) + { + var buttonRect = Surface( + parent, + name, + new Color(0.035f, 0.09f, 0.21f, 0.85f), + Border, + 1, + 12); + buttonRect.anchorMin = new Vector2(1f, 0.5f); + buttonRect.anchorMax = new Vector2(1f, 0.5f); + buttonRect.pivot = new Vector2(1f, 0.5f); + buttonRect.sizeDelta = new Vector2(width, 52f); + buttonRect.anchoredPosition = new Vector2(rightOffset, 0f); + + var image = buttonRect.GetComponent(); + image.raycastTarget = true; + var button = buttonRect.gameObject.AddComponent