diff --git a/Mage.Sets/src/mage/cards/c/CathedralMembrane.java b/Mage.Sets/src/mage/cards/c/CathedralMembrane.java index 923776d1012..9514da271f7 100644 --- a/Mage.Sets/src/mage/cards/c/CathedralMembrane.java +++ b/Mage.Sets/src/mage/cards/c/CathedralMembrane.java @@ -1,21 +1,22 @@ - package mage.cards.c; import mage.MageInt; +import mage.MageObjectReference; import mage.abilities.Ability; -import mage.abilities.common.ZoneChangeTriggeredAbility; +import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.DefenderAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.TurnPhase; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; -import mage.watchers.Watcher; +import mage.watchers.common.BlockedAttackerWatcher; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; /** @@ -35,8 +36,7 @@ public final class CathedralMembrane extends CardImpl { this.addAbility(DefenderAbility.getInstance()); // When Cathedral Membrane dies during combat, it deals 6 damage to each creature it blocked this combat. - this.addAbility(new CathedralMembraneAbility(), new CathedralMembraneWatcher()); - + this.addAbility(new CathedralMembraneAbility()); } private CathedralMembrane(final CathedralMembrane card) { @@ -49,10 +49,11 @@ public final class CathedralMembrane extends CardImpl { } } -class CathedralMembraneAbility extends ZoneChangeTriggeredAbility { +class CathedralMembraneAbility extends DiesSourceTriggeredAbility { CathedralMembraneAbility() { - super(Zone.BATTLEFIELD, Zone.GRAVEYARD, new CathedralMembraneEffect(), "When {this} dies during combat, ", false); + super(new CathedralMembraneEffect()); + setTriggerPhrase("When {this} dies during combat, "); } private CathedralMembraneAbility(CathedralMembraneAbility ability) { @@ -66,12 +67,7 @@ class CathedralMembraneAbility extends ZoneChangeTriggeredAbility { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (super.checkTrigger(event, game)) { - if (game.getTurnPhaseType() == TurnPhase.COMBAT) { - return true; - } - } - return false; + return game.getTurnPhaseType() == TurnPhase.COMBAT && super.checkTrigger(event, game); } } @@ -94,41 +90,13 @@ class CathedralMembraneEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - CathedralMembraneWatcher watcher = game.getState().getWatcher(CathedralMembraneWatcher.class, source.getSourceId()); - if (watcher != null) { - for (UUID uuid : watcher.getBlockedCreatures()) { - Permanent permanent = game.getPermanent(uuid); - if (permanent != null) { - permanent.damage(6, source.getSourceId(), source, game, false, true); - } + Permanent permanent = source.getSourcePermanentOrLKI(game); + BlockedAttackerWatcher watcher = game.getState().getWatcher(BlockedAttackerWatcher.class); + if (watcher != null && permanent != null) { + for (Permanent p : watcher.getBlockedCreatures(new MageObjectReference(permanent, game), game)) { + p.damage(6, source.getSourceId(), source, game, false, true); } } return true; } } - -class CathedralMembraneWatcher extends Watcher { - - private final Set blockedCreatures = new HashSet<>(); - - CathedralMembraneWatcher() { - super(WatcherScope.CARD); - } - - @Override - public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.BLOCKER_DECLARED && event.getSourceId().equals(sourceId)) { - blockedCreatures.add(event.getTargetId()); - } - } - - @Override - public void reset() { - super.reset(); - blockedCreatures.clear(); - } - - Set getBlockedCreatures() { - return blockedCreatures; - } -} diff --git a/Mage.Sets/src/mage/cards/g/GazeOfTheGorgon.java b/Mage.Sets/src/mage/cards/g/GazeOfTheGorgon.java index 176fa182f0c..b0c2e7afb80 100644 --- a/Mage.Sets/src/mage/cards/g/GazeOfTheGorgon.java +++ b/Mage.Sets/src/mage/cards/g/GazeOfTheGorgon.java @@ -102,7 +102,7 @@ class GazeOfTheGorgonEffect extends OneShotEffect { List toDestroy = new ArrayList<>(); for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), source, game)) { if (!creature.getId().equals(targetCreature.getSourceId())) { - if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), targetCreature, game) || watcher.creatureHasBlockedAttacker(targetCreature, new MageObjectReference(creature, game), game)) { + if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), targetCreature) || watcher.creatureHasBlockedAttacker(targetCreature, new MageObjectReference(creature, game))) { toDestroy.add(creature); } } diff --git a/Mage.Sets/src/mage/cards/g/GlyphOfDelusion.java b/Mage.Sets/src/mage/cards/g/GlyphOfDelusion.java index 4d27ac13a29..4b6b8c25125 100644 --- a/Mage.Sets/src/mage/cards/g/GlyphOfDelusion.java +++ b/Mage.Sets/src/mage/cards/g/GlyphOfDelusion.java @@ -20,7 +20,6 @@ import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; -import mage.target.common.TargetCreaturePermanent; import mage.target.targetpointer.FixedTarget; import mage.watchers.common.BlockedAttackerWatcher; @@ -82,7 +81,7 @@ class GlyphOfDelusionSecondTarget extends TargetPermanent { if (targetSource != null) { for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, sourceControllerId, source, game)) { if (!targets.containsKey(creature.getId()) && creature.canBeTargetedBy(targetSource, sourceControllerId, source, game)) { - if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), new MageObjectReference(firstTarget, game), game)) { + if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), new MageObjectReference(firstTarget, game))) { possibleTargets.add(creature.getId()); } } diff --git a/Mage.Sets/src/mage/cards/g/GlyphOfDoom.java b/Mage.Sets/src/mage/cards/g/GlyphOfDoom.java index 31d3ba22b20..101163093f7 100644 --- a/Mage.Sets/src/mage/cards/g/GlyphOfDoom.java +++ b/Mage.Sets/src/mage/cards/g/GlyphOfDoom.java @@ -20,7 +20,6 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.TargetPermanent; -import mage.target.common.TargetCreaturePermanent; import mage.watchers.common.BlockedAttackerWatcher; /** @@ -110,7 +109,7 @@ class GlyphOfDoomEffect extends OneShotEffect { List toDestroy = new ArrayList<>(); for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), source, game)) { if (!creature.getId().equals(targetCreature.getSourceId())) { - if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), targetCreature, game)) { + if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), targetCreature)) { toDestroy.add(creature); } } diff --git a/Mage.Sets/src/mage/cards/g/GlyphOfReincarnation.java b/Mage.Sets/src/mage/cards/g/GlyphOfReincarnation.java index bb85c7ae878..640b7749364 100644 --- a/Mage.Sets/src/mage/cards/g/GlyphOfReincarnation.java +++ b/Mage.Sets/src/mage/cards/g/GlyphOfReincarnation.java @@ -22,7 +22,6 @@ import mage.players.Player; import mage.target.Target; import mage.target.TargetPermanent; import mage.target.common.TargetCardInGraveyard; -import mage.target.common.TargetCreaturePermanent; import mage.watchers.common.BlockedAttackerWatcher; import java.util.HashMap; @@ -87,7 +86,7 @@ class GlyphOfReincarnationEffect extends OneShotEffect { Map destroyed = new HashMap<>(); for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), source, game)) { if (!creature.getId().equals(targetWall.getId())) { - if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), new MageObjectReference(targetWall, game), game)) { + if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), new MageObjectReference(targetWall, game))) { if (creature.destroy(source, game, true) && game.getState().getZone(creature.getId()) == Zone.GRAVEYARD) { // If a commander is replaced to command zone, the creature does not die Player permController = game.getPlayer(creature.getControllerId()); diff --git a/Mage.Sets/src/mage/cards/v/VenomousBreath.java b/Mage.Sets/src/mage/cards/v/VenomousBreath.java index 6da5caa866d..49a7c85d0a9 100644 --- a/Mage.Sets/src/mage/cards/v/VenomousBreath.java +++ b/Mage.Sets/src/mage/cards/v/VenomousBreath.java @@ -101,7 +101,7 @@ class VenomousBreathEffect extends OneShotEffect { List toDestroy = new ArrayList<>(); for (Permanent creature : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), source, game)) { if (!creature.getId().equals(targetCreature.getSourceId())) { - if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), targetCreature, game) || watcher.creatureHasBlockedAttacker(targetCreature, new MageObjectReference(creature, game), game)) { + if (watcher.creatureHasBlockedAttacker(new MageObjectReference(creature, game), targetCreature) || watcher.creatureHasBlockedAttacker(targetCreature, new MageObjectReference(creature, game))) { toDestroy.add(creature); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/watchers/CathedralMembraneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/watchers/CathedralMembraneTest.java new file mode 100644 index 00000000000..c1b47a2556d --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/watchers/CathedralMembraneTest.java @@ -0,0 +1,109 @@ +package org.mage.test.cards.watchers; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class CathedralMembraneTest extends CardTestPlayerBase { + + // see issue #13774 + + private static final String cathedralMembrane = "Cathedral Membrane"; // 0/6 + // Defender + // When this creature dies during combat, it deals 6 damage to each creature it blocked this combat. + + private static final String wurm = "Autochthon Wurm"; // 9/14 convoke trample + private static final String gigantosaurus = "Gigantosaurus"; // 10/10 + + private static final String moraug = "Moraug, Fury of Akoum"; // 6/6 + // Each creature you control gets +1/+0 for each time it has attacked this turn. + // Landfall — Whenever a land you control enters, if it's your main phase, + // there's an additional combat phase after this phase. At the beginning of that combat, untap all creatures you control. + + private static final String recovery = "Miraculous Recovery"; // 4W instant + // Return target creature card from your graveyard to the battlefield. Put a +1/+1 counter on it. + + @Test + public void testMembraneTrigger() { + addCard(Zone.HAND, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerA, gigantosaurus); + addCard(Zone.BATTLEFIELD, playerA, moraug); + addCard(Zone.HAND, playerB, cathedralMembrane); + addCard(Zone.HAND, playerB, recovery); + addCard(Zone.BATTLEFIELD, playerB, "Plains", 6); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, cathedralMembrane); + setChoice(playerB, true); // pay life + + attack(3, playerA, wurm, playerB); + + block(3, playerB, cathedralMembrane, wurm); + setChoiceAmount(playerA, 6); // assign trample damage + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertTapped(wurm, true); + assertTapped(gigantosaurus, false); + assertTapped(moraug, false); + assertGraveyardCount(playerB, cathedralMembrane, 1); + assertLife(playerA, 20); + assertLife(playerB, 20 - 2 - 4); + assertDamageReceived(playerA, wurm, 6); + assertDamageReceived(playerA, gigantosaurus, 0); + assertDamageReceived(playerA, moraug, 0); + + } + + @Test + public void testMembraneTriggerAgain() { + addCard(Zone.HAND, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, wurm); + addCard(Zone.BATTLEFIELD, playerA, gigantosaurus); + addCard(Zone.BATTLEFIELD, playerA, moraug); + addCard(Zone.HAND, playerB, cathedralMembrane); + addCard(Zone.HAND, playerB, recovery); + addCard(Zone.BATTLEFIELD, playerB, "Plains", 6); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, cathedralMembrane); + setChoice(playerB, true); // pay life + + attack(3, playerA, wurm, playerB); + + block(3, playerB, cathedralMembrane, wurm); + setChoiceAmount(playerA, 6); // assign trample damage + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_COMBAT); + execute(); // separate execute needed to separate commands from second combat phase + + playLand(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Mountain"); + castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerB, recovery, cathedralMembrane); + + attack(3, playerA, wurm, playerB); + attack(3, playerA, gigantosaurus, playerB); + + block(3, playerB, cathedralMembrane, gigantosaurus); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertTapped(wurm, true); + assertTapped(gigantosaurus, true); + assertTapped(moraug, false); + assertGraveyardCount(playerB, cathedralMembrane, 1); + assertLife(playerA, 20); + assertLife(playerB, 20 - 2 - 4 - 11); + assertDamageReceived(playerA, wurm, 6); + assertDamageReceived(playerA, gigantosaurus, 1 + 6); + assertDamageReceived(playerA, moraug, 0); + + } + +} diff --git a/Mage/src/main/java/mage/watchers/common/BlockedAttackerWatcher.java b/Mage/src/main/java/mage/watchers/common/BlockedAttackerWatcher.java index 1ceae3d0172..3b4beba4aca 100644 --- a/Mage/src/main/java/mage/watchers/common/BlockedAttackerWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/BlockedAttackerWatcher.java @@ -1,9 +1,8 @@ package mage.watchers.common; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; + import mage.MageObjectReference; import mage.constants.WatcherScope; import mage.game.Game; @@ -17,7 +16,9 @@ import mage.watchers.Watcher; */ public class BlockedAttackerWatcher extends Watcher { - private final Map> blockData = new HashMap<>(); + // key: blocking creatures + // value: set of creatures blocked + private final Map> blockerMap = new HashMap<>(); /** * Game default watcher @@ -29,31 +30,31 @@ public class BlockedAttackerWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { if (event.getType() == GameEvent.EventType.BLOCKER_DECLARED) { - MageObjectReference blocker = new MageObjectReference(event.getSourceId(), game); - Set blockedAttackers = blockData.get(blocker); - if (blockedAttackers != null) { - blockedAttackers.add(new MageObjectReference(event.getTargetId(), game)); - } else { - blockedAttackers = new HashSet<>(); - blockedAttackers.add(new MageObjectReference(event.getTargetId(), game)); - blockData.put(blocker, blockedAttackers); - } + blockerMap.computeIfAbsent(new MageObjectReference(event.getSourceId(), game), k -> new HashSet<>()) + .add(new MageObjectReference(event.getTargetId(), game)); } } @Override public void reset() { super.reset(); - blockData.clear(); + blockerMap.clear(); } public boolean creatureHasBlockedAttacker(Permanent attacker, Permanent blocker, Game game) { - Set blockedAttackers = blockData.get(new MageObjectReference(blocker, game)); - return blockedAttackers != null && blockedAttackers.contains(new MageObjectReference(attacker, game)); + return blockerMap.getOrDefault(new MageObjectReference(blocker, game), Collections.emptySet()) + .contains(new MageObjectReference(attacker, game)); } - public boolean creatureHasBlockedAttacker(MageObjectReference attacker, MageObjectReference blocker, Game game) { - Set blockedAttackers = blockData.get(blocker); - return blockedAttackers != null && blockedAttackers.contains(attacker); + public boolean creatureHasBlockedAttacker(MageObjectReference attacker, MageObjectReference blocker) { + return blockerMap.getOrDefault(blocker, Collections.emptySet()).contains(attacker); + } + + public Set getBlockedCreatures(MageObjectReference blocker, Game game) { + return blockerMap.getOrDefault(blocker, Collections.emptySet()) + .stream() + .map(m -> m.getPermanent(game)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); } }