using System.Collections.Generic; using System.Linq; using NUnit.Framework; using UnityEngine; using Vanguard.Board3D; using Vanguard.Shared.Protocol; namespace Vanguard.Tests { public sealed class GameBoard3DTests { [Test] public void Board_Shows_Attack_Arrow_For_Active_Battle() { var config = PlaymatLayoutConfig.CreateDefault(); var board = GameBoard3D.Create(null, config); try { board.SetVisible(true); var view = BuildView(you: 0); view.Battle = new BattleView { AttackerPlayer = 0, AttackerSlot = 3, TargetSlot = -1, GuardShield = 5000, }; board.Render(view, new BoardRenderState()); Assert.IsTrue(board.AttackArrowVisible); } finally { Object.DestroyImmediate(board.gameObject); } } [Test] public void Board_Renders_Field_Hand_Guardian_Damage_And_Soul() { var config = PlaymatLayoutConfig.CreateDefault(); var board = GameBoard3D.Create(null, config); try { board.SetVisible(true); board.Render(BuildView(you: 0), new BoardRenderState()); var cards = ActiveCards(); Assert.GreaterOrEqual(cards.Count, 9); Assert.IsTrue(cards.Any(card => card.Ref.Zone == BoardZone.RearGuard && card.Ref.Player == 0 && card.Ref.Slot == 3)); Assert.IsTrue(cards.Any(card => card.Ref.Zone == BoardZone.Guardian && card.Ref.InstanceId == 7)); Assert.IsTrue(cards.Any(card => card.Ref.Zone == BoardZone.Hand && card.Ref.Player == 0)); var opponentHand = cards .Where(card => card.Ref.Zone == BoardZone.Hand && card.Ref.Player == 1) .ToList(); Assert.AreEqual(3, opponentHand.Count); Assert.IsTrue(opponentHand.All(card => !card.IsFaceUp)); Assert.IsTrue(cards.Any(card => card.Ref.Zone == BoardZone.Damage && card.Ref.Player == 0)); var rested = cards.Single(card => card.Ref.Zone == BoardZone.RearGuard && card.Ref.Player == 0 && card.Ref.Slot == 3); Assert.IsTrue(rested.IsRested); Assert.AreEqual(90f, Mathf.Abs(Mathf.DeltaAngle(rested.TargetYaw, 0f)), 0.1f); Assert.AreEqual("P8000 C1", rested.StatsText); var hiddenDamage = cards.Single(card => card.Ref.Zone == BoardZone.Damage && card.Ref.Player == 0 && card.Ref.Index == 1); Assert.IsFalse(hiddenDamage.IsFaceUp); Assert.IsTrue(hiddenDamage.IsRested); var visibleDamage = cards.Single(card => card.Ref.Zone == BoardZone.Damage && card.Ref.Player == 0 && card.Ref.Index == 0); Assert.IsTrue(visibleDamage.IsFaceUp); Assert.AreEqual( -90f, Mathf.DeltaAngle(0f, visibleDamage.TargetYaw), 0.1f, "Damage cards must rest counter-clockwise"); } finally { Object.DestroyImmediate(board.gameObject); } } [Test] public void Guardian_And_Battle_Guard_Cards_Are_Not_Duplicated() { var config = PlaymatLayoutConfig.CreateDefault(); var board = GameBoard3D.Create(null, config); try { board.SetVisible(true); var view = BuildView(you: 0); var self = view.Players.Single(player => player.Index == 0); view.Battle = new BattleView { AttackerPlayer = 1, AttackerSlot = 3, TargetSlot = -1, GuardCards = self.Guardian, }; board.Render(view, new BoardRenderState()); var guardianCards = ActiveCards() .Where(card => card.Ref.Zone == BoardZone.Guardian && card.Ref.Player == 0 && card.Ref.InstanceId == 7) .ToList(); Assert.AreEqual( 1, guardianCards.Count, "A guard card present in both Guardian and Battle.GuardCards must render once"); } finally { Object.DestroyImmediate(board.gameObject); } } [Test] public void Hand_Is_Anchored_To_Camera_Bottom_Center_And_Faces_Player() { var config = PlaymatLayoutConfig.CreateDefault(); var cameraGo = new GameObject("Main Camera", typeof(Camera)) { tag = "MainCamera", }; var camera = cameraGo.GetComponent(); camera.fieldOfView = config.cameraFov; camera.transform.position = config.cameraPosition; camera.transform.rotation = Quaternion.LookRotation( config.cameraTarget - config.cameraPosition, Vector3.up); var board = GameBoard3D.Create(null, config); try { board.SetVisible(true); board.Render(BuildView(you: 0), new BoardRenderState()); var hand = ActiveCards() .Where(card => card.Ref.Zone == BoardZone.Hand && card.Ref.Player == 0) .ToList(); Assert.AreEqual(2, hand.Count); foreach (var card in hand) { var viewport = camera.WorldToViewportPoint(card.transform.position); Assert.Greater(viewport.z, 0f, "Hand card must be in front of the camera"); Assert.That(viewport.x, Is.InRange(0.15f, 0.85f)); Assert.That(viewport.y, Is.InRange(0.01f, 0.55f)); var toCamera = (camera.transform.position - card.transform.position).normalized; Assert.Greater( Vector3.Dot(card.transform.up, toCamera), 0.8f, "Hand card must face the camera"); } } finally { Object.DestroyImmediate(board.gameObject); Object.DestroyImmediate(cameraGo); } } [Test] public void Trigger_Zone_Uses_Local_Plus_And_Minus_Ninety_Degrees() { var config = PlaymatLayoutConfig.CreateDefault(); var board = GameBoard3D.Create(null, config); try { board.SetVisible(true); var view = BuildView(you: 0); var self = view.Players.Single(player => player.Index == 0); var opponent = view.Players.Single(player => player.Index == 1); self.Trigger = new List { new CardView { PrintingId = "BT01/002EN", InstanceId = 31, FaceUp = true, }, }; opponent.Trigger = new List { new CardView { PrintingId = "BT01/003EN", InstanceId = 32, FaceUp = true, }, }; board.Render(view, new BoardRenderState()); var own = ActiveCards().Single(card => card.Ref.Zone == BoardZone.Trigger && card.Ref.Player == 0); var enemy = ActiveCards().Single(card => card.Ref.Zone == BoardZone.Trigger && card.Ref.Player == 1); var layout = new PlaymatLayout(config); Assert.AreEqual( 90f, Mathf.DeltaAngle(layout.BaseYaw(0), own.TargetYaw), 0.1f); Assert.AreEqual( -90f, Mathf.DeltaAngle(layout.BaseYaw(1), enemy.TargetYaw), 0.1f); } finally { Object.DestroyImmediate(board.gameObject); } } [Test] public void Pool_Reuses_Cards_Between_Snapshots() { var config = PlaymatLayoutConfig.CreateDefault(); var board = GameBoard3D.Create(null, config); try { board.SetVisible(true); var view = BuildView(you: 0); board.Render(view, new BoardRenderState()); var count = ActiveCards().Count; board.Render(view, new BoardRenderState()); Assert.AreEqual(count, ActiveCards().Count); } finally { Object.DestroyImmediate(board.gameObject); } } [Test] public void Local_Seat_Is_Always_Bottom_Of_The_Board() { var config = PlaymatLayoutConfig.CreateDefault(); var board = GameBoard3D.Create(null, config); try { board.SetVisible(true); board.Render(BuildView(you: 1), new BoardRenderState()); var vanguard = ActiveCards().Single(card => card.Ref.Zone == BoardZone.Vanguard && card.Ref.Player == 1); Assert.Less(vanguard.TargetPosition.z, 0f); var opponent = ActiveCards().Single(card => card.Ref.Zone == BoardZone.Vanguard && card.Ref.Player == 0); Assert.Greater(opponent.TargetPosition.z, 0f); } finally { Object.DestroyImmediate(board.gameObject); } } [Test] public void Highlight_And_Selection_State_Is_Applied_To_Cards() { var config = PlaymatLayoutConfig.CreateDefault(); var board = GameBoard3D.Create(null, config); try { board.SetVisible(true); var state = new BoardRenderState(); state.Highlights.Add(new BoardSlotKey(0, 3)); state.SelectedHandInstanceIds.Add(12); board.Render(BuildView(you: 0), state); var rested = ActiveCards().Single(card => card.Ref.Zone == BoardZone.RearGuard && card.Ref.Player == 0 && card.Ref.Slot == 3); Assert.IsTrue(rested.transform.Find("Highlight").gameObject.activeSelf); } finally { Object.DestroyImmediate(board.gameObject); } } [Test] public void Card_Arc_Move_Interpolates_And_Finishes_At_Target() { var config = PlaymatLayoutConfig.CreateDefault(); var root = new GameObject("ArcRoot"); var card = Card3D.Create(root.transform, config); try { var from = new Vector3(-1f, 0.1f, 0f); var to = new Vector3(1f, 0.1f, 0f); card.PlaceImmediate(from, Quaternion.identity, Vector3.one); card.SetPose( to, Quaternion.identity, true, false, false, scale: 1f); card.StartArcMove( from, Quaternion.identity, Vector3.one, 0.5f, 0.4f); card.Tick(0.25f); Assert.IsTrue(card.IsMoving); Assert.Greater(card.transform.position.y, from.y); card.Tick(0.5f); Assert.IsFalse(card.IsMoving); Assert.Less( Vector3.Distance(card.transform.position, to), 0.001f); } finally { Object.DestroyImmediate(root); } } private static List ActiveCards() => Resources.FindObjectsOfTypeAll() .Where(card => card.gameObject.activeInHierarchy) .ToList(); private static MatchView BuildView(int you) { CardView Card(string id, int instance, bool rested = false, bool faceUp = true) => new CardView { PrintingId = id, InstanceId = instance, Rested = rested, FaceUp = faceUp, Power = 8000, BasePower = 8000, Critical = 1, BaseCritical = 1, }; var self = new PlayerView { Index = you, Name = "Self", Vanguard = Card("BT01/001EN", 1), RearGuards = new CardView[5], Hand = new List { Card("BT01/002EN", 11), Card("BT01/003EN", 12), }, Guardian = new List { Card("BT01/004EN", 7) }, Soul = new List { Card("BT01/005EN", 8) }, FaceUpDamage = new List { Card("BT01/006EN", 9) }, FaceDownDamage = 2, DeckCount = 40, Drop = new List(), }; self.RearGuards[3] = Card("BT01/007EN", 5, rested: true); var otherIndex = 1 - you; var opponent = new PlayerView { Index = otherIndex, Name = "Opponent", Vanguard = Card("BT01/010EN", 21), RearGuards = new CardView[5], Hand = new List(), HandCount = 3, Guardian = new List(), Soul = new List(), FaceUpDamage = new List(), FaceDownDamage = 0, DeckCount = 40, Drop = new List(), }; opponent.RearGuards[4] = Card("BT01/011EN", 22, rested: true); return new MatchView { MatchId = "board-test", You = you, ActivePlayer = you, Phase = GamePhase.Main, Players = new List { self, opponent }, Clock = new ClockView(), LegalIntents = new List(), }; } } }