Drive checks belong to the attacking card
Grade 0-2 vanguard performs one check, grade 3+ performs two, rear-guards perform none by default. CardInstance.DriveChecksOverride lets effects remove Twin Drive or grant checks to a rear-guard. Added DriveCheckedEvent for the log. Five tests.
This commit is contained in:
parent
71db856480
commit
bf2ea337f8
|
|
@ -112,7 +112,7 @@
|
||||||
- [x] Перехват: карта 2 ранга из переднего ряда выходит в круг защиты, сила атаки сравнивается с ней; `InterceptOverride` и `InterceptFromBackRow` позволяют выдать способность эффектом.
|
- [x] Перехват: карта 2 ранга из переднего ряда выходит в круг защиты, сила атаки сравнивается с ней; `InterceptOverride` и `InterceptFromBackRow` позволяют выдать способность эффектом.
|
||||||
- [x] Перемещение Арьергарда в пределах колонки; если круг занят — карты меняются местами; у центрального заднего ряда пары нет.
|
- [x] Перемещение Арьергарда в пределах колонки; если круг занят — карты меняются местами; у центрального заднего ряда пары нет.
|
||||||
- [ ] Discard, Shuffle как отдельные интенты.
|
- [ ] Discard, Shuffle как отдельные интенты.
|
||||||
- [ ] `driveCheckCount` как модифицируемое свойство (сейчас константа 2).
|
- [x] Число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две; Арьергард по умолчанию без проверок; `DriveChecksOverride` меняет число (в том числе снимает двойную).
|
||||||
- [x] Результат боя: `Hit` / `Guarded` как событие.
|
- [x] Результат боя: `Hit` / `Guarded` как событие.
|
||||||
- [ ] Событийная шина и три обработчика: Continuous / Activated / Triggered.
|
- [ ] Событийная шина и три обработчика: Continuous / Activated / Triggered.
|
||||||
- [ ] Check Timing: сбор → сортировка → разрешение по одному → повтор.
|
- [ ] Check Timing: сбор → сортировка → разрешение по одному → повтор.
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ public sealed record CardDefinition
|
||||||
/// <summary>Печатный перехват: по умолчанию у карт 2 ранга.</summary>
|
/// <summary>Печатный перехват: по умолчанию у карт 2 ранга.</summary>
|
||||||
public bool HasIntercept => InterceptOverride ?? Grade == 2;
|
public bool HasIntercept => InterceptOverride ?? Grade == 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Печатное число проверок при атаке из круга Авангарда:
|
||||||
|
/// 0–2 ранг — одна, 3 ранг и выше — двойная.
|
||||||
|
/// </summary>
|
||||||
|
public int DefaultDriveChecks => Grade >= 3 ? 2 : 1;
|
||||||
|
|
||||||
public string? TextEn { get; init; }
|
public string? TextEn { get; init; }
|
||||||
public string? TextRu { get; init; }
|
public string? TextRu { get; init; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ namespace Vanguard.Rules.Engine;
|
||||||
/// в круг защиты и при атаке по Авангарду, и при атаке по Арьергарду;
|
/// в круг защиты и при атаке по Авангарду, и при атаке по Арьергарду;
|
||||||
/// атакованный Арьергард не может перехватить атаку на самого себя;
|
/// атакованный Арьергард не может перехватить атаку на самого себя;
|
||||||
/// из заднего ряда — только если эффект разрешил (InterceptFromBackRow);
|
/// из заднего ряда — только если эффект разрешил (InterceptFromBackRow);
|
||||||
|
/// * число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две;
|
||||||
|
/// Арьергард по умолчанию без проверок, но эффект может их выдать;
|
||||||
/// * триггер требует выбора целей через ResolveTriggerCommand:
|
/// * триггер требует выбора целей через ResolveTriggerCommand:
|
||||||
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
|
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
|
||||||
/// правило одинаково для drive check и damage check;
|
/// правило одинаково для drive check и damage check;
|
||||||
|
|
@ -25,7 +27,6 @@ public sealed class GameEngine
|
||||||
{
|
{
|
||||||
public const int StartingHand = 5;
|
public const int StartingHand = 5;
|
||||||
public const int MaxDamage = 6;
|
public const int MaxDamage = 6;
|
||||||
public const int DriveChecks = 2;
|
|
||||||
public const int TriggerPowerBonus = 5000;
|
public const int TriggerPowerBonus = 5000;
|
||||||
|
|
||||||
private readonly List<GameEvent> _events = new();
|
private readonly List<GameEvent> _events = new();
|
||||||
|
|
@ -48,6 +49,18 @@ public sealed class GameEngine
|
||||||
public GameState State { get; }
|
public GameState State { get; }
|
||||||
public IReadOnlyList<GameEvent> Events => _events;
|
public IReadOnlyList<GameEvent> Events => _events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сколько проверок делает атакующая карта. Эффект может изменить число;
|
||||||
|
/// Арьергард по умолчанию проверок не делает.
|
||||||
|
/// </summary>
|
||||||
|
public int DriveChecksFor(int slot, CardInstance attacker)
|
||||||
|
{
|
||||||
|
if (attacker.DriveChecksOverride is { } overridden)
|
||||||
|
return Math.Max(0, overridden);
|
||||||
|
|
||||||
|
return slot < 0 ? State.Catalog.CardOf(attacker.PrintingId).DefaultDriveChecks : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- старт партии ----------
|
// ---------- старт партии ----------
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
|
|
@ -516,13 +529,21 @@ public sealed class GameEngine
|
||||||
|
|
||||||
private void BeginResolution(BattleState battle)
|
private void BeginResolution(BattleState battle)
|
||||||
{
|
{
|
||||||
if (battle.AttackerSlot >= 0)
|
var attacker = State.GetUnit(battle.AttackerPlayer, battle.AttackerSlot);
|
||||||
|
if (attacker is null)
|
||||||
{
|
{
|
||||||
FinishBattle(battle);
|
FinishBattle(battle);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
battle.ChecksRemaining = DriveChecks;
|
var checks = DriveChecksFor(battle.AttackerSlot, attacker);
|
||||||
|
if (checks == 0)
|
||||||
|
{
|
||||||
|
FinishBattle(battle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
battle.ChecksRemaining = checks;
|
||||||
State.ActivePlayer = battle.AttackerPlayer;
|
State.ActivePlayer = battle.AttackerPlayer;
|
||||||
State.Step = Step.Drive;
|
State.Step = Step.Drive;
|
||||||
ContinueDriveChecks(battle);
|
ContinueDriveChecks(battle);
|
||||||
|
|
@ -546,6 +567,8 @@ public sealed class GameEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
var def = State.Catalog.CardOf(card.PrintingId);
|
var def = State.Catalog.CardOf(card.PrintingId);
|
||||||
|
_events.Add(new DriveCheckedEvent(player, card.PrintingId, def.Trigger));
|
||||||
|
|
||||||
if (def.Trigger == TriggerType.None)
|
if (def.Trigger == TriggerType.None)
|
||||||
{
|
{
|
||||||
State.Players[player].Hand.Add(card);
|
State.Players[player].Hand.Add(card);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ public sealed record CardDrawnEvent(int Player) : GameEvent;
|
||||||
public sealed record UnitRodeEvent(int Player, string PrintingId) : GameEvent;
|
public sealed record UnitRodeEvent(int Player, string PrintingId) : GameEvent;
|
||||||
public sealed record UnitCalledEvent(int Player, int Slot, string PrintingId) : GameEvent;
|
public sealed record UnitCalledEvent(int Player, int Slot, string PrintingId) : GameEvent;
|
||||||
public sealed record UnitMovedEvent(int Player, int FromSlot, int ToSlot) : GameEvent;
|
public sealed record UnitMovedEvent(int Player, int FromSlot, int ToSlot) : GameEvent;
|
||||||
|
public sealed record DriveCheckedEvent(int Player, string PrintingId, TriggerType Trigger) : GameEvent;
|
||||||
public sealed record AttackDeclaredEvent(int Player, int AttackerSlot, int TargetSlot) : GameEvent;
|
public sealed record AttackDeclaredEvent(int Player, int AttackerSlot, int TargetSlot) : GameEvent;
|
||||||
public sealed record BoostedEvent(int Player, int BoosterSlot, int AttackerSlot) : GameEvent;
|
public sealed record BoostedEvent(int Player, int BoosterSlot, int AttackerSlot) : GameEvent;
|
||||||
public sealed record GuardAddedEvent(int Player, string PrintingId, int Shield) : GameEvent;
|
public sealed record GuardAddedEvent(int Player, string PrintingId, int Shield) : GameEvent;
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,12 @@ public sealed class CardInstance
|
||||||
|
|
||||||
/// <summary>Эффект разрешил перехватывать из заднего ряда.</summary>
|
/// <summary>Эффект разрешил перехватывать из заднего ряда.</summary>
|
||||||
public bool InterceptFromBackRow { get; set; }
|
public bool InterceptFromBackRow { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Переопределение числа проверок, выданное эффектом.
|
||||||
|
/// null — по умолчанию: 1 для 0–2 ранга, 2 для 3+, у Арьергарда 0.
|
||||||
|
/// </summary>
|
||||||
|
public int? DriveChecksOverride { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerState
|
public sealed class PlayerState
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,73 @@ public class GameEngineTests
|
||||||
Assert.False(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
Assert.False(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Grade_One_Vanguard_Performs_One_Check()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttack();
|
||||||
|
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||||
|
|
||||||
|
engine.Execute(new AttackCommand(-1, -1));
|
||||||
|
engine.Execute(new PassGuardCommand());
|
||||||
|
|
||||||
|
Assert.Single(engine.Events.OfType<DriveCheckedEvent>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Grade_Three_Vanguard_Performs_Two_Checks()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
engine.Execute(new SkipRideCommand());
|
||||||
|
SetVanguard(engine, 0, Overlord);
|
||||||
|
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||||
|
engine.Execute(new EnterBattleCommand());
|
||||||
|
|
||||||
|
engine.Execute(new AttackCommand(-1, -1));
|
||||||
|
engine.Execute(new PassGuardCommand());
|
||||||
|
|
||||||
|
Assert.Equal(2, engine.Events.OfType<DriveCheckedEvent>().Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Twin_Drive_Can_Be_Removed_By_Effect()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
engine.Execute(new SkipRideCommand());
|
||||||
|
SetVanguard(engine, 0, Overlord);
|
||||||
|
engine.State.Players[0].Vanguard!.DriveChecksOverride = 1;
|
||||||
|
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||||||
|
engine.Execute(new EnterBattleCommand());
|
||||||
|
|
||||||
|
engine.Execute(new AttackCommand(-1, -1));
|
||||||
|
engine.Execute(new PassGuardCommand());
|
||||||
|
|
||||||
|
Assert.Single(engine.Events.OfType<DriveCheckedEvent>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Rear_Guard_Performs_No_Checks_By_Default()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttackRearGuard();
|
||||||
|
|
||||||
|
engine.Execute(new AttackCommand(3, 0));
|
||||||
|
engine.Execute(new PassGuardCommand());
|
||||||
|
|
||||||
|
Assert.Empty(engine.Events.OfType<DriveCheckedEvent>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Effect_Can_Give_Drive_Check_To_Rear_Guard()
|
||||||
|
{
|
||||||
|
var engine = ReadyToAttackRearGuard();
|
||||||
|
engine.State.Players[0].RearGuards[3]!.DriveChecksOverride = 1;
|
||||||
|
StackDeck(engine.State.Players[0], Gallatin);
|
||||||
|
|
||||||
|
engine.Execute(new AttackCommand(3, 0));
|
||||||
|
engine.Execute(new PassGuardCommand());
|
||||||
|
|
||||||
|
Assert.Single(engine.Events.OfType<DriveCheckedEvent>());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue