Add boost selection, battle feedback, and card art pipeline
CI / test (push) Successful in 9m2s
Details
CI / test (push) Successful in 9m2s
Details
This commit is contained in:
parent
17c589ab1e
commit
84e2706d8d
|
|
@ -268,7 +268,8 @@
|
||||||
|
|
||||||
**Визуальный каркас есть:** доска, рука, карточные рамки/арт-заглушки
|
**Визуальный каркас есть:** доска, рука, карточные рамки/арт-заглушки
|
||||||
по клану, сила/крит, часы, все легальные интенты кнопками, сдача и
|
по клану, сила/крит, часы, все легальные интенты кнопками, сдача и
|
||||||
результат. Реальные арты и анимации — следующие. Финальный визуал —
|
результат. Реальные арты подключаются через `imageKey`, event-фидбек уже
|
||||||
|
есть; полноценная очередь анимаций — следующая. Финальный визуал —
|
||||||
отдельная 3D-сцена `Battle`, а не текущий Canvas
|
отдельная 3D-сцена `Battle`, а не текущий Canvas
|
||||||
(`docs/client-visual-direction.md`).
|
(`docs/client-visual-direction.md`).
|
||||||
|
|
||||||
|
|
@ -279,6 +280,8 @@
|
||||||
- [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»).
|
- [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»).
|
||||||
- [x] Ввод: кнопки легальных интентов; клик по картам/зонам,
|
- [x] Ввод: кнопки легальных интентов; клик по картам/зонам,
|
||||||
подсветка целей, группировка действий и журнал боя.
|
подсветка целей, группировка действий и журнал боя.
|
||||||
|
- [x] Boost: выбор карты заднего ряда как поддержки перед атакой.
|
||||||
|
- [x] Event-фидбек: атака, guard, drive, урон, heal, retire, perfect guard.
|
||||||
- [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером.
|
- [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером.
|
||||||
- [ ] Очередь анимаций строго по событиям сервера: ride, call, attack, drive, damage, retire, boost, guard.
|
- [ ] Очередь анимаций строго по событиям сервера: ride, call, attack, drive, damage, retire, boost, guard.
|
||||||
- [ ] Скип/ускорение анимаций.
|
- [ ] Скип/ускорение анимаций.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1e9c4a7b2d5f4c8e9a3b6d0f7c2e8a51
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7a3d1f8c5e2b4a6d9c0f3b7e1a8d2c64
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Card art drop-in
|
||||||
|
|
||||||
|
Положите сюда изображения карт с именем `{imageKey}.png` (или `.jpg`).
|
||||||
|
`imageKey` приходит из `GET /api/cards` (например, `bt01_034`).
|
||||||
|
|
||||||
|
Unity импортирует файл как Sprite; `CardImageService` подхватит его через
|
||||||
|
`Resources.Load("CardArt/{imageKey}")`. Если файла нет — используется
|
||||||
|
процедурный fallback из `CardArt`.
|
||||||
|
|
||||||
|
Рекомендуемый размер: 512×720, Sprite (2D and UI), без анимации.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0b7e4c1f8a2d4f6e9c3a5b7d1e8f2a46
|
||||||
|
|
@ -312,6 +312,7 @@ namespace Vanguard.Api.Contracts
|
||||||
[JsonProperty("hasIntercept")] public bool HasIntercept { get; set; }
|
[JsonProperty("hasIntercept")] public bool HasIntercept { get; set; }
|
||||||
[JsonProperty("isParallel")] public bool IsParallel { get; set; }
|
[JsonProperty("isParallel")] public bool IsParallel { get; set; }
|
||||||
[JsonProperty("basePrintingId")] public string BasePrintingId { get; set; }
|
[JsonProperty("basePrintingId")] public string BasePrintingId { get; set; }
|
||||||
|
[JsonProperty("imageKey")] public string ImageKey { get; set; }
|
||||||
[JsonProperty("textEn")] public string TextEn { get; set; }
|
[JsonProperty("textEn")] public string TextEn { get; set; }
|
||||||
[JsonProperty("textRu")] public string TextRu { get; set; }
|
[JsonProperty("textRu")] public string TextRu { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ namespace Vanguard.UI
|
||||||
private readonly RectTransform _result;
|
private readonly RectTransform _result;
|
||||||
private RectTransform _logContent;
|
private RectTransform _logContent;
|
||||||
private Text _resultText;
|
private Text _resultText;
|
||||||
|
private Text _feedback;
|
||||||
|
private Image _flash;
|
||||||
|
private float _feedbackTime;
|
||||||
|
private float _feedbackDuration;
|
||||||
|
private Color _feedbackColor;
|
||||||
private readonly List<string> _logLines = new List<string>();
|
private readonly List<string> _logLines = new List<string>();
|
||||||
|
|
||||||
private GameConnection _connection;
|
private GameConnection _connection;
|
||||||
|
|
@ -60,6 +65,7 @@ namespace Vanguard.UI
|
||||||
private long _clockReceivedMs;
|
private long _clockReceivedMs;
|
||||||
private readonly List<IntentPayload> _pendingIntents = new List<IntentPayload>();
|
private readonly List<IntentPayload> _pendingIntents = new List<IntentPayload>();
|
||||||
private readonly HashSet<string> _highlightKeys = new HashSet<string>(StringComparer.Ordinal);
|
private readonly HashSet<string> _highlightKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
private int _pendingBoosterSlot = -1;
|
||||||
|
|
||||||
public BattleView(Transform parent, MetaService meta, Action onExit)
|
public BattleView(Transform parent, MetaService meta, Action onExit)
|
||||||
{
|
{
|
||||||
|
|
@ -140,6 +146,23 @@ namespace Vanguard.UI
|
||||||
_result = BuildResultPanel(_root);
|
_result = BuildResultPanel(_root);
|
||||||
_result.gameObject.SetActive(false);
|
_result.gameObject.SetActive(false);
|
||||||
|
|
||||||
|
_flash = UiKit.CreatePanel(_root, "BattleFlash", Color.clear).GetComponent<Image>();
|
||||||
|
_flash.raycastTarget = false;
|
||||||
|
_flash.gameObject.SetActive(false);
|
||||||
|
|
||||||
|
_feedback = UiKit.CreateText(
|
||||||
|
_root, "BattleFeedback", string.Empty, 44,
|
||||||
|
Color.white, TextAnchor.MiddleCenter);
|
||||||
|
_feedback.fontStyle = FontStyle.Bold;
|
||||||
|
_feedback.raycastTarget = false;
|
||||||
|
var feedbackRect = _feedback.rectTransform;
|
||||||
|
feedbackRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||||
|
feedbackRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||||
|
feedbackRect.pivot = new Vector2(0.5f, 0.5f);
|
||||||
|
feedbackRect.sizeDelta = new Vector2(900f, 120f);
|
||||||
|
feedbackRect.anchoredPosition = new Vector2(0f, 130f);
|
||||||
|
_feedback.gameObject.SetActive(false);
|
||||||
|
|
||||||
_root.gameObject.SetActive(false);
|
_root.gameObject.SetActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,9 +178,13 @@ namespace Vanguard.UI
|
||||||
_result.gameObject.SetActive(false);
|
_result.gameObject.SetActive(false);
|
||||||
_pendingIntents.Clear();
|
_pendingIntents.Clear();
|
||||||
_highlightKeys.Clear();
|
_highlightKeys.Clear();
|
||||||
|
_pendingBoosterSlot = -1;
|
||||||
_showAllIntents = false;
|
_showAllIntents = false;
|
||||||
_logLines.Clear();
|
_logLines.Clear();
|
||||||
RebuildLog();
|
RebuildLog();
|
||||||
|
_feedbackTime = 0f;
|
||||||
|
_feedback.gameObject.SetActive(false);
|
||||||
|
_flash.gameObject.SetActive(false);
|
||||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||||
SetStatus("Подключаемся к матчу…", Muted);
|
SetStatus("Подключаемся к матчу…", Muted);
|
||||||
_root.gameObject.SetActive(true);
|
_root.gameObject.SetActive(true);
|
||||||
|
|
@ -180,6 +207,8 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
_root.gameObject.SetActive(false);
|
_root.gameObject.SetActive(false);
|
||||||
_result.gameObject.SetActive(false);
|
_result.gameObject.SetActive(false);
|
||||||
|
_feedback.gameObject.SetActive(false);
|
||||||
|
_flash.gameObject.SetActive(false);
|
||||||
_view = null;
|
_view = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,6 +221,45 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateClock();
|
UpdateClock();
|
||||||
|
UpdateFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowFeedback(string text, Color color, float duration = 1.25f, bool flash = false)
|
||||||
|
{
|
||||||
|
_feedback.text = text;
|
||||||
|
_feedbackColor = color;
|
||||||
|
_feedbackDuration = duration;
|
||||||
|
_feedbackTime = duration;
|
||||||
|
_feedback.gameObject.SetActive(true);
|
||||||
|
_feedback.rectTransform.localScale = Vector3.one * 1.12f;
|
||||||
|
|
||||||
|
if (flash)
|
||||||
|
{
|
||||||
|
_flash.gameObject.SetActive(true);
|
||||||
|
_flash.color = new Color(color.r, color.g, color.b, 0.22f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFeedback()
|
||||||
|
{
|
||||||
|
if (_feedbackTime <= 0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_feedbackTime -= Time.unscaledDeltaTime;
|
||||||
|
var t = Mathf.Clamp01(_feedbackTime / Mathf.Max(0.01f, _feedbackDuration));
|
||||||
|
var alpha = Mathf.Clamp01(t * 1.8f);
|
||||||
|
_feedback.color = new Color(_feedbackColor.r, _feedbackColor.g, _feedbackColor.b, alpha);
|
||||||
|
_feedback.rectTransform.localScale = Vector3.one * Mathf.Lerp(1f, 1.12f, t);
|
||||||
|
|
||||||
|
if (_flash != null && _flash.gameObject.activeSelf)
|
||||||
|
_flash.color = new Color(_feedbackColor.r, _feedbackColor.g, _feedbackColor.b, alpha * 0.22f);
|
||||||
|
|
||||||
|
if (_feedbackTime > 0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_feedback.gameObject.SetActive(false);
|
||||||
|
if (_flash != null)
|
||||||
|
_flash.gameObject.SetActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RectTransform BuildResultPanel(Transform parent)
|
private RectTransform BuildResultPanel(Transform parent)
|
||||||
|
|
@ -308,6 +376,7 @@ namespace Vanguard.UI
|
||||||
_finished = _view.IsFinished;
|
_finished = _view.IsFinished;
|
||||||
_pendingIntents.Clear();
|
_pendingIntents.Clear();
|
||||||
_highlightKeys.Clear();
|
_highlightKeys.Clear();
|
||||||
|
_pendingBoosterSlot = -1;
|
||||||
if (_view.Clock != null)
|
if (_view.Clock != null)
|
||||||
{
|
{
|
||||||
_clockServerMs = _view.Clock.ServerTimeMs;
|
_clockServerMs = _view.Clock.ServerTimeMs;
|
||||||
|
|
@ -326,6 +395,8 @@ namespace Vanguard.UI
|
||||||
var line = Describe(envelope.Event);
|
var line = Describe(envelope.Event);
|
||||||
if (!string.IsNullOrWhiteSpace(line))
|
if (!string.IsNullOrWhiteSpace(line))
|
||||||
_logLines.Add(line);
|
_logLines.Add(line);
|
||||||
|
|
||||||
|
ShowFeedbackFor(envelope.Event);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (_logLines.Count > 40)
|
while (_logLines.Count > 40)
|
||||||
|
|
@ -357,6 +428,51 @@ namespace Vanguard.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowFeedbackFor(EventView view)
|
||||||
|
{
|
||||||
|
switch (view)
|
||||||
|
{
|
||||||
|
case AttackDeclaredView attack:
|
||||||
|
ShowFeedback("АТАКА " + SlotName(attack.AttackerSlot)
|
||||||
|
+ " → " + SlotName(attack.TargetSlot),
|
||||||
|
new Color(1f, 0.82f, 0.35f), 1.0f);
|
||||||
|
break;
|
||||||
|
case GuardAddedView guard:
|
||||||
|
ShowFeedback("GUARD +" + guard.Shield,
|
||||||
|
new Color(0.45f, 0.82f, 1f), 1.0f);
|
||||||
|
break;
|
||||||
|
case DriveCheckedView drive:
|
||||||
|
ShowFeedback("DRIVE " + TriggerShort(drive.Trigger.ToString()),
|
||||||
|
new Color(0.72f, 0.88f, 1f), 1.0f);
|
||||||
|
break;
|
||||||
|
case TriggerResolvedView trigger:
|
||||||
|
ShowFeedback("TRIGGER " + trigger.Trigger,
|
||||||
|
new Color(0.72f, 0.88f, 1f), 1.1f);
|
||||||
|
break;
|
||||||
|
case BattleResolvedView battle:
|
||||||
|
ShowFeedback(battle.Hit ? "HIT" : "BLOCK",
|
||||||
|
battle.Hit ? new Color(1f, 0.45f, 0.35f) : new Color(0.45f, 0.82f, 1f),
|
||||||
|
0.9f);
|
||||||
|
break;
|
||||||
|
case DamageTakenView damage:
|
||||||
|
ShowFeedback("+" + damage.Amount + " DAMAGE",
|
||||||
|
new Color(1f, 0.30f, 0.26f), 1.2f, flash: true);
|
||||||
|
break;
|
||||||
|
case HealedView:
|
||||||
|
ShowFeedback("HEAL",
|
||||||
|
new Color(0.45f, 0.95f, 0.62f), 1.1f, flash: true);
|
||||||
|
break;
|
||||||
|
case UnitRetiredView:
|
||||||
|
ShowFeedback("RETIRE",
|
||||||
|
new Color(1f, 0.55f, 0.30f), 1.0f);
|
||||||
|
break;
|
||||||
|
case PerfectGuardView:
|
||||||
|
ShowFeedback("PERFECT GUARD",
|
||||||
|
new Color(0.60f, 0.82f, 1f), 1.1f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string Describe(EventView view)
|
private string Describe(EventView view)
|
||||||
{
|
{
|
||||||
if (view == null)
|
if (view == null)
|
||||||
|
|
@ -451,6 +567,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
_pendingIntents.Clear();
|
_pendingIntents.Clear();
|
||||||
_highlightKeys.Clear();
|
_highlightKeys.Clear();
|
||||||
|
_pendingBoosterSlot = -1;
|
||||||
_sending = true;
|
_sending = true;
|
||||||
SetStatus("Отправляем: " + Label(intent), Muted);
|
SetStatus("Отправляем: " + Label(intent), Muted);
|
||||||
|
|
||||||
|
|
@ -840,13 +957,21 @@ namespace Vanguard.UI
|
||||||
var art = UiKit.CreatePanel(panel, "Art", Color.white);
|
var art = UiKit.CreatePanel(panel, "Art", Color.white);
|
||||||
UiKit.SetPreferredHeight(art, compact ? 46f : 58f);
|
UiKit.SetPreferredHeight(art, compact ? 46f : 58f);
|
||||||
var artImage = art.GetComponent<Image>();
|
var artImage = art.GetComponent<Image>();
|
||||||
artImage.sprite = card.FaceUp
|
if (card.FaceUp)
|
||||||
? CardArt.ForCard(
|
{
|
||||||
meta != null ? meta.NameId : card.PrintingId,
|
CardImageService.Apply(
|
||||||
meta != null ? meta.Clan : "None")
|
artImage,
|
||||||
: CardArt.Back;
|
meta != null ? meta.ImageKey : null,
|
||||||
artImage.color = Color.white;
|
CardArt.ForCard(
|
||||||
artImage.preserveAspect = false;
|
meta != null ? meta.NameId : card.PrintingId,
|
||||||
|
meta != null ? meta.Clan : "None"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
artImage.sprite = CardArt.Back;
|
||||||
|
artImage.color = Color.white;
|
||||||
|
artImage.preserveAspect = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (card.FaceUp)
|
if (card.FaceUp)
|
||||||
{
|
{
|
||||||
|
|
@ -936,9 +1061,38 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
if (_pendingIntents.Count > 0)
|
if (_pendingIntents.Count > 0)
|
||||||
{
|
{
|
||||||
|
// Клик по своему заднему ряду выбирает карту поддержки.
|
||||||
|
if (target.Player == _view.You && target.Slot is 0 or 1 or 2)
|
||||||
|
{
|
||||||
|
var boosted = _pendingIntents
|
||||||
|
.OfType<AttackIntent>()
|
||||||
|
.Where(attack => attack.BoosterSlot == target.Slot)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (boosted.Count > 0)
|
||||||
|
{
|
||||||
|
_pendingBoosterSlot = target.Slot;
|
||||||
|
RebuildHighlights();
|
||||||
|
RebuildIntents(_view);
|
||||||
|
SetStatus(
|
||||||
|
"Поддержка: " + CardName(target.PrintingId) + ". Выберите цель атаки.",
|
||||||
|
new Color(0.98f, 0.82f, 0.42f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var matches = _pendingIntents
|
var matches = _pendingIntents
|
||||||
.Where(intent => TryGetTarget(intent, out var candidate) && candidate.Matches(target))
|
.Where(intent => TryGetTarget(intent, out var candidate) && candidate.Matches(target))
|
||||||
.OrderBy(intent => intent is AttackIntent attack && attack.BoosterSlot >= 0 ? 1 : 0)
|
.OrderBy(intent =>
|
||||||
|
{
|
||||||
|
if (intent is not AttackIntent attack)
|
||||||
|
return 2;
|
||||||
|
|
||||||
|
if (_pendingBoosterSlot >= 0)
|
||||||
|
return attack.BoosterSlot == _pendingBoosterSlot ? 0 : 3;
|
||||||
|
|
||||||
|
return attack.BoosterSlot < 0 ? 0 : 1;
|
||||||
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (matches.Count > 0)
|
if (matches.Count > 0)
|
||||||
|
|
@ -988,6 +1142,7 @@ namespace Vanguard.UI
|
||||||
{
|
{
|
||||||
_pendingIntents.Clear();
|
_pendingIntents.Clear();
|
||||||
_highlightKeys.Clear();
|
_highlightKeys.Clear();
|
||||||
|
_pendingBoosterSlot = -1;
|
||||||
if (rebuild && _view != null)
|
if (rebuild && _view != null)
|
||||||
RebuildIntents(_view);
|
RebuildIntents(_view);
|
||||||
}
|
}
|
||||||
|
|
@ -999,6 +1154,15 @@ namespace Vanguard.UI
|
||||||
{
|
{
|
||||||
if (TryGetTarget(intent, out var target))
|
if (TryGetTarget(intent, out var target))
|
||||||
_highlightKeys.Add(TargetKey(target.Player, target.Slot, target.PrintingId));
|
_highlightKeys.Add(TargetKey(target.Player, target.Slot, target.PrintingId));
|
||||||
|
|
||||||
|
if (intent is AttackIntent attack && attack.BoosterSlot >= 0 && _view != null)
|
||||||
|
{
|
||||||
|
var booster = CardAt(_view.You, attack.BoosterSlot);
|
||||||
|
_highlightKeys.Add(TargetKey(
|
||||||
|
_view.You,
|
||||||
|
attack.BoosterSlot,
|
||||||
|
booster != null ? booster.PrintingId : null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1138,7 +1302,11 @@ namespace Vanguard.UI
|
||||||
var prompt = UiKit.CreateText(
|
var prompt = UiKit.CreateText(
|
||||||
_intents,
|
_intents,
|
||||||
"TargetPrompt",
|
"TargetPrompt",
|
||||||
"Выберите цель на поле.\nНажмите «Отмена», чтобы снять выбор.",
|
_pendingBoosterSlot >= 0
|
||||||
|
? "Поддержка: " + CardName(
|
||||||
|
CardAt(_view.You, _pendingBoosterSlot)?.PrintingId ?? string.Empty)
|
||||||
|
+ ".\nВыберите цель атаки или «Отмена»."
|
||||||
|
: "Выберите цель на поле.\nНажмите «Отмена», чтобы снять выбор.",
|
||||||
16,
|
16,
|
||||||
new Color(0.98f, 0.82f, 0.42f),
|
new Color(0.98f, 0.82f, 0.42f),
|
||||||
TextAnchor.UpperLeft);
|
TextAnchor.UpperLeft);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace Vanguard.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Загрузка настоящих артов по imageKey. Соглашение: спрайт лежит в
|
||||||
|
/// Assets/Vanguard/Resources/CardArt/{imageKey}.png. Пока файла нет —
|
||||||
|
/// используется процедурный CardArt. Позже этот же вход можно
|
||||||
|
/// перевести на Addressables без изменения экранов.
|
||||||
|
/// </summary>
|
||||||
|
public static class CardImageService
|
||||||
|
{
|
||||||
|
private const string ResourceFolder = "CardArt/";
|
||||||
|
private static readonly Dictionary<string, Sprite> Cache =
|
||||||
|
new Dictionary<string, Sprite>();
|
||||||
|
|
||||||
|
public static void Apply(Image target, string imageKey, Sprite fallback)
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.sprite = fallback;
|
||||||
|
target.color = Color.white;
|
||||||
|
target.preserveAspect = false;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(imageKey))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Cache.TryGetValue(imageKey, out var cached))
|
||||||
|
{
|
||||||
|
if (cached != null)
|
||||||
|
target.sprite = cached;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sprite = Resources.Load<Sprite>(ResourceFolder + imageKey);
|
||||||
|
Cache[imageKey] = sprite;
|
||||||
|
if (sprite != null)
|
||||||
|
target.sprite = sprite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4d2c7a9e6b1f4e8da3c5b7f0e9a21d63
|
||||||
|
|
@ -504,9 +504,10 @@ namespace Vanguard.UI
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var meta = _selectedMeta;
|
var meta = _selectedMeta;
|
||||||
_cardArt.sprite = CardArt.ForCard(meta.NameId, meta.Clan);
|
CardImageService.Apply(
|
||||||
_cardArt.color = Color.white;
|
_cardArt,
|
||||||
_cardArt.preserveAspect = false;
|
meta.ImageKey,
|
||||||
|
CardArt.ForCard(meta.NameId, meta.Clan));
|
||||||
_cardArtName.text = meta.NameEn;
|
_cardArtName.text = meta.NameEn;
|
||||||
_cardTitle.text = meta.NameEn;
|
_cardTitle.text = meta.NameEn;
|
||||||
_cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ")
|
_cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ")
|
||||||
|
|
@ -879,9 +880,10 @@ namespace Vanguard.UI
|
||||||
var art = UiKit.CreatePanel(cell, "Art", Color.white);
|
var art = UiKit.CreatePanel(cell, "Art", Color.white);
|
||||||
UiKit.SetPreferredHeight(art, 112f);
|
UiKit.SetPreferredHeight(art, 112f);
|
||||||
var artImage = art.GetComponent<Image>();
|
var artImage = art.GetComponent<Image>();
|
||||||
artImage.sprite = CardArt.ForCard(meta.NameId, meta.Clan);
|
CardImageService.Apply(
|
||||||
artImage.color = Color.white;
|
artImage,
|
||||||
artImage.preserveAspect = false;
|
meta.ImageKey,
|
||||||
|
CardArt.ForCard(meta.NameId, meta.Clan));
|
||||||
|
|
||||||
var artName = UiKit.CreateText(
|
var artName = UiKit.CreateText(
|
||||||
art, "ArtName", meta.NameEn, 12,
|
art, "ArtName", meta.NameEn, 12,
|
||||||
|
|
@ -964,11 +966,12 @@ namespace Vanguard.UI
|
||||||
row, "Art", Color.white);
|
row, "Art", Color.white);
|
||||||
SetFixedSize(art, 36f, 50f);
|
SetFixedSize(art, 36f, 50f);
|
||||||
var artImage = art.GetComponent<Image>();
|
var artImage = art.GetComponent<Image>();
|
||||||
artImage.sprite = CardArt.ForCard(
|
CardImageService.Apply(
|
||||||
meta != null ? meta.NameId : printingId,
|
artImage,
|
||||||
meta != null ? meta.Clan : "None");
|
meta != null ? meta.ImageKey : null,
|
||||||
artImage.color = Color.white;
|
CardArt.ForCard(
|
||||||
artImage.preserveAspect = false;
|
meta != null ? meta.NameId : printingId,
|
||||||
|
meta != null ? meta.Clan : "None"));
|
||||||
|
|
||||||
var artText = UiKit.CreateText(
|
var artText = UiKit.CreateText(
|
||||||
art, "ArtText", meta != null ? "G" + meta.Grade : "?", 13,
|
art, "ArtText", meta != null ? "G" + meta.Grade : "?", 13,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ http://localhost:5080
|
||||||
действия, журнал событий и подсказки.
|
действия, журнал событий и подсказки.
|
||||||
- Карты в бою и коллекции: процедурные арты по клану, рамка, имя, ранг,
|
- Карты в бою и коллекции: процедурные арты по клану, рамка, имя, ранг,
|
||||||
триггер, сила/крит, REST и рубашка для скрытых карт.
|
триггер, сила/крит, REST и рубашка для скрытых карт.
|
||||||
|
- Реальные арты: `CardImageService` ищет спрайт `Resources/CardArt/{imageKey}`;
|
||||||
|
`imageKey` приходит из `/api/cards`. Пока файлов нет — используется
|
||||||
|
процедурный fallback.
|
||||||
- Экран входа и главный экран с балансами, MMR и переходами в разделы.
|
- Экран входа и главный экран с балансами, MMR и переходами в разделы.
|
||||||
|
|
||||||
## Что дальше
|
## Что дальше
|
||||||
|
|
|
||||||
|
|
@ -749,6 +749,11 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
рубашку; `BattleView` показывает рамки, имена, ранги, триггеры,
|
рубашку; `BattleView` показывает рамки, имена, ранги, триггеры,
|
||||||
силу/крит, REST и скрытые карты; поля обоих игроков, рука и панель
|
силу/крит, REST и скрытые карты; поля обоих игроков, рука и панель
|
||||||
действий. Коллекция использует те же арты.
|
действий. Коллекция использует те же арты.
|
||||||
|
- Реальные арты: `imageKey` в `/api/cards`, `CardImageService` читает
|
||||||
|
`Resources/CardArt/{imageKey}`; без файлов работает процедурный fallback.
|
||||||
|
- Boost UI: клик по своему `SUP`-слоту выбирает поддержку, затем цель.
|
||||||
|
- Event-фидбек: всплывающие атака/guard/drive/урон/heal/retire, не
|
||||||
|
блокирует ввод.
|
||||||
- Боевой UX: клик по карте/зоне, подсветка целей, отдельная группировка
|
- Боевой UX: клик по карте/зоне, подсветка целей, отдельная группировка
|
||||||
действий, журнал последних событий и подсказки выбора.
|
действий, журнал последних событий и подсказки выбора.
|
||||||
- Поле: `R V R` / `R R R` у каждого игрока, зона урона слева, колода и
|
- Поле: `R V R` / `R R R` у каждого игрока, зона урона слева, колода и
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,17 @@
|
||||||
- Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона
|
- Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона
|
||||||
(+soul), справа колода и ниже дроп; у противника зеркально; между
|
(+soul), справа колода и ниже дроп; у противника зеркально; между
|
||||||
полями — зона защиты со щитом и проверками.
|
полями — зона защиты со щитом и проверками.
|
||||||
|
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
||||||
|
стола, не блокирует ввод.
|
||||||
|
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
|
||||||
|
`Resources/CardArt/{imageKey}`; без файлов — процедурный fallback.
|
||||||
- Взаимодействие в бою: клик по картам/зонам, подсветка целей,
|
- Взаимодействие в бою: клик по картам/зонам, подсветка целей,
|
||||||
группировка действий (карты → цели, глобальные кнопки отдельно),
|
группировка действий (карты → цели, глобальные кнопки отдельно),
|
||||||
журнал последних событий.
|
журнал последних событий.
|
||||||
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
||||||
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
||||||
- Не готово: реальные арты/модели/анимации, локализация RU/EN, настройки,
|
- Не готово: файлы реальных артов (пайплайн готов), 3D-модели,
|
||||||
DOTween.
|
полноценная очередь анимаций, локализация RU/EN, настройки, DOTween.
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,7 @@ GET /api/cards [Bearer]
|
||||||
"hasIntercept": true,
|
"hasIntercept": true,
|
||||||
"isParallel": false,
|
"isParallel": false,
|
||||||
"basePrintingId": null,
|
"basePrintingId": null,
|
||||||
|
"imageKey": "bt01_034",
|
||||||
"textEn": "[AUTO](VC/RC):…",
|
"textEn": "[AUTO](VC/RC):…",
|
||||||
"textRu": null
|
"textRu": null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ public sealed record CardCatalogItemDto(
|
||||||
bool HasIntercept,
|
bool HasIntercept,
|
||||||
bool IsParallel,
|
bool IsParallel,
|
||||||
string? BasePrintingId,
|
string? BasePrintingId,
|
||||||
|
string? ImageKey,
|
||||||
string? TextEn,
|
string? TextEn,
|
||||||
string? TextRu);
|
string? TextRu);
|
||||||
|
|
||||||
|
|
@ -111,6 +112,7 @@ public static class MetaEndpoints
|
||||||
card.HasIntercept,
|
card.HasIntercept,
|
||||||
printing.IsParallel,
|
printing.IsParallel,
|
||||||
printing.BasePrintingId,
|
printing.BasePrintingId,
|
||||||
|
printing.ImageKey,
|
||||||
card.TextEn,
|
card.TextEn,
|
||||||
card.TextRu);
|
card.TextRu);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue