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 пока не работает;
|
/// * 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;
|
||||||
{
|
|
||||||
var p = State.Players[player];
|
if (State.Players[player].Damage.Count >= MaxDamage)
|
||||||
for (var i = 0; i < p.RearGuards.Length; i++)
|
|
||||||
{
|
{
|
||||||
if (p.RearGuards[i] is { Rested: true } unit)
|
Defeat(player);
|
||||||
{
|
return;
|
||||||
unit.Rested = false;
|
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue