diff --git a/Mage.Sets/src/mage/cards/a/AcademicProbation.java b/Mage.Sets/src/mage/cards/a/AcademicProbation.java new file mode 100644 index 0000000000..229bc44c34 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AcademicProbation.java @@ -0,0 +1,96 @@ +package mage.cards.a; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.effects.Effect; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.common.OpponentsCantCastChosenUntilNextTurnEffect; +import mage.abilities.effects.common.ChooseACardNameEffect; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetNonlandPermanent; + +/** + * + * @author htrajan + */ +public final class AcademicProbation extends CardImpl { + + public AcademicProbation(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{W}"); + + this.subtype.add(SubType.LESSON); + + // Choose one — + // • Choose a nonland card name. Opponents can't cast spells with the chosen name until your next turn. + Effect effect = new ChooseACardNameEffect(ChooseACardNameEffect.TypeOfName.NON_LAND_NAME); + effect.setText("Choose a nonland card name"); + this.getSpellAbility().addEffect(effect); + this.getSpellAbility().addEffect(new OpponentsCantCastChosenUntilNextTurnEffect().setText("opponents can't cast spells with the chosen name until your next turn")); + + // • Choose target nonland permanent. Until your next turn, it can't attack or block, and its activated abilities can't be activated. + Mode restrictMode = new Mode(); + restrictMode.addEffect(new AcademicProbationRestrictionEffect()); + restrictMode.addTarget(new TargetNonlandPermanent()); + this.getSpellAbility().addMode(restrictMode); + } + + private AcademicProbation(final AcademicProbation card) { + super(card); + } + + @Override + public AcademicProbation copy() { + return new AcademicProbation(this); + } +} + +class AcademicProbationRestrictionEffect extends RestrictionEffect { + + AcademicProbationRestrictionEffect() { + super(Duration.UntilYourNextTurn, Outcome.UnboostCreature); + staticText = "choose target nonland permanent. Until your next turn, it can't attack or block, and its activated abilities can't be activated"; + } + + AcademicProbationRestrictionEffect(final AcademicProbationRestrictionEffect effect) { + super(effect); + } + + @Override + public AcademicProbationRestrictionEffect copy() { + return new AcademicProbationRestrictionEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return this.targetPointer.getTargets(game, source).contains(permanent.getId()); + } + + @Override + public boolean canAttack(Game game, boolean canUseChooseDialogs) { + return false; + } + + @Override + public boolean canBlock(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) { + return false; + } + + @Override + public boolean canUseActivatedAbilities(Permanent permanent, Ability source, Game game, boolean canUseChooseDialogs) { + return false; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/AugmenterPugilist.java b/Mage.Sets/src/mage/cards/a/AugmenterPugilist.java new file mode 100644 index 0000000000..802f888fb7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AugmenterPugilist.java @@ -0,0 +1,110 @@ +package mage.cards.a; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.hint.common.LandsYouControlHint; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardSetInfo; +import mage.cards.ModalDoubleFacesCard; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.util.functions.CopyApplier; + +import java.util.UUID; + +/** + * + * @author htrajan + */ +public final class AugmenterPugilist extends ModalDoubleFacesCard { + + public AugmenterPugilist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, + new CardType[]{CardType.CREATURE}, new SubType[]{SubType.TROLL, SubType.DRUID}, "{1}{G}{G}", + "Echoing Equation", new CardType[]{CardType.SORCERY}, new SubType[]{}, "{3}{U}{U}"); + + // 1. + // Augmenter Pugilist + // Creature — Troll Druid + this.getLeftHalfCard().setPT(3, 3); + + // Trample + this.getLeftHalfCard().addAbility(TrampleAbility.getInstance()); + + // As long as you control eight or more lands, Augmenter Pugilist gets +5/+5. + this.getLeftHalfCard().addAbility(new SimpleStaticAbility( + Zone.BATTLEFIELD, + new ConditionalContinuousEffect( + new BoostSourceEffect( + 5, 5, Duration.WhileOnBattlefield + ), + new PermanentsOnTheBattlefieldCondition( + StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND, + ComparisonType.MORE_THAN, 7 + ), + "as long as you control eight or more lands, {this} gets +5/+5" + ) + ).addHint(LandsYouControlHint.instance)); + + // 2. + // Echoing Equation + // Sorcery + + // Choose target creature you control. Each other creature you control becomes a copy of it until end of turn, except those creatures aren’t legendary if the chosen creature is legendary. + this.getRightHalfCard().getSpellAbility().addEffect(new EchoingEquationEffect()); + this.getRightHalfCard().getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); + + } + + private AugmenterPugilist(final AugmenterPugilist card) { + super(card); + } + + @Override + public AugmenterPugilist copy() { + return new AugmenterPugilist(this); + } +} + +class EchoingEquationEffect extends OneShotEffect { + + public EchoingEquationEffect() { + super(Outcome.Benefit); + staticText = "choose target creature you control. Each other creature you control becomes a copy of it until end of turn, except those creatures aren't legendary if the chosen creature is legendary"; + } + + EchoingEquationEffect(EchoingEquationEffect effect) { + super(effect); + } + + @Override + public EchoingEquationEffect copy() { + return new EchoingEquationEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent copyFrom = game.getPermanent(source.getFirstTarget()); + if (copyFrom != null) { + game.getBattlefield().getAllActivePermanents(source.getControllerId()).stream() + .filter(permanent -> permanent.isCreature() && !permanent.getId().equals(copyFrom.getId())) + .forEach(copyTo -> game.copyPermanent(Duration.EndOfTurn, copyFrom, copyTo.getId(), source, new CopyApplier() { + @Override + public boolean apply(Game game, MageObject blueprint, Ability source, UUID targetObjectId) { + blueprint.getSuperType().remove(SuperType.LEGENDARY); + return true; + } + })); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/b/BalefulMastery.java b/Mage.Sets/src/mage/cards/b/BalefulMastery.java new file mode 100644 index 0000000000..790a5db158 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BalefulMastery.java @@ -0,0 +1,87 @@ +package mage.cards.b; + +import mage.abilities.Ability; +import mage.abilities.costs.AlternativeCostSourceAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCreatureOrPlaneswalker; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author htrajan + */ +public final class BalefulMastery extends CardImpl { + + public BalefulMastery(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{B}"); + + // You may pay {1}{B} rather than pay this spell's mana cost. + Ability costAbility = new AlternativeCostSourceAbility(new ManaCostsImpl<>("{1}{B}")); + this.addAbility(costAbility); + + // If the {1}{B} cost was paid, an opponent draws a card. + this.getSpellAbility().addEffect(new BalefulMasteryAlternativeCostEffect(costAbility.getOriginalId())); + + // Exile target creature or planeswalker. + this.getSpellAbility().addEffect(new ExileTargetEffect()); + this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker()); + } + + private BalefulMastery(final BalefulMastery card) { + super(card); + } + + @Override + public BalefulMastery copy() { + return new BalefulMastery(this); + } +} + +class BalefulMasteryAlternativeCostEffect extends OneShotEffect { + + UUID alternativeCostOriginalID; + + BalefulMasteryAlternativeCostEffect(UUID alternativeCostOriginalID) { + super(Outcome.Detriment); + staticText = "if the {1}{B} cost was paid, an opponent draws a card.
"; + this.alternativeCostOriginalID = alternativeCostOriginalID; + } + + BalefulMasteryAlternativeCostEffect(BalefulMasteryAlternativeCostEffect effect) { + super(effect); + this.alternativeCostOriginalID = effect.alternativeCostOriginalID; + } + + @Override + public BalefulMasteryAlternativeCostEffect copy() { + return new BalefulMasteryAlternativeCostEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + boolean wasActivated = AlternativeCostSourceAbility.getActivatedStatus(game, source, this.alternativeCostOriginalID, false); + if (!wasActivated) { + return false; + } + + Player player = game.getPlayer(source.getControllerId()); + TargetOpponent targetOpponent = new TargetOpponent(true); + if (player.chooseTarget(Outcome.DrawCard, targetOpponent, source, game)) { + Player opponent = game.getPlayer(targetOpponent.getFirstTarget()); + if (opponent != null) { + opponent.drawCards(1, source, game); + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/b/BasicConjuration.java b/Mage.Sets/src/mage/cards/b/BasicConjuration.java new file mode 100644 index 0000000000..b4ad758ab4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BasicConjuration.java @@ -0,0 +1,41 @@ +package mage.cards.b; + +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * + * @author htrajan + */ +public final class BasicConjuration extends CardImpl { + + public BasicConjuration(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{G}{G}"); + + this.subtype.add(SubType.LESSON); + + // Look at the top six cards of your library. You may reveal a creature card from among them and put it into your hand. Put the rest on the bottom of your library in a random order. You gain 3 life. + this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect( + 6, 1, StaticFilters.FILTER_CARD_CREATURE_A, + true, false, Zone.HAND, true + ).setBackInRandomOrder(true)); + this.getSpellAbility().addEffect(new GainLifeEffect(3)); + } + + private BasicConjuration(final BasicConjuration card) { + super(card); + } + + @Override + public BasicConjuration copy() { + return new BasicConjuration(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/ClosingStatement.java b/Mage.Sets/src/mage/cards/c/ClosingStatement.java new file mode 100644 index 0000000000..83c29a92f1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ClosingStatement.java @@ -0,0 +1,97 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.IsPhaseCondition; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.abilities.hint.ConditionHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.Target; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.common.TargetCreatureOrPlaneswalker; + +import java.util.UUID; + +/** + * + * @author htrajan + */ +public final class ClosingStatement extends CardImpl { + + private static final FilterCreatureOrPlaneswalkerPermanent filter = new FilterCreatureOrPlaneswalkerPermanent("creature or planeswalker you don't control"); + + static { + filter.add(TargetController.NOT_YOU.getControllerPredicate()); + } + + public ClosingStatement(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{W}{B}"); + + // This spell costs {2} less to cast during your end step. + IsPhaseCondition condition = new IsPhaseCondition(TurnPhase.END, true); + SimpleStaticAbility ability = new SimpleStaticAbility(Zone.ALL, new SpellCostReductionSourceEffect(2, condition).setText("this spell costs {2} less to cast during your end step")); + ability.addHint(new ConditionHint(condition, "On your end step")); + ability.setRuleAtTheTop(true); + this.addAbility(ability); + + // Destroy target creature or planeswalker you don't control. Put a +1/+1 counter on up to one target creature you control. + this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker(1, 1, filter, false)); + this.getSpellAbility().addEffect(new DestroyTargetEffect()); + Target target = new TargetControlledCreaturePermanent(0, 1); + target.setTargetTag(2); + this.getSpellAbility().addTarget(target); + this.getSpellAbility().addEffect(new ClosingStatementEffect()); + } + + private ClosingStatement(final ClosingStatement card) { + super(card); + } + + @Override + public ClosingStatement copy() { + return new ClosingStatement(this); + } +} + +class ClosingStatementEffect extends OneShotEffect { + + ClosingStatementEffect() { + super(Outcome.Benefit); + staticText = "put a +1/+1 counter on up to one target creature you control"; + } + + private ClosingStatementEffect(final ClosingStatementEffect effect) { + super(effect); + } + + @Override + public ClosingStatementEffect copy() { + return new ClosingStatementEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Target target = source.getTargets().stream() + .filter(t -> t.getTargetTag() == 2) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Expected to find target with tag 2 but none exists")); + Permanent permanent = game.getPermanent(target.getFirstTarget()); + if (permanent != null) { + return permanent.addCounters(CounterType.P1P1.createInstance(), source.getControllerId(), source, game); + } + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/f/FailureComply.java b/Mage.Sets/src/mage/cards/f/FailureComply.java index 22f348f4ad..cd6066ff31 100644 --- a/Mage.Sets/src/mage/cards/f/FailureComply.java +++ b/Mage.Sets/src/mage/cards/f/FailureComply.java @@ -1,23 +1,15 @@ package mage.cards.f; -import mage.MageObject; -import mage.abilities.Ability; -import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.common.ChooseACardNameEffect; +import mage.abilities.effects.common.OpponentsCantCastChosenUntilNextTurnEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.abilities.keyword.AftermathAbility; import mage.cards.CardSetInfo; import mage.cards.SplitCard; import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; import mage.constants.SpellAbilityType; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.target.TargetSpell; -import mage.util.CardUtil; import java.util.UUID; @@ -41,7 +33,7 @@ public final class FailureComply extends SplitCard { Effect effect = new ChooseACardNameEffect(ChooseACardNameEffect.TypeOfName.ALL); effect.setText("Choose a card name"); getRightHalfCard().getSpellAbility().addEffect(effect); - getRightHalfCard().getSpellAbility().addEffect(new ComplyCantCastEffect()); + getRightHalfCard().getSpellAbility().addEffect(new OpponentsCantCastChosenUntilNextTurnEffect()); } private FailureComply(final FailureComply card) { @@ -53,45 +45,3 @@ public final class FailureComply extends SplitCard { return new FailureComply(this); } } - -class ComplyCantCastEffect extends ContinuousRuleModifyingEffectImpl { - - public ComplyCantCastEffect() { - super(Duration.UntilYourNextTurn, Outcome.Benefit); - staticText = "Until your next turn, your opponents can't cast spells with the chosen name"; - } - - public ComplyCantCastEffect(final ComplyCantCastEffect effect) { - super(effect); - } - - @Override - public ComplyCantCastEffect copy() { - return new ComplyCantCastEffect(this); - } - - @Override - public String getInfoMessage(Ability source, GameEvent event, Game game) { - MageObject mageObject = game.getObject(source.getSourceId()); - String cardName = (String) game.getState().getValue(source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY); - if (mageObject != null && cardName != null) { - return "You can't cast a card named " + cardName + " (" + mageObject.getIdName() + ")."; - } - return null; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.CAST_SPELL_LATE; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - String cardName = (String) game.getState().getValue(source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY); - if (game.getOpponents(source.getControllerId()).contains(event.getPlayerId())) { - MageObject object = game.getObject(event.getSourceId()); - return object != null && CardUtil.haveSameNames(object, cardName, game); - } - return false; - } -} diff --git a/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java b/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java index 1debd9121f..df3c8805f8 100644 --- a/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java +++ b/Mage.Sets/src/mage/sets/StrixhavenSchoolOfMages.java @@ -33,6 +33,7 @@ public final class StrixhavenSchoolOfMages extends ExpansionSet { this.maxCardNumberInBooster = 275; cards.add(new SetCardInfo("Academic Dispute", 91, Rarity.UNCOMMON, mage.cards.a.AcademicDispute.class)); + cards.add(new SetCardInfo("Academic Probation", 287, Rarity.RARE, mage.cards.a.AcademicProbation.class)); cards.add(new SetCardInfo("Access Tunnel", 262, Rarity.UNCOMMON, mage.cards.a.AccessTunnel.class)); cards.add(new SetCardInfo("Accomplished Alchemist", 119, Rarity.RARE, mage.cards.a.AccomplishedAlchemist.class)); cards.add(new SetCardInfo("Aether Helix", 162, Rarity.UNCOMMON, mage.cards.a.AetherHelix.class)); @@ -42,6 +43,9 @@ public final class StrixhavenSchoolOfMages extends ExpansionSet { cards.add(new SetCardInfo("Archway Commons", 263, Rarity.COMMON, mage.cards.a.ArchwayCommons.class)); cards.add(new SetCardInfo("Ardent Dustspeaker", 92, Rarity.UNCOMMON, mage.cards.a.ArdentDustspeaker.class)); cards.add(new SetCardInfo("Arrogant Poet", 63, Rarity.COMMON, mage.cards.a.ArrogantPoet.class)); + cards.add(new SetCardInfo("Augmenter Pugilist", 147, Rarity.RARE, mage.cards.a.AugmenterPugilist.class)); + cards.add(new SetCardInfo("Baleful Mastery", 64, Rarity.RARE, mage.cards.b.BalefulMastery.class)); + cards.add(new SetCardInfo("Basic Conjuration", 120, Rarity.RARE, mage.cards.b.BasicConjuration.class)); cards.add(new SetCardInfo("Bayou Groff", 121, Rarity.COMMON, mage.cards.b.BayouGroff.class)); cards.add(new SetCardInfo("Beaming Defiance", 9, Rarity.COMMON, mage.cards.b.BeamingDefiance.class)); cards.add(new SetCardInfo("Beledros Witherbloom", 163, Rarity.MYTHIC, mage.cards.b.BeledrosWitherbloom.class)); @@ -62,6 +66,7 @@ public final class StrixhavenSchoolOfMages extends ExpansionSet { cards.add(new SetCardInfo("Campus Guide", 252, Rarity.COMMON, mage.cards.c.CampusGuide.class)); cards.add(new SetCardInfo("Charge Through", 124, Rarity.COMMON, mage.cards.c.ChargeThrough.class)); cards.add(new SetCardInfo("Clever Lumimancer", 10, Rarity.UNCOMMON, mage.cards.c.CleverLumimancer.class)); + cards.add(new SetCardInfo("Closing Statement", 169, Rarity.UNCOMMON, mage.cards.c.ClosingStatement.class)); cards.add(new SetCardInfo("Cogwork Archivist", 254, Rarity.COMMON, mage.cards.c.CogworkArchivist.class)); cards.add(new SetCardInfo("Combat Professor", 11, Rarity.COMMON, mage.cards.c.CombatProfessor.class)); cards.add(new SetCardInfo("Confront the Past", 67, Rarity.RARE, mage.cards.c.ConfrontThePast.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/stx/BalefulMasteryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/stx/BalefulMasteryTest.java new file mode 100644 index 0000000000..155b7fd98f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/stx/BalefulMasteryTest.java @@ -0,0 +1,182 @@ +package org.mage.test.cards.single.stx; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author htrajan, JayDi85 + */ +public class BalefulMasteryTest extends CardTestPlayerBase { + + @Test + public void test_BalefulMastery_NormalCost() { + // You may pay {1}{B} rather than pay this spell's mana cost. + // If the {1}{B} cost was paid, an opponent draws a card. + // Exile target creature or planeswalker. + addCard(Zone.HAND, playerA, "Baleful Mastery"); // {3}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + // + addCard(Zone.BATTLEFIELD, playerB, "Goblin Piker"); + addCard(Zone.BATTLEFIELD, playerB, "Witchbane Orb"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); + setChoice(playerA, "No"); // use normal cost + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertHandCount(playerA, 0); + assertHandCount(playerB, 0); + assertExileCount(playerB, "Goblin Piker", 1); + } + + @Test + public void test_BalefulMastery_AlternativeCost() { + // You may pay {1}{B} rather than pay this spell's mana cost. + // If the {1}{B} cost was paid, an opponent draws a card. + // Exile target creature or planeswalker. + addCard(Zone.HAND, playerA, "Baleful Mastery"); // {3}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + // + addCard(Zone.BATTLEFIELD, playerB, "Goblin Piker"); + addCard(Zone.BATTLEFIELD, playerB, "Witchbane Orb"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); + setChoice(playerA, "Yes"); // use alternative cost + addTarget(playerA, playerB); // select opponent + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertHandCount(playerA, 0); + assertHandCount(playerB, 1); // +1 from cost's draw + assertExileCount(playerB, "Goblin Piker", 1); + } + + @Test + public void test_BalefulMastery_DoubleCast() { + // You may pay {1}{B} rather than pay this spell's mana cost. + // If the {1}{B} cost was paid, an opponent draws a card. + // Exile target creature or planeswalker. + addCard(Zone.HAND, playerA, "Baleful Mastery", 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2 + 4); // 1x normal, 1x alternative + // + addCard(Zone.BATTLEFIELD, playerB, "Goblin Piker"); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + + // cast 1 - alternative + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); + setChoice(playerA, "Yes"); // use alternative cost + addTarget(playerA, playerB); // select opponent + + // cast 2 - normal + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Grizzly Bears"); + setChoice(playerA, "No"); // normal cast + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertHandCount(playerA, 0); + assertHandCount(playerB, 1); + assertExileCount(playerB, "Goblin Piker", 1); + assertExileCount(playerB, "Grizzly Bears", 1); + } + + @Test + public void test_BalefulMastery_BlinkMustResetAlternativeCost() { + addCustomEffect_ReturnFromAnyToHand(playerA); + + // You may pay {1}{B} rather than pay this spell's mana cost. + // If the {1}{B} cost was paid, an opponent draws a card. + // Exile target creature or planeswalker. + addCard(Zone.HAND, playerA, "Baleful Mastery"); // {3}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2 + 4); // 1x normal, 1x alternative + // + addCard(Zone.BATTLEFIELD, playerB, "Goblin Piker"); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + + // cast 1 - with alternative + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); + setChoice(playerA, "Yes"); // use alternative cost + addTarget(playerA, playerB); // select opponent + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkGraveyardCount("after cast 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", 1); + checkHandCount("after cast 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 0); + checkHandCount("after cast 1", 1, PhaseStep.PRECOMBAT_MAIN, playerB, 1); // +1 from cost's draw + checkExileCount("after cast 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin Piker", 1); + checkExileCount("after cast 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", 0); + + // return to hand + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "return from graveyard"); + addTarget(playerA, "Baleful Mastery"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkHandCardCount("after return", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", 1); + + // cast 2 - without alternative + // possible bug: cost status can be found from previous object (e.g. it ask about opponent select, but must not) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Grizzly Bears"); + setChoice(playerA, "No"); // do not use alternative cost + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkGraveyardCount("after cast 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", 1); + checkHandCount("after cast 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 0); + checkHandCount("after cast 2", 1, PhaseStep.PRECOMBAT_MAIN, playerB, 1); // no draws on cast 2 + checkExileCount("after cast 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin Piker", 1); + checkExileCount("after cast 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_BalefulMastery_CopyMustKeepAlternativeCost() { + // You may pay {1}{B} rather than pay this spell's mana cost. + // If the {1}{B} cost was paid, an opponent draws a card. + // Exile target creature or planeswalker. + addCard(Zone.HAND, playerA, "Baleful Mastery"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + // + // Copy target instant or sorcery spell. You may choose new targets for the copy. + addCard(Zone.HAND, playerA, "Twincast"); // {U}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + // + addCard(Zone.BATTLEFIELD, playerB, "Goblin Piker"); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + + // cast with alternative + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 2); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Baleful Mastery", "Goblin Piker"); + setChoice(playerA, "Yes"); // use alternative cost + // copy spell + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Twincast", "Cast Baleful Mastery", "Cast Baleful Mastery"); + setChoice(playerA, "Yes"); // change target + addTarget(playerA, "Grizzly Bears"); // new target + checkStackSize("before copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true); + checkStackSize("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); + // + // resolve copied spell + // possible bug: alternative cost will be lost for copied spell, so no opponent selections + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true); + addTarget(playerA, playerB); // select opponent + checkStackSize("after copy resolve", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); + // resolve original spell + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true); + addTarget(playerA, playerB); // select opponent + checkStackSize("after original resolve", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 0); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java index c9bdd0da24..533367ec58 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java @@ -11,8 +11,11 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.effects.common.ReturnFromExileEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; import mage.abilities.effects.common.cost.SpellsCostIncreasingAllEffect; import mage.abilities.effects.common.cost.SpellsCostReductionAllEffect; +import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -35,6 +38,9 @@ import mage.server.util.config.GamePlugin; import mage.server.util.config.Plugin; import mage.target.TargetPermanent; import mage.target.common.TargetAnyTarget; +import mage.target.common.TargetCardInExile; +import mage.target.common.TargetCardInGraveyard; +import mage.target.common.TargetCardInLibrary; import mage.util.CardUtil; import mage.util.Copier; import org.apache.log4j.Level; @@ -484,6 +490,39 @@ public abstract class MageTestPlayerBase { ability ); } + + /** + * Return target card to hand that can be called by text "return from ..." + * + * @param controller + */ + protected void addCustomEffect_ReturnFromAnyToHand(TestPlayer controller) { + // graveyard + Ability ability = new SimpleActivatedAbility(new ReturnFromGraveyardToHandTargetEffect().setText("return from graveyard"), new ManaCostsImpl("")); + ability.addTarget(new TargetCardInGraveyard(StaticFilters.FILTER_CARD)); + addCustomCardWithAbility( + "return from graveyard for " + controller.getName(), + controller, + ability + ); + + // exile + ability = new SimpleActivatedAbility(new ReturnFromExileEffect(Zone.HAND).setText("return from exile"), new ManaCostsImpl("")); + ability.addTarget(new TargetCardInExile(StaticFilters.FILTER_CARD)); + addCustomCardWithAbility( + "return from exile for " + controller.getName(), + controller, + ability + ); + + // library + ability = new SimpleActivatedAbility(new SearchLibraryPutInHandEffect(new TargetCardInLibrary(StaticFilters.FILTER_CARD)).setText("return from library"), new ManaCostsImpl("")); + addCustomCardWithAbility( + "return from library for " + controller.getName(), + controller, + ability + ); + } } // custom card with global abilities list to init (can contains abilities per card name) diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index d287515207..4f448d6a71 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -586,6 +586,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * * @param player {@link Player} to remove all cards from hand. */ + @Deprecated // TODO: remove, cause test games don't use starting draws public void removeAllCardsFromHand(TestPlayer player) { getCommands(player).put(Zone.HAND, "clear"); } diff --git a/Mage/src/main/java/mage/abilities/condition/common/IsPhaseCondition.java b/Mage/src/main/java/mage/abilities/condition/common/IsPhaseCondition.java index 95a7c514cb..0e00e9b6b8 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/IsPhaseCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/IsPhaseCondition.java @@ -15,19 +15,29 @@ import java.util.Locale; public class IsPhaseCondition implements Condition { protected TurnPhase turnPhase; + protected boolean yourTurn; public IsPhaseCondition(TurnPhase turnPhase) { + this(turnPhase, false); + } + + public IsPhaseCondition(TurnPhase turnPhase, boolean yourTurn) { this.turnPhase = turnPhase; + this.yourTurn = yourTurn; } @Override public boolean apply(Game game, Ability source) { - return turnPhase == game.getTurn().getPhaseType(); + return turnPhase == game.getTurn().getPhaseType() && (!yourTurn || game.getActivePlayerId().equals(source.getControllerId())); } @Override public String toString() { - return new StringBuilder("during ").append(turnPhase).toString().toLowerCase(Locale.ENGLISH); + return new StringBuilder("during ") + .append(yourTurn ? "your " : "") + .append(turnPhase) + .toString() + .toLowerCase(Locale.ENGLISH); } } diff --git a/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java b/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java index 99d39ab90a..53d22aa391 100644 --- a/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java +++ b/Mage/src/main/java/mage/abilities/costs/AlternativeCostSourceAbility.java @@ -1,4 +1,3 @@ - package mage.abilities.costs; import mage.abilities.Ability; @@ -16,13 +15,15 @@ import mage.players.Player; import mage.util.CardUtil; import java.util.Iterator; -import mage.MageObject; +import java.util.UUID; /** * @author LevelX2 */ public class AlternativeCostSourceAbility extends StaticAbility implements AlternativeSourceCosts { + private static final String ALTERNATIVE_COST_ACTIVATION_KEY = "AlternativeCostActivated"; + private Costs alternateCosts = new CostsImpl<>(); protected Condition condition; protected String rule; @@ -159,6 +160,9 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter } } } + + // save activated status + game.getState().setValue(getActivatedKey(ability), Boolean.TRUE); } else { return false; } @@ -169,6 +173,38 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter return isActivated(ability, game); } + private String getActivatedKey(Ability source) { + return getActivatedKey(this.getOriginalId(), source.getSourceId(), source.getSourceObjectZoneChangeCounter()); + } + + private static String getActivatedKey(UUID alternativeCostOriginalId, UUID sourceId, int sourceZCC) { + // can't use sourceId cause copied cards are different... + // TODO: enable sourceId after copy card fix (it must copy cards with all related game state values) + return ALTERNATIVE_COST_ACTIVATION_KEY + "_" + alternativeCostOriginalId + "_" /*+ sourceId + "_"*/ + sourceZCC; + } + + /** + * Search activated status of alternative cost. + *

+ * If you need it on resolve then use current ZCC (on stack) + * If you need it on battlefield then use previous ZCC (-1) + * + * @param game + * @param source + * @param alternativeCostOriginalId you must save originalId on card's creation + * @param searchPrevZCC true on battlefield, false on stack + * @return + */ + public static boolean getActivatedStatus(Game game, Ability source, UUID alternativeCostOriginalId, boolean searchPrevZCC) { + String key = getActivatedKey( + alternativeCostOriginalId, + source.getSourceId(), + source.getSourceObjectZoneChangeCounter() + (searchPrevZCC ? -1 : 0) + ); + Boolean status = (Boolean) game.getState().getValue(key); + return status != null && status; + } + @Override public boolean isActivated(Ability source, Game game) { Costs alternativeCostsToCheck; diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java index 4ff4e4bac9..5db00825bc 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java @@ -78,7 +78,7 @@ public class CopyEffect extends ContinuousEffectImpl { Permanent permanent = affectedObjectList.get(0).getPermanent(game); if (permanent == null) { permanent = (Permanent) game.getLastKnownInformation(getSourceId(), Zone.BATTLEFIELD, source.getSourceObjectZoneChangeCounter()); - // As long as the permanent is still in the LKI continue to copy to get triggered abilities to TriggeredAbilites for dies events. + // As long as the permanent is still in the LKI continue to copy to get triggered abilities to TriggeredAbilities for dies events. if (permanent == null) { discard(); return false; diff --git a/Mage/src/main/java/mage/abilities/effects/common/OpponentsCantCastChosenUntilNextTurnEffect.java b/Mage/src/main/java/mage/abilities/effects/common/OpponentsCantCastChosenUntilNextTurnEffect.java new file mode 100644 index 0000000000..2eba8953dc --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/OpponentsCantCastChosenUntilNextTurnEffect.java @@ -0,0 +1,55 @@ +package mage.abilities.effects.common; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.util.CardUtil; + +/** + * This effect must be used in tandem with ChooseACardNameEffect + */ +public class OpponentsCantCastChosenUntilNextTurnEffect extends ContinuousRuleModifyingEffectImpl { + + public OpponentsCantCastChosenUntilNextTurnEffect() { + super(Duration.UntilYourNextTurn, Outcome.Benefit); + staticText = "Until your next turn, your opponents can't cast spells with the chosen name"; + } + + public OpponentsCantCastChosenUntilNextTurnEffect(final OpponentsCantCastChosenUntilNextTurnEffect effect) { + super(effect); + } + + @Override + public OpponentsCantCastChosenUntilNextTurnEffect copy() { + return new OpponentsCantCastChosenUntilNextTurnEffect(this); + } + + @Override + public String getInfoMessage(Ability source, GameEvent event, Game game) { + MageObject mageObject = game.getObject(source.getSourceId()); + String cardName = (String) game.getState().getValue(source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY); + if (mageObject != null && cardName != null) { + return "You can't cast a card named " + cardName + " (" + mageObject.getIdName() + ")."; + } + return null; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAST_SPELL_LATE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + String cardName = (String) game.getState().getValue(source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY); + if (game.getOpponents(source.getControllerId()).contains(event.getPlayerId())) { + MageObject object = game.getObject(event.getSourceId()); + return object != null && CardUtil.haveSameNames(object, cardName, game); + } + return false; + } +}