Show vanguard, rear-guard and guard cards face up
CI / test (push) Successful in 2m11s Details

This commit is contained in:
MpakobecEx 2026-10-22 03:17:41 +05:00
parent 7035c11873
commit e23116946c
8 changed files with 78 additions and 9 deletions

View File

@ -1421,6 +1421,44 @@ namespace Vanguard.UI
Muted, Muted,
TextAnchor.MiddleRight); TextAnchor.MiddleRight);
UiKit.SetPreferredHeight(guardian, 32f); UiKit.SetPreferredHeight(guardian, 32f);
AddGuardCards(guard, view);
}
private void AddGuardCards(Transform parent, MatchView view)
{
var cards = view.Battle?.GuardCards;
if (cards == null || cards.Count == 0)
return;
var row = UiKit.CreatePanel(parent, "GuardCards", Color.clear);
var element = row.gameObject.AddComponent<LayoutElement>();
element.preferredWidth = cards.Count * 52f;
element.preferredHeight = 58f;
element.flexibleWidth = 0f;
UiKit.AddHorizontalLayout(row, 0, 4);
foreach (var card in cards)
{
var tile = UiKit.CreatePanel(row, "Guard_" + card.InstanceId, Color.white);
var tileElement = tile.gameObject.AddComponent<LayoutElement>();
tileElement.preferredWidth = 48f;
tileElement.preferredHeight = 58f;
tileElement.flexibleWidth = 0f;
var image = tile.GetComponent<Image>();
_catalog.TryGetValue(card.PrintingId, out var meta);
CardImageService.Apply(
image,
meta != null ? meta.ImageKey : null,
CardArt.ForCard(
meta != null ? meta.NameId : card.PrintingId,
meta != null ? meta.Clan : "None"));
var outline = tile.gameObject.AddComponent<Outline>();
outline.effectColor = new Color(0.55f, 0.86f, 1f, 0.85f);
outline.effectDistance = new Vector2(1f, -1f);
}
} }
private void AddHand(PlayerView self) private void AddHand(PlayerView self)

View File

@ -184,6 +184,10 @@ namespace Vanguard.Tests
ActivePlayer = 0, ActivePlayer = 0,
Phase = GamePhase.Main, Phase = GamePhase.Main,
Players = new List<PlayerView> { self, opponent }, Players = new List<PlayerView> { self, opponent },
Battle = new Vanguard.Shared.Protocol.BattleView
{
GuardCards = new List<CardView> { Card("BT01/011EN", 7) },
},
Clock = new ClockView(), Clock = new ClockView(),
LegalIntents = new List<IntentPayload>(), LegalIntents = new List<IntentPayload>(),
}; };
@ -199,6 +203,7 @@ namespace Vanguard.Tests
Assert.IsTrue(Exists("Damage_0")); Assert.IsTrue(Exists("Damage_0"));
Assert.IsTrue(Exists("Damage_1")); Assert.IsTrue(Exists("Damage_1"));
Assert.IsTrue(Exists("Damage_2")); Assert.IsTrue(Exists("Damage_2"));
Assert.IsTrue(Exists("Guard_7"));
Assert.IsTrue(Exists("Card_R4")); Assert.IsTrue(Exists("Card_R4"));
var hasSidewaysCard = Resources.FindObjectsOfTypeAll<GameObject>() var hasSidewaysCard = Resources.FindObjectsOfTypeAll<GameObject>()
.Where(go => go.name == "Card_R4") .Where(go => go.name == "Card_R4")

View File

@ -71,8 +71,9 @@ http://localhost:5080
триггер, сила/крит, REST поворотом на бок и рубашка для скрытых карт; триггер, сила/крит, REST поворотом на бок и рубашка для скрытых карт;
call/ride-въезд, пульс атаки и призрак ухода в дроп. call/ride-въезд, пульс атаки и призрак ухода в дроп.
- Зона урона: видимые карты на боку; heal выбирает карту урона, Counter - Зона урона: видимые карты на боку; heal выбирает карту урона, Counter
Blast принимает выбранную закрытую карту. Маллиган — выбор карт в руке Blast принимает выбранную открытую карту. Маллиган — выбор карт в руке
с обменом и добиром до пяти. с обменом и добиром до пяти. Карты V/R и круга защиты открыты, в защите
показывается ряд карт.
- Реальные арты: `CardImageService` ищет `Resources/CardArt/{imageKey}`; - Реальные арты: `CardImageService` ищет `Resources/CardArt/{imageKey}`;
`imageKey` приходит из `/api/cards`. Скачаны все **124** арта; `imageKey` приходит из `/api/cards`. Скачаны все **124** арта;
импорт — `tools/import_card_art.py`. импорт — `tools/import_card_art.py`.

View File

@ -828,5 +828,7 @@ Battle; Boot и общие сервисы остаются персистент
раз, оба игрока; в первый ход атака запрещена; первый игрок случайный раз, оба игрока; в первый ход атака запрещена; первый игрок случайный
(для тестов можно зафиксировать); `DeckValidator` требует ровно 16 (для тестов можно зафиксировать); `DeckValidator` требует ровно 16
триггеров, декбилдер показывает счётчик. триггеров, декбилдер показывает счётчик.
- Карты на поле (`V`, `R`) и в круге защиты принудительно отдаются
клиенту открытыми; в круге защиты дополнительно рисуется ряд карт.
- Classic-режим не включает Stride/Вознесение, Legacy, Gift, Front-триггер - Classic-режим не включает Stride/Вознесение, Legacy, Gift, Front-триггер
и Grade support. и Grade support.

View File

@ -63,7 +63,8 @@
без истории, MMR и наград. без истории, MMR и наград.
- Боевой экран: реальный арт на всю карту, рамка по клану, имя, ранг, - Боевой экран: реальный арт на всю карту, рамка по клану, имя, ранг,
триггер, сила/крит и рубашка; fallback — процедурная плашка. триггер, сила/крит и рубашка; fallback — процедурная плашка.
Rest поворачивает карту на бок. Rest поворачивает карту на бок. Карты в кругах V/R и в круге защиты
показываются открытыми.
- Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона - Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона
(+soul), справа колода и ниже дроп; у противника зеркально; между (+soul), справа колода и ниже дроп; у противника зеркально; между
полями — зона защиты со щитом и проверками. полями — зона защиты со щитом и проверками.

View File

@ -309,6 +309,9 @@ Draw, Stand.
8. Heal срабатывает при уроне **>=** урона соперника. 8. Heal срабатывает при уроне **>=** урона соперника.
9. Mulligan: один раз, любое количество карт, возврат в низ → добор → shuffle. 9. Mulligan: один раз, любое количество карт, возврат в низ → добор → shuffle.
10. В первый ход атаковать нельзя. 10. В первый ход атаковать нельзя.
11. Все карты на поле (авангард, арьергард) и в круге защиты лежат
открытыми; закрытыми могут быть только карты в руке/колоде/уроне после
Counter Blast.
--- ---

View File

@ -597,6 +597,7 @@ public sealed class GameEngine
p.Soul.Remove(card); p.Soul.Remove(card);
card.Rested = false; card.Rested = false;
card.FaceUp = true;
p.RearGuards[slot] = card; p.RearGuards[slot] = card;
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId)); _events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
@ -1178,6 +1179,7 @@ public sealed class GameEngine
} }
card.Rested = false; card.Rested = false;
card.FaceUp = true;
p.RearGuards[slot] = card; p.RearGuards[slot] = card;
_events.Add(new UnitCalledEvent(player, slot, card.PrintingId)); _events.Add(new UnitCalledEvent(player, slot, card.PrintingId));
@ -1220,6 +1222,7 @@ public sealed class GameEngine
} }
card.Rested = false; card.Rested = false;
card.FaceUp = true;
p.Vanguard = card; p.Vanguard = card;
_events.Add(new UnitRodeEvent(player, card.PrintingId)); _events.Add(new UnitRodeEvent(player, card.PrintingId));
@ -1703,6 +1706,7 @@ public sealed class GameEngine
var card = p.Deck[index]; var card = p.Deck[index];
p.Deck.RemoveAt(index); p.Deck.RemoveAt(index);
card.FaceUp = true;
p.Vanguard = card; p.Vanguard = card;
} }
@ -1832,6 +1836,7 @@ public sealed class GameEngine
player.Hand.RemoveAt(index); player.Hand.RemoveAt(index);
card.Rested = false; card.Rested = false;
card.FaceUp = true;
player.Vanguard = card; player.Vanguard = card;
_events.Add(new UnitRodeEvent(State.ActivePlayer, card.PrintingId)); _events.Add(new UnitRodeEvent(State.ActivePlayer, card.PrintingId));
@ -1877,6 +1882,7 @@ public sealed class GameEngine
var card = player.Hand[index]; var card = player.Hand[index];
player.Hand.RemoveAt(index); player.Hand.RemoveAt(index);
card.Rested = false; card.Rested = false;
card.FaceUp = true;
player.RearGuards[slot] = card; player.RearGuards[slot] = card;
_events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId)); _events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId));
@ -2097,6 +2103,7 @@ public sealed class GameEngine
var shield = def.IsSentinel ? 0 : def.Shield; var shield = def.IsSentinel ? 0 : def.Shield;
defender.Hand.RemoveAt(index); defender.Hand.RemoveAt(index);
card.FaceUp = true;
defender.Guardian.Add(card); defender.Guardian.Add(card);
battle.GuardCards.Add(card); battle.GuardCards.Add(card);
battle.GuardShield += shield; battle.GuardShield += shield;
@ -2161,6 +2168,7 @@ public sealed class GameEngine
return CommandResult.Reject("Эта карта не может перехватывать."); return CommandResult.Reject("Эта карта не может перехватывать.");
defender.RearGuards[slot] = null; defender.RearGuards[slot] = null;
unit.FaceUp = true;
defender.Guardian.Add(unit); defender.Guardian.Add(unit);
battle.GuardCards.Add(unit); battle.GuardCards.Add(unit);
battle.Interceptor = unit; battle.Interceptor = unit;

