rework more Prevention Effects involving counters. Implement [PIP] Bloatfly Swarm (#12205)

This commit is contained in:
Susucre 2024-05-23 19:48:44 +02:00 committed by GitHub
parent e3e34dae33
commit bcff245a31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 545 additions and 189 deletions

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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()));
}

View file

@ -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) {

View file

@ -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());
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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}"));

View file

@ -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());

View file

@ -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 {
// Ugins 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 Ugins Conjurant while it has a +1/+1 counter on it, prevent that damage and remove that many +1/+1 counters from Ugins 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) {

View file

@ -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) {

View file

@ -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());
// <i>Landfall</i>-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));

View file

@ -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));

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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 "";
}
}

View file

@ -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<MageObjectReference> 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);
}
}

View file

@ -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<MageObjectReference> 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);
}
}

View file

@ -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<Game> {
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)

View file

@ -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;
}