From 26012ee1356abbfff9b04ea2af2ed39de9d4c874 Mon Sep 17 00:00:00 2001 From: Vivian Greenslade Date: Sat, 9 Sep 2023 01:27:11 -0230 Subject: [PATCH] [40K] Implement Kharn the Betrayer (#10885) * implements Kharn the Betrayer * added flavor text * fixed test * fixed issue with triggered ability not working * removed diacritic * added effect text to test * fixed several issues as per PR comments * fixed access * updated Kharn to use PreventionEffect * added unit test for control loss via other sources --- .../src/mage/cards/k/KharnTheBetrayer.java | 134 ++++++++++++++++++ Mage.Sets/src/mage/sets/Warhammer40000.java | 1 + .../single/_40k/KharnTheBetrayerTest.java | 71 ++++++++++ 3 files changed, 206 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KharnTheBetrayer.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/KharnTheBetrayerTest.java diff --git a/Mage.Sets/src/mage/cards/k/KharnTheBetrayer.java b/Mage.Sets/src/mage/cards/k/KharnTheBetrayer.java new file mode 100644 index 00000000000..64623458779 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KharnTheBetrayer.java @@ -0,0 +1,134 @@ +package mage.cards.k; + +import java.util.UUID; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.PreventionEffectImpl; +import mage.abilities.effects.common.DrawCardTargetEffect; +import mage.abilities.effects.common.combat.AttacksIfAbleSourceEffect; +import mage.abilities.effects.common.combat.BlocksIfAbleSourceEffect; +import mage.abilities.effects.common.continuous.GainControlTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; + +/** + * @author Xanderhall + */ +public class KharnTheBetrayer extends CardImpl { + + public KharnTheBetrayer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.ASTARTES, SubType.BERSERKER); + this.power = new MageInt(5); + this.toughness = new MageInt(1); + + // {this} attacks or blocks each combat if able + Ability ability = new SimpleStaticAbility(new AttacksIfAbleSourceEffect(Duration.WhileOnBattlefield).setText("{this} attacks")); + ability.addEffect(new BlocksIfAbleSourceEffect(Duration.WhileOnBattlefield).setText("blocks each combat if able").concatBy("or")); + this.addAbility(ability.withFlavorWord("Berzerker")); + + // When you lose control of {this}, draw two cards + this.addAbility(new KharnTheBetrayerTriggeredAbility().withFlavorWord("Sigil of Corruption")); + + // If damage would be dealt to {this}, prevent that damage and an opponent of your choice gains control of it. + this.addAbility(new SimpleStaticAbility(new KharnTheBetrayerPreventionEffect()).withFlavorWord("The Betrayer")); + } + + protected KharnTheBetrayer(final KharnTheBetrayer card) { + super(card); + } + + @Override + public KharnTheBetrayer copy() { + return new KharnTheBetrayer(this); + } + +} + +class KharnTheBetrayerTriggeredAbility extends TriggeredAbilityImpl { + + KharnTheBetrayerTriggeredAbility () { + super(Zone.BATTLEFIELD, new DrawCardTargetEffect(2)); + this.setTriggerPhrase("When you lose control of {this}, "); + } + + private KharnTheBetrayerTriggeredAbility(KharnTheBetrayerTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.LOST_CONTROL; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (event.getTargetId().equals(sourceId)) { + this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + return true; + } + return false; + } + + @Override + public KharnTheBetrayerTriggeredAbility copy() { + return new KharnTheBetrayerTriggeredAbility(this); + } +} + +class KharnTheBetrayerPreventionEffect extends PreventionEffectImpl { + + KharnTheBetrayerPreventionEffect() { + super(Duration.WhileOnBattlefield); + } + + private KharnTheBetrayerPreventionEffect(final KharnTheBetrayerPreventionEffect effect) { + super(effect); + } + + @Override + public KharnTheBetrayerPreventionEffect copy() { + return new KharnTheBetrayerPreventionEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null || preventDamageAction(event, source, game).getPreventedDamage() == 0) { + return false; + } + + TargetOpponent target = new TargetOpponent(); + if (!player.choose(outcome, target, source, game)) { + return false; + } + ContinuousEffect effect = new GainControlTargetEffect(Duration.Custom, true, target.getFirstTarget()); + effect.setTargetPointer(new FixedTarget(source.getSourceId(), game)); + + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(effect, false, "an opponent of your choice gains control of it."); + game.fireReflexiveTriggeredAbility(ability, source); + return true; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return super.applies(event, source, game) && event.getTargetId().equals(source.getSourceId()); + } + +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Warhammer40000.java b/Mage.Sets/src/mage/sets/Warhammer40000.java index eb257403eeb..3bf7a55a29c 100644 --- a/Mage.Sets/src/mage/sets/Warhammer40000.java +++ b/Mage.Sets/src/mage/sets/Warhammer40000.java @@ -150,6 +150,7 @@ public final class Warhammer40000 extends ExpansionSet { cards.add(new SetCardInfo("Inspiring Call", 217, Rarity.UNCOMMON, mage.cards.i.InspiringCall.class)); cards.add(new SetCardInfo("Island", 307, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Keeper of Secrets", 78, Rarity.RARE, mage.cards.k.KeeperOfSecrets.class)); + cards.add(new SetCardInfo("Kharn the Betrayer", 79, Rarity.RARE, mage.cards.k.KharnTheBetrayer.class)); cards.add(new SetCardInfo("Kill! Maim! Burn!", 128, Rarity.RARE, mage.cards.k.KillMaimBurn.class)); cards.add(new SetCardInfo("Knight Paladin", 160, Rarity.RARE, mage.cards.k.KnightPaladin.class)); cards.add(new SetCardInfo("Knight Rampager", 80, Rarity.RARE, mage.cards.k.KnightRampager.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/KharnTheBetrayerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/KharnTheBetrayerTest.java new file mode 100644 index 00000000000..adef92eafaa --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/_40k/KharnTheBetrayerTest.java @@ -0,0 +1,71 @@ +package org.mage.test.cards.single._40k; + +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +import mage.constants.PhaseStep; +import mage.constants.Zone; + +public class KharnTheBetrayerTest extends CardTestCommander4Players { + + private static final String KHARN = "Kharn the Betrayer"; + private static final String BOLT = "Lightning Bolt"; + private static final String OFFERING = "Harmless Offering"; + + /** + * + Berzerker — Khârn the Betrayer attacks or blocks each combat if able. + Sigil of Corruption — When you lose control of Khârn the Betrayer, draw two cards. + The Betrayer — If damage would be dealt to Khârn the Betrayer, prevent that damage and an opponent of your choice gains control of it. + */ + @Test + public void testEffect() { + addCard(Zone.BATTLEFIELD, playerA, KHARN, 1); + addCard(Zone.LIBRARY, playerA, "Mountain", 5); + addCard(Zone.LIBRARY, playerB, "Mountain", 5); + + addCard(Zone.BATTLEFIELD, playerC, "Mountain", 2); + addCard(Zone.HAND, playerC, BOLT, 2); + + // Player C pings Kharn, which triggers his effect. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerC, BOLT, KHARN); + // Player A chooses Player B to gain control, draws 2 cards when losing control. + setChoice(playerA, "PlayerB"); + + // // Player C pings Kharn, triggering effect again. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerC, BOLT, KHARN); + // // Player B chooses player A, draws 2 cards when losing control. + setChoice(playerB, "PlayerC"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + setStrictChooseMode(true); + execute(); + + // Account for draw for turn + assertHandCount(playerA, 1 + 2); + assertHandCount(playerB, 2); + assertHandCount(playerC, 0); + assertPermanentCount(playerC, KHARN, 1); + } + + @Test + public void testLostControl() { + addCard(Zone.BATTLEFIELD, playerA, KHARN, 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.HAND, playerA, OFFERING); + addCard(Zone.LIBRARY, playerA, "Mountain", 5); + + // Player A gives Kharn to player B, should trigger Kharn's effect + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, OFFERING); + addTarget(playerA, playerB); + addTarget(playerA, KHARN); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + // Account for draw for turn + assertPermanentCount(playerB, KHARN, 1); + assertHandCount(playerA, 1 + 2); + } +}