Damage check triggers also pause for target selection

Damage check now places the card, then pauses on a trigger with PendingTrigger(DamageCheck) and the remaining damage count. Resolving applies the same +5000 and secondary effect as drive check, then continues the remaining damage or closes the battle. 34 tests.
This commit is contained in:
MpakobecEx 2026-10-21 00:27:09 +05:00
parent a4457c6d21
commit ebbfb46834
3 changed files with 72 additions and 31 deletions

View File

@ -9,10 +9,9 @@ namespace Vanguard.Rules.Engine;
/// Упрощения первого среза (карты со способностями подключаются следующим шагом): /// Упрощения первого среза (карты со способностями подключаются следующим шагом):
/// * Sentinel добавляет 0 щита и не оплачивает стоимость — Perfect Guard пока не работает; /// * Sentinel добавляет 0 щита и не оплачивает стоимость — Perfect Guard пока не работает;
/// * Boost не реализован; /// * Boost не реализован;
/// * триггер на drive check требует выбора целей через ResolveTriggerCommand: /// * триггер требует выбора целей через ResolveTriggerCommand:
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект; /// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
/// * триггер на damage check пока применяет только второй эффект, /// правило одинаково для drive check и damage check.
/// Stand поднимает первый повёрнутый Арьергард.
/// </summary> /// </summary>
public sealed class GameEngine public sealed class GameEngine
{ {
@ -485,8 +484,12 @@ public sealed class GameEngine
{ {
State.Step = Step.Drive; State.Step = Step.Drive;
ContinueDriveChecks(battle); ContinueDriveChecks(battle);
return CommandResult.Ok();
} }
if (pending.Context == TriggerContext.DamageCheck)
FinishDamage(pending);
return CommandResult.Ok(); return CommandResult.Ok();
} }
@ -532,6 +535,9 @@ public sealed class GameEngine
} }
} }
if (State.PendingTrigger is not null)
return;
ClearBattle(battle); ClearBattle(battle);
} }
@ -567,17 +573,28 @@ public sealed class GameEngine
if (card is null) if (card is null)
return; 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); var def = State.Catalog.CardOf(card.PrintingId);
if (def.Trigger != TriggerType.None) if (def.Trigger != TriggerType.None)
{ {
_events.Add(new TriggerResolvedEvent(player, card.PrintingId, def.Trigger)); _events.Add(new TriggerResolvedEvent(player, card.PrintingId, def.Trigger));
ApplyDamageTrigger(player, def.Trigger); State.PendingTrigger = new PendingTrigger
{
Player = player,
PrintingId = card.PrintingId,
Trigger = def.Trigger,
Context = TriggerContext.DamageCheck,
GrantsPower = true,
RemainingDamage = amount - i - 1,
};
State.Step = Step.Trigger;
State.ActivePlayer = player;
return;
} }
card.FaceUp = false;
State.Players[player].Damage.Add(card);
_events.Add(new DamageTakenEvent(player, 1, State.Players[player].Damage.Count));
if (State.Players[player].Damage.Count >= MaxDamage) if (State.Players[player].Damage.Count >= MaxDamage)
{ {
Defeat(player); Defeat(player);
@ -586,33 +603,30 @@ public sealed class GameEngine
} }
} }
private void ApplyDamageTrigger(int player, TriggerType trigger) /// <summary>Продолжает урон после разрешения триггера damage check.</summary>
private void FinishDamage(PendingTrigger pending)
{ {
switch (trigger) if (State.IsFinished)
{ return;
case TriggerType.Draw:
Draw(player, 1);
break;
case TriggerType.Stand:
StandFirstRested(player);
break;
case TriggerType.Heal:
Heal(player);
break;
}
}
private void StandFirstRested(int player) var player = pending.Player;
if (State.Players[player].Damage.Count >= MaxDamage)
{ {
var p = State.Players[player]; Defeat(player);
for (var i = 0; i < p.RearGuards.Length; i++)
{
if (p.RearGuards[i] is { Rested: true } unit)
{
unit.Rested = false;
return; return;
} }
}
if (pending.RemainingDamage > 0)
DealDamage(player, pending.RemainingDamage);
if (State.PendingTrigger is not null)
return;
State.Step = Step.None;
if (State.Battle is { } battle)
ClearBattle(battle);
} }
private void Heal(int player) private void Heal(int player)

View File

@ -37,6 +37,9 @@ public sealed class PendingTrigger
/// <summary>На drive check триггер сначала даёт +5000 силы, на damage check — нет.</summary> /// <summary>На drive check триггер сначала даёт +5000 силы, на damage check — нет.</summary>
public required bool GrantsPower { get; init; } public required bool GrantsPower { get; init; }
/// <summary>Сколько урона осталось раздать после этого триггера (контекст damage check).</summary>
public int RemainingDamage { get; init; }
} }
/// <summary>Экземпляр карты в партии. Один принт может встречаться много раз.</summary> /// <summary>Экземпляр карты в партии. Один принт может встречаться много раз.</summary>

View File

@ -153,6 +153,30 @@ public class GameEngineTests
Assert.False(rearGuard.Rested); Assert.False(rearGuard.Rested);
} }
[Fact]
public void Damage_Trigger_Also_Asks_For_Targets_And_Applies_Effect()
{
var engine = ReadyToAttack();
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
StackDeck(engine.State.Players[1], Nike);
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
var pending = engine.State.PendingTrigger;
Assert.NotNull(pending);
Assert.Equal(TriggerContext.DamageCheck, pending!.Context);
Assert.Single(engine.State.Players[1].Damage);
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.Contains(engine.State.Players[1].Damage, c => c.PrintingId == Nike);
Assert.Contains(engine.Events, e => e is TriggerAppliedEvent { Player: 1 });
}
[Fact] [Fact]
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending() public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
{ {