diff --git a/Mage.Sets/src/mage/cards/d/DeeprootPilgrimage.java b/Mage.Sets/src/mage/cards/d/DeeprootPilgrimage.java new file mode 100644 index 00000000000..5cb0568bdc7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DeeprootPilgrimage.java @@ -0,0 +1,46 @@ +package mage.cards.d; + +import mage.abilities.common.BecomesTappedOneOrMoreTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.permanent.token.MerfolkHexproofToken; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class DeeprootPilgrimage extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledPermanent(SubType.MERFOLK, "nontoken Merfolk you control"); + + static { + filter.add(TokenPredicate.FALSE); + } + + public DeeprootPilgrimage(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}"); + + // Whenever one or more nontoken Merfolk you control become tapped, create a 1/1 blue Merfolk creature token with hexproof. + this.addAbility(new BecomesTappedOneOrMoreTriggeredAbility( + Zone.BATTLEFIELD, new CreateTokenEffect(new MerfolkHexproofToken()), false, filter + )); + } + + private DeeprootPilgrimage(final DeeprootPilgrimage card) { + super(card); + } + + @Override + public DeeprootPilgrimage copy() { + return new DeeprootPilgrimage(this); + } +} + diff --git a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java index 7cc97d102be..1359e90ee3a 100644 --- a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java +++ b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java @@ -27,6 +27,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet { cards.add(new SetCardInfo("Cavern of Souls", 269, Rarity.MYTHIC, mage.cards.c.CavernOfSouls.class)); cards.add(new SetCardInfo("Cenote Scout", 178, Rarity.UNCOMMON, mage.cards.c.CenoteScout.class)); cards.add(new SetCardInfo("Chart a Course", 48, Rarity.UNCOMMON, mage.cards.c.ChartACourse.class)); + cards.add(new SetCardInfo("Deeproot Pilgrimage", 52, Rarity.RARE, mage.cards.d.DeeprootPilgrimage.class)); cards.add(new SetCardInfo("Didact Echo", 53, Rarity.COMMON, mage.cards.d.DidactEcho.class)); cards.add(new SetCardInfo("Dinotomaton", 144, Rarity.COMMON, mage.cards.d.Dinotomaton.class)); cards.add(new SetCardInfo("Forest", 401, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/DeeprootPilgrimageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/DeeprootPilgrimageTest.java new file mode 100644 index 00000000000..a247a87c9bb --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/DeeprootPilgrimageTest.java @@ -0,0 +1,102 @@ +package org.mage.test.cards.single.lci; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class DeeprootPilgrimageTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.d.DeeprootPilgrimage}
+ * Deeproot Pilgrimage {1}{U}
+ * Enchantment
+ * Whenever one or more nontoken Merfolk you control become tapped, create a 1/1 blue Merfolk creature token with hexproof. + */ + private static final String pilgrimage = "Deeproot Pilgrimage"; + + // {2}{U}{U} Sorcery + // Tap all creatures target player controls. Those creatures don’t untap during that player’s next untap step. + private static final String sleep = "Sleep"; + + // 3/2 Vanilla merfolk + private static final String commando = "Coral Commando"; + + @Test + public void test_batch_tapped() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, pilgrimage); + addCard(Zone.BATTLEFIELD, playerA, commando, 4); + + addCard(Zone.HAND, playerA, sleep); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sleep", playerA); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertTappedCount(commando, true, 4); + assertPermanentCount(playerA, "Merfolk Token", 1); + } + + @Test + public void test_triggering_on_attack() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, pilgrimage); + addCard(Zone.BATTLEFIELD, playerA, commando); + + attack(1, playerA, commando, playerB); + + setStopAt(1, PhaseStep.DECLARE_BLOCKERS); + execute(); + + assertTappedCount(commando, true, 1); + assertPermanentCount(playerA, "Merfolk Token", 1); + } + + @Test + public void test_not_triggering_on_non_merfolk() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, pilgrimage); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); // 2/2, not a merfolk. + + addCard(Zone.HAND, playerA, sleep); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sleep", playerA); + // no trigger, bears is no Merfolk + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertTappedCount("Grizzly Bears", true, 1); + assertPermanentCount(playerA, "Merfolk Token", 0); + } + + @Test + public void test_not_triggering_on_opp_merfolks() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, pilgrimage); + addCard(Zone.BATTLEFIELD, playerB, commando); + + addCard(Zone.HAND, playerA, sleep); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sleep", playerB); + // no trigger, as Pilgrimage only triggers on Merfolk you control + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertTappedCount(commando, true, 1); + assertPermanentCount(playerA, "Merfolk Token", 0); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/BecomesTappedOneOrMoreTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BecomesTappedOneOrMoreTriggeredAbility.java new file mode 100644 index 00000000000..8ca7082e7c6 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/BecomesTappedOneOrMoreTriggeredAbility.java @@ -0,0 +1,48 @@ +package mage.abilities.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.TappedBatchEvent; + +/** + * @author Susucr + */ +public class BecomesTappedOneOrMoreTriggeredAbility extends TriggeredAbilityImpl { + + protected FilterPermanent filter; + + public BecomesTappedOneOrMoreTriggeredAbility(Zone zone, Effect effect, boolean optional, FilterPermanent filter) { + super(zone, effect, optional); + this.filter = filter; + setTriggerPhrase("Whenever one or more " + filter.getMessage() + " become tapped, "); + } + + protected BecomesTappedOneOrMoreTriggeredAbility(final BecomesTappedOneOrMoreTriggeredAbility ability) { + super(ability); + this.filter = ability.filter.copy(); + } + + @Override + public BecomesTappedOneOrMoreTriggeredAbility copy() { + return new BecomesTappedOneOrMoreTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.TAPPED_BATCH; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + TappedBatchEvent batchEvent = (TappedBatchEvent) event; + return batchEvent + .getTargets() + .stream() + .map(game::getPermanent) + .anyMatch(p -> filter.match(p, getControllerId(), this, game)); + } +} diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 895594904c0..cc0ba1f0fd2 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1,5 +1,6 @@ package mage.game; +import static java.util.Collections.emptyList; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.*; @@ -44,8 +45,6 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -import static java.util.Collections.emptyList; - /** * @author BetaSteward_at_googlemail.com *

