From 5931ce91793d7d3be73c50b34732781bae68426f Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 17 Jan 2026 09:20:06 -0500 Subject: [PATCH] [ECL] Implement Blossombind, rework how adding counters as a cost works (#14256) * add attribute for disabling counter adding, refactor cards which use it * modify counter adding costs to check for ability to add counters, fix #13583 * [ECL] Implement Blossombind * rework implementation * remove unnecessary calls to game.processAction() * fix error * fix saga error * update preexisting tests, add new one * apply requested changes --- Mage.Sets/src/mage/cards/b/Blightbeetle.java | 52 +------- Mage.Sets/src/mage/cards/b/Blossombind.java | 122 ++++++++++++++++++ .../src/mage/cards/m/MeliraSylvokOutcast.java | 45 +------ .../src/mage/cards/m/MelirasKeepers.java | 8 +- Mage.Sets/src/mage/cards/s/Solemnity.java | 73 ++--------- Mage.Sets/src/mage/cards/s/Suncleanser.java | 58 ++++++--- Mage.Sets/src/mage/cards/t/Tatterkite.java | 9 +- Mage.Sets/src/mage/sets/LorwynEclipsed.java | 1 + .../cards/rules/MeliraSylvokOutcastTest.java | 29 ++--- .../cards/single/shm/DevotedDruidTest.java | 18 +-- .../abilities/costs/common/BlightCost.java | 19 ++- .../costs/common/PayLoyaltyCost.java | 24 ++-- .../costs/common/PutCountersSourceCost.java | 10 +- .../costs/common/PutCountersTargetCost.java | 33 +++-- .../CantHaveCountersAllEffect.java | 58 +++++++++ .../CantHaveCountersSourceEffect.java | 13 +- Mage/src/main/java/mage/cards/CardImpl.java | 7 +- .../CanHaveCounterAddedPredicate.java | 29 +++++ .../main/java/mage/game/events/GameEvent.java | 13 ++ .../java/mage/game/permanent/Permanent.java | 8 ++ .../mage/game/permanent/PermanentImpl.java | 25 +++- 21 files changed, 401 insertions(+), 253 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/b/Blossombind.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/ruleModifying/CantHaveCountersAllEffect.java create mode 100644 Mage/src/main/java/mage/filter/predicate/permanent/CanHaveCounterAddedPredicate.java diff --git a/Mage.Sets/src/mage/cards/b/Blightbeetle.java b/Mage.Sets/src/mage/cards/b/Blightbeetle.java index df92f3e7e02..f1a05f030e3 100644 --- a/Mage.Sets/src/mage/cards/b/Blightbeetle.java +++ b/Mage.Sets/src/mage/cards/b/Blightbeetle.java @@ -2,21 +2,15 @@ package mage.cards.b; import mage.MageInt; import mage.ObjectColor; -import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.common.ruleModifying.CantHaveCountersAllEffect; import mage.abilities.keyword.ProtectionAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.Outcome; import mage.constants.SubType; import mage.counters.CounterType; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.players.Player; +import mage.filter.StaticFilters; import java.util.UUID; @@ -36,7 +30,9 @@ public final class Blightbeetle extends CardImpl { this.addAbility(ProtectionAbility.from(ObjectColor.GREEN)); // Creatures your opponents control can't have +1/+1 counters put on them. - this.addAbility(new SimpleStaticAbility(new BlightbeetleEffect())); + this.addAbility(new SimpleStaticAbility(new CantHaveCountersAllEffect( + StaticFilters.FILTER_OPPONENTS_PERMANENT_CREATURES, CounterType.P1P1 + ))); } private Blightbeetle(final Blightbeetle card) { @@ -48,41 +44,3 @@ public final class Blightbeetle extends CardImpl { return new Blightbeetle(this); } } - -class BlightbeetleEffect extends ContinuousRuleModifyingEffectImpl { - - BlightbeetleEffect() { - super(Duration.WhileOnBattlefield, Outcome.Detriment); - staticText = "Creatures your opponents control can't have +1/+1 counters put on them"; - } - - private BlightbeetleEffect(final BlightbeetleEffect effect) { - super(effect); - } - - @Override - public BlightbeetleEffect copy() { - return new BlightbeetleEffect(this); - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ADD_COUNTERS; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - if (!event.getData().equals(CounterType.P1P1.getName())) { - return false; - } - Permanent permanent = game.getPermanentEntering(event.getTargetId()); - if (permanent == null) { - permanent = game.getPermanent(event.getTargetId()); - } - if (permanent == null || !permanent.isCreature(game)) { - return false; - } - Player player = game.getPlayer(permanent.getControllerId()); - return player != null && player.hasOpponent(source.getControllerId(), game); - } -} diff --git a/Mage.Sets/src/mage/cards/b/Blossombind.java b/Mage.Sets/src/mage/cards/b/Blossombind.java new file mode 100644 index 00000000000..8d70e0c0718 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/Blossombind.java @@ -0,0 +1,122 @@ +package mage.cards.b; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.TapEnchantedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class Blossombind extends CardImpl { + + public Blossombind(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget)); + + // When this Aura enters, tap enchanted creature. + this.addAbility(new EntersBattlefieldTriggeredAbility(new TapEnchantedEffect())); + + // Enchanted creature can't become untapped and can't have counters put on it. + Ability ability = new SimpleStaticAbility(new BlossombindUntapEffect()); + ability.addEffect(new BlossombindCounterEffect()); + this.addAbility(ability); + } + + private Blossombind(final Blossombind card) { + super(card); + } + + @Override + public Blossombind copy() { + return new Blossombind(this); + } +} + +class BlossombindUntapEffect extends ReplacementEffectImpl { + + BlossombindUntapEffect() { + super(Duration.WhileOnBattlefield, Outcome.Tap); + staticText = "enchanted creature can't become untapped"; + } + + private BlossombindUntapEffect(final BlossombindUntapEffect effect) { + super(effect); + } + + @Override + public BlossombindUntapEffect copy() { + return new BlossombindUntapEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + return game.getPermanent(event.getTargetId()) != null; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.UNTAP; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return source.getSourceId().equals(event.getTargetId()); + } +} + +class BlossombindCounterEffect extends ContinuousRuleModifyingEffectImpl { + + BlossombindCounterEffect() { + super(Duration.WhileOnBattlefield, Outcome.Detriment); + staticText = "and can't have counters put on it"; + } + + private BlossombindCounterEffect(final BlossombindCounterEffect effect) { + super(effect); + } + + @Override + public BlossombindCounterEffect copy() { + return new BlossombindCounterEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAN_ADD_COUNTERS; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return Optional + .ofNullable(source.getSourcePermanentIfItStillExists(game)) + .map(Permanent::getAttachedTo) + .filter(event.getTargetId()::equals) + .isPresent(); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MeliraSylvokOutcast.java b/Mage.Sets/src/mage/cards/m/MeliraSylvokOutcast.java index b2fae29c25f..1eb3e90f054 100644 --- a/Mage.Sets/src/mage/cards/m/MeliraSylvokOutcast.java +++ b/Mage.Sets/src/mage/cards/m/MeliraSylvokOutcast.java @@ -5,11 +5,13 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.ruleModifying.CantHaveCountersAllEffect; import mage.abilities.keyword.InfectAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.counters.CounterType; +import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; import mage.game.events.GameEvent; @@ -36,7 +38,9 @@ public final class MeliraSylvokOutcast extends CardImpl { this.addAbility(new SimpleStaticAbility(new MeliraSylvokOutcastEffect())); // Creatures you control can't have -1/-1 counters put on them. - this.addAbility(new SimpleStaticAbility(new MeliraSylvokOutcastEffect2())); + this.addAbility(new SimpleStaticAbility(new CantHaveCountersAllEffect( + StaticFilters.FILTER_CONTROLLED_CREATURES, CounterType.M1M1 + ))); // Creatures your opponents control lose infect. this.addAbility(new SimpleStaticAbility(new MeliraSylvokOutcastEffect3())); @@ -86,45 +90,6 @@ class MeliraSylvokOutcastEffect extends ReplacementEffectImpl { } -class MeliraSylvokOutcastEffect2 extends ReplacementEffectImpl { - - public MeliraSylvokOutcastEffect2() { - super(Duration.WhileOnBattlefield, Outcome.PreventDamage); - staticText = "Creatures you control can't have -1/-1 counters put on them"; - } - - private MeliraSylvokOutcastEffect2(final MeliraSylvokOutcastEffect2 effect) { - super(effect); - } - - @Override - public MeliraSylvokOutcastEffect2 copy() { - return new MeliraSylvokOutcastEffect2(this); - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - return true; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ADD_COUNTERS; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - if (event.getData().equals(CounterType.M1M1.getName())) { - Permanent perm = game.getPermanent(event.getTargetId()); - if (perm == null) { - perm = game.getPermanentEntering(event.getTargetId()); - } - return perm != null && perm.isCreature(game) && perm.isControlledBy(source.getControllerId()); - } - return false; - } -} - class MeliraSylvokOutcastEffect3 extends ContinuousEffectImpl { private static FilterCreaturePermanent filter = new FilterCreaturePermanent(); diff --git a/Mage.Sets/src/mage/cards/m/MelirasKeepers.java b/Mage.Sets/src/mage/cards/m/MelirasKeepers.java index 9ad3be48776..555a3746399 100644 --- a/Mage.Sets/src/mage/cards/m/MelirasKeepers.java +++ b/Mage.Sets/src/mage/cards/m/MelirasKeepers.java @@ -1,7 +1,5 @@ - package mage.cards.m; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.ruleModifying.CantHaveCountersSourceEffect; @@ -9,16 +7,16 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; + +import java.util.UUID; /** - * * @author BetaSteward_at_googlemail.com */ public final class MelirasKeepers extends CardImpl { public MelirasKeepers(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}"); this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.WARRIOR); diff --git a/Mage.Sets/src/mage/cards/s/Solemnity.java b/Mage.Sets/src/mage/cards/s/Solemnity.java index 7ca616615ca..8ea34c25e80 100644 --- a/Mage.Sets/src/mage/cards/s/Solemnity.java +++ b/Mage.Sets/src/mage/cards/s/Solemnity.java @@ -1,21 +1,18 @@ package mage.cards.s; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.ruleModifying.CantHaveCountersAllEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.filter.FilterPermanent; import mage.filter.predicate.Predicates; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; -import mage.game.permanent.Permanent; import mage.players.Player; import java.util.UUID; @@ -25,6 +22,17 @@ import java.util.UUID; */ public final class Solemnity extends CardImpl { + private static final FilterPermanent filter = new FilterPermanent(); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate(), + CardType.ENCHANTMENT.getPredicate(), + CardType.LAND.getPredicate() + )); + } + public Solemnity(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}"); @@ -32,7 +40,8 @@ public final class Solemnity extends CardImpl { this.addAbility(new SimpleStaticAbility(new SolemnityEffect())); // Counters can't be put on artifacts, creatures, enchantments, or lands. - this.addAbility(new SimpleStaticAbility(new SolemnityEffect2())); + this.addAbility(new SimpleStaticAbility(new CantHaveCountersAllEffect(filter, null) + .setText("counters can't be put on artifacts, creatures, enchantments, or lands"))); } private Solemnity(final Solemnity card) { @@ -77,57 +86,3 @@ class SolemnityEffect extends ReplacementEffectImpl { return player != null; } } - -class SolemnityEffect2 extends ReplacementEffectImpl { - - private static final FilterPermanent filter = new FilterPermanent(); - - static { - filter.add(Predicates.or( - CardType.ARTIFACT.getPredicate(), - CardType.CREATURE.getPredicate(), - CardType.ENCHANTMENT.getPredicate(), - CardType.LAND.getPredicate())); - } - - public SolemnityEffect2() { - super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "Counters can't be put on artifacts, creatures, enchantments, or lands"; - } - - private SolemnityEffect2(final SolemnityEffect2 effect) { - super(effect); - } - - @Override - public SolemnityEffect2 copy() { - return new SolemnityEffect2(this); - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - return true; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ADD_COUNTERS; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - MageObject object = game.getObject(event.getTargetId()); - Permanent permanent1 = game.getPermanentEntering(event.getTargetId()); - Permanent permanent2 = game.getPermanent(event.getTargetId()); - - if (object instanceof Permanent) { - return filter.match((Permanent) object, game); - } else if (permanent1 != null) { - return filter.match(permanent1, game); - } else if (permanent2 != null) { - return filter.match(permanent2, game); - } - - return false; - } -} diff --git a/Mage.Sets/src/mage/cards/s/Suncleanser.java b/Mage.Sets/src/mage/cards/s/Suncleanser.java index a0eff681763..0de1bb76b51 100644 --- a/Mage.Sets/src/mage/cards/s/Suncleanser.java +++ b/Mage.Sets/src/mage/cards/s/Suncleanser.java @@ -37,15 +37,13 @@ public final class Suncleanser extends CardImpl { // When Suncleanser enters the battlefield, choose one — // • Remove all counters from target creature. It can't have counters put on it for as long as Suncleanser remains on the battlefield. - Ability ability = new EntersBattlefieldTriggeredAbility( - new RemoveAllCountersPermanentTargetEffect(), false - ); - ability.addEffect(new SuncleanserPreventCountersEffect(false)); + Ability ability = new EntersBattlefieldTriggeredAbility(new RemoveAllCountersPermanentTargetEffect()); + ability.addEffect(new SuncleanserPreventCountersPermanentEffect()); ability.addTarget(new TargetCreaturePermanent()); // • Target opponent loses all counters. That player can't get counters for as long as Suncleanser remains on the battlefield. Mode mode = new Mode(new SuncleanserRemoveCountersPlayerEffect()); - mode.addEffect(new SuncleanserPreventCountersEffect(true)); + mode.addEffect(new SuncleanserPreventCountersPlayerEffect()); mode.addTarget(new TargetOpponent()); ability.addMode(mode); this.addAbility(ability); @@ -88,24 +86,20 @@ class SuncleanserRemoveCountersPlayerEffect extends OneShotEffect { } } -class SuncleanserPreventCountersEffect extends ContinuousRuleModifyingEffectImpl { +class SuncleanserPreventCountersPlayerEffect extends ContinuousRuleModifyingEffectImpl { - SuncleanserPreventCountersEffect(boolean player) { - super(Duration.WhileOnBattlefield, Outcome.Detriment); - if (player) { - staticText = "That player can't get counters for as long as {this} remains on the battlefield."; - } else { - staticText = "It can't have counters put on it for as long as {this} remains on the battlefield"; - } + SuncleanserPreventCountersPlayerEffect() { + super(Duration.UntilSourceLeavesBattlefield, Outcome.Detriment); + staticText = "That player can't get counters for as long as {this} remains on the battlefield."; } - private SuncleanserPreventCountersEffect(final SuncleanserPreventCountersEffect effect) { + private SuncleanserPreventCountersPlayerEffect(final SuncleanserPreventCountersPlayerEffect effect) { super(effect); } @Override - public SuncleanserPreventCountersEffect copy() { - return new SuncleanserPreventCountersEffect(this); + public SuncleanserPreventCountersPlayerEffect copy() { + return new SuncleanserPreventCountersPlayerEffect(this); } @Override @@ -126,3 +120,35 @@ class SuncleanserPreventCountersEffect extends ContinuousRuleModifyingEffectImpl return true; } } + +class SuncleanserPreventCountersPermanentEffect extends ContinuousRuleModifyingEffectImpl { + + SuncleanserPreventCountersPermanentEffect() { + super(Duration.UntilSourceLeavesBattlefield, Outcome.Detriment); + staticText = "It can't have counters put on it for as long as {this} remains on the battlefield"; + } + + private SuncleanserPreventCountersPermanentEffect(final SuncleanserPreventCountersPermanentEffect effect) { + super(effect); + } + + @Override + public SuncleanserPreventCountersPermanentEffect copy() { + return new SuncleanserPreventCountersPermanentEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAN_ADD_COUNTERS; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent != null) { + return true; + } + discard(); + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/t/Tatterkite.java b/Mage.Sets/src/mage/cards/t/Tatterkite.java index 88845ec0d43..ad0c8decc40 100644 --- a/Mage.Sets/src/mage/cards/t/Tatterkite.java +++ b/Mage.Sets/src/mage/cards/t/Tatterkite.java @@ -1,7 +1,5 @@ - package mage.cards.t; -import java.util.UUID; import mage.MageInt; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.ruleModifying.CantHaveCountersSourceEffect; @@ -10,16 +8,16 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; + +import java.util.UUID; /** - * * @author jeffwadsworth */ public final class Tatterkite extends CardImpl { public Tatterkite(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT,CardType.CREATURE},"{3}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}"); this.subtype.add(SubType.SCARECROW); this.power = new MageInt(2); this.toughness = new MageInt(1); @@ -29,7 +27,6 @@ public final class Tatterkite extends CardImpl { // Tatterkite can't have counters put on it. this.addAbility(new SimpleStaticAbility(new CantHaveCountersSourceEffect())); - } private Tatterkite(final Tatterkite card) { diff --git a/Mage.Sets/src/mage/sets/LorwynEclipsed.java b/Mage.Sets/src/mage/sets/LorwynEclipsed.java index ea6e5a74569..7786292b08e 100644 --- a/Mage.Sets/src/mage/sets/LorwynEclipsed.java +++ b/Mage.Sets/src/mage/sets/LorwynEclipsed.java @@ -58,6 +58,7 @@ public final class LorwynEclipsed extends ExpansionSet { cards.add(new SetCardInfo("Bloom Tender", 324, Rarity.MYTHIC, mage.cards.b.BloomTender.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Bloom Tender", 390, Rarity.MYTHIC, mage.cards.b.BloomTender.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Bloom Tender", 400, Rarity.MYTHIC, mage.cards.b.BloomTender.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Blossombind", 45, Rarity.COMMON, mage.cards.b.Blossombind.class)); cards.add(new SetCardInfo("Blossoming Defense", 167, Rarity.UNCOMMON, mage.cards.b.BlossomingDefense.class)); cards.add(new SetCardInfo("Boggart Cursecrafter", 206, Rarity.UNCOMMON, mage.cards.b.BoggartCursecrafter.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Boggart Cursecrafter", 331, Rarity.UNCOMMON, mage.cards.b.BoggartCursecrafter.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/rules/MeliraSylvokOutcastTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/rules/MeliraSylvokOutcastTest.java index 84ab8a6f86b..4c9ef97dce1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/rules/MeliraSylvokOutcastTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/rules/MeliraSylvokOutcastTest.java @@ -1,15 +1,11 @@ - package org.mage.test.cards.rules; import mage.constants.PhaseStep; import mage.constants.Zone; -import mage.counters.CounterType; -import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ @@ -25,28 +21,19 @@ public class MeliraSylvokOutcastTest extends CardTestPlayerBase { // You can't get poison counters. // Creatures you control can't have -1/-1 counters placed on them. // Creatures your opponents control lose infect. - addCard(Zone.BATTLEFIELD, playerA, "Melira, Sylvok Outcast", 2); // 2/2 + addCard(Zone.BATTLEFIELD, playerA, "Melira, Sylvok Outcast"); // 2/2 // {T}: Add {G}. // Put a -1/-1 counter on Devoted Druid: Untap Devoted Druid. addCard(Zone.BATTLEFIELD, playerA, "Devoted Druid", 1); // 0/2 - activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}"); - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Put a -1/-1 counter on"); + checkPlayableAbility("can't put counters", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {G}", false); + } - setStopAt(1, PhaseStep.BEGIN_COMBAT); + @Test + public void testBlight() { + addCard(Zone.BATTLEFIELD, playerA, "Gristle Glutton"); + addCard(Zone.BATTLEFIELD, playerA, "Melira, Sylvok Outcast"); - // TODO: improve PutCountersSourceCost, so it can find real playable ability here instead restriction - try { - execute(); - Assert.fail("must throw exception on execute"); - } catch (Throwable e) { - if (!e.getMessage().contains("Put a -1/-1 counter on")) { - Assert.fail("Needed error about not being able to use the Devoted Druid's -1/-1 ability, but got:\n" + e.getMessage()); - } - } - - assertPowerToughness(playerA, "Devoted Druid", 0, 2); - assertCounterCount("Devoted Druid", CounterType.M1M1, 0); - assertTapped("Devoted Druid", true); // Because untapping can't be paid + checkPlayableAbility("can't put counters", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T},", false); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/shm/DevotedDruidTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/shm/DevotedDruidTest.java index cbe3b5ef5cf..5250c37d77a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/shm/DevotedDruidTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/shm/DevotedDruidTest.java @@ -2,8 +2,6 @@ package org.mage.test.cards.single.shm; import mage.constants.PhaseStep; import mage.constants.Zone; -import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -46,24 +44,10 @@ public class DevotedDruidTest extends CardTestPlayerBase { // ... // (2018-12-07) - //checkPlayableAbility("can't put counters", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Put a -1/-1", false); - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Put a -1/-1"); - - setStopAt(1, PhaseStep.END_TURN); - // TODO: improve PutCountersSourceCost, so it can find real playable ability here instead restriction - try { - setStrictChooseMode(true); - execute(); - Assert.fail("must throw exception on execute"); - } catch (Throwable e) { - if (!e.getMessage().contains("Put a -1/-1")) { - Assert.fail("Needed error about not being able to use the Devoted Druid's -1/-1 ability, but got:\n" + e.getMessage()); - } - } + checkPlayableAbility("can't put counters", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Put a -1/-1", false); } @Test - @Ignore // TODO: must fix, see #13583 public void test_PutCounter_ModifiedToZeroCounters() { // {T}: Add {G}. // Put a -1/-1 counter on this creature: Untap this creature. diff --git a/Mage/src/main/java/mage/abilities/costs/common/BlightCost.java b/Mage/src/main/java/mage/abilities/costs/common/BlightCost.java index 79c78b85255..87537049d24 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/BlightCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/BlightCost.java @@ -5,12 +5,13 @@ import mage.abilities.costs.Cost; import mage.abilities.costs.CostImpl; import mage.constants.Outcome; import mage.counters.CounterType; -import mage.filter.StaticFilters; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.permanent.CanHaveCounterAddedPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.TargetPermanent; -import mage.target.common.TargetControlledCreaturePermanent; import java.util.UUID; @@ -19,6 +20,12 @@ import java.util.UUID; */ public class BlightCost extends CostImpl { + private static final FilterPermanent filter = new FilterControlledCreaturePermanent(); + + static { + filter.add(new CanHaveCounterAddedPredicate(CounterType.M1M1)); + } + private int amount; public BlightCost(int amount) { @@ -45,7 +52,7 @@ public class BlightCost extends CostImpl { public static boolean canBlight(UUID controllerId, Game game, Ability source) { return game .getBattlefield() - .contains(StaticFilters.FILTER_CONTROLLED_CREATURE, controllerId, source, game, 1); + .contains(filter, controllerId, source, game, 1); } @Override @@ -56,12 +63,10 @@ public class BlightCost extends CostImpl { } public static Permanent doBlight(Player player, int amount, Game game, Ability source) { - if (player == null || amount < 1 || !game.getBattlefield().contains( - StaticFilters.FILTER_CONTROLLED_CREATURE, player.getId(), source, game, 1 - )) { + if (player == null || amount < 1 || !canBlight(player.getId(), game, source)) { return null; } - TargetPermanent target = new TargetControlledCreaturePermanent(); + TargetPermanent target = new TargetPermanent(filter); target.withNotTarget(true); target.withChooseHint("to put a -1/-1 counter on"); player.choose(Outcome.UnboostCreature, target, source, game); diff --git a/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java b/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java index fff0baae6bb..b8d208df0b0 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java @@ -48,7 +48,9 @@ public class PayLoyaltyCost extends CostImpl { } } - return planeswalker.getCounters(game).getCount(CounterType.LOYALTY) + loyaltyCost >= 0 && planeswalker.canLoyaltyBeUsed(game); + return planeswalker.getCounters(game).getCount(CounterType.LOYALTY) + loyaltyCost >= 0 + && planeswalker.canLoyaltyBeUsed(game) + && (loyaltyCost <= 0 || planeswalker.canHaveCounterAdded(CounterType.LOYALTY, game, source)); } /** @@ -61,15 +63,19 @@ public class PayLoyaltyCost extends CostImpl { @Override public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { Permanent planeswalker = game.getPermanent(source.getSourceId()); - if (planeswalker != null && planeswalker.getCounters(game).getCount(CounterType.LOYALTY) + amount >= 0 && planeswalker.canLoyaltyBeUsed(game)) { - if (amount > 0) { - planeswalker.addCounters(CounterType.LOYALTY.createInstance(amount), source.getControllerId(), ability, game, false); - } else if (amount < 0) { - planeswalker.removeCounters(CounterType.LOYALTY.getName(), Math.abs(amount), source, game); - } - planeswalker.addLoyaltyUsed(); - this.paid = true; + if (planeswalker == null + || planeswalker.getCounters(game).getCount(CounterType.LOYALTY) + amount < 0 + || !planeswalker.canLoyaltyBeUsed(game) + || (amount > 0 && !planeswalker.canHaveCounterAdded(CounterType.LOYALTY, game, source))) { + return paid; } + if (amount > 0) { + planeswalker.addCounters(CounterType.LOYALTY.createInstance(amount), source.getControllerId(), ability, game, false); + } else if (amount < 0) { + planeswalker.removeCounters(CounterType.LOYALTY.getName(), Math.abs(amount), source, game); + } + planeswalker.addLoyaltyUsed(); + this.paid = true; return paid; } diff --git a/Mage/src/main/java/mage/abilities/costs/common/PutCountersSourceCost.java b/Mage/src/main/java/mage/abilities/costs/common/PutCountersSourceCost.java index daf16a82915..b276840da81 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PutCountersSourceCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PutCountersSourceCost.java @@ -7,6 +7,7 @@ import mage.counters.Counter; import mage.game.Game; import mage.game.permanent.Permanent; +import java.util.Optional; import java.util.UUID; /** @@ -28,15 +29,18 @@ public class PutCountersSourceCost extends CostImpl { @Override public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - // TODO: implement permanent.canAddCounters with replacement events check, see tests with Devoted Druid - return true; + return Optional + .ofNullable(source.getSourcePermanentIfItStillExists(game)) + .filter(permanent -> permanent.canHaveCounterAdded(counter, game, source)) + .isPresent(); } @Override public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { Permanent permanent = game.getPermanent(source.getSourceId()); if (permanent != null) { - this.paid = permanent.addCounters(counter, controllerId, ability, game, false); + permanent.addCounters(counter, controllerId, ability, game, false); + this.paid = true; } return paid; } diff --git a/Mage/src/main/java/mage/abilities/costs/common/PutCountersTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/PutCountersTargetCost.java index cb3cbdb36be..c8b1ed6d890 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PutCountersTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PutCountersTargetCost.java @@ -5,10 +5,12 @@ import mage.abilities.costs.Cost; import mage.abilities.costs.CostImpl; import mage.constants.Outcome; import mage.counters.Counter; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.permanent.CanHaveCounterAddedPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; -import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetControlledPermanent; import java.util.UUID; @@ -20,12 +22,19 @@ public class PutCountersTargetCost extends CostImpl { private final Counter counter; - public PutCountersTargetCost(Counter counter){ - this(counter, new TargetControlledCreaturePermanent()); + private static FilterControlledPermanent makeFilter(FilterControlledPermanent filter, Counter counter) { + FilterControlledPermanent newFilter = filter.copy(); + newFilter.add(new CanHaveCounterAddedPredicate(counter)); + return newFilter; } - public PutCountersTargetCost(Counter counter, TargetControlledPermanent target) { + public PutCountersTargetCost(Counter counter) { + this(counter, StaticFilters.FILTER_CONTROLLED_CREATURE); + } + + public PutCountersTargetCost(Counter counter, FilterControlledPermanent filter) { this.counter = counter.copy(); + TargetControlledPermanent target = new TargetControlledPermanent(makeFilter(filter, counter)); target.withNotTarget(true); this.addTarget(target); this.text = "put " + counter.getDescription() + " on " + target.getDescription(); @@ -40,6 +49,11 @@ public class PutCountersTargetCost extends CostImpl { return new PutCountersTargetCost(this); } + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + return canChooseOrAlreadyChosen(ability, source, controllerId, game); + } + @Override public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { Player player = game.getPlayer(ability.getControllerId()); @@ -49,15 +63,12 @@ public class PutCountersTargetCost extends CostImpl { for (UUID targetId : this.getTargets().get(0).getTargets()) { Permanent permanent = game.getPermanent(targetId); if (permanent == null) { - return false; + paid = false; + return paid; } - paid |= permanent.addCounters(counter, controllerId, ability, game); + permanent.addCounters(counter, controllerId, ability, game); + paid = true; } return paid; } - - @Override - public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return canChooseOrAlreadyChosen(ability, source, controllerId, game); - } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/ruleModifying/CantHaveCountersAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ruleModifying/CantHaveCountersAllEffect.java new file mode 100644 index 00000000000..f1660b47835 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/ruleModifying/CantHaveCountersAllEffect.java @@ -0,0 +1,58 @@ + +package mage.abilities.effects.common.ruleModifying; + +import mage.abilities.Ability; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; + +/** + * @author TheElk801 + */ +public class CantHaveCountersAllEffect extends ContinuousRuleModifyingEffectImpl { + + private final FilterPermanent filter; + private final CounterType counterType; + + public CantHaveCountersAllEffect(FilterPermanent filter, CounterType counterType) { + super(Duration.WhileOnBattlefield, Outcome.Detriment); + this.filter = filter; + this.counterType = counterType; + staticText = filter.getMessage() + " can't have " + + (counterType != null ? counterType.getName() + ' ' : "") + + "counters put on them"; + } + + protected CantHaveCountersAllEffect(final CantHaveCountersAllEffect effect) { + super(effect); + this.filter = effect.filter; + this.counterType = effect.counterType; + } + + @Override + public CantHaveCountersAllEffect copy() { + return new CantHaveCountersAllEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAN_ADD_COUNTERS; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (counterType != null && !counterType.getName().equals(event.getData())) { + return false; + } + Permanent permanent = game.getPermanent(event.getTargetId()); + if (permanent == null) { + permanent = game.getPermanentEntering(event.getTargetId()); + } + return permanent != null && filter.match(permanent, source.getControllerId(), source, game); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/ruleModifying/CantHaveCountersSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ruleModifying/CantHaveCountersSourceEffect.java index 11265c18383..d4ebc4d5fee 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ruleModifying/CantHaveCountersSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ruleModifying/CantHaveCountersSourceEffect.java @@ -1,8 +1,5 @@ - package mage.abilities.effects.common.ruleModifying; -import java.util.UUID; - import mage.abilities.Ability; import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.constants.Duration; @@ -10,6 +7,8 @@ import mage.constants.Outcome; import mage.game.Game; import mage.game.events.GameEvent; +import java.util.Objects; + /** * @author Styxo */ @@ -31,15 +30,11 @@ public class CantHaveCountersSourceEffect extends ContinuousRuleModifyingEffectI @Override public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ADD_COUNTERS; + return event.getType() == GameEvent.EventType.CAN_ADD_COUNTERS; } @Override public boolean applies(GameEvent event, Ability source, Game game) { - UUID sourceId = source != null ? source.getSourceId() : null; - if (sourceId != null) { - return sourceId.equals(event.getTargetId()); - } - return false; + return source != null && Objects.equals(source.getSourceId(), event.getTargetId()); } } diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index fe7e0b658b0..058c237f272 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -758,8 +758,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card { } public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List appliedEffects, boolean isEffect, int maxCounters) { - if (this instanceof Permanent && !((Permanent) this).isPhasedIn()) { - return false; + if (this instanceof Permanent) { + Permanent permanent = (Permanent) this; + if (!permanent.isPhasedIn() || !permanent.canHaveCounterAdded(counter, game, source)) { + return false; + } } boolean returnCode = true; diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/CanHaveCounterAddedPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/CanHaveCounterAddedPredicate.java new file mode 100644 index 00000000000..4f08d6b8632 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/permanent/CanHaveCounterAddedPredicate.java @@ -0,0 +1,29 @@ +package mage.filter.predicate.permanent; + +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * @author TheElk801 + */ +public class CanHaveCounterAddedPredicate implements ObjectSourcePlayerPredicate { + + private final CounterType counterType; + + public CanHaveCounterAddedPredicate(Counter counter) { + this(CounterType.findByName(counter.getName())); + } + + public CanHaveCounterAddedPredicate(CounterType counterType) { + this.counterType = counterType; + } + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return input.getObject().canHaveCounterAdded(counterType, game, input.getSource()); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 47cc932a337..79ecb9f566a 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -546,6 +546,19 @@ public class GameEvent implements Serializable { flag not used for this event */ STAY_ATTACHED, + /* CAN_ADD_COUNTERS + ADD_COUNTER, COUNTER_ADDED, + ADD_COUNTERS, COUNTERS_ADDED, + targetId id of the permanent or player getting counter(s) + sourceId id of the ability adding them + playerId player who is adding the counter(s) + amount number of counters being added + data name of the counter(s) being added + + NOTE: only use CAN_ADD_COUNTERS to check whether a permanent can have counters added (e.g. for paying a cost), + otherwise use ADD_COUNTER/ADD_COUNTERS to modify how many counters are added (e.g. doubling or reducing) + */ + CAN_ADD_COUNTERS, ADD_COUNTER, COUNTER_ADDED, ADD_COUNTERS, COUNTERS_ADDED, /* REMOVE_COUNTER, REMOVE_COUNTERS, COUNTER_REMOVED, COUNTERS_REMOVED diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 97ef2ba3491..032dc9ff1de 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -5,6 +5,8 @@ import mage.MageObjectReference; import mage.abilities.Ability; import mage.cards.Card; import mage.constants.Zone; +import mage.counters.Counter; +import mage.counters.CounterType; import mage.game.Controllable; import mage.game.Game; import mage.game.GameState; @@ -109,6 +111,12 @@ public interface Permanent extends Card, Controllable { boolean canBeSacrificed(); + boolean canHaveAnyCounterAdded(Game game, Ability source); + + boolean canHaveCounterAdded(Counter counter, Game game, Ability source); + + boolean canHaveCounterAdded(CounterType counterType, Game game, Ability source); + void setCardNumber(String cid); void setExpansionSetCode(String expansionSetCode); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 1b096877cdc..d23b9cac67a 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -7,6 +7,7 @@ import mage.ObjectColor; import mage.abilities.Abilities; import mage.abilities.Ability; import mage.abilities.SpellAbility; +import mage.abilities.common.RoomAbility; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.RequirementEffect; @@ -17,7 +18,6 @@ import mage.abilities.hint.HintUtils; import mage.abilities.keyword.*; import mage.cards.Card; import mage.cards.CardImpl; -import mage.abilities.common.RoomAbility; import mage.constants.*; import mage.counters.Counter; import mage.counters.CounterType; @@ -1867,6 +1867,29 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { return canBeSacrificed; } + @Override + public boolean canHaveAnyCounterAdded(Game game, Ability source) { + return this.canHaveCounterAdded((CounterType) null, 1, game, source); + } + + @Override + public boolean canHaveCounterAdded(Counter counter, Game game, Ability source) { + return this.canHaveCounterAdded(CounterType.findByName(counter.getName()), counter.getCount(), game, source); + } + + @Override + public boolean canHaveCounterAdded(CounterType counterType, Game game, Ability source) { + return this.canHaveCounterAdded(counterType, 1, game, source); + } + + protected boolean canHaveCounterAdded(CounterType counterType, int amount, Game game, Ability source) { + return !game.replaceEvent(GameEvent.getEvent( + EventType.CAN_ADD_COUNTERS, objectId, source, + source != null ? source.getControllerId() : game.getActivePlayerId(), + counterType != null ? counterType.getName() : "", amount + )); + } + @Override public void setPairedCard(MageObjectReference pairedCard) { this.pairedPermanent = pairedCard;