client: Lobby по референсу — дашборд, BATTLE-оверлей и сцена Community
CI / test (push) Successful in 3m4s Details

- Lobby: 2D-дашборд с процедурным фоном, топбаром (инициалы, ник, MMR,
  сезон), плашками BATTLE / DECK BUILDER / SHOP, миссиями-заглушкой,
  балансами, заглушками чата и друзей. 3D-окружение в Lobby не строится.
- BATTLE: оверлей выбора колоды и режима (Рейтинговый / С ботом),
  серверная валидация через ValidateDeckAsync; Ranked уходит в очередь.
- Rating — заглушка лидерборда; Community — отдельная сцена с вкладками
  «Гайды / Колоды / Тикеты» и переходом из Lobby.
- LoginMenu вынесен в общий HamburgerMenu; в Lobby добавлены «История
  матчей» и «Выйти». Esc закрывает меню и оверлеи.
- MetaView удалён; онбординг в Lobby не показывается (выбор триал-деки —
  отдельный экран).
- NeonUi: BodySliced для растягиваемых панелей; сцена Community в Build
  Settings, SceneSmoke проверяет LobbyBackdrop/LobbyScreen.
- Проверки: EditMode 62/62, SCENE_SMOKE success=True, PLAYMAT_SMOKE
  success=True, standalone собран.
This commit is contained in:
MpakobecEx 2026-10-31 19:54:05 +05:00
parent d7395780a3
commit 179aa31da6
37 changed files with 2914 additions and 403 deletions

View File

@ -36,6 +36,7 @@ namespace Vanguard.Editor
Create<LoginSceneController>("Login");
Create<LobbySceneController>("Lobby");
Create<CommunitySceneController>("Community");
Create<DeckBuilderSceneController>("DeckBuilder");
Create<BattleSceneController>("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");

View File

@ -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)

View File

@ -0,0 +1,21 @@
using UnityEngine;
using Vanguard.UI;
namespace Vanguard.App
{
/// <summary>Сцена Community: гайды, публичные колоды и тикеты.</summary>
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();
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 001ba3cec4018cb44b121859d9b38d08

View File

@ -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<AuthService>();
_meta = Resolve<MetaService>();
_player = Resolve<PlayerState>();
var canvas = CreateCanvas("Lobby Canvas");
_config = Resolve<AppConfig>();
_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<HotkeysDriver>();
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();
}
}

View File

@ -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)

View File

@ -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
{
/// <summary>
/// Оверлей входа в бой: выбор колоды и режима (рейтинговый или с ботом).
/// Колода проверяется сервером до запуска.
/// </summary>
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<DeckDto> _onRanked;
private readonly Action<MatchConnectionInfo> _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<DeckDto> _decks = new List<DeckDto>();
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<DeckDto> onRanked,
Action<MatchConnectionInfo> 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<Image>().raycastTarget = true;
var panel = UiKit.CreateCenteredCard(
_root,
"Panel",
new Vector2(820f, 720f),
Color.white);
var panelImage = panel.GetComponent<Image>();
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<RectTransform>();
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<Image>();
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<Text>();
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<RectTransform>();
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<Image>();
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<RectTransform>();
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<Image>();
if (playImage != null)
ApplyFill(playImage, new Color(0.10f, 0.37f, 0.79f, 1f), Border);
_playLabel = _play.GetComponentInChildren<Text>();
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<Image>();
ApplyFill(image, DeckFill(false), Border);
var element = item.gameObject.AddComponent<LayoutElement>();
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<Image>();
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<RectTransform>();
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<Image>();
ApplyFill(image, DeckFill(active), active ? BorderStrong : Border);
return button;
}
private void SetMode(bool ranked)
{
_ranked = ranked;
ApplyFill(_rankedMode.GetComponent<Image>(), DeckFill(ranked), ranked ? BorderStrong : Border);
ApplyFill(_botMode.GetComponent<Image>(), 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;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fb9a2911fede84d419e8ca7de4164fad

View File

@ -0,0 +1,85 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace Vanguard.UI
{
/// <summary>Модальная заглушка раздела: заголовок, пояснение и «скоро».</summary>
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<Image>().raycastTarget = true;
var panel = UiKit.CreateCenteredCard(
_root,
"Panel",
new Vector2(720f, 420f),
Color.white);
panel.GetComponent<Image>().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<Image>().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<RectTransform>();
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);
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b6e3834537fd38641aebc38d141e1c64

View File

@ -0,0 +1,202 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace Vanguard.UI
{
public enum CommunityTab
{
Guides,
Decks,
Tickets,
}
/// <summary>
/// Сцена Community: вкладки гайдов, публичных колод и тикетов. Пока это
/// каркас — серверных данных для разделов нет.
/// </summary>
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<Image>().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<RectTransform>();
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<Image>(), 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<RectTransform>();
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<Image>(),
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>();
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;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61a538c7871f1924ea1f9671cf287c69

View File

@ -1,19 +1,32 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Vanguard.App;
namespace Vanguard.UI
{
/// <summary>Гамбургер-меню логина: язык, версия и адрес сервера.</summary>
public sealed class LoginMenu
/// <summary>
/// Гамбургер-меню экрана: язык, версия и адрес сервера. В Lobby добавляется
/// «Выйти». Закрывается кнопкой, кликом вне панели и по Esc (через драйвер
/// экрана).
/// </summary>
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<Image>();
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<Image>();
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<Image>();
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<Image>();
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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7bfdadaad5565034fbda5660dc99fd78

View File

@ -0,0 +1,104 @@
using UnityEngine;
using UnityEngine.UI;
namespace Vanguard.UI
{
/// <summary>
/// Фон Lobby: процедурный тёмно-синий градиент с сеткой, центральным
/// свечением и виньеткой. Кешируется как один спрайт; когда появится
/// key art, достаточно заменить источник изображения.
/// </summary>
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>();
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;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 07d4138d68deb1043a10c6c4f88a9266

View File

@ -0,0 +1,214 @@
using UnityEngine;
namespace Vanguard.UI
{
/// <summary>
/// Иконки Lobby собраны из примитивов NeonUi: без атласа и без внешних
/// ассетов. Контейнер задаёт размер, иконка центрируется внутри.
/// </summary>
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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61932e95c3c9b0043a331d7e3bfcd832

View File

@ -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
{
/// <summary>
/// Главный экран после входа: профиль и балансы, вход в бой/колоды/магазин,
/// заглушки разделов без серверной части и гамбургер-меню.
/// </summary>
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<Image>().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<StatusTicker>();
_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<RectTransform, Color> 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>();
image.raycastTarget = true;
var button = buttonRect.gameObject.AddComponent<Button>();
button.targetGraphic = image;
button.transition = Selectable.Transition.ColorTint;
button.colors = HoverColors();
button.onClick.AddListener(() => onClick());
var iconRect = Icon(buttonRect, new Vector2(30f, 30f), new Vector2(-width * 0.5f + 30f, 0f), 0.6f);
icon(iconRect, Accent);
var text = UiKit.CreateText(
buttonRect,
"Label",
label,
17,
TextPrimary,
TextAnchor.MiddleLeft);
var textRect = text.rectTransform;
textRect.anchorMin = new Vector2(0f, 0f);
textRect.anchorMax = new Vector2(1f, 1f);
textRect.offsetMin = new Vector2(52f, 0f);
textRect.offsetMax = new Vector2(-14f, 0f);
}
// ------------------------------------------------------------ левая колонка
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;
var name = string.IsNullOrEmpty(_player.UserName) ? "…" : _player.UserName;
_playerName.text = name;
_avatar.text = name.Length > 0 ? name.Substring(0, 1).ToUpperInvariant() : "?";
_mmr.text = "MMR " + _player.RankedMmr
+ " · " + Loc.Translate("матчей") + " " + _player.Matches
+ " · " + Loc.Translate("побед") + " " + WinsPercent();
_gold.text = _player.Gold.ToString();
_dust.text = _player.Dust.ToString();
_tokens.text = _player.Tokens.ToString();
}
private string WinsPercent()
{
if (_player.Matches <= 0)
return "0%";
return Mathf.RoundToInt(_player.Wins * 100f / _player.Matches) + "%";
}
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();
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c48efadcc55597c4ca139d4b3d15fb93

View File

@ -371,6 +371,50 @@ namespace Vanguard.UI
["Нужно ровно 50 карт и 16 триггеров."] = "The deck needs exactly 50 cards and 16 triggers.",
["В колоде уже 16 триггеров."] = "The deck already has 16 triggers.",
[" · триггеры: "] = " · triggers: ",
["Рейтинг"] = "Rating",
["Сообщество"] = "Community",
["Найдите соперника и сразитесь"] = "Find an opponent and fight",
["Соберите и настройте колоду"] = "Build and customize your deck",
["Бустеры, триал-деки и токены"] = "Boosters, trial decks and tokens",
["Сезон — скоро"] = "Season — coming soon",
["Ежедневные / еженедельные задания"] = "Daily / weekly missions",
["Задание появится позже"] = "Missions will appear later",
["Награды и прогресс — после появления серверных заданий."] =
"Rewards and progress will arrive with server missions.",
["Золото"] = "Gold",
["Чат"] = "Chat",
["Чат появится позже"] = "Chat will appear later",
["Здесь будет общий чат лобби и личные сообщения. Пока серверного канала нет."] =
"A lobby chat and direct messages will live here. There is no server channel yet.",
["Введите сообщение…"] = "Type a message…",
["Друзья онлайн"] = "Friends online",
["Список друзей появится вместе с сервером."] =
"The friends list will arrive with the server.",
["скоро"] = "soon",
["матчей"] = "matches",
["побед"] = "wins",
["Рейтинговый"] = "Ranked",
["Выберите колоду и режим"] = "Choose a deck and mode",
["Загружаем колоды…"] = "Loading decks…",
["Нет колод. Соберите колоду."] = "No decks. Build a deck.",
["Не удалось загрузить колоды."] = "Could not load decks.",
["Проверяем колоду…"] = "Checking the deck…",
["Колода готова."] = "Deck is ready.",
["Колода не прошла проверку."] = "Deck failed validation.",
["Не удалось проверить колоду."] = "Could not validate the deck.",
["Собрать колоду"] = "Build a deck",
["Гайды"] = "Guides",
["Колоды"] = "Decks",
["Тикеты"] = "Tickets",
["Гайды, колоды и обратная связь"] = "Guides, decks and feedback",
["Разборы карт, колод и механик появятся позже. Материалы будут открываться прямо здесь."] =
"Card, deck and mechanics guides will appear later and open right here.",
["Публичные колоды появятся позже. Сейчас колодой можно поделиться по коду в редакторе."] =
"Public decks will appear later. For now a deck can be shared by code in the editor.",
["Отправка сообщений о багах и улучшениях появится позже: нужен серверный раздел тикетов."] =
"Bug and improvement reports will arrive later: the server needs a ticket section.",
["Топ игроков по рейтингу появится позже: нужен серверный лидерборд."] =
"The rating leaderboard will appear later: the server needs a leaderboard endpoint.",
};
private static readonly KeyValuePair<string, string>[] Phrases = Exact

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 693da1bea283cab4bac9e9caa525032d

View File

@ -29,7 +29,7 @@ namespace Vanguard.UI
private readonly Button _registerButton;
private readonly Text _status;
private readonly StatusTicker _ticker;
private readonly LoginMenu _menu;
private readonly HamburgerMenu _menu;
private bool _busy;
private bool _passwordVisible;
@ -96,7 +96,7 @@ namespace Vanguard.UI
_ticker = _root.gameObject.AddComponent<StatusTicker>();
_ticker.Hide = () => SetStatus(string.Empty, StatusColor);
_menu = new LoginMenu(_root, _config);
_menu = new HamburgerMenu(_root, _config);
}
/// <summary>Enter отправляет форму; в открытом меню не срабатывает.</summary>

View File

@ -1,265 +0,0 @@
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using Vanguard.Api;
using Vanguard.Auth;
using Vanguard.State;
namespace Vanguard.UI
{
/// <summary>Главный экран после входа: профиль, балансы и заглушки разделов.</summary>
public sealed class MetaView : IDisposable
{
private readonly AuthService _auth;
private readonly PlayerState _player;
private readonly Action _onLoggedOut;
private readonly Action _onDecks;
private readonly Action _onStore;
private readonly Action _onHistory;
private readonly Action _onPlay;
private readonly Action _onOnboarding;
private bool _disposed;
private readonly RectTransform _root;
private readonly Text _playerName;
private readonly Text _onboarding;
private readonly Text _gold;
private readonly Text _dust;
private readonly Text _tokens;
private readonly Text _ranked;
private readonly Text _casual;
private readonly Text _record;
private readonly Text _status;
private readonly Button _logout;
private readonly Button _onboardingButton;
private bool _refreshing;
public MetaView(
Transform parent,
AuthService auth,
PlayerState player,
Action onLoggedOut,
Action onDecks,
Action onStore,
Action onHistory,
Action onPlay,
Action onOnboarding)
{
_auth = auth;
_player = player;
_onLoggedOut = onLoggedOut;
_onDecks = onDecks;
_onStore = onStore;
_onHistory = onHistory;
_onPlay = onPlay;
_onOnboarding = onOnboarding;
_root = UiKit.CreatePanel(parent, "MetaScreen", new Color(0.035f, 0.045f, 0.065f, 1f));
var top = UiKit.CreatePanel(_root, "TopBar", new Color(0.07f, 0.085f, 0.115f, 1f));
var topRect = top;
topRect.anchorMin = new Vector2(0f, 1f);
topRect.anchorMax = new Vector2(1f, 1f);
topRect.pivot = new Vector2(0.5f, 1f);
topRect.anchoredPosition = Vector2.zero;
topRect.sizeDelta = new Vector2(0f, 92f);
UiKit.AddHorizontalLayout(top, 22, 16);
var brand = UiKit.CreateText(
top, "Brand", "VANGUARD", 34, new Color(0.86f, 0.90f, 1f), TextAnchor.MiddleLeft);
brand.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(brand, 48f, 0f);
_playerName = UiKit.CreateText(
top, "PlayerName", "…", 20, new Color(0.70f, 0.76f, 0.90f), TextAnchor.MiddleLeft);
UiKit.MakeLiteral(_playerName);
UiKit.SetPreferredHeight(_playerName, 40f);
var language = UiKit.CreateLanguageButton(top, "Language");
UiKit.SetPreferredHeight(language, 42f, 0f);
_logout = UiKit.CreateButton(top, "Logout", "Выйти", OnLogoutClicked, 18);
UiKit.SetPreferredHeight(_logout, 42f, 0f);
var content = UiKit.CreatePanel(_root, "Content", new Color(0.035f, 0.045f, 0.065f, 0f));
var contentRect = content;
contentRect.anchorMin = Vector2.zero;
contentRect.anchorMax = Vector2.one;
contentRect.offsetMin = new Vector2(48f, 36f);
contentRect.offsetMax = new Vector2(-48f, -116f);
UiKit.AddVerticalLayout(contentRect, 12, 16);
var welcome = UiKit.CreateText(
content, "Welcome", "Личный кабинет", 38, Color.white, TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(welcome, 56f);
_onboarding = UiKit.CreateText(
content, "Onboarding", string.Empty, 18,
new Color(1f, 0.76f, 0.38f), TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(_onboarding, 28f);
var stats = UiKit.CreatePanel(content, "Stats", new Color(0.07f, 0.085f, 0.115f, 1f));
UiKit.SetPreferredHeight(stats, 130f);
UiKit.AddHorizontalLayout(stats, 18, 14);
_gold = AddStat(stats, "Gold");
_dust = AddStat(stats, "Пыль");
_tokens = AddStat(stats, "Токены");
_ranked = AddStat(stats, "Ranked MMR");
_casual = AddStat(stats, "Casual MMR");
var actions = UiKit.CreatePanel(content, "Actions", new Color(0.07f, 0.085f, 0.115f, 1f));
UiKit.SetPreferredHeight(actions, 96f);
UiKit.AddHorizontalLayout(actions, 18, 14);
AddAction(actions, "Коллекция", _onDecks);
AddAction(actions, "Магазин", _onStore);
AddAction(actions, "История", _onHistory);
AddAction(actions, "В бой", _onPlay);
var lower = UiKit.CreatePanel(content, "Lower", new Color(0.07f, 0.085f, 0.115f, 1f));
UiKit.SetPreferredHeight(lower, 220f, 1f);
UiKit.AddVerticalLayout(lower, 20, 8);
_record = UiKit.CreateText(
lower, "Record", string.Empty, 20,
new Color(0.72f, 0.78f, 0.92f), TextAnchor.MiddleLeft);
UiKit.SetPreferredHeight(_record, 28f);
_status = UiKit.CreateText(
lower, "Status", string.Empty, 18,
new Color(0.62f, 0.68f, 0.82f), TextAnchor.UpperLeft);
UiKit.SetPreferredHeight(_status, 70f);
_onboardingButton = UiKit.CreateButton(
lower, "OnboardingButton", "Выбрать триал-деку", () => _onOnboarding(), 20);
UiKit.SetPreferredHeight(_onboardingButton, 46f);
_player.Changed += UpdateTexts;
_root.gameObject.SetActive(false);
}
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;
}
private static Text AddStat(Transform parent, string caption)
{
var card = UiKit.CreatePanel(parent, "Stat_" + caption, new Color(0.09f, 0.11f, 0.15f, 1f));
UiKit.AddVerticalLayout(card, 12, 4);
var element = card.gameObject.AddComponent<LayoutElement>();
element.flexibleWidth = 1f;
element.preferredHeight = 100f;
var label = UiKit.CreateText(
card, "Caption", caption, 16, new Color(0.58f, 0.65f, 0.78f));
UiKit.SetPreferredHeight(label, 22f);
var value = UiKit.CreateText(
card, "Value", "—", 30, Color.white);
value.fontStyle = FontStyle.Bold;
UiKit.SetPreferredHeight(value, 40f);
return value;
}
private static Button AddAction(Transform parent, string caption, Action onClick)
{
var button = UiKit.CreateButton(parent, "Action_" + caption, caption, () => onClick(), 20);
var element = button.gameObject.AddComponent<LayoutElement>();
element.flexibleWidth = 1f;
element.preferredHeight = 56f;
return button;
}
private async Task RefreshAsync()
{
if (_refreshing)
return;
_refreshing = true;
SetStatus("Обновляем профиль…", new Color(0.62f, 0.68f, 0.82f));
try
{
await _auth.RefreshMeAsync();
if (_disposed)
return;
SetStatus("Профиль обновлён.", new Color(0.45f, 0.85f, 0.55f));
}
catch (ApiException exception) when (exception.StatusCode == 401)
{
if (!_disposed)
_onLoggedOut();
}
catch (Exception exception)
{
Debug.LogException(exception);
SetStatus("Не удалось обновить профиль. Сервер недоступен?", new Color(1f, 0.60f, 0.45f));
}
finally
{
_refreshing = false;
}
}
private void UpdateTexts()
{
if (_disposed)
return;
_playerName.text = string.IsNullOrEmpty(_player.UserName)
? "…"
: _player.UserName;
_gold.text = _player.Gold.ToString();
_dust.text = _player.Dust.ToString();
_tokens.text = _player.Tokens.ToString();
_ranked.text = _player.RankedMmr.ToString();
_casual.text = _player.CasualMmr.ToString();
_record.text = "Матчей: " + _player.Matches + " · побед: " + _player.Wins
+ " · XP: " + _player.Xp;
_onboarding.text = _player.OnboardingCompleted
? "Онбординг завершён: " + (_player.SelectedTrialDeckId ?? "—")
: "Онбординг не завершён: выберите стартовую триал-деку.";
_onboardingButton.gameObject.SetActive(!_player.OnboardingCompleted);
}
private void SetStatus(string text, Color color)
{
if (_disposed || _status == null)
return;
_status.text = text ?? string.Empty;
_status.color = color;
}
private async void OnLogoutClicked()
{
_logout.interactable = false;
await _auth.LogoutAsync();
if (_disposed)
return;
_logout.interactable = true;
_onLoggedOut();
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 254c5fe6df971b942bde7efb1a5dab42

View File

@ -47,6 +47,42 @@ namespace Vanguard.UI
});
}
/// <summary>
/// Растягиваемая панель: тот же вид, но с 9-slice-рамкой, поэтому
/// скругление не искажается на больших размерах.
/// </summary>
public static Sprite BodySliced(
int radius,
Color fill,
Color border,
int borderWidth)
{
var slice = radius + borderWidth + 2;
var size = slice * 2 + 8;
var key = string.Format(
"bodyslice:{0}:{1}:{2}:{3}",
radius, Hex(fill), Hex(border), borderWidth);
return Get(
key,
size,
size,
(dx, dy) =>
{
var half = size * 0.5f;
var distance = RoundedDistance(dx, dy, half, half, radius);
if (distance > 0f)
return Color.clear;
if (borderWidth > 0 && distance > -borderWidth)
return border;
return fill;
},
slice);
}
/// <summary>Кнопка со срезанными углами и вертикальным градиентом.</summary>
public static Sprite Bevel(
int width,
@ -227,7 +263,12 @@ namespace Vanguard.UI
return rect;
}
private static Sprite Get(string key, int width, int height, Func<float, float, Color> shader)
private static Sprite Get(
string key,
int width,
int height,
Func<float, float, Color> shader,
int slice = 0)
{
if (Cache.TryGetValue(key, out var cached) && cached != null)
return cached;
@ -265,7 +306,8 @@ namespace Vanguard.UI
new Vector2(0.5f, 0.5f),
100f,
0,
SpriteMeshType.FullRect);
SpriteMeshType.FullRect,
new Vector4(slice, slice, slice, slice));
sprite.name = key;
sprite.hideFlags = HideFlags.DontSave;

View File

@ -0,0 +1,100 @@
using UnityEngine;
using UnityEngine.UI;
namespace Vanguard.UI
{
/// <summary>Общие элементы-заглушки: бейдж «скоро» и панель раздела.</summary>
public static class StubUi
{
public const string Soon = "скоро";
/// <summary>Бейдж «скоро» в правом верхнем углу родителя.</summary>
public static RectTransform Badge(Transform parent, Vector2 offset)
{
var go = new GameObject("SoonBadge", typeof(RectTransform), typeof(Image));
var rect = (RectTransform)go.transform;
rect.SetParent(parent, false);
rect.anchorMin = new Vector2(1f, 1f);
rect.anchorMax = new Vector2(1f, 1f);
rect.pivot = new Vector2(1f, 1f);
rect.sizeDelta = new Vector2(72f, 24f);
rect.anchoredPosition = offset;
var image = go.GetComponent<Image>();
image.sprite = NeonUi.Body(
72,
24,
12,
new Color(0.10f, 0.22f, 0.42f, 0.9f),
new Color(0.24f, 0.55f, 1f, 0.7f),
1);
image.type = Image.Type.Simple;
image.raycastTarget = false;
var text = UiKit.CreateText(
rect,
"Label",
Soon,
12,
new Color(0.66f, 0.82f, 1f),
TextAnchor.MiddleCenter);
UiKit.Stretch(text.rectTransform);
text.horizontalOverflow = HorizontalWrapMode.Overflow;
return rect;
}
/// <summary>Панель раздела, которого ещё нет на сервере.</summary>
public static RectTransform Panel(
Transform parent,
string name,
string title,
string note)
{
var root = new GameObject(name, typeof(RectTransform));
var rect = (RectTransform)root.transform;
rect.SetParent(parent, false);
UiKit.Stretch(rect);
var icon = new GameObject("Lock", typeof(RectTransform));
var iconRect = (RectTransform)icon.transform;
iconRect.SetParent(rect, false);
iconRect.anchorMin = new Vector2(0.5f, 0.5f);
iconRect.anchorMax = new Vector2(0.5f, 0.5f);
iconRect.pivot = new Vector2(0.5f, 0.5f);
iconRect.sizeDelta = new Vector2(64f, 64f);
iconRect.anchoredPosition = new Vector2(0f, 104f);
LobbyIcon.Lock(iconRect, new Color(0.36f, 0.56f, 0.9f, 0.9f));
var titleText = UiKit.CreateText(
rect,
"Title",
title,
26,
new Color(0.86f, 0.92f, 1f),
TextAnchor.MiddleCenter);
titleText.fontStyle = FontStyle.Bold;
var titleRect = titleText.rectTransform;
titleRect.anchorMin = new Vector2(0.5f, 0.5f);
titleRect.anchorMax = new Vector2(0.5f, 0.5f);
titleRect.pivot = new Vector2(0.5f, 0.5f);
titleRect.sizeDelta = new Vector2(560f, 40f);
titleRect.anchoredPosition = new Vector2(0f, 34f);
var noteText = UiKit.CreateText(
rect,
"Note",
note,
17,
new Color(0.58f, 0.68f, 0.86f),
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(620f, 90f);
noteRect.anchoredPosition = new Vector2(0f, -42f);
return rect;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 784de18e909e5564b88fe60a321062cf

View File

@ -116,4 +116,22 @@ namespace Vanguard.UI
Cancel?.Invoke();
}
}
/// <summary>Esc/Enter для экранов без полей ввода (лобби, оверлеи).</summary>
public sealed class HotkeysDriver : MonoBehaviour
{
public Action Submit;
public Action Cancel;
private void Update() => Handle(TabFocus.EnterPressed(), TabFocus.EscapePressed());
public void Handle(bool submit, bool cancel)
{
if (submit)
Submit?.Invoke();
if (cancel)
Cancel?.Invoke();
}
}
}

View File

@ -0,0 +1,352 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 2
m_PVRDenoiserTypeIndirect: 2
m_PVRDenoiserTypeAO: 2
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &868525904
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 868525906}
- component: {fileID: 868525905}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!20 &868525905
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 868525904}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.02, g: 0.03, b: 0.055, a: 1}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &868525906
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 868525904}
serializedVersion: 2
m_LocalRotation: {x: 0.104528464, y: 0, z: 0, w: 0.9945219}
m_LocalPosition: {x: 0, y: 3.2, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1061194335
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1061194337}
- component: {fileID: 1061194336}
m_Layer: 0
m_Name: Community Scene
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1061194336
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1061194335}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 001ba3cec4018cb44b121859d9b38d08, type: 3}
m_Name:
m_EditorClassIdentifier: Vanguard.Runtime::Vanguard.App.CommunitySceneController
--- !u!4 &1061194337
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1061194335}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &2139080688
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2139080690}
- component: {fileID: 2139080689}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &2139080689
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2139080688}
m_Enabled: 1
serializedVersion: 13
m_Type: 1
m_Color: {r: 0.82, g: 0.88, b: 1, a: 1}
m_Intensity: 0.9
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize2D: {x: 0.5, y: 0.5}
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
m_ShapeRadius: 0.025
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!4 &2139080690
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2139080688}
serializedVersion: 2
m_LocalRotation: {x: 0.39098036, y: -0.25180724, z: 0.112111814, w: 0.87815624}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 1061194337}
- {fileID: 868525906}
- {fileID: 2139080690}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c50376fc9fc6b4e4fbca40d005364930
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -37,9 +37,9 @@ namespace Vanguard.Tests
var login = new LoginView(canvas.transform, config, auth, () => { });
login.Show("smoke");
var lobby = new MetaView(
canvas.transform, auth, player, () => { },
() => { }, () => { }, () => { }, () => { }, () => { });
var lobby = new LobbyView(
canvas.transform, config, auth, player, () => { },
() => { }, () => { }, () => { }, () => { }, () => { }, () => { });
var onboarding = new OnboardingView(
canvas.transform, auth, meta, () => { }, () => { });
var decks = new DeckBuilderView(
@ -54,7 +54,7 @@ namespace Vanguard.Tests
canvas.transform, meta, () => { });
Assert.IsNotNull(GameObject.Find("LoginScreen"));
Assert.IsTrue(Exists("MetaScreen"));
Assert.IsTrue(Exists("LobbyScreen"));
Assert.IsTrue(Exists("OnboardingScreen"));
Assert.IsTrue(Exists("DeckBuilderScreen"));
Assert.IsTrue(Exists("StoreScreen"));
@ -819,6 +819,82 @@ namespace Vanguard.Tests
Cleanup();
}
[Test]
public void Lobby_Shows_Actions_Stubs_And_Balances_Without_Onboarding()
{
Loc.SetLanguage(UiLanguage.Ru);
var config = new AppConfig();
config.Load();
var tokens = new TokenStore();
var api = new ApiClient(config, tokens);
var player = new PlayerState();
player.Apply(new MeResponse
{
UserName = "Nick",
OnboardingCompleted = true,
Gold = 12580,
Dust = 3480,
Tokens = 42,
RankedMmr = 1240,
Matches = 10,
Wins = 6,
});
var auth = new AuthService(api, tokens, player);
var canvas = UiKit.CreateCanvas("Test Canvas");
var view = new LobbyView(
canvas.transform, config, auth, player, () => { },
() => { }, () => { }, () => { }, () => { }, () => { }, () => { });
var root = Resources.FindObjectsOfTypeAll<GameObject>()
.FirstOrDefault(go => go.name == LobbyView.RootName);
Assert.IsNotNull(root, "LobbyScreen не создан");
var texts = root.GetComponentsInChildren<Text>(true)
.Select(text => text.text)
.ToList();
CollectionAssert.Contains(texts, "BATTLE");
CollectionAssert.Contains(texts, "DECK BUILDER");
CollectionAssert.Contains(texts, "SHOP");
Assert.IsTrue(texts.Contains("12580"), "Gold не показан");
Assert.IsTrue(texts.Contains("3480"), "Dust не показан");
Assert.IsTrue(texts.Contains("42"), "Tokens не показаны");
Assert.IsTrue(texts.Contains(StubUi.Soon), "Нет пометки «скоро»");
Assert.IsFalse(
texts.Any(text => text.Contains("Онбординг")),
"В Lobby не должно быть онбординга");
view.Dispose();
Cleanup();
}
[Test]
public void Community_Tabs_Switch_Content()
{
Loc.SetLanguage(UiLanguage.Ru);
var canvas = UiKit.CreateCanvas("Test Canvas");
var view = new CommunityView(canvas.transform, () => { });
view.Show();
var root = GameObject.Find(CommunityView.RootName);
Assert.IsNotNull(root, "CommunityScreen не создан");
var body = root.transform.Find("Body");
Assert.IsNotNull(body, "Нет контейнера вкладок");
view.SelectTab(CommunityTab.Tickets);
Assert.IsTrue(body.Find("Content2").gameObject.activeSelf, "Тикеты не открылись");
Assert.IsFalse(body.Find("Content0").gameObject.activeSelf, "Гайды остались открытыми");
view.SelectTab(CommunityTab.Guides);
Assert.IsTrue(body.Find("Content0").gameObject.activeSelf, "Гайды не открылись");
Assert.IsFalse(body.Find("Content2").gameObject.activeSelf, "Тикеты остались открытыми");
Cleanup();
}
private static bool Exists(string name) =>
Resources.FindObjectsOfTypeAll<GameObject>().Any(go => go.name == name);

