VanguardReMaster/client/Assets/Vanguard/Tests/Editor/GameBoard3DTests.cs

694 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using Vanguard.Battle;
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>();
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<CardView>
{
new CardView
{
PrintingId = "BT01/002EN",
InstanceId = 31,
FaceUp = true,
},
};
opponent.Trigger = new List<CardView>
{
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 CardMoveAnimator_Releases_Input_Lock_When_Card_Is_Cancelled()
{
var config = PlaymatLayoutConfig.CreateDefault();
var root = new GameObject("AnimatorRoot");
var card = Card3D.Create(root.transform, config);
var input = new InputLockService();
var animator = new CardMoveAnimator(input, AnimationConfig.Default);
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);
animator.Enqueue(
card,
from,
Quaternion.identity,
Vector3.one,
0.5f,
0.3f);
animator.Tick(0.01f);
Assert.IsTrue(input.IsLocked);
animator.CancelCard(card);
Assert.IsFalse(input.IsLocked);
}
finally
{
animator.CancelAll();
Object.DestroyImmediate(root);
}
}
[Test]
public void Swapping_Rear_Guards_Does_Not_Release_Or_Duplicate_Cards()
{
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 first = new CardView
{
PrintingId = "BT01/002EN",
InstanceId = 41,
Power = 5000,
BasePower = 5000,
Critical = 1,
BaseCritical = 1,
};
var second = new CardView
{
PrintingId = "BT01/003EN",
InstanceId = 42,
Power = 5000,
BasePower = 5000,
Critical = 1,
BaseCritical = 1,
};
self.RearGuards[0] = first;
self.RearGuards[2] = second;
board.Render(view, new BoardRenderState());
self.RearGuards[0] = second;
self.RearGuards[2] = first;
board.Render(view, new BoardRenderState());
var cards = ActiveCards()
.Where(card => card.Ref.Zone == BoardZone.RearGuard
&& card.Ref.Player == 0
&& (card.Ref.InstanceId == 41
|| card.Ref.InstanceId == 42))
.ToList();
Assert.AreEqual(2, cards.Count);
Assert.AreEqual(
2,
cards.Select(card => card.Ref.InstanceId).Distinct().Count());
Assert.IsTrue(cards.All(card => card.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);
}
}
[Test]
public void Repeated_Render_Reuses_Hidden_Cards_And_Does_Not_Enqueue_Moves()
{
var config = PlaymatLayoutConfig.CreateDefault();
var board = GameBoard3D.Create(null, config);
try
{
board.SetVisible(true);
var view = BuildView(you: 0);
board.Render(view, new BoardRenderState());
board.MoveAnimator.Tick(0.01f);
var hidden = ActiveCards()
.Where(card =>
(card.Ref.Zone == BoardZone.Hand && card.Ref.Player != view.You)
|| (card.Ref.Zone == BoardZone.Damage && card.Ref.InstanceId == 0))
.ToList();
var hiddenCount = hidden.Count;
board.Render(view, new BoardRenderState());
var hiddenAfter = ActiveCards()
.Where(card =>
(card.Ref.Zone == BoardZone.Hand && card.Ref.Player != view.You)
|| (card.Ref.Zone == BoardZone.Damage && card.Ref.InstanceId == 0))
.ToList();
Assert.AreEqual(hiddenCount, hiddenAfter.Count);
Assert.AreEqual(
0,
board.MoveAnimator.PendingCount,
"Repeated render should not queue new moves for stable hidden cards");
}
finally
{
Object.DestroyImmediate(board.gameObject);
}
}
[Test]
public void Revealing_Face_Down_Damage_Reuses_Card_And_Stays_In_Damage_Zone()
{
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 == view.You);
board.Render(view, new BoardRenderState());
var hidden = ActiveCards().Single(card =>
card.Ref.Zone == BoardZone.Damage
&& card.Ref.Player == view.You
&& card.Ref.Index == 1);
self.FaceUpDamage.Add(new CardView
{
PrintingId = "BT01/099EN",
InstanceId = 99,
FaceUp = true,
});
self.FaceDownDamage = 1;
board.Render(view, new BoardRenderState());
var revealed = ActiveCards().Single(card =>
card.Ref.Zone == BoardZone.Damage
&& card.Ref.Player == view.You
&& card.Ref.Index == 1);
Assert.AreSame(hidden, revealed, "Revealed damage must reuse Card3D");
var layout = new PlaymatLayout(config);
var expected = layout.DamagePosition(0, 1);
Assert.Less(
Vector3.Distance(revealed.transform.position, expected),
0.5f,
"Revealed card should stay in damage zone, not fly from deck");
}
finally
{
Object.DestroyImmediate(board.gameObject);
}
}
[Test]
public void New_Field_Unit_Starts_From_Hand_Not_Deck()
{
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 == view.You);
board.Render(view, new BoardRenderState());
var handCard = ActiveCards().Single(card =>
card.Ref.Zone == BoardZone.Hand
&& card.Ref.Player == view.You
&& card.Ref.InstanceId == 11);
var handPosition = handCard.transform.position;
var cardView = self.Hand.First(card => card.InstanceId == 11);
self.Hand.Remove(cardView);
self.RearGuards[0] = cardView;
board.Render(view, new BoardRenderState());
var fieldCard = ActiveCards().Single(card =>
card.Ref.Zone == BoardZone.RearGuard
&& card.Ref.Player == view.You
&& card.Ref.Slot == 0);
Assert.AreSame(handCard, fieldCard);
Assert.Less(
Vector3.Distance(fieldCard.transform.position, handPosition),
0.1f,
"New field unit should start at hand position");
Assert.Greater(
Vector3.Distance(fieldCard.transform.position, board.Layout.DeckPosition(0)),
0.5f,
"New field unit should not start at deck");
}
finally
{
Object.DestroyImmediate(board.gameObject);
}
}
[Test]
public void CardMoveAnimator_Coalesces_Requests_For_Same_Card()
{
var input = new InputLockService();
var animator = new CardMoveAnimator(input, AnimationConfig.Default);
var root = new GameObject("Root");
var cardA = Card3D.Create(root.transform, PlaymatLayoutConfig.Default);
var cardB = Card3D.Create(root.transform, PlaymatLayoutConfig.Default);
try
{
var from = new Vector3(-1f, 0.1f, 0f);
var to = new Vector3(1f, 0.1f, 0f);
cardA.PlaceImmediate(from, Quaternion.identity, Vector3.one);
cardA.SetPose(to, Quaternion.identity, true, false, false);
animator.Enqueue(cardA, from, Quaternion.identity, Vector3.one, 0.5f, 0.3f);
Assert.AreEqual(1, animator.PendingCount);
// Вторая команда той же карты должна заменить первую,
// а не добавиться в очередь.
animator.Enqueue(cardA, from, Quaternion.identity, Vector3.one, 0.4f, 0.3f);
Assert.AreEqual(1, animator.PendingCount);
Assert.AreEqual(0, animator.ActiveCount);
// Другая карта — отдельный запрос.
cardB.PlaceImmediate(from, Quaternion.identity, Vector3.one);
cardB.SetPose(to, Quaternion.identity, true, false, false);
animator.Enqueue(cardB, from, Quaternion.identity, Vector3.one, 0.5f, 0.3f);
Assert.AreEqual(2, animator.PendingCount);
animator.CancelAll();
// Новый запрос во время активного движения отменяет старый
// и снова становится единственным.
cardA.PlaceImmediate(from, Quaternion.identity, Vector3.one);
cardA.SetPose(to, Quaternion.identity, true, false, false);
animator.Enqueue(cardA, from, Quaternion.identity, Vector3.one, 0.5f, 0.3f);
animator.Tick(0.2f);
Assert.AreEqual(1, animator.ActiveCount);
animator.Enqueue(cardA, from, Quaternion.identity, Vector3.one, 0.5f, 0.3f);
Assert.AreEqual(1, animator.ActiveCount + animator.PendingCount);
}
finally
{
animator.CancelAll();
Object.DestroyImmediate(root);
}
}
private static List<Card3D> ActiveCards() =>
Resources.FindObjectsOfTypeAll<Card3D>()
.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<CardView>
{
Card("BT01/002EN", 11),
Card("BT01/003EN", 12),
},
Guardian = new List<CardView> { Card("BT01/004EN", 7) },
Soul = new List<CardView> { Card("BT01/005EN", 8) },
FaceUpDamage = new List<CardView> { Card("BT01/006EN", 9) },
FaceDownDamage = 2,
DeckCount = 40,
Drop = new List<CardView>(),
};
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<CardView>(),
HandCount = 3,
Guardian = new List<CardView>(),
Soul = new List<CardView>(),
FaceUpDamage = new List<CardView>(),
FaceDownDamage = 0,
DeckCount = 40,
Drop = new List<CardView>(),
};
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<PlayerView> { self, opponent },
Clock = new ClockView(),
LegalIntents = new List<IntentPayload>(),
};
}
}
}