[REX] Implement Ian Malcolm, Chaotician (#12117)

* Start on Ian Malcolm, Chaotician, missing key effects

* fox ANY clause in DrawNthCardTriggeredAbility

* Get exile effect working

* Start using Evelyn, the Covetous code

* align exile effect

* align player clause

* align card type clause

* align counter check clause

* align mana clause

* add ownership clause

* remove redundant comments

* fix redundant mana clause description

* fix counter clause in mana cost effect

* fix active clause in mana effect

* use MageObjectReference to associate exiled cards with an Ian Malcolm instance

* optimize imports

* Start tests, failing currently

* fix test and add blink test

* fix signature of constructor

* fix order of super() call in checkTrigger

* clarify hash maps in watcher

* use correct AsThoughEffect

* document header of checkExile

* generalize modal and double faced cards for LKI fetch

* remove land played event for watcher

* Use custom MageIdentifier to filter usedMap
This commit is contained in:
jimga150 2024-04-22 23:58:05 -04:00 committed by GitHub
parent f8f9b0caa0
commit 40143c648f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 398 additions and 1 deletions

View file

@ -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<MageObjectReference, Set<MageObjectReference>> 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<MageObjectReference, Set<UUID>> 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<MageObjectReference> 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));
}
}

View file

@ -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));

View file

@ -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();
}
}

View file

@ -24,6 +24,7 @@ public enum MageIdentifier {
HaukensInsightWatcher,
IntrepidPaleontologistWatcher,
KessDissidentMageWatcher,
IanMalcolmChaoticianWatcher,
MuldrothaTheGravetideWatcher,
ShareTheSpoilsWatcher,
WishWatcher,

View file

@ -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");
}