diff --git a/Mage.Sets/src/mage/cards/b/BloatflySwarm.java b/Mage.Sets/src/mage/cards/b/BloatflySwarm.java new file mode 100644 index 00000000000..2d8bd4db927 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BloatflySwarm.java @@ -0,0 +1,80 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; +import mage.abilities.effects.common.counter.AddCountersPlayersEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class BloatflySwarm extends CardImpl { + + public BloatflySwarm(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}"); + + this.subtype.add(SubType.INSECT); + this.subtype.add(SubType.MUTANT); + this.power = new MageInt(0); + this.toughness = new MageInt(0); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Bloatfly Swarm enters the battlefield with five +1/+1 counters on it. + this.addAbility(new EntersBattlefieldAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(5)), + "with five +1/+1 counters on it" + )); + + // If damage would be dealt to Bloatfly Swarm while it has a +1/+1 counter on it, prevent that damage, remove that many +1/+1 counters from it, then give each player a rad counter for each +1/+1 counter removed this way. + this.addAbility(new SimpleStaticAbility(new BloatflySwarmPreventionEffect()), PreventDamageAndRemoveCountersEffect.createWatcher()); + } + + private BloatflySwarm(final BloatflySwarm card) { + super(card); + } + + @Override + public BloatflySwarm copy() { + return new BloatflySwarm(this); + } +} + +class BloatflySwarmPreventionEffect extends PreventDamageAndRemoveCountersEffect { + + BloatflySwarmPreventionEffect() { + super(true, true, true); + staticText += ", then give each player a rad counter for each +1/+1 counter removed this way"; + } + + private BloatflySwarmPreventionEffect(final BloatflySwarmPreventionEffect effect) { + super(effect); + } + + @Override + public BloatflySwarmPreventionEffect copy() { + return new BloatflySwarmPreventionEffect(this); + } + + @Override + protected void onDamagePrevented(GameEvent event, Ability source, Game game, int amountRemovedInTotal, int amountRemovedThisTime) { + super.onDamagePrevented(event, source, game, amountRemovedInTotal, amountRemovedThisTime); + new AddCountersPlayersEffect(CounterType.RAD.createInstance(amountRemovedThisTime), TargetController.EACH_PLAYER) + .apply(game, source); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/MagmaPummeler.java b/Mage.Sets/src/mage/cards/m/MagmaPummeler.java index 967f112b5d8..809775c9bd0 100644 --- a/Mage.Sets/src/mage/cards/m/MagmaPummeler.java +++ b/Mage.Sets/src/mage/cards/m/MagmaPummeler.java @@ -2,9 +2,11 @@ package mage.cards.m; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedCounterRemovedValue; import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; @@ -15,13 +17,12 @@ import mage.constants.SubType; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.target.common.TargetAnyTarget; import java.util.UUID; /** - * @author TheElk801 + * @author TheElk801, Susucr */ public final class MagmaPummeler extends CardImpl { @@ -35,9 +36,9 @@ public final class MagmaPummeler extends CardImpl { // Magma Pummeler enters the battlefield with X +1/+1 counters on it. this.addAbility(new EntersBattlefieldAbility(new EntersBattlefieldWithXCountersEffect(CounterType.P1P1.createInstance()))); - // If damage would be dealt to Magma Pummeler while it has a +1/+1 counter on it, prevent that damage and remove that many +1/+1 counters from it. - // When one or more counters are removed from Magma Pummeler this way, it deals that much damage to any target. - this.addAbility(new SimpleStaticAbility(new MagmaPummelerEffect())); + // If damage would be dealt to Magma Pummeler while it has a +1/+1 counter on it, prevent that damage and remove that many +1/+1 counters from it. When one or more counters are removed from Magma Pummeler this way, it deals that much damage to any target. + Ability ability = new SimpleStaticAbility(new MagmaPummelerPreventionEffect()); + this.addAbility(ability, PreventDamageAndRemoveCountersEffect.createWatcher()); } private MagmaPummeler(final MagmaPummeler card) { @@ -50,40 +51,53 @@ public final class MagmaPummeler extends CardImpl { } } -class MagmaPummelerEffect extends PreventDamageAndRemoveCountersEffect { +class MagmaPummelerPreventionEffect extends PreventDamageAndRemoveCountersEffect { - MagmaPummelerEffect() { + // This is not standard for the codebase, but we need to trigger only once if multiple + // source deal damage at the same time. + // To achieve that, we store the delayedId's is created on first instance, + // and its inner value gets modified if not triggered yet. + private UUID reflexiveId; + + MagmaPummelerPreventionEffect() { super(true, true, true); - staticText += ". When one or more counters are removed from {this} this way, it deals that much damage to any target"; + staticText = "If damage would be dealt to {this} while it has a +1/+1 counter on it, " + + "prevent that damage and remove that many +1/+1 counters from it. " + + "When one or more counters are removed from {this} this way, it deals that much damage to any target."; + this.reflexiveId = null; } - private MagmaPummelerEffect(final MagmaPummelerEffect effect) { + private MagmaPummelerPreventionEffect(final MagmaPummelerPreventionEffect effect) { super(effect); + this.reflexiveId = effect.reflexiveId; } @Override - public MagmaPummelerEffect copy() { - return new MagmaPummelerEffect(this); + public PreventDamageAndRemoveCountersEffect copy() { + return new MagmaPummelerPreventionEffect(this); } @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent == null) { - return false; - } - int beforeCounters = permanent.getCounters(game).getCount(CounterType.P1P1); - super.replaceEvent(event, source, game); - int countersRemoved = beforeCounters - permanent.getCounters(game).getCount(CounterType.P1P1); - if (countersRemoved > 0) { - ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( - new DamageTargetEffect(countersRemoved), false, - "{this} deals that much damage to any target" + protected void onDamagePrevented(GameEvent event, Ability source, Game game, int amountRemovedInTotal, int amountRemovedThisTime) { + super.onDamagePrevented(event, source, game, amountRemovedInTotal, amountRemovedThisTime); + + if (amountRemovedInTotal == amountRemovedThisTime && amountRemovedInTotal > 0) { + // First instance of damage prevention, we create a new reflexive ability. + ReflexiveTriggeredAbility reflexive = new ReflexiveTriggeredAbility( + new DamageTargetEffect(SavedCounterRemovedValue.MUCH), false, + "When one or more counters are removed from {this} this way, it deals that much damage to any target." ); - ability.addTarget(new TargetAnyTarget()); - game.fireReflexiveTriggeredAbility(ability, source); + reflexive.addTarget(new TargetAnyTarget()); + reflexiveId = game.fireReflexiveTriggeredAbility(reflexive, source, true); + } + if (reflexiveId != null) { + // Set the amount of counters removed to the latest known info. + DelayedTriggeredAbility reflexive = game.getState().getDelayed().get(reflexiveId).orElse(null); + if (reflexive instanceof ReflexiveTriggeredAbility) { + reflexive.getEffects().setValue(SavedCounterRemovedValue.VALUE_KEY, amountRemovedInTotal); + } else { + reflexiveId = null; + } } - return false; } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/o/OathswornKnight.java b/Mage.Sets/src/mage/cards/o/OathswornKnight.java index bd0b07f9278..093fe782c6a 100644 --- a/Mage.Sets/src/mage/cards/o/OathswornKnight.java +++ b/Mage.Sets/src/mage/cards/o/OathswornKnight.java @@ -36,7 +36,9 @@ public final class OathswornKnight extends CardImpl { this.addAbility(new AttacksEachCombatStaticAbility()); // If damage would be dealt to Oathsworn Knight while it has a +1/+1 counter on it, prevent that damage and remove a +1/+1 counter from it. - this.addAbility(new SimpleStaticAbility(new PreventDamageAndRemoveCountersEffect(false, true, true))); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, true, true) + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } private OathswornKnight(final OathswornKnight card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomCentaur.java b/Mage.Sets/src/mage/cards/p/PhantomCentaur.java index 18682c4b0f7..9ed817b06d6 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomCentaur.java +++ b/Mage.Sets/src/mage/cards/p/PhantomCentaur.java @@ -5,7 +5,7 @@ import mage.MageInt; import mage.ObjectColor; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.PhantomPreventionEffect; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.keyword.ProtectionAbility; import mage.cards.CardImpl; @@ -36,7 +36,9 @@ public final class PhantomCentaur extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)), "with three +1/+1 counters on it")); // If damage would be dealt to Phantom Centaur, prevent that damage. Remove a +1/+1 counter from Phantom Centaur. - this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, false, false).withPhantomText() + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } private PhantomCentaur(final PhantomCentaur card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomFlock.java b/Mage.Sets/src/mage/cards/p/PhantomFlock.java index ef667a25540..d0cbf7ce647 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomFlock.java +++ b/Mage.Sets/src/mage/cards/p/PhantomFlock.java @@ -4,7 +4,7 @@ package mage.cards.p; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.PhantomPreventionEffect; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; @@ -35,7 +35,9 @@ public final class PhantomFlock extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)), "with three +1/+1 counters on it")); // If damage would be dealt to Phantom Flock, prevent that damage. Remove a +1/+1 counter from Phantom Flock. - this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, false, false).withPhantomText() + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } private PhantomFlock(final PhantomFlock card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomNantuko.java b/Mage.Sets/src/mage/cards/p/PhantomNantuko.java index 073074502e8..e4d22e5c842 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomNantuko.java +++ b/Mage.Sets/src/mage/cards/p/PhantomNantuko.java @@ -6,7 +6,7 @@ import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.effects.PhantomPreventionEffect; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -36,7 +36,9 @@ public final class PhantomNantuko extends CardImpl { // Phantom Nantuko enters the battlefield with two +1/+1 counters on it. this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(2), true), "with two +1/+1 counters on it")); // If damage would be dealt to Phantom Nantuko, prevent that damage. Remove a +1/+1 counter from Phantom Nantuko. - this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, false, false).withPhantomText() + ), PreventDamageAndRemoveCountersEffect.createWatcher()); // {tap}: Put a +1/+1 counter on Phantom Nantuko. this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new AddCountersSourceEffect(CounterType.P1P1.createInstance()), new TapSourceCost())); } diff --git a/Mage.Sets/src/mage/cards/p/PhantomNishoba.java b/Mage.Sets/src/mage/cards/p/PhantomNishoba.java index a66ae3b4fd8..bf61b40e26c 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomNishoba.java +++ b/Mage.Sets/src/mage/cards/p/PhantomNishoba.java @@ -5,7 +5,7 @@ import mage.MageInt; import mage.abilities.common.DealsDamageGainLifeSourceTriggeredAbility; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.PhantomPreventionEffect; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; @@ -39,7 +39,9 @@ public final class PhantomNishoba extends CardImpl { this.addAbility(new DealsDamageGainLifeSourceTriggeredAbility()); // If damage would be dealt to Phantom Nishoba, prevent that damage. Remove a +1/+1 counter from Phantom Nishoba. - this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, false, false).withPhantomText() + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } private PhantomNishoba(final PhantomNishoba card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomNomad.java b/Mage.Sets/src/mage/cards/p/PhantomNomad.java index 7052b8243c5..0cc83aebf06 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomNomad.java +++ b/Mage.Sets/src/mage/cards/p/PhantomNomad.java @@ -4,7 +4,7 @@ package mage.cards.p; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.PhantomPreventionEffect; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -32,7 +32,9 @@ public final class PhantomNomad extends CardImpl { "with two +1/+1 counters on it")); // If damage would be dealt to Phantom Nomad, prevent that damage. Remove a +1/+1 counter from Phantom Nomad. - this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, false, false).withPhantomText() + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } diff --git a/Mage.Sets/src/mage/cards/p/PhantomTiger.java b/Mage.Sets/src/mage/cards/p/PhantomTiger.java index 1521811f679..7ff64b715d2 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomTiger.java +++ b/Mage.Sets/src/mage/cards/p/PhantomTiger.java @@ -4,7 +4,7 @@ package mage.cards.p; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.PhantomPreventionEffect; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -30,7 +30,9 @@ public final class PhantomTiger extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), "with two +1/+1 counters on it")); // If damage would be dealt to Phantom Tiger, prevent that damage. Remove a +1/+1 counter from Phantom Tiger. - this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, false, false).withPhantomText() + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } private PhantomTiger(final PhantomTiger card) { diff --git a/Mage.Sets/src/mage/cards/p/PhantomWurm.java b/Mage.Sets/src/mage/cards/p/PhantomWurm.java index 291b1b2367f..36a849e80e4 100644 --- a/Mage.Sets/src/mage/cards/p/PhantomWurm.java +++ b/Mage.Sets/src/mage/cards/p/PhantomWurm.java @@ -4,7 +4,7 @@ package mage.cards.p; import mage.MageInt; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.PhantomPreventionEffect; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -32,7 +32,9 @@ public final class PhantomWurm extends CardImpl { "with four +1/+1 counters on it")); // If damage would be dealt to Phantom Wurm, prevent that damage. Remove a +1/+1 counter from Phantom Wurm. - this.addAbility(new SimpleStaticAbility(new PhantomPreventionEffect()), PhantomPreventionEffect.createWatcher()); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, false, false).withPhantomText() + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } private PhantomWurm(final PhantomWurm card) { diff --git a/Mage.Sets/src/mage/cards/p/PolukranosUnchained.java b/Mage.Sets/src/mage/cards/p/PolukranosUnchained.java index 73555a78335..b8cf9c491f8 100644 --- a/Mage.Sets/src/mage/cards/p/PolukranosUnchained.java +++ b/Mage.Sets/src/mage/cards/p/PolukranosUnchained.java @@ -51,7 +51,9 @@ public final class PolukranosUnchained extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new PolukranosUnchainedEffect())); // If damage would be dealt to Polukranos while it has a +1/+1 counter on it, prevent that damage and remove that many +1/+1 counters from it. - this.addAbility(new SimpleStaticAbility(new PreventDamageAndRemoveCountersEffect(true, true, true))); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(true, true, true) + ), PreventDamageAndRemoveCountersEffect.createWatcher()); // {1}{B}{G}: Polukranos fights another target creature. Ability ability = new SimpleActivatedAbility(new FightTargetSourceEffect(), new ManaCostsImpl<>("{1}{B}{G}")); diff --git a/Mage.Sets/src/mage/cards/p/ProteanHydra.java b/Mage.Sets/src/mage/cards/p/ProteanHydra.java index 335e9602a78..e7d5640e7d1 100644 --- a/Mage.Sets/src/mage/cards/p/ProteanHydra.java +++ b/Mage.Sets/src/mage/cards/p/ProteanHydra.java @@ -1,6 +1,5 @@ package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.TriggeredAbilityImpl; @@ -19,14 +18,15 @@ import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; +import java.util.UUID; + /** - * * @author BetaSteward_at_googlemail.com */ public final class ProteanHydra extends CardImpl { public ProteanHydra(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{X}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{X}{G}"); this.subtype.add(SubType.HYDRA); this.power = new MageInt(0); @@ -36,7 +36,9 @@ public final class ProteanHydra extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new EntersBattlefieldWithXCountersEffect(CounterType.P1P1.createInstance()))); // If damage would be dealt to Protean Hydra, prevent that damage and remove that many +1/+1 counters from it. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PreventDamageAndRemoveCountersEffect(true, false, true))); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(true, false, true) + ), PreventDamageAndRemoveCountersEffect.createWatcher()); // Whenever a +1/+1 counter is removed from Protean Hydra, put two +1/+1 counters on it at the beginning of the next end step. this.addAbility(new ProteanHydraAbility()); diff --git a/Mage.Sets/src/mage/cards/u/UginsConjurant.java b/Mage.Sets/src/mage/cards/u/UginsConjurant.java index 1c6f87732bd..0c9ebd1f097 100644 --- a/Mage.Sets/src/mage/cards/u/UginsConjurant.java +++ b/Mage.Sets/src/mage/cards/u/UginsConjurant.java @@ -1,28 +1,26 @@ package mage.cards.u; -import java.util.UUID; import mage.MageInt; -import mage.constants.SubType; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; +import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; - -import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; -import mage.abilities.effects.PreventDamageAndRemoveCountersEffect; -import mage.abilities.common.SimpleStaticAbility; +import mage.constants.SubType; import mage.counters.CounterType; -import mage.constants.Zone; + +import java.util.UUID; /** - * * @author antoni-g */ public final class UginsConjurant extends CardImpl { public UginsConjurant(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{X}"); - + this.subtype.add(SubType.SPIRIT); this.subtype.add(SubType.MONK); this.power = new MageInt(0); @@ -31,7 +29,9 @@ public final class UginsConjurant extends CardImpl { // Ugin’s Conjurant enters the battlefield with X +1/+1 counters on it. this.addAbility(new EntersBattlefieldAbility(new EntersBattlefieldWithXCountersEffect(CounterType.P1P1.createInstance()))); // If damage would be dealt to Ugin’s Conjurant while it has a +1/+1 counter on it, prevent that damage and remove that many +1/+1 counters from Ugin’s Conjurant. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PreventDamageAndRemoveCountersEffect(true, true, false))); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(true, true, false) + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } private UginsConjurant(final UginsConjurant card) { diff --git a/Mage.Sets/src/mage/cards/u/UnbreathingHorde.java b/Mage.Sets/src/mage/cards/u/UnbreathingHorde.java index 733e9124824..1fd11461fc1 100644 --- a/Mage.Sets/src/mage/cards/u/UnbreathingHorde.java +++ b/Mage.Sets/src/mage/cards/u/UnbreathingHorde.java @@ -34,8 +34,9 @@ public final class UnbreathingHorde extends CardImpl { this.addAbility(new EntersBattlefieldAbility(new UnbreathingHordeEntersEffect(), "with a +1/+1 counter on it for each other Zombie you control and each Zombie card in your graveyard")); // If Unbreathing Horde would be dealt damage, prevent that damage and remove a +1/+1 counter from it. - this.addAbility(new SimpleStaticAbility(new PreventDamageAndRemoveCountersEffect(false, false, true) - .setText("if {this} would be dealt damage, prevent that damage and remove a +1/+1 counter from it"))); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, false, true) + ), PreventDamageAndRemoveCountersEffect.createWatcher()); } private UnbreathingHorde(final UnbreathingHorde card) { diff --git a/Mage.Sets/src/mage/cards/u/UndergrowthChampion.java b/Mage.Sets/src/mage/cards/u/UndergrowthChampion.java index 5016251c3fc..46af1295609 100644 --- a/Mage.Sets/src/mage/cards/u/UndergrowthChampion.java +++ b/Mage.Sets/src/mage/cards/u/UndergrowthChampion.java @@ -1,7 +1,6 @@ package mage.cards.u; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.LandfallAbility; import mage.abilities.common.SimpleStaticAbility; @@ -13,20 +12,23 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.counters.CounterType; +import java.util.UUID; + /** - * * @author fireshoes */ public final class UndergrowthChampion extends CardImpl { public UndergrowthChampion(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{1}{G}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{G}"); this.subtype.add(SubType.ELEMENTAL); this.power = new MageInt(2); this.toughness = new MageInt(2); // If damage would be dealt to Undergrowth Champion while it has a +1/+1 counter on it, prevent that damage and remove a +1/+1 counter from Undergrowth Champion. - this.addAbility(new SimpleStaticAbility(new PreventDamageAndRemoveCountersEffect(false, true, false))); + this.addAbility(new SimpleStaticAbility( + new PreventDamageAndRemoveCountersEffect(false, true, false) + ), PreventDamageAndRemoveCountersEffect.createWatcher()); // Landfall-Whenever a land enters the battlefield under your control, put a +1/+1 counter on Undergrowth Champion. this.addAbility(new LandfallAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false)); diff --git a/Mage.Sets/src/mage/sets/Fallout.java b/Mage.Sets/src/mage/sets/Fallout.java index cd99b28d7a7..193859d42b8 100644 --- a/Mage.Sets/src/mage/sets/Fallout.java +++ b/Mage.Sets/src/mage/sets/Fallout.java @@ -54,6 +54,7 @@ public final class Fallout extends ExpansionSet { cards.add(new SetCardInfo("Biomass Mutation", 212, Rarity.RARE, mage.cards.b.BiomassMutation.class)); cards.add(new SetCardInfo("Black Market", 183, Rarity.RARE, mage.cards.b.BlackMarket.class)); cards.add(new SetCardInfo("Blasphemous Act", 188, Rarity.RARE, mage.cards.b.BlasphemousAct.class)); + cards.add(new SetCardInfo("Bloatfly Swarm", 42, Rarity.UNCOMMON, mage.cards.b.BloatflySwarm.class)); cards.add(new SetCardInfo("Bloodforged Battle-Axe", 226, Rarity.RARE, mage.cards.b.BloodforgedBattleAxe.class)); cards.add(new SetCardInfo("Boomer Scrapper", 95, Rarity.RARE, mage.cards.b.BoomerScrapper.class)); cards.add(new SetCardInfo("Bottle-Cap Blast", 55, Rarity.UNCOMMON, mage.cards.b.BottleCapBlast.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventDamageRemoveCountersTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventDamageRemoveCountersTest.java index 4ea3ac7d895..bb9407453f1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventDamageRemoveCountersTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/prevention/PreventDamageRemoveCountersTest.java @@ -2,14 +2,13 @@ package org.mage.test.cards.prevention; import mage.constants.PhaseStep; import mage.constants.Zone; -import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; public class PreventDamageRemoveCountersTest extends CardTestPlayerBase { @Test - public void testCounterRemoval() { + public void test_OathswornKnight_CounterRemoval() { setStrictChooseMode(true); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); @@ -32,7 +31,7 @@ public class PreventDamageRemoveCountersTest extends CardTestPlayerBase { } @Test - public void testMagmaPummeler() { + public void test_MagmaPummeler() { setStrictChooseMode(true); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); @@ -50,11 +49,117 @@ public class PreventDamageRemoveCountersTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Magma Pummeler", 1); assertPowerToughness(playerA, "Magma Pummeler", 3, 3); // 2 counters removed assertLife(playerB, 18); // 2 damage dealt - } @Test - public void testProteanHydraBoosted() { + public void test_MagmaPummeler_DoubleBlocked() { + // The part of this one that is weird is that there should be only a single trigger, that sums + // all the counter removed by multiple prevention effects occuring at the same time. + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + addCard(Zone.HAND, playerA, "Magma Pummeler", 1); + addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1); + addCard(Zone.BATTLEFIELD, playerB, "Goblin Piker", 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Magma Pummeler"); + setChoice(playerA, "X=5"); + + attack(3, playerA, "Magma Pummeler", playerB); + block(3, playerB, "Memnite", "Magma Pummeler"); + block(3, playerB, "Goblin Piker", "Magma Pummeler"); + setChoice(playerA, "X=5"); // damage for Pummeler, does not really matter for this test. + addTarget(playerA, playerB); // For the one trigger + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Magma Pummeler", 1); + assertPowerToughness(playerA, "Magma Pummeler", 2, 2); // 3 counters removed + assertLife(playerB, 20 - 3); // 3 damage dealt by the 1 trigger. + } + + @Test + public void test_MagmaPummeler_KilledByMore() { + + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 7 + 5); + addCard(Zone.HAND, playerA, "Magma Pummeler", 1); + addCard(Zone.HAND, playerA, "Shivan Meteor", 1); // 13 damage to target creature + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Magma Pummeler"); + setChoice(playerA, "X=5"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shivan Meteor", "Magma Pummeler"); + addTarget(playerA, playerB); // For the reflective trigger + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Magma Pummeler", 1); + assertGraveyardCount(playerA, "Shivan Meteor", 1); + assertLife(playerB, 20 - 5); // 5 counters removed in total. + } + + @Test + public void test_MagmaPummeler_DoubleBlocked_And_Die() { + // The part of this one that is weird is that there should be only a single trigger, that sums + // all the counter removed by multiple prevention effects occuring at the same time. + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + addCard(Zone.HAND, playerA, "Magma Pummeler", 1); + addCard(Zone.BATTLEFIELD, playerB, "Centaur Courser", 1); // 3/3 + addCard(Zone.BATTLEFIELD, playerB, "Air Elemental", 1); // 4/4 + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Magma Pummeler"); + setChoice(playerA, "X=5"); + + attack(3, playerA, "Magma Pummeler", playerB); + block(3, playerB, "Centaur Courser", "Magma Pummeler"); + block(3, playerB, "Air Elemental", "Magma Pummeler"); + setChoice(playerA, "X=5"); // damage for Pummeler, does not really matter for this test. + addTarget(playerA, playerB); // For the one trigger + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Magma Pummeler", 1); + assertLife(playerB, 20 - 5); // 5 counters prevented, Pummeler's trigger dealt 5. + } + + + @Test + public void test_UndergrowthChampion_DoubleBlocked() { + setStrictChooseMode(true); + + // Undergrowth Champion {1}{G}{G} + // Creature — Elemental + // If damage would be dealt to Undergrowth Champion while it has a +1/+1 counter on it, prevent that damage and remove a +1/+1 counter from Undergrowth Champion. + // Landfall — Whenever a land enters the battlefield under your control, put a +1/+1 counter on Undergrowth Champion. + // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Undergrowth Champion"); + addCard(Zone.HAND, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerB, "Elite Vanguard"); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plains"); + // Champion now has a +1/+1 counter + + attack(1, playerA, "Undergrowth Champion", playerB); + block(1, playerB, "Grizzly Bears", "Undergrowth Champion"); + block(1, playerB, "Elite Vanguard", "Undergrowth Champion"); + setChoice(playerA, "X=2"); // damage attribution + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertGraveyardCount(playerB, 2); + assertDamageReceived(playerA, "Undergrowth Champion", 0); // All the damage should be prevented. + assertPowerToughness(playerA, "Undergrowth Champion", 2, 2); + } + + @Test + public void test_ProteanHydra_Boosted() { setStrictChooseMode(true); addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/BloatflySwarmTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/BloatflySwarmTest.java new file mode 100644 index 00000000000..18669190a78 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/BloatflySwarmTest.java @@ -0,0 +1,91 @@ +package org.mage.test.cards.single.pip; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class BloatflySwarmTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.b.BloatflySwarm Bloatfly Swarm} {3}{B} + * Creature — Insect Mutant + * Flying + * Bloatfly Swarm enters the battlefield with five +1/+1 counters on it. + * If damage would be dealt to Bloatfly Swarm while it has a +1/+1 counter on it, prevent that damage, remove that many +1/+1 counters from it, then give each player a rad counter for each +1/+1 counter removed this way. + * 0/0 + */ + private static final String swarm = "Bloatfly Swarm"; + + @Test + public void test_Bolt() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, swarm); + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 6); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, swarm, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", swarm); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPowerToughness(playerA, swarm, 2, 2); + assertCounterCount(playerA, CounterType.RAD, 3); + assertCounterCount(playerB, CounterType.RAD, 3); + } + + @Test + public void test_Combat() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Concordant Crossroads"); // For haste + addCard(Zone.HAND, playerA, swarm); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.BATTLEFIELD, playerB, "Brimstone Dragon"); // 6/6 Flying Haste + addCard(Zone.BATTLEFIELD, playerB, "Giant Spider"); // 2/4 Reach + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, swarm); + + attack(1, playerA, swarm); + block(1, playerB, "Brimstone Dragon", swarm); + block(1, playerB, "Giant Spider", swarm); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, swarm, 1); + assertCounterCount(playerA, CounterType.RAD, 5); + assertCounterCount(playerB, CounterType.RAD, 5); + } + + @Test + public void test_Combat_Small() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Concordant Crossroads"); // For haste + addCard(Zone.HAND, playerA, swarm); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + addCard(Zone.BATTLEFIELD, playerB, "Wind Drake"); // 2/2 Flying + addCard(Zone.BATTLEFIELD, playerB, "Giant Spider"); // 2/4 Reach + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, swarm); + + attack(1, playerA, swarm); + block(1, playerB, "Wind Drake", swarm); + block(1, playerB, "Giant Spider", swarm); + setChoice(playerA, "X=5"); // damage attribution + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, swarm, 1, 1); + assertCounterCount(playerA, CounterType.RAD, 4); + assertCounterCount(playerB, CounterType.RAD, 4); + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedCounterRemovedValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedCounterRemovedValue.java new file mode 100644 index 00000000000..fa9ec3898a2 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/SavedCounterRemovedValue.java @@ -0,0 +1,46 @@ +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.game.Game; + +import java.util.Optional; + +/** + * For trigger/prevention effects that save a value of removed counters. + * Retrieve the value in resulting effects without need for custom ones. + * + * @author Susucr + */ +public enum SavedCounterRemovedValue implements DynamicValue { + MANY("many"), + MUCH("much"); + + private final String message; + public static final String VALUE_KEY = "CounterRemoved"; + + SavedCounterRemovedValue(String message) { + this.message = "that " + message; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return Optional.ofNullable((Integer) effect.getValue(VALUE_KEY)).orElse(0); + } + + @Override + public SavedCounterRemovedValue copy() { + return this; + } + + @Override + public String toString() { + return message; + } + + @Override + public String getMessage() { + return ""; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/PhantomPreventionEffect.java b/Mage/src/main/java/mage/abilities/effects/PhantomPreventionEffect.java deleted file mode 100644 index 5eb8a35de7a..00000000000 --- a/Mage/src/main/java/mage/abilities/effects/PhantomPreventionEffect.java +++ /dev/null @@ -1,105 +0,0 @@ -package mage.abilities.effects; - -import mage.MageObjectReference; -import mage.abilities.Ability; -import mage.constants.Duration; -import mage.constants.WatcherScope; -import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.watchers.Watcher; - -import java.util.HashSet; -import java.util.Set; - -/** - * This requires to add the PhantomPreventionWatcher - * - * @author Susucr - */ -public class PhantomPreventionEffect extends PreventionEffectImpl { - - public static PhantomPreventionWatcher createWatcher() { - return new PhantomPreventionWatcher(); - } - - public PhantomPreventionEffect() { - super(Duration.WhileOnBattlefield); - staticText = "If damage would be dealt to {this}, prevent that damage. Remove a +1/+1 counter from {this}"; - } - - protected PhantomPreventionEffect(final PhantomPreventionEffect effect) { - super(effect); - } - - @Override - public PhantomPreventionEffect copy() { - return new PhantomPreventionEffect(this); - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - preventDamageAction(event, source, game); - Permanent permanent = game.getPermanent(source.getSourceId()); - PhantomPreventionWatcher watcher = game.getState().getWatcher(PhantomPreventionWatcher.class); - if (permanent != null && watcher != null) { - MageObjectReference mor = new MageObjectReference(source.getId(), source.getSourceObjectZoneChangeCounter(), game); - if (!watcher.hadMORCounterRemovedThisBatch(mor)) { - watcher.addMOR(mor); - if (permanent.getCounters(game).containsKey(CounterType.P1P1)) { - StringBuilder sb = new StringBuilder(permanent.getName()).append(": "); - permanent.removeCounters(CounterType.P1P1.createInstance(), source, game); - sb.append("Removed a +1/+1 counter "); - game.informPlayers(sb.toString()); - } - } - } - - return false; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - if (super.applies(event, source, game)) { - if (event.getTargetId().equals(source.getSourceId())) { - return true; - } - } - return false; - } -} - -class PhantomPreventionWatcher extends Watcher { - - // We keep a very short-lived set of which PhantomPreventionEffect caused - // +1/+1 to get removed during the current damage batch. - private final Set morRemovedCounterThisDamageBatch = new HashSet<>(); - - PhantomPreventionWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - // This watcher resets every time a Damage Batch could have fired (even if all damage was prevented) - if (event.getType() != GameEvent.EventType.DAMAGED_BATCH_COULD_HAVE_FIRED) { - return; - } - morRemovedCounterThisDamageBatch.clear(); - } - - @Override - public void reset() { - super.reset(); - morRemovedCounterThisDamageBatch.clear(); - } - - boolean hadMORCounterRemovedThisBatch(MageObjectReference mor) { - return morRemovedCounterThisDamageBatch.contains(mor); - } - - void addMOR(MageObjectReference mor) { - morRemovedCounterThisDamageBatch.add(mor); - } -} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/effects/PreventDamageAndRemoveCountersEffect.java b/Mage/src/main/java/mage/abilities/effects/PreventDamageAndRemoveCountersEffect.java index acc5eab775a..c9faecee202 100644 --- a/Mage/src/main/java/mage/abilities/effects/PreventDamageAndRemoveCountersEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/PreventDamageAndRemoveCountersEffect.java @@ -1,18 +1,31 @@ package mage.abilities.effects; +import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.SavedCounterRemovedValue; import mage.constants.Duration; +import mage.constants.WatcherScope; import mage.counters.CounterType; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; +import mage.watchers.Watcher; + +import java.util.HashSet; +import java.util.Set; /** - * @author antoni-g + * This requires to add the Watcher from createWatcher() together with the Prevention Effect. + * + * @author Susucr */ public class PreventDamageAndRemoveCountersEffect extends PreventionEffectImpl { - private final boolean thatMany; - private final boolean whileHasCounter; + private final boolean thatMany; // If true, remove as many counters as damage prevent. If false, remove 1 counter. + private final boolean whileHasCounter; // If true, the creature need a counter for the effect to be active. + + public static Watcher createWatcher() { + return new PreventDamageAndRemoveCountersWatcher(); + } public PreventDamageAndRemoveCountersEffect(boolean thatMany, boolean whileHasCounter, boolean textFromIt) { super(Duration.WhileOnBattlefield, Integer.MAX_VALUE, false, false); @@ -25,6 +38,14 @@ public class PreventDamageAndRemoveCountersEffect extends PreventionEffectImpl { " from " + (textFromIt ? "it" : "{this}"); } + /** + * A specific wording for the old "Phantom [...]" not covered by the new wording using "and" + */ + public PreventDamageAndRemoveCountersEffect withPhantomText() { + staticText = "If damage would be dealt to {this}, prevent that damage. Remove a +1/+1 counter from {this}."; + return this; + } + protected PreventDamageAndRemoveCountersEffect(final PreventDamageAndRemoveCountersEffect effect) { super(effect); this.thatMany = effect.thatMany; @@ -38,25 +59,94 @@ public class PreventDamageAndRemoveCountersEffect extends PreventionEffectImpl { @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { - int damage = event.getAmount(); + int damageAmount = event.getAmount(); + // Prevent the damage. + // Note that removing counters does not care if prevention did work. + // Ruling on Phantom Wurm for instance: + // > If damage that can't be prevented is be dealt to Phantom Wurm, + // > you still remove a counter even though the damage is dealt. (2021-03-19) preventDamageAction(event, source, game); Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent == null) { + PreventDamageAndRemoveCountersWatcher watcher = game.getState().getWatcher(PreventDamageAndRemoveCountersWatcher.class); + if (permanent == null || watcher == null || damageAmount <= 0) { return false; } - if (!thatMany) { - damage = 1; + MageObjectReference mor = new MageObjectReference(source.getId(), source.getSourceObjectZoneChangeCounter(), game); + int beforeCount = permanent.getCounters(game).getCount(CounterType.P1P1); + if (thatMany) { + // Remove them. + permanent.removeCounters(CounterType.P1P1.createInstance(damageAmount), source, game); + } else if (!watcher.hadMORCounterRemovedThisBatch(mor)) { + // Remove one. + permanent.removeCounters(CounterType.P1P1.createInstance(), source, game); } - permanent.removeCounters(CounterType.P1P1.createInstance(damage), source, game); //MTG ruling (this) loses counters even if the damage isn't prevented + int amountRemovedThisTime = beforeCount - permanent.getCounters(game).getCount(CounterType.P1P1); + int amountRemovedInTotal = amountRemovedThisTime; + if (!watcher.hadMORCounterRemovedThisBatch(mor)) { + watcher.addMOR(mor); + } else { + // Sum the previous added counter + amountRemovedInTotal += (Integer) source.getEffects().get(0).getValue(SavedCounterRemovedValue.VALUE_KEY); + } + onDamagePrevented(event, source, game, amountRemovedInTotal, amountRemovedThisTime); return false; } + /** + * Meant to be Overriden if needs be. + */ + protected void onDamagePrevented(GameEvent event, Ability source, Game game, int amountRemovedInTotal, int amountRemovedThisTime) { + source.getEffects().setValue(SavedCounterRemovedValue.VALUE_KEY, amountRemovedInTotal); + } + @Override public boolean applies(GameEvent event, Ability source, Game game) { Permanent permanent = game.getPermanent(event.getTargetId()); - return super.applies(event, source, game) - && permanent != null - && event.getTargetId().equals(source.getSourceId()) - && (!whileHasCounter || permanent.getCounters(game).containsKey(CounterType.P1P1)); + if (!super.applies(event, source, game) + || permanent == null + || !event.getTargetId().equals(source.getSourceId())) { + return false; + } + if (whileHasCounter && !permanent.getCounters(game).containsKey(CounterType.P1P1)) { + // If the last counter has already be removed for the same batch of prevention, we still want to prevent the damage. + PreventDamageAndRemoveCountersWatcher watcher = game.getState().getWatcher(PreventDamageAndRemoveCountersWatcher.class); + MageObjectReference mor = new MageObjectReference(source.getId(), source.getSourceObjectZoneChangeCounter(), game); + return watcher != null && watcher.hadMORCounterRemovedThisBatch(mor); + } + return true; } } + +class PreventDamageAndRemoveCountersWatcher extends Watcher { + + // We keep a very short-lived set of which PreventDamageAndRemoveCountersEffect caused + // +1/+1 to get removed during the current damage batch. + private final Set morRemovedCounterThisDamageBatch = new HashSet<>(); + + PreventDamageAndRemoveCountersWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + // This watcher resets every time a Damage Batch could have fired (even if all damage was prevented) + if (event.getType() != GameEvent.EventType.DAMAGED_BATCH_COULD_HAVE_FIRED) { + return; + } + morRemovedCounterThisDamageBatch.clear(); + } + + @Override + public void reset() { + super.reset(); + morRemovedCounterThisDamageBatch.clear(); + } + + boolean hadMORCounterRemovedThisBatch(MageObjectReference mor) { + return morRemovedCounterThisDamageBatch.contains(mor); + } + + void addMOR(MageObjectReference mor) { + morRemovedCounterThisDamageBatch.add(mor); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index b870e19124a..c12cfcba855 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -29,7 +29,6 @@ import mage.game.match.MatchType; import mage.game.mulligan.Mulligan; import mage.game.permanent.Battlefield; import mage.game.permanent.Permanent; -import mage.game.permanent.PermanentCard; import mage.game.stack.Spell; import mage.game.stack.SpellStack; import mage.game.turn.Phase; @@ -500,6 +499,8 @@ public interface Game extends MageItem, Serializable, Copyable { UUID fireReflexiveTriggeredAbility(ReflexiveTriggeredAbility reflexiveAbility, Ability source); + UUID fireReflexiveTriggeredAbility(ReflexiveTriggeredAbility reflexiveAbility, Ability source, boolean fireAsSimultaneousEvent); + /** * Inner game engine call to reset game objects to actual versions * (reset all objects and apply all effects due layer system) diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 7ca471242c5..9b548e4098c 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2192,8 +2192,18 @@ public abstract class GameImpl implements Game { @Override public UUID fireReflexiveTriggeredAbility(ReflexiveTriggeredAbility reflexiveAbility, Ability source) { + return fireReflexiveTriggeredAbility(reflexiveAbility, source, false); + } + + @Override + public UUID fireReflexiveTriggeredAbility(ReflexiveTriggeredAbility reflexiveAbility, Ability source, boolean fireAsSimultaneousEvent) { UUID uuid = this.addDelayedTriggeredAbility(reflexiveAbility, source); - this.fireEvent(GameEvent.getEvent(GameEvent.EventType.OPTION_USED, source.getOriginalId(), source, source.getControllerId())); + GameEvent event = GameEvent.getEvent(GameEvent.EventType.OPTION_USED, source.getOriginalId(), source, source.getControllerId()); + if (fireAsSimultaneousEvent) { + this.getState().addSimultaneousEvent(event, this); + } else { + this.fireEvent(event); + } return uuid; }