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()
{