From c2480bab6385e8c2d6d18a12f649dee0aabf95ce Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Wed, 21 Oct 2026 00:30:28 +0500 Subject: [PATCH] Trigger buffs last until end of turn; trigger cards go through the trigger zone first Order is now: reveal -> trigger zone -> resolve effects -> hand (drive check) or damage (damage check). Damage is added only after the trigger resolves, so heal compares the pre-damage count. Temporary power/crit are cleared at end of turn instead of end of battle. --- IMPLEMENTATION_CHECKLIST.md | 2 +- .../src/Vanguard.Rules/Engine/GameEngine.cs | 79 ++++++++++++------- .../src/Vanguard.Rules/Engine/GameModels.cs | 1 + .../Vanguard.Rules.Tests/GameEngineTests.cs | 23 +++++- 4 files changed, 75 insertions(+), 30 deletions(-) diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md index 422a4e4..18e06d3 100644 --- a/IMPLEMENTATION_CHECKLIST.md +++ b/IMPLEMENTATION_CHECKLIST.md @@ -114,7 +114,7 @@ - [ ] Check Timing: сбор → сортировка → разрешение по одному → повтор. - [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck. - [ ] Цели: авто по фильтру и выбор игроком, «до N» / «ровно N». -- [~] Слои модификаторов силы/крита: временные бонусы до конца боя есть, постоянных слоёв нет. +- [~] Слои модификаторов силы/крита: усиления от триггеров живут до конца хода, постоянных слоёв нет. - [x] Валидация интентов: `Rejected(reason)` без изменения состояния. - [ ] Список легальных интентов на каждый момент (для UI и античита). - [~] Детерминированный RNG: один поток на партию, состояние в `GameState`. diff --git a/server/src/Vanguard.Rules/Engine/GameEngine.cs b/server/src/Vanguard.Rules/Engine/GameEngine.cs index a5e1917..662a6eb 100644 --- a/server/src/Vanguard.Rules/Engine/GameEngine.cs +++ b/server/src/Vanguard.Rules/Engine/GameEngine.cs @@ -11,7 +11,9 @@ namespace Vanguard.Rules.Engine; /// * Boost не реализован; /// * триггер требует выбора целей через ResolveTriggerCommand: /// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект; -/// правило одинаково для drive check и damage check. +/// правило одинаково для drive check и damage check; +/// * порядок проверки: карта → зона триггера → эффекты → рука (drive) или урон (damage); +/// * усиления от триггеров живут до конца хода. /// public sealed class GameEngine { @@ -324,6 +326,8 @@ public sealed class GameEngine State.Phase = Phase.End; _events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.End)); + ClearTurnModifiers(); + if (State.IsFinished) return CommandResult.Ok(); @@ -368,7 +372,15 @@ public sealed class GameEngine } } - // ---------- бой ---------- + /// Усиления от триггеров живут до конца хода, а не до конца боя. + private void ClearTurnModifiers() + { + foreach (var unit in AllUnits(0).Concat(AllUnits(1))) + { + unit.TemporaryPower = 0; + unit.TemporaryCritical = 0; + } + } private void BeginResolution(BattleState battle) { @@ -418,6 +430,7 @@ public sealed class GameEngine { Player = player, PrintingId = card.PrintingId, + Card = card, Trigger = def.Trigger, Context = TriggerContext.DriveCheck, GrantsPower = true, @@ -480,16 +493,23 @@ public sealed class GameEngine State.PendingTrigger = null; _events.Add(new TriggerAppliedEvent(player, command.PowerTarget, command.EffectTarget)); - if (pending.Context == TriggerContext.DriveCheck && State.Battle is { } battle) + if (pending.Context == TriggerContext.DriveCheck) { - State.Step = Step.Drive; - ContinueDriveChecks(battle); + // эффекты сработали — теперь карта уходит из зоны триггера в руку + var p = State.Players[player]; + p.Trigger.Remove(pending.Card); + p.Hand.Add(pending.Card); + + if (State.Battle is { } battle) + { + State.Step = Step.Drive; + ContinueDriveChecks(battle); + } + return CommandResult.Ok(); } - if (pending.Context == TriggerContext.DamageCheck) - FinishDamage(pending); - + FinishDamage(pending); return CommandResult.Ok(); } @@ -550,12 +570,6 @@ public sealed class GameEngine defender.Drop.Add(guard); } - foreach (var unit in AllUnits(0).Concat(AllUnits(1))) - { - unit.TemporaryPower = 0; - unit.TemporaryCritical = 0; - } - State.Battle = null; State.PendingTrigger = null; State.Step = Step.None; @@ -573,18 +587,18 @@ public sealed class GameEngine if (card is null) return; - card.FaceUp = false; - State.Players[player].Damage.Add(card); - _events.Add(new DamageTakenEvent(player, 1, State.Players[player].Damage.Count)); - var def = State.Catalog.CardOf(card.PrintingId); if (def.Trigger != TriggerType.None) { + // карта попадает в зону триггера, эффекты срабатывают до урона + State.Players[player].Trigger.Add(card); _events.Add(new TriggerResolvedEvent(player, card.PrintingId, def.Trigger)); + State.PendingTrigger = new PendingTrigger { Player = player, PrintingId = card.PrintingId, + Card = card, Trigger = def.Trigger, Context = TriggerContext.DamageCheck, GrantsPower = true, @@ -595,30 +609,39 @@ public sealed class GameEngine return; } - if (State.Players[player].Damage.Count >= MaxDamage) - { - Defeat(player); + PutIntoDamage(player, card); + + if (State.IsFinished) return; - } } } + private void PutIntoDamage(int player, CardInstance card) + { + card.FaceUp = false; + var p = State.Players[player]; + p.Damage.Add(card); + _events.Add(new DamageTakenEvent(player, 1, p.Damage.Count)); + + if (p.Damage.Count >= MaxDamage) + Defeat(player); + } + /// Продолжает урон после разрешения триггера damage check. private void FinishDamage(PendingTrigger pending) { if (State.IsFinished) return; - var player = pending.Player; + var p = State.Players[pending.Player]; + p.Trigger.Remove(pending.Card); + PutIntoDamage(pending.Player, pending.Card); - if (State.Players[player].Damage.Count >= MaxDamage) - { - Defeat(player); + if (State.IsFinished) return; - } if (pending.RemainingDamage > 0) - DealDamage(player, pending.RemainingDamage); + DealDamage(pending.Player, pending.RemainingDamage); if (State.PendingTrigger is not null) return; diff --git a/server/src/Vanguard.Rules/Engine/GameModels.cs b/server/src/Vanguard.Rules/Engine/GameModels.cs index ec5dde7..20d26c8 100644 --- a/server/src/Vanguard.Rules/Engine/GameModels.cs +++ b/server/src/Vanguard.Rules/Engine/GameModels.cs @@ -32,6 +32,7 @@ public sealed class PendingTrigger { public required int Player { get; init; } public required string PrintingId { get; init; } + public required CardInstance Card { get; init; } public required TriggerType Trigger { get; init; } public required TriggerContext Context { get; init; } diff --git a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs index fe19eb7..ebdc21b 100644 --- a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs +++ b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs @@ -166,17 +166,38 @@ public class GameEngineTests var pending = engine.State.PendingTrigger; Assert.NotNull(pending); Assert.Equal(TriggerContext.DamageCheck, pending!.Context); - Assert.Single(engine.State.Players[1].Damage); + + // до разрешения карта лежит в зоне триггера, а не в уроне + Assert.Empty(engine.State.Players[1].Damage); + Assert.Contains(engine.State.Players[1].Trigger, c => c.PrintingId == Nike); Assert.True(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted); Assert.Null(engine.State.PendingTrigger); Assert.Null(engine.State.Battle); Assert.Single(engine.State.Players[1].Damage); + Assert.Empty(engine.State.Players[1].Trigger); Assert.Contains(engine.State.Players[1].Damage, c => c.PrintingId == Nike); Assert.Contains(engine.Events, e => e is TriggerAppliedEvent { Player: 1 }); } + [Fact] + public void Trigger_Power_Lasts_Until_End_Of_Turn() + { + var engine = ReadyToAttack(); + StackDeck(engine.State.Players[0], Nike, Gallatin); + + engine.Execute(new AttackCommand(-1, -1)); + engine.Execute(new PassGuardCommand()); + engine.Execute(new ResolveTriggerCommand(-1, -1)); + + var vanguard = engine.State.Players[0].Vanguard!; + Assert.Equal(GameEngine.TriggerPowerBonus, vanguard.TemporaryPower); + + Assert.True(engine.Execute(new EndTurnCommand()).Accepted); + Assert.Equal(0, vanguard.TemporaryPower); + } + [Fact] public void ResolveTrigger_Rejects_When_Nothing_Is_Pending() {