Let player choose rear-guard circle for call-from-soul abilities
CI / test (push) Successful in 1m51s
Details
CI / test (push) Successful in 1m51s
Details
This commit is contained in:
parent
69f2121e16
commit
6f514e85b6
|
|
@ -71,6 +71,10 @@ namespace Vanguard.UI
|
||||||
private Text _abilityOverlayName;
|
private Text _abilityOverlayName;
|
||||||
private RectTransform _abilityOverlayButtons;
|
private RectTransform _abilityOverlayButtons;
|
||||||
private int _abilityPromptInstanceId;
|
private int _abilityPromptInstanceId;
|
||||||
|
private RectTransform _choiceOverlay;
|
||||||
|
private Text _choiceOverlayTitle;
|
||||||
|
private RectTransform _choiceOverlayButtons;
|
||||||
|
private string _choicePromptKey;
|
||||||
private MatchConnectionInfo _info;
|
private MatchConnectionInfo _info;
|
||||||
private MatchView _view;
|
private MatchView _view;
|
||||||
private bool _connecting;
|
private bool _connecting;
|
||||||
|
|
@ -231,6 +235,7 @@ namespace Vanguard.UI
|
||||||
BuildPhasePanel();
|
BuildPhasePanel();
|
||||||
BuildCombatStats();
|
BuildCombatStats();
|
||||||
BuildAbilityOverlay();
|
BuildAbilityOverlay();
|
||||||
|
BuildChoiceOverlay();
|
||||||
BuildReconnectOverlay();
|
BuildReconnectOverlay();
|
||||||
_cardMenu = new CardActionMenuView();
|
_cardMenu = new CardActionMenuView();
|
||||||
_cardMenu.Build(_root);
|
_cardMenu.Build(_root);
|
||||||
|
|
@ -301,6 +306,7 @@ namespace Vanguard.UI
|
||||||
_board3D?.SetVisible(true);
|
_board3D?.SetVisible(true);
|
||||||
_overlays?.CloseAll();
|
_overlays?.CloseAll();
|
||||||
HideAbilityOverlay();
|
HideAbilityOverlay();
|
||||||
|
HideChoiceOverlay();
|
||||||
_selfZones?.Update(null, self: true);
|
_selfZones?.Update(null, self: true);
|
||||||
_opponentZones?.Update(null, self: false);
|
_opponentZones?.Update(null, self: false);
|
||||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||||
|
|
@ -333,6 +339,7 @@ namespace Vanguard.UI
|
||||||
_skipFeedback.gameObject.SetActive(false);
|
_skipFeedback.gameObject.SetActive(false);
|
||||||
_overlays?.CloseAll();
|
_overlays?.CloseAll();
|
||||||
HideAbilityOverlay();
|
HideAbilityOverlay();
|
||||||
|
HideChoiceOverlay();
|
||||||
_cardMenu?.Hide();
|
_cardMenu?.Hide();
|
||||||
_combatStats?.gameObject.SetActive(false);
|
_combatStats?.gameObject.SetActive(false);
|
||||||
_board3D?.SetVisible(false);
|
_board3D?.SetVisible(false);
|
||||||
|
|
@ -352,6 +359,7 @@ namespace Vanguard.UI
|
||||||
UpdateFeedback();
|
UpdateFeedback();
|
||||||
SanitizeInputLocks();
|
SanitizeInputLocks();
|
||||||
SyncAbilityPrompt();
|
SyncAbilityPrompt();
|
||||||
|
SyncChoicePrompt();
|
||||||
UpdateAutoPhases();
|
UpdateAutoPhases();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -374,6 +382,8 @@ namespace Vanguard.UI
|
||||||
_inputLock.Release("card-menu");
|
_inputLock.Release("card-menu");
|
||||||
if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf)
|
if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf)
|
||||||
_inputLock.Release("ability");
|
_inputLock.Release("ability");
|
||||||
|
if (_choiceOverlay == null || !_choiceOverlay.gameObject.activeSelf)
|
||||||
|
_inputLock.Release("choice");
|
||||||
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
||||||
{
|
{
|
||||||
_feedbackLockHeld = false;
|
_feedbackLockHeld = false;
|
||||||
|
|
@ -736,6 +746,7 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
Render();
|
Render();
|
||||||
SyncAbilityPrompt();
|
SyncAbilityPrompt();
|
||||||
|
SyncChoicePrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEvents(ServerEvents events)
|
private void OnEvents(ServerEvents events)
|
||||||
|
|
@ -1287,6 +1298,115 @@ namespace Vanguard.UI
|
||||||
_inputLock.Release("ability");
|
_inputLock.Release("ability");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildChoiceOverlay()
|
||||||
|
{
|
||||||
|
_choiceOverlay = UiKit.CreatePanel(
|
||||||
|
_root,
|
||||||
|
"ChoiceOverlay",
|
||||||
|
new Color(0f, 0f, 0f, 0.72f));
|
||||||
|
var card = UiKit.CreateCenteredCard(
|
||||||
|
_choiceOverlay,
|
||||||
|
"ChoiceCard",
|
||||||
|
new Vector2(760f, 300f),
|
||||||
|
Panel);
|
||||||
|
UiKit.AddVerticalLayout(card, 22, 10);
|
||||||
|
|
||||||
|
var title = UiKit.CreateText(
|
||||||
|
card,
|
||||||
|
"ChoiceTitle",
|
||||||
|
"Выбор",
|
||||||
|
22,
|
||||||
|
new Color(1f, 0.78f, 0.42f),
|
||||||
|
TextAnchor.MiddleCenter);
|
||||||
|
title.fontStyle = FontStyle.Bold;
|
||||||
|
UiKit.SetPreferredHeight(title, 34f);
|
||||||
|
|
||||||
|
_choiceOverlayTitle = UiKit.CreateText(
|
||||||
|
card,
|
||||||
|
"ChoicePrompt",
|
||||||
|
string.Empty,
|
||||||
|
18,
|
||||||
|
Color.white,
|
||||||
|
TextAnchor.MiddleCenter);
|
||||||
|
UiKit.SetPreferredHeight(_choiceOverlayTitle, 52f);
|
||||||
|
|
||||||
|
_choiceOverlayButtons = UiKit.CreatePanel(
|
||||||
|
card, "ChoiceButtons", Color.clear);
|
||||||
|
UiKit.AddHorizontalLayout(_choiceOverlayButtons, 0, 8);
|
||||||
|
UiKit.SetPreferredHeight(_choiceOverlayButtons, 52f);
|
||||||
|
_choiceOverlay.gameObject.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsRearGuardChoice()
|
||||||
|
{
|
||||||
|
var prompt = _view?.Choice?.Prompt;
|
||||||
|
return !string.IsNullOrEmpty(prompt)
|
||||||
|
&& (prompt.IndexOf("круг", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| prompt.IndexOf("rear", StringComparison.OrdinalIgnoreCase) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncChoicePrompt()
|
||||||
|
{
|
||||||
|
if (_choiceOverlay == null || _sending)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var choice = _view?.Choice;
|
||||||
|
if (choice == null
|
||||||
|
|| _view == null
|
||||||
|
|| _view.ActivePlayer != _view.You
|
||||||
|
|| !IsRearGuardChoice())
|
||||||
|
{
|
||||||
|
HideChoiceOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = (choice.Prompt ?? string.Empty) + "|" +
|
||||||
|
string.Join(",", choice.Options.Select(option => option.Slot));
|
||||||
|
if (_choiceOverlay.gameObject.activeSelf
|
||||||
|
&& string.Equals(_choicePromptKey, key, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_choicePromptKey = key;
|
||||||
|
_choiceOverlayTitle.text = choice.Prompt ?? "Выберите круг";
|
||||||
|
UiKit.MakeLiteral(_choiceOverlayTitle);
|
||||||
|
ClearChildren(_choiceOverlayButtons);
|
||||||
|
|
||||||
|
for (var i = 0; i < choice.Options.Count; i++)
|
||||||
|
{
|
||||||
|
var index = i;
|
||||||
|
var option = choice.Options[i];
|
||||||
|
var button = UiKit.CreateButton(
|
||||||
|
_choiceOverlayButtons,
|
||||||
|
"Choice_" + option.Slot,
|
||||||
|
string.IsNullOrWhiteSpace(option.Label)
|
||||||
|
? "Круг " + (option.Slot + 1)
|
||||||
|
: option.Label,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
HideChoiceOverlay();
|
||||||
|
OnIntentClicked(
|
||||||
|
new ChooseIntent { Index = index },
|
||||||
|
ignoreInputLock: true);
|
||||||
|
},
|
||||||
|
16);
|
||||||
|
UiKit.SetPreferredHeight(button, 46f);
|
||||||
|
}
|
||||||
|
|
||||||
|
_choiceOverlay.SetAsLastSibling();
|
||||||
|
_choiceOverlay.gameObject.SetActive(true);
|
||||||
|
_inputLock.Acquire("choice");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideChoiceOverlay()
|
||||||
|
{
|
||||||
|
if (_choiceOverlay == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_choicePromptKey = null;
|
||||||
|
_choiceOverlay.gameObject.SetActive(false);
|
||||||
|
_inputLock.Release("choice");
|
||||||
|
}
|
||||||
|
|
||||||
private void BuildReconnectOverlay()
|
private void BuildReconnectOverlay()
|
||||||
{
|
{
|
||||||
_reconnectOverlay = UiKit.CreatePanel(
|
_reconnectOverlay = UiKit.CreatePanel(
|
||||||
|
|
@ -1426,6 +1546,15 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
if (_view != null)
|
if (_view != null)
|
||||||
{
|
{
|
||||||
|
if (IsRearGuardChoice())
|
||||||
|
{
|
||||||
|
foreach (var option in _view.Choice.Options)
|
||||||
|
{
|
||||||
|
if (option.Slot is >= 0 and < 5)
|
||||||
|
state.Highlights.Add(new BoardSlotKey(_view.You, option.Slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var intent in _view.LegalIntents ?? new List<IntentPayload>())
|
foreach (var intent in _view.LegalIntents ?? new List<IntentPayload>())
|
||||||
{
|
{
|
||||||
switch (intent)
|
switch (intent)
|
||||||
|
|
@ -1718,7 +1847,24 @@ namespace Vanguard.UI
|
||||||
|
|
||||||
private void OnBoard3DSlotClicked(BoardCardRef slot)
|
private void OnBoard3DSlotClicked(BoardCardRef slot)
|
||||||
{
|
{
|
||||||
if (_view == null || _sending || _inputLock.IsLocked || _discardChoiceActive)
|
if (_view == null || _sending || _discardChoiceActive)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsRearGuardChoice() && slot.Player == _view.You)
|
||||||
|
{
|
||||||
|
var index = _view.Choice.Options.FindIndex(
|
||||||
|
option => option.Slot == slot.Slot);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
HideChoiceOverlay();
|
||||||
|
OnIntentClicked(
|
||||||
|
new ChooseIntent { Index = index },
|
||||||
|
ignoreInputLock: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inputLock.IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (slot.Zone == BoardZone.Guardian)
|
if (slot.Zone == BoardZone.Guardian)
|
||||||
|
|
@ -2877,7 +3023,8 @@ namespace Vanguard.UI
|
||||||
.Where(intent => !IsBoardIntent(intent)
|
.Where(intent => !IsBoardIntent(intent)
|
||||||
&& !IsPhaseIntent(intent)
|
&& !IsPhaseIntent(intent)
|
||||||
&& intent is not ActivateAbilityIntent
|
&& intent is not ActivateAbilityIntent
|
||||||
&& intent is not ResolveAbilityIntent)
|
&& intent is not ResolveAbilityIntent
|
||||||
|
&& !(intent is ChooseIntent && IsRearGuardChoice()))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (_pendingIntents.Count > 0)
|
if (_pendingIntents.Count > 0)
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,61 @@ namespace Vanguard.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Rear_Guard_Choice_Shows_Overlay_With_Circles()
|
||||||
|
{
|
||||||
|
var config = new AppConfig();
|
||||||
|
config.Load();
|
||||||
|
var tokens = new TokenStore();
|
||||||
|
var api = new ApiClient(config, tokens);
|
||||||
|
var player = new PlayerState();
|
||||||
|
var auth = new AuthService(api, tokens, player);
|
||||||
|
var meta = new MetaService(api, player);
|
||||||
|
var canvas = UiKit.CreateCanvas("Choice Test Canvas");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var battle = new BattleView(canvas.transform, meta, () => { });
|
||||||
|
var match = new MatchView
|
||||||
|
{
|
||||||
|
You = 0,
|
||||||
|
ActivePlayer = 0,
|
||||||
|
Choice = new ChoiceView
|
||||||
|
{
|
||||||
|
Player = 0,
|
||||||
|
Prompt = "Выберите круг для призыва Barcgal",
|
||||||
|
Options = new List<ChoiceOptionView>
|
||||||
|
{
|
||||||
|
new() { Slot = 0, Label = "Круг 1" },
|
||||||
|
new() { Slot = 1, Label = "Круг 2" },
|
||||||
|
new() { Slot = 2, Label = "Круг 3" },
|
||||||
|
new() { Slot = 3, Label = "Круг 4" },
|
||||||
|
new() { Slot = 4, Label = "Круг 5" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LegalIntents = new List<IntentPayload>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
typeof(BattleView)
|
||||||
|
.GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
|
.SetValue(battle, match);
|
||||||
|
typeof(BattleView)
|
||||||
|
.GetMethod("SyncChoicePrompt", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||||
|
.Invoke(battle, null);
|
||||||
|
|
||||||
|
var overlay = Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
|
.First(go => go.name == "ChoiceOverlay");
|
||||||
|
Assert.IsTrue(overlay.activeSelf);
|
||||||
|
Assert.IsTrue(Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
|
.Any(go => go.name == "Choice_3"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Object.DestroyImmediate(canvas.gameObject);
|
||||||
|
Cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static RectTransform FindRect(string name) =>
|
private static RectTransform FindRect(string name) =>
|
||||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||||
.Where(go => go.scene.IsValid() && go.name == name)
|
.Where(go => go.scene.IsValid() && go.name == name)
|
||||||
|
|
|
||||||
|
|
@ -351,7 +351,10 @@ public sealed class BarcgalScript : CardScript
|
||||||
Condition = ctx =>
|
Condition = ctx =>
|
||||||
ctx.Other is not null &&
|
ctx.Other is not null &&
|
||||||
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == Clan.RoyalPaladin,
|
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == Clan.RoyalPaladin,
|
||||||
Resolve = ctx => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner),
|
Resolve = ctx => ctx.Engine.AskEmptyRearGuardChoice(
|
||||||
|
ctx.Owner,
|
||||||
|
"Выберите круг для призыва Barcgal",
|
||||||
|
slot => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner, slot)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -744,7 +744,10 @@ public sealed class ReturnToDeckOnBoostScript : CardScript
|
||||||
Condition = ctx =>
|
Condition = ctx =>
|
||||||
ctx.Other is not null &&
|
ctx.Other is not null &&
|
||||||
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == _clan,
|
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == _clan,
|
||||||
Resolve = ctx => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner),
|
Resolve = ctx => ctx.Engine.AskEmptyRearGuardChoice(
|
||||||
|
ctx.Owner,
|
||||||
|
"Выберите круг для призыва карты",
|
||||||
|
slot => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner, slot)),
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new AbilityDefinition
|
yield return new AbilityDefinition
|
||||||
|
|
@ -802,7 +805,10 @@ public sealed class GuidingZombieScript : CardScript
|
||||||
Condition = ctx =>
|
Condition = ctx =>
|
||||||
ctx.Other is not null &&
|
ctx.Other is not null &&
|
||||||
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == Clan.Granblue,
|
ctx.Catalog.CardOf(ctx.Other.PrintingId).Clan == Clan.Granblue,
|
||||||
Resolve = ctx => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner),
|
Resolve = ctx => ctx.Engine.AskEmptyRearGuardChoice(
|
||||||
|
ctx.Owner,
|
||||||
|
"Выберите круг для призыва карты",
|
||||||
|
slot => ctx.Engine.CallFromSoul(ctx.Source, ctx.Owner, slot)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -585,14 +585,13 @@ public sealed class GameEngine
|
||||||
u is not null && State.Catalog.CardOf(u.PrintingId).Clan == clan);
|
u is not null && State.Catalog.CardOf(u.PrintingId).Clan == clan);
|
||||||
|
|
||||||
/// <summary>Призыв карты из души — используется способностями вроде Barcgal.</summary>
|
/// <summary>Призыв карты из души — используется способностями вроде Barcgal.</summary>
|
||||||
public bool CallFromSoul(CardInstance card, int player)
|
public bool CallFromSoul(CardInstance card, int player, int slot = -1)
|
||||||
{
|
{
|
||||||
var p = State.Players[player];
|
var p = State.Players[player];
|
||||||
if (!p.Soul.Contains(card))
|
if (!p.Soul.Contains(card))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var slot = Array.FindIndex(p.RearGuards, u => u is null);
|
if (slot < 0 || slot >= PlayerState.RearGuardSlots || p.RearGuards[slot] is not null)
|
||||||
if (slot < 0)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
p.Soul.Remove(card);
|
p.Soul.Remove(card);
|
||||||
|
|
|
||||||
|
|
@ -795,7 +795,17 @@ public class GameEngineTests
|
||||||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||||||
|
|
||||||
Assert.Null(engine.State.PendingAbility);
|
Assert.Null(engine.State.PendingAbility);
|
||||||
Assert.Contains(engine.State.Players[0].RearGuards, u => u?.PrintingId == Barcgal);
|
Assert.NotNull(engine.State.PendingChoice);
|
||||||
|
Assert.Equal(Step.Choice, engine.State.Step);
|
||||||
|
Assert.Equal(5, engine.State.PendingChoice!.Options.Count);
|
||||||
|
|
||||||
|
// Choose a non-first circle to verify the player controls placement.
|
||||||
|
Assert.True(engine.Execute(new ChooseCommand(3)).Accepted);
|
||||||
|
|
||||||
|
Assert.Null(engine.State.PendingChoice);
|
||||||
|
Assert.Equal(
|
||||||
|
Barcgal,
|
||||||
|
engine.State.Players[0].RearGuards[3]?.PrintingId);
|
||||||
Assert.DoesNotContain(engine.State.Players[0].Soul, c => c.PrintingId == Barcgal);
|
Assert.DoesNotContain(engine.State.Players[0].Soul, c => c.PrintingId == Barcgal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue