diff --git a/Mage.Sets/src/mage/cards/i/IanMalcolmChaotician.java b/Mage.Sets/src/mage/cards/i/IanMalcolmChaotician.java new file mode 100644 index 00000000000..20494b03b74 --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/IanMalcolmChaotician.java @@ -0,0 +1,282 @@ +package mage.cards.i; + +import java.util.*; + +import mage.MageIdentifier; +import mage.MageInt; +import mage.MageObject; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.DrawNthCardTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.AsThoughManaEffect; +import mage.abilities.effects.OneShotEffect; +import mage.cards.*; +import mage.constants.*; +import mage.game.CardState; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.ManaPoolItem; +import mage.players.Player; +import mage.util.CardUtil; +import mage.watchers.Watcher; +import mage.target.targetpointer.FixedTarget; + +/** + * + * @author jimga150 + */ +public final class IanMalcolmChaotician extends CardImpl { + + public IanMalcolmChaotician(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SCIENTIST); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Whenever a player draws their second card each turn, that player exiles the top card of their library. + this.addAbility(new IanMalcolmChaoticianDrawTriggerAbility(), new IanMalcolmChaoticianWatcher()); + + // During each player's turn, that player may cast a spell from among the cards they don't own exiled with + // Ian Malcolm, Chaotician, and mana of any type can be spent to cast it. + Ability ability = new SimpleStaticAbility(new IanMalcolmChaoticianCastEffect()) + .setIdentifier(MageIdentifier.IanMalcolmChaoticianWatcher); + ability.addEffect(new IanMalcolmChaoticianManaEffect()); + this.addAbility(ability); + } + + private IanMalcolmChaotician(final IanMalcolmChaotician card) { + super(card); + } + + @Override + public IanMalcolmChaotician copy() { + return new IanMalcolmChaotician(this); + } +} + +class IanMalcolmChaoticianDrawTriggerAbility extends DrawNthCardTriggeredAbility { + + IanMalcolmChaoticianDrawTriggerAbility() { + super(new IanMalcolmChaoticianExileEffect(), false, TargetController.ANY, 2); + } + + private IanMalcolmChaoticianDrawTriggerAbility(final IanMalcolmChaoticianDrawTriggerAbility ability) { + super(ability); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (super.checkTrigger(event, game)){ + getEffects().setTargetPointer(new FixedTarget(event.getPlayerId())); + return true; + } + return false; + } + + @Override + public IanMalcolmChaoticianDrawTriggerAbility copy() { + return new IanMalcolmChaoticianDrawTriggerAbility(this); + } +} + +class IanMalcolmChaoticianExileEffect extends OneShotEffect { + + IanMalcolmChaoticianExileEffect() { + super(Outcome.Exile); + staticText = "that player exiles the top card of their library"; + } + + private IanMalcolmChaoticianExileEffect(final IanMalcolmChaoticianExileEffect effect) { + super(effect); + } + + @Override + public IanMalcolmChaoticianExileEffect copy() { + return new IanMalcolmChaoticianExileEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + UUID targetPlayerID = getTargetPointer().getFirst(game, source); + Player targetPlayer = game.getPlayer(targetPlayerID); + MageObject sourceObject = source.getSourceObject(game); + if (targetPlayer == null || sourceObject == null) { + return false; + } + + Card card = targetPlayer.getLibrary().getFromTop(game); + if (card == null) { + return false; + } + + UUID exileZoneId = CardUtil.getExileZoneId(game, sourceObject.getId(), sourceObject.getZoneChangeCounter(game)); + targetPlayer.moveCardsToExile(card, source, game, true, exileZoneId, sourceObject.getIdName()); + MageObjectReference sourceMOR = new MageObjectReference(source.getSourceId(), game); + IanMalcolmChaoticianWatcher.addCard(sourceMOR, card, game); + return true; + } +} + +class IanMalcolmChaoticianCastEffect extends AsThoughEffectImpl { + + IanMalcolmChaoticianCastEffect() { + super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PlayForFree); + staticText = "During each player's turn, that player may cast a spell from among the cards they don't own " + + "exiled with {this}"; + } + + private IanMalcolmChaoticianCastEffect(final IanMalcolmChaoticianCastEffect effect) { + super(effect); + } + + @Override + public IanMalcolmChaoticianCastEffect copy() { + return new IanMalcolmChaoticianCastEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + if (!game.isActivePlayer(affectedControllerId) || IanMalcolmChaoticianWatcher.checkUsed(source, game)) { + return false; + } + Card card = game.getCard(CardUtil.getMainCardId(game, sourceId)); + if (card == null || card.isLand(game)){ + return false; + } + MageObjectReference sourceMOR = new MageObjectReference(source.getSourceId(), game); + return !card.getOwnerId().equals(affectedControllerId) + && IanMalcolmChaoticianWatcher.checkExile(sourceMOR, card, game, 0); + } +} + +class IanMalcolmChaoticianManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { + + IanMalcolmChaoticianManaEffect() { + super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = ", and mana of any type can be spent to cast it"; + } + + private IanMalcolmChaoticianManaEffect(final IanMalcolmChaoticianManaEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public IanMalcolmChaoticianManaEffect copy() { + return new IanMalcolmChaoticianManaEffect(this); + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + if (!game.isActivePlayer(affectedControllerId) || IanMalcolmChaoticianWatcher.checkUsed(source, game)) { + return false; + } + MageObjectReference sourceMOR = new MageObjectReference(source.getSourceId(), game); + Card card = game.getCard(CardUtil.getMainCardId(game, sourceId)); + if (card == null) { + return false; + } + if (game.getState().getZone(card.getId()) == Zone.EXILED) { + return IanMalcolmChaoticianWatcher.checkExile(sourceMOR, card, game, 0); + } + // not exiled, must be on the stack--get LKI + CardState cardState = game.getLastKnownInformationCard(card.getMainCard().getId(), Zone.EXILED);; + return cardState != null && IanMalcolmChaoticianWatcher.checkExile(sourceMOR, card, game, 1); + } + + @Override + public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { + return mana.getFirstAvailable(); + } +} + +class IanMalcolmChaoticianWatcher extends Watcher { + + // Maps MOR representing the specific instance of Ian Malcolm (changes when it changes zones, i.e. blinked) + // to many exiled cards exiled with Ian Malcolm + private final Map> exiledMap = new HashMap<>(); + + // Maps instances of approving MORs (some of which might be instances of Ian Malcolm, if his ability was used to + // cast that spell) to UUIDs of players that have used that approving object this turn + private final Map> usedMap = new HashMap<>(); + + IanMalcolmChaoticianWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.SPELL_CAST && + event.hasApprovingIdentifier(MageIdentifier.IanMalcolmChaoticianWatcher)) { + usedMap.computeIfAbsent( + event.getAdditionalReference() + .getApprovingMageObjectReference(), + x -> new HashSet<>() + ).add(event.getPlayerId()); + } + } + + @Override + public void reset() { + super.reset(); + usedMap.clear(); + } + + static void addCard(MageObjectReference sourceObj, Card card, Game game) { + Set set = game + .getState() + .getWatcher(IanMalcolmChaoticianWatcher.class) + .exiledMap + .computeIfAbsent(sourceObj, x -> new HashSet<>()); + MageObjectReference mor = new MageObjectReference(card, game); + set.add(mor); + } + + static boolean checkUsed(Ability source, Game game) { + Permanent sourceObject = game.getPermanent(source.getSourceId()); + if (sourceObject == null) { + return true; + } + return game + .getState() + .getWatcher(IanMalcolmChaoticianWatcher.class) + .usedMap + .getOrDefault( + new MageObjectReference(sourceObject, game), + Collections.emptySet() + ).contains(source.getControllerId()); + } + + /** + * Returns true if card was added to the tracked exiled cards under sourceObj + * + * @param sourceObj exiling card + * @param card exiled card to check + * @param game + * @param offset zone change counter offset: 0 = in exile, 1 = on the stack waiting to be cast + */ + static boolean checkExile(MageObjectReference sourceObj, Card card, Game game, int offset) { + return game + .getState() + .getWatcher(IanMalcolmChaoticianWatcher.class) + .exiledMap + .getOrDefault(sourceObj, Collections.emptySet()) + .stream() + .anyMatch(mor -> mor.refersTo(card, game, offset)); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/JurassicWorldCollection.java b/Mage.Sets/src/mage/sets/JurassicWorldCollection.java index 14f8bdac61f..fc1c5d87594 100644 --- a/Mage.Sets/src/mage/sets/JurassicWorldCollection.java +++ b/Mage.Sets/src/mage/sets/JurassicWorldCollection.java @@ -33,6 +33,7 @@ public final class JurassicWorldCollection extends ExpansionSet { cards.add(new SetCardInfo("Grim Giganotosaurus", 11, Rarity.RARE, mage.cards.g.GrimGiganotosaurus.class)); cards.add(new SetCardInfo("Henry Wu, InGen Geneticist", 12, Rarity.RARE, mage.cards.h.HenryWuInGenGeneticist.class)); cards.add(new SetCardInfo("Hunting Velociraptor", 4, Rarity.RARE, mage.cards.h.HuntingVelociraptor.class)); + cards.add(new SetCardInfo("Ian Malcolm, Chaotician", 13, Rarity.RARE, mage.cards.i.IanMalcolmChaotician.class)); cards.add(new SetCardInfo("Indoraptor, the Perfect Hybrid", 15, Rarity.RARE, mage.cards.i.IndoraptorThePerfectHybrid.class)); cards.add(new SetCardInfo("Island", 22, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Island", "22b", Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/watchers/IanMalcolmChaoticianTests.java b/Mage.Tests/src/test/java/org/mage/test/cards/watchers/IanMalcolmChaoticianTests.java new file mode 100644 index 00000000000..777ecea6a57 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/watchers/IanMalcolmChaoticianTests.java @@ -0,0 +1,108 @@ +package org.mage.test.cards.watchers; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +/** + * + * @author jimga150 + */ +public class IanMalcolmChaoticianTests extends CardTestCommander4Players { + + @Test + public void testManaCostsandWatcher() { + + skipInitShuffling(); + + // Whenever a player draws their second card each turn, that player exiles the top card of their library. + // During each player's turn, that player may cast a spell from among the cards they don't own exiled with + // Ian Malcolm, Chaotician, and mana of any type can be spent to cast it. + addCard(Zone.BATTLEFIELD, playerA, "Ian Malcolm, Chaotician"); + + // Flying + // At the beginning of your draw step, draw an additional card. + // At the beginning of your end step, discard your hand. + addCard(Zone.BATTLEFIELD, playerA, "Avaricious Dragon"); + addCard(Zone.BATTLEFIELD, playerB, "Avaricious Dragon"); + addCard(Zone.BATTLEFIELD, playerC, "Avaricious Dragon"); + addCard(Zone.BATTLEFIELD, playerD, "Avaricious Dragon"); + + addCard(Zone.LIBRARY, playerA, "Horde of Notions", 3); + addCard(Zone.LIBRARY, playerB, "Fusion Elemental", 10); + addCard(Zone.LIBRARY, playerC, "Chromanticore", 10); + addCard(Zone.LIBRARY, playerD, "Garth One-Eye", 10); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 15); + + // Should be able to cast all cards exiled from other players' decks + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Fusion Elemental", true); + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Chromanticore", true); + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Garth One-Eye", true); + + // Should NOT be able to cast own card exiled in same way + checkPlayableAbility("Able to cast Horde of Notions, but should not be.", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Horde of Notions", false); + + // Cast a card exiled with Ian Malcolm, preventing any future casts from this zone on this turn. + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Fusion Elemental", true); + + checkPlayableAbility("Able to cast Chromanticore, but should not be due to watcher.", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Chromanticore", false); + checkPlayableAbility("Able to cast Garth One-Eye, but should not be due to watcher.", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Garth One-Eye", false); + + setStopAt(5, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + } + + @Test + public void testBlink() { + + skipInitShuffling(); + + // Whenever a player draws their second card each turn, that player exiles the top card of their library. + // During each player's turn, that player may cast a spell from among the cards they don't own exiled with + // Ian Malcolm, Chaotician, and mana of any type can be spent to cast it. + addCard(Zone.BATTLEFIELD, playerA, "Ian Malcolm, Chaotician"); + + // Flying + // At the beginning of your draw step, draw an additional card. + // At the beginning of your end step, discard your hand. + addCard(Zone.BATTLEFIELD, playerA, "Heightened Awareness"); + addCard(Zone.BATTLEFIELD, playerB, "Heightened Awareness"); + addCard(Zone.BATTLEFIELD, playerC, "Heightened Awareness"); + addCard(Zone.BATTLEFIELD, playerD, "Heightened Awareness"); + + addCard(Zone.LIBRARY, playerA, "Horde of Notions", 3); + addCard(Zone.LIBRARY, playerA, "Ephemerate", 1); + addCard(Zone.LIBRARY, playerB, "Fusion Elemental", 10); + addCard(Zone.LIBRARY, playerC, "Chromanticore", 10); + addCard(Zone.LIBRARY, playerD, "Garth One-Eye", 10); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 15); + + // Should be able to cast all cards exiled from other players' decks + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Fusion Elemental", true); + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Chromanticore", true); + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Garth One-Eye", true); + + // Should NOT be able to cast own card exiled in same way + checkPlayableAbility("Able to cast Horde of Notions, but should not be.", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Horde of Notions", false); + + // Blink Ian Malcolm, causing all cards in his current exile zone to become uncastable + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Ephemerate", true); + addTarget(playerA, "Ian Malcolm, Chaotician"); + + // Should no longer be able to cast all cards exiled from other players' decks + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Fusion Elemental", false); + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Chromanticore", false); + checkPlayableAbility("Can't cast card exiled with this Ian Malcolm", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Garth One-Eye", false); + + setStopAt(5, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + } + +} diff --git a/Mage/src/main/java/mage/MageIdentifier.java b/Mage/src/main/java/mage/MageIdentifier.java index cc10c5eb0ca..6b1c5300a84 100644 --- a/Mage/src/main/java/mage/MageIdentifier.java +++ b/Mage/src/main/java/mage/MageIdentifier.java @@ -24,6 +24,7 @@ public enum MageIdentifier { HaukensInsightWatcher, IntrepidPaleontologistWatcher, KessDissidentMageWatcher, + IanMalcolmChaoticianWatcher, MuldrothaTheGravetideWatcher, ShareTheSpoilsWatcher, WishWatcher, diff --git a/Mage/src/main/java/mage/abilities/common/DrawNthCardTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/DrawNthCardTriggeredAbility.java index 12911302c05..fbaf439e815 100644 --- a/Mage/src/main/java/mage/abilities/common/DrawNthCardTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/DrawNthCardTriggeredAbility.java @@ -42,7 +42,7 @@ public class DrawNthCardTriggeredAbility extends TriggeredAbilityImpl { setTriggerPhrase(generateTriggerPhrase()); } - private DrawNthCardTriggeredAbility(final DrawNthCardTriggeredAbility ability) { + protected DrawNthCardTriggeredAbility(final DrawNthCardTriggeredAbility ability) { super(ability); this.targetController = ability.targetController; this.cardNumber = ability.cardNumber; @@ -72,6 +72,9 @@ public class DrawNthCardTriggeredAbility extends TriggeredAbilityImpl { return false; } break; + case ANY: + // Doesn't matter who + break; default: throw new IllegalArgumentException("TargetController " + targetController + " not supported"); } @@ -87,6 +90,8 @@ public class DrawNthCardTriggeredAbility extends TriggeredAbilityImpl { return "Whenever a player draws their " + CardUtil.numberToOrdinalText(cardNumber) + " card during their turn, "; case OPPONENT: return "Whenever an opponent draws their " + CardUtil.numberToOrdinalText(cardNumber) + " card each turn, "; + case ANY: + return "Whenever a player draws their " + CardUtil.numberToOrdinalText(cardNumber) + " card each turn, "; default: throw new IllegalArgumentException("TargetController " + targetController + " not supported"); }