[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
This commit is contained in:
Evan Kranzler 2026-01-17 09:20:06 -05:00 committed by GitHub
parent 5b4a1618f9
commit 5931ce9179
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 401 additions and 253 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -758,8 +758,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
}
public boolean addCounters(Counter counter, UUID playerAddingCounters, Ability source, Game game, List<UUID> 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;

View file

@ -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<Permanent> {
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<Permanent> input, Game game) {
return input.getObject().canHaveCounterAdded(counterType, game, input.getSource());
}
}

View file

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

View file

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

View file

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