Add phase controls, trigger zone, card stats and combat fixes
CI / test (push) Has been cancelled
Details
CI / test (push) Has been cancelled
Details
This commit is contained in:
parent
e3243e58b6
commit
fdf88894e7
|
|
@ -31,6 +31,7 @@ namespace Vanguard.Board3D
|
||||||
Deck,
|
Deck,
|
||||||
Drop,
|
Drop,
|
||||||
Hand,
|
Hand,
|
||||||
|
Trigger,
|
||||||
Bind,
|
Bind,
|
||||||
GenerationBreak,
|
GenerationBreak,
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +157,16 @@ namespace Vanguard.Board3D
|
||||||
card != null ? card.PrintingId : null,
|
card != null ? card.PrintingId : null,
|
||||||
faceUp);
|
faceUp);
|
||||||
|
|
||||||
|
public static BoardCardRef Trigger(int player, int index, CardView card) =>
|
||||||
|
new BoardCardRef(
|
||||||
|
BoardZone.Trigger,
|
||||||
|
player,
|
||||||
|
-6,
|
||||||
|
index,
|
||||||
|
card != null ? card.InstanceId : 0,
|
||||||
|
card != null ? card.PrintingId : null,
|
||||||
|
true);
|
||||||
|
|
||||||
public static BoardCardRef Pile(BoardZone zone, int player, int count) =>
|
public static BoardCardRef Pile(BoardZone zone, int player, int count) =>
|
||||||
new BoardCardRef(zone, player, -5, count, 0, null, false);
|
new BoardCardRef(zone, player, -5, count, 0, null, false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,14 @@ namespace Vanguard.Board3D
|
||||||
public Vector3 CirclePositionForPlayer(int player, int selfPlayer, int slot) =>
|
public Vector3 CirclePositionForPlayer(int player, int selfPlayer, int slot) =>
|
||||||
CirclePosition(ToLocalSeat(player, selfPlayer), slot);
|
CirclePosition(ToLocalSeat(player, selfPlayer), slot);
|
||||||
|
|
||||||
/// <summary>Круги урона: две колонки по три карты на боку.</summary>
|
/// <summary>Урон — один вертикальный ряд, карты на боку.</summary>
|
||||||
public Vector3 DamagePosition(int localSeat, int index)
|
public Vector3 DamagePosition(int localSeat, int index)
|
||||||
{
|
{
|
||||||
var zSign = ZSign(localSeat);
|
var zSign = ZSign(localSeat);
|
||||||
var sideSign = localSeat == 0 ? -1f : 1f;
|
var sideSign = localSeat == 0 ? -1f : 1f;
|
||||||
var innerSign = localSeat == 0 ? -1f : 1f;
|
var x = sideSign * _config.damageX;
|
||||||
var xCenter = sideSign * _config.damageX;
|
var z = zSign * (_config.frontRowZ + 0.4f)
|
||||||
var x = xCenter + (index % 2 == 0 ? innerSign : -innerSign) * 0.42f;
|
+ zSign * index * 0.4f;
|
||||||
var z = zSign * (_config.frontRowZ + 0.45f)
|
|
||||||
+ zSign * (index / 2) * 0.55f;
|
|
||||||
return new Vector3(x, _config.DamageCenterY, z);
|
return new Vector3(x, _config.DamageCenterY, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,6 +87,16 @@ namespace Vanguard.Board3D
|
||||||
return new Vector3(x, _config.pileLift, z);
|
return new Vector3(x, _config.pileLift, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Зона проверки триггера — над колодой, ближе к центру стола.</summary>
|
||||||
|
public Vector3 TriggerPosition(int localSeat, int index, int count)
|
||||||
|
{
|
||||||
|
var x = (localSeat == 0 ? 1f : -1f) * _config.pileX;
|
||||||
|
var offset = index - (Mathf.Max(1, count) - 1) * 0.5f;
|
||||||
|
// «Над колодой» — дальше от камеры, то есть выше на экране.
|
||||||
|
var z = (ZSign(localSeat) * _config.frontRowZ) + 1.0f;
|
||||||
|
return new Vector3(x + offset * 0.42f, _config.cardLift, z);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Душа — визуальная стопка под кругом V, чуть ближе к игроку.</summary>
|
/// <summary>Душа — визуальная стопка под кругом V, чуть ближе к игроку.</summary>
|
||||||
public Vector3 SoulPosition(int localSeat)
|
public Vector3 SoulPosition(int localSeat)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ namespace Vanguard.Board3D
|
||||||
Card("BT01/011EN", 41),
|
Card("BT01/011EN", 41),
|
||||||
Card("BT01/012EN", 42),
|
Card("BT01/012EN", 42),
|
||||||
},
|
},
|
||||||
|
Trigger = new List<CardView> { Card("BT01/090EN", 61) },
|
||||||
DeckCount = 38,
|
DeckCount = 38,
|
||||||
Drop = new List<CardView> { Card("BT01/013EN", 51) },
|
Drop = new List<CardView> { Card("BT01/013EN", 51) },
|
||||||
};
|
};
|
||||||
|
|
@ -177,6 +178,7 @@ namespace Vanguard.Board3D
|
||||||
Soul = new List<CardView> { Card("BT01/021EN", 131) },
|
Soul = new List<CardView> { Card("BT01/021EN", 131) },
|
||||||
FaceDownDamage = 1,
|
FaceDownDamage = 1,
|
||||||
FaceUpDamage = new List<CardView> { Card("BT01/022EN", 141) },
|
FaceUpDamage = new List<CardView> { Card("BT01/022EN", 141) },
|
||||||
|
Trigger = new List<CardView> { Card("BT01/091EN", 161) },
|
||||||
DeckCount = 40,
|
DeckCount = 40,
|
||||||
Drop = new List<CardView>(),
|
Drop = new List<CardView>(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ namespace Vanguard.Board3D
|
||||||
private MeshRenderer _highlight;
|
private MeshRenderer _highlight;
|
||||||
private BoxCollider _collider;
|
private BoxCollider _collider;
|
||||||
private MaterialPropertyBlock _block;
|
private MaterialPropertyBlock _block;
|
||||||
|
private TextMesh _stats;
|
||||||
|
|
||||||
private bool _faceUp = true;
|
private bool _faceUp = true;
|
||||||
private bool _rested;
|
private bool _rested;
|
||||||
|
|
@ -36,6 +37,9 @@ namespace Vanguard.Board3D
|
||||||
public Vector3 TargetPosition => _targetPosition;
|
public Vector3 TargetPosition => _targetPosition;
|
||||||
public float TargetYaw => _targetRotation.eulerAngles.y;
|
public float TargetYaw => _targetRotation.eulerAngles.y;
|
||||||
public Quaternion TargetRotation => _targetRotation;
|
public Quaternion TargetRotation => _targetRotation;
|
||||||
|
public string StatsText => _stats != null && _stats.gameObject.activeSelf
|
||||||
|
? _stats.text
|
||||||
|
: null;
|
||||||
|
|
||||||
public static Card3D Create(Transform parent, PlaymatLayoutConfig config)
|
public static Card3D Create(Transform parent, PlaymatLayoutConfig config)
|
||||||
{
|
{
|
||||||
|
|
@ -95,6 +99,14 @@ namespace Vanguard.Board3D
|
||||||
BoardMaterials.SetShadows(_highlight, false);
|
BoardMaterials.SetShadows(_highlight, false);
|
||||||
_highlight.gameObject.SetActive(false);
|
_highlight.gameObject.SetActive(false);
|
||||||
|
|
||||||
|
_stats = BoardText.Create(
|
||||||
|
transform,
|
||||||
|
string.Empty,
|
||||||
|
0.028f,
|
||||||
|
Color.white,
|
||||||
|
Vector3.zero);
|
||||||
|
_stats.gameObject.SetActive(false);
|
||||||
|
|
||||||
_collider = gameObject.AddComponent<BoxCollider>();
|
_collider = gameObject.AddComponent<BoxCollider>();
|
||||||
_collider.size = new Vector3(
|
_collider.size = new Vector3(
|
||||||
width * 1.02f,
|
width * 1.02f,
|
||||||
|
|
@ -126,6 +138,37 @@ namespace Vanguard.Board3D
|
||||||
_front.SetPropertyBlock(_block);
|
_front.SetPropertyBlock(_block);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetStats(string text, bool modified)
|
||||||
|
{
|
||||||
|
if (_stats == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
_stats.gameObject.SetActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stats.text = text;
|
||||||
|
_stats.color = modified
|
||||||
|
? new Color(1f, 0.82f, 0.28f)
|
||||||
|
: new Color(0.92f, 0.95f, 1f, 0.92f);
|
||||||
|
_stats.gameObject.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LateUpdate()
|
||||||
|
{
|
||||||
|
if (_stats == null || !_stats.gameObject.activeSelf)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var position = transform.position;
|
||||||
|
_stats.transform.position = new Vector3(
|
||||||
|
position.x,
|
||||||
|
position.y + 0.015f,
|
||||||
|
position.z - (_config.cardHeight * 0.62f));
|
||||||
|
_stats.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
public void SetState(
|
public void SetState(
|
||||||
Quaternion baseRotation,
|
Quaternion baseRotation,
|
||||||
bool faceUp,
|
bool faceUp,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ namespace Vanguard.Board3D
|
||||||
private readonly Stack<Card3D> _pool = new Stack<Card3D>();
|
private readonly Stack<Card3D> _pool = new Stack<Card3D>();
|
||||||
private readonly List<Pile3D> _piles = new List<Pile3D>();
|
private readonly List<Pile3D> _piles = new List<Pile3D>();
|
||||||
private readonly BoardZoneView[] _damageZones = new BoardZoneView[2];
|
private readonly BoardZoneView[] _damageZones = new BoardZoneView[2];
|
||||||
|
private readonly BoardZoneView[] _triggerZones = new BoardZoneView[2];
|
||||||
private readonly HashSet<BoardSlotKey> _activeHighlights = new HashSet<BoardSlotKey>();
|
private readonly HashSet<BoardSlotKey> _activeHighlights = new HashSet<BoardSlotKey>();
|
||||||
private readonly HashSet<BoardSlotKey> _validDropSlots = new HashSet<BoardSlotKey>();
|
private readonly HashSet<BoardSlotKey> _validDropSlots = new HashSet<BoardSlotKey>();
|
||||||
private readonly List<HandCardBinding> _handBindings = new List<HandCardBinding>();
|
private readonly List<HandCardBinding> _handBindings = new List<HandCardBinding>();
|
||||||
|
|
@ -50,8 +51,6 @@ namespace Vanguard.Board3D
|
||||||
private Pile3D _deckOpponent;
|
private Pile3D _deckOpponent;
|
||||||
private Pile3D _dropSelf;
|
private Pile3D _dropSelf;
|
||||||
private Pile3D _dropOpponent;
|
private Pile3D _dropOpponent;
|
||||||
private Pile3D _soulSelf;
|
|
||||||
private Pile3D _soulOpponent;
|
|
||||||
|
|
||||||
private enum PointerMode
|
private enum PointerMode
|
||||||
{
|
{
|
||||||
|
|
@ -76,6 +75,7 @@ namespace Vanguard.Board3D
|
||||||
public BoardArtProvider ArtProvider { get; set; }
|
public BoardArtProvider ArtProvider { get; set; }
|
||||||
|
|
||||||
public event Action<BoardCardRef> CardClicked;
|
public event Action<BoardCardRef> CardClicked;
|
||||||
|
public event Action<BoardCardRef> CardRightClicked;
|
||||||
public event Action<BoardCardRef> SlotClicked;
|
public event Action<BoardCardRef> SlotClicked;
|
||||||
public event Action<BoardCardRef, BoardCardRef> CardDropped;
|
public event Action<BoardCardRef, BoardCardRef> CardDropped;
|
||||||
public event Action<BoardCardRef> PileClicked;
|
public event Action<BoardCardRef> PileClicked;
|
||||||
|
|
@ -137,10 +137,8 @@ namespace Vanguard.Board3D
|
||||||
|
|
||||||
_deckSelf.SetPlayer(LocalToPlayer(0));
|
_deckSelf.SetPlayer(LocalToPlayer(0));
|
||||||
_dropSelf.SetPlayer(LocalToPlayer(0));
|
_dropSelf.SetPlayer(LocalToPlayer(0));
|
||||||
_soulSelf.SetPlayer(LocalToPlayer(0));
|
|
||||||
_deckOpponent.SetPlayer(LocalToPlayer(1));
|
_deckOpponent.SetPlayer(LocalToPlayer(1));
|
||||||
_dropOpponent.SetPlayer(LocalToPlayer(1));
|
_dropOpponent.SetPlayer(LocalToPlayer(1));
|
||||||
_soulOpponent.SetPlayer(LocalToPlayer(1));
|
|
||||||
_hasRendered = false;
|
_hasRendered = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,6 +206,7 @@ namespace Vanguard.Board3D
|
||||||
spec.DamageLayout,
|
spec.DamageLayout,
|
||||||
spec.Scale);
|
spec.Scale);
|
||||||
card.SetHighlight(spec.Highlight);
|
card.SetHighlight(spec.Highlight);
|
||||||
|
card.SetStats(spec.Stats, spec.StatsModified);
|
||||||
|
|
||||||
if (spec.IsHand)
|
if (spec.IsHand)
|
||||||
{
|
{
|
||||||
|
|
@ -356,9 +355,9 @@ namespace Vanguard.Board3D
|
||||||
new Vector3(
|
new Vector3(
|
||||||
sideSign * (_config.damageX + 0.02f),
|
sideSign * (_config.damageX + 0.02f),
|
||||||
0.011f,
|
0.011f,
|
||||||
zSign * (_config.frontRowZ + 1.15f)),
|
zSign * (_config.frontRowZ + 1.3f)),
|
||||||
1.72f,
|
1.15f,
|
||||||
2.25f,
|
2.6f,
|
||||||
Mathf.Max(0.018f, _config.cardWidth * 0.03f),
|
Mathf.Max(0.018f, _config.cardWidth * 0.03f),
|
||||||
_config.damageColor,
|
_config.damageColor,
|
||||||
null,
|
null,
|
||||||
|
|
@ -367,6 +366,21 @@ namespace Vanguard.Board3D
|
||||||
new Color(1f, 0.78f, 0.76f, 0.75f),
|
new Color(1f, 0.78f, 0.76f, 0.75f),
|
||||||
_config.highlightColor);
|
_config.highlightColor);
|
||||||
_damageZones[localSeat] = zone;
|
_damageZones[localSeat] = zone;
|
||||||
|
|
||||||
|
var triggerPosition = _layout.TriggerPosition(localSeat, 0, 1);
|
||||||
|
_triggerZones[localSeat] = BoardZoneView.Create(
|
||||||
|
_zonesRoot,
|
||||||
|
"Trigger Zone " + localSeat,
|
||||||
|
new Vector3(triggerPosition.x, 0.011f, triggerPosition.z),
|
||||||
|
1.25f,
|
||||||
|
1.05f,
|
||||||
|
Mathf.Max(0.016f, _config.cardWidth * 0.025f),
|
||||||
|
new Color(0.30f, 0.18f, 0.52f),
|
||||||
|
"TRIGGER",
|
||||||
|
0.04f,
|
||||||
|
zSign * 0.72f,
|
||||||
|
new Color(0.82f, 0.76f, 1f, 0.75f),
|
||||||
|
_config.highlightColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,8 +393,6 @@ namespace Vanguard.Board3D
|
||||||
_deckOpponent = CreatePile(BoardZone.Deck, 1, 1, "DECK", new Color(0.60f, 0.26f, 0.26f));
|
_deckOpponent = CreatePile(BoardZone.Deck, 1, 1, "DECK", new Color(0.60f, 0.26f, 0.26f));
|
||||||
_dropSelf = CreatePile(BoardZone.Drop, 0, 0, "DROP", new Color(0.30f, 0.30f, 0.40f));
|
_dropSelf = CreatePile(BoardZone.Drop, 0, 0, "DROP", new Color(0.30f, 0.30f, 0.40f));
|
||||||
_dropOpponent = CreatePile(BoardZone.Drop, 1, 1, "DROP", new Color(0.40f, 0.28f, 0.28f));
|
_dropOpponent = CreatePile(BoardZone.Drop, 1, 1, "DROP", new Color(0.40f, 0.28f, 0.28f));
|
||||||
_soulSelf = CreatePile(BoardZone.Soul, 0, 0, "SOUL", _config.soulColor);
|
|
||||||
_soulOpponent = CreatePile(BoardZone.Soul, 1, 1, "SOUL", _config.soulColor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pile3D CreatePile(
|
private Pile3D CreatePile(
|
||||||
|
|
@ -452,6 +464,7 @@ namespace Vanguard.Board3D
|
||||||
AddFieldSpecs(player, localSeat, state, specs);
|
AddFieldSpecs(player, localSeat, state, specs);
|
||||||
AddGuardianSpecs(view, player, localSeat, specs);
|
AddGuardianSpecs(view, player, localSeat, specs);
|
||||||
AddDamageSpecs(player, localSeat, state, specs);
|
AddDamageSpecs(player, localSeat, state, specs);
|
||||||
|
AddTriggerSpecs(player, localSeat, specs);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddHandSpecs(view, state, specs);
|
AddHandSpecs(view, state, specs);
|
||||||
|
|
@ -494,6 +507,9 @@ namespace Vanguard.Board3D
|
||||||
// Карты в кругах V/R всегда открыты, независимо от снимка.
|
// Карты в кругах V/R всегда открыты, независимо от снимка.
|
||||||
FaceUp = true,
|
FaceUp = true,
|
||||||
Rested = card.Rested,
|
Rested = card.Rested,
|
||||||
|
Stats = "P" + card.Power + " C" + card.Critical,
|
||||||
|
StatsModified = card.Power != card.BasePower
|
||||||
|
|| card.Critical != card.BaseCritical,
|
||||||
Highlight = state.Highlights.Contains(key)
|
Highlight = state.Highlights.Contains(key)
|
||||||
? _config.highlightColor
|
? _config.highlightColor
|
||||||
: (Color?)null,
|
: (Color?)null,
|
||||||
|
|
@ -581,6 +597,33 @@ namespace Vanguard.Board3D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddTriggerSpecs(
|
||||||
|
PlayerView player,
|
||||||
|
int localSeat,
|
||||||
|
List<CardSpec> specs)
|
||||||
|
{
|
||||||
|
var triggers = player.Trigger ?? new List<CardView>();
|
||||||
|
if (triggers.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var count = Mathf.Min(3, triggers.Count);
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var card = triggers[i];
|
||||||
|
specs.Add(new CardSpec
|
||||||
|
{
|
||||||
|
Key = "T:" + player.Index + ":" + card.InstanceId,
|
||||||
|
Ref = BoardCardRef.Trigger(player.Index, i, card),
|
||||||
|
Art = ResolveArt(card.PrintingId),
|
||||||
|
Position = _layout.TriggerPosition(localSeat, i, count),
|
||||||
|
// Карты проверки всегда развёрнуты к локальному игроку.
|
||||||
|
BaseRotation = Quaternion.identity,
|
||||||
|
FaceUp = true,
|
||||||
|
Rested = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Color? DamageHighlight(BoardRenderState state, int index)
|
private Color? DamageHighlight(BoardRenderState state, int index)
|
||||||
{
|
{
|
||||||
if (!state.DamageChoice && !state.AbilityDamageTargeting)
|
if (!state.DamageChoice && !state.AbilityDamageTargeting)
|
||||||
|
|
@ -720,8 +763,8 @@ namespace Vanguard.Board3D
|
||||||
var self = view.Players.FirstOrDefault(player => player.Index == view.You);
|
var self = view.Players.FirstOrDefault(player => player.Index == view.You);
|
||||||
var opponent = view.Players.FirstOrDefault(player => player.Index != view.You);
|
var opponent = view.Players.FirstOrDefault(player => player.Index != view.You);
|
||||||
|
|
||||||
UpdatePileSide(self, _deckSelf, _dropSelf, _soulSelf);
|
UpdatePileSide(self, _deckSelf, _dropSelf);
|
||||||
UpdatePileSide(opponent, _deckOpponent, _dropOpponent, _soulOpponent);
|
UpdatePileSide(opponent, _deckOpponent, _dropOpponent);
|
||||||
|
|
||||||
_damageZones[0]?.SetLabel(string.Empty);
|
_damageZones[0]?.SetLabel(string.Empty);
|
||||||
_damageZones[1]?.SetLabel(string.Empty);
|
_damageZones[1]?.SetLabel(string.Empty);
|
||||||
|
|
@ -733,8 +776,7 @@ namespace Vanguard.Board3D
|
||||||
private void UpdatePileSide(
|
private void UpdatePileSide(
|
||||||
PlayerView player,
|
PlayerView player,
|
||||||
Pile3D deck,
|
Pile3D deck,
|
||||||
Pile3D drop,
|
Pile3D drop)
|
||||||
Pile3D soul)
|
|
||||||
{
|
{
|
||||||
if (deck == null)
|
if (deck == null)
|
||||||
return;
|
return;
|
||||||
|
|
@ -743,7 +785,6 @@ namespace Vanguard.Board3D
|
||||||
{
|
{
|
||||||
deck.SetCount(0, null, "DECK", Color.white);
|
deck.SetCount(0, null, "DECK", Color.white);
|
||||||
drop.SetCount(0, null, "DROP", Color.white);
|
drop.SetCount(0, null, "DROP", Color.white);
|
||||||
soul.SetCount(0, null, "SOUL", _config.soulColor);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -761,15 +802,6 @@ namespace Vanguard.Board3D
|
||||||
topDrop != null ? ResolveArt(topDrop.PrintingId).Front : null,
|
topDrop != null ? ResolveArt(topDrop.PrintingId).Front : null,
|
||||||
"DROP",
|
"DROP",
|
||||||
new Color(0.30f, 0.30f, 0.40f));
|
new Color(0.30f, 0.30f, 0.40f));
|
||||||
|
|
||||||
var topSoul = player.Soul != null && player.Soul.Count > 0
|
|
||||||
? player.Soul[player.Soul.Count - 1]
|
|
||||||
: null;
|
|
||||||
soul.SetCount(
|
|
||||||
player.Soul?.Count ?? 0,
|
|
||||||
topSoul != null ? ResolveArt(topSoul.PrintingId).Front : null,
|
|
||||||
"SOUL",
|
|
||||||
_config.soulColor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSlots(BoardRenderState state)
|
private void UpdateSlots(BoardRenderState state)
|
||||||
|
|
@ -827,9 +859,16 @@ namespace Vanguard.Board3D
|
||||||
}
|
}
|
||||||
|
|
||||||
var ray = GetRay(mouse.position.ReadValue());
|
var ray = GetRay(mouse.position.ReadValue());
|
||||||
if (!TryRaycast(ray, out _, out _, out var pile) || pile == null)
|
if (!TryRaycast(ray, out var card, out _, out var pile))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (card != null)
|
||||||
|
{
|
||||||
|
CardRightClicked?.Invoke(card.Ref);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pile != null)
|
||||||
PileRightClicked?.Invoke(pile.Ref);
|
PileRightClicked?.Invoke(pile.Ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1249,6 +1288,8 @@ namespace Vanguard.Board3D
|
||||||
public bool IsHand;
|
public bool IsHand;
|
||||||
public int HandIndex;
|
public int HandIndex;
|
||||||
public int HandCount;
|
public int HandCount;
|
||||||
|
public string Stats;
|
||||||
|
public bool StatsModified;
|
||||||
public Color? Highlight;
|
public Color? Highlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,18 @@ namespace Vanguard.Board3D
|
||||||
{
|
{
|
||||||
_count = count;
|
_count = count;
|
||||||
var safe = Mathf.Max(0, count);
|
var safe = Mathf.Max(0, count);
|
||||||
var layers = safe <= 0 ? 1 : safe < 5 ? 1 : safe < 20 ? 2 : 3;
|
if (safe == 0)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _layers.Length; i++)
|
||||||
|
_layers[i].SetActive(false);
|
||||||
|
_top.gameObject.SetActive(false);
|
||||||
|
if (_label != null)
|
||||||
|
BoardText.SetText(_label, title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_top.gameObject.SetActive(true);
|
||||||
|
var layers = safe < 5 ? 1 : safe < 20 ? 2 : 3;
|
||||||
if (Zone == BoardZone.Soul)
|
if (Zone == BoardZone.Soul)
|
||||||
layers = safe < 10 ? 1 : 2;
|
layers = safe < 10 ? 1 : 2;
|
||||||
var thickness = Mathf.Max(0.006f, _config.cardThickness * 1.2f);
|
var thickness = Mathf.Max(0.006f, _config.cardThickness * 1.2f);
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,11 @@ namespace Vanguard.UI
|
||||||
private Text _soulTitle;
|
private Text _soulTitle;
|
||||||
private Text _selfStatsText;
|
private Text _selfStatsText;
|
||||||
private Text _opponentStatsText;
|
private Text _opponentStatsText;
|
||||||
|
private RectTransform _phasePanel;
|
||||||
|
private bool _autoFinishMulligan;
|
||||||
|
private float _autoFinishMulliganAt;
|
||||||
|
private bool _autoEndBattleArmed;
|
||||||
|
private float _autoEndBattleAt;
|
||||||
|
|
||||||
public BattleView(Transform parent, MetaService meta, Action onExit)
|
public BattleView(Transform parent, MetaService meta, Action onExit)
|
||||||
{
|
{
|
||||||
|
|
@ -183,10 +188,12 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
BuildSoulOverlay();
|
BuildSoulOverlay();
|
||||||
BuildStatsOverlays();
|
BuildStatsOverlays();
|
||||||
|
BuildPhasePanel();
|
||||||
|
|
||||||
_board3D = GameBoard3D.Create(null, PlaymatLayoutConfig.Default);
|
_board3D = GameBoard3D.Create(null, PlaymatLayoutConfig.Default);
|
||||||
_board3D.ArtProvider = ResolveBoardArt;
|
_board3D.ArtProvider = ResolveBoardArt;
|
||||||
_board3D.CardClicked += OnBoard3DCardClicked;
|
_board3D.CardClicked += OnBoard3DCardClicked;
|
||||||
|
_board3D.CardRightClicked += OnBoard3DCardRightClicked;
|
||||||
_board3D.SlotClicked += OnBoard3DSlotClicked;
|
_board3D.SlotClicked += OnBoard3DSlotClicked;
|
||||||
_board3D.CardDropped += OnBoard3DCardDropped;
|
_board3D.CardDropped += OnBoard3DCardDropped;
|
||||||
_board3D.PileClicked += OnBoard3DPileClicked;
|
_board3D.PileClicked += OnBoard3DPileClicked;
|
||||||
|
|
@ -214,6 +221,10 @@ namespace Vanguard.UI
|
||||||
_pendingBoosterSlot = -1;
|
_pendingBoosterSlot = -1;
|
||||||
_triggerPowerSlot = null;
|
_triggerPowerSlot = null;
|
||||||
_showAllIntents = false;
|
_showAllIntents = false;
|
||||||
|
_autoFinishMulligan = false;
|
||||||
|
_autoFinishMulliganAt = 0f;
|
||||||
|
_autoEndBattleArmed = false;
|
||||||
|
_autoEndBattleAt = 0f;
|
||||||
_logLines.Clear();
|
_logLines.Clear();
|
||||||
RebuildLog();
|
RebuildLog();
|
||||||
_feedbackQueue.Clear();
|
_feedbackQueue.Clear();
|
||||||
|
|
@ -222,7 +233,6 @@ namespace Vanguard.UI
|
||||||
_feedback.gameObject.SetActive(false);
|
_feedback.gameObject.SetActive(false);
|
||||||
_flash.gameObject.SetActive(false);
|
_flash.gameObject.SetActive(false);
|
||||||
_skipFeedback.gameObject.SetActive(false);
|
_skipFeedback.gameObject.SetActive(false);
|
||||||
_highlightSlots.Clear();
|
|
||||||
_board3D?.Clear();
|
_board3D?.Clear();
|
||||||
_board3D?.SetVisible(true);
|
_board3D?.SetVisible(true);
|
||||||
if (_soulOverlay != null)
|
if (_soulOverlay != null)
|
||||||
|
|
@ -272,6 +282,71 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
UpdateClock();
|
UpdateClock();
|
||||||
UpdateFeedback();
|
UpdateFeedback();
|
||||||
|
UpdateAutoPhases();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAutoPhases()
|
||||||
|
{
|
||||||
|
if (_view == null || _sending)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var legal = _view.LegalIntents ?? new List<IntentPayload>();
|
||||||
|
|
||||||
|
if (_autoFinishMulligan)
|
||||||
|
{
|
||||||
|
if (_view.Phase != GamePhase.Mulligan)
|
||||||
|
{
|
||||||
|
_autoFinishMulligan = false;
|
||||||
|
_autoFinishMulliganAt = 0f;
|
||||||
|
}
|
||||||
|
else if (_view.ActivePlayer == _view.You)
|
||||||
|
{
|
||||||
|
var finish = legal.OfType<FinishMulliganIntent>().FirstOrDefault();
|
||||||
|
if (finish != null)
|
||||||
|
{
|
||||||
|
if (_autoFinishMulliganAt <= 0f)
|
||||||
|
{
|
||||||
|
_autoFinishMulliganAt = Time.unscaledTime + 0.6f;
|
||||||
|
}
|
||||||
|
else if (Time.unscaledTime >= _autoFinishMulliganAt)
|
||||||
|
{
|
||||||
|
_autoFinishMulligan = false;
|
||||||
|
_autoFinishMulliganAt = 0f;
|
||||||
|
OnIntentClicked(finish);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var battle = _view.Phase == GamePhase.Battle
|
||||||
|
&& _view.ActivePlayer == _view.You
|
||||||
|
&& !_view.IsFinished;
|
||||||
|
var actionable = legal.Any(intent =>
|
||||||
|
intent is AttackIntent
|
||||||
|
or ActivateAbilityIntent
|
||||||
|
or ResolveAbilityIntent
|
||||||
|
or ResolveTriggerIntent
|
||||||
|
or SelectTriggerIntent
|
||||||
|
or ChooseIntent);
|
||||||
|
var endTurn = legal.OfType<EndTurnIntent>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (battle && !actionable && endTurn != null && _pendingIntents.Count == 0)
|
||||||
|
{
|
||||||
|
if (!_autoEndBattleArmed)
|
||||||
|
{
|
||||||
|
_autoEndBattleArmed = true;
|
||||||
|
_autoEndBattleAt = Time.unscaledTime + 1.0f;
|
||||||
|
}
|
||||||
|
else if (Time.unscaledTime >= _autoEndBattleAt && !_feedbackPlaying)
|
||||||
|
{
|
||||||
|
_autoEndBattleArmed = false;
|
||||||
|
OnIntentClicked(endTurn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_autoEndBattleArmed = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowFeedback(string text, Color color, float duration = 1.25f, bool flash = false)
|
private void ShowFeedback(string text, Color color, float duration = 1.25f, bool flash = false)
|
||||||
|
|
@ -529,40 +604,46 @@ namespace Vanguard.UI
|
||||||
case AttackDeclaredView attack:
|
case AttackDeclaredView attack:
|
||||||
ShowFeedback("АТАКА " + SlotName(attack.AttackerSlot)
|
ShowFeedback("АТАКА " + SlotName(attack.AttackerSlot)
|
||||||
+ " → " + SlotName(attack.TargetSlot),
|
+ " → " + SlotName(attack.TargetSlot),
|
||||||
new Color(1f, 0.82f, 0.35f), 1.0f);
|
new Color(1f, 0.82f, 0.35f), 1.2f);
|
||||||
break;
|
break;
|
||||||
case GuardAddedView guard:
|
case GuardAddedView guard:
|
||||||
ShowFeedback("GUARD +" + guard.Shield,
|
ShowFeedback("GUARD " + CardName(guard.PrintingId)
|
||||||
new Color(0.45f, 0.82f, 1f), 1.0f);
|
+ " +" + guard.Shield,
|
||||||
|
new Color(0.45f, 0.82f, 1f), 1.9f);
|
||||||
break;
|
break;
|
||||||
case DriveCheckedView drive:
|
case DriveCheckedView drive:
|
||||||
ShowFeedback("DRIVE " + TriggerShort(drive.Trigger.ToString()),
|
ShowFeedback("DRIVE " + CardName(drive.PrintingId)
|
||||||
new Color(0.72f, 0.88f, 1f), 1.0f);
|
+ " · " + TriggerShort(drive.Trigger.ToString()),
|
||||||
|
new Color(0.72f, 0.88f, 1f), 1.5f);
|
||||||
break;
|
break;
|
||||||
case TriggerResolvedView trigger:
|
case TriggerResolvedView trigger:
|
||||||
ShowFeedback("TRIGGER " + trigger.Trigger,
|
ShowFeedback("TRIGGER " + CardName(trigger.PrintingId),
|
||||||
new Color(0.72f, 0.88f, 1f), 1.1f);
|
new Color(0.72f, 0.88f, 1f), 1.4f);
|
||||||
break;
|
break;
|
||||||
case BattleResolvedView battle:
|
case BattleResolvedView battle:
|
||||||
ShowFeedback(battle.Hit ? "HIT" : "BLOCK",
|
ShowFeedback(battle.Hit ? "HIT" : "BLOCK",
|
||||||
battle.Hit ? new Color(1f, 0.45f, 0.35f) : new Color(0.45f, 0.82f, 1f),
|
battle.Hit ? new Color(1f, 0.45f, 0.35f) : new Color(0.45f, 0.82f, 1f),
|
||||||
0.9f);
|
1.3f);
|
||||||
break;
|
break;
|
||||||
case DamageTakenView damage:
|
case DamageTakenView damage:
|
||||||
ShowFeedback("+" + damage.Amount + " DAMAGE",
|
ShowFeedback("+" + damage.Amount + " DAMAGE",
|
||||||
new Color(1f, 0.30f, 0.26f), 1.2f, flash: true);
|
new Color(1f, 0.30f, 0.26f), 1.6f, flash: true);
|
||||||
break;
|
break;
|
||||||
case HealedView:
|
case HealedView:
|
||||||
ShowFeedback("HEAL",
|
ShowFeedback("HEAL",
|
||||||
new Color(0.45f, 0.95f, 0.62f), 1.1f, flash: true);
|
new Color(0.45f, 0.95f, 0.62f), 1.4f, flash: true);
|
||||||
break;
|
break;
|
||||||
case UnitRetiredView:
|
case UnitRetiredView:
|
||||||
ShowFeedback("RETIRE",
|
ShowFeedback("RETIRE",
|
||||||
new Color(1f, 0.55f, 0.30f), 1.0f);
|
new Color(1f, 0.55f, 0.30f), 1.2f);
|
||||||
break;
|
break;
|
||||||
case PerfectGuardView:
|
case InterceptedView intercept:
|
||||||
ShowFeedback("PERFECT GUARD",
|
ShowFeedback("INTERCEPT " + CardName(intercept.PrintingId),
|
||||||
new Color(0.60f, 0.82f, 1f), 1.1f);
|
new Color(0.55f, 0.82f, 1f), 1.9f);
|
||||||
|
break;
|
||||||
|
case PerfectGuardView perfect:
|
||||||
|
ShowFeedback("PERFECT GUARD " + CardName(perfect.SentinelPrintingId),
|
||||||
|
new Color(0.60f, 0.82f, 1f), 2.0f);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -672,6 +753,13 @@ namespace Vanguard.UI
|
||||||
_highlightSlots.Clear();
|
_highlightSlots.Clear();
|
||||||
_pendingBoosterSlot = -1;
|
_pendingBoosterSlot = -1;
|
||||||
_triggerPowerSlot = null;
|
_triggerPowerSlot = null;
|
||||||
|
if (intent is FinishMulliganIntent or SkipRideIntent
|
||||||
|
or EnterBattleIntent or EndTurnIntent)
|
||||||
|
{
|
||||||
|
_autoFinishMulligan = false;
|
||||||
|
_autoFinishMulliganAt = 0f;
|
||||||
|
_autoEndBattleArmed = false;
|
||||||
|
}
|
||||||
_sending = true;
|
_sending = true;
|
||||||
SetStatus("Отправляем: " + Label(intent), Muted);
|
SetStatus("Отправляем: " + Label(intent), Muted);
|
||||||
|
|
||||||
|
|
@ -773,6 +861,7 @@ namespace Vanguard.UI
|
||||||
UpdateStatsOverlay();
|
UpdateStatsOverlay();
|
||||||
|
|
||||||
RebuildIntents(_view);
|
RebuildIntents(_view);
|
||||||
|
RebuildPhaseControls(_view);
|
||||||
UpdateClock();
|
UpdateClock();
|
||||||
|
|
||||||
if (_view.IsFinished)
|
if (_view.IsFinished)
|
||||||
|
|
@ -997,6 +1086,15 @@ namespace Vanguard.UI
|
||||||
ShowSoul(pile.Player);
|
ShowSoul(pile.Player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnBoard3DCardRightClicked(BoardCardRef card)
|
||||||
|
{
|
||||||
|
if (_view == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (card.Zone == BoardZone.Vanguard)
|
||||||
|
ShowSoul(card.Player);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnBoard3DPileClicked(BoardCardRef pile)
|
private void OnBoard3DPileClicked(BoardCardRef pile)
|
||||||
{
|
{
|
||||||
if (_view == null)
|
if (_view == null)
|
||||||
|
|
@ -1022,9 +1120,80 @@ namespace Vanguard.UI
|
||||||
SetStatus("В колоде " + player.DeckCount + " карт.", Muted);
|
SetStatus("В колоде " + player.DeckCount + " карт.", Muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildStatsOverlays()
|
private void BuildPhasePanel()
|
||||||
{
|
{
|
||||||
_selfStatsText = CreateStatsPanel(
|
_phasePanel = UiKit.CreatePanel(
|
||||||
|
_root,
|
||||||
|
"PhasePanel",
|
||||||
|
new Color(0.02f, 0.035f, 0.055f, 0.84f));
|
||||||
|
_phasePanel.anchorMin = new Vector2(1f, 0.5f);
|
||||||
|
_phasePanel.anchorMax = new Vector2(1f, 0.5f);
|
||||||
|
_phasePanel.pivot = new Vector2(1f, 0.5f);
|
||||||
|
_phasePanel.anchoredPosition = new Vector2(-468f, 0f);
|
||||||
|
_phasePanel.sizeDelta = new Vector2(156f, 188f);
|
||||||
|
UiKit.AddVerticalLayout(_phasePanel, 8, 6);
|
||||||
|
_phasePanel.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildPhaseControls(MatchView view)
|
||||||
|
{
|
||||||
|
if (_phasePanel == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ClearChildren(_phasePanel);
|
||||||
|
if (view == null || _pendingIntents.Count > 0)
|
||||||
|
{
|
||||||
|
_phasePanel.gameObject.SetActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var intents = (view.LegalIntents ?? new List<IntentPayload>())
|
||||||
|
.Where(IsPhaseIntent)
|
||||||
|
.ToList();
|
||||||
|
if (intents.Count == 0)
|
||||||
|
{
|
||||||
|
_phasePanel.gameObject.SetActive(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_phasePanel.gameObject.SetActive(true);
|
||||||
|
var title = UiKit.CreateText(
|
||||||
|
_phasePanel,
|
||||||
|
"PhaseTitle",
|
||||||
|
"ФАЗЫ",
|
||||||
|
14,
|
||||||
|
new Color(0.72f, 0.80f, 0.94f));
|
||||||
|
title.fontStyle = FontStyle.Bold;
|
||||||
|
UiKit.SetPreferredHeight(title, 22f);
|
||||||
|
|
||||||
|
foreach (var intent in intents)
|
||||||
|
{
|
||||||
|
var captured = intent;
|
||||||
|
var button = UiKit.CreateButton(
|
||||||
|
_phasePanel,
|
||||||
|
"Phase_" + captured.GetType().Name,
|
||||||
|
PhaseLabel(captured),
|
||||||
|
() => OnIntentClicked(captured),
|
||||||
|
14);
|
||||||
|
UiKit.SetPreferredHeight(button, 40f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPhaseIntent(IntentPayload intent) =>
|
||||||
|
intent is FinishMulliganIntent or SkipRideIntent
|
||||||
|
or EnterBattleIntent or EndTurnIntent;
|
||||||
|
|
||||||
|
private static string PhaseLabel(IntentPayload intent) => intent switch
|
||||||
|
{
|
||||||
|
FinishMulliganIntent => "Закончить муллиган",
|
||||||
|
SkipRideIntent => "Пропустить смену",
|
||||||
|
EnterBattleIntent => "В бой",
|
||||||
|
EndTurnIntent => "Закончить ход",
|
||||||
|
_ => intent.GetType().Name,
|
||||||
|
};
|
||||||
|
|
||||||
|
private void BuildStatsOverlays()
|
||||||
|
{ _selfStatsText = CreateStatsPanel(
|
||||||
"SelfStats",
|
"SelfStats",
|
||||||
new Vector2(0f, 0f),
|
new Vector2(0f, 0f),
|
||||||
new Vector2(0f, 0f),
|
new Vector2(0f, 0f),
|
||||||
|
|
@ -1316,6 +1485,8 @@ namespace Vanguard.UI
|
||||||
var indexes = _mulliganSelected.OrderBy(index => index).ToList();
|
var indexes = _mulliganSelected.OrderBy(index => index).ToList();
|
||||||
_mulliganSelected.Clear();
|
_mulliganSelected.Clear();
|
||||||
_mulliganUsed = true;
|
_mulliganUsed = true;
|
||||||
|
_autoFinishMulligan = true;
|
||||||
|
_autoFinishMulliganAt = 0f;
|
||||||
OnIntentClicked(new MulliganIntent { HandIndexes = indexes });
|
OnIntentClicked(new MulliganIntent { HandIndexes = indexes });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1651,7 +1822,9 @@ namespace Vanguard.UI
|
||||||
var legal = view.LegalIntents ?? new List<IntentPayload>();
|
var legal = view.LegalIntents ?? new List<IntentPayload>();
|
||||||
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
var playable = legal.Where(intent => intent is not ConcedeIntent).ToList();
|
||||||
var board = playable.Where(IsBoardIntent).ToList();
|
var board = playable.Where(IsBoardIntent).ToList();
|
||||||
var global = playable.Where(intent => !IsBoardIntent(intent)).ToList();
|
var global = playable
|
||||||
|
.Where(intent => !IsBoardIntent(intent) && !IsPhaseIntent(intent))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (_pendingIntents.Count > 0)
|
if (_pendingIntents.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ namespace Vanguard.Tests
|
||||||
&& card.Ref.Slot == 3);
|
&& card.Ref.Slot == 3);
|
||||||
Assert.IsTrue(rested.IsRested);
|
Assert.IsTrue(rested.IsRested);
|
||||||
Assert.AreEqual(90f, Mathf.Abs(Mathf.DeltaAngle(rested.TargetYaw, 0f)), 0.1f);
|
Assert.AreEqual(90f, Mathf.Abs(Mathf.DeltaAngle(rested.TargetYaw, 0f)), 0.1f);
|
||||||
|
Assert.AreEqual("P8000 C1", rested.StatsText);
|
||||||
|
|
||||||
var hiddenDamage = cards.Single(card =>
|
var hiddenDamage = cards.Single(card =>
|
||||||
card.Ref.Zone == BoardZone.Damage
|
card.Ref.Zone == BoardZone.Damage
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ namespace Vanguard.Tests
|
||||||
.ToList();
|
.ToList();
|
||||||
Assert.AreEqual(PlaymatLayout.DamageSlots,
|
Assert.AreEqual(PlaymatLayout.DamageSlots,
|
||||||
damage.Select(position => position.ToString()).Distinct().Count());
|
damage.Select(position => position.ToString()).Distinct().Count());
|
||||||
|
Assert.IsTrue(damage.All(position =>
|
||||||
|
Mathf.Abs(position.x - damage[0].x) < 0.001f));
|
||||||
|
|
||||||
var hand = Enumerable.Range(0, 5)
|
var hand = Enumerable.Range(0, 5)
|
||||||
.Select(index => layout.GetHandPose(index, 5).Position)
|
.Select(index => layout.GetHandPose(index, 5).Position)
|
||||||
|
|
@ -80,6 +82,18 @@ namespace Vanguard.Tests
|
||||||
Assert.IsTrue(hand.All(position => position.z < -4f));
|
Assert.IsTrue(hand.All(position => position.z < -4f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Trigger_Zone_Is_Above_Deck()
|
||||||
|
{
|
||||||
|
var config = PlaymatLayoutConfig.CreateDefault();
|
||||||
|
var layout = new PlaymatLayout(config);
|
||||||
|
|
||||||
|
var deck = layout.DeckPosition(0);
|
||||||
|
var trigger = layout.TriggerPosition(0, 0, 1);
|
||||||
|
Assert.Greater(trigger.z, deck.z);
|
||||||
|
Assert.AreEqual(deck.x, trigger.x, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Deck_Is_Closer_Than_Drop_After_Swap()
|
public void Deck_Is_Closer_Than_Drop_After_Swap()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -866,18 +866,26 @@ Battle; Boot и общие сервисы остаются персистент
|
||||||
- Статистика карт вынесена в отдельные UI-панели: своя — снизу слева,
|
- Статистика карт вынесена в отдельные UI-панели: своя — снизу слева,
|
||||||
противника — сверху справа (`Рука/Колода/Дроп/Урон/Душа`); 3D-подписи
|
противника — сверху справа (`Рука/Колода/Дроп/Урон/Душа`); 3D-подписи
|
||||||
стопок и зон больше не дублируют числа.
|
стопок и зон больше не дублируют числа.
|
||||||
- Колода и дроп поменяны местами: колода ближе к игроку. Верх дропа
|
- Колода и дроп поменяны местами: колода ближе к игроку. Пустой дроп не
|
||||||
показывает последнюю положенную карту, ЛКМ открывает список всего дропа.
|
рисует рубашку; верх дропа показывает последнюю положенную карту, ЛКМ
|
||||||
Душа открывается только ПКМ по стопке под V; кнопка души из топ-бара
|
открывает список всего дропа.
|
||||||
убрана.
|
- Душа — это юниты под картой VC, отдельной стопки на поле нет. ПКМ по
|
||||||
- Душа: клик по стопке или кнопка «Душа: n» в верхней панели открывает
|
VC открывает список души; кнопка души из топ-бара убрана.
|
||||||
оверлей со списком карт.
|
- Над каждой колодой — триггер-зона (`TRIGGER`), куда выкладывается карта
|
||||||
|
проверки эффекта/урона. Урон — один вертикальный ряд карт на боку.
|
||||||
|
Под картами V/R — динамические `P/C`, жёлтые при изменённой силе/крите.
|
||||||
|
- Справа от круга защиты — панель фаз: «Закончить муллиган» (авто после
|
||||||
|
обмена), «Пропустить смену», «В бой», «Закончить ход». Боевая фаза
|
||||||
|
завершается сама, если нет атак/активаций/выборов.
|
||||||
|
- Event-фидбек guard/drive/intercept показывает имя карты и держится
|
||||||
|
1.5–2 с; добавлены паузы, чтобы успеть прочитать защиту бота.
|
||||||
|
- Атака проходит при силе атакующего **>=** силы защищающегося.
|
||||||
- Песочница: `Assets/Vanguard/Scenes/GameBoard3D.unity` +
|
- Песочница: `Assets/Vanguard/Scenes/GameBoard3D.unity` +
|
||||||
`PlaymatSandbox`; собирается `Vanguard → Rebuild Playmat Sandbox`,
|
`PlaymatSandbox`; собирается `Vanguard → Rebuild Playmat Sandbox`,
|
||||||
проверяется `SceneSmoke.RunPlaymat`.
|
проверяется `SceneSmoke.RunPlaymat`.
|
||||||
- Проверки: EditMode **19/19** (геометрия, зеркальность, правила, рендер
|
- Проверки: EditMode **20/20** (геометрия, зеркальность, правила, рендер
|
||||||
3D-карт, пул, рука в поле зрения, обмен колоды/дропа),
|
3D-карт, пул, рука в поле зрения, обмен колоды/дропа, триггер-зона),
|
||||||
`SCENE_SMOKE success=True`,
|
`SCENE_SMOKE success=True`,
|
||||||
`PLAYMAT_SMOKE success=True cards=20 hand=5/5 fieldVisible=True`.
|
`PLAYMAT_SMOKE success=True cards=22 hand=5/5 fieldVisible=True`.
|
||||||
- Ограничения: retire/ghost-анимации старого 2D-поля пока не перенесены;
|
- Ограничения: retire/ghost-анимации старого 2D-поля пока не перенесены;
|
||||||
модели карт/стола — процедурные примитивы.
|
модели карт/стола — процедурные примитивы.
|
||||||
|
|
|
||||||
|
|
@ -69,17 +69,23 @@
|
||||||
общая для 2D и 3D. Рука закреплена в поле зрения камеры снизу по центру
|
общая для 2D и 3D. Рука закреплена в поле зрения камеры снизу по центру
|
||||||
и всегда смотрит на игрока, а не лежит на столе; уменьшена, чтобы не
|
и всегда смотрит на игрока, а не лежит на столе; уменьшена, чтобы не
|
||||||
закрывать задний ряд RC. Камера фиксирована над полем под небольшим
|
закрывать задний ряд RC. Камера фиксирована над полем под небольшим
|
||||||
углом и не двигается игроком. Драг-н-дроп на плоскость стола с
|
углом и не двигается игроком: `(1.4, 5.8, -8)`, цель `(1.4, 0, 0)`,
|
||||||
подсветкой целей и плавным снапом.
|
FOV 46. Драг-н-дроп на плоскость стола с подсветкой целей и плавным
|
||||||
|
снапом.
|
||||||
- Поле: у каждого игрока два ряда `R V R` / `R R R` (V + 5 RC), у
|
- Поле: у каждого игрока два ряда `R V R` / `R R R` (V + 5 RC), у
|
||||||
противника зеркально; между полями общий круг защиты; душа — стопка под
|
противника зеркально; между полями общий круг защиты; колода, дроп,
|
||||||
V; колода, дроп и урон — 3D-стопки/карты. Локальный игрок всегда снизу,
|
триггер-зона и урон — 3D-зоны. Локальный игрок всегда снизу, даже если
|
||||||
даже если он за вторым местом. Карты в кругах V/R принудительно
|
он за вторым местом. Карты в кругах V/R принудительно показываются
|
||||||
показываются лицом вверх.
|
лицом вверх; урон — один вертикальный ряд, карта входит открытой.
|
||||||
|
Над колодой — зона проверки триггера. Под картами V/R — динамические
|
||||||
|
`P/C`.
|
||||||
- Статистика карт — отдельное UI-наложение: своя панель снизу слева,
|
- Статистика карт — отдельное UI-наложение: своя панель снизу слева,
|
||||||
противника — сверху справа. Душа открывается только ПКМ по стопке под V;
|
противника — сверху справа. Душа — юниты под VC: отдельной стопки нет,
|
||||||
дроп показывает верхнюю карту, по ЛКМ — список всех карт дропа; колода и
|
ПКМ по VC открывает список. Дроп по умолчанию пуст; сверху показывает
|
||||||
дроп поменяны местами (колода ближе к игроку).
|
последнюю карту, ЛКМ — список всего дропа. Колода и дроп поменяны
|
||||||
|
местами (колода ближе к игроку). Справа от круга защиты — панель фаз:
|
||||||
|
конец муллигана (авто после обмена), скип смены, конец главной/боевой;
|
||||||
|
боевая завершается сама, если нет атак и активаций.
|
||||||
- Зона урона: карты лежат открытыми и на боку; после Counter Blast
|
- Зона урона: карты лежат открытыми и на боку; после Counter Blast
|
||||||
выбранная карта переворачивается рубашкой. Heal выбирает карту урона;
|
выбранная карта переворачивается рубашкой. Heal выбирает карту урона;
|
||||||
Counter Blast — открытую карту до активации способности.
|
Counter Blast — открытую карту до активации способности.
|
||||||
|
|
@ -87,8 +93,10 @@
|
||||||
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты.
|
колоды, добираются новые, затем shuffle. Клик по руке выбирает карты.
|
||||||
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
- Анимации: event-фидбек (атака, guard, drive, урон, retire, heal) поверх
|
||||||
стола, не блокирует ввод; очередь событий + кнопка «Пропустить анимации».
|
стола, не блокирует ввод; очередь событий + кнопка «Пропустить анимации».
|
||||||
3D-карты плавно снапятся, поворачиваются в Rest и переворачиваются;
|
Guard/drive/intercept показывают имя карты и держатся 1.5–2 с, чтобы
|
||||||
подсветка целей и точки броска обновляются на каждом снимке.
|
успеть прочитать защиту бота. 3D-карты плавно снапятся, поворачиваются
|
||||||
|
в Rest и переворачиваются; подсветка целей и точки броска обновляются
|
||||||
|
на каждом снимке.
|
||||||
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
|
- Реальные арты: `imageKey` из `/api/cards` + `CardImageService` ищет
|
||||||
`Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт:
|
`Resources/CardArt/{imageKey}`. Скачаны все 124 арта. Импорт:
|
||||||
`tools/import_card_art.py`.
|
`tools/import_card_art.py`.
|
||||||
|
|
@ -96,6 +104,7 @@
|
||||||
группировка действий (карты → цели, глобальные кнопки отдельно),
|
группировка действий (карты → цели, глобальные кнопки отдельно),
|
||||||
выбор силы/эффекта триггера, журнал последних событий. Позванные
|
выбор силы/эффекта триггера, журнал последних событий. Позванные
|
||||||
rear-guard'ы входят в стоячем положении; rest — только после атаки/буста.
|
rear-guard'ы входят в стоячем положении; rest — только после атаки/буста.
|
||||||
|
Атака проходит при силе атакующего **>=** силы защищающегося.
|
||||||
В первый ход атака запрещена; первый игрок определяется сидом.
|
В первый ход атака запрещена; первый игрок определяется сидом.
|
||||||
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
- Отдельная сцена `Collection` и `CollectionView` удалены; коллекция
|
||||||
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
||||||
|
|
@ -139,12 +148,12 @@ curl http://127.0.0.1:18080/health
|
||||||
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
-executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat
|
||||||
```
|
```
|
||||||
|
|
||||||
Клиентские проверки: **19/19** EditMode (сборка всех экранов, импорт арта,
|
Клиентские проверки: **20/20** EditMode (сборка всех экранов, импорт арта,
|
||||||
перевод статических/динамических строк, live-переключение языка, рендер
|
перевод статических/динамических строк, live-переключение языка, рендер
|
||||||
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
3D-доски с уроном и отдыхающей картой, геометрия поля, правила
|
||||||
перемещения, рука в поле зрения камеры, обмен колоды/дропа),
|
перемещения, рука в поле зрения камеры, обмен колоды/дропа, триггер-зона),
|
||||||
`SCENE_SMOKE success=True`,
|
`SCENE_SMOKE success=True`,
|
||||||
`PLAYMAT_SMOKE success=True cards=20 hand=5/5 fieldVisible=True`.
|
`PLAYMAT_SMOKE success=True cards=22 hand=5/5 fieldVisible=True`.
|
||||||
|
|
||||||
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
`SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены
|
||||||
ломает стандартный PlayMode test runner. `SceneBuilder` пересобирает сцены:
|
ломает стандартный PlayMode test runner. `SceneBuilder` пересобирает сцены:
|
||||||
|
|
|
||||||
|
|
@ -2533,7 +2533,7 @@ public sealed class GameEngine
|
||||||
|
|
||||||
var attackPower = EffectivePower(attacker);
|
var attackPower = EffectivePower(attacker);
|
||||||
var defensePower = EffectivePower(defenseUnit) + battle.GuardShield;
|
var defensePower = EffectivePower(defenseUnit) + battle.GuardShield;
|
||||||
var hit = !battle.Nullified && attackPower > defensePower;
|
var hit = !battle.Nullified && attackPower >= defensePower;
|
||||||
|
|
||||||
_events.Add(new BattleResolvedEvent(hit));
|
_events.Add(new BattleResolvedEvent(hit));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,43 @@ public class GameEngineTests
|
||||||
Assert.Single(engine.State.Players[1].Drop);
|
Assert.Single(engine.State.Players[1].Drop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equal_Power_Attack_Hits()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttack();
|
||||||
|
var attacker = engine.State.Players[0].Vanguard!;
|
||||||
|
var defender = engine.State.Players[1].Vanguard!;
|
||||||
|
var attackPower = engine.EffectivePower(attacker);
|
||||||
|
var defensePower = engine.EffectivePower(defender);
|
||||||
|
|
||||||
|
if (attackPower > defensePower)
|
||||||
|
{
|
||||||
|
defender.Modifiers.Add(new StatModifier
|
||||||
|
{
|
||||||
|
Power = attackPower - defensePower,
|
||||||
|
Source = defender,
|
||||||
|
EffectId = "equalize",
|
||||||
|
Duration = ModifierDuration.UntilEndOfBattle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (defensePower > attackPower)
|
||||||
|
{
|
||||||
|
attacker.Modifiers.Add(new StatModifier
|
||||||
|
{
|
||||||
|
Power = defensePower - attackPower,
|
||||||
|
Source = attacker,
|
||||||
|
EffectId = "equalize",
|
||||||
|
Duration = ModifierDuration.UntilEndOfBattle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||||
|
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||||||
|
|
||||||
|
Assert.Single(engine.State.Players[1].Damage);
|
||||||
|
Assert.Contains(engine.Events, e => e is BattleResolvedEvent { Hit: true });
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Sixth_Damage_Ends_The_Game()
|
public void Sixth_Damage_Ends_The_Game()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue