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