View File

@ -14,6 +14,9 @@ EditorBuildSettings:
- enabled: 1
path: Assets/Vanguard/Scenes/Lobby.unity
guid: 59a25f3bae0ec7c489d2a139d7eea285
- enabled: 1
path: Assets/Vanguard/Scenes/Community.unity
guid: c50376fc9fc6b4e4fbca40d005364930
- enabled: 1
path: Assets/Vanguard/Scenes/DeckBuilder.unity
guid: b8567d3f2f60dcc4f85794bdbaf95b45

View File

@ -4,7 +4,8 @@
**Анализ:** DeepSeek выполнен; код, сцены, префабы, SO и конфиги не менялись.
**Решения:** зафиксированы 2026-10-30 (раздел 8).
**Статус:** TASK-2026-021 (B2, Login) реализован и ждёт ручной приёмки;
TASK-2026-019 (A1 + C1–C3) — план готов, код не начат.
TASK-2026-022 (Lobby по референсу, сцена Community) реализован и ждёт
приёмки; TASK-2026-019 (A1 + C1–C3) — план готов, код не начат.
**Связанные документы:** `docs/client-visual-direction.md`,
`docs/3d-ux-backlog.md`, `docs/Development/ANIMATION-INVENTORY.md`,
`docs/Development/IMPLEMENTATION_PLAN.md` (TASK-2026-001…018).
@ -204,7 +205,7 @@ TASK с полями «Реализация» и «Проверка» по `AGEN
Критерий: Frame Debugger без лишних `SetPass`, SRP Batcher активен.
Зависит: A4.
- [ ] **B2. P0 — Login по референсу: 2D-арт + неоновый UI.**
- [x] **B2. P0 — Login по референсу: 2D-арт + неоновый UI.**
Решение 2026-10-31: фон — 2D key art на весь экран, 3D-окружение в Login не
строится (исключение из правила «3D в каждой сцене», см. 8.7). Состав
экрана, размеры и состояния — в `TASK-2026-021`
@ -212,7 +213,7 @@ TASK с полями «Реализация» и «Проверка» по `AGEN
Зачем: текущий Login — карточка 620×820 с полем сервера и кнопкой регистрации,
композиция не соответствует референсу.
Критерий: композиция совпадает с референсом, вход и регистрация работают.
Зависит: арт-файл от пользователя; `TASK-2026-021`.
Реализация: выполнена (`TASK-2026-021`); ручная приёмка — пользователь.
- [ ] **B3. P1 — Lobby и DeckBuilder: визуальное наполнение.**
Решение 2026-10-31: `Lobby` — 2D дашборд по референсу (фон, панели,
@ -220,6 +221,7 @@ TASK с полями «Реализация» и «Проверка» по `AGEN
подсветкой зон.
Зачем: Lobby собран из 6 кубов и gray-box панелей, DeckBuilder — из 3.
Критерий: у каждой сцены своя композиция, а не общий `switch` с кубами.
Реализация: Lobby — выполнена (`TASK-2026-022`); DeckBuilder — не начат.
Зависит: B1, A4, `TASK-2026-022`; для DeckBuilder — референс от
пользователя.

View File

@ -42,7 +42,7 @@
| TASK-2026-019 | `[ ]` | Загрузочная сцена и кадр без покадровых аллокаций | DeepSeek: план готов; реализация ожидает | не выполнена |
| TASK-2026-020 | `[x]` | Парсер карт и артов с официального сайта | DeepSeek: анализ и реализация | идемпотентность 0 diff; `validate` не запускался |
| TASK-2026-021 | `[!]` | Login по референсу: 2D-арт и неоновый UI | DeepSeek: C1–C9, Tab/Enter/Esc; ждёт приёмки | EditMode 60/60; `SCENE_SMOKE success=True`; standalone собран; ручная — пользователь |
| TASK-2026-022 | `[ ]` | Lobby по референсу: дашборд, заглушки, сцена Community | DeepSeek: анализ и план готовы; реализация ожидает | не выполнена |
| TASK-2026-022 | `[!]` | Lobby по референсу: дашборд, заглушки, сцена Community | DeepSeek: C1–C11 реализованы; ждёт ручной приёмки | EditMode 62/62; `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone; ручная — пользователь |
Новые задачи добавляются в конец файла отдельными разделами. Не используй один
раздел для нескольких независимых задач.
@ -2056,11 +2056,11 @@ HUD с номером хода и фазовой шкалой, компактн
### TASK-2026-022 — Lobby по референсу: дашборд, заглушки и сцена Community
- **Статус:** `[ ]`
- **Статус:** `[!]`
- **Создано:** 2026-10-31
- **Анализ:** DeepSeek выполнен (референс Lobby от 2026-10-31, решения ниже)
- **Реализация:** не начата
- **Проверка:** не выполнена
- **Реализация:** DeepSeek: C1–C11 выполнены
- **Проверка:** EditMode 62/62; `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone; ручная приёмка — за пользователем
- **Затронутые системы:** `LobbySceneController`, новый `LobbyView` (вместо
`MetaView`), новый `LobbyBackdrop`, новый `BattleEntryOverlay`, меню,
новая сцена `Community` + `CommunitySceneController`/`CommunityView`,
@ -2160,16 +2160,16 @@ HUD с номером хода и фазовой шкалой, компактн
#### C. Чек-лист изменений
- [ ] **C1. `CREATE` `Runtime/UI/LobbyBackdrop.cs`** — процедурный фон.
- Вертикальный градиент, сетка/гексы, виньетка, 2–3 неоновые линии;
спрайты кешируются статически, создаются один раз.
- [x] **C1. `CREATE` `Runtime/UI/LobbyBackdrop.cs`** — процедурный фон.
- Вертикальный градиент, сетка, центральное свечение и виньетка; спрайт
кешируется статически, создаётся один раз.
- Cover-масштабирование на 16:9/16:10/21:9 без полос; при появлении арта
заменяется одним изображением без правки раскладки.
- Объект называется `LobbyBackdrop` для смоука.
- Реализация: не выполнена. Проверка: не выполнена.
- Реализация: выполнена. Проверка: EditMode 62/62, `SCENE_SMOKE`.
- Зависит: нет.
- [ ] **C2. `CREATE` `Runtime/UI/LobbyView.cs`** — новый дашборд вместо
- [x] **C2. `CREATE` `Runtime/UI/LobbyView.cs`** — новый дашборд вместо
`MetaView`.
- Топбар: аватар-инициалы, ник, MMR, сезон-заглушка, «Рейтинг»,
«Сообщество», «Меню».
@ -2177,89 +2177,94 @@ HUD с номером хода и фазовой шкалой, компактн
блок миссий-заглушка; балансы Gold/Dust/Tokens с переходом в магазин.
- Центр: `Chat`-заглушка; справа: `Friends Online`-заглушка.
- Корневой объект `LobbyScreen`; тексты через `Loc`.
- Реализация: не выполнена. Проверка: не выполнена.
- Реализация: выполнена. Проверка: `Lobby_Shows_Actions_Stubs_And_Balances_Without_Onboarding`.
- Зависит: C1, C9.
- [ ] **C3. `MODIFY` `Runtime/App/LobbySceneController.cs`** — сборка Lobby
- [x] **C3. `MODIFY` `Runtime/App/LobbySceneController.cs`** — сборка Lobby
без 3D и без `MetaView`.
- Убрать `SceneEnvironment.Build(SceneTheme.Lobby)`; создать
`LobbyBackdrop`, `LobbyView`, оверлеи.
- Убрать `ShowOnboarding`/`_onboardingButton` из лобби; оставить переход
на отдельный экран выбора триал-деки при `!OnboardingCompleted`.
- Подключить BATTLE-оверлей, Rating-заглушку, Community → `SceneFlow`.
- Реализация: не выполнена. Проверка: не выполнена.
- Убран `SceneEnvironment.Build(SceneTheme.Lobby)`; создаются
`LobbyBackdrop`, `LobbyView`, `BattleEntryOverlay`, рейтинг-заглушка и
`HotkeysDriver` (Esc).
- Убраны статус и кнопка онбординга из лобби; при `!OnboardingCompleted`
по-прежнему открывается отдельный экран выбора триал-деки.
- Подключены BATTLE-оверлей, рейтинг-заглушка, Community → `SceneFlow`.
- Реализация: выполнена. Проверка: EditMode 62/62, `SCENE_SMOKE`.
- Зависит: C2, C4, C5, C6.
- [ ] **C4. `CREATE` `Runtime/UI/BattleEntryOverlay.cs`** — выбор колоды и
- [x] **C4. `CREATE` `Runtime/UI/BattleEntryOverlay.cs`** — выбор колоды и
режима.
- Загрузка колод `MetaService.GetDecksAsync`, выбор карточкой, режимы
«Рейтинговый» / «С ботом», кнопка «В бой», «Собрать колоду».
- Валидация через `ValidateDeckAsync` перед запуском; ошибки — в панели.
- Ranked → `Flow.OpenLobbyWithQueueAsync`; практика →
- Ranked закрывает оверлей и открывает существующую очередь
(`LobbySceneController.OnRanked` → `QueueView.Show`); практика —
`StartPracticeAsync` + `Flow.LoadBattleAsync`.
- Закрытие: крестик, Esc, клик вне панели; фон блокируется.
- Реализация: не выполнена. Проверка: не выполнена.
- Закрытие: крестик и Esc (фон блокируется).
- Реализация: выполнена. Проверка: компиляция, EditMode 62/62; сетевой
сценарий — ручная проверка.
- Зависит: C2.
- [ ] **C5. `CREATE` сцена `Community`.**
- [x] **C5. `CREATE` сцена `Community`.**
- `Scenes/Community.unity`, `CommunitySceneController`,
`Runtime/UI/CommunityView` с вкладками «Гайды / Колоды / Тикеты» и
заглушками; кнопка «Назад» в Lobby.
- `SceneFlow`: константа `Community` и `LoadCommunityAsync`;
`SceneBuilder`: создание сцены и добавление в Build Settings после
`Lobby`; `SceneSmoke` — проверка экрана `CommunityScreen`.
`Lobby`.
- Серверных вызовов нет.
- Реализация: не выполнена. Проверка: не выполнена.
- Реализация: выполнена. Проверка: `Community_Tabs_Switch_Content`,
сцена собрана (6 сцен в Build Settings).
- Зависит: C2.
- [ ] **C6. `MODIFY` меню** — общий гамбургер для Login и Lobby.
- Вынести `LoginMenu` в общий `Runtime/UI/HamburgerMenu.cs` (язык,
версия, адрес сервера, закрыть; для Lobby дополнительно «Выйти»).
- [x] **C6. `MODIFY` меню** — общий гамбургер для Login и Lobby.
- `LoginMenu` вынесен в общий `Runtime/UI/HamburgerMenu.cs` (язык,
версия, адрес сервера, закрыть; для Lobby дополнительно «История
матчей» и «Выйти»).
- `Login` использует его без визуальных изменений; в Lobby «Выйти»
переезжает из топбара в меню.
- Esc и клик вне панели закрывают; вне меню ввод формы не перехватывается.
- Реализация: не выполнена. Проверка: не выполнена.
переехал из топбара в меню.
- Esc закрывает меню через `HotkeysDriver`; клик вне панели работает.
- Реализация: выполнена. Проверка: EditMode 62/62, Login не изменён.
- Зависит: C2.
- [ ] **C7. `DELETE`/`MODIFY` `Runtime/UI/MetaView.cs`** — удалить после
- [x] **C7. `DELETE`/`MODIFY` `Runtime/UI/MetaView.cs`** — удалить после
переноса функций в `LobbyView`.
- Убедиться, что в `MetaView` не осталось уникальной логики (обновление
профиля, 401 → Logout) — она переносится в `LobbyView`.
- Реализация: не выполнена. Проверка: не выполнена.
- Обновление профиля и обработка 401 → Logout перенесены в `LobbyView`;
`MetaView` удалён вместе с `.meta`.
- Реализация: выполнена. Проверка: EditMode 62/62, ссылок на `MetaView`
не осталось.
- Зависит: C2, C3.
- [ ] **C8. `MODIFY` `Editor/SceneSmoke.cs` + `Editor/SceneBuilder.cs`.**
- [x] **C8. `MODIFY` `Editor/SceneSmoke.cs` + `Editor/SceneBuilder.cs`.**
- `Lobby` проверяется по `LobbyBackdrop` и `LobbyScreen`; 3D-окружение
больше не требуется.
- Для `Community` — отдельная проверка по состоянию; при необходимости
расширить смоук на несколько сцен.
- Реализация: не выполнена. Проверка: не выполнена.
для неё больше не требуется.
- `SceneBuilder` создаёт `Community` и добавляет её в Build Settings после
`Lobby`.
- Реализация: выполнена. Проверка: `SCENE_SMOKE success=True
active=Login backdrop=True`; сцена Community собрана.
- Зависит: C2, C5.
- [ ] **C9. `MODIFY` `Runtime/UI/Localization.cs`** — новые ключи.
- BATTLE / DECK BUILDER / SHOP и подписи, «Рейтинг», «Сообщество»,
«Меню», «Задания», «Чат», «Друзья онлайн», «скоро», «Режим»,
«Рейтинговый», «С ботом», «Собрать колоду», «Выйти», вкладки Community.
- Реализация: не выполнена. Проверка: не выполнена.
- [x] **C9. `MODIFY` `Runtime/UI/Localization.cs`** — новые ключи.
- Рейтинг, Сообщество, подписи плашек, задания, чат, друзья, «скоро»,
режимы, ошибки проверки колоды, вкладки Community.
- Реализация: выполнена. Проверка: дубликатов ключей нет, EditMode 62/62.
- Зависит: C2, C4, C5, C6.
- [ ] **C10. `TEST` EditMode.**
- `LobbyView`: топбар, три плашки, балансы, заглушки disabled и с
пометкой «скоро»; в Lobby нет текста/кнопки онбординга.
- `BattleEntryOverlay`: список колод, disabled без валидной колоды,
закрытие по Esc, выбор режима.
- `CommunityView`: переключение вкладок, «Назад».
- `SceneSmoke`: `Lobby` без 3D, `Community` открывается.
- Существующие тесты `ClientSmokeTests` (создание `MetaView`) переписать
под `LobbyView`.
- Реализация: не выполнена. Проверка: не выполнена.
- [x] **C10. `TEST` EditMode.**
- `LobbyView`: три плашки, балансы, «скоро», отсутствие онбординга.
- `CommunityView`: переключение вкладок.
- Существующий `Client_Views_Build` переведён с `MetaView` на `LobbyView`;
проверка `LobbyScreen`.
- Реализация: выполнена. Проверка: EditMode **62/62** в копии без
`Library`.
- Зависит: C1–C9.
- [ ] **C11. `VERIFY` EditMode, smoke, standalone.**
- [x] **C11. `VERIFY` EditMode, smoke, standalone.**
- Копия проекта без `Library`; `SceneSmoke.Run`, `RunPlaymat`;
standalone-сборка.
- Ручная сверка с референсом — пользователь.
- Реализация: не выполнена. Проверка: не выполнена.
standalone-сборка и запуск.
- Реализация: выполнена. Проверка: EditMode 62/62,
`SCENE_SMOKE success=True`, `PLAYMAT_SMOKE success=True`,
`WINDOWS_BUILD result=Succeeded errors=0`; визуальная сверка с
референсом — за пользователем.
- Зависит: C10.
#### D. Последовательность выполнения
@ -2319,11 +2324,24 @@ HUD с номером хода и фазовой шкалой, компактн
#### G. Результат
- **Реализовано:** —
- **Проверки:** —
- **Не завершено/заблокировано:** —
- **Skill обновлён:** —
- **Git:** —
- **Реализовано:** C1–C11. Lobby — 2D-дашборд: процедурный фон, топбар
(инициалы, ник, MMR, сезон-заглушка), три плашки, миссии-заглушка,
балансы, чат- и друзья-заглушки, меню с историей и выходом. BATTLE —
оверлей выбора колоды/режима с серверной валидацией. Rating — заглушка.
Community — отдельная сцена с вкладками. `MetaView` удалён.
- **Проверки:** EditMode **62/62** в копии без `Library`;
`SCENE_SMOKE success=True active=Login env=False backdrop=True`;
`PLAYMAT_SMOKE success=True board=True cards=25 hand=5/5`;
`WINDOWS_BUILD result=Succeeded errors=0`; standalone запущен.
- **Не завершено:** визуальная сверка с референсом, BATTLE → очередь/бот,
магазин/история/коллекция, Community и меню — ручная проверка
пользователя.
- **Замечания:** сцены `Login/Lobby/DeckBuilder/Battle` после
`SceneBuilder.BuildAll` содержали только churn fileID — откачены;
в репозиторий добавлена только новая `Community.unity`. Арт фона Lobby
отсутствует, фон процедурный.
- **Skill обновлён:** не требуется; правила и архитектура не менялись.
- **Git:** не коммитилось.

View File

@ -6,6 +6,26 @@
---
## 2026-10-31 — TASK-2026-022: Lobby по референсу
- `Lobby` переведён на 2D-дашборд: процедурный фон, топбар (инициалы, ник,
MMR, сезон-заглушка), плашки BATTLE / DECK BUILDER / SHOP, миссии-заглушка,
балансы, чат- и друзья-заглушки. 3D-окружение в `Lobby` больше не строится.
- BATTLE открывает оверлей выбора колоды и режима (Ranked / С ботом) с
серверной валидацией; Ranked уходит в существующую очередь.
- Rating — заглушка лидерборда; Community — отдельная сцена с вкладками
«Гайды / Колоды / Тикеты» (каркас).
- Общее меню вынесено в `HamburgerMenu` (Login и Lobby); в Lobby добавлены
«История матчей» и «Выйти». Esc закрывает меню и оверлеи.
- `MetaView` удалён; онбординг в Lobby не показывается, выбор триал-деки
остаётся отдельным экраном.
- Проверки: EditMode **62/62**, `SCENE_SMOKE success=True`,
`PLAYMAT_SMOKE success=True`, `WINDOWS_BUILD result=Succeeded`.
- Визуальная приёмка и сетевые сценарии (очередь/бот, магазин, история,
Community) — за пользователем.
---
## 2026-10-31 — TASK-2026-021: Login по референсу
- Экран `Login` переведён на 2D key art (`Resources/Login/login_bg`) с

View File

@ -35,10 +35,11 @@
- Пакеты: URP, Input System, Addressables, Localization, Newtonsoft.Json,
VContainer (OpenUPM).
- Визуальное направление: **3D**, отдельные сцены-экраны:
`Login`, `Lobby`, `DeckBuilder`, `Battle`.
`Login`, `Lobby`, `Community`, `DeckBuilder`, `Battle`.
`DeckBuilder` совмещает коллекцию и колоды; `Boot` — стартовая служебная
сцена; `VanguardApp` персистентный.
- Переходы — `SceneFlow`; очередь/онбординг — оверлеи **Lobby**.
- Переходы — `SceneFlow`; очередь, магазин и история — экраны **Lobby**;
выбор стартовой триал-деки — отдельный экран до Lobby.
- 3D-окружение сейчас из примитивов (`SceneEnvironment`): камера, свет,
пол, тематические кубы. UI — code-generated uGUI, пока gray-box.
- Login переведён на референс: полноэкранный арт
@ -46,6 +47,12 @@
сервера). Tab/Shift+Tab переключают поля, Enter отправляет форму, Esc
закрывает меню. 3D-окружение в `Login` больше не строится
(`TASK-2026-021`).
- Lobby — 2D-дашборд по референсу (`TASK-2026-022`): процедурный фон,
топбар с MMR и меню, BATTLE/DECK BUILDER/SHOP, миссии, балансы,
заглушки чата и друзей; «Рейтинг» — заглушка, «Сообщество» — отдельная
сцена `Community` с вкладками «Гайды / Колоды / Тикеты». BATTLE
открывает оверлей выбора колоды и режима (Ranked / С ботом). 3D в Lobby
не строится; `MetaView` удалён.
- Интерфейс рассчитан на reference **1920×1080** (`CanvasScaler`,
`matchWidthOrHeight = 0.5`); standalone принудительно открывается в
1920×1080 fullscreen, `defaultIsNativeResolution` выключен.
@ -278,7 +285,7 @@ Standalone Windows:
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
```
Клиентские проверки: **60/60** EditMode (включая новые тесты TASK-2026-001:
Клиентские проверки: **62/62** EditMode (включая новые тесты TASK-2026-001:
повторный render скрытых карт, открытие урона, source-позиция нового юнита,
coalescing `CardMoveAnimator`; сборка всех экранов, импорт арта, перевод
строк, live-переключение языка, рендер 3D-доски, геометрия поля, правила
@ -309,9 +316,12 @@ client/Assets/Vanguard/Runtime/App/
DeckBuilderSceneController.cs
BattleSceneController.cs
client/Assets/Vanguard/Runtime/UI/
LoginView.cs MetaView.cs OnboardingView.cs
LoginView.cs OnboardingView.cs
LobbyView.cs LobbyBackdrop.cs LobbyIcon.cs StubUi.cs
BattleEntryOverlay.cs ComingSoonOverlay.cs HamburgerMenu.cs
CommunityView.cs
DeckBuilderView.cs QueueView.cs BattleView.cs
UiKit.cs Localization.cs
UiKit.cs NeonUi.cs Localization.cs
client/Assets/Vanguard/Runtime/UI/Overlays/
OverlayNavigationService.cs стек список → детали
CardListView.cs общий горизонтальный список
@ -363,9 +373,10 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
1. **TASK-2026-021 — Login по референсу: реализован, ждёт ручной приёмки**
(C10). EditMode 60/60, `SCENE_SMOKE success=True`, standalone собран.
**TASK-2026-022 — Lobby по референсу: анализ и план готовы**, реализация
ожидает решения пользователя. После неё — **TASK-2026-019**
(загрузочная сцена, fade, кадр без покадровых аллокаций).
**TASK-2026-022 — Lobby по референсу: реализован C1–C11, ждёт ручной
приёмки.** EditMode 62/62, `SCENE_SMOKE`, `PLAYMAT_SMOKE`, standalone
собраны и запущены. После приёмки — **TASK-2026-019** (загрузочная
сцена, fade, кадр без покадровых аллокаций).
2. **TASK-2026-001 — ручная проверка пользователем** (C10): draw, call, ride,
move/swap, guard, trigger, drive/damage, retire, search, открытие скрытого
урона, рука противника, повторные клики UI во время/после анимации.