1454 lines
56 KiB
C#
1454 lines
56 KiB
C#
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
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("\u25B28000", 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 Hands_Fill_Mirrored_And_Newest_Is_On_Top()
|
||
{
|
||
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 player = ActiveCards()
|
||
.Where(card => card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0)
|
||
.OrderBy(card => card.Ref.Index)
|
||
.ToList();
|
||
var opponent = ActiveCards()
|
||
.Where(card => card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 1)
|
||
.OrderBy(card => card.Ref.Index)
|
||
.ToList();
|
||
Assert.AreEqual(2, player.Count);
|
||
Assert.AreEqual(3, opponent.Count);
|
||
|
||
var playerOldest = camera.WorldToViewportPoint(
|
||
player[0].transform.position);
|
||
var playerNewest = camera.WorldToViewportPoint(
|
||
player[1].transform.position);
|
||
Assert.Less(
|
||
playerOldest.x,
|
||
playerNewest.x,
|
||
"Player hand fills left-to-right: the newest card is on the right");
|
||
Assert.Less(
|
||
playerNewest.z,
|
||
playerOldest.z,
|
||
"Player newest card must be closer to the camera");
|
||
|
||
var opponentOldest = camera.WorldToViewportPoint(
|
||
opponent[0].transform.position);
|
||
var opponentNewest = camera.WorldToViewportPoint(
|
||
opponent[2].transform.position);
|
||
Assert.Greater(
|
||
opponentOldest.x,
|
||
opponentNewest.x,
|
||
"Opponent hand fills right-to-left: the newest card is on the left");
|
||
Assert.Less(
|
||
opponentNewest.z,
|
||
opponentOldest.z,
|
||
"Opponent newest card must be closer to the camera");
|
||
|
||
var view = BuildView(you: 0);
|
||
view.Players[0].Hand.Add(new CardView
|
||
{
|
||
PrintingId = "BT01/008EN",
|
||
InstanceId = 13,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
});
|
||
view.Players[1].HandCount = 4;
|
||
board.Render(view, new BoardRenderState(), animateMoves: false);
|
||
|
||
var newPlayer = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 2);
|
||
var newOpponent = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 1
|
||
&& card.Ref.Index == 3);
|
||
Assert.Greater(
|
||
camera.WorldToViewportPoint(newPlayer.TargetPosition).x,
|
||
playerNewest.x,
|
||
"A newly drawn player card must appear to the right");
|
||
Assert.Less(
|
||
camera.WorldToViewportPoint(newOpponent.TargetPosition).x,
|
||
opponentNewest.x,
|
||
"A newly drawn opponent card must appear to the left");
|
||
|
||
var drawSource = board.Layout.DeckPosition(0)
|
||
+ (Vector3.up * 0.03f);
|
||
Assert.Less(
|
||
Vector3.Distance(newPlayer.transform.position, drawSource),
|
||
0.01f,
|
||
"A drawn card must start above the deck");
|
||
Assert.AreEqual(
|
||
config.cardScale,
|
||
newPlayer.transform.localScale.x,
|
||
0.01f,
|
||
"A drawn card must start at deck scale, not hand scale");
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
Object.DestroyImmediate(cameraGo);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Hand_Reflows_While_A_Card_Leaves_From_Hand()
|
||
{
|
||
var config = PlaymatLayoutConfig.CreateDefault();
|
||
var board = GameBoard3D.Create(null, config);
|
||
try
|
||
{
|
||
var view = BuildView(you: 0);
|
||
var leaving = new CardView
|
||
{
|
||
PrintingId = "BT01/008EN",
|
||
InstanceId = 13,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
};
|
||
view.Players[0].Hand.Add(leaving);
|
||
board.SetVisible(true);
|
||
board.Render(view, new BoardRenderState());
|
||
|
||
var oldestBefore = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
|
||
var next = BuildView(you: 0);
|
||
next.Players[0].RearGuards[3] = leaving;
|
||
board.Render(next, new BoardRenderState());
|
||
|
||
var oldestAfter = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
Assert.Greater(
|
||
Vector3.Distance(oldestBefore, oldestAfter),
|
||
0.01f,
|
||
"The hand must start reflowing when a card leaves");
|
||
Assert.GreaterOrEqual(
|
||
board.MoveAnimator.PendingCount,
|
||
1,
|
||
"The leaving card must still be queued for movement");
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Dropped_Hand_Card_Holds_At_Drop_Until_Returned()
|
||
{
|
||
var config = PlaymatLayoutConfig.CreateDefault();
|
||
var board = GameBoard3D.Create(null, config);
|
||
try
|
||
{
|
||
board.SetVisible(true);
|
||
board.Render(BuildView(you: 0), new BoardRenderState());
|
||
var card = ActiveCards().Single(item =>
|
||
item.Ref.Zone == BoardZone.Hand
|
||
&& item.Ref.Player == 0
|
||
&& item.Ref.Index == 0);
|
||
var handTarget = card.TargetPosition;
|
||
|
||
card.BeginDrag();
|
||
card.DragTo(new Vector3(1f, 0f, 1f));
|
||
var held = card.transform.position;
|
||
card.EndDragAtCurrent();
|
||
|
||
Assert.IsFalse(card.IsDragging);
|
||
Assert.Less(
|
||
Vector3.Distance(card.TargetPosition, held),
|
||
0.001f,
|
||
"Dropped card must stay at the drop point");
|
||
card.Tick(0.2f);
|
||
Assert.Less(
|
||
Vector3.Distance(card.transform.position, held),
|
||
0.001f,
|
||
"Dropped card must not twitch while waiting for a snapshot");
|
||
|
||
var pendingField = typeof(GameBoard3D).GetField(
|
||
"_dropPendingCard",
|
||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||
Assert.IsNotNull(pendingField);
|
||
pendingField.SetValue(board, card);
|
||
|
||
board.ReturnDroppedCard();
|
||
Assert.Less(
|
||
Vector3.Distance(card.TargetPosition, handTarget),
|
||
0.001f,
|
||
"Returned card must target its hand pose again");
|
||
card.Tick(0.5f);
|
||
Assert.Less(
|
||
Vector3.Distance(card.transform.position, handTarget),
|
||
0.05f);
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Hand_Hover_Lifts_Card_By_Ten_Percent()
|
||
{
|
||
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 handCard = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0);
|
||
var before = handCard.TargetPosition;
|
||
|
||
var hoverField = typeof(GameBoard3D).GetField(
|
||
"_hoveredHandCard",
|
||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||
var updateHand = typeof(GameBoard3D).GetMethod(
|
||
"UpdateHandView",
|
||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||
Assert.IsNotNull(hoverField);
|
||
Assert.IsNotNull(updateHand);
|
||
|
||
hoverField.SetValue(board, handCard);
|
||
updateHand.Invoke(board, null);
|
||
var lifted = handCard.TargetPosition;
|
||
var delta = lifted - before;
|
||
Assert.Greater(delta.magnitude, 0.01f);
|
||
Assert.Greater(
|
||
Vector3.Dot(delta.normalized, camera.transform.up),
|
||
0.9f,
|
||
"Hover lift must go up in camera space");
|
||
|
||
hoverField.SetValue(board, null);
|
||
updateHand.Invoke(board, null);
|
||
Assert.Less(
|
||
Vector3.Distance(handCard.TargetPosition, before),
|
||
0.001f,
|
||
"Unhover must return the card to its hand pose");
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
Object.DestroyImmediate(cameraGo);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Hand_Draw_Stages_New_Cards_One_By_One()
|
||
{
|
||
var config = PlaymatLayoutConfig.CreateDefault();
|
||
var board = GameBoard3D.Create(null, config);
|
||
try
|
||
{
|
||
var view = BuildView(you: 0);
|
||
board.SetVisible(true);
|
||
board.Render(view, new BoardRenderState());
|
||
|
||
var oldestTargetBefore = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
|
||
var next = BuildView(you: 0);
|
||
next.Players[0].Hand.Add(new CardView
|
||
{
|
||
PrintingId = "BT01/008EN",
|
||
InstanceId = 13,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
});
|
||
next.Players[0].Hand.Add(new CardView
|
||
{
|
||
PrintingId = "BT01/009EN",
|
||
InstanceId = 14,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
});
|
||
board.Render(next, new BoardRenderState());
|
||
|
||
Assert.IsTrue(board.StagedDrawHandActive);
|
||
Assert.AreEqual(2, board.StagedDrawCards.Count);
|
||
Assert.AreEqual(2, board.MoveAnimator.PendingCount);
|
||
Assert.AreEqual(2, ActiveCards().Count(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0));
|
||
|
||
// Повторный snapshot без новых доборов не должен сбрасывать
|
||
// уже начатую ступенчатую раздачу.
|
||
board.Render(next, new BoardRenderState());
|
||
Assert.IsTrue(board.StagedDrawHandActive);
|
||
Assert.AreEqual(2, board.StagedDrawCards.Count);
|
||
|
||
var oldestTargetAtDrawStart = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
Assert.Less(
|
||
Vector3.Distance(
|
||
oldestTargetBefore,
|
||
oldestTargetAtDrawStart),
|
||
0.001f,
|
||
"The hand must not reflow before the first draw arrives");
|
||
|
||
board.MoveAnimator.Tick(0f);
|
||
Assert.AreEqual(1, board.MoveAnimator.ActiveCount);
|
||
var firstDrawn = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 2);
|
||
firstDrawn.Tick(0.7f);
|
||
board.MoveAnimator.Tick(0f);
|
||
|
||
Assert.AreEqual(3, board.StagedDrawCards.Count);
|
||
Assert.IsTrue(board.StagedDrawHandActive);
|
||
var oldestTargetAfter = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
Assert.Greater(
|
||
Vector3.Distance(oldestTargetBefore, oldestTargetAfter),
|
||
0.01f,
|
||
"The hand must reflow after the first drawn card arrives");
|
||
Assert.AreEqual(3, ActiveCards().Count(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0));
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Mulligan_Remaining_Cards_Reflow_After_Each_Return()
|
||
{
|
||
var config = PlaymatLayoutConfig.CreateDefault();
|
||
var board = GameBoard3D.Create(null, config);
|
||
try
|
||
{
|
||
var view = BuildView(you: 0);
|
||
view.Players[0].Hand.Add(new CardView
|
||
{
|
||
PrintingId = "BT01/008EN",
|
||
InstanceId = 13,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
});
|
||
view.Players[0].Hand.Add(new CardView
|
||
{
|
||
PrintingId = "BT01/009EN",
|
||
InstanceId = 14,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
});
|
||
board.SetVisible(true);
|
||
board.Render(view, new BoardRenderState());
|
||
|
||
var oldestTargetBefore = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
|
||
var next = BuildView(you: 0);
|
||
next.Players[0].Hand.Add(new CardView
|
||
{
|
||
PrintingId = "BT01/010EN",
|
||
InstanceId = 15,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
});
|
||
next.Players[0].Hand.Add(new CardView
|
||
{
|
||
PrintingId = "BT01/011EN",
|
||
InstanceId = 16,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
});
|
||
board.QueueHandReturnToDeck(0, 2);
|
||
board.Render(next, new BoardRenderState());
|
||
|
||
Assert.IsTrue(board.StagedReturnHandActive);
|
||
Assert.AreEqual(4, board.StagedReturnCards.Count);
|
||
Assert.IsTrue(board.StagedDrawHandActive);
|
||
|
||
// Первый возврат.
|
||
board.MoveAnimator.Tick(0f);
|
||
board.ReturningCards[0].Tick(0.7f);
|
||
board.MoveAnimator.Tick(0f);
|
||
// Второй возврат.
|
||
board.MoveAnimator.Tick(0.3f);
|
||
board.ReturningCards[0].Tick(0.7f);
|
||
board.MoveAnimator.Tick(0f);
|
||
|
||
Assert.IsFalse(board.StagedReturnHandActive);
|
||
Assert.AreEqual(2, board.StagedReturnCards.Count);
|
||
Assert.IsTrue(board.StagedDrawHandActive);
|
||
|
||
var oldestTargetAfter = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
Assert.Greater(
|
||
Vector3.Distance(oldestTargetBefore, oldestTargetAfter),
|
||
0.01f,
|
||
"Remaining mulligan cards must reflow after returns");
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Hand_Return_To_Deck_Starts_Right_To_Left_And_Targets_Deck_Bottom()
|
||
{
|
||
var config = PlaymatLayoutConfig.CreateDefault();
|
||
var board = GameBoard3D.Create(null, config);
|
||
try
|
||
{
|
||
var view = BuildView(you: 0);
|
||
view.Players[0].Hand.Add(new CardView
|
||
{
|
||
PrintingId = "BT01/008EN",
|
||
InstanceId = 13,
|
||
FaceUp = true,
|
||
Power = 8000,
|
||
BasePower = 8000,
|
||
Critical = 1,
|
||
BaseCritical = 1,
|
||
});
|
||
board.SetVisible(true);
|
||
board.Render(view, new BoardRenderState());
|
||
|
||
var oldHand = ActiveCards()
|
||
.Where(card => card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0)
|
||
.ToDictionary(card => card.Ref.Index);
|
||
var source13 = oldHand[2].transform.position;
|
||
var source12 = oldHand[1].transform.position;
|
||
var oldestTargetBefore = oldHand[0].TargetPosition;
|
||
|
||
var next = BuildView(you: 0);
|
||
next.Players[0].Hand = new List<CardView>
|
||
{
|
||
next.Players[0].Hand[0],
|
||
};
|
||
board.QueueHandReturnToDeck(0, 2);
|
||
board.Render(next, new BoardRenderState());
|
||
|
||
Assert.AreEqual(2, board.ReturningCardCount);
|
||
var returning = board.ReturningCards;
|
||
Assert.AreEqual(2, returning[0].Ref.Index);
|
||
Assert.AreEqual(1, returning[1].Ref.Index);
|
||
Assert.Less(
|
||
Vector3.Distance(returning[0].transform.position, source13),
|
||
0.001f,
|
||
"The rightmost return card must wait in the hand until its turn");
|
||
Assert.Less(
|
||
Vector3.Distance(returning[1].transform.position, source12),
|
||
0.001f,
|
||
"The next return card must stay in the hand until the first finishes");
|
||
|
||
Assert.IsTrue(board.StagedReturnHandActive);
|
||
Assert.AreEqual(3, board.StagedReturnCards.Count);
|
||
var oldestAfter = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
Assert.Less(
|
||
Vector3.Distance(oldestTargetBefore, oldestAfter),
|
||
0.001f,
|
||
"The hand must not reflow before the first card leaves");
|
||
|
||
var deckBottom = board.Layout.DeckPosition(0)
|
||
- (Vector3.up * 0.04f);
|
||
Assert.IsTrue(board.TryGetHandReturnTarget(
|
||
returning[0],
|
||
out var returnTarget,
|
||
out var returnRotation));
|
||
Assert.Less(
|
||
Vector3.Distance(returnTarget, deckBottom),
|
||
0.001f);
|
||
Assert.Greater(
|
||
Vector3.Dot(returnRotation * Vector3.up, Vector3.down),
|
||
0.9f);
|
||
Assert.AreEqual(2, board.MoveAnimator.PendingCount);
|
||
|
||
board.MoveAnimator.Tick(0f);
|
||
Assert.IsTrue(returning[0].IsMoving);
|
||
returning[0].Tick(0.7f);
|
||
board.MoveAnimator.Tick(0f);
|
||
|
||
Assert.AreEqual(1, board.ReturningCardCount);
|
||
Assert.AreEqual(2, board.StagedReturnCards.Count);
|
||
var oldestAfterFirst = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == 0
|
||
&& card.Ref.Index == 0).TargetPosition;
|
||
Assert.Greater(
|
||
Vector3.Distance(oldestTargetBefore, oldestAfterFirst),
|
||
0.01f,
|
||
"The hand must reflow after the first card leaves");
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void CardMoveAnimator_Runs_Sequential_Requests_And_Completes_Each()
|
||
{
|
||
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);
|
||
cardB.PlaceImmediate(from, Quaternion.identity, Vector3.one);
|
||
cardB.SetPose(to, Quaternion.identity, true, false, false);
|
||
|
||
var completed = new List<Card3D>();
|
||
animator.Enqueue(
|
||
cardA, from, Quaternion.identity, Vector3.one,
|
||
0.1f, 0.2f, sequential: true,
|
||
onCompleted: card => completed.Add(card));
|
||
animator.Enqueue(
|
||
cardB, from, Quaternion.identity, Vector3.one,
|
||
0.1f, 0.2f, sequential: true,
|
||
onCompleted: card => completed.Add(card));
|
||
|
||
animator.Tick(0f);
|
||
Assert.AreEqual(1, animator.ActiveCount);
|
||
Assert.AreEqual(1, animator.PendingCount);
|
||
|
||
cardA.Tick(0.2f);
|
||
animator.Tick(0f);
|
||
CollectionAssert.AreEqual(new[] { cardA }, completed);
|
||
Assert.AreEqual(0, animator.ActiveCount);
|
||
Assert.AreEqual(1, animator.PendingCount);
|
||
|
||
animator.Tick(0.3f);
|
||
Assert.AreEqual(1, animator.ActiveCount);
|
||
cardB.Tick(0.2f);
|
||
animator.Tick(0f);
|
||
CollectionAssert.AreEqual(new[] { cardA, cardB }, completed);
|
||
Assert.IsFalse(animator.IsBusy);
|
||
}
|
||
finally
|
||
{
|
||
animator.CancelAll();
|
||
Object.DestroyImmediate(root);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Trigger_Zone_Uses_Local_Minus_And_Plus_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 Removing_Card_From_Hand_Does_Not_Queue_Arcs_For_Remaining_Hand()
|
||
{
|
||
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 called = self.Hand.First(card => card.InstanceId == 11);
|
||
self.Hand.Remove(called);
|
||
self.RearGuards[0] = called;
|
||
|
||
board.Render(view, new BoardRenderState());
|
||
|
||
var remaining = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.Hand
|
||
&& card.Ref.Player == view.You
|
||
&& card.Ref.InstanceId == 12);
|
||
|
||
Assert.IsFalse(
|
||
board.MoveAnimator.IsCardQueued(remaining),
|
||
"Remaining hand cards must reflow without arc requests");
|
||
Assert.IsFalse(
|
||
board.MoveAnimator.IsCardActive(remaining),
|
||
"Remaining hand cards must reflow without active arc requests");
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Queued_Card_Holds_At_Source_Until_Arc_Starts()
|
||
{
|
||
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 first = self.Hand[0];
|
||
var second = self.Hand[1];
|
||
self.Hand.RemoveAt(1);
|
||
self.Hand.RemoveAt(0);
|
||
self.RearGuards[0] = first;
|
||
self.RearGuards[1] = second;
|
||
|
||
board.Render(view, new BoardRenderState());
|
||
board.MoveAnimator.Tick(0.001f);
|
||
|
||
var firstCard = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.RearGuard
|
||
&& card.Ref.Player == view.You
|
||
&& card.Ref.Slot == 0);
|
||
var secondCard = ActiveCards().Single(card =>
|
||
card.Ref.Zone == BoardZone.RearGuard
|
||
&& card.Ref.Player == view.You
|
||
&& card.Ref.Slot == 1);
|
||
|
||
Assert.IsTrue(firstCard.IsMoving || board.MoveAnimator.IsCardActive(firstCard),
|
||
"First card should already be moving");
|
||
Assert.IsTrue(board.MoveAnimator.IsCardQueued(secondCard),
|
||
"Second card should be queued behind the first");
|
||
Assert.Greater(
|
||
Vector3.Distance(secondCard.transform.position, secondCard.TargetPosition),
|
||
0.5f,
|
||
"Queued card must stay at source, not smooth-move toward target");
|
||
|
||
board.MoveAnimator.Tick(10f);
|
||
Assert.IsTrue(secondCard.IsMoving || board.MoveAnimator.IsCardActive(secondCard),
|
||
"Second card should start moving after the first finishes");
|
||
}
|
||
finally
|
||
{
|
||
Object.DestroyImmediate(board.gameObject);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Cancel_Queued_Move_Releases_Hold()
|
||
{
|
||
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);
|
||
|
||
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);
|
||
|
||
animator.Tick(0.001f);
|
||
Assert.IsTrue(animator.IsCardQueued(cardB));
|
||
|
||
animator.CancelCard(cardB);
|
||
Assert.IsFalse(animator.IsCardQueued(cardB));
|
||
|
||
cardB.Tick(0.5f);
|
||
Assert.Less(
|
||
Vector3.Distance(cardB.transform.position, cardB.TargetPosition),
|
||
0.5f,
|
||
"Canceled queued card should resume normal movement to target");
|
||
}
|
||
finally
|
||
{
|
||
animator.CancelAll();
|
||
Object.DestroyImmediate(root);
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void Hand_Visibility_Toggles()
|
||
{
|
||
var config = PlaymatLayoutConfig.CreateDefault();
|
||
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.Greater(hand.Count, 0);
|
||
|
||
board.SetHandVisible(false);
|
||
Assert.IsTrue(hand.All(card => !card.gameObject.activeSelf),
|
||
"Hand cards should be hidden");
|
||
|
||
board.SetHandVisible(true);
|
||
Assert.IsTrue(hand.All(card => card.gameObject.activeSelf),
|
||
"Hand cards should be visible again");
|
||
}
|
||
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>(),
|
||
};
|
||
}
|
||
}
|
||
}
|