From 2cc9957753a601fb80ea944c06a5f32476721206 Mon Sep 17 00:00:00 2001 From: ssk97 Date: Wed, 22 Nov 2023 13:31:56 -0800 Subject: [PATCH] Costs Tag Tracking part 4: Convoke (#11446) * Switch Convoke to using costs tag system * Add Convoke copy/clone tests * update author name on sufficiently changed files * Remove now-unused CONVOKED event --- .../src/mage/cards/a/AncientImperiosaur.java | 2 +- .../src/mage/cards/k/KnightErrantOfEos.java | 3 +- Mage.Sets/src/mage/cards/l/LethalScheme.java | 9 +- .../src/mage/cards/v/VeneratedLoxodon.java | 2 +- Mage.Sets/src/mage/cards/z/ZephyrSinger.java | 2 +- .../cards/abilities/keywords/ConvokeTest.java | 110 ++++++++++++++++++ .../common/ConvokedSourceCount.java | 17 ++- .../abilities/keyword/ConvokeAbility.java | 14 +-- .../permanent/ConvokedSourcePredicate.java | 21 ++-- .../main/java/mage/game/events/GameEvent.java | 6 - .../mage/watchers/common/ConvokeWatcher.java | 58 --------- 11 files changed, 141 insertions(+), 103 deletions(-) delete mode 100644 Mage/src/main/java/mage/watchers/common/ConvokeWatcher.java diff --git a/Mage.Sets/src/mage/cards/a/AncientImperiosaur.java b/Mage.Sets/src/mage/cards/a/AncientImperiosaur.java index dcc8b86ab82..9c7d8c6e3b1 100644 --- a/Mage.Sets/src/mage/cards/a/AncientImperiosaur.java +++ b/Mage.Sets/src/mage/cards/a/AncientImperiosaur.java @@ -23,7 +23,7 @@ import java.util.UUID; */ public final class AncientImperiosaur extends CardImpl { - private static final DynamicValue xValue = new MultipliedValue(ConvokedSourceCount.SPELL, 2); + private static final DynamicValue xValue = new MultipliedValue(ConvokedSourceCount.instance, 2); public AncientImperiosaur(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{G}{G}"); diff --git a/Mage.Sets/src/mage/cards/k/KnightErrantOfEos.java b/Mage.Sets/src/mage/cards/k/KnightErrantOfEos.java index 58a7cde1e09..167eb4f2f1b 100644 --- a/Mage.Sets/src/mage/cards/k/KnightErrantOfEos.java +++ b/Mage.Sets/src/mage/cards/k/KnightErrantOfEos.java @@ -62,8 +62,7 @@ class KnightErrantOfEosEffect extends OneShotEffect { return input .getObject() .getManaValue() - <= ConvokedSourceCount - .PERMANENT + <= ConvokedSourceCount.instance .calculate(game, input.getSource(), null); } } diff --git a/Mage.Sets/src/mage/cards/l/LethalScheme.java b/Mage.Sets/src/mage/cards/l/LethalScheme.java index 414bcc64265..d466754cd23 100644 --- a/Mage.Sets/src/mage/cards/l/LethalScheme.java +++ b/Mage.Sets/src/mage/cards/l/LethalScheme.java @@ -12,14 +12,11 @@ import mage.choices.Choice; import mage.choices.ChoiceImpl; import mage.constants.CardType; import mage.constants.Outcome; -import mage.filter.FilterPermanent; -import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.permanent.ConvokedSourcePredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCreatureOrPlaneswalker; -import mage.watchers.common.ConvokeWatcher; +import mage.util.CardUtil; import java.util.*; import java.util.stream.Collectors; @@ -71,8 +68,10 @@ class LethalSchemeEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { + HashSet convokingCreatures = CardUtil.getSourceCostsTag(game, source, + ConvokeAbility.convokingCreaturesKey, new HashSet<>(0)); Set> playerPermanentsPairs = - ConvokeWatcher.getConvokingCreatures(new MageObjectReference(source),game) + convokingCreatures .stream() .map(mor->mor.getPermanentOrLKIBattlefield(game)) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/v/VeneratedLoxodon.java b/Mage.Sets/src/mage/cards/v/VeneratedLoxodon.java index 6219c1cae05..4d625a5be6a 100644 --- a/Mage.Sets/src/mage/cards/v/VeneratedLoxodon.java +++ b/Mage.Sets/src/mage/cards/v/VeneratedLoxodon.java @@ -23,7 +23,7 @@ public final class VeneratedLoxodon extends CardImpl { private static final FilterPermanent filter = new FilterCreaturePermanent("creature that convoked it"); static { - filter.add(ConvokedSourcePredicate.PERMANENT); + filter.add(ConvokedSourcePredicate.instance); } public VeneratedLoxodon(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Sets/src/mage/cards/z/ZephyrSinger.java b/Mage.Sets/src/mage/cards/z/ZephyrSinger.java index 233f472ed07..b7ae1d9b89e 100644 --- a/Mage.Sets/src/mage/cards/z/ZephyrSinger.java +++ b/Mage.Sets/src/mage/cards/z/ZephyrSinger.java @@ -25,7 +25,7 @@ public final class ZephyrSinger extends CardImpl { private static final FilterPermanent filter = new FilterCreaturePermanent("creature that convoked it"); static { - filter.add(ConvokedSourcePredicate.PERMANENT); + filter.add(ConvokedSourcePredicate.instance); } public ZephyrSinger(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConvokeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConvokeTest.java index 655a410756d..6bc971bba3a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConvokeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ConvokeTest.java @@ -349,4 +349,114 @@ public class ConvokeTest extends CardTestPlayerBaseWithAIHelps { assertPermanentCount(playerA, "Soldier Token", 1); } + @Test + public void test_AncientImperiosaur_Convoke() { + // Ancient Imperiosaur {5}{G}{G} + // Convoke, Trample, ward {2} + // Ancient Imperiosaur enters the battlefield with two +1/+1 counters on it for each creature that convoked it. + // 6/6 + addCard(Zone.HAND, playerA, "Ancient Imperiosaur", 1); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 6); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ancient Imperiosaur"); + addTarget(playerA, "Grizzly Bears", 6); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertPowerToughness(playerA,"Ancient Imperiosaur",18,18); + } + @Test + public void test_Copy_Counters_Convoke() { + // Venerated Loxodon {4}{W} + // Convoke (Your creatures can help cast this spell. Each creature you tap while casting this spell pays for {1} or one mana of that creature’s color.) + // When Venerated Loxodon enters the battlefield, put a +1/+1 counter on each creature that convoked it. + + //Copying a spell on the stack copies the costs paid, so both the original and the copy should add counters + addCard(Zone.HAND, playerA, "Venerated Loxodon", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.BATTLEFIELD, playerA, "Memnite", 2); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 2); + + addCard(Zone.HAND, playerA, "Double Major", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + + addCard(Zone.HAND, playerB, "Hideous Laughter", 1); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); + + // use special action to pay (need disabled auto-payment and prepared mana pool) + disableManaAutoPayment(playerA); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Venerated Loxodon"); + setChoice(playerA, "White", 3); // pay WWW + setChoice(playerA, "Convoke"); + addTarget(playerA, "Memnite"); // pay 4 as convoke + setChoice(playerA, "Convoke"); + addTarget(playerA, "Grizzly Bears"); // pay 5 as convoke + + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Venerated Loxodon"); + setChoice(playerA, "Blue", 1); + setChoice(playerA, "Green", 1); + + waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Hideous Laughter"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertGraveyardCount(playerA,"Memnite",1); + assertGraveyardCount(playerA,"Grizzly Bears",1); + assertPowerToughness(playerA,"Memnite", 1, 1); + assertPowerToughness(playerA,"Grizzly Bears", 2, 2); + assertPermanentCount(playerA,"Venerated Loxodon",2); + } + @Test + public void test_Clone_Counters_Convoke() { + // Venerated Loxodon {4}{W} + // Convoke (Your creatures can help cast this spell. Each creature you tap while casting this spell pays for {1} or one mana of that creature’s color.) + // When Venerated Loxodon enters the battlefield, put a +1/+1 counter on each creature that convoked it. + + // Cloning a creature on the battlefield should not add counters + addCard(Zone.HAND, playerA, "Venerated Loxodon", 1); + addCard(Zone.BATTLEFIELD, playerA, "Memnite", 2); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 2); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + + addCard(Zone.HAND, playerA, "Clone", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 4); + addCard(Zone.HAND, playerB, "Hideous Laughter", 1); + + // use special action to pay (need disabled auto-payment and prepared mana pool) + disableManaAutoPayment(playerA); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Venerated Loxodon"); + setChoice(playerA, "White", 3); //Pay WWW + setChoice(playerA, "Convoke"); + addTarget(playerA, "Memnite"); // pay 4 as convoke + setChoice(playerA, "Convoke"); + addTarget(playerA, "Grizzly Bears"); // pay 5 as convoke + + waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 4); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Clone"); + setChoice(playerA, "Blue", 4); + setChoice(playerA,true); + setChoice(playerA,"Venerated Loxodon"); + + waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Hideous Laughter"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertGraveyardCount(playerA,"Memnite",2); + assertGraveyardCount(playerA,"Grizzly Bears",1); + assertPowerToughness(playerA,"Grizzly Bears", 1, 1); + assertPermanentCount(playerA,"Venerated Loxodon",2); + } } \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ConvokedSourceCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ConvokedSourceCount.java index e2be96efede..4af0a7d1837 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ConvokedSourceCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ConvokedSourceCount.java @@ -1,27 +1,26 @@ package mage.abilities.dynamicvalue.common; -import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; +import mage.abilities.keyword.ConvokeAbility; import mage.game.Game; -import mage.watchers.common.ConvokeWatcher; +import mage.util.CardUtil; + +import java.util.HashSet; /** - * @author TheElk801 + * @author notgreat */ public enum ConvokedSourceCount implements DynamicValue { - PERMANENT(-1), - SPELL(0); - private final int offset; + instance; - ConvokedSourceCount(int offset) { - this.offset = offset; + ConvokedSourceCount() { } @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - return ConvokeWatcher.getConvokingCreatures(new MageObjectReference(game.getObject(sourceAbility), game, offset), game).size(); + return CardUtil.getSourceCostsTag(game, sourceAbility, ConvokeAbility.convokingCreaturesKey, new HashSet<>(0)).size(); } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/ConvokeAbility.java b/Mage/src/main/java/mage/abilities/keyword/ConvokeAbility.java index 2470abed5ad..5f85ae74af4 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ConvokeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ConvokeAbility.java @@ -1,5 +1,6 @@ package mage.abilities.keyword; +import mage.MageObjectReference; import mage.Mana; import mage.ObjectColor; import mage.abilities.Ability; @@ -30,12 +31,9 @@ import mage.players.ManaPool; import mage.players.Player; import mage.target.Target; import mage.target.common.TargetControlledCreaturePermanent; -import mage.watchers.common.ConvokeWatcher; +import mage.util.CardUtil; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.UUID; +import java.util.*; /** * 502.46. Convoke @@ -71,6 +69,7 @@ import java.util.UUID; */ public class ConvokeAbility extends SimpleStaticAbility implements AlternateManaPaymentAbility { + public static String convokingCreaturesKey = "convokingCreatures"; private static final FilterControlledCreaturePermanent filterUntapped = new FilterControlledCreaturePermanent(); static { @@ -80,7 +79,6 @@ public class ConvokeAbility extends SimpleStaticAbility implements AlternateMana public ConvokeAbility() { super(Zone.ALL, null); // all AlternateManaPaymentAbility must use ALL zone to calculate playable abilities this.setRuleAtTheTop(true); - this.addWatcher(new ConvokeWatcher()); this.addHint(new ValueHint("Untapped creatures you control", new PermanentsOnBattlefieldCount(filterUntapped))); } @@ -265,7 +263,9 @@ class ConvokeEffect extends OneShotEffect { manaPool.unlockManaType(ManaType.COLORLESS); manaName = "colorless"; } - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.CONVOKED, perm.getId(), source, source.getControllerId())); + HashSet set = CardUtil.getSourceCostsTag(game, spell.getSpellAbility(), ConvokeAbility.convokingCreaturesKey, new HashSet<>()); + set.add(new MageObjectReference(perm, game)); + spell.getSpellAbility().setCostsTag(ConvokeAbility.convokingCreaturesKey, set); game.informPlayers("Convoke: " + controller.getLogName() + " taps " + perm.getLogName() + " to pay one " + manaName + " mana"); // can't use mana abilities after that (convoke cost must be payed after mana abilities only) diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/ConvokedSourcePredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/ConvokedSourcePredicate.java index 39fa7f6cc5e..cbb2b853a42 100644 --- a/Mage/src/main/java/mage/filter/predicate/permanent/ConvokedSourcePredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/permanent/ConvokedSourcePredicate.java @@ -1,28 +1,23 @@ package mage.filter.predicate.permanent; import mage.MageObjectReference; +import mage.abilities.keyword.ConvokeAbility; import mage.filter.predicate.ObjectSourcePlayer; import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.watchers.common.ConvokeWatcher; +import mage.util.CardUtil; + +import java.util.HashSet; /** - * @author TheElk801 + * @author notgreat */ public enum ConvokedSourcePredicate implements ObjectSourcePlayerPredicate { - PERMANENT(-1), - SPELL(0); - private final int offset; - - ConvokedSourcePredicate(int offset) { - this.offset = offset; - } - + instance; @Override public boolean apply(ObjectSourcePlayer input, Game game) { - return ConvokeWatcher.checkConvoke( - new MageObjectReference(input.getSource(), offset), input.getObject(), game - ); + HashSet set = CardUtil.getSourceCostsTag(game, input.getSource(), ConvokeAbility.convokingCreaturesKey, new HashSet<>(0)); + return set.contains(new MageObjectReference(input.getObject(), game)); } } diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index bc1be40fd93..ec2889e7d4a 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -86,12 +86,6 @@ public class GameEvent implements Serializable { MADNESS_CARD_EXILED, INVESTIGATED, // playerId is the player who investigated KICKED, - /* CONVOKED - targetId id of the creature that was taped to convoke the sourceId - sourceId sourceId of the convoked spell - playerId controller of the convoked spell - */ - CONVOKED, /* DISCARD_CARD flag event is result of effect (1) or result of cost (0) */ diff --git a/Mage/src/main/java/mage/watchers/common/ConvokeWatcher.java b/Mage/src/main/java/mage/watchers/common/ConvokeWatcher.java deleted file mode 100644 index 50a6d563921..00000000000 --- a/Mage/src/main/java/mage/watchers/common/ConvokeWatcher.java +++ /dev/null @@ -1,58 +0,0 @@ -package mage.watchers.common; - -import mage.MageObjectReference; -import mage.constants.WatcherScope; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.game.stack.Spell; -import mage.watchers.Watcher; - -import java.util.*; - -/** - * @author LevelX2 - */ -public class ConvokeWatcher extends Watcher { - - private final Map> convokingCreatures = new HashMap<>(); - - public ConvokeWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (event.getType() != GameEvent.EventType.CONVOKED) { - return; - } - Spell spell = game.getSpell(event.getSourceId()); - Permanent tappedCreature = game.getPermanentOrLKIBattlefield(event.getTargetId()); - if (spell == null || tappedCreature == null) { - return; - } - convokingCreatures - .computeIfAbsent(new MageObjectReference(spell.getSourceId(), game), x -> new HashSet<>()) - .add(new MageObjectReference(tappedCreature, game)); - } - - public static Set getConvokingCreatures(MageObjectReference mor, Game game) { - return game - .getState() - .getWatcher(ConvokeWatcher.class) - .convokingCreatures - .getOrDefault(mor, Collections.emptySet()); - } - - public static boolean checkConvoke(MageObjectReference mor, Permanent permanent, Game game) { - return getConvokingCreatures(mor, game) - .stream() - .anyMatch(m -> m.refersTo(permanent, game)); - } - - @Override - public void reset() { - super.reset(); - convokingCreatures.clear(); - } -} \ No newline at end of file