From f32d0b3e6ea4b92f47d6731e07c7aab076c00b6a Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 23 Feb 2022 21:37:42 -0500 Subject: [PATCH] fixed issues with Conspire, added additional tests --- .../src/mage/cards/w/WortTheRaidmother.java | 14 ++- .../abilities/keywords/ConspireTest.java | 91 +++++++++++++++++-- .../abilities/keyword/ConspireAbility.java | 48 +++++++--- 3 files changed, 129 insertions(+), 24 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WortTheRaidmother.java b/Mage.Sets/src/mage/cards/w/WortTheRaidmother.java index 5cdfdad760..89a6007974 100644 --- a/Mage.Sets/src/mage/cards/w/WortTheRaidmother.java +++ b/Mage.Sets/src/mage/cards/w/WortTheRaidmother.java @@ -35,10 +35,12 @@ public final class WortTheRaidmother extends CardImpl { this.toughness = new MageInt(3); // When Wort, the Raidmother enters the battlefield, create two 1/1 red and green Goblin Warrior creature tokens. - this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new GoblinWarriorToken(), 2), false)); + this.addAbility(new EntersBattlefieldTriggeredAbility( + new CreateTokenEffect(new GoblinWarriorToken(), 2) + )); // Each red or green instant or sorcery spell you cast has conspire. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new WortGainConspireEffect())); + this.addAbility(new SimpleStaticAbility(new WortGainConspireEffect())); } private WortTheRaidmother(final WortTheRaidmother card) { @@ -59,13 +61,17 @@ class WortGainConspireEffect extends ContinuousEffectImpl { filter.add(Predicates.or(new ColorPredicate(ObjectColor.RED), new ColorPredicate(ObjectColor.GREEN))); } + private final ConspireAbility conspireAbility; + public WortGainConspireEffect() { super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); staticText = "Each red or green instant or sorcery spell you cast has conspire. (As you cast the spell, you may tap two untapped creatures you control that share a color with it. When you do, copy it and you may choose new targets for the copy.)"; + this.conspireAbility = new ConspireAbility(ConspireAbility.ConspireTargets.MORE); } public WortGainConspireEffect(final WortGainConspireEffect effect) { super(effect); + this.conspireAbility = effect.conspireAbility; } @Override @@ -77,13 +83,13 @@ class WortGainConspireEffect extends ContinuousEffectImpl { public boolean apply(Game game, Ability source) { for (StackObject stackObject : game.getStack()) { // only spells cast, so no copies of spells - if ((!(stackObject instanceof Spell)) || stackObject.isCopy() + if (!(stackObject instanceof Spell) || stackObject.isCopy() || !stackObject.isControlledBy(source.getControllerId())) { continue; } Spell spell = (Spell) stackObject; if (filter.match(stackObject, game)) { - game.getState().addOtherAbility(spell.getCard(), new ConspireAbility(ConspireAbility.ConspireTargets.MORE)); + game.getState().addOtherAbility(spell.getCard(), conspireAbility.setAddedById(source.getSourceId())); } } return true; diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConspireTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConspireTest.java index e9d95cdb6c..617b7a1864 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConspireTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConspireTest.java @@ -48,7 +48,7 @@ public class ConspireTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - assertLife(playerB, 14); + assertLife(playerB, 20 - 3 - 3); assertGraveyardCount(playerA, "Burn Trail", 1); assertTapped("Goblin Roughrider", true); assertTapped("Raging Goblin", true); @@ -68,7 +68,7 @@ public class ConspireTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); - assertLife(playerB, 17); + assertLife(playerB, 20 - 3); assertGraveyardCount(playerA, "Burn Trail", 1); assertTapped("Goblin Roughrider", false); assertTapped("Raging Goblin", false); @@ -101,12 +101,35 @@ public class ConspireTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Wort, the Raidmother", 1); assertGraveyardCount(playerA, "Lightning Bolt", 1); - assertLife(playerB, 14); + assertLife(playerB, 20 - 3 - 3); } @Test - public void testWortTheRaidmotherWithConspireSpell() { + public void testWortTheRaidmotherWithConspireSpellOnce() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + // When Wort, the Raidmother enters the battlefield, put two 1/1 red and green Goblin Warrior creature tokens onto the battlefield. + // Each red or green instant or sorcery spell you cast has conspire. + // (As you cast the spell, you may tap two untapped creatures you control that share a color with it. When you do, copy it and you may choose new targets for the copy.) + addCard(Zone.HAND, playerA, "Wort, the Raidmother"); + addCard(Zone.HAND, playerA, "Burn Trail"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wort, the Raidmother"); // {4}{R/G}{R/G} + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Burn Trail", playerB); + setChoice(playerA, true); // use Conspire from Burn Trail itself + setChoice(playerA, false); // don't use Conspire gained from Wort, the Raidmother + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, "Wort, the Raidmother", 1); + assertLife(playerB, 20 - 3 - 3); + assertLife(playerA, 20); + assertGraveyardCount(playerA, "Burn Trail", 1); + } + + @Test + public void testWortTheRaidmotherWithConspireSpellTwice() { addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); addCard(Zone.BATTLEFIELD, playerA, "Raging Goblin", 2); // When Wort, the Raidmother enters the battlefield, put two 1/1 red and green Goblin Warrior creature tokens onto the battlefield. @@ -124,12 +147,66 @@ public class ConspireTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); assertPermanentCount(playerA, "Wort, the Raidmother", 1); - assertLife(playerB, 11); + assertLife(playerB, 20 - 3 - 3 - 3); assertLife(playerA, 20); assertGraveyardCount(playerA, "Burn Trail", 1); } + @Test + public void testWortTheRaidmotherWithSakashimaOnce() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + // When Wort, the Raidmother enters the battlefield, put two 1/1 red and green Goblin Warrior creature tokens onto the battlefield. + // Each red or green instant or sorcery spell you cast has conspire. + // (As you cast the spell, you may tap two untapped creatures you control that share a color with it. When you do, copy it and you may choose new targets for the copy.) + addCard(Zone.HAND, playerA, "Wort, the Raidmother"); + addCard(Zone.HAND, playerA, "Sakashima the Impostor"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wort, the Raidmother"); // {4}{R/G}{R/G} + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sakashima the Impostor"); // {2}{U}{U} + setChoice(playerA, "Wort, the Raidmother"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + setChoice(playerA, true); // use Conspire gained from Wort, the Raidmother + setChoice(playerA, false); // don't use Conspire gained from Sakashima the Imposter + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, "Wort, the Raidmother", 1); + assertPermanentCount(playerA, "Sakashima the Impostor", 1); + assertLife(playerB, 20 - 3 - 3); + assertLife(playerA, 20); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + } + + @Test + public void testWortTheRaidmotherWithSakashimaTwice() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 11); + // When Wort, the Raidmother enters the battlefield, put two 1/1 red and green Goblin Warrior creature tokens onto the battlefield. + // Each red or green instant or sorcery spell you cast has conspire. + // (As you cast the spell, you may tap two untapped creatures you control that share a color with it. When you do, copy it and you may choose new targets for the copy.) + addCard(Zone.HAND, playerA, "Wort, the Raidmother"); + addCard(Zone.HAND, playerA, "Sakashima the Impostor"); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wort, the Raidmother"); // {4}{R/G}{R/G} + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sakashima the Impostor"); // {2}{U}{U} + setChoice(playerA, "Wort, the Raidmother"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + setChoice(playerA, true); // use Conspire gained from Wort, the Raidmother + setChoice(playerA, true); // use Conspire gained from Sakashima the Imposter + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, "Wort, the Raidmother", 1); + assertPermanentCount(playerA, "Sakashima the Impostor", 1); + assertLife(playerB, 20 - 3 - 3 - 3); + assertLife(playerA, 20); + assertGraveyardCount(playerA, "Lightning Bolt", 1); + } + @Test public void testConspire_User() { // Burn Trail deals 3 damage to any target. @@ -151,7 +228,7 @@ public class ConspireTest extends CardTestPlayerBase { assertAllCommandsUsed(); assertGraveyardCount(playerA, "Burn Trail", 1); - assertLife(playerB, 20 - 3 * 2); + assertLife(playerB, 20 - 3 - 3); assertTapped("Goblin Assailant", true); } @@ -176,7 +253,7 @@ public class ConspireTest extends CardTestPlayerBase { assertAllCommandsUsed(); assertGraveyardCount(playerA, "Burn Trail", 1); - assertLife(playerB, 20 - 3 * 2); + assertLife(playerB, 20 - 3 - 3); assertTapped("Goblin Assailant", true); } diff --git a/Mage/src/main/java/mage/abilities/keyword/ConspireAbility.java b/Mage/src/main/java/mage/abilities/keyword/ConspireAbility.java index 32de82ddf5..7532725b29 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ConspireAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ConspireAbility.java @@ -16,15 +16,14 @@ import mage.filter.predicate.mageobject.SharesColorWithSourcePredicate; import mage.filter.predicate.permanent.TappedPredicate; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.players.Player; import mage.target.common.TargetControlledPermanent; +import mage.util.CardUtil; -import java.util.Collection; import java.util.Iterator; -import java.util.List; import java.util.Objects; +import java.util.UUID; /* * 702.77. Conspire @@ -44,7 +43,6 @@ import java.util.Objects; public class ConspireAbility extends StaticAbility implements OptionalAdditionalSourceCosts { private static final String keywordText = "Conspire"; - protected static final String CONSPIRE_ACTIVATION_KEY = "ConspireActivation"; private static final FilterControlledPermanent filter = new FilterControlledPermanent("untapped creatures you control that share a color with it"); @@ -70,6 +68,8 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional } } + private final UUID conspireId; + private UUID addedById = null; private final String reminderText; private final OptionalAdditionalCost conspireCost; @@ -81,17 +81,20 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional */ public ConspireAbility(ConspireTargets conspireTargets) { super(Zone.STACK, null); + this.conspireId = UUID.randomUUID(); reminderText = conspireTargets.getReminder(); this.conspireCost = new OptionalAdditionalCostImpl( keywordText, " ", reminderText, new TapTargetCost(new TargetControlledPermanent(2, filter)) ); this.conspireCost.setCostType(VariableCostType.ADDITIONAL); - addSubAbility(new ConspireTriggeredAbility()); + this.addSubAbility(new ConspireTriggeredAbility(conspireId)); } public ConspireAbility(final ConspireAbility ability) { super(ability); + this.conspireId = ability.conspireId; + this.addedById = ability.addedById; this.conspireCost = ability.conspireCost.copy(); this.reminderText = ability.reminderText; } @@ -124,9 +127,11 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional } // AI supports conspire if (!conspireCost.canPay(ability, this, getControllerId(), game) - || !player.chooseUse(Outcome.Benefit, "Pay " + conspireCost.getText(false) + " ?", ability, game)) { + || !player.chooseUse(Outcome.Benefit, "Pay " + + conspireCost.getText(false) + " ?", ability, game)) { return; } + ability.getEffects().setValue("ConspireActivation" + conspireId + addedById, true); for (Iterator it = ((Costs) conspireCost).iterator(); it.hasNext(); ) { Cost cost = (Cost) it.next(); if (cost instanceof ManaCostsImpl) { @@ -151,17 +156,32 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional public String getCastMessageSuffix() { return conspireCost != null ? conspireCost.getCastSuffixMessage(0) : ""; } + + public ConspireAbility setAddedById(UUID addedById) { + this.addedById = addedById; + CardUtil.castStream( + this.subAbilities.stream(), + ConspireTriggeredAbility.class + ).forEach(ability -> ability.setAddedById(addedById)); + return this; + } } class ConspireTriggeredAbility extends CastSourceTriggeredAbility { - public ConspireTriggeredAbility() { + private final UUID conspireId; + private UUID addedById = null; + + public ConspireTriggeredAbility(UUID conspireId) { super(new CopySourceSpellEffect(), false); this.setRuleVisible(false); + this.conspireId = conspireId; } private ConspireTriggeredAbility(final ConspireTriggeredAbility ability) { super(ability); + this.conspireId = ability.conspireId; + this.addedById = ability.addedById; } @Override @@ -175,19 +195,21 @@ class ConspireTriggeredAbility extends CastSourceTriggeredAbility { return false; } Spell spell = game.getStack().getSpell(event.getSourceId()); - return spell != null && spell + return spell != null + && spell .getSpellAbility() .getEffects() .stream() - .map(effect -> effect.getValue("tappedPermanents")) - .filter(Objects::nonNull) - .map(x -> (List) x) - .flatMap(Collection::stream) - .count() >= 2; + .map(effect -> effect.getValue("ConspireActivation" + conspireId + addedById)) + .anyMatch(Objects::nonNull); } @Override public String getRule() { return "When you pay the conspire costs, copy it and you may choose a new target for the copy."; } + + public void setAddedById(UUID addedById) { + this.addedById = addedById; + } }