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 RectTransform _abilityOverlayButtons;
|
||||
private int _abilityPromptInstanceId;
|
||||
private RectTransform _choiceOverlay;
|
||||
private Text _choiceOverlayTitle;
|
||||
private RectTransform _choiceOverlayButtons;
|
||||
private string _choicePromptKey;
|
||||
private MatchConnectionInfo _info;
|
||||
private MatchView _view;
|
||||
private bool _connecting;
|
||||
|
|
@ -231,6 +235,7 @@ namespace Vanguard.UI
|
|||
BuildPhasePanel();
|
||||
BuildCombatStats();
|
||||
BuildAbilityOverlay();
|
||||
BuildChoiceOverlay();
|
||||
BuildReconnectOverlay();
|
||||
_cardMenu = new CardActionMenuView();
|
||||
_cardMenu.Build(_root);
|
||||
|
|
@ -301,6 +306,7 @@ namespace Vanguard.UI
|
|||
_board3D?.SetVisible(true);
|
||||
_overlays?.CloseAll();
|
||||
HideAbilityOverlay();
|
||||
HideChoiceOverlay();
|
||||
_selfZones?.Update(null, self: true);
|
||||
_opponentZones?.Update(null, self: false);
|
||||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||
|
|
@ -333,6 +339,7 @@ namespace Vanguard.UI
|
|||
_skipFeedback.gameObject.SetActive(false);
|
||||
_overlays?.CloseAll();
|
||||
HideAbilityOverlay();
|
||||
HideChoiceOverlay();
|
||||
_cardMenu?.Hide();
|
||||
_combatStats?.gameObject.SetActive(false);
|
||||
_board3D?.SetVisible(false);
|
||||
|
|
@ -352,6 +359,7 @@ namespace Vanguard.UI
|
|||
UpdateFeedback();
|
||||
SanitizeInputLocks();
|
||||
SyncAbilityPrompt();
|
||||
SyncChoicePrompt();
|
||||
UpdateAutoPhases();
|
||||
}
|
||||
|
||||
|
|
@ -374,6 +382,8 @@ namespace Vanguard.UI
|
|||
_inputLock.Release("card-menu");
|
||||
if (_abilityOverlay == null || !_abilityOverlay.gameObject.activeSelf)
|
||||
_inputLock.Release("ability");
|
||||
if (_choiceOverlay == null || !_choiceOverlay.gameObject.activeSelf)
|
||||
_inputLock.Release("choice");
|
||||
if (!_feedbackPlaying && _feedbackQueue.Count == 0)
|
||||
{
|
||||
_feedbackLockHeld = false;
|
||||
|
|
@ -736,6 +746,7 @@ namespace Vanguard.UI
|
|||
|
||||
Render();
|
||||
SyncAbilityPrompt();
|
||||
SyncChoicePrompt();
|
||||
}
|
||||
|
||||
private void OnEvents(ServerEvents events)
|
||||
|
|
@ -1287,6 +1298,115 @@ namespace Vanguard.UI
|
|||
_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()
|
||||
{
|
||||
_reconnectOverlay = UiKit.CreatePanel(
|
||||
|
|
@ -1426,6 +1546,15 @@ namespace Vanguard.UI
|
|||
|
||||
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>())
|
||||
{
|
||||
switch (intent)
|
||||
|
|
@ -1718,7 +1847,24 @@ namespace Vanguard.UI
|
|||
|
||||
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;
|
||||
|
||||
if (slot.Zone == BoardZone.Guardian)
|
||||
|
|
@ -2877,7 +3023,8 @@ namespace Vanguard.UI
|
|||
.Where(intent => !IsBoardIntent(intent)
|
||||
&& !IsPhaseIntent(intent)
|
||||
&& intent is not ActivateAbilityIntent
|
||||
&& intent is not ResolveAbilityIntent)
|
||||
&& intent is not ResolveAbilityIntent
|
||||
&& !(intent is ChooseIntent && IsRearGuardChoice()))
|
||||
.ToList();
|
||||
|
||||
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) =>
|
||||
Resources.FindObjectsOfTypeAll<GameObject>()
|
||||
.Where(go => go.scene.IsValid() && go.name == name)
|
||||
|
|
|
|||
|
|
@ -351,7 +351,10 @@ public sealed class BarcgalScript : CardScript
|
|||
Condition = ctx =>
|
||||
ctx.Other is not null &&
|
||||
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 =>
|
||||
ctx.Other is not null &&
|
||||
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
|
||||
|
|
@ -802,7 +805,10 @@ public sealed class GuidingZombieScript : CardScript
|
|||
Condition = ctx =>
|
||||
ctx.Other is not null &&
|
||||
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);
|
||||
|
||||
/// <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];
|
||||
if (!p.Soul.Contains(card))
|
||||
return false;
|
||||
|
||||
var slot = Array.FindIndex(p.RearGuards, u => u is null);
|
||||
if (slot < 0)
|
||||
if (slot < 0 || slot >= PlayerState.RearGuardSlots || p.RearGuards[slot] is not null)
|
||||
return false;
|
||||
|
||||
p.Soul.Remove(card);
|
||||
|
|
|
|||
|
|
@ -795,7 +795,17 @@ public class GameEngineTests
|
|||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue