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

449 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

using System;
using System.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;
}
}
}