Card choice from hand as an effect cost
AskHandCardChoice pauses on the hand and returns the chosen card, so abilities can pay discard costs interactively. Knight of Rose, Morgana implemented: on attack, optionally discard a card for +4000 until end of turn. Test covers the full pause chain and the return to the guard step.
This commit is contained in:
parent
6538a470da
commit
6f5bd4e287
|
|
@ -120,6 +120,7 @@
|
||||||
- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
|
- [x] Выбор целей: `PendingChoice` + `ChooseCommand`, используется Blaster Blade для уничтожения Арьергарда противника.
|
||||||
- [ ] Стоимости: 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] Поиск в колоде без просмотра: 0 совпадений — уведомление и перемешивание, 1 — сразу в руку, несколько — выбор; разные арты считаются разными картами.
|
- [x] Поиск в колоде без просмотра: 0 совпадений — уведомление и перемешивание, 1 — сразу в руку, несколько — выбор; разные арты считаются разными картами.
|
||||||
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API, Llew).
|
- [x] Поиск с призывом в круг Арьергарда (Alfred) и с райдом (API, Llew).
|
||||||
- [x] Просмотр верхней карты с решением «оставить на верху / положить в низ» (Cocoa).
|
- [x] Просмотр верхней карты с решением «оставить на верху / положить в низ» (Cocoa).
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,42 @@ public static class CardScripts
|
||||||
["tyrant-deathrex"] = new DeathrexScript(),
|
["tyrant-deathrex"] = new DeathrexScript(),
|
||||||
["demon-eater"] = new DemonEaterScript(),
|
["demon-eater"] = new DemonEaterScript(),
|
||||||
["maiden-of-libra"] = new MaidenOfLibraScript(),
|
["maiden-of-libra"] = new MaidenOfLibraScript(),
|
||||||
|
["knight-of-rose-morgana"] = new MorganaScript(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Knight of Rose, Morgana: при атаке можно сбросить карту из руки и получить +4000 до конца хода.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MorganaScript : CardScript
|
||||||
|
{
|
||||||
|
public override string NameId => "knight-of-rose-morgana";
|
||||||
|
|
||||||
|
public override IEnumerable<AbilityDefinition> Abilities
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
yield return new AbilityDefinition
|
||||||
|
{
|
||||||
|
Trigger = AbilityTrigger.OnAttackDeclared,
|
||||||
|
Optional = true,
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Tyrant, Deathrex: когда эта карта атакует, получает +5000 до конца боя.</summary>
|
/// <summary>Tyrant, Deathrex: когда эта карта атакует, получает +5000 до конца боя.</summary>
|
||||||
public sealed class DeathrexScript : CardScript
|
public sealed class DeathrexScript : CardScript
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -692,6 +692,33 @@ public sealed class GameEngine
|
||||||
public bool HasEmptyRearGuard(int player) =>
|
public bool HasEmptyRearGuard(int player) =>
|
||||||
State.Players[player].RearGuards.Any(u => u is null);
|
State.Players[player].RearGuards.Any(u => u is null);
|
||||||
|
|
||||||
|
/// <summary>Выбор карты из руки (стоимость, сброс, цель эффекта).</summary>
|
||||||
|
public void AskHandCardChoice(
|
||||||
|
int player,
|
||||||
|
string prompt,
|
||||||
|
Func<CardInstance, bool>? predicate,
|
||||||
|
Action<CardInstance> onChosen)
|
||||||
|
{
|
||||||
|
var cards = State.Players[player].Hand
|
||||||
|
.Where(c => predicate is null || predicate(c))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (cards.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
State.PendingChoice = new PendingChoice
|
||||||
|
{
|
||||||
|
Player = player,
|
||||||
|
Prompt = prompt,
|
||||||
|
Options = cards
|
||||||
|
.Select((card, index) => new ChoiceOption(index, State.Catalog.CardOf(card.PrintingId).NameEn))
|
||||||
|
.ToList(),
|
||||||
|
Resolve = index => onChosen(cards[index]),
|
||||||
|
};
|
||||||
|
State.Step = Step.Choice;
|
||||||
|
State.ActivePlayer = player;
|
||||||
|
}
|
||||||
|
|
||||||
public void AskEmptyRearGuardChoice(int player, string prompt, Action<int> onChosen)
|
public void AskEmptyRearGuardChoice(int player, string prompt, Action<int> onChosen)
|
||||||
{
|
{
|
||||||
var slots = Enumerable.Range(0, PlayerState.RearGuardSlots)
|
var slots = Enumerable.Range(0, PlayerState.RearGuardSlots)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ public class GameEngineTests
|
||||||
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
|
||||||
|
private const string Morgana = "TD01/011EN"; // Royal Paladin, grade 1, discard +4000
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
||||||
|
|
@ -940,6 +941,30 @@ public class GameEngineTests
|
||||||
Assert.NotNull(engine.State.PendingAbility);
|
Assert.NotNull(engine.State.PendingAbility);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Morgana_Discards_To_Gain_4000_When_Attacking()
|
||||||
|
{
|
||||||
|
var engine = StartedEngine();
|
||||||
|
engine.Execute(new SkipRideCommand());
|
||||||
|
SetVanguard(engine, 0, Morgana);
|
||||||
|
engine.Execute(new EnterBattleCommand());
|
||||||
|
|
||||||
|
var morgana = engine.State.Players[0].Vanguard!;
|
||||||
|
var handBefore = engine.State.Players[0].Hand.Count;
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||||||
|
Assert.NotNull(engine.State.PendingAbility);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||||||
|
Assert.NotNull(engine.State.PendingChoice);
|
||||||
|
|
||||||
|
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||||||
|
|
||||||
|
Assert.Equal(4000, morgana.TemporaryPower);
|
||||||
|
Assert.Equal(handBefore - 1, engine.State.Players[0].Hand.Count);
|
||||||
|
Assert.Equal(Step.Guard, engine.State.Step);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||||
{
|
{
|
||||||
|
|
@ -1079,6 +1104,7 @@ public class GameEngineTests
|
||||||
(Lien, 2),
|
(Lien, 2),
|
||||||
(Cocoa, 2), (Libra, 2),
|
(Cocoa, 2), (Libra, 2),
|
||||||
(Deathrex, 2), (DemonEater, 2),
|
(Deathrex, 2), (DemonEater, 2),
|
||||||
|
(Morgana, 2),
|
||||||
};
|
};
|
||||||
|
|
||||||
private List<string> BuildDeck()
|
private List<string> BuildDeck()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue