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:
parent
a4457c6d21
commit
ebbfb46834
|
|
@ -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 p = State.Players[player];
|
||||
for (var i = 0; i < p.RearGuards.Length; i++)
|
||||
var player = pending.Player;
|
||||
|
||||
if (State.Players[player].Damage.Count >= MaxDamage)
|
||||
{
|
||||
if (p.RearGuards[i] is { Rested: true } unit)
|
||||
{
|
||||
unit.Rested = false;
|
||||
return;
|
||||
}
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue