[STX] add more cards (#7720)

* implement AcademicProbation
* implement AugmenterPugilist // EchoingEquation
* Implement BalefulMastery
* implement BasicConjuration
* implement ClosingStatement
* Test framework: added custom effect to return card from any zone to hand;

Co-authored-by: Oleg Agafonov <jaydi85@gmail.com>
This commit is contained in:
htrajan 2021-04-09 06:56:34 -07:00 committed by GitHub
parent ac2c2acfe4
commit 10cd439955
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 766 additions and 57 deletions

View file

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

View file

@ -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 arent 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;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<AlternativeCost2> 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.
* <p>
* 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<AlternativeCost2> alternativeCostsToCheck;

View file

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

View file

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