View File

@ -76,12 +76,18 @@ public static class MatchProjection
? player.Hand.Select(card => ProjectCard(engine, card)).ToList() ? player.Hand.Select(card => ProjectCard(engine, card)).ToList()
: new List<CardView>(), : new List<CardView>(),
Vanguard = player.Vanguard is null ? null : ProjectCard(engine, player.Vanguard), Vanguard = player.Vanguard is null
? null
: ProjectCard(engine, player.Vanguard, forceFaceUp: true),
RearGuards = player.RearGuards RearGuards = player.RearGuards
.Select(card => card is null ? null : ProjectCard(engine, card)) .Select(card => card is null
? null
: ProjectCard(engine, card, forceFaceUp: true))
.ToArray(), .ToArray(),
Guardian = player.Guardian.Select(card => ProjectCard(engine, card)).ToList(), Guardian = player.Guardian
.Select(card => ProjectCard(engine, card, forceFaceUp: true))
.ToList(),
Soul = player.Soul.Select(card => ProjectCard(engine, card)).ToList(), Soul = player.Soul.Select(card => ProjectCard(engine, card)).ToList(),
FaceDownDamage = player.Damage.Count(card => !card.FaceUp), FaceDownDamage = player.Damage.Count(card => !card.FaceUp),
@ -96,7 +102,10 @@ public static class MatchProjection
}; };
} }
private static CardView ProjectCard(GameEngine engine, CardInstance card) private static CardView ProjectCard(
GameEngine engine,
CardInstance card,
bool forceFaceUp = false)
{ {
var definition = engine.State.Catalog.CardOf(card.PrintingId); var definition = engine.State.Catalog.CardOf(card.PrintingId);
@ -105,7 +114,7 @@ public static class MatchProjection
InstanceId = card.Id, InstanceId = card.Id,
PrintingId = card.PrintingId, PrintingId = card.PrintingId,
Rested = card.Rested, Rested = card.Rested,
FaceUp = card.FaceUp, FaceUp = forceFaceUp || card.FaceUp,
BasePower = definition.Power, BasePower = definition.Power,
Power = engine.EffectivePower(card), Power = engine.EffectivePower(card),
BaseCritical = definition.Critical, BaseCritical = definition.Critical,
@ -133,7 +142,9 @@ public static class MatchProjection
? -1 ? -1
: Array.IndexOf(defender.RearGuards, battle.Interceptor), : Array.IndexOf(defender.RearGuards, battle.Interceptor),
BoosterSlot = battle.BoosterSlot, BoosterSlot = battle.BoosterSlot,
GuardCards = battle.GuardCards.Select(card => ProjectCard(engine, card)).ToList(), GuardCards = battle.GuardCards
.Select(card => ProjectCard(engine, card, forceFaceUp: true))
.ToList(),
}; };
} }