From e914c38986ba7d94aa228ab0b7d0c0e77bd51769 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Mon, 29 Jun 2020 20:05:38 -0400 Subject: [PATCH] Implemented Idol of Endurance --- .../src/mage/cards/i/IdolOfEndurance.java | 265 ++++++++++++++++++ Mage.Sets/src/mage/sets/CoreSet2021.java | 1 + .../cards/single/IdolOfEnduranceTest.java | 125 +++++++++ 3 files changed, 391 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/i/IdolOfEndurance.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/IdolOfEnduranceTest.java diff --git a/Mage.Sets/src/mage/cards/i/IdolOfEndurance.java b/Mage.Sets/src/mage/cards/i/IdolOfEndurance.java new file mode 100644 index 00000000000..f57ff0ec57a --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/IdolOfEndurance.java @@ -0,0 +1,265 @@ +package mage.cards.i; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.OneShotEffect; +import mage.cards.*; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.ConvertedManaCostPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.watchers.Watcher; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author TheElk801 + */ +public final class IdolOfEndurance extends CardImpl { + + public IdolOfEndurance(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{W}"); + + // When Idol of Endurance enters the battlefield, exile all creature cards with converted mana cost 3 or less from your graveyard until Idol of Endurance leaves the battlefield. + this.addAbility(new EntersBattlefieldTriggeredAbility(new IdolOfEnduranceExileEffect())); + + // {1}{W}, {T}: Until end of turn, you may cast a creature spell from among the cards exiled with Idol of Endurance without paying its mana cost. + Ability ability = new SimpleActivatedAbility( + new IdolOfEnduranceCastFromExileEffect(), new ManaCostsImpl<>("{1}{W}") + ); + ability.addCost(new TapSourceCost()); + this.addAbility(ability, new IdolOfEnduranceWatcher()); + } + + private IdolOfEndurance(final IdolOfEndurance card) { + super(card); + } + + @Override + public IdolOfEndurance copy() { + return new IdolOfEndurance(this); + } +} + +class IdolOfEnduranceExileEffect extends OneShotEffect { + + private static final FilterCard filter = new FilterCreatureCard(); + + static { + filter.add(new ConvertedManaCostPredicate(ComparisonType.FEWER_THAN, 4)); + } + + IdolOfEnduranceExileEffect() { + super(Outcome.Benefit); + staticText = "exile all creature cards with converted mana cost 3 or less from your graveyard until {this} leaves the battlefield"; + } + + private IdolOfEnduranceExileEffect(final IdolOfEnduranceExileEffect effect) { + super(effect); + } + + @Override + public IdolOfEnduranceExileEffect copy() { + return new IdolOfEnduranceExileEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (player == null || permanent == null) { + return false; + } + Cards cards = new CardsImpl(player.getGraveyard().getCards(filter, game)); + MageObjectReference mor = new MageObjectReference(permanent, game); + player.moveCards(cards, Zone.EXILED, source, game); + Set morSet = cards + .getCards(game) + .stream() + .filter(Objects::nonNull) + .map(card -> new MageObjectReference(card, game)) + .collect(Collectors.toSet()); + game.getState().setValue("" + mor.getSourceId() + mor.getZoneChangeCounter(), morSet); + game.addDelayedTriggeredAbility(new IdolOfEnduranceDelayedTrigger(morSet), source); + return true; + } +} + +class IdolOfEnduranceDelayedTrigger extends DelayedTriggeredAbility { + + IdolOfEnduranceDelayedTrigger(Set morSet) { + super(new IdolOfEnduranceLeaveEffect(morSet), Duration.Custom, true, false); + this.usesStack = false; + this.setRuleVisible(false); + } + + private IdolOfEnduranceDelayedTrigger(final IdolOfEnduranceDelayedTrigger ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (event.getTargetId().equals(this.getSourceId())) { + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (zEvent.getFromZone() == Zone.BATTLEFIELD) { + return true; + } + } + return false; + } + + @Override + public IdolOfEnduranceDelayedTrigger copy() { + return new IdolOfEnduranceDelayedTrigger(this); + } +} + +class IdolOfEnduranceLeaveEffect extends OneShotEffect { + + private final Set morSet = new HashSet<>(); + + IdolOfEnduranceLeaveEffect(Set morSet) { + super(Outcome.Benefit); + this.morSet.addAll(morSet); + } + + private IdolOfEnduranceLeaveEffect(final IdolOfEnduranceLeaveEffect effect) { + super(effect); + } + + @Override + public IdolOfEnduranceLeaveEffect copy() { + return new IdolOfEnduranceLeaveEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + return player != null && player.moveCards( + morSet.stream() + .map(mor -> mor.getCard(game)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()), + Zone.GRAVEYARD, source, game + ); + } +} + +class IdolOfEnduranceCastFromExileEffect extends AsThoughEffectImpl { + + IdolOfEnduranceCastFromExileEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit); + staticText = "until end of turn, you may cast a creature spell from among the cards exiled with {this} without paying its mana cost"; + } + + private IdolOfEnduranceCastFromExileEffect(final IdolOfEnduranceCastFromExileEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public IdolOfEnduranceCastFromExileEffect copy() { + return new IdolOfEnduranceCastFromExileEffect(this); + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + IdolOfEnduranceWatcher watcher = game.getState().getWatcher(IdolOfEnduranceWatcher.class); + if (watcher != null) { + watcher.addPlayable(source, game); + } + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + IdolOfEnduranceWatcher watcher = game.getState().getWatcher(IdolOfEnduranceWatcher.class); + if (watcher == null || !watcher.checkPermission(affectedControllerId, source, game)) { + return false; + } + Object value = game.getState().getValue("" + source.getSourceId() + source.getSourceObjectZoneChangeCounter()); + if (!(value instanceof Set)) { + discard(); + return false; + } + Set morSet = (Set) value; + if (game.getState().getZone(sourceId) != Zone.EXILED + || morSet.stream().noneMatch(mor -> mor.refersTo(sourceId, game))) { + return false; + } + Card card = game.getCard(sourceId); + if (card == null || !card.isCreature() || card.isLand()) { + return false; + } + return allowCardToPlayWithoutMana(sourceId, source, affectedControllerId, game); + } +} + +class IdolOfEnduranceWatcher extends Watcher { + + private final Map> morMap = new HashMap<>(); + + IdolOfEnduranceWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.SPELL_CAST) { + if (event.getAdditionalReference() == null) { + return; + } + morMap.computeIfAbsent(event.getAdditionalReference(), m -> new HashMap<>()) + .compute(event.getPlayerId(), (u, i) -> i == null ? 0 : Integer.sum(i, -1)); + return; + } + } + + @Override + public void reset() { + morMap.clear(); + super.reset(); + } + + boolean checkPermission(UUID playerId, Ability source, Game game) { + if (!playerId.equals(source.getControllerId())) { + return false; + } + MageObjectReference mor = new MageObjectReference( + source.getSourceId(), source.getSourceObjectZoneChangeCounter(), game + ); + if (!morMap.containsKey(mor)) { + return false; + } + return morMap.get(mor).getOrDefault(playerId, 0) > 0; + } + + void addPlayable(Ability source, Game game) { + MageObjectReference mor = new MageObjectReference( + source.getSourceId(), source.getSourceObjectZoneChangeCounter(), game + ); + morMap.computeIfAbsent(mor, m -> new HashMap<>()) + .compute(source.getControllerId(), (u, i) -> i == null ? 1 : Integer.sum(i, 1)); + } +} diff --git a/Mage.Sets/src/mage/sets/CoreSet2021.java b/Mage.Sets/src/mage/sets/CoreSet2021.java index 1c7be8b046b..0b0123d1467 100644 --- a/Mage.Sets/src/mage/sets/CoreSet2021.java +++ b/Mage.Sets/src/mage/sets/CoreSet2021.java @@ -142,6 +142,7 @@ public final class CoreSet2021 extends ExpansionSet { cards.add(new SetCardInfo("Hobblefiend", 152, Rarity.COMMON, mage.cards.h.Hobblefiend.class)); cards.add(new SetCardInfo("Hooded Blightfang", 104, Rarity.RARE, mage.cards.h.HoodedBlightfang.class)); cards.add(new SetCardInfo("Hunter's Edge", 189, Rarity.COMMON, mage.cards.h.HuntersEdge.class)); + cards.add(new SetCardInfo("Idol of Endurance", 23, Rarity.RARE, mage.cards.i.IdolOfEndurance.class)); cards.add(new SetCardInfo("Igneous Cur", 153, Rarity.COMMON, mage.cards.i.IgneousCur.class)); cards.add(new SetCardInfo("Indulging Patrician", 219, Rarity.UNCOMMON, mage.cards.i.IndulgingPatrician.class)); cards.add(new SetCardInfo("Infernal Scarring", 105, Rarity.COMMON, mage.cards.i.InfernalScarring.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/IdolOfEnduranceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/IdolOfEnduranceTest.java new file mode 100644 index 00000000000..63834e0496d --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/IdolOfEnduranceTest.java @@ -0,0 +1,125 @@ +package org.mage.test.cards.single; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class IdolOfEnduranceTest extends CardTestPlayerBase { + + private static final String idol = "Idol of Endurance"; + private static final String key = "Voltaic Key"; + private static final String sqr = "Squire"; + private static final String glrskr = "Glory Seeker"; + + @Test + public void testIdol() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.HAND, playerA, idol); + addCard(Zone.GRAVEYARD, playerA, sqr); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, idol); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sqr); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, sqr, 1); + } + + @Test + public void testIdol2() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.HAND, playerA, idol); + addCard(Zone.GRAVEYARD, playerA, sqr); + addCard(Zone.GRAVEYARD, playerA, glrskr); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, idol); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sqr); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, glrskr); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, sqr, 1); + assertPermanentCount(playerA, glrskr, 0); + } + + @Test + public void testIdolTwice() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 8); + addCard(Zone.BATTLEFIELD, playerA, key); + addCard(Zone.HAND, playerA, idol); + addCard(Zone.GRAVEYARD, playerA, sqr); + addCard(Zone.GRAVEYARD, playerA, glrskr); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, idol); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1},", idol); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sqr); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, glrskr); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, sqr, 1); + assertPermanentCount(playerA, glrskr, 1); + } + + @Test + public void testIdolTwice2() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 8); + addCard(Zone.BATTLEFIELD, playerA, key); + addCard(Zone.HAND, playerA, idol); + addCard(Zone.GRAVEYARD, playerA, sqr); + addCard(Zone.GRAVEYARD, playerA, glrskr); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, idol); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sqr); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1},", idol); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, glrskr); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, sqr, 1); + assertPermanentCount(playerA, glrskr, 1); + } +}