diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index fd0e895..5f52186 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -1421,6 +1421,44 @@ namespace Vanguard.UI Muted, TextAnchor.MiddleRight); 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(); + 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(); + tileElement.preferredWidth = 48f; + tileElement.preferredHeight = 58f; + tileElement.flexibleWidth = 0f; + + var image = tile.GetComponent(); + _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.effectColor = new Color(0.55f, 0.86f, 1f, 0.85f); + outline.effectDistance = new Vector2(1f, -1f); + } } private void AddHand(PlayerView self) diff --git a/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs b/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs index 7665624..20aa13f 100644 --- a/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs +++ b/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs @@ -184,6 +184,10 @@ namespace Vanguard.Tests ActivePlayer = 0, Phase = GamePhase.Main, Players = new List { self, opponent }, + Battle = new Vanguard.Shared.Protocol.BattleView + { + GuardCards = new List { Card("BT01/011EN", 7) }, + }, Clock = new ClockView(), LegalIntents = new List(), }; @@ -199,6 +203,7 @@ namespace Vanguard.Tests Assert.IsTrue(Exists("Damage_0")); Assert.IsTrue(Exists("Damage_1")); Assert.IsTrue(Exists("Damage_2")); + Assert.IsTrue(Exists("Guard_7")); Assert.IsTrue(Exists("Card_R4")); var hasSidewaysCard = Resources.FindObjectsOfTypeAll() .Where(go => go.name == "Card_R4") diff --git a/client/README.md b/client/README.md index b4b685f..c65b2c4 100644 --- a/client/README.md +++ b/client/README.md @@ -71,8 +71,9 @@ http://localhost:5080 триггер, сила/крит, REST поворотом на бок и рубашка для скрытых карт; call/ride-въезд, пульс атаки и призрак ухода в дроп. - Зона урона: видимые карты на боку; heal выбирает карту урона, Counter - Blast принимает выбранную закрытую карту. Маллиган — выбор карт в руке - с обменом и добиром до пяти. + Blast принимает выбранную открытую карту. Маллиган — выбор карт в руке + с обменом и добиром до пяти. Карты V/R и круга защиты открыты, в защите + показывается ряд карт. - Реальные арты: `CardImageService` ищет `Resources/CardArt/{imageKey}`; `imageKey` приходит из `/api/cards`. Скачаны все **124** арта; импорт — `tools/import_card_art.py`. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 131d708..fb42122 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -828,5 +828,7 @@ Battle; Boot и общие сервисы остаются персистент раз, оба игрока; в первый ход атака запрещена; первый игрок случайный (для тестов можно зафиксировать); `DeckValidator` требует ровно 16 триггеров, декбилдер показывает счётчик. +- Карты на поле (`V`, `R`) и в круге защиты принудительно отдаются + клиенту открытыми; в круге защиты дополнительно рисуется ряд карт. - Classic-режим не включает Stride/Вознесение, Legacy, Gift, Front-триггер и Grade support. diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index 1d663fd..8b59c81 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -63,7 +63,8 @@ без истории, MMR и наград. - Боевой экран: реальный арт на всю карту, рамка по клану, имя, ранг, триггер, сила/крит и рубашка; fallback — процедурная плашка. - Rest поворачивает карту на бок. + Rest поворачивает карту на бок. Карты в кругах V/R и в круге защиты + показываются открытыми. - Поле: у каждого игрока два ряда `R V R` / `R R R`, слева зона урона (+soul), справа колода и ниже дроп; у противника зеркально; между полями — зона защиты со щитом и проверками. diff --git a/docs/game-rules.md b/docs/game-rules.md index 8f1ceb4..f71dbed 100644 --- a/docs/game-rules.md +++ b/docs/game-rules.md @@ -309,6 +309,9 @@ Draw, Stand. 8. Heal срабатывает при уроне **>=** урона соперника. 9. Mulligan: один раз, любое количество карт, возврат в низ → добор → shuffle. 10. В первый ход атаковать нельзя. +11. Все карты на поле (авангард, арьергард) и в круге защиты лежат + открытыми; закрытыми могут быть только карты в руке/колоде/уроне после + Counter Blast. --- diff --git a/server/src/Vanguard.Rules/Engine/GameEngine.cs b/server/src/Vanguard.Rules/Engine/GameEngine.cs index 768de42..ca6329b 100644 --- a/server/src/Vanguard.Rules/Engine/GameEngine.cs +++ b/server/src/Vanguard.Rules/Engine/GameEngine.cs @@ -597,6 +597,7 @@ public sealed class GameEngine p.Soul.Remove(card); card.Rested = false; + card.FaceUp = true; p.RearGuards[slot] = card; _events.Add(new UnitCalledEvent(player, slot, card.PrintingId)); @@ -1178,6 +1179,7 @@ public sealed class GameEngine } card.Rested = false; + card.FaceUp = true; p.RearGuards[slot] = card; _events.Add(new UnitCalledEvent(player, slot, card.PrintingId)); @@ -1220,6 +1222,7 @@ public sealed class GameEngine } card.Rested = false; + card.FaceUp = true; p.Vanguard = card; _events.Add(new UnitRodeEvent(player, card.PrintingId)); @@ -1703,6 +1706,7 @@ public sealed class GameEngine var card = p.Deck[index]; p.Deck.RemoveAt(index); + card.FaceUp = true; p.Vanguard = card; } @@ -1832,6 +1836,7 @@ public sealed class GameEngine player.Hand.RemoveAt(index); card.Rested = false; + card.FaceUp = true; player.Vanguard = card; _events.Add(new UnitRodeEvent(State.ActivePlayer, card.PrintingId)); @@ -1877,6 +1882,7 @@ public sealed class GameEngine var card = player.Hand[index]; player.Hand.RemoveAt(index); card.Rested = false; + card.FaceUp = true; player.RearGuards[slot] = card; _events.Add(new UnitCalledEvent(State.ActivePlayer, slot, card.PrintingId)); @@ -2097,6 +2103,7 @@ public sealed class GameEngine var shield = def.IsSentinel ? 0 : def.Shield; defender.Hand.RemoveAt(index); + card.FaceUp = true; defender.Guardian.Add(card); battle.GuardCards.Add(card); battle.GuardShield += shield; @@ -2161,6 +2168,7 @@ public sealed class GameEngine return CommandResult.Reject("Эта карта не может перехватывать."); defender.RearGuards[slot] = null; + unit.FaceUp = true; defender.Guardian.Add(unit); battle.GuardCards.Add(unit); battle.Interceptor = unit; diff --git a/server/src/Vanguard.Shared/Projection/MatchProjection.cs b/server/src/Vanguard.Shared/Projection/MatchProjection.cs index f8733b2..8dbd377 100644 --- a/server/src/Vanguard.Shared/Projection/MatchProjection.cs +++ b/server/src/Vanguard.Shared/Projection/MatchProjection.cs @@ -76,12 +76,18 @@ public static class MatchProjection ? player.Hand.Select(card => ProjectCard(engine, card)).ToList() : new List(), - 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 - .Select(card => card is null ? null : ProjectCard(engine, card)) + .Select(card => card is null + ? null + : ProjectCard(engine, card, forceFaceUp: true)) .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(), 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); @@ -105,7 +114,7 @@ public static class MatchProjection InstanceId = card.Id, PrintingId = card.PrintingId, Rested = card.Rested, - FaceUp = card.FaceUp, + FaceUp = forceFaceUp || card.FaceUp, BasePower = definition.Power, Power = engine.EffectivePower(card), BaseCritical = definition.Critical, @@ -133,7 +142,9 @@ public static class MatchProjection ? -1 : Array.IndexOf(defender.RearGuards, battle.Interceptor), BoosterSlot = battle.BoosterSlot, - GuardCards = battle.GuardCards.Select(card => ProjectCard(engine, card)).ToList(), + GuardCards = battle.GuardCards + .Select(card => ProjectCard(engine, card, forceFaceUp: true)) + .ToList(), }; }