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 пока не работает;
/// * Boost не реализован;
/// * триггер на drive check требует выбора целей через ResolveTriggerCommand:
/// * триггер требует выбора целей через ResolveTriggerCommand:
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
/// * триггер на damage check пока применяет только второй эффект,
/// Stand поднимает первый повёрнутый Арьергард.
/// правило одинаково для drive check и damage check.
/// </summary>
public sealed class GameEngine
{
@ -485,8 +484,12 @@ public sealed class GameEngine
{
State.Step = Step.Drive;
ContinueDriveChecks(battle);
return CommandResult.Ok();
}
if (pending.Context == TriggerContext.DamageCheck)
FinishDamage(pending);
return CommandResult.Ok();
}
@ -532,6 +535,9 @@ public sealed class GameEngine
}
}
if (State.PendingTrigger is not null)
return;
ClearBattle(battle);
}
@ -567,17 +573,28 @@ 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)
{
_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)
{
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)
{
case TriggerType.Draw:
Draw(player, 1);
break;
case TriggerType.Stand:
StandFirstRested(player);
break;
case TriggerType.Heal:
Heal(player);
break;
}
}
if (State.IsFinished)
return;
private void StandFirstRested(int player)
var player = pending.Player;
if (State.Players[player].Damage.Count >= MaxDamage)
{
var p = State.Players[player];
for (var i = 0; i < p.RearGuards.Length; i++)
{
if (p.RearGuards[i] is { Rested: true } unit)
{
unit.Rested = false;
Defeat(player);
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)

View File

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

View File

@ -153,6 +153,30 @@ public class GameEngineTests
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]
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
{