diff --git a/Mage.Sets/src/mage/cards/i/ImodaneThePyrohammer.java b/Mage.Sets/src/mage/cards/i/ImodaneThePyrohammer.java new file mode 100644 index 00000000000..ea496f245f8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/ImodaneThePyrohammer.java @@ -0,0 +1,143 @@ +package mage.cards.i; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DamagePlayersEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValuePositiveHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterSpell; +import mage.filter.StaticFilters; +import mage.filter.predicate.other.HasOnlySingleTargetPermanentPredicate; +import mage.game.Game; +import mage.game.events.DamagedPermanentBatchEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.StackObject; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class ImodaneThePyrohammer extends CardImpl { + + public ImodaneThePyrohammer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.KNIGHT); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Whenever an instant or sorcery spell you control that targets only a single creature deals damage to that creature, Imodane deals that much damage to each opponent. + this.addAbility(new ImodaneThePyrohammerTriggeredAbility()); + } + + private ImodaneThePyrohammer(final ImodaneThePyrohammer card) { + super(card); + } + + @Override + public ImodaneThePyrohammer copy() { + return new ImodaneThePyrohammer(this); + } +} + +class ImodaneThePyrohammerTriggeredAbility extends TriggeredAbilityImpl { + + private static final FilterSpell filter = new FilterSpell("instant or sorcery spell you control that targets only a single creature"); + + static { + filter.add(TargetController.YOU.getControllerPredicate()); + filter.add(new HasOnlySingleTargetPermanentPredicate(StaticFilters.FILTER_PERMANENT_CREATURE)); + } + + private static final Hint hint = new ValuePositiveHint("Damage dealt to the target", ImodaneThePyrohammerDynamicValue.instance); + + ImodaneThePyrohammerTriggeredAbility() { + super(Zone.BATTLEFIELD, new DamagePlayersEffect(Outcome.Damage, ImodaneThePyrohammerDynamicValue.instance, TargetController.OPPONENT), false); + setTriggerPhrase("Whenever an instant or sorcery spell you control that targets only a single creature deals damage to that creature, "); + addHint(hint); + } + + private ImodaneThePyrohammerTriggeredAbility(final ImodaneThePyrohammerTriggeredAbility ability) { + super(ability); + } + + @Override + public ImodaneThePyrohammerTriggeredAbility copy() { + return new ImodaneThePyrohammerTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT_BATCH; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + DamagedPermanentBatchEvent dEvent = (DamagedPermanentBatchEvent) event; + int damage = dEvent + .getEvents() + .stream() + .filter(damagedEvent -> { + MageObject sourceObject = game.getObject(damagedEvent.getSourceId()); + Permanent target = game.getPermanentOrLKIBattlefield(damagedEvent.getTargetId()); + // We keep only the events + // 1/ That have sourceId matching the spell filter + // 2/ That have targetId as the spell's only target + return sourceObject != null && target != null + && sourceObject instanceof StackObject + && filter.match((StackObject) sourceObject, controllerId, this, game) + && target.getId().equals(((StackObject) sourceObject).getStackAbility().getFirstTarget()); + }) + .mapToInt(GameEvent::getAmount) + .sum(); + if (damage < 1) { + return false; + } + + this.getEffects().setValue(ImodaneThePyrohammerDynamicValue.IMODANE_VALUE_KEY, damage); + return true; + } +} + +enum ImodaneThePyrohammerDynamicValue implements DynamicValue { + instance; + + static final String IMODANE_VALUE_KEY = "Imodane-Damage-Amount"; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + StackObject source = game.getStack().getStackObject(sourceAbility.getSourceId()); + if (source == null) { + return 0; + } + + Integer value = (Integer) sourceAbility.getEffects().get(0).getValue(IMODANE_VALUE_KEY); + return value == null ? 0 : value; + } + + @Override + public ImodaneThePyrohammerDynamicValue copy() { + return this; + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return "that much damage"; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/i/IvyGleefulSpellthief.java b/Mage.Sets/src/mage/cards/i/IvyGleefulSpellthief.java index 8286148b5da..ac070bb8829 100644 --- a/Mage.Sets/src/mage/cards/i/IvyGleefulSpellthief.java +++ b/Mage.Sets/src/mage/cards/i/IvyGleefulSpellthief.java @@ -12,18 +12,18 @@ import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.SuperType; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectSourcePlayer; -import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.StaticFilters; import mage.filter.predicate.mageobject.MageObjectReferencePredicate; +import mage.filter.predicate.other.HasOnlySingleTargetPermanentPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.game.stack.StackObject; -import mage.target.Target; -import mage.util.TargetAddress; import mage.util.functions.StackObjectCopyApplier; -import java.util.*; +import java.util.Arrays; +import java.util.Iterator; +import java.util.UUID; /** * @author TheElk801 @@ -34,7 +34,7 @@ public final class IvyGleefulSpellthief extends CardImpl { = new FilterSpell("a spell that targets only a single creature other than {this}"); static { - filter.add(IvyGleefulSpellthiefPredicate.instance); + filter.add(new HasOnlySingleTargetPermanentPredicate(StaticFilters.FILTER_ANOTHER_CREATURE)); } public IvyGleefulSpellthief(UUID ownerId, CardSetInfo setInfo) { @@ -63,36 +63,6 @@ public final class IvyGleefulSpellthief extends CardImpl { } } -enum IvyGleefulSpellthiefPredicate implements ObjectSourcePlayerPredicate { - instance; - - @Override - public boolean apply(ObjectSourcePlayer input, Game game) { - Spell spell = input.getObject(); - if (spell == null) { - return false; - } - UUID singleTarget = null; - for (TargetAddress addr : TargetAddress.walk(spell)) { - Target targetInstance = addr.getTarget(spell); - for (UUID targetId : targetInstance.getTargets()) { - if (singleTarget == null) { - singleTarget = targetId; - } else if (!singleTarget.equals(targetId)) { - return false; - } - } - } - if (singleTarget == null) { - return false; - } - Permanent permanent = game.getPermanent(singleTarget); - return permanent != null - && permanent.isCreature(game) - && !permanent.getId().equals(input.getSourceId()); - } -} - class IvyGleefulSpellthiefEffect extends OneShotEffect { private static final class IvyGleefulSpellthiefApplier implements StackObjectCopyApplier { diff --git a/Mage.Sets/src/mage/cards/m/MuckDrubb.java b/Mage.Sets/src/mage/cards/m/MuckDrubb.java index 4cddfdb676c..bb2d8f2c68c 100644 --- a/Mage.Sets/src/mage/cards/m/MuckDrubb.java +++ b/Mage.Sets/src/mage/cards/m/MuckDrubb.java @@ -1,11 +1,10 @@ package mage.cards.m; -import java.util.UUID; import mage.MageInt; -import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.common.ChangeATargetOfTargetSpellAbilityToSourceEffect; import mage.abilities.keyword.FlashAbility; @@ -15,15 +14,12 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.filter.FilterSpell; -import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.ObjectSourcePlayer; -import mage.filter.predicate.ObjectSourcePlayerPredicate; -import mage.filter.predicate.mageobject.TargetsPermanentPredicate; -import mage.game.Game; -import mage.game.stack.Spell; +import mage.filter.StaticFilters; +import mage.filter.predicate.other.HasOnlySingleTargetPermanentPredicate; import mage.target.Target; import mage.target.TargetSpell; -import mage.util.TargetAddress; + +import java.util.UUID; /** * @@ -34,8 +30,7 @@ public final class MuckDrubb extends CardImpl { protected static final FilterSpell filter = new FilterSpell("spell that targets only a single creature"); static { - filter.add(new SpellWithOnlySingleTargetPredicate()); - filter.add(new TargetsPermanentPredicate(new FilterCreaturePermanent())); + filter.add(new HasOnlySingleTargetPermanentPredicate(StaticFilters.FILTER_PERMANENT_CREATURE)); } public MuckDrubb(UUID ownerId, CardSetInfo setInfo) { @@ -67,27 +62,4 @@ public final class MuckDrubb extends CardImpl { public MuckDrubb copy() { return new MuckDrubb(this); } -} - -class SpellWithOnlySingleTargetPredicate implements ObjectSourcePlayerPredicate { - - @Override - public boolean apply(ObjectSourcePlayer input, Game game) { - Spell spell = input.getObject(); - if (spell == null) { - return false; - } - UUID singleTarget = null; - for (TargetAddress addr : TargetAddress.walk(spell)) { - Target targetInstance = addr.getTarget(spell); - for (UUID targetId : targetInstance.getTargets()) { - if (singleTarget == null) { - singleTarget = targetId; - } else if (!singleTarget.equals(targetId)) { - return false; - } - } - } - return singleTarget != null; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/PrecursorGolem.java b/Mage.Sets/src/mage/cards/p/PrecursorGolem.java index 14055e2644d..ec591676645 100644 --- a/Mage.Sets/src/mage/cards/p/PrecursorGolem.java +++ b/Mage.Sets/src/mage/cards/p/PrecursorGolem.java @@ -59,6 +59,7 @@ public final class PrecursorGolem extends CardImpl { } class PrecursorGolemCopyTriggeredAbility extends TriggeredAbilityImpl { + // TODO: could be reworked to use SpellCastAllTriggeredAbility like Vesuvan Duplimancy. PrecursorGolemCopyTriggeredAbility() { super(Zone.BATTLEFIELD, new PrecursorGolemCopySpellEffect(), false); diff --git a/Mage.Sets/src/mage/cards/v/VesuvanDuplimancy.java b/Mage.Sets/src/mage/cards/v/VesuvanDuplimancy.java index 0c3afd1b496..8ac363f22e8 100644 --- a/Mage.Sets/src/mage/cards/v/VesuvanDuplimancy.java +++ b/Mage.Sets/src/mage/cards/v/VesuvanDuplimancy.java @@ -8,14 +8,15 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; +import mage.filter.FilterPermanent; import mage.filter.FilterSpell; -import mage.filter.predicate.ObjectSourcePlayer; -import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.other.HasOnlySingleTargetPermanentPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.stack.Spell; import mage.target.Target; -import mage.util.TargetAddress; import java.util.Collection; import java.util.Objects; @@ -29,8 +30,14 @@ public final class VesuvanDuplimancy extends CardImpl { private static final FilterSpell filter = new FilterSpell("a spell that targets only a single artifact or creature you control"); + private static final FilterPermanent subfilter = new FilterControlledPermanent("artifact or creature you control"); + static { - filter.add(VesuvanDuplimancyPredicate.instance); + subfilter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate() + )); + filter.add(new HasOnlySingleTargetPermanentPredicate(subfilter)); } public VesuvanDuplimancy(UUID ownerId, CardSetInfo setInfo) { @@ -50,36 +57,6 @@ public final class VesuvanDuplimancy extends CardImpl { } } -enum VesuvanDuplimancyPredicate implements ObjectSourcePlayerPredicate { - instance; - - @Override - public boolean apply(ObjectSourcePlayer input, Game game) { - Spell spell = input.getObject(); - if (spell == null) { - return false; - } - UUID singleTarget = null; - for (TargetAddress addr : TargetAddress.walk(spell)) { - Target targetInstance = addr.getTarget(spell); - for (UUID targetId : targetInstance.getTargets()) { - if (singleTarget == null) { - singleTarget = targetId; - } else if (!singleTarget.equals(targetId)) { - return false; - } - } - } - if (singleTarget == null) { - return false; - } - Permanent permanent = game.getPermanent(singleTarget); - return permanent != null - && permanent.isControlledBy(input.getPlayerId()) - && (permanent.isCreature(game) || permanent.isArtifact(game)); - } -} - class VesuvanDuplimancyEffect extends OneShotEffect { VesuvanDuplimancyEffect() { diff --git a/Mage.Sets/src/mage/cards/z/ZevlorElturelExile.java b/Mage.Sets/src/mage/cards/z/ZevlorElturelExile.java index d281bcbe490..2f105f1bb50 100644 --- a/Mage.Sets/src/mage/cards/z/ZevlorElturelExile.java +++ b/Mage.Sets/src/mage/cards/z/ZevlorElturelExile.java @@ -69,6 +69,7 @@ public final class ZevlorElturelExile extends CardImpl { } class ZevlorElturelExileTriggeredAbility extends DelayedTriggeredAbility { + // TODO: it might be possible to refactor Zevlor using the same Trigger/filter than Ivy, Gleeful Spellthief. ZevlorElturelExileTriggeredAbility() { super(new ZevlorElturelExileEffect(), Duration.EndOfTurn, true, false); diff --git a/Mage.Sets/src/mage/sets/WildsOfEldraine.java b/Mage.Sets/src/mage/sets/WildsOfEldraine.java index 0479045af43..6ed57382bf2 100644 --- a/Mage.Sets/src/mage/sets/WildsOfEldraine.java +++ b/Mage.Sets/src/mage/sets/WildsOfEldraine.java @@ -90,6 +90,7 @@ public final class WildsOfEldraine extends ExpansionSet { cards.add(new SetCardInfo("Hylda's Crown of Winter", 247, Rarity.RARE, mage.cards.h.HyldasCrownOfWinter.class)); cards.add(new SetCardInfo("Ice Out", 54, Rarity.COMMON, mage.cards.i.IceOut.class)); cards.add(new SetCardInfo("Icewrought Sentry", 55, Rarity.UNCOMMON, mage.cards.i.IcewroughtSentry.class)); + cards.add(new SetCardInfo("Imodane, the Pyrohammer", 137, Rarity.RARE, mage.cards.i.ImodaneThePyrohammer.class)); cards.add(new SetCardInfo("Ingenious Prodigy", 56, Rarity.RARE, mage.cards.i.IngeniousProdigy.class)); cards.add(new SetCardInfo("Into the Fae Court", 57, Rarity.COMMON, mage.cards.i.IntoTheFaeCourt.class)); cards.add(new SetCardInfo("Island", 263, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/ImodaneThePyrohammerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/ImodaneThePyrohammerTest.java new file mode 100644 index 00000000000..9403c7dcfac --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/ImodaneThePyrohammerTest.java @@ -0,0 +1,140 @@ +package org.mage.test.cards.single.woe; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class ImodaneThePyrohammerTest extends CardTestPlayerBase { + + /** + * Imodane, the Pyrohammer + * {2}{R}{R} + * Legendary Creature — Human Knight + *

+ * Whenever an instant or sorcery spell you control that targets only a single creature deals damage to that creature, Imodane deals that much damage to each opponent. + *

+ * 4/4 + */ + private static final String imodane = "Imodane, the Pyrohammer"; + + // 2/2 + private static final String bears = "Grizzly Bears"; + // 2/2 + private static final String lion = "Silvercoat Lion"; + + @Test + public void boltTheBear() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, imodane); + addCard(Zone.BATTLEFIELD, playerB, bears); + // 3 damage to any target + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + + castSpell(1, PhaseStep.UPKEEP, playerA, "Lightning Bolt", bears); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 3); + assertPermanentCount(playerB, bears, 0); + } + + @Test + public void flamesOfTheRazeBoarTheBear() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, imodane); + addCard(Zone.BATTLEFIELD, playerB, bears); + addCard(Zone.BATTLEFIELD, playerB, lion, 4); + + // Flames of the Raze-Boar deals 4 damage to target creature an opponent controls. + // Then Flames of the Raze-Boar deals 2 damage to each other creature that player + // controls if you control a creature with power 4 or greater. + addCard(Zone.HAND, playerA, "Flames of the Raze-Boar", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + castSpell(1, PhaseStep.UPKEEP, playerA, "Flames of the Raze-Boar", bears); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 4); // Only damage on the bears should be accounted for. + assertPermanentCount(playerB, bears, 0); + assertPermanentCount(playerB, lion, 0); + } + + @Test + public void pyroclasmDoesNotTriggerImodane() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, imodane); + addCard(Zone.BATTLEFIELD, playerB, bears); + addCard(Zone.BATTLEFIELD, playerB, lion, 4); + + // 2 damage to each creature + addCard(Zone.HAND, playerA, "Pyroclasm", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pyroclasm"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerB, 20); // No damage from Imodane + assertPermanentCount(playerB, bears, 0); + assertPermanentCount(playerB, lion, 0); + } + + @Test + public void targetTowThingsDoesNotTriggerImodane() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, imodane); + addCard(Zone.BATTLEFIELD, playerB, bears); + + // Shower of Sparks deals 1 damage to target creature and 1 damage to target player or planeswalker. + addCard(Zone.HAND, playerA, "Shower of Sparks", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + castSpell(1, PhaseStep.UPKEEP, playerA, "Shower of Sparks"); + addTarget(playerA, bears); + addTarget(playerA, playerB); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerB, 20 - 1); // No damage from Imodane, but one from Shower of Sparks + assertPermanentCount(playerB, bears, 1); + assertDamageReceived(playerB, bears, 1); + } + + @Test + public void nonSpellsDoesNotTriggerImodane() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, imodane); + addCard(Zone.BATTLEFIELD, playerB, bears); + + // Enchantment + // {2}: Task Mage Assembly deals 1 damage to target creature. + // Any player may activate this ability but only as a sorcery. + addCard(Zone.BATTLEFIELD, playerA, "Task Mage Assembly", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: "); + addTarget(playerA, bears); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerB, 20); // No damage from Imodane + assertPermanentCount(playerB, bears, 1); + assertDamageReceived(playerB, bears, 1); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/hint/ValuePositiveHint.java b/Mage/src/main/java/mage/abilities/hint/ValuePositiveHint.java new file mode 100644 index 00000000000..717f55a2081 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/hint/ValuePositiveHint.java @@ -0,0 +1,35 @@ +package mage.abilities.hint; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.game.Game; + +/** + * @author Susucr + */ +public class ValuePositiveHint implements Hint { + + private final String name; + private final DynamicValue value; + + public ValuePositiveHint(String name, DynamicValue value) { + this.name = name; + this.value = value; + } + + private ValuePositiveHint(final ValuePositiveHint hint) { + this.name = hint.name; + this.value = hint.value.copy(); + } + + @Override + public String getText(Game game, Ability ability) { + int amount = value.calculate(game, ability, null); + return amount <= 0 ? "" : name + ": " + amount; + } + + @Override + public ValuePositiveHint copy() { + return new ValuePositiveHint(this); + } +} diff --git a/Mage/src/main/java/mage/filter/predicate/other/HasOnlySingleTargetPermanentPredicate.java b/Mage/src/main/java/mage/filter/predicate/other/HasOnlySingleTargetPermanentPredicate.java new file mode 100644 index 00000000000..d091fe53cde --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/other/HasOnlySingleTargetPermanentPredicate.java @@ -0,0 +1,56 @@ +package mage.filter.predicate.other; + +import mage.filter.FilterPermanent; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.target.Target; +import mage.util.TargetAddress; + +import java.util.UUID; + +/** + * @author TheElk801, Susucr + */ +public class HasOnlySingleTargetPermanentPredicate implements ObjectSourcePlayerPredicate { + + private final FilterPermanent filter; + + public HasOnlySingleTargetPermanentPredicate(FilterPermanent filter) { + this.filter = filter.copy(); + } + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + Spell spell = input.getObject(); + if (spell == null) { + return false; + } + UUID singleTarget = null; + for (TargetAddress addr : TargetAddress.walk(spell)) { + Target targetInstance = addr.getTarget(spell); + for (UUID targetId : targetInstance.getTargets()) { + if (singleTarget == null) { + singleTarget = targetId; + } else if (!singleTarget.equals(targetId)) { + // Ruling on Ivy, Gleeful Spellthief + // (2022-09-09) The second ability triggers whenever a player casts a spell that targets + // only one creature and no other object or player. + return false; + } + } + } + if (singleTarget == null) { + return false; + } + Permanent permanent = game.getPermanent(singleTarget); + return filter.match(permanent, input.getPlayerId(), input.getSource(), game); + } + + @Override + public String toString() { + return "that targets only a single " + filter.getMessage(); + } +} \ No newline at end of file