From 6836db01960c80b988a4197f684ab97a1785ce52 Mon Sep 17 00:00:00 2001 From: Alexander Dahmen Date: Wed, 23 Aug 2023 01:25:05 +0200 Subject: [PATCH] [CLB] Implement Kagha Shadow Archdruid (#10919) Signed-off-by: Alexander Dahmen --- .../mage/cards/k/KaghaShadowArchdruid.java | 152 ++++++++++++++++ .../CommanderLegendsBattleForBaldursGate.java | 1 + .../KaghaShadowArchdruidAbilityTest.java | 165 ++++++++++++++++++ Mage/src/main/java/mage/MageIdentifier.java | 3 +- 4 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/k/KaghaShadowArchdruid.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/KaghaShadowArchdruidAbilityTest.java diff --git a/Mage.Sets/src/mage/cards/k/KaghaShadowArchdruid.java b/Mage.Sets/src/mage/cards/k/KaghaShadowArchdruid.java new file mode 100644 index 00000000000..e0cb4d0aee1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KaghaShadowArchdruid.java @@ -0,0 +1,152 @@ +package mage.cards.k; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import mage.MageIdentifier; +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.WatcherScope; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.watchers.Watcher; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AsThoughEffectType; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; + +/** + * + * @author Fyusel + */ +public final class KaghaShadowArchdruid extends CardImpl { + + public KaghaShadowArchdruid(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ELF); + this.subtype.add(SubType.DRUID); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // Whenever Kagha, Shadow Archdruid attacks, it gains deathtouch until end of turn. Mill two cards. + Ability ability = new AttacksTriggeredAbility(new GainAbilitySourceEffect( + DeathtouchAbility.getInstance(), + Duration.EndOfTurn)); + ability.addEffect(new MillCardsControllerEffect(2)); + this.addAbility(ability); + + // Once during each of your turns, you may play a land or cast a permanent spell from among cards in your graveyard that were put there from your library this turn. + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new KaghaShadowArchdruidEffect()) + .setIdentifier(MageIdentifier.KaghaShadowArchdruidWatcher), + new KaghaShadowArchdruidWatcher()); + } + + private KaghaShadowArchdruid(final KaghaShadowArchdruid card) { + super(card); + } + + @Override + public KaghaShadowArchdruid copy() { + return new KaghaShadowArchdruid(this); + } +} + +class KaghaShadowArchdruidEffect extends AsThoughEffectImpl { + + KaghaShadowArchdruidEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true); + this.staticText = "Once during each of your turns, you may play a land or cast a permanent spell from among cards in your graveyard that were put there from your library this turn."; + } + + private KaghaShadowArchdruidEffect(final KaghaShadowArchdruidEffect effect) { + super(effect); + } + + @Override + public KaghaShadowArchdruidEffect copy() { + return new KaghaShadowArchdruidEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + if (source.isControlledBy(affectedControllerId) + && Zone.GRAVEYARD.equals(game.getState().getZone(objectId)) + && game.isActivePlayer(affectedControllerId)) { + Card card = game.getCard(objectId); + Permanent sourceObject = source.getSourcePermanentIfItStillExists(game); + KaghaShadowArchdruidWatcher watcher = game.getState().getWatcher(KaghaShadowArchdruidWatcher.class); + if (card != null && sourceObject != null + && card.isOwnedBy(affectedControllerId) + && card.isPermanent(game) + && watcher != null + && watcher.checkCard(card, game)) { + return watcher.abilityNotUsed(new MageObjectReference(sourceObject, game)); + } + } + return false; + } +} + +class KaghaShadowArchdruidWatcher extends Watcher { + + private final Set usedFrom = new HashSet<>(); + private final Set morSet = new HashSet<>(); + + KaghaShadowArchdruidWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if ((GameEvent.EventType.SPELL_CAST.equals(event.getType()) || GameEvent.EventType.LAND_PLAYED.equals(event.getType())) + && event.hasApprovingIdentifier(MageIdentifier.KaghaShadowArchdruidWatcher)) { + usedFrom.add(event.getAdditionalReference().getApprovingMageObjectReference()); + } + if (event.getType() == GameEvent.EventType.ZONE_CHANGE) { + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (zEvent.getFromZone() == Zone.LIBRARY && zEvent.getToZone() == Zone.GRAVEYARD) { + morSet.add(new MageObjectReference(zEvent.getTargetId(), game)); + } + } + } + + @Override + public void reset() { + super.reset(); + usedFrom.clear(); + morSet.clear(); + } + + boolean abilityNotUsed(MageObjectReference mor) { + return !usedFrom.contains(mor); + } + + boolean checkCard(Card card, Game game) { + return morSet + .stream() + .anyMatch(mor -> mor.refersTo(card, game)); + } +} diff --git a/Mage.Sets/src/mage/sets/CommanderLegendsBattleForBaldursGate.java b/Mage.Sets/src/mage/sets/CommanderLegendsBattleForBaldursGate.java index 927a75016ca..422ba16afc5 100644 --- a/Mage.Sets/src/mage/sets/CommanderLegendsBattleForBaldursGate.java +++ b/Mage.Sets/src/mage/sets/CommanderLegendsBattleForBaldursGate.java @@ -336,6 +336,7 @@ public final class CommanderLegendsBattleForBaldursGate extends ExpansionSet { cards.add(new SetCardInfo("Jeska's Will", 799, Rarity.RARE, mage.cards.j.JeskasWill.class)); cards.add(new SetCardInfo("Journey to the Lost City", 681, Rarity.RARE, mage.cards.j.JourneyToTheLostCity.class)); cards.add(new SetCardInfo("Juvenile Mist Dragon", 79, Rarity.UNCOMMON, mage.cards.j.JuvenileMistDragon.class)); + cards.add(new SetCardInfo("Kagha, Shadow Archdruid", 279, Rarity.UNCOMMON, mage.cards.k.KaghaShadowArchdruid.class)); cards.add(new SetCardInfo("Karlach, Fury of Avernus", 186, Rarity.MYTHIC, mage.cards.k.KarlachFuryOfAvernus.class)); cards.add(new SetCardInfo("Kazuul, Tyrant of the Cliffs", 800, Rarity.RARE, mage.cards.k.KazuulTyrantOfTheCliffs.class)); cards.add(new SetCardInfo("Keiga, the Tide Star", 725, Rarity.RARE, mage.cards.k.KeigaTheTideStar.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/KaghaShadowArchdruidAbilityTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/KaghaShadowArchdruidAbilityTest.java new file mode 100644 index 00000000000..aea2d7a097f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/KaghaShadowArchdruidAbilityTest.java @@ -0,0 +1,165 @@ +package org.mage.test.cards.abilities.oneshot; + +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import mage.abilities.keyword.DeathtouchAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; + +public class KaghaShadowArchdruidAbilityTest extends CardTestPlayerBase { + + // 1. Deathtouch and mill attack trigger + @Test + public void testKaghaAttackTrigger() { + // initialize library, hand and battlefield + removeAllCardsFromLibrary(playerA); + addCard(Zone.HAND, playerA, "Swamp",7); + addCard(Zone.LIBRARY, playerA, "Forest", 4); + addCard(Zone.BATTLEFIELD, playerA, "Kagha, Shadow Archdruid", 1); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + attack(3, playerA, "Kagha, Shadow Archdruid"); + setStopAt(3, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, 2); + assertGraveyardCount(playerA, "Forest", 2); + assertAbility(playerA, "Kagha, Shadow Archdruid", DeathtouchAbility.getInstance(), true); + } + + // 2. Test if possible to play a land or cast a permanent spell + @Test + public void testKaghaCheckSpecialEffect() { + + // initialize library, hand and battlefield + removeAllCardsFromLibrary(playerA); + addCard(Zone.HAND, playerA, "Swamp",6); + addCard(Zone.HAND, playerA, "Mulch",1); + + addCard(Zone.LIBRARY, playerA, "Forest", 1); + addCard(Zone.LIBRARY, playerA, "Sol Ring", 1); + addCard(Zone.LIBRARY, playerA, "Grapple with the Past", 1); + addCard(Zone.LIBRARY, playerA, "Mountain", 1); // will be drawn - new turn + addCard(Zone.LIBRARY, playerA, "Kagha, Shadow Archdruid", 4); // will be milled via Mulch + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, "Kagha, Shadow Archdruid", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + // test previously milled cards are not taken into account - 4 cards are milled + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mulch"); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + // attack and trigger milling 2 cards via Kagha + attack(3, playerA, "Kagha, Shadow Archdruid"); + + // milled card from turn one (4x Kagha) cannot be cast anymore + checkPlayableAbility("before", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Kagha, Shadow Archdruid", false); + // Grapple from the Past is not a permanent card + checkPlayableAbility("before", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Grapple with the Past", false); + // Sol Ring can be cast + checkPlayableAbility("before", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Sol Ring", true); + + setStopAt(3, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, 7); + assertGraveyardCount(playerA, "Kagha, Shadow Archdruid", 4); + assertGraveyardCount(playerA, "Grapple with the Past", 1); + assertGraveyardCount(playerA, "Sol Ring", 1); + assertGraveyardCount(playerA, "Mulch", 1); + } + + + // 3. check if only one permanent can be cast/played + @Test + public void testKaghaCheckSpecialEffectOnce() { + + // initialize library, hand and battlefield + removeAllCardsFromLibrary(playerA); + addCard(Zone.HAND, playerA, "Swamp",6); + addCard(Zone.HAND, playerA, "Mulch",1); + + addCard(Zone.LIBRARY, playerA, "Sol Ring", 2); + addCard(Zone.LIBRARY, playerA, "Mountain", 1); // will be drawn - new turn + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, "Kagha, Shadow Archdruid", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + attack(3, playerA, "Kagha, Shadow Archdruid"); + + // only one spell can be cast from graveyard + checkPlayableAbility("check cast Sol Ring", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Sol Ring", true); + castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Sol Ring"); + // another one should not be possible + checkPlayableAbility("check cast Sol Ring", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Sol Ring", false); + + setStopAt(3, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, 1); + assertGraveyardCount(playerA, "Sol Ring", 1); + assertAbility(playerA, "Kagha, Shadow Archdruid", DeathtouchAbility.getInstance(), true); + } + + // 4. Other mill sources should work as well + @Test + public void testKaghaCheckSpecialEffectOtherSources() { + + // initialize library, hand and battlefield + removeAllCardsFromLibrary(playerA); + addCard(Zone.HAND, playerA, "Swamp",6); + addCard(Zone.HAND, playerA, "Mulch",1); + + addCard(Zone.LIBRARY, playerA, "Forest", 1); + addCard(Zone.LIBRARY, playerA, "Sol Ring", 1); + addCard(Zone.LIBRARY, playerA, "Grapple with the Past", 1); + addCard(Zone.LIBRARY, playerA, "Kagha, Shadow Archdruid", 4); // will be milled via Mulch + addCard(Zone.LIBRARY, playerA, "Mountain", 1); // will be drawn - new turn + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, "Kagha, Shadow Archdruid", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + // mill other cards this turn + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Mulch"); + + // attack and trigger milling 2 cards via Kagha + attack(3, playerA, "Kagha, Shadow Archdruid"); + + // Kagha(s) can be cast + checkPlayableAbility("before", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Kagha, Shadow Archdruid", true); + // Grapple from the Past is not a permanent card + checkPlayableAbility("before", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Grapple with the Past", false); + // Sol Ring can be cast + checkPlayableAbility("before", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Sol Ring", true); + + setStopAt(3, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, 7); + assertGraveyardCount(playerA, "Kagha, Shadow Archdruid", 4); + assertGraveyardCount(playerA, "Grapple with the Past", 1); + assertGraveyardCount(playerA, "Sol Ring", 1); + assertGraveyardCount(playerA, "Mulch", 1); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/MageIdentifier.java b/Mage/src/main/java/mage/MageIdentifier.java index 63af34c517a..81ef3adcebd 100644 --- a/Mage/src/main/java/mage/MageIdentifier.java +++ b/Mage/src/main/java/mage/MageIdentifier.java @@ -20,5 +20,6 @@ public enum MageIdentifier { GlimpseTheCosmosWatcher, SerraParagonWatcher, OneWithTheMultiverseWatcher, - JohannApprenticeSorcererWatcher + JohannApprenticeSorcererWatcher, + KaghaShadowArchdruidWatcher }