diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md
index 62a6b02..d6d42c9 100644
--- a/IMPLEMENTATION_CHECKLIST.md
+++ b/IMPLEMENTATION_CHECKLIST.md
@@ -121,6 +121,7 @@
- [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck.
- [x] Выбор цели игроком (`PendingChoice` / `ChooseCommand`); «до N» / «ровно N» ещё нет.
- [x] Выбор карты из руки как стоимости эффекта (Morgana: сброс → +4000).
+- [x] Модификаторы силы/крита с источником и сроком жизни (`StatModifier`); выдача и снятие способностей эффектами (`GrantedAbility`).
- [x] Поиск в колоде без просмотра: 0 совпадений — уведомление и перемешивание, 1 — сразу в руку, несколько — выбор; разные арты считаются разными картами.
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API, Llew).
- [x] Просмотр верхней карты с решением «оставить на верху / положить в низ» (Cocoa).
diff --git a/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs b/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs
index 7464565..4c3d4d5 100644
--- a/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs
+++ b/server/src/Vanguard.Rules/Engine/Abilities/CardScripts.cs
@@ -1,8 +1,8 @@
-using Vanguard.Rules.Cards;
+using Vanguard.Rules.Cards;
namespace Vanguard.Rules.Engine.Abilities;
-/// Скрипты карт первого сета, подключённые к движку.
+/// Скрипты карт первого сета, подключённые к движку.
public static class CardScripts
{
public static IReadOnlyDictionary Default { get; } =
@@ -25,7 +25,7 @@ public static class CardScripts
}
///
-/// Knight of Rose, Morgana: при атаке можно сбросить карту из руки и получить +4000 до конца хода.
+/// Knight of Rose, Morgana: при атаке можно сбросить карту из руки и получить +4000 до конца хода.
///
public sealed class MorganaScript : CardScript
{
@@ -42,21 +42,21 @@ public sealed class MorganaScript : CardScript
CanPay = ctx => ctx.State.Players[ctx.Owner].Hand.Count > 0,
Resolve = ctx => ctx.Engine.AskHandCardChoice(
ctx.Owner,
- "Сбросьте карту из руки",
+ "Сбросьте карту из руки",
null,
card =>
{
var p = ctx.State.Players[ctx.Owner];
p.Hand.Remove(card);
p.Drop.Add(card);
- ctx.Source.TemporaryPower += 4000;
+ ctx.Engine.AddPower(ctx.Source, 4000, ctx.Source, "morgana", ModifierDuration.UntilEndOfTurn);
}),
};
}
}
}
-/// Tyrant, Deathrex: когда эта карта атакует, получает +5000 до конца боя.
+/// Tyrant, Deathrex: когда эта карта атакует, получает +5000 до конца боя.
public sealed class DeathrexScript : CardScript
{
public override string NameId => "tyrant-deathrex";
@@ -68,13 +68,13 @@ public sealed class DeathrexScript : CardScript
yield return new AbilityDefinition
{
Trigger = AbilityTrigger.OnAttackDeclared,
- Resolve = ctx => ctx.Engine.AddBattlePower(5000),
+ Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 5000),
};
}
}
}
-/// Demon Eater: в начале Основной фазы Soul Charge 1 и +2000 до конца хода.
+/// Demon Eater: в начале Основной фазы Soul Charge 1 и +2000 до конца хода.
public sealed class DemonEaterScript : CardScript
{
public override string NameId => "demon-eater";
@@ -89,14 +89,14 @@ public sealed class DemonEaterScript : CardScript
Resolve = ctx =>
{
ctx.Engine.SoulCharge(ctx.Owner, 1);
- ctx.Source.TemporaryPower += 2000;
+ ctx.Engine.AddPower(ctx.Source, 2000, ctx.Source, "demon-eater", ModifierDuration.UntilEndOfTurn);
},
};
}
}
}
-/// Maiden of Libra: когда атака попадает, Counter Blast 2 и взять карту.
+/// Maiden of Libra: когда атака попадает, Counter Blast 2 и взять карту.
public sealed class MaidenOfLibraScript : CardScript
{
public override string NameId => "maiden-of-libra";
@@ -118,8 +118,8 @@ public sealed class MaidenOfLibraScript : CardScript
}
///
-/// Battle Sister, Cocoa: при размещении на VC/RC, если Авангард Oracle Think Tank,
-/// посмотреть верхнюю карту и оставить её на верху или отправить в низ.
+/// Battle Sister, Cocoa: при размещении на VC/RC, если Авангард Oracle Think Tank,
+/// посмотреть верхнюю карту и оставить её на верху или отправить в низ.
///
public sealed class CocoaScript : CardScript
{
@@ -133,15 +133,15 @@ public sealed class CocoaScript : CardScript
{
Trigger = AbilityTrigger.OnPlace,
Condition = ctx => ctx.Engine.VanguardClan(ctx.Owner) == Clan.OracleThinkTank,
- Resolve = ctx => ctx.Engine.LookTopAndReorder(ctx.Owner, "Верхняя карта колоды"),
+ Resolve = ctx => ctx.Engine.LookTopAndReorder(ctx.Owner, "Верхняя карта колоды"),
};
}
}
}
///
-/// Lizard Soldier, Conroe: [ACT](RC): Counter Blast 1 и уничтожить эту карту →
-/// найти в колоде Kagero 1 ранга или ниже и добавить в руку.
+/// Lizard Soldier, Conroe: [ACT](RC): Counter Blast 1 и уничтожить эту карту →
+/// найти в колоде Kagero 1 ранга или ниже и добавить в руку.
///
public sealed class ConroeScript : CardScript
{
@@ -160,7 +160,7 @@ public sealed class ConroeScript : CardScript
ctx.Engine.CounterBlast(ctx.Owner, 1);
ctx.Engine.SearchDeckToHand(
ctx.Owner,
- "Найдите Kagero 1 ранга или ниже",
+ "Найдите Kagero 1 ранга или ниже",
d => d.Clan == Clan.Kagero && d.Grade <= 1);
},
};
@@ -169,8 +169,8 @@ public sealed class ConroeScript : CardScript
}
///
-/// «[ACT]: повернуть эту карту и сбросить карту из руки → взять карту».
-/// Общий скрипт для Lake Maiden, Lien, Dragon Monk, Gojo и Shout.
+/// «[ACT]: повернуть эту карту и сбросить карту из руки → взять карту».
+/// Общий скрипт для Lake Maiden, Lien, Dragon Monk, Gojo и Shout.
///
public sealed class DrawOneScript : CardScript
{
@@ -194,8 +194,8 @@ public sealed class DrawOneScript : CardScript
}
///
-/// Blaster Blade: при размещении оплатить Counter Blast 2 и уничтожить Арьергард противника.
-/// На Авангарде — любой, на Арьергарде — только 2 ранга или выше и при Авангарде Royal Paladin.
+/// Blaster Blade: при размещении оплатить Counter Blast 2 и уничтожить Арьергард противника.
+/// На Авангарде — любой, на Арьергарде — только 2 ранга или выше и при Авангарде Royal Paladin.
///
public sealed class BlasterBladeScript : CardScript
{
@@ -222,7 +222,7 @@ public sealed class BlasterBladeScript : CardScript
ctx.Engine.AskUnitChoice(
ctx.Owner,
opponent,
- "Выберите Арьергард противника",
+ "Выберите Арьергард противника",
slots,
slot => ctx.Engine.RetireRearGuard(opponent, slot));
},
@@ -247,7 +247,7 @@ public sealed class BlasterBladeScript : CardScript
ctx.Engine.AskUnitChoice(
ctx.Owner,
opponent,
- "Выберите Арьергард противника 2 ранга или выше",
+ "Выберите Арьергард противника 2 ранга или выше",
slots,
slot => ctx.Engine.RetireRearGuard(opponent, slot));
},
@@ -257,7 +257,7 @@ public sealed class BlasterBladeScript : CardScript
}
///
-/// Barcgal: когда другой Royal Paladin сменяет эту карту, вы можете призвать её в круг Арьергарда.
+/// Barcgal: когда другой Royal Paladin сменяет эту карту, вы можете призвать её в круг Арьергарда.
///
public sealed class BarcgalScript : CardScript
{
@@ -281,7 +281,7 @@ public sealed class BarcgalScript : CardScript
}
///
-/// Wingal: когда эта карта поддерживает Blaster Blade, усиленная карта получает +4000 до конца боя.
+/// Wingal: когда эта карта поддерживает Blaster Blade, усиленная карта получает +4000 до конца боя.
///
public sealed class WingalScript : CardScript
{
@@ -299,15 +299,15 @@ public sealed class WingalScript : CardScript
Condition = ctx =>
ctx.Other is not null &&
ctx.Catalog.CardOf(ctx.Other.PrintingId).NameId == BlasterBlade,
- Resolve = ctx => ctx.Engine.AddBattlePower(4000),
+ Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Other!, 4000, ctx.Source),
};
}
}
}
///
-/// King of Knights, Alfred: в свой ход на круге Авангарда получает +2000
-/// за каждый свой Арьергард Royal Paladin.
+/// King of Knights, Alfred: в свой ход на круге Авангарда получает +2000
+/// за каждый свой Арьергард Royal Paladin.
///
public sealed class AlfredScript : CardScript
{
@@ -341,10 +341,11 @@ public sealed class AlfredScript : CardScript
ctx.Engine.CounterBlast(ctx.Owner, 3);
ctx.Engine.SearchDeckToCall(
ctx.Owner,
- "Найдите Royal Paladin 2 ранга или ниже",
+ "Найдите Royal Paladin 2 ранга или ниже",
d => d.Clan == Clan.RoyalPaladin && d.Grade <= 2);
},
};
}
}
}
+
diff --git a/server/src/Vanguard.Rules/Engine/GameEngine.cs b/server/src/Vanguard.Rules/Engine/GameEngine.cs
index 5b7b375..4777129 100644
--- a/server/src/Vanguard.Rules/Engine/GameEngine.cs
+++ b/server/src/Vanguard.Rules/Engine/GameEngine.cs
@@ -18,6 +18,8 @@ namespace Vanguard.Rules.Engine;
/// из заднего ряда — только если эффект разрешил (InterceptFromBackRow);
/// * число проверок принадлежит атакующей карте: 0–2 ранг — одна, 3+ — две;
/// Арьергард по умолчанию без проверок, но эффект может их выдать;
+/// * модификаторы силы и крита хранят источник и срок жизни (бой / ход / постоянно);
+/// способности можно выдавать эффектом и снимать по источнику;
/// * эффекты попадания срабатывают после нанесения урона и помещения карты в зону урона;
/// * эффекты не вкладываются друг в друга: триггеры, поднятые во время эффекта,
/// копятся в очереди и разрешаются после него;
@@ -84,10 +86,6 @@ public sealed class GameEngine
if (source is null)
return;
- var nameId = State.Catalog.CardOf(source.PrintingId).NameId;
- if (!_scripts.TryGetValue(nameId, out var script))
- return;
-
var context = new AbilityContext
{
Engine = this,
@@ -97,7 +95,7 @@ public sealed class GameEngine
Phase = State.Phase,
};
- var triggered = script.Abilities
+ var triggered = AbilitiesOf(source)
.Where(a => a.Trigger == trigger && (a.Condition is null || a.Condition(context)))
.Select(a => new TriggeredAbility { Definition = a, Context = context })
.ToList();
@@ -108,6 +106,20 @@ public sealed class GameEngine
RaiseBatch(triggered);
}
+ /// Печатные способности карты плюс выданные эффектами.
+ private IEnumerable AbilitiesOf(CardInstance unit)
+ {
+ var nameId = State.Catalog.CardOf(unit.PrintingId).NameId;
+ if (_scripts.TryGetValue(nameId, out var script))
+ {
+ foreach (var ability in script.Abilities)
+ yield return ability;
+ }
+
+ foreach (var granted in unit.GrantedAbilities)
+ yield return granted.Definition;
+ }
+
private void RaiseBatch(List triggered)
{
if (triggered.Count == 0)
@@ -141,10 +153,6 @@ public sealed class GameEngine
{
foreach (var unit in AllUnits(owner))
{
- var nameId = State.Catalog.CardOf(unit.PrintingId).NameId;
- if (!_scripts.TryGetValue(nameId, out var script))
- continue;
-
var context = new AbilityContext
{
Engine = this,
@@ -153,7 +161,7 @@ public sealed class GameEngine
Phase = State.Phase,
};
- triggered.AddRange(script.Abilities
+ triggered.AddRange(AbilitiesOf(unit)
.Where(a => a.Trigger == trigger && (a.Condition is null || a.Condition(context)))
.Select(a => new TriggeredAbility { Definition = a, Context = context }));
}
@@ -397,13 +405,17 @@ public sealed class GameEngine
return CommandResult.Ok();
}
- /// Сила карты с учётом непрерывных эффектов. Пересчитывается при каждом запросе.
+ /// Сила карты с учётом всех модификаторов и непрерывных эффектов.
public int EffectivePower(CardInstance unit)
{
- var total = State.Catalog.CardOf(unit.PrintingId).Power + unit.TemporaryPower;
+ var total = State.Catalog.CardOf(unit.PrintingId).Power;
+ total += unit.Modifiers.Sum(m => m.Power);
return total + ContinuousPower(unit);
}
+ public int EffectiveCritical(CardInstance unit) =>
+ State.Catalog.CardOf(unit.PrintingId).Critical + unit.Modifiers.Sum(m => m.Critical);
+
private int ContinuousPower(CardInstance unit)
{
var owner = OwnerOf(unit);
@@ -468,10 +480,53 @@ public sealed class GameEngine
}
/// Бонус к силе текущего боя от способности карты.
- public void AddBattlePower(int amount)
+ public void AddBattlePower(CardInstance source, int amount) =>
+ AddBattlePower(source, amount, source);
+
+ public void AddBattlePower(CardInstance target, int amount, CardInstance source) =>
+ AddPower(target, amount, source, "ability", ModifierDuration.UntilEndOfBattle);
+
+ /// Добавить модификатор силы и крита с указанием источника и срока жизни.
+ public void AddPower(
+ CardInstance unit,
+ int power,
+ CardInstance source,
+ string effectId,
+ ModifierDuration duration,
+ int critical = 0)
{
- if (State.Battle is { } battle)
- battle.AbilityPower += amount;
+ unit.Modifiers.Add(new StatModifier
+ {
+ Power = power,
+ Critical = critical,
+ Source = source,
+ EffectId = effectId,
+ Duration = duration,
+ });
+ }
+
+ /// Выдать карте способность, которой нет в её тексте.
+ public void GrantAbility(
+ CardInstance unit,
+ AbilityDefinition definition,
+ CardInstance source,
+ string effectId,
+ ModifierDuration duration)
+ {
+ unit.GrantedAbilities.Add(new GrantedAbility
+ {
+ Definition = definition,
+ Source = source,
+ EffectId = effectId,
+ Duration = duration,
+ });
+ }
+
+ /// Снять способности, выданные конкретным эффектом.
+ public void RemoveGrantedAbilities(Func predicate)
+ {
+ foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
+ unit.GrantedAbilities.RemoveAll(g => predicate(g));
}
// ---------- стоимости ----------
@@ -1119,7 +1174,13 @@ public sealed class GameEngine
if (booster is not null)
{
booster.Rested = true;
- State.Battle.BoostPower += State.Catalog.CardOf(booster.PrintingId).Power;
+ AddPower(
+ attacker,
+ State.Catalog.CardOf(booster.PrintingId).Power,
+ booster,
+ "boost",
+ ModifierDuration.UntilEndOfBattle);
+
_events.Add(new BoostedEvent(attackerPlayer, boosterSlot, attackerSlot));
TriggerFromCard(attackerPlayer, booster, AbilityTrigger.OnBoost, attacker);
@@ -1313,8 +1374,8 @@ public sealed class GameEngine
{
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
{
- unit.TemporaryPower = 0;
- unit.TemporaryCritical = 0;
+ unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfTurn);
+ unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfTurn);
}
}
@@ -1418,12 +1479,21 @@ public sealed class GameEngine
}
if (pending.GrantsPower)
- State.GetUnit(player, command.PowerTarget)!.TemporaryPower += TriggerPowerBonus;
+ {
+ var powerTarget = State.GetUnit(player, command.PowerTarget)!;
+ AddPower(powerTarget, TriggerPowerBonus, pending.Card, "trigger", ModifierDuration.UntilEndOfTurn);
+ }
switch (pending.Trigger)
{
case TriggerType.Critical:
- State.GetUnit(player, command.EffectTarget)!.TemporaryCritical += 1;
+ AddPower(
+ State.GetUnit(player, command.EffectTarget)!,
+ 0,
+ pending.Card,
+ "trigger",
+ ModifierDuration.UntilEndOfTurn,
+ critical: 1);
break;
case TriggerType.Stand:
State.GetUnit(player, command.EffectTarget)!.Rested = false;
@@ -1480,7 +1550,7 @@ public sealed class GameEngine
var attackerDef = State.Catalog.CardOf(attacker.PrintingId);
var defenseUnit = battle.Interceptor ?? target;
- var attackPower = EffectivePower(attacker) + battle.BoostPower + battle.AbilityPower;
+ var attackPower = EffectivePower(attacker);
var defensePower = EffectivePower(defenseUnit) + battle.GuardShield;
var hit = !battle.Nullified && attackPower > defensePower;
@@ -1496,7 +1566,7 @@ public sealed class GameEngine
}
else if (battle.TargetSlot < 0)
{
- var damage = attackerDef.Critical + attacker.TemporaryCritical;
+ var damage = EffectiveCritical(attacker);
void AfterDamage()
{
@@ -1551,6 +1621,12 @@ public sealed class GameEngine
defender.Drop.Add(guard);
}
+ foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
+ {
+ unit.Modifiers.RemoveAll(m => m.Duration == ModifierDuration.UntilEndOfBattle);
+ unit.GrantedAbilities.RemoveAll(g => g.Duration == ModifierDuration.UntilEndOfBattle);
+ }
+
State.Battle = null;
State.PendingTrigger = null;
State.Step = Step.None;
diff --git a/server/src/Vanguard.Rules/Engine/GameModels.cs b/server/src/Vanguard.Rules/Engine/GameModels.cs
index 4a55de8..d64a1ea 100644
--- a/server/src/Vanguard.Rules/Engine/GameModels.cs
+++ b/server/src/Vanguard.Rules/Engine/GameModels.cs
@@ -72,12 +72,11 @@ public sealed class CardInstance
/// Открыта ли карта. Для урона — лицом вверх после Counter Blast.
public bool FaceUp { get; set; }
- /// Временный бонус силы до конца текущего боя (триггеры, эффекты).
- public int TemporaryPower { get; set; }
-
- /// Временный бонус крита до конца текущего боя.
- public int TemporaryCritical { get; set; }
+ /// Модификаторы силы и крита с указанием источника и срока жизни.
+ public List Modifiers { get; } = new();
+ /// Способности, выданные эффектами, а не напечатанные на карте.
+ public List GrantedAbilities { get; } = new();
///
/// Переопределение способности поддержки, выданное эффектом.
/// null — как напечатано на карте.
diff --git a/server/src/Vanguard.Rules/Engine/GameState.cs b/server/src/Vanguard.Rules/Engine/GameState.cs
index 6cc5c10..483f8af 100644
--- a/server/src/Vanguard.Rules/Engine/GameState.cs
+++ b/server/src/Vanguard.Rules/Engine/GameState.cs
@@ -22,11 +22,6 @@ public sealed class BattleState
/// Карта, вышедшая на перехват. Сила сравнения берётся по ней.
public CardInstance? Interceptor { get; set; }
- /// Сила карт поддержки, действует только в этом бою.
- public int BoostPower { get; set; }
-
- /// Сила от способностей карт, действует только в этом бою.
- public int AbilityPower { get; set; }
public List GuardCards { get; } = new();
}
diff --git a/server/src/Vanguard.Rules/Engine/Modifiers.cs b/server/src/Vanguard.Rules/Engine/Modifiers.cs
new file mode 100644
index 0000000..4caedd6
--- /dev/null
+++ b/server/src/Vanguard.Rules/Engine/Modifiers.cs
@@ -0,0 +1,32 @@
+using Vanguard.Rules.Engine.Abilities;
+
+namespace Vanguard.Rules.Engine;
+
+public enum ModifierDuration
+{
+ UntilEndOfBattle,
+ UntilEndOfTurn,
+ Permanent,
+}
+
+///
+/// Прибавка к силе или криту. Знает, от какой карты пришла и каким эффектом,
+/// поэтому её можно точечно снять, не трогая остальные.
+///
+public sealed class StatModifier
+{
+ public required int Power { get; init; }
+ public int Critical { get; init; }
+ public required CardInstance Source { get; init; }
+ public required string EffectId { get; init; }
+ public required ModifierDuration Duration { get; init; }
+}
+
+/// Способность, выданная эффектом и не напечатанная на карте.
+public sealed class GrantedAbility
+{
+ public required AbilityDefinition Definition { get; init; }
+ public required CardInstance Source { get; init; }
+ public required string EffectId { get; init; }
+ public required ModifierDuration Duration { get; init; }
+}
diff --git a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
index 2975f8f..ee695de 100644
--- a/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
+++ b/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs
@@ -1,5 +1,6 @@
-using Vanguard.Rules.Cards;
+using Vanguard.Rules.Cards;
using Vanguard.Rules.Engine;
+using Vanguard.Rules.Engine.Abilities;
using Xunit;
namespace Vanguard.Rules.Tests;
@@ -8,7 +9,7 @@ public class GameEngineTests
{
private readonly CardCatalog _catalog = TestCatalog.Instance;
- private const string Barcgal = "BT01/003EN"; // grade 0, без триггера — стартовый Авангард
+ private const string Barcgal = "BT01/003EN"; // grade 0, без триггера — стартовый Авангард
private const string Marron = "BT01/042EN"; // grade 1, 8000
private const string Gallatin = "BT01/021EN"; // grade 2, 10000
private const string Alfred = "BT01/001EN"; // grade 3
@@ -18,11 +19,11 @@ public class GameEngineTests
private const string Iseult = "BT01/011EN"; // Sentinel, Royal Paladin
private const string Nehalem = "BT01/022EN"; // Kagero
private const string Overlord = "BT01/004EN"; // Kagero, 11000
- private const string Conroe = "BT01/016EN"; // Kagero, grade 0 без триггера
+ private const string Conroe = "BT01/016EN"; // Kagero, grade 0 без триггера
private const string BlasterBlade = "BT01/002EN";
private const string Wingal = "BT01/044EN";
- private const string Lien = "BT01/043EN"; // activated: rest + discard → draw
- private const string Cocoa = "BT01/007EN"; // OTT, смотрит верхнюю карту
+ private const string Lien = "BT01/043EN"; // activated: rest + discard в†’ draw
+ private const string Cocoa = "BT01/007EN"; // OTT, смотрит верхнюю карту
private const string Libra = "BT01/017EN"; // OTT, grade 2
private const string Deathrex = "BT01/033EN"; // Tachikaze, 10000
private const string DemonEater = "BT01/036EN"; // Dark Irregulars, 10000
@@ -179,7 +180,7 @@ public class GameEngineTests
Assert.NotNull(pending);
Assert.Equal(TriggerContext.DamageCheck, pending!.Context);
- // до разрешения карта лежит в зоне триггера, а не в уроне
+ // до разрешения карта лежит в зоне триггера, а не в уроне
Assert.Empty(engine.State.Players[1].Damage);
Assert.Contains(engine.State.Players[1].Trigger, c => c.PrintingId == Nike);
@@ -204,10 +205,10 @@ public class GameEngineTests
engine.Execute(new ResolveTriggerCommand(-1, -1));
var vanguard = engine.State.Players[0].Vanguard!;
- Assert.Equal(GameEngine.TriggerPowerBonus, vanguard.TemporaryPower);
+ Assert.Equal(GameEngine.TriggerPowerBonus, vanguard.Modifiers.Sum(m => m.Power));
Assert.True(engine.Execute(new EndTurnCommand()).Accepted);
- Assert.Equal(0, vanguard.TemporaryPower);
+ Assert.Empty(vanguard.Modifiers);
}
[Fact]
@@ -335,8 +336,8 @@ public class GameEngineTests
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
var defender = engine.State.Players[1];
- Assert.NotNull(defender.RearGuards[0]); // цель выжила
- Assert.Null(defender.RearGuards[3]); // перехватчик покинул круг
+ Assert.NotNull(defender.RearGuards[0]); // цель выжила
+ Assert.Null(defender.RearGuards[3]); // перехватчик покинул круг
Assert.Contains(defender.Drop, c => c.PrintingId == Gallatin);
Assert.Contains(engine.Events, e => e is InterceptedEvent);
Assert.Contains(engine.Events, e => e is BattleResolvedEvent { Hit: false });
@@ -476,7 +477,7 @@ public class GameEngineTests
[Fact]
public void Move_Is_Available_Only_In_Main_Phase()
{
- var engine = StartedEngine(); // фаза Смены
+ var engine = StartedEngine(); // фаза Смены
Assert.False(engine.Execute(new MoveCommand(0, 3)).Accepted);
}
@@ -586,7 +587,7 @@ public class GameEngineTests
var engine = ReadyToAttackWithWingal(BlasterBlade);
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
- Assert.Equal(4000, engine.State.Battle!.AbilityPower);
+ Assert.Equal(4000, engine.State.Players[0].RearGuards[3]!.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
}
[Fact]
@@ -595,7 +596,7 @@ public class GameEngineTests
var engine = ReadyToAttackWithWingal(Gallatin);
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
- Assert.Equal(0, engine.State.Battle!.AbilityPower);
+ Assert.Equal(0, engine.State.Players[0].RearGuards[3]!.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
}
[Fact]
@@ -704,7 +705,7 @@ public class GameEngineTests
Assert.True(lien.Rested);
Assert.Contains(engine.State.Players[0].Drop, c => c.PrintingId == Marron);
- Assert.Equal(handBefore, engine.State.Players[0].Hand.Count); // -1 сброс, +1 добор
+ Assert.Equal(handBefore, engine.State.Players[0].Hand.Count); // -1 СЃР±СЂРѕСЃ, +1 РґРѕР±РѕСЂ
}
[Fact]
@@ -737,7 +738,7 @@ public class GameEngineTests
var engine = StartedEngine();
var handBefore = engine.State.Players[0].Hand.Count;
- engine.SearchDeckToHand(0, "Карта X", d => d.NameId == "no-such-card");
+ engine.SearchDeckToHand(0, "Карта X", d => d.NameId == "no-such-card");
Assert.Contains(engine.Events, e => e is SearchFailedEvent);
Assert.Null(engine.State.PendingChoice);
@@ -885,7 +886,7 @@ public class GameEngineTests
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
- Assert.Equal(5000, engine.State.Battle!.AbilityPower);
+ Assert.Equal(5000, engine.State.Players[0].Vanguard!.Modifiers.Sum(m => m.Power));
}
[Fact]
@@ -936,7 +937,7 @@ public class GameEngineTests
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
- // урон уже нанесён, и только теперь спрашивают про эффект попадания
+ // урон уже нанесён, и только теперь спрашивают про эффект попадания
Assert.Single(engine.State.Players[1].Damage);
Assert.NotNull(engine.State.PendingAbility);
}
@@ -960,11 +961,85 @@ public class GameEngineTests
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
- Assert.Equal(4000, morgana.TemporaryPower);
+ Assert.Equal(4000, morgana.Modifiers.Sum(m => m.Power));
Assert.Equal(handBefore - 1, engine.State.Players[0].Hand.Count);
Assert.Equal(Step.Guard, engine.State.Step);
}
+ [Fact]
+ public void Battle_Modifiers_Expire_But_Turn_Modifiers_Stay()
+ {
+ var engine = StartedEngine();
+ SetVanguard(engine, 0, DemonEater);
+ engine.Execute(new SkipRideCommand());
+
+ var vanguard = engine.State.Players[0].Vanguard!;
+ Assert.Equal(2000, vanguard.Modifiers.Sum(m => m.Power));
+
+ StackDeck(engine.State.Players[0], Gallatin, Gallatin);
+ engine.Execute(new EnterBattleCommand());
+ engine.Execute(new AttackCommand(-1, -1));
+ engine.Execute(new PassGuardCommand());
+
+ Assert.Equal(2000, vanguard.Modifiers.Sum(m => m.Power));
+
+ engine.Execute(new EndTurnCommand());
+ Assert.Empty(vanguard.Modifiers);
+ }
+
+ [Fact]
+ public void Granted_Ability_Triggers()
+ {
+ var engine = StartedEngine();
+ engine.Execute(new SkipRideCommand());
+ SetVanguard(engine, 0, Gallatin);
+ StackDeck(engine.State.Players[0], Gallatin, Gallatin);
+ engine.Execute(new EnterBattleCommand());
+
+ var vanguard = engine.State.Players[0].Vanguard!;
+ engine.GrantAbility(
+ vanguard,
+ new AbilityDefinition
+ {
+ Trigger = AbilityTrigger.OnAttackDeclared,
+ Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
+ },
+ vanguard,
+ "test",
+ ModifierDuration.UntilEndOfTurn);
+
+ engine.Execute(new AttackCommand(-1, -1));
+
+ Assert.Equal(3000, vanguard.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
+ }
+
+ [Fact]
+ public void Granted_Ability_Can_Be_Removed()
+ {
+ var engine = StartedEngine();
+ engine.Execute(new SkipRideCommand());
+ SetVanguard(engine, 0, Gallatin);
+ StackDeck(engine.State.Players[0], Gallatin, Gallatin);
+ engine.Execute(new EnterBattleCommand());
+
+ var vanguard = engine.State.Players[0].Vanguard!;
+ engine.GrantAbility(
+ vanguard,
+ new AbilityDefinition
+ {
+ Trigger = AbilityTrigger.OnAttackDeclared,
+ Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
+ },
+ vanguard,
+ "test",
+ ModifierDuration.UntilEndOfTurn);
+
+ engine.RemoveGrantedAbilities(g => g.EffectId == "test");
+ engine.Execute(new AttackCommand(-1, -1));
+
+ Assert.Equal(0, vanguard.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
+ }
+
[Fact]
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
{
@@ -1005,7 +1080,7 @@ public class GameEngineTests
engine.Start();
engine.Execute(new FinishMulliganCommand());
- // фиксируем стартовый Авангард, чтобы Barcgal не срабатывал в обычных тестах
+ // фиксируем стартовый Авангард, чтобы Barcgal не срабатывал в обычных тестах
SetVanguard(engine, 0, Conroe);
return engine;
}
@@ -1061,14 +1136,14 @@ public class GameEngineTests
Give(engine, 0, Marron);
engine.Execute(new RideCommand(Marron));
- // сзади слева — поддержка (1 ранг), впереди слева — атакующий (2 ранг)
+ // сзади слева — поддержка (1 ранг), впереди слева — атакующий (2 ранг)
Give(engine, 0, Marron);
engine.Execute(new CallCommand(Marron, 0));
Give(engine, 0, Gallatin);
engine.Execute(new CallCommand(Gallatin, 3));
- // призванные карты входят повёрнутыми — для теста поддержки поднимаем обе
+ // призванные карты входят повёрнутыми — для теста поддержки поднимаем обе
engine.State.Players[0].RearGuards[0]!.Rested = false;
engine.State.Players[0].RearGuards[3]!.Rested = false;
@@ -1144,7 +1219,7 @@ public class GameEngineTests
{
var p = engine.State.Players[player];
var index = p.Deck.FindIndex(c => c.PrintingId == printingId);
- Assert.True(index >= 0, $"В колоде нет {printingId}");
+ Assert.True(index >= 0, $"В колоде нет {printingId}");
var card = p.Deck[index];
p.Deck.RemoveAt(index);
@@ -1157,7 +1232,7 @@ public class GameEngineTests
foreach (var printingId in printingIds)
{
var index = player.Deck.FindIndex(c => c.PrintingId == printingId);
- Assert.True(index >= 0, $"В колоде нет {printingId}");
+ Assert.True(index >= 0, $"В колоде нет {printingId}");
cards.Add(player.Deck[index]);
player.Deck.RemoveAt(index);
@@ -1195,7 +1270,7 @@ public class GameEngineTests
{
var p = engine.State.Players[player];
var index = p.Deck.FindIndex(c => c.PrintingId == printingId);
- Assert.True(index >= 0, $"В колоде нет {printingId}");
+ Assert.True(index >= 0, $"В колоде нет {printingId}");
var card = p.Deck[index];
p.Deck.RemoveAt(index);
@@ -1205,7 +1280,7 @@ public class GameEngineTests
private static CardInstance TakeFromDeck(PlayerState player, string printingId)
{
var index = player.Deck.FindIndex(c => c.PrintingId == printingId);
- Assert.True(index >= 0, $"В колоде нет {printingId}");
+ Assert.True(index >= 0, $"В колоде нет {printingId}");
var card = player.Deck[index];
player.Deck.RemoveAt(index);
@@ -1220,3 +1295,5 @@ public class GameEngineTests
return card;
}
}
+
+