diff --git a/Mage.Sets/src/mage/cards/a/AlteredEgo.java b/Mage.Sets/src/mage/cards/a/AlteredEgo.java index 29fcc368edf..1573b57df86 100644 --- a/Mage.Sets/src/mage/cards/a/AlteredEgo.java +++ b/Mage.Sets/src/mage/cards/a/AlteredEgo.java @@ -1,32 +1,30 @@ - package mage.cards.a; -import java.util.UUID; import mage.MageInt; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.CantBeCounteredSourceAbility; import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.effects.Effect; import mage.abilities.effects.common.CopyPermanentEffect; import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.counters.Counter; import mage.counters.CounterType; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.permanent.Permanent; +import mage.util.functions.CopyApplier; + +import java.util.UUID; /** - * * @author LevelX2 */ public final class AlteredEgo extends CardImpl { public AlteredEgo(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{X}{2}{G}{U}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{X}{2}{G}{U}"); this.subtype.add(SubType.SHAPESHIFTER); this.power = new MageInt(0); this.toughness = new MageInt(0); @@ -35,13 +33,10 @@ public final class AlteredEgo extends CardImpl { this.addAbility(new CantBeCounteredSourceAbility()); // You may have Altered Ego enter the battlefield as a copy of any creature on the battlefield, except it enters with an additional X +1/+1 counters on it. - Effect effect = new CopyPermanentEffect(StaticFilters.FILTER_PERMANENT_CREATURE, null); - effect.setText("a copy of any creature on the battlefield"); - EntersBattlefieldAbility ability = new EntersBattlefieldAbility(effect, true); - effect = new AlteredEgoAddCountersEffect(CounterType.P1P1.createInstance()); - effect.setText(", except it enters with an additional X +1/+1 counters on it"); - ability.addEffect(effect); - this.addAbility(ability); + this.addAbility(new EntersBattlefieldAbility( + new CopyPermanentEffect(StaticFilters.FILTER_PERMANENT_CREATURE, new AlteredEgoCopyApplier()), + true + )); } private AlteredEgo(final AlteredEgo card) { @@ -54,31 +49,29 @@ public final class AlteredEgo extends CardImpl { } } -class AlteredEgoAddCountersEffect extends EntersBattlefieldWithXCountersEffect { +class AlteredEgoCopyApplier extends CopyApplier { - public AlteredEgoAddCountersEffect(Counter counter) { - super(counter); - } - - public AlteredEgoAddCountersEffect(EntersBattlefieldWithXCountersEffect effect) { - super(effect); + @Override + public String getText() { + return ", except it enters with an additional X +1/+1 counters on it"; } @Override - public boolean apply(Game game, Ability source) { - Permanent permanent = game.getPermanentEntering(source.getSourceId()); - if (permanent != null) { - // except only takes place if something was copied - if (permanent.isCopy()) { - return super.apply(game, source); - } + public boolean apply(Game game, MageObject blueprint, Ability source, UUID copyToObjectId) { + // counters only for original card, not copies, see rules: + // 706.9e + // Some replacement effects that generate copy effects include an exception that’s an additional + // effect rather than a modification of the affected object’s characteristics. If another copy + // effect is applied to that object after applying the copy effect with that exception, the + // exception’s effect doesn’t happen. + + if (!isCopyOfCopy(source, blueprint, copyToObjectId)) { + // except it enters with an additional X +1/+1 counters on it + blueprint.getAbilities().add( + new EntersBattlefieldAbility(new EntersBattlefieldWithXCountersEffect(CounterType.P1P1.createInstance())) + ); } - return false; - } - @Override - public EntersBattlefieldWithXCountersEffect copy() { - return new AlteredEgoAddCountersEffect(this); + return true; } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/MoritteOfTheFrost.java b/Mage.Sets/src/mage/cards/m/MoritteOfTheFrost.java index 594b71fecea..950ee66732c 100644 --- a/Mage.Sets/src/mage/cards/m/MoritteOfTheFrost.java +++ b/Mage.Sets/src/mage/cards/m/MoritteOfTheFrost.java @@ -61,7 +61,7 @@ class MoritteOfTheFrostCopyApplier extends CopyApplier { blueprint.addSuperType(SuperType.LEGENDARY); blueprint.addSuperType(SuperType.SNOW); - if (!isCopyOfCopy(source, copyToObjectId) && blueprint.isCreature()) { + if (!isCopyOfCopy(source, blueprint, copyToObjectId) && blueprint.isCreature()) { blueprint.getAbilities().add(new ChangelingAbility()); blueprint.getAbilities().add(new EntersBattlefieldAbility( new AddCountersSourceEffect(CounterType.P1P1.createInstance(2), false) diff --git a/Mage.Sets/src/mage/cards/s/SparkDouble.java b/Mage.Sets/src/mage/cards/s/SparkDouble.java index 985f8b13efd..96193ed92f4 100644 --- a/Mage.Sets/src/mage/cards/s/SparkDouble.java +++ b/Mage.Sets/src/mage/cards/s/SparkDouble.java @@ -26,7 +26,7 @@ import java.util.UUID; */ public final class SparkDouble extends CardImpl { - private static FilterPermanent filter = new FilterControlledPermanent("a creature or planeswalker you control"); + private static final FilterPermanent filter = new FilterControlledPermanent("a creature or planeswalker you control"); static { filter.add(Predicates.or( @@ -40,13 +40,11 @@ public final class SparkDouble extends CardImpl { this.power = new MageInt(0); this.toughness = new MageInt(0); - // You may have Spark Double enter the battlefield as a copy of a creature or planeswalker you control, - // except it enters with an additional +1/+1 counter on it if it’s a creature, - // it enters with an additional loyalty counter on it if it’s a planeswalker, and it isn’t legendary if that permanent is legendary. - Effect effect = new CopyPermanentEffect(filter, new SparkDoubleExceptEffectsCopyApplier()); - effect.setText("as a copy of a creature or planeswalker you control, " + // You may have Spark Double enter the battlefield as a copy of a creature or planeswalker you control, except it enters with an additional +1/+1 counter on it if it’s a creature, it enters with an additional loyalty counter on it if it’s a planeswalker, and it isn’t legendary if that permanent is legendary. + Effect effect = new CopyPermanentEffect(filter, new SparkDoubleCopyApplier()); + /*effect.setText("as a copy of a creature or planeswalker you control, " + "except it enters with an additional +1/+1 counter on it if it's a creature, " - + "it enters with an additional loyalty counter on it if it's a planeswalker, and it isn't legendary if that permanent is legendary."); + + "it enters with an additional loyalty counter on it if it's a planeswalker, and it isn't legendary if that permanent is legendary.");*/ EntersBattlefieldAbility ability = new EntersBattlefieldAbility(effect, true); this.addAbility(ability); } @@ -61,7 +59,14 @@ public final class SparkDouble extends CardImpl { } } -class SparkDoubleExceptEffectsCopyApplier extends CopyApplier { +class SparkDoubleCopyApplier extends CopyApplier { + + @Override + public String getText() { + return ", except it enters with an additional +1/+1 counter on it if it’s a creature, it enters with " + + "an additional loyalty counter on it if it’s a planeswalker, and it isn’t legendary if " + + "that permanent is legendary."; + } @Override public boolean apply(Game game, MageObject blueprint, Ability source, UUID copyToObjectId) { @@ -88,12 +93,12 @@ class SparkDoubleExceptEffectsCopyApplier extends CopyApplier { // Spark Double enters as a planeswalker creature and gets both kinds of counters. // counters only for original card, not copies - if (!isCopyOfCopy(source, copyToObjectId)) { + if (!isCopyOfCopy(source, blueprint, copyToObjectId)) { // enters with an additional +1/+1 counter on it if it’s a creature if (blueprint.isCreature()) { blueprint.getAbilities().add(new EntersBattlefieldAbility( new AddCountersSourceEffect(CounterType.P1P1.createInstance(), false) - .setText("with an additional +1/+1 counter on it") + .setText("with an additional +1/+1 counter on it") )); } @@ -101,7 +106,7 @@ class SparkDoubleExceptEffectsCopyApplier extends CopyApplier { if (blueprint.isPlaneswalker()) { blueprint.getAbilities().add(new EntersBattlefieldAbility( new AddCountersSourceEffect(CounterType.LOYALTY.createInstance(), false) - .setText("with an additional loyalty counter on it") + .setText("with an additional loyalty counter on it") )); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/AlteredEgoTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/AlteredEgoTest.java index 6c3196aecf8..9d8f74121f4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/copy/AlteredEgoTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/AlteredEgoTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.copy; import mage.constants.PhaseStep; @@ -7,7 +6,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class AlteredEgoTest extends CardTestPlayerBase { @@ -24,9 +22,13 @@ public class AlteredEgoTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Altered Ego"); setChoice(playerA, "X=3"); + setChoice(playerA, "Yes"); // use copy + setChoice(playerA, "Silvercoat Lion"); // copy target + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, "Silvercoat Lion", 1); assertPowerToughness(playerA, "Silvercoat Lion", 5, 5); @@ -42,9 +44,12 @@ public class AlteredEgoTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Altered Ego"); setChoice(playerA, "X=3"); + setChoice(playerA, "Yes"); // use copy (but no targets for copy) + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, "Altered Ego", 0); assertGraveyardCount(playerA, "Altered Ego", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/SparkDoubleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/SparkDoubleTest.java index c624bb7fb1d..7164314b83a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/copy/SparkDoubleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/SparkDoubleTest.java @@ -244,4 +244,46 @@ public class SparkDoubleTest extends CardTestPlayerBase { assertAllCommandsUsed(); } + @Test + public void test_SparkCopyEachOther() { + // rules: + // 706.9e Some replacement effects that generate copy effects include an exception that’s an + // additional effect rather than a modification of the affected object’s characteristics. + // If another copy effect is applied to that object after applying the copy effect with that + // exception, the exception’s effect doesn’t happen. + // Example: Altered Ego reads, “You may have Altered Ego enter the battlefield as a copy of any + // creature on the battlefield, except it enters with X additional +1/+1 counters on it.” You + // choose for it to enter the battlefield as a copy of Clone, which reads “You may have Clone + // enter the battlefield as a copy of any creature on the battlefield,” for which no creature + // was chosen as it entered the battlefield. If you then choose a creature to copy as you apply + // the replacement effect Altered Ego gains by copying Clone, Altered Ego’s replacement effect + // won’t cause it to enter the battlefield with any +1/+1 counters on it. + + addCard(Zone.HAND, playerA, "Spark Double", 2); // {3}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 4 * 2); + // + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + + // cast first spark + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spark Double"); + setChoice(playerA, "Yes"); + setChoice(playerA, "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", 2); + + // cast second spark + // rules 706.9e affected, so must get only 1 counter + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spark Double"); + setChoice(playerA, "Yes"); + setChoice(playerA, "Grizzly Bears[only copy]"); + //setChoice(playerA, "Grizzly Bears"); // possible bug: two etb effects + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("after 2", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears", 3); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } + } diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 9f57a60ec4b..d7de1fc71bd 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -1369,7 +1369,7 @@ public class VerifyCardDataTest { public void test_showCardInfo() throws Exception { // debug only: show direct card info (takes it from class file, not from db repository) // can check multiple cards at once, example: name1;name2;name3 - String cardNames = "Dire Fleet Warmonger"; + String cardNames = "Spark Double"; CardScanner.scan(); Arrays.stream(cardNames.split(";")).forEach(cardName -> { cardName = cardName.trim(); diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java index 62b1a840f63..b3fa5953a3c 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopyPermanentEffect.java @@ -53,7 +53,15 @@ public class CopyPermanentEffect extends OneShotEffect { this.applier = applier; this.filter = filter; this.useTargetOfAbility = useTarget; - this.staticText = "as a copy of any " + filter.getMessage() + " on the battlefield"; + + String text = "as a copy of"; + if (filter.getMessage().startsWith("a ") || filter.getMessage().startsWith("an ")) { + text += " " + filter.getMessage(); + } else { + text += " any " + filter.getMessage() + " on battlefield"; + } + text += applier == null ? "" : applier.getText(); + this.staticText = text; } public CopyPermanentEffect(final CopyPermanentEffect effect) { diff --git a/Mage/src/main/java/mage/cards/Card.java b/Mage/src/main/java/mage/cards/Card.java index f6cca7b1495..c4686e25a61 100644 --- a/Mage/src/main/java/mage/cards/Card.java +++ b/Mage/src/main/java/mage/cards/Card.java @@ -182,7 +182,7 @@ public interface Card extends MageObject { } /** - * Commander tax calculation. Can be change from {2} to life life cost (see Liesa, Shroud of Dusk) + * Commander tax calculation. Tax logic can be changed (example: from {2} to life cost, see Liesa, Shroud of Dusk) * * @param game * @param source diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 0d9262d6374..bcb196150d8 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1687,6 +1687,7 @@ public abstract class GameImpl implements Game, Serializable { } } } + // if it was no copy of copy take the target itself if (newBluePrint == null) { newBluePrint = copyFromPermanent.copy(); diff --git a/Mage/src/main/java/mage/util/functions/CopyApplier.java b/Mage/src/main/java/mage/util/functions/CopyApplier.java index 4b1892ed31b..37288ee4903 100644 --- a/Mage/src/main/java/mage/util/functions/CopyApplier.java +++ b/Mage/src/main/java/mage/util/functions/CopyApplier.java @@ -5,7 +5,6 @@ import mage.abilities.Ability; import mage.game.Game; import java.io.Serializable; -import java.util.Objects; import java.util.UUID; /** @@ -20,10 +19,14 @@ public abstract class CopyApplier implements Serializable { // 2. It applies to the blueprint, not the real object (the real object is targetObjectId and can be card or token, even from outside the game like EmptyToken); // 3. "source" is the current copy ability and can be different from the original copy ability (copy of copy); // 4. Don't use "source" param at all; - // 5. Use isCopyOfCopy() to detect it (some effects can apply to copy of copy, but others can't -- see Spark Double as an example). + // 5. For exception/non-copyable effects use isCopyOfCopy() to detect that situation (example: 706.9e, Spark Double, Altered Ego). public abstract boolean apply(Game game, MageObject blueprint, Ability source, UUID targetObjectId); - public boolean isCopyOfCopy(Ability source, UUID targetObjectId) { - return !Objects.equals(targetObjectId, source.getSourceId()); + public boolean isCopyOfCopy(Ability source, MageObject blueprint, UUID targetObjectId) { + return blueprint.isCopy(); + } + + public String getText() { + return ""; } }