@@ -860,6 +859,27 @@ public class GameState implements Serializable, Copyable { } } + public void addSimultaneousTapped(TappedEvent tappedEvent, Game game) { + // Combine multiple tapped events in the single event (batch) + + boolean isTappedBatchUsed = false; + for (GameEvent event : simultaneousEvents) { + if (event instanceof TappedBatchEvent) { + // Adding to the existing batch + ((TappedBatchEvent) event).addEvent(tappedEvent); + isTappedBatchUsed = true; + break; + } + } + + // new batch + if (!isTappedBatchUsed) { + TappedBatchEvent batch = new TappedBatchEvent(); + batch.addEvent(tappedEvent); + addSimultaneousEvent(batch, game); + } + } + public void handleEvent(GameEvent event, Game game) { watchers.watch(event, game); delayed.checkTriggers(event, game); diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 1d97b0f19ec..4fa62d7736b 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -350,12 +350,17 @@ public class GameEvent implements Serializable { flag is it tapped for combat */ TAPPED, - TAPPED_FOR_MANA, /* TAPPED_FOR_MANA During calculation of the available mana for a player the "TappedForMana" event is fired to simulate triggered mana production. By checking the inCheckPlayableState these events are handled to give back only the available mana of instead really producing mana. IMPORTANT: Triggered non mana abilities have to ignore the event if game.inCheckPlayableState is true. */ + TAPPED_FOR_MANA, + /* TAPPED_BATCH + combine all TAPPED events occuring at the same time in a single event + */ + TAPPED_BATCH, + UNTAP, UNTAPPED, FLIP, FLIPPED, TRANSFORMING, TRANSFORMED, diff --git a/Mage/src/main/java/mage/game/events/TappedBatchEvent.java b/Mage/src/main/java/mage/game/events/TappedBatchEvent.java new file mode 100644 index 00000000000..ff7a253284e --- /dev/null +++ b/Mage/src/main/java/mage/game/events/TappedBatchEvent.java @@ -0,0 +1,56 @@ +package mage.game.events; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author Susucr + */ +public class TappedBatchEvent extends GameEvent implements BatchGameEvent { + + private final Set events = new HashSet<>(); + + public TappedBatchEvent() { + super(EventType.TAPPED_BATCH, null, null, null); + } + + @Override + public Set getEvents() { + return events; + } + + @Override + public Set getTargets() { + return events.stream() + .map(GameEvent::getTargetId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + @Override + public int getAmount() { + return events + .stream() + .mapToInt(GameEvent::getAmount) + .sum(); + } + + @Override + @Deprecated // events can store a diff value, so search it from events list instead + public UUID getTargetId() { + throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list or use CardUtil.getEventTargets(event)"); + } + + @Override + @Deprecated // events can store a diff value, so search it from events list instead + public UUID getSourceId() { + throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list."); + } + + public void addEvent(TappedEvent event) { + this.events.add(event); + } +} diff --git a/Mage/src/main/java/mage/game/events/TappedEvent.java b/Mage/src/main/java/mage/game/events/TappedEvent.java new file mode 100644 index 00000000000..0d23d8fce08 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/TappedEvent.java @@ -0,0 +1,14 @@ +package mage.game.events; + +import mage.abilities.Ability; + +import java.util.UUID; + +/** + * @author Susucr + */ +public class TappedEvent extends GameEvent { + public TappedEvent(UUID targetId, Ability source, UUID playerId, boolean forCombat) { + super(EventType.TAPPED, targetId, source, playerId, 0, forCombat); + } +} diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 037aba7804e..97194267e41 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -561,7 +561,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { //20091005 - 701.15a if (!tapped && !replaceEvent(EventType.TAP, game)) { this.tapped = true; - game.fireEvent(new GameEvent(GameEvent.EventType.TAPPED, objectId, source, source == null ? null : source.getControllerId(), 0, forCombat)); + TappedEvent event = new TappedEvent(objectId, source, source == null ? null : source.getControllerId(), forCombat); + game.fireEvent(event); + game.getState().addSimultaneousTapped(event, game); return true; } return false;