[OTJ] Implement Plot mechanic (+8 cards) (#12017)

[OTJ] Implement Aloe Alchemist
[OTJ] Implement Aven Interrupter
[OTJ] Implement Longhorn Shapshooter
[OTJ] Implement Kellan Joins Up
[OTJ] Implement Make Your Own Luck
[OTJ] Implement Jace Reawakened
[OTJ] Implement Lilah, Undefeated Slickshot
[OTJ] Implement Doc Aurlock, Grizzled Genius
This commit is contained in:
Susucre 2024-03-31 17:06:55 +02:00 committed by GitHub
parent ed3d6e3078
commit 97ab8074b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1476 additions and 25 deletions

View file

@ -0,0 +1,59 @@
package mage.cards.a;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.BecomesPlottedSourceTriggeredAbility;
import mage.abilities.effects.common.continuous.BoostTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.keyword.PlotAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
* @author Susucr
*/
public final class AloeAlchemist extends CardImpl {
public AloeAlchemist(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}");
this.subtype.add(SubType.PLANT);
this.subtype.add(SubType.WARLOCK);
this.power = new MageInt(3);
this.toughness = new MageInt(2);
// Trample
this.addAbility(TrampleAbility.getInstance());
// When Aloe Alchemist becomes plotted, target creature gets +3/+2 and gains trample until end of turn.
Ability ability = new BecomesPlottedSourceTriggeredAbility(
new BoostTargetEffect(3, 2, Duration.EndOfTurn)
.setText("target creature gets +3/+2")
);
ability.addTarget(new TargetCreaturePermanent());
ability.addEffect(
new GainAbilityTargetEffect(TrampleAbility.getInstance(), Duration.EndOfTurn)
.setText("and gains trample until end of turn")
);
this.addAbility(ability);
// Plot {1}{G}
this.addAbility(new PlotAbility("{1}{G}"));
}
private AloeAlchemist(final AloeAlchemist card) {
super(card);
}
@Override
public AloeAlchemist copy() {
return new AloeAlchemist(this);
}
}

View file

@ -0,0 +1,104 @@
package mage.cards.a;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.cost.SpellsCostIncreasingAllEffect;
import mage.abilities.keyword.FlashAbility;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.PlotAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.card.CastFromZonePredicate;
import mage.game.Game;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.TargetSpell;
import java.util.UUID;
/**
* @author Susucr
*/
public final class AvenInterrupter extends CardImpl {
private static final FilterCard filter = new FilterCard("spells your opponent cast from graveyards or from exile");
static {
filter.add(Predicates.or(
new CastFromZonePredicate(Zone.GRAVEYARD),
new CastFromZonePredicate(Zone.EXILED)
));
}
public AvenInterrupter(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}{W}");
this.subtype.add(SubType.BIRD);
this.subtype.add(SubType.ROGUE);
this.power = new MageInt(2);
this.toughness = new MageInt(2);
// Flash
this.addAbility(FlashAbility.getInstance());
// Flying
this.addAbility(FlyingAbility.getInstance());
// When Aven Interrupter enters the battlefield, exile target spell. It becomes plotted.
Ability ability = new EntersBattlefieldTriggeredAbility(new AvenInterrupterEffect());
ability.addTarget(new TargetSpell());
this.addAbility(ability);
// Spells your opponents cast from graveyards or from exile cost 2 more to cast.
this.addAbility(new SimpleStaticAbility(
new SpellsCostIncreasingAllEffect(2, filter, TargetController.OPPONENT)
));
}
private AvenInterrupter(final AvenInterrupter card) {
super(card);
}
@Override
public AvenInterrupter copy() {
return new AvenInterrupter(this);
}
}
class AvenInterrupterEffect extends OneShotEffect {
AvenInterrupterEffect() {
super(Outcome.Detriment);
staticText = "exile target spell. It becomes plotted";
}
private AvenInterrupterEffect(final AvenInterrupterEffect effect) {
super(effect);
}
@Override
public AvenInterrupterEffect copy() {
return new AvenInterrupterEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = source.getSourceObject(game);
if (controller == null || sourceObject == null) {
return false;
}
Spell spell = game.getStack().getSpell(getTargetPointer().getFirst(game, source));
if (spell == null) {
return false;
}
return PlotAbility.doExileAndPlotCard(spell, game, source);
}
}

View file

@ -0,0 +1,106 @@
package mage.cards.d;
import mage.MageInt;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.cost.CostModificationEffectImpl;
import mage.abilities.effects.common.cost.SpellsCostReductionControllerEffect;
import mage.abilities.keyword.PlotAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.card.CastFromZonePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author Susucr
*/
public final class DocAurlockGrizzledGenius extends CardImpl {
private static final FilterCard filter = new FilterCard("Spells you cast from your graveyard or from exile");
static {
filter.add(Predicates.or(
new CastFromZonePredicate(Zone.GRAVEYARD),
new CastFromZonePredicate(Zone.EXILED)
));
}
public DocAurlockGrizzledGenius(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.BEAR);
this.subtype.add(SubType.DRUID);
this.power = new MageInt(2);
this.toughness = new MageInt(3);
// Spells you cast from your graveyard or from exile cost {2} less to cast.
this.addAbility(new SimpleStaticAbility(new SpellsCostReductionControllerEffect(filter, 2)));
// Plotting cards from your hand costs {2} less.
this.addAbility(new SimpleStaticAbility(new DocAurlockGrizzledGeniusEffect()));
}
private DocAurlockGrizzledGenius(final DocAurlockGrizzledGenius card) {
super(card);
}
@Override
public DocAurlockGrizzledGenius copy() {
return new DocAurlockGrizzledGenius(this);
}
}
/**
* Inspired by {@link mage.cards.f.Fluctuator}
*/
class DocAurlockGrizzledGeniusEffect extends CostModificationEffectImpl {
public DocAurlockGrizzledGeniusEffect() {
super(Duration.Custom, Outcome.Benefit, CostModificationType.REDUCE_COST);
staticText = "Plotting cards from your hand costs {2} less";
}
private DocAurlockGrizzledGeniusEffect(final DocAurlockGrizzledGeniusEffect effect) {
super(effect);
}
@Override
public DocAurlockGrizzledGeniusEffect copy() {
return new DocAurlockGrizzledGeniusEffect(this);
}
@Override
public boolean applies(Ability abilityToModify, Ability source, Game game) {
return abilityToModify.isControlledBy(source.getControllerId())
&& (abilityToModify instanceof PlotAbility);
}
@Override
public boolean apply(Game game, Ability source, Ability abilityToModify) {
if (!Zone.HAND.equals(game.getState().getZone(abilityToModify.getSourceId()))) {
return false;
}
Player controller = game.getPlayer(abilityToModify.getControllerId());
if (controller != null) {
Mana mana = abilityToModify.getManaCostsToPay().getMana();
int reduce = mana.getGeneric();
if (reduce > 2) {
reduce = 2;
}
if (reduce > 0) {
CardUtil.reduceCost(abilityToModify, reduce);
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,145 @@
package mage.cards.j;
import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.LoyaltyAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ContinuousRuleModifyingEffectImpl;
import mage.abilities.effects.common.CopyTargetSpellEffect;
import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect;
import mage.abilities.effects.common.DrawDiscardControllerEffect;
import mage.abilities.effects.common.MayExileCardFromHandPlottedEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
* @author Susucr
*/
public final class JaceReawakened extends CardImpl {
private static final FilterCard filter = new FilterCard("nonland card with mana value 3 or less");
static {
filter.add(Predicates.not(CardType.LAND.getPredicate()));
filter.add(new ManaValuePredicate(ComparisonType.OR_LESS, 3));
}
public JaceReawakened(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{U}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.JACE);
this.setStartingLoyalty(3);
// You can't cast this spell during your first, second, or third turns of the game.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new CantCastJaceReawakenedEffect()));
// +1: Draw a card, then discard a card.
this.addAbility(new LoyaltyAbility(new DrawDiscardControllerEffect(1, 1), 1));
// +1: You may exile a nonland card with mana value 3 or less from your hand. If you do, it becomes plotted.
this.addAbility(new LoyaltyAbility(new MayExileCardFromHandPlottedEffect(filter), 1));
// -6: Until end of turn, whenever you cast a spell, copy it. You may choose new targets for the copy.
this.addAbility(new LoyaltyAbility(
new CreateDelayedTriggeredAbilityEffect(
new JaceReawakenedDelayedTriggeredAbility()
), -6
));
}
private JaceReawakened(final JaceReawakened card) {
super(card);
}
@Override
public JaceReawakened copy() {
return new JaceReawakened(this);
}
}
/**
* Same as {@link mage.cards.s.SerraAvenger Serra Avenger}
*/
class CantCastJaceReawakenedEffect extends ContinuousRuleModifyingEffectImpl {
CantCastJaceReawakenedEffect() {
super(Duration.WhileOnBattlefield, Outcome.Detriment);
staticText = "You can't cast this spell during your first, second, or third turns of the game";
}
private CantCastJaceReawakenedEffect(final CantCastJaceReawakenedEffect effect) {
super(effect);
}
@Override
public CantCastJaceReawakenedEffect copy() {
return new CantCastJaceReawakenedEffect(this);
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.CAST_SPELL;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
if (event.getSourceId().equals(source.getSourceId())) {
Player controller = game.getPlayer(source.getControllerId());
// it can be cast on other players turn 1 - 3 if some effect let allow you to do this
if (controller != null && controller.getTurns() <= 3 && game.isActivePlayer(source.getControllerId())) {
return true;
}
}
return false;
}
}
class JaceReawakenedDelayedTriggeredAbility extends DelayedTriggeredAbility {
JaceReawakenedDelayedTriggeredAbility() {
super(new CopyTargetSpellEffect(true), Duration.EndOfTurn, false);
}
private JaceReawakenedDelayedTriggeredAbility(final JaceReawakenedDelayedTriggeredAbility ability) {
super(ability);
}
@Override
public JaceReawakenedDelayedTriggeredAbility copy() {
return new JaceReawakenedDelayedTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SPELL_CAST;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(this.getControllerId())) {
Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell != null) {
this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId()));
return true;
}
}
return false;
}
@Override
public String getRule() {
return "Until end of turn, whenever you cast a spell, copy it. You may choose new targets for the copy.";
}
}

View file

@ -0,0 +1,60 @@
package mage.cards.k;
import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.MayExileCardFromHandPlottedEffect;
import mage.abilities.effects.common.counter.AddCountersAllEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.ComparisonType;
import mage.constants.SuperType;
import mage.counters.CounterType;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import java.util.UUID;
/**
* @author Susucr
*/
public final class KellanJoinsUp extends CardImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("legendary creature");
private static final FilterCard filterCard = new FilterCard("nonland card with mana value 3 or less");
static {
filter.add(SuperType.LEGENDARY.getPredicate());
filterCard.add(Predicates.not(CardType.LAND.getPredicate()));
filterCard.add(new ManaValuePredicate(ComparisonType.OR_LESS, 3));
}
public KellanJoinsUp(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{G}{W}{U}");
this.supertype.add(SuperType.LEGENDARY);
// When Kellan Joins Up enters the battlefield, you may exile a nonland card with mana value 3 or less from your hand. If you do, it becomes plotted.
this.addAbility(new EntersBattlefieldTriggeredAbility(new MayExileCardFromHandPlottedEffect(filterCard)));
// Whenever a legendary creature enters the battlefield under your control, put a +1/+1 counter on each creature you control.
this.addAbility(new EntersBattlefieldControlledTriggeredAbility(
new AddCountersAllEffect(
CounterType.P1P1.createInstance(),
StaticFilters.FILTER_CONTROLLED_CREATURE
), filter
));
}
private KellanJoinsUp(final KellanJoinsUp card) {
super(card);
}
@Override
public KellanJoinsUp copy() {
return new KellanJoinsUp(this);
}
}

View file

@ -0,0 +1,152 @@
package mage.cards.l;
import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.abilities.keyword.PlotAbility;
import mage.abilities.keyword.ProwessAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterSpell;
import mage.filter.common.FilterInstantOrSorcerySpell;
import mage.filter.predicate.mageobject.MulticoloredPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.stack.Spell;
import java.util.UUID;
/**
* @author Susucr
*/
public final class LilahUndefeatedSlickshot extends CardImpl {
public LilahUndefeatedSlickshot(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.ROGUE);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// Prowess
this.addAbility(new ProwessAbility());
// Whenever you cast a multicolored instant or sorcery spell from your hand, exile that spell instead of putting it into your graveyard as it resolves. If you do, it becomes plotted.
this.addAbility(new LilahUndefeatedSlickshotTriggeredAbility());
}
private LilahUndefeatedSlickshot(final LilahUndefeatedSlickshot card) {
super(card);
}
@Override
public LilahUndefeatedSlickshot copy() {
return new LilahUndefeatedSlickshot(this);
}
}
class LilahUndefeatedSlickshotTriggeredAbility extends TriggeredAbilityImpl {
private static final FilterSpell filter = new FilterInstantOrSorcerySpell();
static {
filter.add(MulticoloredPredicate.instance);
}
LilahUndefeatedSlickshotTriggeredAbility() {
super(Zone.BATTLEFIELD, null, false);
}
private LilahUndefeatedSlickshotTriggeredAbility(final LilahUndefeatedSlickshotTriggeredAbility ability) {
super(ability);
}
@Override
public LilahUndefeatedSlickshotTriggeredAbility copy() {
return new LilahUndefeatedSlickshotTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SPELL_CAST;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!isControlledBy(event.getPlayerId())) {
return false;
}
Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell == null || !filter.match(spell, getControllerId(), this, game) || !Zone.HAND.equals(spell.getFromZone())) {
return false;
}
this.getEffects().clear();
this.addEffect(new LilahUndefeatedSlickshotEffect(new MageObjectReference(spell, game)));
return true;
}
@Override
public String getRule() {
return "Whenever you cast an instant or sorcery spell from your hand, "
+ "exile that spell instead of putting it into your graveyard as it resolves. "
+ "If you do, it becomes plotted.";
}
}
/**
* Quite inspired by {@link mage.cards.f.FeatherTheRedeemed Feather the Redeemed}
*/
class LilahUndefeatedSlickshotEffect extends ReplacementEffectImpl {
private final MageObjectReference mor;
LilahUndefeatedSlickshotEffect(MageObjectReference mor) {
super(Duration.WhileOnStack, Outcome.Benefit);
this.mor = mor;
}
private LilahUndefeatedSlickshotEffect(final LilahUndefeatedSlickshotEffect effect) {
super(effect);
this.mor = effect.mor;
}
@Override
public LilahUndefeatedSlickshotEffect copy() {
return new LilahUndefeatedSlickshotEffect(this);
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Spell sourceSpell = game.getStack().getSpell(event.getTargetId());
if (sourceSpell == null || sourceSpell.isCopy()) {
return false;
}
PlotAbility.doExileAndPlotCard(sourceSpell, game, source);
return true;
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
ZoneChangeEvent zEvent = ((ZoneChangeEvent) event);
if (zEvent.getFromZone() != Zone.STACK
|| zEvent.getToZone() != Zone.GRAVEYARD
|| event.getSourceId() == null
|| !event.getSourceId().equals(event.getTargetId())
|| mor.getZoneChangeCounter() != game.getState().getZoneChangeCounter(event.getSourceId())) {
return false;
}
Spell spell = game.getStack().getSpell(mor.getSourceId());
return spell != null && spell.isInstantOrSorcery(game);
}
}

View file

@ -0,0 +1,50 @@
package mage.cards.l;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.BecomesPlottedSourceTriggeredAbility;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.keyword.PlotAbility;
import mage.abilities.keyword.ReachAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.target.common.TargetAnyTarget;
import java.util.UUID;
/**
* @author Susucr
*/
public final class LonghornSharpshooter extends CardImpl {
public LonghornSharpshooter(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}");
this.subtype.add(SubType.MINOTAUR);
this.subtype.add(SubType.ROGUE);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// Reach
this.addAbility(ReachAbility.getInstance());
// When Longhorn Sharpshooter becomes plotted, it deals 2 damage to any target.
Ability ability = new BecomesPlottedSourceTriggeredAbility(new DamageTargetEffect(2));
ability.addTarget(new TargetAnyTarget());
this.addAbility(ability);
// Plot {3}{R}
this.addAbility(new PlotAbility("{3}{R}"));
}
private LonghornSharpshooter(final LonghornSharpshooter card) {
super(card);
}
@Override
public LonghornSharpshooter copy() {
return new LonghornSharpshooter(this);
}
}

View file

@ -0,0 +1,67 @@
package mage.cards.m;
import mage.abilities.Ability;
import mage.abilities.effects.common.LookLibraryAndPickControllerEffect;
import mage.abilities.keyword.PlotAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.Cards;
import mage.constants.CardType;
import mage.constants.PutCards;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
import java.util.UUID;
/**
* @author Susucr
*/
public final class MakeYourOwnLuck extends CardImpl {
public MakeYourOwnLuck(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}{U}");
// Look at the top three cards of your library. You may exile a nonland card from among them. If you do, it becomes plotted. Put the rest into your hand.
this.getSpellAbility().addEffect(new MakeYourOwnLuckEffect());
}
private MakeYourOwnLuck(final MakeYourOwnLuck card) {
super(card);
}
@Override
public MakeYourOwnLuck copy() {
return new MakeYourOwnLuck(this);
}
}
class MakeYourOwnLuckEffect extends LookLibraryAndPickControllerEffect {
MakeYourOwnLuckEffect() {
super(3, 1, StaticFilters.FILTER_CARD_NON_LAND, PutCards.EXILED, PutCards.HAND);
staticText = "Look at the top three cards of your library. "
+ "You may exile a nonland card from among them. "
+ "If you do, it becomes plotted. Put the rest into your hand";
}
private MakeYourOwnLuckEffect(final MakeYourOwnLuckEffect effect) {
super(effect);
}
@Override
public MakeYourOwnLuckEffect copy() {
return new MakeYourOwnLuckEffect(this);
}
@Override
public boolean actionWithPickedCards(Game game, Ability source, Player player, Cards pickedCards, Cards otherCards) {
boolean result = false;
for (Card card : pickedCards.getCards(game)) {
result |= PlotAbility.doExileAndPlotCard(card, game, source);
}
result |= putLookedCards.moveCards(player, otherCards, source, game);
return result;
}
}

View file

@ -4,16 +4,11 @@ import mage.cards.ExpansionSet;
import mage.constants.Rarity;
import mage.constants.SetType;
import java.util.Arrays;
import java.util.List;
/**
* @author TheElk801
*/
public final class OutlawsOfThunderJunction extends ExpansionSet {
// This is a list of cards with the plot mechanic, it will be removed when the mechanic is implemented
private static final List<String> unfinished = Arrays.asList("Aloe Alchemist", "Aven Interrupter", "Beastbond Outcaster", "Blacksnag Buzzard", "Brimstone Roundup", "Cunning Coyote", "Demonic Ruckus", "Djinn of Fool's Fall", "Doc Aurlock, Grizzled Genius", "Fblthp, Lost on the Range", "Irascible Wolverine", "Jace Reawakened", "Kellan Joins Up", "Lilah, Undefeated Slickshot", "Loan Shark", "Longhorn Sharpshooter", "Make Your Own Luck", "Outcaster Trailblazer", "Outlaw Stitcher", "Pillage the Bog", "Pitiless Carnage", "Plan the Heist", "Railway Brawler", "Rictus Robber", "Rise of the Varmints", "Sheriff of Safe Passage", "Slickshot Lockpicker", "Slickshot Show-Off", "Step Between Worlds", "Stingerback Terror", "Tumbleweed Rising", "Unscrupulous Contractor", "Visage Bandit");
private static final OutlawsOfThunderJunction instance = new OutlawsOfThunderJunction();
public static OutlawsOfThunderJunction getInstance() {
@ -27,6 +22,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
this.hasBoosters = false; // temporary
cards.add(new SetCardInfo("Abraded Bluffs", 251, Rarity.COMMON, mage.cards.a.AbradedBluffs.class));
cards.add(new SetCardInfo("Aloe Alchemist", 152, Rarity.UNCOMMON, mage.cards.a.AloeAlchemist.class));
cards.add(new SetCardInfo("Annie Flash, the Veteran", 190, Rarity.MYTHIC, mage.cards.a.AnnieFlashTheVeteran.class));
cards.add(new SetCardInfo("Another Round", 1, Rarity.RARE, mage.cards.a.AnotherRound.class));
cards.add(new SetCardInfo("Archangel of Tithes", 2, Rarity.MYTHIC, mage.cards.a.ArchangelOfTithes.class));
@ -34,6 +30,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
cards.add(new SetCardInfo("Arid Archway", 252, Rarity.UNCOMMON, mage.cards.a.AridArchway.class));
cards.add(new SetCardInfo("Armored Armadillo", 3, Rarity.COMMON, mage.cards.a.ArmoredArmadillo.class));
cards.add(new SetCardInfo("At Knifepoint", 193, Rarity.UNCOMMON, mage.cards.a.AtKnifepoint.class));
cards.add(new SetCardInfo("Aven Interrupter", 4, Rarity.RARE, mage.cards.a.AvenInterrupter.class));
cards.add(new SetCardInfo("Badlands Revival", 194, Rarity.UNCOMMON, mage.cards.b.BadlandsRevival.class));
cards.add(new SetCardInfo("Bandit's Haul", 240, Rarity.UNCOMMON, mage.cards.b.BanditsHaul.class));
cards.add(new SetCardInfo("Baron Bertram Graywater", 195, Rarity.UNCOMMON, mage.cards.b.BaronBertramGraywater.class));
@ -71,6 +68,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
cards.add(new SetCardInfo("Desperate Bloodseeker", 86, Rarity.COMMON, mage.cards.d.DesperateBloodseeker.class));
cards.add(new SetCardInfo("Discerning Peddler", 121, Rarity.COMMON, mage.cards.d.DiscerningPeddler.class));
cards.add(new SetCardInfo("Djinn of Fool's Fall", 43, Rarity.COMMON, mage.cards.d.DjinnOfFoolsFall.class));
cards.add(new SetCardInfo("Doc Aurlock, Grizzled Genius", 201, Rarity.UNCOMMON, mage.cards.d.DocAurlockGrizzledGenius.class));
cards.add(new SetCardInfo("Double Down", 44, Rarity.MYTHIC, mage.cards.d.DoubleDown.class));
cards.add(new SetCardInfo("Duelist of the Mind", 45, Rarity.RARE, mage.cards.d.DuelistOfTheMind.class));
cards.add(new SetCardInfo("Emergent Haunting", 46, Rarity.UNCOMMON, mage.cards.e.EmergentHaunting.class));
@ -110,18 +108,22 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
cards.add(new SetCardInfo("Inventive Wingsmith", 17, Rarity.COMMON, mage.cards.i.InventiveWingsmith.class));
cards.add(new SetCardInfo("Irascible Wolverine", 130, Rarity.COMMON, mage.cards.i.IrascibleWolverine.class));
cards.add(new SetCardInfo("Island", 273, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Jace Reawakened", 271, Rarity.MYTHIC, mage.cards.j.JaceReawakened.class));
cards.add(new SetCardInfo("Jagged Barrens", 259, Rarity.COMMON, mage.cards.j.JaggedBarrens.class));
cards.add(new SetCardInfo("Jolene, Plundering Pugilist", 210, Rarity.UNCOMMON, mage.cards.j.JolenePlunderingPugilist.class));
cards.add(new SetCardInfo("Kellan Joins Up", 212, Rarity.RARE, mage.cards.k.KellanJoinsUp.class));
cards.add(new SetCardInfo("Kraum, Violent Cacophony", 214, Rarity.UNCOMMON, mage.cards.k.KraumViolentCacophony.class));
cards.add(new SetCardInfo("Lassoed by the Law", 18, Rarity.UNCOMMON, mage.cards.l.LassoedByTheLaw.class));
cards.add(new SetCardInfo("Lavaspur Boots", 243, Rarity.UNCOMMON, mage.cards.l.LavaspurBoots.class));
cards.add(new SetCardInfo("Lilah, Undefeated Slickshot", 217, Rarity.RARE, mage.cards.l.LilahUndefeatedSlickshot.class));
cards.add(new SetCardInfo("Loan Shark", 55, Rarity.COMMON, mage.cards.l.LoanShark.class));
cards.add(new SetCardInfo("Lonely Arroyo", 260, Rarity.COMMON, mage.cards.l.LonelyArroyo.class));
cards.add(new SetCardInfo("Longhorn Sharpshooter", 132, Rarity.UNCOMMON, mage.cards.l.LonghornSharpshooter.class));
cards.add(new SetCardInfo("Lush Oasis", 261, Rarity.COMMON, mage.cards.l.LushOasis.class));
cards.add(new SetCardInfo("Luxurious Locomotive", 244, Rarity.UNCOMMON, mage.cards.l.LuxuriousLocomotive.class));
cards.add(new SetCardInfo("Magda, the Hoardmaster", 133, Rarity.RARE, mage.cards.m.MagdaTheHoardmaster.class));
cards.add(new SetCardInfo("Make Your Own Luck", 218, Rarity.UNCOMMON, mage.cards.m.MakeYourOwnLuck.class));
cards.add(new SetCardInfo("Malcolm, the Eyes", 219, Rarity.RARE, mage.cards.m.MalcolmTheEyes.class));
cards.add(new SetCardInfo("Map the Frontier", 170, Rarity.UNCOMMON, mage.cards.m.MapTheFrontier.class));
cards.add(new SetCardInfo("Marauding Sphinx", 56, Rarity.UNCOMMON, mage.cards.m.MaraudingSphinx.class));
cards.add(new SetCardInfo("Marchesa, Dealer of Death", 220, Rarity.RARE, mage.cards.m.MarchesaDealerOfDeath.class));
cards.add(new SetCardInfo("Mine Raider", 135, Rarity.COMMON, mage.cards.m.MineRaider.class));
@ -197,7 +199,5 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
cards.add(new SetCardInfo("Vraska Joins Up", 236, Rarity.RARE, mage.cards.v.VraskaJoinsUp.class));
cards.add(new SetCardInfo("Wanted Griffin", 38, Rarity.COMMON, mage.cards.w.WantedGriffin.class));
cards.add(new SetCardInfo("Wrangler of the Damned", 238, Rarity.UNCOMMON, mage.cards.w.WranglerOfTheDamned.class));
cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); // remove when mechanic is implemented
}
}

View file

@ -0,0 +1,284 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class PlotTest extends CardTestPlayerBase {
/**
* {@link mage.cards.d.DjinnOfFoolsFall Djinn of Fool's Fall} {4}{U}
* Creature Djinn
* Flying
* Plot {3}{U}
* 4/3
*/
private static final String djinn = "Djinn of Fool's Fall";
@Test
public void TestSimplePlot() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
addCard(Zone.HAND, playerA, djinn);
checkPlayableAbility("plot can't be used during upkeep", 1, PhaseStep.UPKEEP, playerA, "Plot", false);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
checkExileCount("plot is in exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, djinn, 1);
checkPlayableAbility("Can not be cast on same turn", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast " + djinn + " using Plot", false);
checkPlayableAbility("Can not be cast on opponent turn", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + djinn + " using Plot", false);
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast " + djinn + " using Plot", false);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + djinn + " using Plot", true);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, djinn + " using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, djinn, 1);
assertTappedCount("Island", true, 0);
}
/**
* {@link mage.cards.l.LonghornSharpshooter Longhorn Sharpshooter} {2}{R}
* Creature Minotaur Rogue
* Reach
* When Longhorn Sharpshooter becomes plotted, it deals 2 damage to any target.
* Plot {3}{R} (You may pay {3}{R} and exile this card from your hand. Cast it as a sorcery on a later turn without paying its mana cost. Plot only as a sorcery.)
* 3/3
*/
private static final String sharpshooter = "Longhorn Sharpshooter";
@Test
public void TestSharpshooterTrigger() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.HAND, playerA, sharpshooter);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
addTarget(playerA, playerB);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertExileCount(playerA, sharpshooter, 1);
assertLife(playerB, 20 - 2);
}
/**
* {@link mage.cards.k.KellanJoinsUp Kellan Joins Up} {G}{W}{U}
* Legendary Enchantment
* <p>
* When Kellan Joins Up enters the battlefield, you may exile a nonland card with mana value 3 or less from your hand. If you do, it becomes plotted. (You may cast it as a sorcery on a later turn without paying its mana cost.)
* <p>
* Whenever a legendary creature enters the battlefield under your control, put a +1/+1 counter on each creature you control.
*/
private static final String kellanJoinsUp = "Kellan Joins Up";
@Test
public void TestKellanJoinsUpTriggerSharpshooter() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.HAND, playerA, kellanJoinsUp);
addCard(Zone.HAND, playerA, sharpshooter);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kellanJoinsUp);
addTarget(playerA, sharpshooter); // choose sharpshooter to exile & plot
addTarget(playerA, playerB); // sharpshooter does trigger and deals 2
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, sharpshooter + " using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, sharpshooter, 1);
assertLife(playerB, 20 - 2);
}
@Test
public void TestPlottingInstant() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.HAND, playerA, kellanJoinsUp);
addCard(Zone.HAND, playerA, "Lightning Bolt");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kellanJoinsUp);
addTarget(playerA, "Lightning Bolt"); // choose Bolt to exile & plot
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Lightning Bolt using Plot", false);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt using Plot", true);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt using Plot", playerB);
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Lightning Bolt", 1);
assertLife(playerB, 20 - 3);
}
@Test
public void TestPlottingAdventure_CastRegularSide() {
setStrictChooseMode(true);
skipInitShuffling();
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.HAND, playerA, kellanJoinsUp);
addCard(Zone.HAND, playerA, "Bramble Familiar");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kellanJoinsUp);
addTarget(playerA, "Bramble Familiar");
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Bramble Familiar using Plot", false);
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Fetch Quest using Plot", false);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Bramble Familiar using Plot", true);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Fetch Quest using Plot", true);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Bramble Familiar using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Bramble Familiar", 1);
}
@Test
public void TestPlottingAdventure_CastAdventureSide() {
setStrictChooseMode(true);
skipInitShuffling();
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.HAND, playerA, kellanJoinsUp);
addCard(Zone.HAND, playerA, "Bramble Familiar");
addCard(Zone.LIBRARY, playerA, "Plateau", 2); // The card. Add 2 since one will be drawn t3
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kellanJoinsUp);
addTarget(playerA, "Bramble Familiar");
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Bramble Familiar using Plot", false);
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Fetch Quest using Plot", false);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Bramble Familiar using Plot", true);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Fetch Quest using Plot", true);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Fetch Quest using Plot");
setChoice(playerA, "Plateau"); // choice for Fetch Quest
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertExileCount(playerA, "Bramble Familiar", 1); // The card is back in exile, but as an adventuring card, not a plotted one.
assertPermanentCount(playerA, "Plateau", 1);
}
@Test
public void TestPlottingSplit_CastLeft() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.HAND, playerA, kellanJoinsUp);
addCard(Zone.HAND, playerA, "Wear // Tear");
addCard(Zone.BATTLEFIELD, playerB, "Memnite");
addCard(Zone.BATTLEFIELD, playerB, "Glorious Anthem");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kellanJoinsUp);
addTarget(playerA, "Wear // Tear");
checkExileCount("assert the full card is in exile", 1, PhaseStep.BEGIN_COMBAT, playerA, "Wear // Tear", 1);
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Wear using Plot", false);
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Tear using Plot", false);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Wear using Plot", true);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Tear using Plot", true);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Wear using Plot", "Memnite");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerB, "Memnite", 0);
assertPermanentCount(playerB, "Glorious Anthem", 1);
assertGraveyardCount(playerA, "Wear // Tear", 1);
}
@Test
public void TestPlottingSplit_CastRight() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.HAND, playerA, kellanJoinsUp);
addCard(Zone.HAND, playerA, "Wear // Tear");
addCard(Zone.BATTLEFIELD, playerB, "Memnite");
addCard(Zone.BATTLEFIELD, playerB, "Glorious Anthem");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kellanJoinsUp);
addTarget(playerA, "Wear // Tear");
checkExileCount("assert the full card is in exile", 1, PhaseStep.BEGIN_COMBAT, playerA, "Wear // Tear", 1);
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Wear using Plot", false);
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Tear using Plot", false);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Wear using Plot", true);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Tear using Plot", true);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Tear using Plot", "Glorious Anthem");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerB, "Memnite", 1);
assertPermanentCount(playerB, "Glorious Anthem", 0);
assertGraveyardCount(playerA, "Wear // Tear", 1);
}
@Test
public void TestPlottingMDFC() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
addCard(Zone.HAND, playerA, kellanJoinsUp);
addCard(Zone.HAND, playerA, "Tangled Florahedron // Tangled Vale");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kellanJoinsUp);
addTarget(playerA, "Tangled Florahedron");
checkExileCount("assert the card is in exile", 1, PhaseStep.BEGIN_COMBAT, playerA, "Tangled Florahedron", 1);
checkPlayableAbility("Can not be cast on non-main phase", 3, PhaseStep.UPKEEP, playerA, "Cast Tangled Florahedron using Plot", false);
checkPlayableAbility("Can not cast lands", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Tangled Vale using Plot", false);
checkPlayableAbility("Can be cast on main phase", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Tangled Florahedron using Plot", true);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Tangled Florahedron using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Tangled Florahedron", 1);
}
}

View file

@ -0,0 +1,51 @@
package org.mage.test.cards.single.otj;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class DocAurlockGrizzledGeniusTest extends CardTestPlayerBase {
/**
* {@link mage.cards.d.DocAurlockGrizzledGenius Doc Aurlock, Grizzled Genius} {G}{U}
* Legendary Creature Bear Druid
* Spells you cast from your graveyard or from exile cost {2} less to cast.
* Plotting cards from your hand costs {2} less.
* 2/3
*/
private static final String doc = "Doc Aurlock, Grizzled Genius";
/**
* {@link mage.cards.d.DjinnOfFoolsFall Djinn of Fool's Fall} {4}{U}
* Creature Djinn
* Flying
* Plot {3}{U}
* 4/3
*/
private static final String djinn = "Djinn of Fool's Fall";
@Test
public void TestPlottingCostReduction() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, doc);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.HAND, playerA, djinn);
checkPlayableAbility("plot can't be used during upkeep", 1, PhaseStep.UPKEEP, playerA, "Plot", false);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
checkExileCount("plot is in exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, djinn, 1);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, djinn + " using Plot");
setStopAt(3, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, djinn, 1);
assertTappedCount("Island", true, 0);
}
}

View file

@ -429,6 +429,7 @@ public abstract class AbilityImpl implements Ability {
case BESTOW:
case MORPH:
case DISGUISE:
case PLOT:
// from Snapcaster Mage:
// If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs
// (such as that of Foil). (2018-12-07)
@ -521,7 +522,7 @@ public abstract class AbilityImpl implements Ability {
String message = controller.getLogName() + " announces a value of " + xValue + " (" + variableCost.getActionText() + ')'
+ CardUtil.getSourceLogName(game, this);
announceString.append(message);
setCostsTag("X",xValue);
setCostsTag("X", xValue);
}
}
return announceString.toString();
@ -626,7 +627,7 @@ public abstract class AbilityImpl implements Ability {
}
addManaCostsToPay(new ManaCostsImpl<>(manaString.toString()));
getManaCostsToPay().setX(xValue * xValueMultiplier, amountMana);
setCostsTag("X",xValue * xValueMultiplier);
setCostsTag("X", xValue * xValueMultiplier);
}
variableManaCost.setPaid();
}
@ -718,7 +719,8 @@ public abstract class AbilityImpl implements Ability {
public Map<String, Object> getCostsTagMap() {
return costsTagMap;
}
public void setCostsTag(String tag, Object value){
public void setCostsTag(String tag, Object value) {
if (costsTagMap == null) {
costsTagMap = new HashMap<>();
}

View file

@ -0,0 +1,44 @@
package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
/**
* @author Susucr
*/
public class BecomesPlottedSourceTriggeredAbility extends TriggeredAbilityImpl {
public BecomesPlottedSourceTriggeredAbility(Effect effect, boolean optional) {
super(Zone.EXILED, effect, optional);
setTriggerPhrase("When {this} becomes plotted, ");
}
public BecomesPlottedSourceTriggeredAbility(Effect effect) {
this(effect, false);
}
protected BecomesPlottedSourceTriggeredAbility(final BecomesPlottedSourceTriggeredAbility ability) {
super(ability);
}
@Override
public BecomesPlottedSourceTriggeredAbility copy() {
return new BecomesPlottedSourceTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.BECOME_PLOTTED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getTargetId().equals(this.getSourceId())) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,48 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.PlotAbility;
import mage.cards.Card;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInHand;
public class MayExileCardFromHandPlottedEffect extends OneShotEffect {
private final FilterCard filter;
public MayExileCardFromHandPlottedEffect(FilterCard filter) {
super(Outcome.PutCardInPlay);
this.filter = filter;
this.staticText = "you may exile a " + filter.getMessage() + " from your hand. If you do, it becomes plotted";
}
private MayExileCardFromHandPlottedEffect(final MayExileCardFromHandPlottedEffect effect) {
super(effect);
this.filter = effect.filter;
}
@Override
public MayExileCardFromHandPlottedEffect copy() {
return new MayExileCardFromHandPlottedEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
TargetCardInHand target = new TargetCardInHand(0, 1, filter);
if (player.chooseTarget(outcome, target, source, game)) {
Card card = game.getCard(target.getFirstTarget());
if (card != null) {
PlotAbility.doExileAndPlotCard(card, game, source);
}
}
return true;
}
}

View file

@ -332,17 +332,17 @@ public class ForetellAbility extends SpecialAction {
if (game.getState().getZone(mainCardId) != Zone.EXILED) {
return ActivationStatus.getFalse();
}
Integer foretoldTurn = (Integer) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number");
UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility");
// Card must be Foretold
if (game.getState().getValue(mainCardId.toString() + "Foretell Turn Number") == null
&& game.getState().getValue(mainCardId + "foretellAbility") == null) {
if (foretoldTurn == null || exileId == null) {
return ActivationStatus.getFalse();
}
// Can't be cast if the turn it was Foretold is the same
if ((int) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number") == game.getTurnNum()) {
if (foretoldTurn == game.getTurnNum()) {
return ActivationStatus.getFalse();
}
// Check that the card is actually in the exile zone (ex: Oblivion Ring exiles it after it was Foretold, etc)
UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility");
ExileZone exileZone = game.getState().getExile().getExileZone(exileId);
if (exileZone != null
&& exileZone.isEmpty()) {

View file

@ -1,26 +1,43 @@
package mage.abilities.keyword;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
import mage.constants.TimingRule;
import mage.constants.Zone;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.*;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.List;
import java.util.UUID;
/**
* TODO: Implement this
*
* @author TheElk801
* @author Susucr
*/
public class PlotAbility extends SpecialAction {
private final String rule;
public PlotAbility(String plotCost) {
super(Zone.HAND);
this.addCost(new ManaCostsImpl<>(plotCost));
this.addEffect(new PlotSourceExileEffect());
this.setTiming(TimingRule.SORCERY);
this.usesStack = false;
this.rule = "Plot " + plotCost;
}
private PlotAbility(final PlotAbility ability) {
super(ability);
this.rule = ability.rule;
}
@Override
@ -30,6 +47,258 @@ public class PlotAbility extends SpecialAction {
@Override
public String getRule() {
return "Plot";
return rule;
}
// TODO: handle [[Fblthp, Lost on the Range]] allowing player to plot from library.
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
// plot can only be activated from a hand
// TODO: change that for Fblthp.
if (game.getState().getZone(getSourceId()) != Zone.HAND) {
return ActivationStatus.getFalse();
}
// suspend uses card's timing restriction
Card card = game.getCard(getSourceId());
if (card == null) {
return ActivationStatus.getFalse();
}
if (!card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game)) {
return ActivationStatus.getFalse();
}
return super.canActivate(playerId, game);
}
static UUID getPlotExileId(UUID playerId, Game game) {
UUID exileId = (UUID) game.getState().getValue("PlotExileId" + playerId.toString());
if (exileId == null) {
exileId = UUID.randomUUID();
game.getState().setValue("PlotExileId" + playerId, exileId);
}
return exileId;
}
static String getPlotTurnKeyForCard(UUID cardId) {
return cardId.toString() + "|" + "Plotted Turn";
}
/**
* To be used in an OneShotEffect's apply.
* 'Plot' the provided card. The card is exiled in it's owner plot zone,
* and may be cast by that player without paying its mana cost at sorcery
* speed on a future turn.
*/
public static boolean doExileAndPlotCard(Card card, Game game, Ability source) {
if (card == null) {
return false;
}
Player owner = game.getPlayer(card.getOwnerId());
if (owner == null) {
return false;
}
UUID exileId = PlotAbility.getPlotExileId(owner.getId(), game);
String exileZoneName = "Plots of " + owner.getName();
Card mainCard = card.getMainCard();
if (mainCard.moveToExile(exileId, exileZoneName, source, game)) {
// Remember on which turn the card was last plotted.
game.getState().setValue(PlotAbility.getPlotTurnKeyForCard(mainCard.getId()), game.getTurnNum());
game.addEffect(new PlotAddSpellAbilityEffect(new MageObjectReference(mainCard, game)), source);
game.informPlayers(owner.getLogName() + " plots " + mainCard.getLogName());
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.BECOME_PLOTTED, mainCard.getId(), source, owner.getId()));
}
return true;
}
}
/**
* Exile the source card in the plot exile zone of its owner
* and allow its owner to cast it at sorcery speed starting
* next turn.
*/
class PlotSourceExileEffect extends OneShotEffect {
PlotSourceExileEffect() {
super(Outcome.Benefit);
}
private PlotSourceExileEffect(final PlotSourceExileEffect effect) {
super(effect);
}
@Override
public PlotSourceExileEffect copy() {
return new PlotSourceExileEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
return PlotAbility.doExileAndPlotCard(game.getCard(source.getSourceId()), game, source);
}
}
class PlotAddSpellAbilityEffect extends ContinuousEffectImpl {
private final MageObjectReference mor;
PlotAddSpellAbilityEffect(MageObjectReference mor) {
super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.mor = mor;
staticText = "Plot card";
}
private PlotAddSpellAbilityEffect(final PlotAddSpellAbilityEffect effect) {
super(effect);
this.mor = effect.mor;
}
@Override
public PlotAddSpellAbilityEffect copy() {
return new PlotAddSpellAbilityEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Card card = mor.getCard(game);
if (card == null) {
discard();
return true;
}
Card mainCard = card.getMainCard();
UUID mainCardId = mainCard.getId();
Player player = game.getPlayer(card.getOwnerId());
if (game.getState().getZone(mainCardId) != Zone.EXILED || player == null) {
discard();
return true;
}
List<Card> faces = CardUtil.getCastableComponents(mainCard, null, source, player, game, null, false);
for (Card face : faces) {
// Add the spell ability to each castable face to have the proper name/paramaters.
PlotSpellAbility ability = new PlotSpellAbility(face.getName());
ability.setSourceId(face.getId());
ability.setControllerId(player.getId());
ability.setSpellAbilityType(face.getSpellAbility().getSpellAbilityType());
game.getState().addOtherAbility(face, ability);
}
return true;
}
}
/**
* This is inspired (after a little cleanup) by how {@link ForetellAbility} does it.
*/
class PlotSpellAbility extends SpellAbility {
private String faceCardName; // Same as with Foretell, we identify the proper face with its spell name.
private SpellAbility spellAbilityToResolve;
PlotSpellAbility(String faceCardName) {
super(null, faceCardName, Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.PLOT);
this.setRuleVisible(false);
this.setAdditionalCostsRuleVisible(false);
this.faceCardName = faceCardName;
this.addCost(new ManaCostsImpl<>("{0}"));
}
private PlotSpellAbility(final PlotSpellAbility ability) {
super(ability);
this.faceCardName = ability.faceCardName;
this.spellAbilityToResolve = ability.spellAbilityToResolve;
}
@Override
public PlotSpellAbility copy() {
return new PlotSpellAbility(this);
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
if (super.canActivate(playerId, game).canActivate()) {
Card card = game.getCard(getSourceId());
if (card != null) {
Card mainCard = card.getMainCard();
UUID mainCardId = mainCard.getId();
// Card must be in the exile zone
if (game.getState().getZone(mainCardId) != Zone.EXILED) {
return ActivationStatus.getFalse();
}
Integer plottedTurn = (Integer) game.getState().getValue(PlotAbility.getPlotTurnKeyForCard(mainCardId));
// Card must have been plotted
if (plottedTurn == null) {
return ActivationStatus.getFalse();
}
// Can't be cast if the turn it was last Plotted is the same
if (plottedTurn == game.getTurnNum()) {
return ActivationStatus.getFalse();
}
// Only allow the cast at sorcery speed
if (!game.canPlaySorcery(playerId)) {
return ActivationStatus.getFalse();
}
// Check that the proper face can be cast.
// TODO: As with Foretell, this does not look very clean. Is the face card sometimes incorrect on calling canActivate?
if (mainCard instanceof CardWithHalves) {
if (((CardWithHalves) mainCard).getLeftHalfCard().getName().equals(faceCardName)) {
return ((CardWithHalves) mainCard).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((CardWithHalves) mainCard).getRightHalfCard().getName().equals(faceCardName)) {
return ((CardWithHalves) mainCard).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
} else if (card instanceof AdventureCard) {
if (card.getMainCard().getName().equals(faceCardName)) {
return card.getMainCard().getSpellAbility().canActivate(playerId, game);
} else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
}
}
return ActivationStatus.getFalse();
}
@Override
public SpellAbility getSpellAbilityToResolve(Game game) {
Card card = game.getCard(getSourceId());
if (card != null) {
if (spellAbilityToResolve == null) {
SpellAbility spellAbilityCopy = null;
// TODO: As with Foretell, this does not look very clean. Is the face card sometimes incorrect on calling getSpellAbilityToResolve?
if (card instanceof CardWithHalves) {
if (((CardWithHalves) card).getLeftHalfCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithHalves) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((CardWithHalves) card).getRightHalfCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithHalves) card).getRightHalfCard().getSpellAbility().copy();
}
} else if (card instanceof AdventureCard) {
if (card.getMainCard().getName().equals(faceCardName)) {
spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
} else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();
}
if (spellAbilityCopy == null) {
return null;
}
spellAbilityCopy.setId(this.getId());
spellAbilityCopy.clearManaCosts();
spellAbilityCopy.clearManaCostsToPay();
spellAbilityCopy.addCost(this.getCosts().copy());
spellAbilityCopy.addCost(this.getManaCosts().copy());
spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
spellAbilityToResolve = spellAbilityCopy;
}
}
return spellAbilityToResolve;
}
@Override
public Costs<Cost> getCosts() {
if (spellAbilityToResolve == null) {
return super.getCosts();
}
return spellAbilityToResolve.getCosts();
}
}

View file

@ -21,7 +21,8 @@ public enum SpellAbilityCastMode {
DISGUISE("Disguise", false, true),
TRANSFORMED("Transformed", true),
DISTURB("Disturb", true),
MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true);
MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true),
PLOT("Plot");
private final String text;
@ -91,6 +92,7 @@ public enum SpellAbilityCastMode {
case MADNESS:
case FLASHBACK:
case DISTURB:
case PLOT:
case MORE_THAN_MEETS_THE_EYE:
// it changes only cost, so keep other characteristics
// TODO: research - why TRANSFORMED here - is it used in this.isTransformed code?!

View file

@ -623,6 +623,12 @@ public class GameEvent implements Serializable {
playerId controller of the creature mentoring
*/
MENTORED_CREATURE,
/* the card becomes plotted
targetId card that was plotted
sourceId of the plotting ability (may be the card itself or another one)
playerId owner of the plotted card (the one able to cast the card)
*/
BECOME_PLOTTED,
//custom events
CUSTOM_EVENT
}

View file

@ -1323,7 +1323,7 @@ public final class CardUtil {
* such as the adventure and main side of adventure spells or both sides of a fuse card.
*
* @param cardToCast
* @param filter A filter to determine if a card is eligible for casting.
* @param filter An optional filter to determine if a card is eligible for casting.
* @param source The ability or source responsible for the casting.
* @param player
* @param game
@ -1347,7 +1347,9 @@ public final class CardUtil {
if (!playLand || !player.canPlayLand() || !game.isActivePlayer(playerId)) {
cards.removeIf(card -> card.isLand(game));
}
if (filter != null) {
cards.removeIf(card -> !filter.match(card, playerId, source, game));
}
if (spellCastTracker != null) {
cards.removeIf(card -> !spellCastTracker.checkCard(card, game));
}