Animate card movement and add RU/EN localization
CI / test (push) Successful in 2m6s
Details
CI / test (push) Successful in 2m6s
Details
This commit is contained in:
parent
b81f5e3dd0
commit
0e2907803a
|
|
@ -257,8 +257,8 @@
|
|||
- [x] История матчей и реплей: список результатов, события и кадры.
|
||||
- [x] Тренировка с ботом: серверный casual-матч, случайный легальный бот,
|
||||
без истории/MMR/наград.
|
||||
- [ ] Локализация RU/EN, переключение на лету (пакет подключён).
|
||||
- [ ] Настройки: язык, звук, FPS, мьют.
|
||||
- [x] Локализация RU/EN, переключение на лету; выбор сохраняется.
|
||||
- [ ] Настройки: звук, FPS, мьют (язык уже есть).
|
||||
|
||||
**Готово, когда:** все мета-экраны работают против реального сервера, язык переключается без перезапуска.
|
||||
|
||||
|
|
@ -266,17 +266,17 @@
|
|||
|
||||
## 8. Клиент: боевая сцена `[20–30 дней]`
|
||||
|
||||
**Визуальный каркас есть:** доска, рука, карточные рамки/арт-заглушки
|
||||
по клану, сила/крит, часы, все легальные интенты кнопками, сдача и
|
||||
результат. Реальные арты подключаются через `imageKey`, event-фидбек уже
|
||||
есть; полноценная очередь анимаций — следующая. Финальный визуал —
|
||||
отдельная 3D-сцена `Battle`, а не текущий Canvas
|
||||
**Визуальный каркас есть:** доска, рука, реальные арты (124/124) с
|
||||
fallback по клану, сила/крит, часы, все легальные интенты кнопками, сдача
|
||||
и результат. Event-фидбек и перемещения карт работают; отдельные
|
||||
drive/damage-анимации и звук — следующие. Финальный визуал — отдельная
|
||||
3D-сцена `Battle`, а не текущий Canvas
|
||||
(`docs/client-visual-direction.md`).
|
||||
|
||||
- [x] Поле: `R V R` / `R R R` у каждого игрока, damage/soul слева,
|
||||
колода/дроп справа, зеркально у противника, зона защиты между полями.
|
||||
- [x] Процедурные карты: цвет/градиент клана, рубашка, имя, ранг, триггер,
|
||||
сила/крит; REST/скрытая карта.
|
||||
- [x] Карты: реальный арт из `imageKey`, рамка клана, рубашка, имя, ранг,
|
||||
триггер, сила/крит, REST/скрытая карта; процедурный fallback.
|
||||
- [x] Старт: маллиган (сервер пока даёт только «закончить маллиган»).
|
||||
- [x] Ввод: кнопки легальных интентов; клик по картам/зонам,
|
||||
подсветка целей, группировка действий, выбор силы/эффекта триггера
|
||||
|
|
@ -285,7 +285,8 @@
|
|||
- [x] Event-фидбек: атака, guard, drive, урон, heal, retire, perfect guard.
|
||||
- [x] Отображение силы/крита из `MatchView`; модификаторы уже посчитаны сервером.
|
||||
- [x] Очередь event-анимаций и кнопка «Пропустить анимации».
|
||||
- [ ] Полноценные move/attack/drive/damage-анимации карт и стола.
|
||||
- [x] Перемещения карт call/ride и уход в дроп, пульс атаки.
|
||||
- [ ] Отдельные drive/damage-анимации карт и стола, звук.
|
||||
- [x] Таймер хода, индикатор реконнекта, кнопка сдачи (базово).
|
||||
- [x] Экран результата: победа/поражение; Gold/XP и «ещё матч» — дальше.
|
||||
- [x] Догон состояния по `resync`/snapshot после реконнекта (без переигрывания анимаций).
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -21,7 +21,7 @@
|
|||
| CI (Forgejo Actions) | готово: `ci.yml` проходит на `home-runner` |
|
||||
| EF-миграции | готово (`InitialCreate`) |
|
||||
| Orleans | готово: silo + `MatchActorGrain`, включается `Orleans:Enabled` |
|
||||
| Клиент | Unity 6.6: 3D-сцены, логин, онбординг, коллекция, декбилдер, магазин, история/реплей, очередь, тренировка с ботом, бой с процедурными картами |
|
||||
| Клиент | Unity 6.6: 3D-сцены, логин, онбординг, коллекция, декбилдер, магазин, история/реплей, очередь, тренировка с ботом, бой с реальными артами, анимациями и RU/EN |
|
||||
| Лаунчер | не начато |
|
||||
|
||||
## Требования
|
||||
|
|
@ -143,6 +143,14 @@ dotnet run --project server/src/Vanguard.Cli -- translations check
|
|||
токены, которых нет в справочнике. Карты без эффекта считаются готовыми
|
||||
сразу — им переводить нечего.
|
||||
|
||||
## Язык клиента
|
||||
|
||||
Интерфейс переключается между **RU** и **EN** кнопкой в Login и Lobby;
|
||||
выбор сохраняется в `PlayerPrefs`. Переводы UI живут в
|
||||
`client/Assets/Vanguard/Runtime/UI/Localization.cs`, а имена и тексты карт
|
||||
берутся из пары `nameRu/nameEn` и `textRu/textEn` каталога `/api/cards`.
|
||||
Переключение применяется на лету, без перезагрузки сцены.
|
||||
|
||||
## Документация
|
||||
|
||||
- `plan.md` — исходный дизайн-документ.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Vanguard.Api;
|
|||
using Vanguard.Auth;
|
||||
using Vanguard.Meta;
|
||||
using Vanguard.State;
|
||||
using Vanguard.UI;
|
||||
|
||||
namespace Vanguard.App
|
||||
{
|
||||
|
|
@ -29,6 +30,7 @@ namespace Vanguard.App
|
|||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
Application.targetFrameRate = 60;
|
||||
Loc.Initialize();
|
||||
|
||||
var config = new AppConfig();
|
||||
config.Load();
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ namespace Vanguard.UI
|
|||
private CancellationTokenSource _cts;
|
||||
private MatchConnectionInfo _info;
|
||||
private MatchView _view;
|
||||
private MatchView _previousView;
|
||||
private bool _connecting;
|
||||
private bool _sending;
|
||||
private bool _snapshotPending;
|
||||
|
|
@ -73,6 +74,15 @@ namespace Vanguard.UI
|
|||
private readonly Dictionary<string, RectTransform> _cardPanels =
|
||||
new Dictionary<string, RectTransform>(StringComparer.Ordinal);
|
||||
private readonly List<CardAnimation> _cardAnimations = new List<CardAnimation>();
|
||||
private readonly List<CardEntry> _pendingEntries = new List<CardEntry>();
|
||||
private readonly List<GhostAnimation> _ghostAnimations = new List<GhostAnimation>();
|
||||
private readonly Dictionary<string, string> _cardPrintings =
|
||||
new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, bool> _cardFaceUp =
|
||||
new Dictionary<string, bool>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, CardAnchor> _cardAnchors =
|
||||
new Dictionary<string, CardAnchor>(StringComparer.Ordinal);
|
||||
private RectTransform _fxLayer;
|
||||
|
||||
public BattleView(Transform parent, MetaService meta, Action onExit)
|
||||
{
|
||||
|
|
@ -95,6 +105,7 @@ namespace Vanguard.UI
|
|||
|
||||
_topInfo = UiKit.CreateText(
|
||||
top, "Match", string.Empty, 17, Muted, TextAnchor.MiddleLeft);
|
||||
UiKit.MakeLiteral(_topInfo);
|
||||
UiKit.SetPreferredHeight(_topInfo, 38f);
|
||||
|
||||
_clock = UiKit.CreateText(
|
||||
|
|
@ -158,6 +169,9 @@ namespace Vanguard.UI
|
|||
_result = BuildResultPanel(_root);
|
||||
_result.gameObject.SetActive(false);
|
||||
|
||||
_fxLayer = UiKit.CreatePanel(_root, "BattleFx", Color.clear);
|
||||
_fxLayer.GetComponent<Image>().raycastTarget = false;
|
||||
|
||||
_flash = UiKit.CreatePanel(_root, "BattleFlash", Color.clear).GetComponent<Image>();
|
||||
_flash.raycastTarget = false;
|
||||
_flash.gameObject.SetActive(false);
|
||||
|
|
@ -182,6 +196,7 @@ namespace Vanguard.UI
|
|||
{
|
||||
_info = info;
|
||||
_view = null;
|
||||
_previousView = null;
|
||||
_finished = false;
|
||||
_confirmConcede = false;
|
||||
_concedeLabel.text = "Сдаться";
|
||||
|
|
@ -201,6 +216,8 @@ namespace Vanguard.UI
|
|||
_feedback.gameObject.SetActive(false);
|
||||
_flash.gameObject.SetActive(false);
|
||||
_skipFeedback.gameObject.SetActive(false);
|
||||
ClearGhosts();
|
||||
_cardAnchors.Clear();
|
||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||
SetStatus("Подключаемся к матчу…", Muted);
|
||||
_root.gameObject.SetActive(true);
|
||||
|
|
@ -315,21 +332,185 @@ namespace Vanguard.UI
|
|||
var scale = Mathf.Lerp(animation.StartScale, 1f, eased);
|
||||
animation.Target.localScale = new Vector3(scale, scale, 1f);
|
||||
|
||||
if (animation.Move)
|
||||
{
|
||||
animation.Target.anchoredPosition = Vector2.Lerp(
|
||||
animation.StartPosition,
|
||||
animation.Destination,
|
||||
eased);
|
||||
}
|
||||
|
||||
if (t >= 1f)
|
||||
{
|
||||
animation.Target.localScale = Vector3.one;
|
||||
if (animation.Move)
|
||||
animation.Target.anchoredPosition = animation.Destination;
|
||||
_cardAnimations.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateGhostAnimations();
|
||||
}
|
||||
|
||||
private void AnimateCardIn(RectTransform target)
|
||||
private void UpdateGhostAnimations()
|
||||
{
|
||||
for (var i = _ghostAnimations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var ghost = _ghostAnimations[i];
|
||||
if (ghost.Target == null)
|
||||
{
|
||||
_ghostAnimations.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
ghost.Time += Time.unscaledDeltaTime;
|
||||
var t = Mathf.Clamp01(ghost.Time / ghost.Duration);
|
||||
var eased = 1f - Mathf.Pow(1f - t, 2f);
|
||||
ghost.Target.position = Vector3.Lerp(ghost.Start, ghost.End, eased);
|
||||
ghost.Target.localScale = Vector3.one * Mathf.Lerp(1f, 0.82f, eased);
|
||||
if (ghost.Group != null)
|
||||
ghost.Group.alpha = 1f - eased;
|
||||
|
||||
if (t < 1f)
|
||||
continue;
|
||||
|
||||
if (ghost.Target != null)
|
||||
UnityEngine.Object.Destroy(ghost.Target.gameObject);
|
||||
_ghostAnimations.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearGhosts()
|
||||
{
|
||||
foreach (var ghost in _ghostAnimations)
|
||||
{
|
||||
if (ghost.Target != null)
|
||||
UnityEngine.Object.Destroy(ghost.Target.gameObject);
|
||||
}
|
||||
|
||||
_ghostAnimations.Clear();
|
||||
}
|
||||
|
||||
private void CaptureCardAnchors()
|
||||
{
|
||||
foreach (var pair in _cardPrintings)
|
||||
{
|
||||
if (!_cardPanels.TryGetValue(pair.Key, out var panel) || panel == null)
|
||||
continue;
|
||||
|
||||
_cardFaceUp.TryGetValue(pair.Key, out var faceUp);
|
||||
_cardAnchors[pair.Key] = new CardAnchor(
|
||||
panel.position,
|
||||
panel.rect.size,
|
||||
pair.Value,
|
||||
faceUp);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnRetireGhosts(Dictionary<string, CardAnchor> oldAnchors)
|
||||
{
|
||||
if (oldAnchors.Count == 0 || _fxLayer == null)
|
||||
return;
|
||||
|
||||
foreach (var pair in oldAnchors)
|
||||
{
|
||||
if (_cardAnchors.ContainsKey(pair.Key))
|
||||
continue;
|
||||
|
||||
// Если карта просто переехала в другой слот, не считаем это уходом в дроп.
|
||||
if (_cardAnchors.Values.Any(item => item.PrintingId == pair.Value.PrintingId))
|
||||
continue;
|
||||
|
||||
CreateRetireGhost(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateRetireGhost(string key, CardAnchor anchor)
|
||||
{
|
||||
var panel = UiKit.CreatePanel(_fxLayer, "Retired_" + key, Color.white);
|
||||
panel.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
panel.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
panel.pivot = new Vector2(0.5f, 0.5f);
|
||||
panel.sizeDelta = anchor.Size;
|
||||
panel.position = anchor.World;
|
||||
|
||||
var image = panel.GetComponent<Image>();
|
||||
image.raycastTarget = false;
|
||||
var meta = _catalog.TryGetValue(anchor.PrintingId, out var found) ? found : null;
|
||||
if (anchor.FaceUp)
|
||||
{
|
||||
CardImageService.Apply(
|
||||
image,
|
||||
meta != null ? meta.ImageKey : null,
|
||||
CardArt.ForCard(
|
||||
meta != null ? meta.NameId : anchor.PrintingId,
|
||||
meta != null ? meta.Clan : "None"));
|
||||
}
|
||||
else
|
||||
{
|
||||
image.sprite = CardArt.Back;
|
||||
image.color = Color.white;
|
||||
}
|
||||
|
||||
var outline = panel.gameObject.AddComponent<Outline>();
|
||||
outline.effectColor = new Color(1f, 0.35f, 0.28f, 0.9f);
|
||||
outline.effectDistance = new Vector2(2f, -2f);
|
||||
|
||||
var group = panel.gameObject.AddComponent<CanvasGroup>();
|
||||
group.blocksRaycasts = false;
|
||||
group.interactable = false;
|
||||
|
||||
var player = 0;
|
||||
var split = key.IndexOf(':');
|
||||
if (split > 0)
|
||||
int.TryParse(key.Substring(0, split), out player);
|
||||
|
||||
var direction = _view != null && player == _view.You ? -1f : 1f;
|
||||
_ghostAnimations.Add(new GhostAnimation(
|
||||
panel,
|
||||
group,
|
||||
anchor.World,
|
||||
anchor.World + new Vector3(0f, direction * 190f, 0f),
|
||||
0.36f));
|
||||
}
|
||||
|
||||
private void FinalizeCardEntries()
|
||||
{
|
||||
// Сначала даём layout-группам посчитать конечные позиции карт.
|
||||
Canvas.ForceUpdateCanvases();
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(_board);
|
||||
CaptureCardAnchors();
|
||||
|
||||
if (_pendingEntries.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var entry in _pendingEntries)
|
||||
{
|
||||
if (entry.Target == null)
|
||||
continue;
|
||||
|
||||
var destination = entry.Target.anchoredPosition;
|
||||
var start = destination + entry.Offset;
|
||||
entry.Target.anchoredPosition = start;
|
||||
_cardAnimations.Add(new CardAnimation(
|
||||
entry.Target,
|
||||
0.24f,
|
||||
entry.StartScale,
|
||||
start,
|
||||
destination,
|
||||
move: true));
|
||||
}
|
||||
|
||||
_pendingEntries.Clear();
|
||||
}
|
||||
|
||||
private void AnimateCardIn(RectTransform target, Vector2 offset)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
target.localScale = Vector3.one * 0.86f;
|
||||
_cardAnimations.Add(new CardAnimation(target, 0.18f, 0.86f));
|
||||
_pendingEntries.Add(new CardEntry(target, offset, 0.86f));
|
||||
}
|
||||
|
||||
private void PulseCard(int player, int slot, float startScale)
|
||||
|
|
@ -337,30 +518,115 @@ namespace Vanguard.UI
|
|||
if (!_cardPanels.TryGetValue(CardKey(player, slot), out var target) || target == null)
|
||||
return;
|
||||
|
||||
for (var i = _cardAnimations.Count - 1; i >= 0; i--)
|
||||
target.localScale = Vector3.one * startScale;
|
||||
_cardAnimations.Add(new CardAnimation(
|
||||
target, 0.28f, startScale, target.anchoredPosition, target.anchoredPosition, move: false));
|
||||
}
|
||||
|
||||
private bool ShouldAnimateCard(int player, int slot, string printingId)
|
||||
{
|
||||
if (_previousView == null)
|
||||
return true;
|
||||
|
||||
var side = _previousView.Players.FirstOrDefault(item => item.Index == player);
|
||||
if (side == null)
|
||||
return true;
|
||||
|
||||
if (slot == -2)
|
||||
{
|
||||
if (_cardAnimations[i].Target == target)
|
||||
_cardAnimations.RemoveAt(i);
|
||||
return side.Hand == null
|
||||
|| side.Hand.All(item => item.PrintingId != printingId);
|
||||
}
|
||||
|
||||
target.localScale = Vector3.one * startScale;
|
||||
_cardAnimations.Add(new CardAnimation(target, 0.28f, startScale));
|
||||
if (slot == -1)
|
||||
return side.Vanguard == null || side.Vanguard.PrintingId != printingId;
|
||||
|
||||
return side.RearGuards == null
|
||||
|| slot >= side.RearGuards.Length
|
||||
|| side.RearGuards[slot] == null
|
||||
|| side.RearGuards[slot].PrintingId != printingId;
|
||||
}
|
||||
|
||||
private static string CardKey(int player, int slot) => player + ":" + slot;
|
||||
|
||||
private readonly struct CardAnchor
|
||||
{
|
||||
public CardAnchor(Vector3 world, Vector2 size, string printingId, bool faceUp)
|
||||
{
|
||||
World = world;
|
||||
Size = size;
|
||||
PrintingId = printingId;
|
||||
FaceUp = faceUp;
|
||||
}
|
||||
|
||||
public Vector3 World { get; }
|
||||
public Vector2 Size { get; }
|
||||
public string PrintingId { get; }
|
||||
public bool FaceUp { get; }
|
||||
}
|
||||
|
||||
private sealed class GhostAnimation
|
||||
{
|
||||
public GhostAnimation(
|
||||
RectTransform target,
|
||||
CanvasGroup group,
|
||||
Vector3 start,
|
||||
Vector3 end,
|
||||
float duration)
|
||||
{
|
||||
Target = target;
|
||||
Group = group;
|
||||
Start = start;
|
||||
End = end;
|
||||
Duration = duration;
|
||||
}
|
||||
|
||||
public RectTransform Target { get; }
|
||||
public CanvasGroup Group { get; }
|
||||
public Vector3 Start { get; }
|
||||
public Vector3 End { get; }
|
||||
public float Duration { get; }
|
||||
public float Time { get; set; }
|
||||
}
|
||||
|
||||
private sealed class CardEntry
|
||||
{
|
||||
public CardEntry(RectTransform target, Vector2 offset, float startScale)
|
||||
{
|
||||
Target = target;
|
||||
Offset = offset;
|
||||
StartScale = startScale;
|
||||
}
|
||||
|
||||
public RectTransform Target { get; }
|
||||
public Vector2 Offset { get; }
|
||||
public float StartScale { get; }
|
||||
}
|
||||
|
||||
private sealed class CardAnimation
|
||||
{
|
||||
public CardAnimation(RectTransform target, float duration, float startScale)
|
||||
public CardAnimation(
|
||||
RectTransform target,
|
||||
float duration,
|
||||
float startScale,
|
||||
Vector2 startPosition,
|
||||
Vector2 destination,
|
||||
bool move)
|
||||
{
|
||||
Target = target;
|
||||
Duration = duration;
|
||||
StartScale = startScale;
|
||||
StartPosition = startPosition;
|
||||
Destination = destination;
|
||||
Move = move;
|
||||
}
|
||||
|
||||
public RectTransform Target { get; }
|
||||
public float Duration { get; }
|
||||
public float StartScale { get; }
|
||||
public Vector2 StartPosition { get; }
|
||||
public Vector2 Destination { get; }
|
||||
public bool Move { get; }
|
||||
public float Time { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -500,6 +766,7 @@ namespace Vanguard.UI
|
|||
if (!_root.gameObject.activeSelf || snapshot?.View == null)
|
||||
return;
|
||||
|
||||
_previousView = _view;
|
||||
_view = snapshot.View;
|
||||
_finished = _view.IsFinished;
|
||||
_pendingIntents.Clear();
|
||||
|
|
@ -792,9 +1059,14 @@ namespace Vanguard.UI
|
|||
if (_view == null)
|
||||
return;
|
||||
|
||||
var oldAnchors = new Dictionary<string, CardAnchor>(_cardAnchors, StringComparer.Ordinal);
|
||||
ClearChildren(_board);
|
||||
_cardPanels.Clear();
|
||||
_cardPrintings.Clear();
|
||||
_cardFaceUp.Clear();
|
||||
_cardAnchors.Clear();
|
||||
_cardAnimations.Clear();
|
||||
_pendingEntries.Clear();
|
||||
var opponentIndex = _view.You == 0 ? 1 : 0;
|
||||
var opponent = _view.Players.FirstOrDefault(player => player.Index == opponentIndex)
|
||||
?? (_view.Players.Count > opponentIndex ? _view.Players[opponentIndex] : null);
|
||||
|
|
@ -805,6 +1077,8 @@ namespace Vanguard.UI
|
|||
AddGuardZone(_view);
|
||||
AddSideField(self, true);
|
||||
AddHand(self);
|
||||
FinalizeCardEntries();
|
||||
SpawnRetireGhosts(oldAnchors);
|
||||
|
||||
if (_view.Battle != null)
|
||||
{
|
||||
|
|
@ -1073,9 +1347,23 @@ namespace Vanguard.UI
|
|||
? new Vector2(3f, -3f)
|
||||
: new Vector2(2f, -2f);
|
||||
|
||||
_cardPanels[CardKey(playerIndex, slotIndex)] = panel;
|
||||
if (card != null)
|
||||
AnimateCardIn(panel);
|
||||
var key = CardKey(playerIndex, slotIndex);
|
||||
_cardPanels[key] = panel;
|
||||
if (card != null && slotIndex >= -1)
|
||||
{
|
||||
_cardPrintings[key] = card.PrintingId;
|
||||
_cardFaceUp[key] = card.FaceUp;
|
||||
}
|
||||
|
||||
if (card != null && ShouldAnimateCard(playerIndex, slotIndex, card.PrintingId))
|
||||
{
|
||||
var offset = slotIndex == -2
|
||||
? new Vector2(0f, -120f)
|
||||
: playerIndex == _view.You
|
||||
? new Vector2(0f, -250f)
|
||||
: new Vector2(0f, 250f);
|
||||
AnimateCardIn(panel, offset);
|
||||
}
|
||||
UiKit.AddVerticalLayout(panel, 4, 2);
|
||||
|
||||
var slotText = UiKit.CreateText(
|
||||
|
|
@ -1119,8 +1407,9 @@ namespace Vanguard.UI
|
|||
if (card.FaceUp)
|
||||
{
|
||||
var artName = UiKit.CreateText(
|
||||
art, "ArtName", meta != null ? meta.NameEn : card.PrintingId, 10,
|
||||
art, "ArtName", meta != null ? Loc.CardName(meta) : card.PrintingId, 10,
|
||||
new Color(1f, 1f, 1f, 0.94f), TextAnchor.MiddleCenter);
|
||||
UiKit.MakeLiteral(artName);
|
||||
UiKit.Stretch(artName.rectTransform);
|
||||
artName.rectTransform.offsetMin = new Vector2(3f, 3f);
|
||||
artName.rectTransform.offsetMax = new Vector2(-3f, -2f);
|
||||
|
|
@ -1161,10 +1450,11 @@ namespace Vanguard.UI
|
|||
|
||||
var name = UiKit.CreateText(
|
||||
panel, "Name",
|
||||
meta != null ? meta.NameEn : card.PrintingId,
|
||||
meta != null ? Loc.CardName(meta) : card.PrintingId,
|
||||
10,
|
||||
card.FaceUp ? Color.white : Muted,
|
||||
TextAnchor.UpperLeft);
|
||||
UiKit.MakeLiteral(name);
|
||||
name.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
name.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(name, compact ? 22f : 24f);
|
||||
|
|
@ -1193,7 +1483,7 @@ namespace Vanguard.UI
|
|||
};
|
||||
|
||||
private string CardName(string printingId) =>
|
||||
_catalog.TryGetValue(printingId, out var meta) ? meta.NameEn : printingId;
|
||||
_catalog.TryGetValue(printingId, out var meta) ? Loc.CardName(meta) : printingId;
|
||||
|
||||
private void OnBoardTargetClicked(BoardTarget target)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -399,6 +399,7 @@ namespace Vanguard.UI
|
|||
_cardArtName = UiKit.CreateText(
|
||||
art, "ArtName", string.Empty, 18,
|
||||
new Color(1f, 1f, 1f, 0.92f), TextAnchor.MiddleCenter);
|
||||
UiKit.MakeLiteral(_cardArtName);
|
||||
UiKit.Stretch(_cardArtName.rectTransform);
|
||||
_cardArtName.rectTransform.offsetMin = new Vector2(14f, 16f);
|
||||
_cardArtName.rectTransform.offsetMax = new Vector2(-14f, -16f);
|
||||
|
|
@ -412,6 +413,7 @@ namespace Vanguard.UI
|
|||
|
||||
_cardTitle = UiKit.CreateText(
|
||||
info, "Title", string.Empty, 30, Color.white, TextAnchor.MiddleLeft);
|
||||
UiKit.MakeLiteral(_cardTitle);
|
||||
_cardTitle.fontStyle = FontStyle.Bold;
|
||||
_cardTitle.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UiKit.SetPreferredHeight(_cardTitle, 44f);
|
||||
|
|
@ -419,6 +421,7 @@ namespace Vanguard.UI
|
|||
_cardSubtitle = UiKit.CreateText(
|
||||
info, "Subtitle", string.Empty, 16,
|
||||
new Color(0.62f, 0.70f, 0.84f), TextAnchor.MiddleLeft);
|
||||
UiKit.MakeLiteral(_cardSubtitle);
|
||||
UiKit.SetPreferredHeight(_cardSubtitle, 26f);
|
||||
|
||||
_cardStats = UiKit.CreateText(
|
||||
|
|
@ -435,6 +438,7 @@ namespace Vanguard.UI
|
|||
_cardAbilities = UiKit.CreateText(
|
||||
info, "Abilities", string.Empty, 15,
|
||||
new Color(0.82f, 0.86f, 0.96f), TextAnchor.UpperLeft);
|
||||
UiKit.MakeLiteral(_cardAbilities);
|
||||
_cardAbilities.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
var abilitiesElement = _cardAbilities.gameObject.AddComponent<LayoutElement>();
|
||||
abilitiesElement.flexibleHeight = 1f;
|
||||
|
|
@ -508,9 +512,10 @@ namespace Vanguard.UI
|
|||
_cardArt,
|
||||
meta.ImageKey,
|
||||
CardArt.ForCard(meta.NameId, meta.Clan));
|
||||
_cardArtName.text = meta.NameEn;
|
||||
_cardTitle.text = meta.NameEn;
|
||||
_cardSubtitle.text = (string.IsNullOrEmpty(meta.NameRu) ? string.Empty : meta.NameRu + " · ")
|
||||
var otherName = Loc.OtherCardName(meta);
|
||||
_cardArtName.text = Loc.CardName(meta);
|
||||
_cardTitle.text = Loc.CardName(meta);
|
||||
_cardSubtitle.text = (string.IsNullOrEmpty(otherName) ? string.Empty : otherName + " · ")
|
||||
+ meta.Clan + " · G" + meta.Grade;
|
||||
_cardStats.text = "Тип: " + meta.Type + " · Редкость: " + meta.Rarity + "\n"
|
||||
+ "Power " + meta.Power + " · Shield " + meta.Shield + " · Critical " + meta.Critical + "\n"
|
||||
|
|
@ -519,9 +524,10 @@ namespace Vanguard.UI
|
|||
+ " · Boost: " + (meta.HasBoost ? "да" : "нет")
|
||||
+ " · Intercept: " + (meta.HasIntercept ? "да" : "нет") + "\n"
|
||||
+ "Принт: " + meta.PrintingId;
|
||||
_cardAbilities.text = string.IsNullOrWhiteSpace(meta.TextRu)
|
||||
? (string.IsNullOrWhiteSpace(meta.TextEn) ? "Текст способности отсутствует." : meta.TextEn)
|
||||
: meta.TextRu;
|
||||
var abilityText = Loc.CardText(meta);
|
||||
_cardAbilities.text = string.IsNullOrWhiteSpace(abilityText)
|
||||
? Loc.Translate("Текст способности отсутствует.")
|
||||
: abilityText;
|
||||
_cardOwned.text = "В коллекции: ×" + (_selectedItem != null ? _selectedItem.Count : 0)
|
||||
+ " · в колоде: " + UsedByName(meta.NameId)
|
||||
+ " · пыль: " + _dust;
|
||||
|
|
@ -847,7 +853,7 @@ namespace Vanguard.UI
|
|||
.Where(pair => pair.Meta != null && MatchesFilter(pair.Meta))
|
||||
.OrderBy(pair => pair.Meta.Clan, StringComparer.Ordinal)
|
||||
.ThenBy(pair => pair.Meta.Grade)
|
||||
.ThenBy(pair => pair.Meta.NameEn, StringComparer.Ordinal);
|
||||
.ThenBy(pair => Loc.CardName(pair.Meta), StringComparer.Ordinal);
|
||||
|
||||
foreach (var pair in items)
|
||||
AddCollectionCell(content, pair.Item, pair.Meta, countTexts, showDeckCount, onAdd);
|
||||
|
|
@ -886,8 +892,9 @@ namespace Vanguard.UI
|
|||
CardArt.ForCard(meta.NameId, meta.Clan));
|
||||
|
||||
var artName = UiKit.CreateText(
|
||||
art, "ArtName", meta.NameEn, 12,
|
||||
art, "ArtName", Loc.CardName(meta), 12,
|
||||
new Color(1f, 1f, 1f, 0.9f), TextAnchor.MiddleCenter);
|
||||
UiKit.MakeLiteral(artName);
|
||||
UiKit.Stretch(artName.rectTransform);
|
||||
artName.rectTransform.offsetMin = new Vector2(6f, 8f);
|
||||
artName.rectTransform.offsetMax = new Vector2(-6f, -6f);
|
||||
|
|
@ -913,7 +920,8 @@ namespace Vanguard.UI
|
|||
triggerRect.sizeDelta = new Vector2(34f, 20f);
|
||||
|
||||
var name = UiKit.CreateText(
|
||||
cell, "Name", meta.NameEn, 13, Color.white, TextAnchor.UpperLeft);
|
||||
cell, "Name", Loc.CardName(meta), 13, Color.white, TextAnchor.UpperLeft);
|
||||
UiKit.MakeLiteral(name);
|
||||
name.fontStyle = FontStyle.Bold;
|
||||
name.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UiKit.SetPreferredHeight(name, 32f);
|
||||
|
|
@ -948,7 +956,7 @@ namespace Vanguard.UI
|
|||
var printingId = group.Key;
|
||||
var count = group.Count();
|
||||
var meta = _catalog.TryGetValue(printingId, out var found) ? found : null;
|
||||
var name = meta != null ? meta.NameEn : printingId;
|
||||
var name = meta != null ? Loc.CardName(meta) : printingId;
|
||||
|
||||
var row = UiKit.CreatePanel(
|
||||
_deckCardContent, "DeckCard_" + printingId, new Color(0.08f, 0.11f, 0.17f, 1f));
|
||||
|
|
@ -986,6 +994,7 @@ namespace Vanguard.UI
|
|||
|
||||
var nameText = UiKit.CreateText(
|
||||
texts, "Name", name, 13, Color.white, TextAnchor.MiddleLeft);
|
||||
UiKit.MakeLiteral(nameText);
|
||||
nameText.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
UiKit.SetPreferredHeight(nameText, 30f);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,505 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Vanguard.Api.Contracts;
|
||||
|
||||
namespace Vanguard.UI
|
||||
{
|
||||
public enum UiLanguage
|
||||
{
|
||||
Ru,
|
||||
En,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Небольшой runtime-словарь интерфейса. Русские строки остаются ключами,
|
||||
/// поэтому существующие экраны не нужно переписывать под отдельные id.
|
||||
/// </summary>
|
||||
public static class Loc
|
||||
{
|
||||
private const string PreferenceKey = "vanguard.ui.language";
|
||||
private static bool _initialized;
|
||||
|
||||
public static UiLanguage Language { get; private set; } = UiLanguage.Ru;
|
||||
public static bool IsEnglish => Language == UiLanguage.En;
|
||||
public static event Action LanguageChanged;
|
||||
|
||||
private static readonly Dictionary<string, string> Exact = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["ВЫ"] = "YOU",
|
||||
["Вы"] = "You",
|
||||
["вы"] = "you",
|
||||
["да"] = "yes",
|
||||
["Бой"] = "Battle",
|
||||
["Все"] = "All",
|
||||
["нет"] = "no",
|
||||
["Вы: "] = "You: ",
|
||||
["ДРОП"] = "DROP",
|
||||
["Кадр"] = "Frame",
|
||||
["Клан"] = "Clan",
|
||||
["Пыль"] = "Dust",
|
||||
["Ранг"] = "Grade",
|
||||
["Рука"] = "Hand",
|
||||
["УРОН"] = "DAMAGE",
|
||||
[" карт"] = " cards",
|
||||
["» за "] = "» for ",
|
||||
["В бой"] = "To battle",
|
||||
["ВОЙТИ"] = "LOG IN",
|
||||
["Выйти"] = "Log out",
|
||||
["Кадр "] = "Frame ",
|
||||
["Ничья"] = "Draw",
|
||||
["Пусто"] = "Empty",
|
||||
["Сброс"] = "Reset",
|
||||
["Тип: "] = "Type: ",
|
||||
["Щит: "] = "Shield: ",
|
||||
["цена "] = "cost ",
|
||||
[" карт."] = " cards.",
|
||||
[" пыли)"] = " dust)",
|
||||
["Атака "] = "Attack ",
|
||||
["В меню"] = "To menu",
|
||||
["Готово"] = "Done",
|
||||
["Журнал"] = "Log",
|
||||
["КОЛОДА"] = "DECK",
|
||||
["Кадр ▶"] = "Frame ▶",
|
||||
["Клан: "] = "Clan: ",
|
||||
["Колода"] = "Deck",
|
||||
["Купить"] = "Buy",
|
||||
["Отмена"] = "Cancel",
|
||||
["Пароль"] = "Password",
|
||||
["Победа"] = "Victory",
|
||||
["Ранг: "] = "Grade: ",
|
||||
["Реплей"] = "Replay",
|
||||
["СКРЫТА"] = "HIDDEN",
|
||||
["Сервер"] = "Server",
|
||||
["Токены"] = "Tokens",
|
||||
["Урон: "] = "Damage: ",
|
||||
["Фаза: "] = "Phase: ",
|
||||
["◀ Кадр"] = "◀ Frame",
|
||||
[" · ход "] = " · turn ",
|
||||
["1 токен"] = "1 token",
|
||||
["\nМатч "] = "\nMatch ",
|
||||
["АТАКА "] = "ATTACK ",
|
||||
["Атака: "] = "Attack: ",
|
||||
["Всего: "] = "Total: ",
|
||||
["Готово."] = "Done.",
|
||||
["Забрать"] = "Claim",
|
||||
["Закрыть"] = "Close",
|
||||
["История"] = "History",
|
||||
["Карта: "] = "Card: ",
|
||||
["Магазин"] = "Store",
|
||||
["Победа!"] = "Victory!",
|
||||
["Принт: "] = "Printing: ",
|
||||
["С ботом"] = "Vs bot",
|
||||
["Сдаться"] = "Concede",
|
||||
["События"] = "Events",
|
||||
["Триггер"] = "Trigger",
|
||||
["Удалить"] = "Delete",
|
||||
["Фильтры"] = "Filters",
|
||||
["← Назад"] = "← Back",
|
||||
[" рука "] = " hand ",
|
||||
[" (всего "] = " (total ",
|
||||
[" · пыль "] = " · dust ",
|
||||
[" · рука "] = " · hand ",
|
||||
[" · урон "] = " · damage ",
|
||||
[" · шаг: "] = " · step: ",
|
||||
[" карт · "] = " cards · ",
|
||||
[" против "] = " vs ",
|
||||
["Действия"] = "Actions",
|
||||
["Кадров: "] = "Frames: ",
|
||||
["Колода: "] = "Deck: ",
|
||||
["Крафтим…"] = "Crafting…",
|
||||
["Купить «"] = "Buy «",
|
||||
["Матчей: "] = "Matches: ",
|
||||
["Принято."] = "Accepted.",
|
||||
["Уже есть"] = "Already owned",
|
||||
[" · пыль: "] = " · dust: ",
|
||||
[", эффект "] = ", effect ",
|
||||
["Все карты"] = "All cards",
|
||||
["Выбрать: "] = "Choose: ",
|
||||
["Клан: все"] = "Clan: all",
|
||||
["Коллекция"] = "Collection",
|
||||
["Нет колод"] = "No decks",
|
||||
["ПРОТИВНИК"] = "OPPONENT",
|
||||
["Поражение"] = "Defeat",
|
||||
["Противник"] = "Opponent",
|
||||
["РУКА · "] = "HAND · ",
|
||||
["Разобрать"] = "Dismantle",
|
||||
["Ранг: все"] = "Grade: all",
|
||||
["Реплей · "] = "Replay · ",
|
||||
["Скрафтить"] = "Craft",
|
||||
["Сохранить"] = "Save",
|
||||
["Триггер: "] = "Trigger: ",
|
||||
["противник"] = "opponent",
|
||||
["← В лобби"] = "← To lobby",
|
||||
[" · урон "] = " · damage ",
|
||||
[" · колода "] = " · deck ",
|
||||
[" завершён."] = " finished.",
|
||||
["В фазу боя"] = "To battle phase",
|
||||
["Имя игрока"] = "Player name",
|
||||
["Кадров нет"] = "No frames",
|
||||
["Купить за "] = "Buy for ",
|
||||
["Маллиган: "] = "Mulligan: ",
|
||||
["Мои колоды"] = "My decks",
|
||||
["Нет реплея"] = "No replay",
|
||||
["Ожидание: "] = "Waiting: ",
|
||||
["Поражение."] = "Defeat.",
|
||||
["Проверок: "] = "Checks: ",
|
||||
["Разбираем…"] = "Dismantling…",
|
||||
["Рука пуста"] = "Hand is empty",
|
||||
["Сохраняем…"] = "Saving…",
|
||||
["Триал-деки"] = "Trial decks",
|
||||
[" · решает: "] = " · deciding: ",
|
||||
[" уникальных"] = " unique",
|
||||
["Активируем "] = "Activating ",
|
||||
["Бустер BT01"] = "BT01 Booster",
|
||||
["ЗОНА ЗАЩИТЫ"] = "GUARD ZONE",
|
||||
["Отклонено ("] = "Rejected (",
|
||||
["Поддержка: "] = "Boost: ",
|
||||
["Противник: "] = "Opponent: ",
|
||||
["Сохранено: "] = "Saved: ",
|
||||
["Способности"] = "Abilities",
|
||||
["без причины"] = "without reason",
|
||||
["в коллекции"] = "in collection",
|
||||
["← К истории"] = "← To history",
|
||||
["← Коллекция"] = "← Collection",
|
||||
[" · Пыль "] = " · Dust ",
|
||||
[" · банк "] = " · bank ",
|
||||
[" · колода "] = " · deck ",
|
||||
[" · в колоде "] = " · in deck ",
|
||||
[" · событий: "] = " · events: ",
|
||||
[" карт · "] = " cards · ",
|
||||
["3–32 символа"] = "3–32 characters",
|
||||
["Колода пуста"] = "Deck is empty",
|
||||
["Колода: 0/50"] = "Deck: 0/50",
|
||||
["Новая колода"] = "New deck",
|
||||
["Отправляем: "] = "Sending: ",
|
||||
["Разобрать (+"] = "Dismantle (+",
|
||||
["Скрафтить (−"] = "Craft (−",
|
||||
["Событий нет."] = "No events.",
|
||||
["Триггер: все"] = "Trigger: all",
|
||||
[" · Редкость: "] = " · Rarity: ",
|
||||
[" · в колоде: "] = " · in deck: ",
|
||||
["Атака отбита."] = "Attack was guarded.",
|
||||
["Атака прошла."] = "Attack went through.",
|
||||
["Бустер открыт"] = "Booster opened",
|
||||
["Закончить ход"] = "End turn",
|
||||
["Матч завершён"] = "Match finished",
|
||||
["Подключаемся…"] = "Connecting…",
|
||||
["Способность #"] = "Ability #",
|
||||
[" · Токены "] = " · Tokens ",
|
||||
[" · побед: "] = " · wins: ",
|
||||
["8–128 символов"] = "8–128 characters",
|
||||
["Бустер открыт!"] = "Booster opened!",
|
||||
["В коллекции: ×"] = "In collection: ×",
|
||||
["История матчей"] = "Match history",
|
||||
["Личный кабинет"] = "Profile",
|
||||
["Матч завершён."] = "Match finished.",
|
||||
["Отменить поиск"] = "Cancel search",
|
||||
["Создать колоду"] = "Create deck",
|
||||
["Триггер: сила "] = "Trigger: power ",
|
||||
["Ищем соперника…"] = "Searching for opponent…",
|
||||
["Колода удалена."] = "Deck deleted.",
|
||||
["Название колоды"] = "Deck name",
|
||||
["Повторить поиск"] = "Search again",
|
||||
["Пропустить Ride"] = "Skip Ride",
|
||||
["СОЗДАТЬ АККАУНТ"] = "CREATE ACCOUNT",
|
||||
["Стартовый набор"] = "Starter set",
|
||||
["Удалить колоду?"] = "Delete deck?",
|
||||
["Уже в коллекции"] = "Already in collection",
|
||||
["Бустер открыт · "] = "Booster opened · ",
|
||||
["Готовим очередь…"] = "Preparing queue…",
|
||||
["Карта разобрана."] = "Card dismantled.",
|
||||
["Карта скрафчена."] = "Card crafted.",
|
||||
["Матчей пока нет."] = "No matches yet.",
|
||||
["Нельзя разобрать"] = "Cannot dismantle",
|
||||
["Нельзя скрафтить"] = "Cannot craft",
|
||||
["Переподключиться"] = "Reconnect",
|
||||
["Поиск противника"] = "Opponent search",
|
||||
["Пропустить Guard"] = "Skip Guard",
|
||||
["Создаём аккаунт…"] = "Creating account…",
|
||||
["Выберите покупку."] = "Choose a purchase.",
|
||||
["Выбрать бесплатно"] = "Choose for free",
|
||||
["Выбрать вариант #"] = "Choose option #",
|
||||
["Выбрать триггер #"] = "Choose trigger #",
|
||||
["Выйти из аккаунта"] = "Log out",
|
||||
["Дека уже получена"] = "Deck already claimed",
|
||||
["Загружаем реплей…"] = "Loading replay…",
|
||||
["Купить за 1 токен"] = "Buy for 1 token",
|
||||
["Очередь закрыта: "] = "Queue closed: ",
|
||||
["Партия завершена."] = "Game over.",
|
||||
["Подтвердить сдачу"] = "Confirm concede",
|
||||
["Профиль обновлён."] = "Profile updated.",
|
||||
["Фильтры коллекции"] = "Collection filters",
|
||||
["Выбрать триал-деку"] = "Choose trial deck",
|
||||
["Выполняем покупку…"] = "Processing purchase…",
|
||||
["Загружаем историю…"] = "Loading history…",
|
||||
["Загружаем магазин…"] = "Loading store…",
|
||||
["Закончить маллиган"] = "Finish mulligan",
|
||||
["Карт в коллекции: "] = "Cards in collection: ",
|
||||
["Купить за 100 Gold"] = "Buy for 100 Gold",
|
||||
["Обновляем профиль…"] = "Updating profile…",
|
||||
["Повторить загрузку"] = "Retry loading",
|
||||
["Подтвердите покупку"] = "Confirm purchase",
|
||||
["Покупка не удалась."] = "Purchase failed.",
|
||||
["Пропустить анимации"] = "Skip animations",
|
||||
["Скрыть все действия"] = "Hide all actions",
|
||||
[" с · окно MMR: ±"] = " with · MMR window: ±",
|
||||
["Онбординг завершён: "] = "Onboarding complete: ",
|
||||
["Повтор уже применён."] = "Replay already applied.",
|
||||
["В колоде уже 50 карт."] = "The deck already has 50 cards.",
|
||||
["Загружаем триал-деки…"] = "Loading trial decks…",
|
||||
["Подключаемся к матчу…"] = "Connecting to match…",
|
||||
["Показать все действия"] = "Show all actions",
|
||||
["Получаем новый тикет…"] = "Getting a new ticket…",
|
||||
["Применить способность"] = "Activate ability",
|
||||
["Сервер вернул ошибку "] = "Server returned error ",
|
||||
["Соединение потеряно: "] = "Connection lost: ",
|
||||
[" · защита между полями"] = " · guard between fields",
|
||||
[". Выберите цель атаки."] = ". Choose an attack target.",
|
||||
["Выберите цель на поле."] = "Choose a target on the field.",
|
||||
["Колод пока нет. Пыль: "] = "No decks yet. Dust: ",
|
||||
["Не удалось связаться: "] = "Could not connect: ",
|
||||
["Пропустить способность"] = "Skip ability",
|
||||
["Сервер не выдал тикет."] = "Server did not issue a ticket.",
|
||||
["Триал-деки недоступны."] = "Trial decks are unavailable.",
|
||||
["Ничья или матч прерван."] = "Draw or match interrupted.",
|
||||
["Открыть бустер BT01 за "] = "Open BT01 booster for ",
|
||||
["» добавлена в коллекцию."] = "» added to collection.",
|
||||
["Не удалось создать матч."] = "Could not create a match.",
|
||||
["Сейчас решает противник…"] = "Opponent is deciding…",
|
||||
["Не удалось подключиться: "] = "Could not connect: ",
|
||||
["Ошибка отправки интента: "] = "Failed to send intent: ",
|
||||
["Сначала сохраните колоду."] = "Save the deck first.",
|
||||
["RE:MASTER · СЕТЕВОЙ КЛИЕНТ"] = "RE:MASTER · NETWORK CLIENT",
|
||||
["Матч найден! Подключаемся…"] = "Match found! Connecting…",
|
||||
["Не удалось удалить колоду."] = "Could not delete the deck.",
|
||||
["Готовим тренировочный матч…"] = "Preparing practice match…",
|
||||
["Не удалось разобрать карту."] = "Could not dismantle the card.",
|
||||
["Не удалось скрафтить карту."] = "Could not craft the card.",
|
||||
["Переподключение не удалось."] = "Reconnect failed.",
|
||||
["Состояние кадра недоступно."] = "Frame state unavailable.",
|
||||
["Classic Ranked · скрытый MMR"] = "Classic Ranked · hidden MMR",
|
||||
["Введите имя игрока и пароль."] = "Enter player name and password.",
|
||||
["Не удалось загрузить реплей."] = "Could not load replay.",
|
||||
["Не удалось сохранить колоду."] = "Could not save the deck.",
|
||||
["Триггер: выберите цель силы."] = "Trigger: choose a power target.",
|
||||
[". ПКМ по карте — подробности."] = ". Right-click a card for details.",
|
||||
["Загружаем коллекцию и колоды…"] = "Loading collection and decks…",
|
||||
["Не удалось загрузить историю."] = "Could not load history.",
|
||||
["Не удалось загрузить магазин."] = "Could not load store.",
|
||||
["Состав появится после выбора."] = "The lineup appears after selection.",
|
||||
[" · ПКМ по карте — подробности."] = " · Right-click a card for details.",
|
||||
["Не больше 4 копий одной карты."] = "No more than 4 copies of a card.",
|
||||
["Не удалось обновить состояние."] = "Could not refresh state.",
|
||||
["Текст способности отсутствует."] = "Ability text is missing.",
|
||||
["Не удалось завершить онбординг."] = "Could not complete onboarding.",
|
||||
["Не удалось загрузить декбилдер."] = "Could not load deck builder.",
|
||||
["Не удалось начать матч с ботом."] = "Could not start a match against the bot.",
|
||||
["Триггер: выберите цель эффекта."] = "Trigger: choose an effect target.",
|
||||
[".\nВыберите цель атаки или «Отмена»."] = ".\nChoose an attack target or “Cancel”.",
|
||||
["Нажмите «Подтвердить сдачу» ещё раз."] = "Press “Confirm concede” again.",
|
||||
["Очередь недоступна. Проверьте сервер."] = "Queue unavailable. Check the server.",
|
||||
["Сервер не вернул ни одной триал-деки."] = "Server returned no trial decks.",
|
||||
["Соединение открыто. Ожидаем состояние…"] = "Connection open. Waiting for state…",
|
||||
["В коллекции больше нет свободных копий."] = "No free copies left in the collection.",
|
||||
["Выберите деку — выбор делается один раз."] = "Choose a deck — this can be done only once.",
|
||||
["\nКолода будет удалена без восстановления."] = "\nThe deck will be deleted permanently.",
|
||||
["Нажмите на колоду,\nчтобы открыть редактор."] = "Click a deck\nto open the editor.",
|
||||
["ЛКМ — убрать копию,\nПКМ — посмотреть карту."] = "LMB — remove a copy,\nRMB — view the card.",
|
||||
["Для этой карты сейчас нет доступного действия."] = "No action is available for this card right now.",
|
||||
["Только ваши матчи · реплей доступен участникам"] = "Only your matches · replay available to participants",
|
||||
["Не удалось обновить профиль. Сервер недоступен?"] = "Could not update profile. Is the server down?",
|
||||
["Не удалось загрузить триал-деки. Сервер недоступен?"] = "Could not load trial decks. Is the server down?",
|
||||
["Онбординг не завершён: выберите стартовую триал-деку."] = "Onboarding incomplete: choose a starter trial deck.",
|
||||
[" · стенд 192.168.31.158:18080 · localhost:5080"] = " · stand 192.168.31.158:18080 · localhost:5080",
|
||||
["Нет связи с сервером. Проверьте адрес и запущен ли сервер."] = "No connection to the server. Check the address and that the server is running.",
|
||||
["Выберите цель на поле.\nНажмите «Отмена», чтобы снять выбор."] = "Choose a target on the field.\nPress “Cancel” to clear the selection.",
|
||||
["5 карт: 4 C + 1 R+ · duplicate protection · 100 Gold или 1 токен."] = "5 cards: 4 C + 1 R+ · duplicate protection · 100 Gold or 1 token.",
|
||||
["Нажмите на карту в руке или на поле.\nДопустимые цели подсветятся."] = "Click a card in hand or on the field.\nValid targets will be highlighted.",
|
||||
["Выберите одну триал-деку. 50 карт попадут в коллекцию, плюс 5 токенов на бустеры."] = "Choose one trial deck. 50 cards will be added to your collection, plus 5 tokens for boosters.",
|
||||
["Unity блокирует HTTP. Player Settings → Allow downloads over HTTP = Always, затем перезапусти Unity."] = "Unity blocks HTTP. Player Settings → Allow downloads over HTTP = Always, then restart Unity.",
|
||||
["VanguardApp не создан. Запусти игру из Boot или через Play в любой сцене клиента."] = "VanguardApp was not created. Start the game from Boot or via Play in any client scene.",
|
||||
};
|
||||
|
||||
private static readonly KeyValuePair<string, string>[] Phrases = Exact
|
||||
.Where(pair => IsPhraseKey(pair.Key))
|
||||
.OrderByDescending(pair => pair.Key.Length)
|
||||
.ToArray();
|
||||
|
||||
private static readonly Regex YesWord = new Regex(
|
||||
@"\bда\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
private static readonly Regex NoWord = new Regex(
|
||||
@"\bнет\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_initialized = true;
|
||||
Language = PlayerPrefs.GetString(PreferenceKey, "ru") == "en"
|
||||
? UiLanguage.En
|
||||
: UiLanguage.Ru;
|
||||
}
|
||||
|
||||
public static void SetLanguage(UiLanguage language)
|
||||
{
|
||||
Initialize();
|
||||
if (Language == language)
|
||||
return;
|
||||
|
||||
Language = language;
|
||||
PlayerPrefs.SetString(PreferenceKey, language == UiLanguage.En ? "en" : "ru");
|
||||
PlayerPrefs.Save();
|
||||
LanguageChanged?.Invoke();
|
||||
}
|
||||
|
||||
public static void Toggle() =>
|
||||
SetLanguage(IsEnglish ? UiLanguage.Ru : UiLanguage.En);
|
||||
|
||||
public static string Translate(string source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source) || !IsEnglish)
|
||||
return source;
|
||||
|
||||
if (Exact.TryGetValue(source, out var exact))
|
||||
return exact;
|
||||
|
||||
var result = source;
|
||||
foreach (var pair in Phrases)
|
||||
{
|
||||
if (result.Contains(pair.Key, StringComparison.Ordinal))
|
||||
result = result.Replace(pair.Key, pair.Value, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
result = YesWord.Replace(result, "yes");
|
||||
result = NoWord.Replace(result, "no");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string CardName(CardCatalogItemDto card)
|
||||
{
|
||||
if (card == null)
|
||||
return string.Empty;
|
||||
|
||||
var localized = IsEnglish ? card.NameEn : card.NameRu;
|
||||
return string.IsNullOrWhiteSpace(localized) ? card.NameEn : localized;
|
||||
}
|
||||
|
||||
public static string OtherCardName(CardCatalogItemDto card)
|
||||
{
|
||||
if (card == null)
|
||||
return string.Empty;
|
||||
|
||||
var other = IsEnglish ? card.NameRu : card.NameEn;
|
||||
return string.IsNullOrWhiteSpace(other) ? string.Empty : other;
|
||||
}
|
||||
|
||||
public static string CardText(CardCatalogItemDto card)
|
||||
{
|
||||
if (card == null)
|
||||
return string.Empty;
|
||||
|
||||
var localized = IsEnglish ? card.TextEn : card.TextRu;
|
||||
if (!string.IsNullOrWhiteSpace(localized))
|
||||
return localized;
|
||||
|
||||
return card.TextEn ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool IsPhraseKey(string key)
|
||||
{
|
||||
if (key.Length >= 6)
|
||||
return true;
|
||||
|
||||
return key.Any(character =>
|
||||
character == ' ' || character == ':' || character == '.' ||
|
||||
character == ',' || character == '!' || character == '?' ||
|
||||
character == '«' || character == '»' || character == '·' ||
|
||||
character == '—' || character == '–' || character == '\n' ||
|
||||
character == '(' || character == ')' || character == '±' ||
|
||||
character == '←' || character == '→');
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переводит uGUI-лейбл при смене языка и подхватывает текст, выставленный кодом.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class LocalizedText : MonoBehaviour
|
||||
{
|
||||
private Text _text;
|
||||
private string _source = string.Empty;
|
||||
private string _applied = string.Empty;
|
||||
private bool _literal;
|
||||
private bool _subscribed;
|
||||
|
||||
private void Awake() => Subscribe();
|
||||
|
||||
private void OnDestroy() => Unsubscribe();
|
||||
|
||||
public void Initialize(string source)
|
||||
{
|
||||
_text = GetComponent<Text>();
|
||||
_source = source ?? string.Empty;
|
||||
_literal = false;
|
||||
Subscribe();
|
||||
ApplyCurrent();
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
if (_subscribed)
|
||||
return;
|
||||
|
||||
Loc.LanguageChanged += ApplyCurrent;
|
||||
_subscribed = true;
|
||||
}
|
||||
|
||||
private void Unsubscribe()
|
||||
{
|
||||
if (!_subscribed)
|
||||
return;
|
||||
|
||||
Loc.LanguageChanged -= ApplyCurrent;
|
||||
_subscribed = false;
|
||||
}
|
||||
|
||||
public void SetLiteral(string value)
|
||||
{
|
||||
_literal = true;
|
||||
_source = value ?? string.Empty;
|
||||
_applied = _source;
|
||||
if (_text != null)
|
||||
_text.text = _source;
|
||||
}
|
||||
|
||||
private void LateUpdate() => ApplyNow();
|
||||
|
||||
public void ApplyNow()
|
||||
{
|
||||
if (_literal || _text == null)
|
||||
return;
|
||||
|
||||
if (!string.Equals(_text.text, _applied, StringComparison.Ordinal))
|
||||
_source = _text.text;
|
||||
|
||||
ApplyCurrent();
|
||||
}
|
||||
|
||||
private void ApplyCurrent()
|
||||
{
|
||||
if (_literal || _text == null)
|
||||
return;
|
||||
|
||||
_applied = Loc.Translate(_source);
|
||||
if (!string.Equals(_text.text, _applied, StringComparison.Ordinal))
|
||||
_text.text = _applied;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9dc96684cbf149d49fc1b94b0800041a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
|
|
@ -110,6 +110,14 @@ namespace Vanguard.UI
|
|||
14,
|
||||
new Color(0.38f, 0.46f, 0.62f));
|
||||
UiKit.SetPreferredHeight(footer, 24f);
|
||||
|
||||
var language = UiKit.CreateLanguageButton(_root, "Language");
|
||||
var languageRect = language.GetComponent<RectTransform>();
|
||||
languageRect.anchorMin = new Vector2(1f, 1f);
|
||||
languageRect.anchorMax = new Vector2(1f, 1f);
|
||||
languageRect.pivot = new Vector2(1f, 1f);
|
||||
languageRect.anchoredPosition = new Vector2(-24f, -24f);
|
||||
languageRect.sizeDelta = new Vector2(72f, 40f);
|
||||
}
|
||||
|
||||
public void Show(string status = null)
|
||||
|
|
|
|||
|
|
@ -73,8 +73,12 @@ namespace Vanguard.UI
|
|||
|
||||
_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);
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ namespace Vanguard.UI
|
|||
UiKit.AddVerticalLayout(card, 24, 10);
|
||||
|
||||
var name = UiKit.CreateText(card, "Name", deck.Name, 30, Color.white, TextAnchor.MiddleLeft);
|
||||
UiKit.MakeLiteral(name);
|
||||
name.fontStyle = FontStyle.Bold;
|
||||
UiKit.SetPreferredHeight(name, 42f);
|
||||
|
||||
|
|
|
|||
|
|
@ -144,10 +144,21 @@ namespace Vanguard.UI
|
|||
text.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
text.raycastTarget = false;
|
||||
text.supportRichText = true;
|
||||
text.gameObject.AddComponent<LocalizedText>().Initialize(content);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
public static void MakeLiteral(Text text)
|
||||
{
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
var localized = text.GetComponent<LocalizedText>();
|
||||
if (localized != null)
|
||||
localized.SetLiteral(text.text);
|
||||
}
|
||||
|
||||
public static Button CreateButton(
|
||||
Transform parent,
|
||||
string name,
|
||||
|
|
@ -173,6 +184,29 @@ namespace Vanguard.UI
|
|||
return button;
|
||||
}
|
||||
|
||||
public static Button CreateLanguageButton(Transform parent, string name = "Language")
|
||||
{
|
||||
Button button = null;
|
||||
button = CreateButton(
|
||||
parent,
|
||||
name,
|
||||
Loc.IsEnglish ? "RU" : "EN",
|
||||
() =>
|
||||
{
|
||||
Loc.Toggle();
|
||||
var label = button != null ? button.GetComponentInChildren<Text>() : null;
|
||||
if (label != null)
|
||||
label.text = Loc.IsEnglish ? "RU" : "EN";
|
||||
},
|
||||
17);
|
||||
|
||||
var text = button.GetComponentInChildren<Text>();
|
||||
if (text != null)
|
||||
MakeLiteral(text);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
public static InputField CreateInput(
|
||||
Transform parent,
|
||||
string name,
|
||||
|
|
@ -203,6 +237,7 @@ namespace Vanguard.UI
|
|||
input.selectionColor = new Color(0.20f, 0.42f, 0.78f, 0.6f);
|
||||
|
||||
var text = CreateText(rect, "Text", string.Empty, 22, Color.white, TextAnchor.MiddleLeft);
|
||||
MakeLiteral(text);
|
||||
Stretch(text.rectTransform);
|
||||
var textRect = text.rectTransform;
|
||||
textRect.offsetMin = new Vector2(16f, 0f);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Linq;
|
|||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using Vanguard.Api;
|
||||
using Vanguard.Api.Contracts;
|
||||
using Vanguard.App;
|
||||
|
|
@ -75,6 +76,39 @@ namespace Vanguard.Tests
|
|||
Cleanup();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Localization_Translates_Static_And_Dynamic_Strings()
|
||||
{
|
||||
Loc.SetLanguage(UiLanguage.Ru);
|
||||
Assert.AreEqual("Коллекция", Loc.Translate("Коллекция"));
|
||||
|
||||
Loc.SetLanguage(UiLanguage.En);
|
||||
Assert.AreEqual("Collection", Loc.Translate("Коллекция"));
|
||||
Assert.AreEqual("Grade: all", Loc.Translate("Ранг: все"));
|
||||
Assert.AreEqual("Cards in collection: 12", Loc.Translate("Карт в коллекции: 12"));
|
||||
Assert.AreEqual("Victory!", Loc.Translate("Победа!"));
|
||||
Assert.AreEqual("Sentinel: yes · Boost: no", Loc.Translate("Sentinel: да · Boost: нет"));
|
||||
|
||||
Loc.SetLanguage(UiLanguage.Ru);
|
||||
|
||||
var go = new GameObject("LocalizedLabel", typeof(RectTransform), typeof(Text));
|
||||
var label = go.GetComponent<Text>();
|
||||
var localized = go.AddComponent<LocalizedText>();
|
||||
localized.Initialize("Коллекция");
|
||||
Assert.AreEqual("Коллекция", label.text);
|
||||
|
||||
Loc.SetLanguage(UiLanguage.En);
|
||||
Assert.AreEqual("Collection", label.text);
|
||||
|
||||
label.text = "Магазин";
|
||||
localized.ApplyNow();
|
||||
Assert.AreEqual("Store", label.text);
|
||||
|
||||
Loc.SetLanguage(UiLanguage.Ru);
|
||||
Assert.AreEqual("Магазин", label.text);
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
private static bool Exists(string name) =>
|
||||
Resources.FindObjectsOfTypeAll<GameObject>().Any(go => go.name == name);
|
||||
|
||||
|
|
|
|||
|
|
@ -62,24 +62,28 @@ http://localhost:5080
|
|||
без истории/MMR/наград.
|
||||
- `GameConnection`: WebSocket + MessagePack, handshake, snapshot/events,
|
||||
интенты, ответы на ping, закрытие и переподключение.
|
||||
- Боевой gray-box: доска, рука, часы, кнопки всех легальных интентов,
|
||||
- Боевой экран: доска, рука, часы, кнопки всех легальных интентов,
|
||||
сдача с подтверждением и экран результата.
|
||||
- Боевой UX: клик по картам/зонам, подсветка целей, сгруппированные
|
||||
действия, выбор силы/эффекта триггера, журнал событий и подсказки;
|
||||
event-анимации с очередью и скипом.
|
||||
- Карты в бою и коллекции: процедурные арты по клану, рамка, имя, ранг,
|
||||
триггер, сила/крит, REST и рубашка для скрытых карт.
|
||||
- Карты в бою и коллекции: реальные арты, рамка по клану, имя, ранг,
|
||||
триггер, сила/крит, REST и рубашка для скрытых карт; call/ride-въезд,
|
||||
пульс атаки и призрак ухода в дроп.
|
||||
- Реальные арты: `CardImageService` ищет `Resources/CardArt/{imageKey}`;
|
||||
`imageKey` приходит из `/api/cards`. Скачаны все **124** арта;
|
||||
импорт — `tools/import_card_art.py`.
|
||||
- Локализация RU/EN: `Localization.cs` (`Loc` + `LocalizedText`),
|
||||
переключатель в Login/Lobby, сохранение в `PlayerPrefs`; имена и тексты
|
||||
карт берутся из `nameRu/nameEn` и `textRu/textEn`.
|
||||
- Экран входа и главный экран с балансами, MMR и переходами в разделы.
|
||||
|
||||
## Что дальше
|
||||
|
||||
1. Довести боевой UI: арты карт, зоны, анимации, скип/ускорение.
|
||||
1. Drive/damage-анимации карт и стола, звук, ускорение/скип.
|
||||
2. Реконнект на всех шагах и аккуратная обработка таймаутов.
|
||||
3. Декбилдер: drag&drop, live-валидация, редкость/тип/арт, пресеты.
|
||||
4. Локализация RU/EN, настройки, DOTween-анимации.
|
||||
4. Настройки (язык уже есть), DOTween-анимации.
|
||||
|
||||
## Плагины
|
||||
|
||||
|
|
@ -102,10 +106,9 @@ MessagePack-сериализатор и юнион-кадры.
|
|||
защищённое хранилище.
|
||||
- Текущий UI собран кодом на uGUI; каждая сцена имеет свой Canvas.
|
||||
Префабы/UI Toolkit подключим, когда появятся макеты.
|
||||
- DOTween пока не добавлен; анимации UI — после перехода на сцены.
|
||||
- DOTween пока не добавлен; текущие анимации карт — собственные корутины/тики.
|
||||
- Мета-экраны — заглушки: сеть и авторизация настоящие, разделы следующие.
|
||||
- Бой — gray-box: карты показываются по `printingId`, без артов и анимаций;
|
||||
действия — кнопки всех `MatchView.LegalIntents`.
|
||||
- В бою нет звука и отдельных drive/damage-анимаций; остальное работает.
|
||||
- `GameConnection` использует `System.Net.WebSockets.ClientWebSocket`
|
||||
(Windows/десктоп first). Для WebGL понадобится другой транспорт или
|
||||
нативный WebSocket-плагин.
|
||||
|
|
@ -131,4 +134,5 @@ Smoke сцен (входит в Play Mode, открывает Boot → Login, п
|
|||
```
|
||||
|
||||
Тесты не ходят в сеть: проверяются composition root, создание Canvas,
|
||||
экранов и 3D-окружения.
|
||||
экранов, импорт арта и локализация (статические строки, динамика,
|
||||
live-переключение). Сейчас **3/3**.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Передача сессии
|
||||
|
||||
Состояние проекта на **21 октября 2026**. Документ нужен, чтобы новая сессия
|
||||
Состояние проекта на **22 октября 2026**. Документ нужен, чтобы новая сессия
|
||||
продолжила работу без повторного разбора истории. Краткая оперативная
|
||||
сводка — `docs/SESSION-STATE.md`; этот файл хранит подробную историю.
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
|||
|
||||
- ветка `master`;
|
||||
- remote `ssh://git@192.168.31.158:2222/mpakobecex/VanguardReMaster.git`;
|
||||
- актуальный HEAD смотреть через `git log -1` (на момент записи — `6ed0998`);
|
||||
- актуальный HEAD смотреть через `git log -1`;
|
||||
- рабочее дерево чистое.
|
||||
|
||||
Главные принципы:
|
||||
|
|
@ -47,7 +47,7 @@ MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты
|
|||
| Протокол и контракты | `docs/protocol.md`, `Vanguard.Shared` готовы |
|
||||
| Сервер | сеть, матчмейкинг, PostgreSQL + Redis, Orleans, Docker |
|
||||
| Мета-API | JWT, коллекция, колоды, магазин, экономика, история, реплеи |
|
||||
| Клиент | Unity 6.6, 3D-сцены Login/Lobby/DeckBuilder/Battle, логин/коллекция/колоды/очередь/gray-box бой |
|
||||
| Клиент | Unity 6.6, 3D-сцены Login/Lobby/DeckBuilder/Battle, логин/коллекция/колоды/очередь/бой, 124 арта, RU/EN, анимации карт |
|
||||
| Лаунчер | не начат |
|
||||
|
||||
Блок «Игровое ядро» из `IMPLEMENTATION_CHECKLIST.md` закрыт по его критериям:
|
||||
|
|
@ -762,3 +762,31 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
дроп справа, зеркально у противника, зона защиты между полями.
|
||||
- Проверки визуального блока: Unity EditMode 2/2, `SCENE_SMOKE success=True`,
|
||||
live-практика с ботом на стенде.
|
||||
|
||||
## 2026-10-22 — перемещения карт и локализация RU/EN
|
||||
|
||||
- Перемещения карт в бою:
|
||||
- при call/ride карта въезжает из руки (свой) или сверху (противник),
|
||||
с scale-in;
|
||||
- атакующий и цель пульсируют;
|
||||
- карта, ушедшая из слота в дроп, оставляет призрак: летит вниз/вверх,
|
||||
уменьшается и растворяется; для закрытой карты противника используется
|
||||
рубашка, лицо не раскрывается;
|
||||
- позиции карт снимаются после layout, поэтому анимации не зависят от
|
||||
внешних твинов; при новом snapshot анимации пересобираются.
|
||||
- Локализация:
|
||||
- `Localization.cs`: `Loc` + `LocalizedText`; русская строка — ключ,
|
||||
английский перевод — словарь + фразовые подстановки для динамических
|
||||
строк; `да/нет` переводятся по границам слов;
|
||||
- `UiKit.CreateText` автоматически вешает `LocalizedText`, поэтому
|
||||
существующие экраны переведены без переписывания;
|
||||
- переключатель `EN`/`RU` в Login и Lobby; выбор в `PlayerPrefs`
|
||||
(`vanguard.ui.language`), применяется на лету;
|
||||
- имена/тексты карт: `Loc.CardName` / `Loc.CardText` выбирают
|
||||
`nameRu/nameEn` и `textRu/textEn`; карточные поля помечены literal,
|
||||
чтобы словарь не портил переводы карт.
|
||||
- Проверки: Unity EditMode **3/3** (экраны, импорт арта, перевод статики и
|
||||
динамики, live-смена языка), `SCENE_SMOKE success=True active=Login`.
|
||||
- Ограничения: звука пока нет; полноценные drive/damage-анимации карт и
|
||||
стола — следующий блок; локализация сделана runtime-словарём, без
|
||||
Unity Localization-таблиц (пакет остаётся подключён).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# Vanguard ReMaster — состояние сессии
|
||||
|
||||
**Дата:** 2026-10-21
|
||||
**HEAD:** см. `git log -1`
|
||||
**Дата:** 2026-10-22
|
||||
|
||||
**HEAD:** см. `git log -1`
|
||||
**Коротко:** сервер готов, клиент — рабочий каркас с 3D-сценами.
|
||||
|
||||
## Правила работы
|
||||
|
|
@ -49,19 +50,26 @@
|
|||
- Декбилдер: `DeckBuilderView`; каталог метаданных — `GET /api/cards`
|
||||
(grade, trigger, clan, rarity, printing, power/shield/type, текст
|
||||
способности), клиент объединяет с коллекцией.
|
||||
Изображения карт пока заглушки-плашки по цвету клана.
|
||||
Изображения карт — реальные арты из `Resources/CardArt` (124/124), с
|
||||
процедурным fallback по цвету клана.
|
||||
- ПКМ по карте — оверлей с картой, способностями, разбором в пыль и
|
||||
крафтом; SP/TD не разбираются и не крафтятся.
|
||||
- Локализация RU/EN: `Loc` + `LocalizedText`; переключатель в Login и
|
||||
Lobby (кнопка `EN`/`RU`), выбор сохраняется в `PlayerPrefs`. Русские
|
||||
строки остаются ключами, переводятся и статические, и динамические
|
||||
подписи; имена/тексты карт берутся из `nameRu/nameEn` и `textRu/textEn`.
|
||||
- Тренировка с ботом: кнопка «С ботом» в редакторе колоды; сервер
|
||||
создаёт casual-матч против случайного бота (`POST /api/game/practice`),
|
||||
без истории, MMR и наград.
|
||||
- Боевой экран: процедурные карты с арт-градиентом по клану, рамкой,
|
||||
именем, рангом, триггером, силой/критом и рубашкой.
|
||||
- Боевой экран: реальные арты карт, рамка по клану, имя, ранг, триггер,
|
||||
сила/крит, REST и рубашка; процедурные плашки остаются fallback.
|
||||
- Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона
|
||||
(+soul), справа колода и ниже дроп; у противника зеркально; между
|
||||
полями — зона защиты со щитом и проверками.
|
||||
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
||||
стола, не блокирует ввод; очередь событий + кнопка «Пропустить анимации».
|
||||
Карты въезжают из руки/колоды при call/ride, атакующий и цель пульсируют,
|
||||
ушедшие в дроп карты летят призраком и растворяются.
|
||||
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
|
||||
`Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт:
|
||||
`tools/import_card_art.py`.
|
||||
|
|
@ -70,8 +78,8 @@
|
|||
выбор силы/эффекта триггера, журнал последних событий.
|
||||
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
||||
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
||||
- Не готово: файлы реальных артов (пайплайн готов), 3D-модели,
|
||||
полноценная очередь анимаций, локализация RU/EN, настройки, DOTween.
|
||||
- Не готово: 3D-модели, звук, настройки (язык уже есть), DOTween,
|
||||
drag&drop и live-валидация декбилдера.
|
||||
|
||||
## Запуск
|
||||
|
||||
|
|
@ -103,6 +111,10 @@ curl http://127.0.0.1:18080/health
|
|||
-executeMethod Vanguard.Editor.SceneSmoke.Run
|
||||
```
|
||||
|
||||
Клиентские проверки: **3/3** EditMode (сборка всех экранов, импорт арта,
|
||||
перевод статических/динамических строк и live-переключение языка),
|
||||
`SCENE_SMOKE success=True`.
|
||||
|
||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||
ломает стандартный PlayMode test runner. `SceneBuilder` пересобирает сцены:
|
||||
|
||||
|
|
@ -126,7 +138,7 @@ client/Assets/Vanguard/Runtime/App/
|
|||
client/Assets/Vanguard/Runtime/UI/
|
||||
LoginView.cs MetaView.cs OnboardingView.cs
|
||||
DeckBuilderView.cs QueueView.cs BattleView.cs
|
||||
UiKit.cs
|
||||
UiKit.cs Localization.cs
|
||||
client/Assets/Vanguard/Runtime/Battle/GameConnection.cs
|
||||
client/Assets/Vanguard/Runtime/Api/ApiClient.cs + ApiContracts.cs
|
||||
client/Assets/Vanguard/Editor/SceneBuilder.cs + SceneSmoke.cs
|
||||
|
|
@ -149,13 +161,12 @@ client/Assets/Plugins/Vanguard.Shared + MessagePack
|
|||
|
||||
## Следующие шаги
|
||||
|
||||
1. Боевой UI: арты, зоны, анимации, скип; реконнект и таймауты.
|
||||
2. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
||||
3. 3D-наполнение: модели/арт, prefab-экраны, камера.
|
||||
4. Локализация RU/EN, настройки, DOTween.
|
||||
5. Лаунчер WPF.
|
||||
1. Декбилдер: drag&drop, счётчики лимитов, live-валидация, пресеты.
|
||||
2. 3D-наполнение: модели/арт, prefab-экраны, камера.
|
||||
3. Звук и настройки, DOTween.
|
||||
4. Лаунчер WPF.
|
||||
|
||||
Тренировка с ботом уже есть — следующий блок можно начинать с боевого UI.
|
||||
Тренировка с ботом и боевой UI с артами/анимациями/локализацией уже есть.
|
||||
|
||||
## Подводные камни
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue