diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/triggers/CounterRemovalTests.java b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/CounterRemovalTests.java new file mode 100644 index 00000000000..33605126e73 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/triggers/CounterRemovalTests.java @@ -0,0 +1,194 @@ +package org.mage.test.cards.triggers; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersPlayersEffect; +import mage.constants.*; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +public class CounterRemovalTests extends CardTestCommander4Players { + + @Test + public void CounterRemovalTest(){ + + int nCountersToAdd = 3; + + addCard(Zone.HAND, playerA, "Basri's Solidarity", nCountersToAdd); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + + // Remove all counters from all permanents and exile all tokens. + addCard(Zone.HAND, playerA, "Aether Snap"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + + Ability ability = new MultiCountersRemovedTriggeredAbility(); + addCustomCardWithAbility("multicountertrigdcard", playerA, ability, null, CardType.CREATURE, "", Zone.BATTLEFIELD); + + ability = new SingleCounterRemovedTriggeredAbility(); + addCustomCardWithAbility("singlecountertrigdcard", playerA, ability, null, CardType.CREATURE, "", Zone.BATTLEFIELD); + + ability = new SimpleStaticAbility(new MultiCountersRemoveReplacementEffect()); + addCustomCardWithAbility("multicounterreplcard", playerA, ability, null, CardType.CREATURE, "", Zone.BATTLEFIELD); + + ability = new SimpleStaticAbility(new SingleCounterRemoveReplacementEffect()); + addCustomCardWithAbility("singlecounterreplcard", playerA, ability, null, CardType.CREATURE, "", Zone.BATTLEFIELD); + + for (int i = 0; i < nCountersToAdd; ++i){ + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Basri's Solidarity", true); + } + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aether Snap", true); + + //choose the single counter triggers to go on the stack first + setChoice(playerA, "When a counter", nCountersToAdd); + + setStrictChooseMode(true); + execute(); + + assertLife(playerA, currentGame.getStartingLife() - 1); + assertCounterCount(playerA, CounterType.ENERGY, nCountersToAdd); + assertCounterCount("multicounterreplcard", CounterType.P1P1, 1); + assertCounterCount("singlecounterreplcard", CounterType.P1P1, 2); + + } + +} + +class SingleCounterRemoveReplacementEffect extends ReplacementEffectImpl { + + SingleCounterRemoveReplacementEffect() { + super(Duration.Custom, Outcome.Benefit); + this.setText("When a counter would be removed from {this} that would bring it to less than 2 of that counter, that counter is not removed."); + } + + private SingleCounterRemoveReplacementEffect(final SingleCounterRemoveReplacementEffect ability) { + super(ability); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + if (event.getTargetId().equals(source.getSourceId())) { + Permanent sourcePermanent = game.getPermanent(source.getSourceId()); + if (sourcePermanent != null) { + int countersCount = sourcePermanent.getCounters(game).getCount(event.getData()); + return countersCount - 1 < 2; + } + } + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.REMOVE_COUNTER; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return true; + } + + @Override + public SingleCounterRemoveReplacementEffect copy() { + return new SingleCounterRemoveReplacementEffect(this); + } + +} + +class MultiCountersRemoveReplacementEffect extends ReplacementEffectImpl { + + MultiCountersRemoveReplacementEffect() { + super(Duration.Custom, Outcome.Benefit); + this.setText("When any number of counters would be removed from {this}, one less counter is removed."); + } + + private MultiCountersRemoveReplacementEffect(final MultiCountersRemoveReplacementEffect ability) { + super(ability); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.REMOVE_COUNTERS; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (event.getTargetId().equals(source.getSourceId())) { + event.setAmount(event.getAmount() - 1); + } + return false; + } + + @Override + public MultiCountersRemoveReplacementEffect copy() { + return new MultiCountersRemoveReplacementEffect(this); + } + +} + +class MultiCountersRemovedTriggeredAbility extends TriggeredAbilityImpl { + + MultiCountersRemovedTriggeredAbility() { + super(Zone.BATTLEFIELD, new LoseLifeSourceControllerEffect(1)); + this.setTriggerPhrase("When any number of counters are removed from {this}, "); + } + + private MultiCountersRemovedTriggeredAbility(final MultiCountersRemovedTriggeredAbility ability) { + super(ability); + } + + @Override + public MultiCountersRemovedTriggeredAbility copy() { + return new MultiCountersRemovedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.COUNTERS_REMOVED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getTargetId().equals(this.getSourceId()); + } + +} + +class SingleCounterRemovedTriggeredAbility extends TriggeredAbilityImpl { + + SingleCounterRemovedTriggeredAbility() { + super(Zone.BATTLEFIELD, new AddCountersPlayersEffect(CounterType.ENERGY.createInstance(), TargetController.YOU)); + this.setTriggerPhrase("When a counter is removed from {this}, "); + } + + private SingleCounterRemovedTriggeredAbility(final SingleCounterRemovedTriggeredAbility ability) { + super(ability); + } + + @Override + public SingleCounterRemovedTriggeredAbility copy() { + return new SingleCounterRemovedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.COUNTER_REMOVED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getTargetId().equals(this.getSourceId()); + } + +} diff --git a/Mage/src/main/java/mage/cards/Card.java b/Mage/src/main/java/mage/cards/Card.java index be08b51aa0e..9df2eee06c5 100644 --- a/Mage/src/main/java/mage/cards/Card.java +++ b/Mage/src/main/java/mage/cards/Card.java @@ -175,9 +175,17 @@ public interface Card extends MageObject, Ownerable { boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect, int maxCounters); - void removeCounters(String name, int amount, Ability source, Game game); + default void removeCounters(String name, int amount, Ability source, Game game){ + removeCounters(name, amount, source, game, false); + } - void removeCounters(Counter counter, Ability source, Game game); + void removeCounters(String name, int amount, Ability source, Game game, boolean damage); + + default void removeCounters(Counter counter, Ability source, Game game) { + removeCounters(counter, source, game, false); + } + + void removeCounters(Counter counter, Ability source, Game game, boolean damage); @Override Card copy(); diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index 1ec3d688aab..28617471606 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -815,35 +815,46 @@ public abstract class CardImpl extends MageObjectImpl implements Card { } @Override - public void removeCounters(String name, int amount, Ability source, Game game) { + public void removeCounters(String name, int amount, Ability source, Game game, boolean isDamage) { + + if (amount <= 0){ + return; + } + + if (getCounters(game).getCount(name) <= 0){ + return; + } + + GameEvent removeCountersEvent = new RemoveCountersEvent(name, this, source, amount, isDamage); + if (game.replaceEvent(removeCountersEvent)){ + return; + } + int finalAmount = 0; - for (int i = 0; i < amount; i++) { + for (int i = 0; i < removeCountersEvent.getAmount(); i++) { + + GameEvent event = new RemoveCounterEvent(name, this, source, isDamage); + if (game.replaceEvent(event)){ + continue; + } + if (!getCounters(game).removeCounter(name, 1)) { break; } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, objectId, source, getControllerOrOwnerId()); - if (source != null - && source.getControllerId() != null) { - event.setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counter - } - event.setData(name); + + event = new CounterRemovedEvent(name, this, source, isDamage); game.fireEvent(event); + finalAmount++; } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, objectId, source, getControllerOrOwnerId()); - if (source != null - && source.getControllerId() != null) { - event.setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counter - } - event.setData(name); - event.setAmount(finalAmount); + GameEvent event = new CountersRemovedEvent(name, this, source, finalAmount, isDamage); game.fireEvent(event); } @Override - public void removeCounters(Counter counter, Ability source, Game game) { + public void removeCounters(Counter counter, Ability source, Game game, boolean isDamage) { if (counter != null) { - removeCounters(counter.getName(), counter.getCount(), source, game); + removeCounters(counter.getName(), counter.getCount(), source, game, isDamage); } } diff --git a/Mage/src/main/java/mage/game/events/CounterRemovedEvent.java b/Mage/src/main/java/mage/game/events/CounterRemovedEvent.java new file mode 100644 index 00000000000..582bb461402 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/CounterRemovedEvent.java @@ -0,0 +1,29 @@ +package mage.game.events; + +import mage.abilities.Ability; +import mage.cards.Card; +import mage.players.Player; + +public class CounterRemovedEvent extends GameEvent { + + boolean isDamage; + + public CounterRemovedEvent(String name, Card targetCard, Ability source, boolean isDamage){ + super(EventType.COUNTER_REMOVED, targetCard.getId(), source, + (source == null ? null : source.getControllerId())); + setData(name); + this.isDamage = isDamage; + } + + public CounterRemovedEvent(String name, Player targetPlayer, Ability source, boolean isDamage){ + super(EventType.COUNTER_REMOVED, targetPlayer.getId(), source, + (source == null ? null : source.getControllerId())); + setData(name); + this.isDamage = isDamage; + } + + boolean counterRemovedDueToDamage(){ + return this.isDamage; + } + +} diff --git a/Mage/src/main/java/mage/game/events/CountersRemovedEvent.java b/Mage/src/main/java/mage/game/events/CountersRemovedEvent.java new file mode 100644 index 00000000000..82ea75f0477 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/CountersRemovedEvent.java @@ -0,0 +1,29 @@ +package mage.game.events; + +import mage.abilities.Ability; +import mage.cards.Card; +import mage.players.Player; + +public class CountersRemovedEvent extends GameEvent { + + boolean isDamage; + + public CountersRemovedEvent(String name, Card targetCard, Ability source, int amount, boolean isDamage){ + super(EventType.COUNTERS_REMOVED, targetCard.getId(), source, + (source == null ? null : source.getControllerId()), amount, false); + setData(name); + this.isDamage = isDamage; + } + + public CountersRemovedEvent(String name, Player targetPlayer, Ability source, int amount, boolean isDamage){ + super(EventType.COUNTERS_REMOVED, targetPlayer.getId(), source, + (source == null ? null : source.getControllerId()), amount, false); + setData(name); + this.isDamage = isDamage; + } + + boolean counterRemovedDueToDamage(){ + return this.isDamage; + } + +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index f80cd1702a2..4cac8db1e47 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -541,6 +541,14 @@ public class GameEvent implements Serializable { STAY_ATTACHED, ADD_COUNTER, COUNTER_ADDED, ADD_COUNTERS, COUNTERS_ADDED, + /* REMOVE_COUNTER, REMOVE_COUNTERS, COUNTER_REMOVED, COUNTERS_REMOVED + targetId id of the permanent or player losing counter(s) + sourceId id of the ability removing them + playerId player who controls the ability removing the counters + amount number of counters being removed + data name of the counter(s) being removed + */ + REMOVE_COUNTER, REMOVE_COUNTERS, COUNTER_REMOVED, COUNTERS_REMOVED, LOSE_CONTROL, /* LOST_CONTROL diff --git a/Mage/src/main/java/mage/game/events/RemoveCounterEvent.java b/Mage/src/main/java/mage/game/events/RemoveCounterEvent.java new file mode 100644 index 00000000000..b4192cb57ac --- /dev/null +++ b/Mage/src/main/java/mage/game/events/RemoveCounterEvent.java @@ -0,0 +1,29 @@ +package mage.game.events; + +import mage.abilities.Ability; +import mage.cards.Card; +import mage.players.Player; + +public class RemoveCounterEvent extends GameEvent { + + boolean isDamage; + + public RemoveCounterEvent(String name, Card targetCard, Ability source, boolean isDamage){ + super(GameEvent.EventType.REMOVE_COUNTER, targetCard.getId(), source, + (source == null ? null : source.getControllerId())); + setData(name); + this.isDamage = isDamage; + } + + public RemoveCounterEvent(String name, Player targetPlayer, Ability source, boolean isDamage){ + super(GameEvent.EventType.REMOVE_COUNTER, targetPlayer.getId(), source, + (source == null ? null : source.getControllerId())); + setData(name); + this.isDamage = isDamage; + } + + boolean counterRemovedDueToDamage(){ + return this.isDamage; + } + +} diff --git a/Mage/src/main/java/mage/game/events/RemoveCountersEvent.java b/Mage/src/main/java/mage/game/events/RemoveCountersEvent.java new file mode 100644 index 00000000000..7a108618eee --- /dev/null +++ b/Mage/src/main/java/mage/game/events/RemoveCountersEvent.java @@ -0,0 +1,33 @@ +package mage.game.events; + +import mage.abilities.Ability; +import mage.cards.Card; +import mage.players.Player; + +public class RemoveCountersEvent extends GameEvent { + + boolean isDamage; + + public RemoveCountersEvent(String name, Card targetCard, Ability source, int amount, boolean isDamage){ + super(EventType.REMOVE_COUNTERS, targetCard.getId(), source, + targetCard.getControllerOrOwnerId(), amount, false); + + if (source != null && source.getControllerId() != null) { + setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counters + } + setData(name); + this.isDamage = isDamage; + } + + public RemoveCountersEvent(String name, Player targetPlayer, Ability source, int amount, boolean isDamage){ + super(EventType.REMOVE_COUNTERS, targetPlayer.getId(), source, + (source == null ? null : source.getControllerId()), amount, false); + setData(name); + this.isDamage = isDamage; + } + + boolean counterRemovedDueToDamage(){ + return this.isDamage; + } + +} diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index d8407779588..f21686aca3d 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -1059,7 +1059,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { if (attacker != null && markDamage) { markDamage(CounterType.LOYALTY.createInstance(countersToRemove), attacker, false); } else { - removeCounters(CounterType.LOYALTY.getName(), countersToRemove, source, game); + removeCounters(CounterType.LOYALTY.getName(), countersToRemove, source, game, true); } } if (this.isBattle(game)) { @@ -1068,7 +1068,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { if (attacker != null && markDamage) { markDamage(CounterType.DEFENSE.createInstance(countersToRemove), attacker, false); } else { - removeCounters(CounterType.DEFENSE.getName(), countersToRemove, source, game); + removeCounters(CounterType.DEFENSE.getName(), countersToRemove, source, game, true); } } DamagedEvent damagedEvent = new DamagedPermanentEvent(this.getId(), attackerId, this.getControllerId(), actualDamageDone, combat); @@ -1174,15 +1174,17 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { /* Tokens don't have a spellAbility. We must make a phony one as the source so the events in addCounters * can trace the source back to an object/controller. */ - source = new SpellAbility(null, ((PermanentToken) mdi.sourceObject).name); - source.setSourceId(((PermanentToken) mdi.sourceObject).objectId); + PermanentToken sourceToken = (PermanentToken) mdi.sourceObject; + source = new SpellAbility(null, sourceToken.name); + source.setSourceId(sourceToken.objectId); + source.setControllerId(sourceToken.controllerId); } else if (mdi.sourceObject instanceof Permanent) { source = ((Permanent) mdi.sourceObject).getSpellAbility(); } if (mdi.addCounters) { addCounters(mdi.counter, game.getControllerId(mdi.sourceObject.getId()), source, game); } else { - removeCounters(mdi.counter, source, game); + removeCounters(mdi.counter, source, game, true); } } markedDamage.clear(); diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index a6a723bca8c..eb04218198a 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -1090,13 +1090,13 @@ public class Spell extends StackObjectImpl implements Card { } @Override - public void removeCounters(String name, int amount, Ability source, Game game) { - card.removeCounters(name, amount, source, game); + public void removeCounters(String name, int amount, Ability source, Game game, boolean isDamage) { + card.removeCounters(name, amount, source, game, isDamage); } @Override - public void removeCounters(Counter counter, Ability source, Game game) { - card.removeCounters(counter, source, game); + public void removeCounters(Counter counter, Ability source, Game game, boolean isDamage) { + card.removeCounters(counter, source, game, isDamage); } public Card getCard() { diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 7daffcc8fd0..2e59328526f 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2398,22 +2398,28 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void removeCounters(String name, int amount, Ability source, Game game) { + + GameEvent removeCountersEvent = new RemoveCountersEvent(name, this, source, amount, false); + if (game.replaceEvent(removeCountersEvent)){ + return; + } + int finalAmount = 0; for (int i = 0; i < amount; i++) { + + GameEvent event = new RemoveCounterEvent(name, this, source, false); + if (game.replaceEvent(event)){ + continue; + } + if (!counters.removeCounter(name, 1)) { break; } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, - getId(), source, (source == null ? null : source.getControllerId())); - event.setData(name); - event.setAmount(1); + event = new CounterRemovedEvent(name, this, source, false); game.fireEvent(event); finalAmount++; } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, - getId(), source, (source == null ? null : source.getControllerId())); - event.setData(name); - event.setAmount(finalAmount); + GameEvent event = new CountersRemovedEvent(name, this, source, finalAmount, false); game.fireEvent(event); }