diff --git a/Mage.Sets/src/mage/cards/w/WhipgrassEntangler.java b/Mage.Sets/src/mage/cards/w/WhipgrassEntangler.java index 1d84afd6fbd8..817f6a72d399 100644 --- a/Mage.Sets/src/mage/cards/w/WhipgrassEntangler.java +++ b/Mage.Sets/src/mage/cards/w/WhipgrassEntangler.java @@ -1,21 +1,20 @@ package mage.cards.w; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.common.LinkedEffectIdStaticAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; -import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.abilities.effects.common.combat.CantAttackBlockUnlessPaysSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.constants.Zone; import mage.filter.FilterPermanent; import mage.game.Game; @@ -23,23 +22,24 @@ import mage.game.permanent.Permanent; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** - * - * @author emerald000 & L_J + * @author Susucr */ public final class WhipgrassEntangler extends CardImpl { public WhipgrassEntangler(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{W}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.CLERIC); this.power = new MageInt(1); this.toughness = new MageInt(3); // {1}{W}: Until end of turn, target creature gains "This creature can't attack or block unless its controller pays {1} for each Cleric on the battlefield." - Ability abilityToGain = new SimpleStaticAbility(Zone.BATTLEFIELD, new WhipgrassEntanglerCantAttackUnlessYouPayEffect()); - Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, - new GainAbilityTargetEffect(abilityToGain, Duration.EndOfTurn).setText("Until end of turn, target creature gains \"This creature can't attack or block unless its controller pays {1} for each Cleric on the battlefield.\""), + Ability gainedAbility = new LinkedEffectIdStaticAbility(new WhipgrassEntanglerCantAttackUnlessYouPayEffect()); + Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, + new GainAbilityTargetEffect(gainedAbility, Duration.EndOfTurn).setText("Until end of turn, target creature gains \"This creature can't attack or block unless its controller pays {1} for each Cleric on the battlefield.\""), new ManaCostsImpl<>("{1}{W}")); ability.addTarget(new TargetCreaturePermanent()); this.addAbility(ability); @@ -56,9 +56,10 @@ public WhipgrassEntangler copy() { } } -class WhipgrassEntanglerCantAttackUnlessYouPayEffect extends CantAttackBlockUnlessPaysSourceEffect { +class WhipgrassEntanglerCantAttackUnlessYouPayEffect extends CantAttackBlockUnlessPaysSourceEffect implements LinkedEffectIdStaticAbility.ChildEffect { private static final FilterPermanent filter = new FilterPermanent("Cleric on the battlefield"); + private UUID parentLinkHandshake = null; static { filter.add(SubType.CLERIC.getPredicate()); @@ -69,13 +70,16 @@ class WhipgrassEntanglerCantAttackUnlessYouPayEffect extends CantAttackBlockUnle staticText = "This creature can't attack or block unless its controller pays {1} for each Cleric on the battlefield"; } - WhipgrassEntanglerCantAttackUnlessYouPayEffect(WhipgrassEntanglerCantAttackUnlessYouPayEffect effect) { + private WhipgrassEntanglerCantAttackUnlessYouPayEffect(final WhipgrassEntanglerCantAttackUnlessYouPayEffect effect) { super(effect); + this.parentLinkHandshake = effect.parentLinkHandshake; } @Override public boolean applies(GameEvent event, Ability source, Game game) { - return source.getSourceId().equals(event.getSourceId()); + return source.getSourceId().equals(event.getSourceId()) + && source instanceof LinkedEffectIdStaticAbility + && ((LinkedEffectIdStaticAbility) source).checkLinked(this.parentLinkHandshake); } @Override @@ -95,4 +99,16 @@ public WhipgrassEntanglerCantAttackUnlessYouPayEffect copy() { return new WhipgrassEntanglerCantAttackUnlessYouPayEffect(this); } + @Override + public void newId() { + // No id set this way. Parent Ability takes responsability of calling manualNewId() + } + + public void manualNewId() { + super.newId(); + } + + public void setParentLinkHandshake(UUID parentLinkHandshake) { + this.parentLinkHandshake = parentLinkHandshake; + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lgn/WhipgrassEntanglerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lgn/WhipgrassEntanglerTest.java new file mode 100644 index 000000000000..f51b9b70d48e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lgn/WhipgrassEntanglerTest.java @@ -0,0 +1,146 @@ +package org.mage.test.cards.single.lgn; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class WhipgrassEntanglerTest extends CardTestPlayerBase { + + /** + * Whipgrass Entangler + * {2}{W} + * Creature — Human Cleric + *
+ * {1}{W}: Until end of turn, target creature gains "This creature can't attack or block unless its controller pays {1} for each Cleric on the battlefield." + *
+ * 1/3 + */ + private static final String entangler = "Whipgrass Entangler"; + + @Test + public void gainAbilityHimselfAndPay() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, entangler); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 20); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", entangler); + attack(1, playerA, entangler); + setChoice(playerA, true); // yes to "pay {1} for Entangler to attack" + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 1); + assertTappedCount("Plains", true, 2 * 1 + 1 * 1); + } + + @Test + public void gainAbilityTwiceAndPay() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, entangler); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 20); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", entangler); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", entangler); + attack(1, playerA, entangler); + setChoice(playerA, "Whipgrass Entangler"); // two of the same replacement effect to choose from. + setChoice(playerA, true); // yes to "pay {1} for Entangler to attack" + setChoice(playerA, true); // yes to "pay {1} for Entangler to attack" + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 1); + assertTappedCount("Plains", true, 2 * 2 + 1 * 2); + } + + @Test + public void gainAbilityWithTwoClericAndPay() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, entangler); + addCard(Zone.BATTLEFIELD, playerA, "Akroma's Devoted"); // 2/4 Cleric + addCard(Zone.BATTLEFIELD, playerA, "Plains", 20); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", entangler); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", "Akroma's Devoted"); + attack(1, playerA, entangler); + setChoice(playerA, true); // yes to "pay {2} for Entangler to attack" + attack(1, playerA, "Akroma's Devoted"); + setChoice(playerA, true); // yes to "pay {2} for Akroma's Devoted to attack" + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 1 - 2); + assertTappedCount("Plains", true, 2 * 2 + 2 * 2); + } + + @Test + public void gainAbilityFromTwoEntanglerAndPay() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Akroma's Devoted"); // 2/4 Cleric + addCard(Zone.BATTLEFIELD, playerA, entangler, 1); + + // In order to make sure the ability is activated from both entanglers, we exile one and play the second one after + addCard(Zone.HAND, playerA, entangler); + addCard(Zone.HAND, playerA, "Swords to Plowshares", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 20); + + // Activate first entangler + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", "Akroma's Devoted"); + // Exile first entangler + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swords to Plowshares", entangler, true); + + // Cast second entangler + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, entangler, true); + // Activate second entangler + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", "Akroma's Devoted"); + + attack(1, playerA, "Akroma's Devoted"); + setChoice(playerA, "Akroma's Devoted"); // two of the same replacement effect to choose from. + setChoice(playerA, true); // yes to "pay {2} for Akroma's Devoted to attack" + setChoice(playerA, true); // yes to "pay {2} for Akroma's Devoted to attack" + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 2); + assertTappedCount("Plains", true, 2 * 2 + 2 * 2 + 3 + 1); + } + + @Test + public void gainAbilityWithCloneAndPay() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Akroma's Devoted"); // 2/4 Cleric + addCard(Zone.BATTLEFIELD, playerA, entangler); + addCard(Zone.HAND, playerA, "Sakashima the Impostor"); // Clone with different name for easy targets + + // In order to make sure the ability is activated from the clone, we exile the original one before activating Sakashima + addCard(Zone.HAND, playerA, "Swords to Plowshares", 1); + addCard(Zone.BATTLEFIELD, playerA, "Tundra", 20); + + // Activate the original Entangler + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", "Akroma's Devoted"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + // Cast Sakashima + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sakashima the Impostor", true); + setChoice(playerA, true); // yes to clone. + setChoice(playerA, entangler); // copy entangler + // Exile original entangler + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Swords to Plowshares", entangler, true); + // Activate Sakashima. + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}: Until end of turn, ", "Akroma's Devoted"); + + attack(1, playerA, "Akroma's Devoted"); + setChoice(playerA, "Akroma's Devoted"); // two of the same replacement effect to choose from. + setChoice(playerA, true); // yes to "pay {2} for Akroma's Devoted to attack" + setChoice(playerA, true); // yes to "pay {2} for Akroma's Devoted to attack" + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 2); + assertTappedCount("Tundra", true, 2 * 2 + 2 * 2 + 4 + 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/LinkedEffectIdStaticAbility.java b/Mage/src/main/java/mage/abilities/common/LinkedEffectIdStaticAbility.java new file mode 100644 index 000000000000..81faea7d7d7a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/LinkedEffectIdStaticAbility.java @@ -0,0 +1,82 @@ +package mage.abilities.common; + +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.UUID; + +/** + * Warning: please test with a lot of care when using this class for new things. + *
+ * A static Ability linked to an Effect. + * The parent Ability does take responsability of setting the id for the child. + * + * @author Susucr + */ +public class LinkedEffectIdStaticAbility extends SimpleStaticAbility { + + public interface ChildEffect extends Effect { + /** + * Set the link for the child. + */ + void setParentLinkHandshake(UUID parentLinkHandshake); + + /** + * The child Id should only change on copy when the parent wants it to. + */ + void manualNewId(); + } + + + /** + * The handshake UUID between this parent ability and its child. + */ + private UUID linkedHandshake; + + public LinkedEffectIdStaticAbility(ChildEffect effect) { + this(Zone.BATTLEFIELD, effect); + } + + public LinkedEffectIdStaticAbility(Zone zone, ChildEffect effect) { + super(Zone.BATTLEFIELD, effect); + this.linkedHandshake = UUID.randomUUID(); + initHandshake(); + setEffectIdManually(); + } + + private LinkedEffectIdStaticAbility(final LinkedEffectIdStaticAbility effect) { + super(effect); + this.linkedHandshake = UUID.randomUUID(); + initHandshake(); + } + + @Override + public LinkedEffectIdStaticAbility copy() { + return new LinkedEffectIdStaticAbility(this); + } + + private void initHandshake() { + this.linkedHandshake = UUID.randomUUID(); + CardUtil.castStream(this.getEffects().stream(), ChildEffect.class) + .filter(Objects::nonNull) + .forEach(e -> e.setParentLinkHandshake(linkedHandshake)); + } + + public void setEffectIdManually() { + CardUtil.castStream(this.getEffects().stream(), ChildEffect.class) + .filter(Objects::nonNull) + .forEach(e -> e.manualNewId()); + } + + public boolean checkLinked(UUID handshake) { + return linkedHandshake.equals(handshake); + } + + @Override + public void newId() { + super.newId(); + } +} + diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackBlockUnlessPaysSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackBlockUnlessPaysSourceEffect.java index dde923561663..235acfa04846 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackBlockUnlessPaysSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackBlockUnlessPaysSourceEffect.java @@ -8,7 +8,6 @@ import mage.constants.Outcome; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; /** * @author LevelX2 @@ -26,7 +25,7 @@ public CantAttackBlockUnlessPaysSourceEffect(ManaCosts manaCosts, RestrictType r staticText = "{this} can't " + restrictType.toString() + " unless you pay " + (manaCosts == null ? "" : manaCosts.getText()); } - public CantAttackBlockUnlessPaysSourceEffect(CantAttackBlockUnlessPaysSourceEffect effect) { + protected CantAttackBlockUnlessPaysSourceEffect(final CantAttackBlockUnlessPaysSourceEffect effect) { super(effect); } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java index f5aabf3a5f8d..e4780c8e425a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java @@ -3,13 +3,12 @@ import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.Mode; +import mage.abilities.common.LinkedEffectIdStaticAbility; import mage.abilities.effects.ContinuousEffectImpl; import mage.cards.Card; import mage.constants.*; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.target.Target; -import mage.util.CardUtil; import java.util.*; @@ -18,7 +17,7 @@ */ public class GainAbilityTargetEffect extends ContinuousEffectImpl { - protected Ability ability; + protected final Ability ability; // shall a card gain the ability (otherwise a permanent) private final boolean useOnCard; // only one card per ability supported @@ -43,7 +42,8 @@ public GainAbilityTargetEffect(Ability ability, Duration duration, String rule) public GainAbilityTargetEffect(Ability ability, Duration duration, String rule, boolean useOnCard) { super(duration, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, ability.getEffects().getOutcome(ability, Outcome.AddAbility)); - this.ability = ability; + this.ability = copyAbility(ability); // See the method's comment, ability.copy() is not enough. + this.staticText = rule; this.useOnCard = useOnCard; @@ -52,8 +52,7 @@ public GainAbilityTargetEffect(Ability ability, Duration duration, String rule, protected GainAbilityTargetEffect(final GainAbilityTargetEffect effect) { super(effect); - this.ability = effect.ability.copy(); - this.ability.newId(); // This is needed if the effect is copied e.g. by a clone so the ability can be added multiple times to permanents + this.ability = copyAbility(effect.ability); // See the method's comment, ability.copy() is not enough. this.useOnCard = effect.useOnCard; this.waitingCardPermanent = effect.waitingCardPermanent; this.durationPhaseStep = effect.durationPhaseStep; @@ -202,6 +201,23 @@ public boolean apply(Game game, Ability source) { return affectedTargets > 0; } + /** + * Copying the ability and providing ability is needed in a few situations, + * The copy in order to have internal fields be proper to that ability in particular. + * Id must be different for the copy, for a few things like the GainAbilityTargetEffect gained + * by a clone, or in the case of an activated ability, called multiple times on the same target, + * and thus the ability should be gained multiple times. + */ + + private Ability copyAbility(Ability toCopyAbility) { + Ability ability = toCopyAbility.copy(); + ability.newId(); + if (ability instanceof LinkedEffectIdStaticAbility) { + ((LinkedEffectIdStaticAbility) ability).setEffectIdManually(); + } + return ability; + } + @Override public String getText(Mode mode) { if (staticText != null && !staticText.isEmpty()) {