449 lines
16 KiB
C#
449 lines
16 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|