From aa330c6cfa72b3a361375b583a6c7c1e8aa6f7c3 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Fri, 23 Oct 2026 19:08:23 +0500 Subject: [PATCH] Add boost attack gestures and Barcgal activated search --- .../Assets/Vanguard/Runtime/UI/BattleView.cs | 65 ++++++++++++++++++- .../Engine/Abilities/CardScripts.cs | 18 +++++ .../src/Vanguard.Rules/Engine/GameEngine.cs | 23 +++++-- .../Vanguard.Rules.Tests/GameEngineTests.cs | 45 +++++++++++++ 4 files changed, 145 insertions(+), 6 deletions(-) diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index f3499c3..2f6d3f7 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -90,6 +90,7 @@ namespace Vanguard.UI private string _discardPrompt; private Action _discardCancelAction; private int _pendingBoosterSlot = -1; + private int _pendingBoostAttackerSlot = -2; private int? _triggerPowerSlot; private readonly HashSet _mulliganSelected = new HashSet(); private bool _mulliganUsed; @@ -278,6 +279,7 @@ namespace Vanguard.UI _mulliganUsed = false; _abilityDamageIndex = -1; _pendingBoosterSlot = -1; + _pendingBoostAttackerSlot = -2; _triggerPowerSlot = null; _showAllIntents = false; _autoFinishMulligan = false; @@ -1087,6 +1089,7 @@ namespace Vanguard.UI _pendingIntents.Clear(); _highlightSlots.Clear(); _pendingBoosterSlot = -1; + _pendingBoostAttackerSlot = -2; _triggerPowerSlot = null; if (intent is FinishMulliganIntent or SkipRideIntent or EnterBattleIntent or EndTurnIntent) @@ -2105,7 +2108,22 @@ namespace Vanguard.UI var legal = _view.LegalIntents ?? new List(); IntentPayload intent = null; - if (source.Zone == BoardZone.Hand) + if (_pendingBoosterSlot >= 0 && source.IsField) + { + if (source.Slot != _pendingBoostAttackerSlot) + { + SetStatus( + "Поддержка уже объявлена: атаковать можно только поддерживаемой картой.", + new Color(1f, 0.55f, 0.45f)); + return; + } + + intent = legal.OfType() + .FirstOrDefault(item => item.AttackerSlot == source.Slot + && item.TargetSlot == target.Slot + && item.BoosterSlot == _pendingBoosterSlot); + } + else if (source.Zone == BoardZone.Hand) { if (target.Zone == BoardZone.Guardian) { @@ -2139,6 +2157,15 @@ namespace Vanguard.UI intent = legal.OfType() .FirstOrDefault(item => item.Slot == source.Slot); } + else if (target.Player == _view.You + && source.Slot is 0 or 1 or 2 + && (target.Zone == BoardZone.Vanguard + || (target.Zone == BoardZone.RearGuard + && target.Slot is 3 or 4))) + { + BeginBoostAttack(source.Slot, target.Slot, legal); + return; + } else if (target.Player == _view.You && target.Zone == BoardZone.RearGuard) { intent = legal.OfType() @@ -2166,6 +2193,36 @@ namespace Vanguard.UI OnIntentClicked(intent); } + private void BeginBoostAttack( + int boosterSlot, + int attackerSlot, + IReadOnlyList legal) + { + var attacks = legal.OfType() + .Where(intent => intent.BoosterSlot == boosterSlot + && intent.AttackerSlot == attackerSlot) + .ToList(); + if (attacks.Count == 0) + { + SetStatus( + "Эта карта не может поддержать выбранный круг.", + new Color(1f, 0.55f, 0.45f)); + return; + } + + _pendingBoosterSlot = boosterSlot; + _pendingBoostAttackerSlot = attackerSlot; + _pendingIntents.Clear(); + _pendingIntents.AddRange(attacks); + RebuildHighlights(); + RebuildIntents(_view); + SetStatus( + "Поддержка: " + CardName( + CardAt(_view.You, boosterSlot)?.PrintingId ?? string.Empty) + + ". Атакуйте только этой картой и выберите цель.", + new Color(0.98f, 0.82f, 0.42f)); + } + private void OnBoard3DPileRightClicked(BoardCardRef pile) { if (_view == null || _inputLock.IsLocked) @@ -2260,6 +2317,11 @@ namespace Vanguard.UI ? "Выберите карту в руке для сброса." : _discardPrompt; } + else if (_pendingBoosterSlot >= 0 && _pendingIntents.Count > 0) + { + title = "ПОДДЕРЖКА"; + hint = "Атакуйте только поддерживаемой картой и выберите цель."; + } else if (_pendingIntents.Count > 0) { title = "ВЫБОР ЦЕЛИ"; @@ -2809,6 +2871,7 @@ namespace Vanguard.UI _pendingIntents.Clear(); _highlightSlots.Clear(); _pendingBoosterSlot = -1; + _pendingBoostAttackerSlot = -2; _triggerPowerSlot = null; if (rebuild && _view != null) RebuildIntents(_view); diff --git a/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs b/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs index 8b97667..b3def80 100644 --- a/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs +++ b/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs @@ -358,6 +358,24 @@ public sealed class BarcgalScript : CardScript }; } } + + public override IEnumerable Activated + { + get + { + yield return new ActivatedAbility + { + RestsSelf = true, + CanActivate = ctx => + !ctx.Engine.IsVanguardOf(ctx.Source, ctx.Owner) && + ctx.Engine.HasEmptyRearGuard(ctx.Owner), + Resolve = ctx => ctx.Engine.SearchDeckToCall( + ctx.Owner, + "Найдите Future Knight, Llew или Flogal", + definition => definition.NameId is "future-knight-llew" or "flogal"), + }; + } + } } /// diff --git a/server/src/Vanguard.Rules/Engine/GameEngine.cs b/server/src/Vanguard.Rules/Engine/GameEngine.cs index 486ff69..163681a 100644 --- a/server/src/Vanguard.Rules/Engine/GameEngine.cs +++ b/server/src/Vanguard.Rules/Engine/GameEngine.cs @@ -1510,9 +1510,15 @@ public sealed class GameEngine foreach (var target in targets) commands.Add(new AttackCommand(attackerSlot, target)); - if (attackerSlot is 3 or 4) + var boosterSlot = attackerSlot switch + { + -1 => 1, // Vanguard is boosted from the centre back row. + 3 => 0, + 4 => 2, + _ => -1, + }; + if (boosterSlot >= 0) { - var boosterSlot = attackerSlot == 3 ? 0 : 2; var booster = State.GetUnit(player, boosterSlot); if (booster is not null && !booster.Rested && State.CanBoost(booster)) { @@ -1986,10 +1992,17 @@ public sealed class GameEngine CardInstance? booster = null; if (boosterSlot >= 0) { - if (attackerSlot is not (3 or 4)) - return CommandResult.Reject("Поддерживать можно только атаку Арьергарда из переднего ряда."); + var expected = attackerSlot switch + { + -1 => 1, + 3 => 0, + 4 => 2, + _ => -1, + }; + if (expected < 0) + return CommandResult.Reject( + "Поддерживать можно только Авангард или Арьергард переднего ряда."); - var expected = attackerSlot == 3 ? 0 : 2; if (boosterSlot != expected) return CommandResult.Reject("Поддержка возможна только из заднего ряда той же колонны."); diff --git a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs index b277be6..bed9275 100644 --- a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs +++ b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs @@ -823,6 +823,51 @@ public class GameEngineTests Assert.DoesNotContain(engine.State.Players[0].RearGuards, u => u?.PrintingId == Barcgal); } + [Fact] + public void Vanguard_Can_Be_Boosted_From_Centre_Back_Row() + { + var engine = StartedEngine(); + Give(engine, 0, Marron); + engine.Execute(new RideCommand(Marron)); + + Give(engine, 0, Marron); + Assert.True(engine.Execute(new CallCommand(Marron, 1)).Accepted); + engine.State.Players[0].RearGuards[1]!.Rested = false; + + SetVanguard(engine, 1, Gallatin); + engine.Execute(new EnterBattleCommand()); + + Assert.True(engine.Execute(new AttackCommand(-1, -1, 1)).Accepted); + Assert.NotNull(engine.State.Battle); + Assert.Equal(-1, engine.State.Battle!.AttackerSlot); + Assert.Equal(1, engine.State.Battle.BoosterSlot); + } + + [Fact] + public void Barcgal_Activated_Searches_And_Calls_Unit_To_Chosen_Circle() + { + var engine = InMainPhase(); + Give(engine, 0, Barcgal); + var barcgal = engine.State.Players[0].Hand + .Single(card => card.PrintingId == Barcgal); + + Assert.True(engine.Execute(new CallCommand(Barcgal, 0)).Accepted); + barcgal.Rested = false; + Assert.True(engine.Execute(new ActivateAbilityCommand(barcgal.Id)).Accepted); + + Assert.NotNull(engine.State.PendingChoice); + Assert.Equal(Step.Choice, engine.State.Step); + Assert.True(engine.Execute(new ChooseCommand(0)).Accepted); + + Assert.NotNull(engine.State.PendingChoice); + Assert.True(engine.Execute(new ChooseCommand(1)).Accepted); + + Assert.Equal( + Flogal, + engine.State.Players[0].RearGuards[2]?.PrintingId); + Assert.True(barcgal.Rested); + } + [Fact] public void Wingal_Gives_BlasterBlade_4000_During_Battle() {