mirror of
https://github.com/magefree/mage.git
synced 2026-01-26 21:29:17 -08:00
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
This commit is contained in:
parent
31f028d41e
commit
2cc9957753
11 changed files with 141 additions and 103 deletions
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ class KnightErrantOfEosEffect extends OneShotEffect {
|
|||
return input
|
||||
.getObject()
|
||||
.getManaValue()
|
||||
<= ConvokedSourceCount
|
||||
.PERMANENT
|
||||
<= ConvokedSourceCount.instance
|
||||
.calculate(game, input.getSource(), null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MageObjectReference> convokingCreatures = CardUtil.getSourceCostsTag(game, source,
|
||||
ConvokeAbility.convokingCreaturesKey, new HashSet<>(0));
|
||||
Set<AbstractMap.SimpleEntry<UUID, Permanent>> playerPermanentsPairs =
|
||||
ConvokeWatcher.getConvokingCreatures(new MageObjectReference(source),game)
|
||||
convokingCreatures
|
||||
.stream()
|
||||
.map(mor->mor.getPermanentOrLKIBattlefield(game))
|
||||
.filter(Objects::nonNull)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<MageObjectReference> 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)
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
PERMANENT(-1),
|
||||
SPELL(0);
|
||||
private final int offset;
|
||||
|
||||
ConvokedSourcePredicate(int offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
instance;
|
||||
@Override
|
||||
public boolean apply(ObjectSourcePlayer<Permanent> input, Game game) {
|
||||
return ConvokeWatcher.checkConvoke(
|
||||
new MageObjectReference(input.getSource(), offset), input.getObject(), game
|
||||
);
|
||||
HashSet<MageObjectReference> set = CardUtil.getSourceCostsTag(game, input.getSource(), ConvokeAbility.convokingCreaturesKey, new HashSet<>(0));
|
||||
return set.contains(new MageObjectReference(input.getObject(), game));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<MageObjectReference, Set<MageObjectReference>> 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<MageObjectReference> 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue