client: Lobby по референсу — дашборд, BATTLE-оверлей и сцена Community
CI / test (push) Successful in 3m4s
Details
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:
parent
d7395780a3
commit
179aa31da6
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 001ba3cec4018cb44b121859d9b38d08
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fb9a2911fede84d419e8ca7de4164fad
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b6e3834537fd38641aebc38d141e1c64
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 61a538c7871f1924ea1f9671cf287c69
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7bfdadaad5565034fbda5660dc99fd78
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 07d4138d68deb1043a10c6c4f88a9266
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 61932e95c3c9b0043a331d7e3bfcd832
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c48efadcc55597c4ca139d4b3d15fb93
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 693da1bea283cab4bac9e9caa525032d
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 254c5fe6df971b942bde7efb1a5dab42
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 784de18e909e5564b88fe60a321062cf
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c50376fc9fc6b4e4fbca40d005364930
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 — референс от
|
||||
пользователя.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:** не коммитилось.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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`) с
|
||||
|
|
|
|||
|
|
@ -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 во время/после анимации.
|
||||
|
|
|
|||
Loading…
Reference in New Issue