From d29a244ebd59d3fc972f31dacef8219f5d5977e0 Mon Sep 17 00:00:00 2001 From: androosss <101566943+androosss@users.noreply.github.com> Date: Wed, 2 Apr 2025 20:31:02 +0200 Subject: [PATCH 001/133] [TDM] Implement Lotuslight Dancers (#13493) * Implemented Lotusligh Dancers * filter fix * author fix * fix verify test --- .../src/mage/cards/l/LotuslightDancers.java | 135 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 2 + 2 files changed, 137 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LotuslightDancers.java diff --git a/Mage.Sets/src/mage/cards/l/LotuslightDancers.java b/Mage.Sets/src/mage/cards/l/LotuslightDancers.java new file mode 100644 index 00000000000..998672a320b --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LotuslightDancers.java @@ -0,0 +1,135 @@ +package mage.cards.l; + +import java.util.UUID; +import mage.MageInt; +import mage.ObjectColor; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.ColorPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInLibrary; +import mage.abilities.Ability; +import mage.abilities.assignment.common.ColorAssignment; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; + +/** + * + * @author androosss + */ +public final class LotuslightDancers extends CardImpl { + + public LotuslightDancers(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{G}{U}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.BARD); + this.power = new MageInt(3); + this.toughness = new MageInt(6); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // When this creature enters, search your library for a black card, a green card, and a blue card. Put those cards into your graveyard, then shuffle. + this.addAbility(new EntersBattlefieldTriggeredAbility(new LotuslightDancersEffect())); + } + + private LotuslightDancers(final LotuslightDancers card) { + super(card); + } + + @Override + public LotuslightDancers copy() { + return new LotuslightDancers(this); + } +} + +class LotuslightDancersEffect extends OneShotEffect { + + LotuslightDancersEffect() { + super(Outcome.Neutral); + staticText = "search your library for a black card, a green card, and a blue card. " + + "Put those cards into your graveyard, then shuffle."; + } + + private LotuslightDancersEffect(final LotuslightDancersEffect effect) { + super(effect); + } + + @Override + public LotuslightDancersEffect copy() { + return new LotuslightDancersEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + TargetCardInLibrary target = new LotuslightDancersTarget(); + controller.searchLibrary(target, source, game); + Cards cards = new CardsImpl(target.getTargets()); + cards.retainZone(Zone.LIBRARY, game); + controller.moveCards(cards, Zone.GRAVEYARD, source, game); + controller.shuffleLibrary(source, game); + return true; + } +} + +class LotuslightDancersTarget extends TargetCardInLibrary { + + private static final FilterCard filter + = new FilterCard("a black card, a green card, and a blue card"); + + static { + filter.add(Predicates.or( + new ColorPredicate(ObjectColor.BLUE), + new ColorPredicate(ObjectColor.BLACK), + new ColorPredicate(ObjectColor.GREEN) + )); + } + + private static final ColorAssignment colorAssigner = new ColorAssignment("U", "B", "G"); + + LotuslightDancersTarget() { + super(0, 3, filter); + } + + private LotuslightDancersTarget(final LotuslightDancersTarget target) { + super(target); + } + + @Override + public LotuslightDancersTarget copy() { + return new LotuslightDancersTarget(this); + } + + @Override + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + if (!super.canTarget(playerId, id, source, game)) { + return false; + } + Card card = game.getCard(id); + if (card == null) { + return false; + } + if (this.getTargets().isEmpty()) { + return true; + } + Cards cards = new CardsImpl(this.getTargets()); + cards.add(card); + return colorAssigner.getRoleCount(cards, game) >= cards.size(); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 1a0c64cf5d8..1e10c71f25f 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -121,6 +121,8 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Kotis, the Fangkeeper", 202, Rarity.RARE, mage.cards.k.KotisTheFangkeeper.class)); cards.add(new SetCardInfo("Krotiq Nestguard", 148, Rarity.COMMON, mage.cards.k.KrotiqNestguard.class)); cards.add(new SetCardInfo("Lightfoot Technique", 14, Rarity.COMMON, mage.cards.l.LightfootTechnique.class)); + cards.add(new SetCardInfo("Lotuslight Dancers", 204, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Lotuslight Dancers", 363, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Loxodon Battle Priest", 15, Rarity.UNCOMMON, mage.cards.l.LoxodonBattlePriest.class)); cards.add(new SetCardInfo("Mammoth Bellow", 205, Rarity.UNCOMMON, mage.cards.m.MammothBellow.class)); cards.add(new SetCardInfo("Mardu Devotee", 16, Rarity.COMMON, mage.cards.m.MarduDevotee.class)); From 090f181e3e257daee4197302430c83237f39042b Mon Sep 17 00:00:00 2001 From: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com> Date: Wed, 2 Apr 2025 19:53:49 +0100 Subject: [PATCH 002/133] Fix Bandit's Talent hint text --- Mage.Sets/src/mage/cards/b/BanditsTalent.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BanditsTalent.java b/Mage.Sets/src/mage/cards/b/BanditsTalent.java index 49d95f90602..f9e840fbc0c 100644 --- a/Mage.Sets/src/mage/cards/b/BanditsTalent.java +++ b/Mage.Sets/src/mage/cards/b/BanditsTalent.java @@ -144,10 +144,9 @@ class BanditsTalentDiscardEffect extends OneShotEffect { } } - enum BanditsTalentValue implements DynamicValue { instance; - private static final Hint hint = new ValueHint("opponents who have one or fewer cards in hand", instance); + private static final Hint hint = new ValueHint("Opponents who have one or fewer cards in hand", instance); public static Hint getHint() { return hint; From c548a13fdbb68f42b83897b88c9c6b68e0cd856b Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 2 Apr 2025 14:40:15 -0400 Subject: [PATCH 003/133] [TDM] Implement Effortless Master --- .../src/mage/cards/e/EffortlessMaster.java | 68 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 69 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/EffortlessMaster.java diff --git a/Mage.Sets/src/mage/cards/e/EffortlessMaster.java b/Mage.Sets/src/mage/cards/e/EffortlessMaster.java new file mode 100644 index 00000000000..6ddf6b305f3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EffortlessMaster.java @@ -0,0 +1,68 @@ +package mage.cards.e; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.condition.Condition; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.MenaceAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.game.Game; +import mage.watchers.common.SpellsCastWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EffortlessMaster extends CardImpl { + + public EffortlessMaster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{R}"); + + this.subtype.add(SubType.ORC); + this.subtype.add(SubType.MONK); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Menace + this.addAbility(new MenaceAbility()); + + // This creature enters with two +1/+1 counters on it if you've cast two or more spells this turn. + this.addAbility(new EntersBattlefieldAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), + EffortlessMasterCondition.instance, null, + "with two +1/+1 counters on it if you've cast two or more spells this turn" + )); + } + + private EffortlessMaster(final EffortlessMaster card) { + super(card); + } + + @Override + public EffortlessMaster copy() { + return new EffortlessMaster(this); + } +} + +enum EffortlessMasterCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return game + .getState() + .getWatcher(SpellsCastWatcher.class) + .getSpellsCastThisTurn(source.getControllerId()) + .size() >= 2; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 1e10c71f25f..6d11bb48e00 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -78,6 +78,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Dragonstorm Globe", 241, Rarity.COMMON, mage.cards.d.DragonstormGlobe.class)); cards.add(new SetCardInfo("Dusyut Earthcarver", 141, Rarity.COMMON, mage.cards.d.DusyutEarthcarver.class)); cards.add(new SetCardInfo("Duty Beyond Death", 10, Rarity.UNCOMMON, mage.cards.d.DutyBeyondDeath.class)); + cards.add(new SetCardInfo("Effortless Master", 181, Rarity.UNCOMMON, mage.cards.e.EffortlessMaster.class)); cards.add(new SetCardInfo("Elspeth, Storm Slayer", 11, Rarity.MYTHIC, mage.cards.e.ElspethStormSlayer.class)); cards.add(new SetCardInfo("Embermouth Sentinel", 242, Rarity.COMMON, mage.cards.e.EmbermouthSentinel.class)); cards.add(new SetCardInfo("Encroaching Dragonstorm", 142, Rarity.UNCOMMON, mage.cards.e.EncroachingDragonstorm.class)); From d490f5fb20f1ba1177e7eafb49e9a3d6b87461cd Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 2 Apr 2025 14:41:29 -0400 Subject: [PATCH 004/133] [TDM] Implement Flamehold Grappler --- .../src/mage/cards/f/FlameholdGrappler.java | 46 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 47 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FlameholdGrappler.java diff --git a/Mage.Sets/src/mage/cards/f/FlameholdGrappler.java b/Mage.Sets/src/mage/cards/f/FlameholdGrappler.java new file mode 100644 index 00000000000..2d96ae46614 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FlameholdGrappler.java @@ -0,0 +1,46 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.delayed.CopyNextSpellDelayedTriggeredAbility; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FlameholdGrappler extends CardImpl { + + public FlameholdGrappler(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{R}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.MONK); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // First strike + this.addAbility(FirstStrikeAbility.getInstance()); + + // When this creature enters, copy the next spell you cast this turn when you cast it. You may choose new targets for the copy. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateDelayedTriggeredAbilityEffect( + new CopyNextSpellDelayedTriggeredAbility(StaticFilters.FILTER_SPELL) + ))); + } + + private FlameholdGrappler(final FlameholdGrappler card) { + super(card); + } + + @Override + public FlameholdGrappler copy() { + return new FlameholdGrappler(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 6d11bb48e00..418107a899d 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -88,6 +88,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Fangkeeper's Familiar", 183, Rarity.RARE, mage.cards.f.FangkeepersFamiliar.class)); cards.add(new SetCardInfo("Felothar, Dawn of the Abzan", 184, Rarity.RARE, mage.cards.f.FelotharDawnOfTheAbzan.class)); cards.add(new SetCardInfo("Fire-Rim Form", 107, Rarity.COMMON, mage.cards.f.FireRimForm.class)); + cards.add(new SetCardInfo("Flamehold Grappler", 185, Rarity.RARE, mage.cards.f.FlameholdGrappler.class)); cards.add(new SetCardInfo("Fleeting Effigy", 108, Rarity.UNCOMMON, mage.cards.f.FleetingEffigy.class)); cards.add(new SetCardInfo("Forest", 285, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Fortress Kin-Guard", 12, Rarity.COMMON, mage.cards.f.FortressKinGuard.class)); From 4062f6d85a250c8d283e6dc6f93e83c94a033116 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 2 Apr 2025 14:45:46 -0400 Subject: [PATCH 005/133] [TDM] Implement Formation Breaker --- .../src/mage/cards/f/FormationBreaker.java | 65 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 66 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FormationBreaker.java diff --git a/Mage.Sets/src/mage/cards/f/FormationBreaker.java b/Mage.Sets/src/mage/cards/f/FormationBreaker.java new file mode 100644 index 00000000000..9510dbd3408 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FormationBreaker.java @@ -0,0 +1,65 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.CounterAnyPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FormationBreaker extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(); + private static final FilterPermanent filter2 = new FilterControlledCreaturePermanent(); + + static { + filter2.add(CounterAnyPredicate.instance); + } + + private static final Condition condition = new PermanentsOnTheBattlefieldCondition(filter2); + private static final Hint hint = new ConditionHint(condition, "You control a creature with a counter on it"); + + public FormationBreaker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}"); + + this.subtype.add(SubType.BEAST); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Creatures with power less than this creature's power can't block it. + this.addAbility(new SimpleStaticAbility(new CantBeBlockedByCreaturesSourceEffect(filter, Duration.WhileOnBattlefield) + .setText("creatures with power less than this creature's power can't block it"))); + + // As long as you control a creature with a counter on it, this creature gets +1/+2. + this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( + new BoostSourceEffect(1, 2, Duration.WhileOnBattlefield), + condition, "as long as you control a creature with a counter on it, this creature gets +1/+2" + )).addHint(hint)); + } + + private FormationBreaker(final FormationBreaker card) { + super(card); + } + + @Override + public FormationBreaker copy() { + return new FormationBreaker(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 418107a899d..d1fff0bbf23 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -91,6 +91,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Flamehold Grappler", 185, Rarity.RARE, mage.cards.f.FlameholdGrappler.class)); cards.add(new SetCardInfo("Fleeting Effigy", 108, Rarity.UNCOMMON, mage.cards.f.FleetingEffigy.class)); cards.add(new SetCardInfo("Forest", 285, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Formation Breaker", 143, Rarity.UNCOMMON, mage.cards.f.FormationBreaker.class)); cards.add(new SetCardInfo("Fortress Kin-Guard", 12, Rarity.COMMON, mage.cards.f.FortressKinGuard.class)); cards.add(new SetCardInfo("Fresh Start", 46, Rarity.UNCOMMON, mage.cards.f.FreshStart.class)); cards.add(new SetCardInfo("Frontier Bivouac", 256, Rarity.UNCOMMON, mage.cards.f.FrontierBivouac.class)); From caf5984b62375b1f720b5e1dcfc54f728de5f2be Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 2 Apr 2025 14:49:02 -0400 Subject: [PATCH 006/133] [TDM] Implement Karakyk Guardian --- .../src/mage/cards/k/KarakykGuardian.java | 110 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 111 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KarakykGuardian.java diff --git a/Mage.Sets/src/mage/cards/k/KarakykGuardian.java b/Mage.Sets/src/mage/cards/k/KarakykGuardian.java new file mode 100644 index 00000000000..6fd569725a6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KarakykGuardian.java @@ -0,0 +1,110 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.HexproofAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.WatcherScope; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.watchers.Watcher; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KarakykGuardian extends CardImpl { + + public KarakykGuardian(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}{U}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(6); + this.toughness = new MageInt(5); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // This creature has hexproof if it hasn't dealt damage yet. + this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( + new GainAbilitySourceEffect(HexproofAbility.getInstance()), + KarakykGuardianCondition.instance, "{this} has hexproof if it hasn't dealt damage yet" + )), new KarakykGuardianWatcher()); + } + + private KarakykGuardian(final KarakykGuardian card) { + super(card); + } + + @Override + public KarakykGuardian copy() { + return new KarakykGuardian(this); + } +} + +enum KarakykGuardianCondition implements Condition { + + instance; + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getSourceId()); + KarakykGuardianWatcher watcher = game.getState().getWatcher(KarakykGuardianWatcher.class); + return permanent != null && !watcher.getDamagers().contains(new MageObjectReference(permanent, game)); + } + + @Override + public String toString() { + return "{this} hasn't dealt damage yet"; + } + +} + +class KarakykGuardianWatcher extends Watcher { + + private final Set damagers = new HashSet<>(); + + public KarakykGuardianWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + switch (event.getType()) { + case DAMAGED_PERMANENT: + case DAMAGED_PLAYER: + break; + default: + return; + } + Permanent permanent = game.getPermanent(event.getSourceId()); + if (permanent != null) { + damagers.add(new MageObjectReference(permanent, game)); + } + } + + public Set getDamagers() { + return damagers; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index d1fff0bbf23..818979e5a06 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -114,6 +114,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Jeskai Monument", 244, Rarity.UNCOMMON, mage.cards.j.JeskaiMonument.class)); cards.add(new SetCardInfo("Jeskai Shrinekeeper", 197, Rarity.UNCOMMON, mage.cards.j.JeskaiShrinekeeper.class)); cards.add(new SetCardInfo("Jungle Hollow", 258, Rarity.COMMON, mage.cards.j.JungleHollow.class)); + cards.add(new SetCardInfo("Karakyk Guardian", 198, Rarity.UNCOMMON, mage.cards.k.KarakykGuardian.class)); cards.add(new SetCardInfo("Kheru Goldkeeper", 199, Rarity.UNCOMMON, mage.cards.k.KheruGoldkeeper.class)); cards.add(new SetCardInfo("Kin-Tree Nurturer", 83, Rarity.COMMON, mage.cards.k.KinTreeNurturer.class)); cards.add(new SetCardInfo("Kin-Tree Severance", 200, Rarity.UNCOMMON, mage.cards.k.KinTreeSeverance.class)); From 32d0e3d74095a424953e664ee32854dc08dac4f1 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 2 Apr 2025 14:52:55 -0400 Subject: [PATCH 007/133] [TDM] Implement Dragonclaw Strike --- .../src/mage/cards/d/DragonclawStrike.java | 73 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 74 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DragonclawStrike.java diff --git a/Mage.Sets/src/mage/cards/d/DragonclawStrike.java b/Mage.Sets/src/mage/cards/d/DragonclawStrike.java new file mode 100644 index 00000000000..c8318674d70 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DragonclawStrike.java @@ -0,0 +1,73 @@ +package mage.cards.d; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.FightTargetsEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.common.TargetOpponentsCreaturePermanent; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DragonclawStrike extends CardImpl { + + public DragonclawStrike(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2/U}{2/R}{2/G}"); + + // Double the power and toughness of target creature you control until end of turn. Then it fights up to one target creature an opponent controls. + this.getSpellAbility().addEffect(new DragonclawStrikeEffect()); + this.getSpellAbility().addEffect(new FightTargetsEffect() + .setText("Then it fights up to one target creature an opponent controls")); + this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); + this.getSpellAbility().addTarget(new TargetOpponentsCreaturePermanent(0, 1)); + } + + private DragonclawStrike(final DragonclawStrike card) { + super(card); + } + + @Override + public DragonclawStrike copy() { + return new DragonclawStrike(this); + } +} + +class DragonclawStrikeEffect extends OneShotEffect { + + DragonclawStrikeEffect() { + super(Outcome.Benefit); + staticText = "double the power and toughness of target creature you control until end of turn"; + } + + private DragonclawStrikeEffect(final DragonclawStrikeEffect effect) { + super(effect); + } + + @Override + public DragonclawStrikeEffect copy() { + return new DragonclawStrikeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + game.addEffect(new BoostTargetEffect( + permanent.getPower().getValue(), + permanent.getToughness().getValue() + ).setTargetPointer(new FixedTarget(permanent, game)), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 818979e5a06..e5fadd39e9d 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -73,6 +73,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Dragonback Assault", 179, Rarity.MYTHIC, mage.cards.d.DragonbackAssault.class)); cards.add(new SetCardInfo("Dragonback Lancer", 9, Rarity.COMMON, mage.cards.d.DragonbackLancer.class)); cards.add(new SetCardInfo("Dragonbroods' Relic", 140, Rarity.UNCOMMON, mage.cards.d.DragonbroodsRelic.class)); + cards.add(new SetCardInfo("Dragonclaw Strike", 180, Rarity.UNCOMMON, mage.cards.d.DragonclawStrike.class)); cards.add(new SetCardInfo("Dragonologist", 42, Rarity.RARE, mage.cards.d.Dragonologist.class)); cards.add(new SetCardInfo("Dragonstorm Forecaster", 43, Rarity.UNCOMMON, mage.cards.d.DragonstormForecaster.class)); cards.add(new SetCardInfo("Dragonstorm Globe", 241, Rarity.COMMON, mage.cards.d.DragonstormGlobe.class)); From 48173bee0056e1bb4f0b844f38f92069b3d5c159 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 2 Apr 2025 14:58:11 -0400 Subject: [PATCH 008/133] [TDM] Implement Clarion Conquerer --- .../src/mage/cards/c/ClarionConqueror.java | 72 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 73 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/ClarionConqueror.java diff --git a/Mage.Sets/src/mage/cards/c/ClarionConqueror.java b/Mage.Sets/src/mage/cards/c/ClarionConqueror.java new file mode 100644 index 00000000000..efaf4e73ab0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ClarionConqueror.java @@ -0,0 +1,72 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ClarionConqueror extends CardImpl { + + public ClarionConqueror(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Activated abilities of artifacts, creatures, and planeswalkers can't be activated. + this.addAbility(new SimpleStaticAbility(new ClarionConquerorEffect())); + } + + private ClarionConqueror(final ClarionConqueror card) { + super(card); + } + + @Override + public ClarionConqueror copy() { + return new ClarionConqueror(this); + } +} + +class ClarionConquerorEffect extends RestrictionEffect { + + ClarionConquerorEffect() { + super(Duration.WhileOnBattlefield); + staticText = "activated abilities of artifacts, creatures, and planeswalkers can't be activated"; + } + + private ClarionConquerorEffect(final ClarionConquerorEffect effect) { + super(effect); + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return permanent.isArtifact(game) || permanent.isCreature(game) || permanent.isPlaneswalker(game); + } + + @Override + public boolean canUseActivatedAbilities(Permanent permanent, Ability source, Game game, boolean canUseChooseDialogs) { + return false; + } + + @Override + public ClarionConquerorEffect copy() { + return new ClarionConquerorEffect(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index e5fadd39e9d..00384deb02b 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -52,6 +52,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Caustic Exhale", 74, Rarity.COMMON, mage.cards.c.CausticExhale.class)); cards.add(new SetCardInfo("Champion of Dusan", 137, Rarity.COMMON, mage.cards.c.ChampionOfDusan.class)); cards.add(new SetCardInfo("Channeled Dragonfire", 102, Rarity.UNCOMMON, mage.cards.c.ChanneledDragonfire.class)); + cards.add(new SetCardInfo("Clarion Conqueror", 5, Rarity.RARE, mage.cards.c.ClarionConqueror.class)); cards.add(new SetCardInfo("Constrictor Sage", 39, Rarity.UNCOMMON, mage.cards.c.ConstrictorSage.class)); cards.add(new SetCardInfo("Coordinated Maneuver", 6, Rarity.COMMON, mage.cards.c.CoordinatedManeuver.class)); cards.add(new SetCardInfo("Cori Mountain Monastery", 252, Rarity.RARE, mage.cards.c.CoriMountainMonastery.class)); From e223e35e6da65dedc4919a7b1ef2ac8c7c6f9c23 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 2 Apr 2025 15:01:29 -0400 Subject: [PATCH 009/133] [TDM] add missing predicate to Formation Breaker --- .../src/mage/cards/f/FormationBreaker.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Mage.Sets/src/mage/cards/f/FormationBreaker.java b/Mage.Sets/src/mage/cards/f/FormationBreaker.java index 9510dbd3408..492a6fcc11e 100644 --- a/Mage.Sets/src/mage/cards/f/FormationBreaker.java +++ b/Mage.Sets/src/mage/cards/f/FormationBreaker.java @@ -1,6 +1,7 @@ package mage.cards.f; import mage.MageInt; +import mage.MageObject; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.Condition; import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; @@ -17,8 +18,13 @@ import mage.constants.SubType; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.filter.predicate.permanent.CounterAnyPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import java.util.Optional; import java.util.UUID; /** @@ -30,6 +36,7 @@ public final class FormationBreaker extends CardImpl { private static final FilterPermanent filter2 = new FilterControlledCreaturePermanent(); static { + filter.add(FormationBreakerPredicate.instance); filter2.add(CounterAnyPredicate.instance); } @@ -63,3 +70,17 @@ public final class FormationBreaker extends CardImpl { return new FormationBreaker(this); } } + +enum FormationBreakerPredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return Optional + .ofNullable(input.getSource().getSourcePermanentIfItStillExists(game)) + .map(MageObject::getPower) + .map(MageInt::getValue) + .map(x -> input.getObject().getPower().getValue() < x) + .orElse(false); + } +} From bd7b66c17d31fd6ff5e10e184aac07538693b73d Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 2 Apr 2025 15:07:29 -0400 Subject: [PATCH 010/133] [TDM] Implement Starry-Eyed Skyrider --- .../src/mage/cards/s/StarryEyedSkyrider.java | 65 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 66 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StarryEyedSkyrider.java diff --git a/Mage.Sets/src/mage/cards/s/StarryEyedSkyrider.java b/Mage.Sets/src/mage/cards/s/StarryEyedSkyrider.java new file mode 100644 index 00000000000..4e1325fcc25 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StarryEyedSkyrider.java @@ -0,0 +1,65 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.predicate.permanent.AttackingPredicate; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StarryEyedSkyrider extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("attacking tokens"); + + static { + filter.add(AttackingPredicate.instance); + filter.add(TokenPredicate.TRUE); + } + + public StarryEyedSkyrider(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SCOUT); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever this creature attacks, another target creature you control gains flying until end of turn. + Ability ability = new AttacksTriggeredAbility(new GainAbilityTargetEffect(FlyingAbility.getInstance())); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_ANOTHER_TARGET_CREATURE_YOU_CONTROL)); + this.addAbility(ability); + + // Attacking tokens you control have flying. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + FlyingAbility.getInstance(), Duration.WhileOnBattlefield, filter + ))); + } + + private StarryEyedSkyrider(final StarryEyedSkyrider card) { + super(card); + } + + @Override + public StarryEyedSkyrider copy() { + return new StarryEyedSkyrider(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 00384deb02b..1b6d33d1b3c 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -191,6 +191,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Sonic Shrieker", 226, Rarity.UNCOMMON, mage.cards.s.SonicShrieker.class)); cards.add(new SetCardInfo("Spectral Denial", 58, Rarity.UNCOMMON, mage.cards.s.SpectralDenial.class)); cards.add(new SetCardInfo("Stadium Headliner", 122, Rarity.RARE, mage.cards.s.StadiumHeadliner.class)); + cards.add(new SetCardInfo("Starry-Eyed Skyrider", 25, Rarity.UNCOMMON, mage.cards.s.StarryEyedSkyrider.class)); cards.add(new SetCardInfo("Static Snare", 26, Rarity.UNCOMMON, mage.cards.s.StaticSnare.class)); cards.add(new SetCardInfo("Stormbeacon Blade", 27, Rarity.UNCOMMON, mage.cards.s.StormbeaconBlade.class)); cards.add(new SetCardInfo("Stormplain Detainment", 28, Rarity.COMMON, mage.cards.s.StormplainDetainment.class)); From 35b0ae8abd63d5c6534c974f84040b9e21a08c38 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:38:03 -0400 Subject: [PATCH 011/133] [TDC] Implement Elsha, Threefold Master --- .../mage/cards/e/ElshaThreefoldMaster.java | 51 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 52 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/ElshaThreefoldMaster.java diff --git a/Mage.Sets/src/mage/cards/e/ElshaThreefoldMaster.java b/Mage.Sets/src/mage/cards/e/ElshaThreefoldMaster.java new file mode 100644 index 00000000000..c6e328b9144 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/ElshaThreefoldMaster.java @@ -0,0 +1,51 @@ +package mage.cards.e; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.dynamicvalue.common.SavedDamageValue; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.ProwessAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.MonasteryMentorToken; + +/** + * + * @author Grath + */ +public final class ElshaThreefoldMaster extends CardImpl { + + public ElshaThreefoldMaster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{R}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.DJINN); + this.subtype.add(SubType.MONK); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Prowess + this.addAbility(new ProwessAbility()); + + // Whenever Elsha deals combat damage to a player, create that many 1/1 white Monk creature tokens with prowess. + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( + new CreateTokenEffect(new MonasteryMentorToken(), SavedDamageValue.MANY), false, true)); + } + + private ElshaThreefoldMaster(final ElshaThreefoldMaster card) { + super(card); + } + + @Override + public ElshaThreefoldMaster copy() { + return new ElshaThreefoldMaster(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 15997a8fce0..0cae8e90d6a 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -107,6 +107,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Electrodominance", 212, Rarity.RARE, mage.cards.e.Electrodominance.class)); cards.add(new SetCardInfo("Elemental Bond", 254, Rarity.UNCOMMON, mage.cards.e.ElementalBond.class)); cards.add(new SetCardInfo("Eliminate the Competition", 179, Rarity.RARE, mage.cards.e.EliminateTheCompetition.class)); + cards.add(new SetCardInfo("Elsha, Threefold Master", 2, Rarity.MYTHIC, mage.cards.e.ElshaThreefoldMaster.class)); cards.add(new SetCardInfo("Emeria Angel", 114, Rarity.RARE, mage.cards.e.EmeriaAngel.class)); cards.add(new SetCardInfo("Exotic Orchard", 360, Rarity.RARE, mage.cards.e.ExoticOrchard.class)); cards.add(new SetCardInfo("Expansion // Explosion", 287, Rarity.RARE, mage.cards.e.ExpansionExplosion.class)); From dfb68964531d9d1416518ae90524842a1d4028b4 Mon Sep 17 00:00:00 2001 From: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:34:04 +0100 Subject: [PATCH 012/133] Skip prompting player with no blockers to select blockers (#13496) * Don't prompt creatureless player to select blockers * Move getting possible blockers back to while loop * Several preferences text improvements, always skip select blockers prompt if no blockers --- .../src/main/java/mage/client/dialog/PreferencesDialog.form | 6 +++--- .../src/main/java/mage/client/dialog/PreferencesDialog.java | 6 +++--- .../src/mage/player/human/HumanPlayer.java | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form index 2fb5afdb761..0138479d0b1 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form @@ -2621,19 +2621,19 @@ - + - + - + diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java index 94238a49468..1a0b396cc53 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java @@ -2396,15 +2396,15 @@ public class PreferencesDialog extends javax.swing.JDialog { phases_stopSettings.add(cbStopAttack); cbStopBlockWithAny.setSelected(true); - cbStopBlockWithAny.setText("STOP skips on declare blockers if ANY blockers are available"); + cbStopBlockWithAny.setText("STOP skips when attacked and on declare blockers if ANY blockers are available"); cbStopBlockWithAny.setActionCommand(""); phases_stopSettings.add(cbStopBlockWithAny); - cbStopBlockWithZero.setText("STOP skips on declare blockers if ZERO blockers are available"); + cbStopBlockWithZero.setText("STOP skips when attacked if ZERO blockers are available"); cbStopBlockWithZero.setActionCommand(""); phases_stopSettings.add(cbStopBlockWithZero); - cbStopOnNewStackObjects.setText("Skip to STACK resolved (F10): stop on new objects added (on) or stop until empty (off)"); + cbStopOnNewStackObjects.setText("Skip to STACK resolved (F10): stop on new objects added (on) or stop when stack empty (off)"); cbStopOnNewStackObjects.setActionCommand(""); cbStopOnNewStackObjects.setPreferredSize(new java.awt.Dimension(300, 25)); phases_stopSettings.add(cbStopOnNewStackObjects); diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index f035a45fe6b..7115082ba57 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -2072,7 +2072,6 @@ public class HumanPlayer extends PlayerImpl { // stop skip on any/zero permanents available int possibleBlockersCount = game.getBattlefield().count(filter, playerId, source, game); boolean canStopOnAny = possibleBlockersCount != 0 && getControllingPlayersUserData(game).getUserSkipPrioritySteps().isStopOnDeclareBlockersWithAnyPermanents(); - boolean canStopOnZero = possibleBlockersCount == 0 && getControllingPlayersUserData(game).getUserSkipPrioritySteps().isStopOnDeclareBlockersWithZeroPermanents(); // skip declare blocker step // as opposed to declare attacker - it can be skipped by ANY skip button TODO: make same for declare attackers and rework skip buttons (normal and forced) @@ -2081,9 +2080,11 @@ public class HumanPlayer extends PlayerImpl { || passedTurn || passedUntilEndOfTurn || passedUntilNextMain; - if (skipButtonActivated && !canStopOnAny && !canStopOnZero) { + if (skipButtonActivated && !canStopOnAny) { return; } + // Skip prompt to select blockers if player has none + if (possibleBlockersCount == 0) return; while (canRespond()) { prepareForResponse(game); From 4f2279bd1d7669694c317650f854309be2d3905e Mon Sep 17 00:00:00 2001 From: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com> Date: Fri, 4 Apr 2025 03:37:56 +0100 Subject: [PATCH 013/133] Fix Fungal Plots exiling cards to source exile zone --- Mage.Sets/src/mage/cards/f/FungalPlots.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Mage.Sets/src/mage/cards/f/FungalPlots.java b/Mage.Sets/src/mage/cards/f/FungalPlots.java index 4f9dc2b38d2..d833aba5152 100644 --- a/Mage.Sets/src/mage/cards/f/FungalPlots.java +++ b/Mage.Sets/src/mage/cards/f/FungalPlots.java @@ -1,4 +1,3 @@ - package mage.cards.f; import java.util.UUID; @@ -13,12 +12,10 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterCreatureCard; import mage.game.permanent.token.SaprolingToken; import mage.target.common.TargetCardInYourGraveyard; -import mage.target.common.TargetControlledPermanent; /** * @@ -40,7 +37,7 @@ public final class FungalPlots extends CardImpl { SimpleActivatedAbility ability = new SimpleActivatedAbility( new CreateTokenEffect(new SaprolingToken()), new ManaCostsImpl<>("{1}{G}")); - ability.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(filter))); + ability.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(filter)).withSourceExileZone(false)); this.addAbility(ability); // Sacrifice two Saprolings: You gain 2 life and draw a card. From 41d3464b5c9bf8efcbc3fbfe48bfbb1ba489387f Mon Sep 17 00:00:00 2001 From: ilyagart <55062984+ilyagart@users.noreply.github.com> Date: Fri, 4 Apr 2025 20:41:32 +0300 Subject: [PATCH 014/133] [TDC] Implement Ureni of the Unwritten (#13497) --- .../src/mage/cards/u/UreniOfTheUnwritten.java | 94 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 95 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/u/UreniOfTheUnwritten.java diff --git a/Mage.Sets/src/mage/cards/u/UreniOfTheUnwritten.java b/Mage.Sets/src/mage/cards/u/UreniOfTheUnwritten.java new file mode 100644 index 00000000000..fa56c051e20 --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UreniOfTheUnwritten.java @@ -0,0 +1,94 @@ +package mage.cards.u; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; + +import java.util.UUID; + +/** + * @author ilyagart + */ +public final class UreniOfTheUnwritten extends CardImpl { + + public UreniOfTheUnwritten(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(7); + this.toughness = new MageInt(7); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Whenever Ureni enters or attacks, look at the top eight cards of your library. You may put a Dragon creature card from among them onto the battlefield. Put the rest on the bottom of your library in a random order. + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new UreniOfTheUnwrittenEffect(), false)); + } + + private UreniOfTheUnwritten(final UreniOfTheUnwritten card) { + super(card); + } + + @Override + public UreniOfTheUnwritten copy() { + return new UreniOfTheUnwritten(this); + } +} + +class UreniOfTheUnwrittenEffect extends OneShotEffect { + + UreniOfTheUnwrittenEffect() { + super(Outcome.Benefit); + this.staticText = "look at the top eight cards of your library. You may put a Dragon creature card from among them onto the battlefield. Put the rest on the bottom of your library in a random order."; + } + + private UreniOfTheUnwrittenEffect(final mage.cards.u.UreniOfTheUnwrittenEffect effect) { + super(effect); + } + + @Override + public mage.cards.u.UreniOfTheUnwrittenEffect copy() { + return new mage.cards.u.UreniOfTheUnwrittenEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Cards cards = new CardsImpl(controller.getLibrary().getTopCards(game, 8)); + if (!cards.isEmpty()) { + FilterCreatureCard filter = new FilterCreatureCard("Dragon creature cards"); + filter.add(SubType.DRAGON.getPredicate()); + TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, filter); + targetCard.withNotTarget(true); + controller.choose(Outcome.PutCreatureInPlay, cards, targetCard, source, game); + controller.moveCards(game.getCard(targetCard.getFirstTarget()), Zone.BATTLEFIELD, source, game); + cards.retainZone(Zone.LIBRARY, game); + controller.putCardsOnBottomOfLibrary(cards, game, source, false); + } + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 0cae8e90d6a..8cba642f9e3 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -321,6 +321,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Tree of Redemption", 97, Rarity.MYTHIC, mage.cards.t.TreeOfRedemption.class)); cards.add(new SetCardInfo("Twilight Drover", 136, Rarity.RARE, mage.cards.t.TwilightDrover.class)); cards.add(new SetCardInfo("Twilight Mire", 409, Rarity.RARE, mage.cards.t.TwilightMire.class)); + cards.add(new SetCardInfo("Ureni of the Unwritten", 9, Rarity.MYTHIC, mage.cards.u.UreniOfTheUnwritten.class)); cards.add(new SetCardInfo("Vanquish the Horde", 91, Rarity.RARE, mage.cards.v.VanquishTheHorde.class)); cards.add(new SetCardInfo("Vault of the Archangel", 410, Rarity.RARE, mage.cards.v.VaultOfTheArchangel.class)); cards.add(new SetCardInfo("Velomachus Lorehold", 309, Rarity.MYTHIC, mage.cards.v.VelomachusLorehold.class)); From 8e1805c8742cdba1d4b5078facff8601116cc458 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Fri, 4 Apr 2025 23:25:53 -0500 Subject: [PATCH 015/133] Fix Ward batch event bug Fixes #13498 getTargetingStackObject wasn't processing all stackObjects in a batch event added tests for some related cards that also use the method - Agrus Kos, Eternal Soldier - Pawpatch Recruit - Ward Ability --- .../mage/cards/a/AgrusKosEternalSoldier.java | 2 +- .../src/mage/cards/p/PawpatchRecruit.java | 2 +- .../cards/abilities/keywords/WardTest.java | 50 +++++++++++++++++++ .../cards/single/blb/PawpatchRecruitTest.java | 41 +++++++++++++++ .../j22/AgrusKosEternalSoldierTest.java | 31 ++++++++++++ .../BecomesTargetAnyTriggeredAbility.java | 2 +- ...BecomesTargetAttachedTriggeredAbility.java | 2 +- ...comesTargetControllerTriggeredAbility.java | 2 +- .../BecomesTargetSourceTriggeredAbility.java | 2 +- .../mage/abilities/keyword/WardAbility.java | 2 +- Mage/src/main/java/mage/util/CardUtil.java | 13 ++++- ...rOfTimesPermanentTargetedATurnWatcher.java | 2 +- 12 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/blb/PawpatchRecruitTest.java diff --git a/Mage.Sets/src/mage/cards/a/AgrusKosEternalSoldier.java b/Mage.Sets/src/mage/cards/a/AgrusKosEternalSoldier.java index 3789ce55f66..7253374c3df 100644 --- a/Mage.Sets/src/mage/cards/a/AgrusKosEternalSoldier.java +++ b/Mage.Sets/src/mage/cards/a/AgrusKosEternalSoldier.java @@ -90,7 +90,7 @@ class AgrusKosEternalSoldierTriggeredAbility extends TriggeredAbilityImpl { if (!event.getTargetId().equals(getSourceId())) { return false; } - StackObject targetingObject = CardUtil.getTargetingStackObject(event, game); + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); if (targetingObject == null || targetingObject instanceof Spell) { return false; } diff --git a/Mage.Sets/src/mage/cards/p/PawpatchRecruit.java b/Mage.Sets/src/mage/cards/p/PawpatchRecruit.java index f6ea0a2222a..0a72089951c 100644 --- a/Mage.Sets/src/mage/cards/p/PawpatchRecruit.java +++ b/Mage.Sets/src/mage/cards/p/PawpatchRecruit.java @@ -96,7 +96,7 @@ class PawpatchRecruitTriggeredAbility extends TriggeredAbilityImpl { if (permanent == null || !filterTarget.match(permanent, getControllerId(), this, game)) { return false; } - StackObject targetingObject = CardUtil.getTargetingStackObject(event, game); + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) { return false; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/WardTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/WardTest.java index b200bb76bfb..323d9b4cdd3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/WardTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/WardTest.java @@ -30,4 +30,54 @@ public class WardTest extends CardTestPlayerBase { assertGraveyardCount(playerA, "Solitude", 1); assertPermanentCount(playerB, "Waterfall Aerialist", 1); } + + @Test + public void wardPanharmonicon() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Panharmonicon"); + addCard(Zone.BATTLEFIELD, playerA, "Young Red Dragon"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 7); + addCard(Zone.BATTLEFIELD, playerB, "Roaming Throne"); + addCard(Zone.HAND, playerA, "Scourge of Valkas"); + + setChoice(playerB, "Dragon"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scourge of Valkas"); + setChoice(playerA, "Whenever {this} or another Dragon"); + addTarget(playerA, "Roaming Throne"); + addTarget(playerA, "Roaming Throne"); + setChoice(playerB, "ward {2}"); + setChoice(playerA, "Yes"); + setChoice(playerA, "No"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerB, "Roaming Throne", 1); + assertDamageReceived(playerB, "Roaming Throne", 2); + } + + @Test + public void wardPanharmoniconCounter() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Panharmonicon"); + addCard(Zone.BATTLEFIELD, playerA, "Young Red Dragon"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerB, "Roaming Throne"); + addCard(Zone.HAND, playerA, "Scourge of Valkas"); + + setChoice(playerB, "Dragon"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scourge of Valkas"); + setChoice(playerA, "Whenever {this} or another Dragon"); + addTarget(playerA, "Roaming Throne"); + addTarget(playerA, "Roaming Throne"); + setChoice(playerB, "ward {2}"); + setChoice(playerA, "No"); + setChoice(playerA, "No"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerB, "Roaming Throne", 1); + assertDamageReceived(playerB, "Roaming Throne", 0); + } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/PawpatchRecruitTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/PawpatchRecruitTest.java new file mode 100644 index 00000000000..343d8267800 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/PawpatchRecruitTest.java @@ -0,0 +1,41 @@ +package org.mage.test.cards.single.blb; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class PawpatchRecruitTest extends CardTestPlayerBase { + + private static final String paw = "Pawpatch Recruit"; + private static final String cub = "Bear Cub"; + private static final String panharm = "Panharmonicon"; + private static final String prowler = "Chrome Prowler"; + + @Test + public void testCopiedTriggerAbility() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, paw); + addCard(Zone.BATTLEFIELD, playerB, cub); + addCard(Zone.BATTLEFIELD, playerA, panharm); + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.HAND, playerA, prowler); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, prowler); + setChoice(playerA, "When {this} enters"); + addTarget(playerA, paw); + addTarget(playerA, cub); + setChoice(playerB, "Whenever a creature"); + addTarget(playerB, paw); + addTarget(playerB, cub); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertTapped(paw, true); + assertTapped(cub, true); + assertCounterCount(playerB, paw, CounterType.P1P1, 1); + assertCounterCount(playerB, cub, CounterType.P1P1, 1); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/j22/AgrusKosEternalSoldierTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/j22/AgrusKosEternalSoldierTest.java index 0df627d9194..349dda8af12 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/j22/AgrusKosEternalSoldierTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/j22/AgrusKosEternalSoldierTest.java @@ -67,4 +67,35 @@ public class AgrusKosEternalSoldierTest extends CardTestPlayerBase { assertLife(playerB, 20); } + @Test + public void testCopiedTriggerAbility() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerB, agrus); + addCard(Zone.BATTLEFIELD, playerB, turtle); + addCard(Zone.BATTLEFIELD, playerB, firewalker); + addCard(Zone.BATTLEFIELD, playerB, "Plateau", 4); + addCard(Zone.BATTLEFIELD, playerA, "Panharmonicon"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 7); + addCard(Zone.HAND, playerA, "Smoldering Werewolf"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Smoldering Werewolf"); + setChoice(playerA, "When {this} enters, it deals"); + addTarget(playerA, agrus); + addTarget(playerA, agrus); + setChoice(playerB, true); // gain life + setChoice(playerB, "Whenever {this} becomes"); + setChoice(playerB, true); // pay to copy + setChoice(playerB, true); // pay to copy + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertDamageReceived(playerB, agrus, 2); + assertDamageReceived(playerB, turtle, 2); + assertDamageReceived(playerB, firewalker, 0); + assertLife(playerA, 20); + assertLife(playerB, 21); + } + } diff --git a/Mage/src/main/java/mage/abilities/common/BecomesTargetAnyTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BecomesTargetAnyTriggeredAbility.java index 9e5a7490e98..13377578b94 100644 --- a/Mage/src/main/java/mage/abilities/common/BecomesTargetAnyTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BecomesTargetAnyTriggeredAbility.java @@ -70,7 +70,7 @@ public class BecomesTargetAnyTriggeredAbility extends TriggeredAbilityImpl { if (permanent == null || !filterTarget.match(permanent, getControllerId(), this, game)) { return false; } - StackObject targetingObject = CardUtil.getTargetingStackObject(event, game); + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) { return false; } diff --git a/Mage/src/main/java/mage/abilities/common/BecomesTargetAttachedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BecomesTargetAttachedTriggeredAbility.java index 2995cacb11b..2c4c8d13a85 100644 --- a/Mage/src/main/java/mage/abilities/common/BecomesTargetAttachedTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BecomesTargetAttachedTriggeredAbility.java @@ -54,7 +54,7 @@ public class BecomesTargetAttachedTriggeredAbility extends TriggeredAbilityImpl if (enchantment == null || enchantment.getAttachedTo() == null || !event.getTargetId().equals(enchantment.getAttachedTo())) { return false; } - StackObject targetingObject = CardUtil.getTargetingStackObject(event, game); + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) { return false; } diff --git a/Mage/src/main/java/mage/abilities/common/BecomesTargetControllerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BecomesTargetControllerTriggeredAbility.java index c31b39b22d2..9b60880a21d 100644 --- a/Mage/src/main/java/mage/abilities/common/BecomesTargetControllerTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BecomesTargetControllerTriggeredAbility.java @@ -63,7 +63,7 @@ public class BecomesTargetControllerTriggeredAbility extends TriggeredAbilityImp return false; } } - StackObject targetingObject = CardUtil.getTargetingStackObject(event, game); + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) { return false; } diff --git a/Mage/src/main/java/mage/abilities/common/BecomesTargetSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BecomesTargetSourceTriggeredAbility.java index e1fcc98a772..0b410c7e078 100644 --- a/Mage/src/main/java/mage/abilities/common/BecomesTargetSourceTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/BecomesTargetSourceTriggeredAbility.java @@ -57,7 +57,7 @@ public class BecomesTargetSourceTriggeredAbility extends TriggeredAbilityImpl { if (!event.getTargetId().equals(getSourceId())) { return false; } - StackObject targetingObject = CardUtil.getTargetingStackObject(event, game); + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) { return false; } diff --git a/Mage/src/main/java/mage/abilities/keyword/WardAbility.java b/Mage/src/main/java/mage/abilities/keyword/WardAbility.java index b6bebf87769..c68ff475567 100644 --- a/Mage/src/main/java/mage/abilities/keyword/WardAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/WardAbility.java @@ -77,7 +77,7 @@ public class WardAbility extends TriggeredAbilityImpl { if (!getSourceId().equals(event.getTargetId())) { return false; } - StackObject targetingObject = CardUtil.getTargetingStackObject(event, game); + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); if (targetingObject == null || !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId())) { return false; } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 7ba17e4036f..b55d2a33da6 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1086,13 +1086,22 @@ public final class CardUtil { * @param game the Game from checkTrigger() or watch() * @return the StackObject which targeted the source, or null if not found */ - public static StackObject getTargetingStackObject(GameEvent event, Game game) { + public static StackObject getTargetingStackObject(String checkingReference, GameEvent event, Game game) { // In case of multiple simultaneous triggered abilities from the same source, // need to get the actual one that targeted, see #8026, #8378 // Also avoids triggering on cancelled selections, see #8802 + String stateKey = "targetedMap" + checkingReference; + Map> targetMap = (Map>) game.getState().getValue(stateKey); + // targetMap: key - targetId; value - Set of stackObject Ids + if (targetMap == null) { + targetMap = new HashMap<>(); + } else { + targetMap = new HashMap<>(targetMap); // must have new object reference if saved back to game state + } + Set targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>()); for (StackObject stackObject : game.getStack()) { Ability stackAbility = stackObject.getStackAbility(); - if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId())) { + if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId()) || targetingObjects.contains(stackObject.getId())) { continue; } if (CardUtil.getAllSelectedTargets(stackAbility, game).contains(event.getTargetId())) { diff --git a/Mage/src/main/java/mage/watchers/common/NumberOfTimesPermanentTargetedATurnWatcher.java b/Mage/src/main/java/mage/watchers/common/NumberOfTimesPermanentTargetedATurnWatcher.java index c97cf9436f3..accc6b4628c 100644 --- a/Mage/src/main/java/mage/watchers/common/NumberOfTimesPermanentTargetedATurnWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/NumberOfTimesPermanentTargetedATurnWatcher.java @@ -29,7 +29,7 @@ public class NumberOfTimesPermanentTargetedATurnWatcher extends Watcher { if (event.getType() != GameEvent.EventType.TARGETED) { return; } - StackObject targetingObject = CardUtil.getTargetingStackObject(event, game); + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getKey(), event, game); if (targetingObject == null || CardUtil.checkTargetedEventAlreadyUsed(this.getKey(), targetingObject, event, game)) { return; } From 97b53416886d06dad0b2f1e111f763fa6278ef89 Mon Sep 17 00:00:00 2001 From: androosss <101566943+androosss@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:44:16 +0200 Subject: [PATCH 016/133] - Implemented Highspire Bell-Ringer (#13495) - Commonized second spell condition --- .../src/mage/cards/h/HighspireBellRinger.java | 48 +++++++++++++++++++ .../src/mage/cards/r/RagingBattleMouse.java | 14 +----- .../src/mage/sets/TarkirDragonstorm.java | 1 + ...YouCastExactOneSpellThisTurnCondition.java | 19 ++++++++ 4 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/h/HighspireBellRinger.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/YouCastExactOneSpellThisTurnCondition.java diff --git a/Mage.Sets/src/mage/cards/h/HighspireBellRinger.java b/Mage.Sets/src/mage/cards/h/HighspireBellRinger.java new file mode 100644 index 00000000000..c42a4e7c72c --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HighspireBellRinger.java @@ -0,0 +1,48 @@ +package mage.cards.h; + +import java.util.UUID; +import mage.MageInt; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.YouCastExactOneSpellThisTurnCondition; +import mage.abilities.decorator.ConditionalCostModificationEffect; +import mage.abilities.effects.common.cost.SpellsCostReductionControllerEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; + +/** + * + * @author androosss + */ +public final class HighspireBellRinger extends CardImpl { + + public HighspireBellRinger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}"); + + this.subtype.add(SubType.DJINN); + this.subtype.add(SubType.MONK); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // The second spell you cast each turn costs {1} less to cast. + this.addAbility(new SimpleStaticAbility(new ConditionalCostModificationEffect( + new SpellsCostReductionControllerEffect(StaticFilters.FILTER_CARD, 1), + YouCastExactOneSpellThisTurnCondition.instance, "the second spell you cast each turn costs {1} less to cast" + ))); + } + + private HighspireBellRinger(final HighspireBellRinger card) { + super(card); + } + + @Override + public HighspireBellRinger copy() { + return new HighspireBellRinger(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RagingBattleMouse.java b/Mage.Sets/src/mage/cards/r/RagingBattleMouse.java index 3f7ae7785d6..fbd10b2475b 100644 --- a/Mage.Sets/src/mage/cards/r/RagingBattleMouse.java +++ b/Mage.Sets/src/mage/cards/r/RagingBattleMouse.java @@ -4,7 +4,6 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.Condition; import mage.abilities.condition.common.CelebrationCondition; import mage.abilities.decorator.ConditionalCostModificationEffect; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; @@ -14,10 +13,9 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.StaticFilters; -import mage.game.Game; import mage.target.common.TargetControlledCreaturePermanent; import mage.watchers.common.PermanentsEnteredBattlefieldWatcher; -import mage.watchers.common.SpellsCastWatcher; +import mage.abilities.condition.common.YouCastExactOneSpellThisTurnCondition; import java.util.UUID; @@ -63,13 +61,3 @@ public final class RagingBattleMouse extends CardImpl { return new RagingBattleMouse(this); } } - -enum YouCastExactOneSpellThisTurnCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - return watcher != null && watcher.getSpellsCastThisTurn(source.getControllerId()).size() == 1; - } -} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 1b6d33d1b3c..91bd8ef6d1d 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -104,6 +104,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Gurmag Rakshasa", 81, Rarity.UNCOMMON, mage.cards.g.GurmagRakshasa.class)); cards.add(new SetCardInfo("Hardened Tactician", 191, Rarity.UNCOMMON, mage.cards.h.HardenedTactician.class)); cards.add(new SetCardInfo("Heritage Reclamation", 145, Rarity.COMMON, mage.cards.h.HeritageReclamation.class)); + cards.add(new SetCardInfo("Highspire Bell-Ringer", 47, Rarity.COMMON, mage.cards.h.HighspireBellRinger.class)); cards.add(new SetCardInfo("Humbling Elder", 48, Rarity.COMMON, mage.cards.h.HumblingElder.class)); cards.add(new SetCardInfo("Iceridge Serpent", 49, Rarity.COMMON, mage.cards.i.IceridgeSerpent.class)); cards.add(new SetCardInfo("Inevitable Defeat", 194, Rarity.RARE, mage.cards.i.InevitableDefeat.class)); diff --git a/Mage/src/main/java/mage/abilities/condition/common/YouCastExactOneSpellThisTurnCondition.java b/Mage/src/main/java/mage/abilities/condition/common/YouCastExactOneSpellThisTurnCondition.java new file mode 100644 index 00000000000..98aa5c75dc0 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/YouCastExactOneSpellThisTurnCondition.java @@ -0,0 +1,19 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.watchers.common.SpellsCastWatcher; + +/** + * @author androosss + */ +public enum YouCastExactOneSpellThisTurnCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + return watcher != null && watcher.getSpellsCastThisTurn(source.getControllerId()).size() == 1; + } +} From fcba64b8c0432af395b96ea04629044cbf4c1c97 Mon Sep 17 00:00:00 2001 From: androosss <101566943+androosss@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:45:58 +0200 Subject: [PATCH 017/133] [TDM] Implement Glacierwood Siege (#13499) * implemented glacierwood siege * GraphicCardInfo fix --- .../src/mage/cards/g/GlacierwoodSiege.java | 66 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 8 ++- 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/g/GlacierwoodSiege.java diff --git a/Mage.Sets/src/mage/cards/g/GlacierwoodSiege.java b/Mage.Sets/src/mage/cards/g/GlacierwoodSiege.java new file mode 100644 index 00000000000..693845f7cd6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GlacierwoodSiege.java @@ -0,0 +1,66 @@ +package mage.cards.g; + +import java.util.UUID; + +import mage.abilities.TriggeredAbility; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.condition.common.ModeChoiceSourceCondition; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.MillCardsTargetEffect; +import mage.abilities.effects.common.ruleModifying.PlayFromGraveyardControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterSpell; +import mage.filter.predicate.Predicates; +import mage.target.TargetPlayer; + +/** + * + * @author androosss + */ +public final class GlacierwoodSiege extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("an instant or sorcery spell"); + + static { + filter.add(Predicates.or( + CardType.INSTANT.getPredicate(), + CardType.SORCERY.getPredicate())); + } + + public GlacierwoodSiege(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{G}{U}"); + + // As this enchantment enters, choose Temur or Sultai. + this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Temur or Sultai?", "Temur", "Sultai"), null, + "As {this} enters, choose Temur or Sultai.", "")); + + // * Temur -- Whenever you cast an instant or sorcery spell, target player mills four cards. + TriggeredAbility temurAbility = new SpellCastControllerTriggeredAbility(new MillCardsTargetEffect(4), filter, false); + temurAbility.addTarget(new TargetPlayer()); + this.addAbility(new ConditionalTriggeredAbility( + temurAbility, + new ModeChoiceSourceCondition("Temur"), + "• Temur — Whenever you cast an instant or sorcery spell, target player mills four cards.")); + + // * Sultai -- You may play lands from your graveyard. + this.addAbility(new SimpleStaticAbility(new ConditionalAsThoughEffect( + PlayFromGraveyardControllerEffect.playLands(), + new ModeChoiceSourceCondition("Sultai")).setText("• Sultai — You may play lands from your graveyard.") + )); + } + + private GlacierwoodSiege(final GlacierwoodSiege card) { + super(card); + } + + @Override + public GlacierwoodSiege copy() { + return new GlacierwoodSiege(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 91bd8ef6d1d..500d5c49860 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -1,12 +1,12 @@ package mage.sets; +import java.util.Arrays; +import java.util.List; + import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ @@ -99,6 +99,8 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Frontier Bivouac", 256, Rarity.UNCOMMON, mage.cards.f.FrontierBivouac.class)); cards.add(new SetCardInfo("Frontline Rush", 186, Rarity.UNCOMMON, mage.cards.f.FrontlineRush.class)); cards.add(new SetCardInfo("Glacial Dragonhunt", 188, Rarity.UNCOMMON, mage.cards.g.GlacialDragonhunt.class)); + cards.add(new SetCardInfo("Glacierwood Siege", 189, Rarity.RARE, mage.cards.g.GlacierwoodSiege.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Glacierwood Siege", 386, Rarity.RARE, mage.cards.g.GlacierwoodSiege.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Great Arashin City", 257, Rarity.RARE, mage.cards.g.GreatArashinCity.class)); cards.add(new SetCardInfo("Gurmag Nightwatch", 190, Rarity.COMMON, mage.cards.g.GurmagNightwatch.class)); cards.add(new SetCardInfo("Gurmag Rakshasa", 81, Rarity.UNCOMMON, mage.cards.g.GurmagRakshasa.class)); From 5a377017da276fac485b79c8778b727791749242 Mon Sep 17 00:00:00 2001 From: androosss <101566943+androosss@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:46:07 +0200 Subject: [PATCH 018/133] implemented frostcliff siege (#13500) --- .../src/mage/cards/f/FrostcliffSiege.java | 74 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 2 + 2 files changed, 76 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FrostcliffSiege.java diff --git a/Mage.Sets/src/mage/cards/f/FrostcliffSiege.java b/Mage.Sets/src/mage/cards/f/FrostcliffSiege.java new file mode 100644 index 00000000000..61ce64fc2b5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FrostcliffSiege.java @@ -0,0 +1,74 @@ +package mage.cards.f; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.OneOrMoreCombatDamagePlayerTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.ModeChoiceSourceCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.filter.StaticFilters; + +/** + * + * @author androosss + */ +public final class FrostcliffSiege extends CardImpl { + + private static final String rule1Trigger = "• Jeskai — Whenever one or more creatures you control deal combat damage to a player, draw a card."; + + public FrostcliffSiege(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}{R}"); + + + // As this enchantment enters, choose Jeskai or Temur. + this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Jeskai or Temur?", "Jeskai", "Temur"), null, + "As {this} enters, choose Jeskai or Temur.", "")); + + // * Jeskai -- Whenever one or more creatures you control deal combat damage to a player, draw a card. + this.addAbility(new ConditionalTriggeredAbility( + new OneOrMoreCombatDamagePlayerTriggeredAbility(new DrawCardSourceControllerEffect(1)), + new ModeChoiceSourceCondition("Jeskai"), + rule1Trigger)); + + // * Temur -- Creatures you control get +1/+0 and have trample and haste. + Ability temurAbility = new SimpleStaticAbility(new ConditionalContinuousEffect( + new BoostControlledEffect(1, 0, Duration.WhileOnBattlefield), + new ModeChoiceSourceCondition("Temur"), + "• Temur — Creatures you control get +1/+0" + )); + temurAbility.addEffect(new ConditionalContinuousEffect( + new GainAbilityControlledEffect(TrampleAbility.getInstance(), Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURES), + new ModeChoiceSourceCondition("Temur"), + "and have trample" + )); + temurAbility.addEffect(new ConditionalContinuousEffect( + new GainAbilityControlledEffect(HasteAbility.getInstance(), Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURES), + new ModeChoiceSourceCondition("Temur"), + "and haste." + )); + this.addAbility(temurAbility); + } + + private FrostcliffSiege(final FrostcliffSiege card) { + super(card); + } + + @Override + public FrostcliffSiege copy() { + return new FrostcliffSiege(this); + } + +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 500d5c49860..ebb85007cdf 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -98,6 +98,8 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Fresh Start", 46, Rarity.UNCOMMON, mage.cards.f.FreshStart.class)); cards.add(new SetCardInfo("Frontier Bivouac", 256, Rarity.UNCOMMON, mage.cards.f.FrontierBivouac.class)); cards.add(new SetCardInfo("Frontline Rush", 186, Rarity.UNCOMMON, mage.cards.f.FrontlineRush.class)); + cards.add(new SetCardInfo("Frostcliff Siege", 187, Rarity.RARE, mage.cards.f.FrostcliffSiege.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Frostcliff Siege", 385, Rarity.RARE, mage.cards.f.FrostcliffSiege.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Glacial Dragonhunt", 188, Rarity.UNCOMMON, mage.cards.g.GlacialDragonhunt.class)); cards.add(new SetCardInfo("Glacierwood Siege", 189, Rarity.RARE, mage.cards.g.GlacierwoodSiege.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Glacierwood Siege", 386, Rarity.RARE, mage.cards.g.GlacierwoodSiege.class, NON_FULL_USE_VARIOUS)); From 508c5348845c6fdf66068b81af7e818a3abc92db Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 5 Apr 2025 13:57:40 -0400 Subject: [PATCH 019/133] [TDM] Implement Temur Battlecrier --- .../src/mage/cards/t/TemurBattlecrier.java | 104 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 7 +- 2 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TemurBattlecrier.java diff --git a/Mage.Sets/src/mage/cards/t/TemurBattlecrier.java b/Mage.Sets/src/mage/cards/t/TemurBattlecrier.java new file mode 100644 index 00000000000..03c8765bbcf --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TemurBattlecrier.java @@ -0,0 +1,104 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.game.Game; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TemurBattlecrier extends CardImpl { + + public TemurBattlecrier(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{U}{R}"); + + this.subtype.add(SubType.ORC); + this.subtype.add(SubType.RANGER); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // During your turn, spells you cast cost {1} less to cast for each creature you control with power 4 or greater. + this.addAbility(new SimpleStaticAbility(new TemurBattlecrierEffect()).addHint(TemurBattlecrierEffect.getHint())); + } + + private TemurBattlecrier(final TemurBattlecrier card) { + super(card); + } + + @Override + public TemurBattlecrier copy() { + return new TemurBattlecrier(this); + } +} + +class TemurBattlecrierEffect extends CostModificationEffectImpl { + + private static final FilterPermanent filter = new FilterControlledCreaturePermanent(); + + static { + filter.add(new PowerPredicate(ComparisonType.MORE_THAN, 3)); + } + + private static final Hint hint = new ValueHint( + "Creatures you control with power 4 or greater", new PermanentsOnBattlefieldCount(filter) + ); + + public static Hint getHint() { + return hint; + } + + TemurBattlecrierEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.REDUCE_COST); + staticText = "during your turn, spells you cast cost {1} less to cast " + + "for each creature you control with power 4 or greater"; + } + + private TemurBattlecrierEffect(final TemurBattlecrierEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source, Ability abilityToModify) { + Ability spellAbility = abilityToModify; + if (spellAbility == null) { + return false; + } + int amount = game.getBattlefield().count(filter, source.getControllerId(), source, game); + if (amount > 0) { + CardUtil.reduceCost(spellAbility, amount); + } + return true; + } + + @Override + public boolean applies(Ability abilityToModify, Ability source, Game game) { + if (!(abilityToModify instanceof SpellAbility) + || !abilityToModify.isControlledBy(source.getControllerId()) + || !game.isActivePlayer(source.getControllerId())) { + return false; + } + Card spellCard = ((SpellAbility) abilityToModify).getCharacteristics(game); + return spellCard != null; + } + + @Override + public TemurBattlecrierEffect copy() { + return new TemurBattlecrierEffect(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index ebb85007cdf..b5fe3c80cc2 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -1,12 +1,12 @@ package mage.sets; -import java.util.Arrays; -import java.util.List; - import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.Arrays; +import java.util.List; + /** * @author TheElk801 */ @@ -209,6 +209,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Swiftwater Cliffs", 268, Rarity.COMMON, mage.cards.s.SwiftwaterCliffs.class)); cards.add(new SetCardInfo("Teeming Dragonstorm", 30, Rarity.UNCOMMON, mage.cards.t.TeemingDragonstorm.class)); cards.add(new SetCardInfo("Tempest Hawk", 31, Rarity.COMMON, mage.cards.t.TempestHawk.class)); + cards.add(new SetCardInfo("Temur Battlecrier", 228, Rarity.RARE, mage.cards.t.TemurBattlecrier.class)); cards.add(new SetCardInfo("Temur Devotee", 61, Rarity.COMMON, mage.cards.t.TemurDevotee.class)); cards.add(new SetCardInfo("Temur Monument", 248, Rarity.UNCOMMON, mage.cards.t.TemurMonument.class)); cards.add(new SetCardInfo("Temur Tawnyback", 229, Rarity.COMMON, mage.cards.t.TemurTawnyback.class)); From eac265f4f7ca8682d994d84dfc1a001ee055ec03 Mon Sep 17 00:00:00 2001 From: padfoothelix Date: Sat, 5 Apr 2025 20:10:45 +0200 Subject: [PATCH 020/133] [WHO] Implement Fugitive of the Judoon (#13467) --- .../sources/ScryfallImageSupportTokens.java | 4 +- .../src/mage/cards/f/FugitiveOfTheJudoon.java | 82 +++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 4 +- .../game/permanent/token/AlienRhinoToken.java | 34 ++++++++ .../token/Human11WithWard2Token.java | 33 ++++++++ Mage/src/main/resources/tokens-database.txt | 4 +- 6 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/f/FugitiveOfTheJudoon.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/AlienRhinoToken.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/Human11WithWard2Token.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java index 9441b6aa677..a6479a6c40a 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java @@ -2183,6 +2183,7 @@ public class ScryfallImageSupportTokens { // WHO put("WHO/Alien", "https://api.scryfall.com/cards/twho/2?format=image"); put("WHO/Alien Insect", "https://api.scryfall.com/cards/twho/19/en?format=image"); + put("WHO/Alien Rhino", "https://api.scryfall.com/cards/twho/3/en?format=image"); put("WHO/Alien Salamander", "https://api.scryfall.com/cards/twho/16?format=image"); put("WHO/Alien Warrior", "https://api.scryfall.com/cards/twho/14?format=image"); put("WHO/Beast", "https://api.scryfall.com/cards/twho/17?format=image"); @@ -2196,7 +2197,8 @@ public class ScryfallImageSupportTokens { put("WHO/Food/2", "https://api.scryfall.com/cards/twho/26?format=image"); put("WHO/Food/3", "https://api.scryfall.com/cards/twho/27?format=image"); put("WHO/Horse", "https://api.scryfall.com/cards/twho/4/en?format=image"); - put("WHO/Human", "https://api.scryfall.com/cards/twho/5?format=image"); + put("WHO/Human/1", "https://api.scryfall.com/cards/twho/6/en?format=image"); + put("WHO/Human/2", "https://api.scryfall.com/cards/twho/5/en?format=image"); put("WHO/Human Noble", "https://api.scryfall.com/cards/twho/7/en?format=image"); put("WHO/Mark of the Rani", "https://api.scryfall.com/cards/twho/15?format=image"); put("WHO/Soldier", "https://api.scryfall.com/cards/twho/8?format=image"); diff --git a/Mage.Sets/src/mage/cards/f/FugitiveOfTheJudoon.java b/Mage.Sets/src/mage/cards/f/FugitiveOfTheJudoon.java new file mode 100644 index 00000000000..05e2b69c45a --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FugitiveOfTheJudoon.java @@ -0,0 +1,82 @@ +package mage.cards.f; + +import java.util.UUID; +import mage.abilities.common.SagaAbility; +import mage.abilities.costs.CompositeCost; +import mage.abilities.costs.common.ExileTargetCost; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.search.SearchLibraryPutInPlayEffect; +import mage.abilities.effects.keyword.InvestigateEffect; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SagaChapter; +import mage.filter.FilterCard; +import mage.filter.common.FilterControlledArtifactPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.game.permanent.token.AlienRhinoToken; +import mage.game.permanent.token.Human11WithWard2Token; +import mage.target.common.TargetCardInLibrary; +import mage.target.common.TargetControlledPermanent; + + +/** + * + * @author padfoothelix + */ +public final class FugitiveOfTheJudoon extends CardImpl { + + private static final FilterCard filter = new FilterCard("a Doctor card"); + private static final FilterControlledPermanent filterHuman = new FilterControlledPermanent(SubType.HUMAN,"a Human you control"); + private static final FilterControlledArtifactPermanent filterArtifact = new FilterControlledArtifactPermanent("an artifact you control"); + + static { + filter.add(SubType.DOCTOR.getPredicate()); + } + + public FugitiveOfTheJudoon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{G}"); + + this.subtype.add(SubType.SAGA); + + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.) + SagaAbility sagaAbility = new SagaAbility(this); + + // I -- Create a 1/1 white Human creature token with ward {2} and a 4/4 white Alien Rhino creature token. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_I, + new CreateTokenEffect(new Human11WithWard2Token()).withAdditionalTokens(new AlienRhinoToken()) + ); + + // II -- Investigate. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_II, new InvestigateEffect()); + + // III -- You may exile a Human you control and an artifact you control. If you do, search your library for a Doctor card, put it onto the battlefield, then shuffle. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_III, + new DoIfCostPaid( + new SearchLibraryPutInPlayEffect(new TargetCardInLibrary(filter)), + new CompositeCost( + new ExileTargetCost(new TargetControlledPermanent(1, 1, filterHuman, true)), + new ExileTargetCost(new TargetControlledPermanent(1, 1, filterArtifact, true)), + "exile a Human you control and an artifact you control" + ), + "Exile a Human and an artifact ?" + ) + ); + + this.addAbility(sagaAbility); + + } + + private FugitiveOfTheJudoon(final FugitiveOfTheJudoon card) { + super(card); + } + + @Override + public FugitiveOfTheJudoon copy() { + return new FugitiveOfTheJudoon(this); + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index e03ac42718d..61e361fa3a2 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -376,8 +376,8 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("Frostboil Snarl", 282, Rarity.RARE, mage.cards.f.FrostboilSnarl.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Frostboil Snarl", 498, Rarity.RARE, mage.cards.f.FrostboilSnarl.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Frostboil Snarl", 873, Rarity.RARE, mage.cards.f.FrostboilSnarl.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Fugitive of the Judoon", 103, Rarity.RARE, mage.cards.f.FugitiveOfTheJudoon.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Fugitive of the Judoon", 708, Rarity.RARE, mage.cards.f.FugitiveOfTheJudoon.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Fugitive of the Judoon", 103, Rarity.RARE, mage.cards.f.FugitiveOfTheJudoon.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Fugitive of the Judoon", 708, Rarity.RARE, mage.cards.f.FugitiveOfTheJudoon.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Furycalm Snarl", 1090, Rarity.RARE, mage.cards.f.FurycalmSnarl.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Furycalm Snarl", 283, Rarity.RARE, mage.cards.f.FurycalmSnarl.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Furycalm Snarl", 499, Rarity.RARE, mage.cards.f.FurycalmSnarl.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage/src/main/java/mage/game/permanent/token/AlienRhinoToken.java b/Mage/src/main/java/mage/game/permanent/token/AlienRhinoToken.java new file mode 100644 index 00000000000..43b6fbd5da4 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/AlienRhinoToken.java @@ -0,0 +1,34 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.PreventDamageToSourceEffect; +import mage.abilities.keyword.VanishingAbility; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Duration; + +/** + * @author padfoothelix + */ +public final class AlienRhinoToken extends TokenImpl { + + public AlienRhinoToken() { + super("Alien Rhino Token", "4/4 white Alien Rhino creature token"); + cardType.add(CardType.CREATURE); + color.setWhite(true); + subtype.add(SubType.ALIEN); + subtype.add(SubType.RHINO); + power = new MageInt(4); + toughness = new MageInt(4); + } + + private AlienRhinoToken(final AlienRhinoToken token) { + super(token); + } + + @Override + public AlienRhinoToken copy() { + return new AlienRhinoToken(this); + } +} diff --git a/Mage/src/main/java/mage/game/permanent/token/Human11WithWard2Token.java b/Mage/src/main/java/mage/game/permanent/token/Human11WithWard2Token.java new file mode 100644 index 00000000000..a39848fcb5c --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/Human11WithWard2Token.java @@ -0,0 +1,33 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.keyword.WardAbility; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Duration; + +/** + * @author padfoothelix + */ +public final class Human11WithWard2Token extends TokenImpl { + + public Human11WithWard2Token() { + super("Human Token", "1/1 white Human creature token with ward {2}"); + cardType.add(CardType.CREATURE); + color.setWhite(true); + subtype.add(SubType.HUMAN); + power = new MageInt(1); + toughness = new MageInt(1); + this.addAbility(new WardAbility(new GenericManaCost(2))); + } + + private Human11WithWard2Token(final Human11WithWard2Token token) { + super(token); + } + + @Override + public Human11WithWard2Token copy() { + return new Human11WithWard2Token(this); + } +} diff --git a/Mage/src/main/resources/tokens-database.txt b/Mage/src/main/resources/tokens-database.txt index 0b77e47469c..8dc1ecf4e63 100644 --- a/Mage/src/main/resources/tokens-database.txt +++ b/Mage/src/main/resources/tokens-database.txt @@ -2235,6 +2235,7 @@ # WHO |Generate|TOK:WHO|Alien|||AlienToken| |Generate|TOK:WHO|Alien Insect|||AlienInsectToken| +|Generate|TOK:WHO|Alien Rhino|||AlienRhinoToken| |Generate|TOK:WHO|Alien Salamander|||AlienSalamanderToken| |Generate|TOK:WHO|Alien Warrior|||AlienWarriorToken| |Generate|TOK:WHO|Beast|||BeastToken| @@ -2248,7 +2249,8 @@ |Generate|TOK:WHO|Food|2||FoodToken| |Generate|TOK:WHO|Food|3||FoodToken| |Generate|TOK:WHO|Horse|||TheGirlInTheFireplaceHorseToken| -|Generate|TOK:WHO|Human|||TheEleventhHourToken| +|Generate|TOK:WHO|Human|1||Human11WithWard2Token| +|Generate|TOK:WHO|Human|2||TheEleventhHourToken| |Generate|TOK:WHO|Human Noble|||TheGirlInTheFireplaceHumanNobleToken| |Generate|TOK:WHO|Mark of the Rani|||MarkOfTheRaniToken| |Generate|TOK:WHO|Soldier|||SoldierToken| From b6667d7e458287d84b59811d6d701b1d9b5ac7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Krist=C3=B3f?= <20043803+balazskristof@users.noreply.github.com> Date: Sat, 5 Apr 2025 20:11:02 +0200 Subject: [PATCH 021/133] [ACR] Implement Layla Hassan (#13471) --- Mage.Sets/src/mage/cards/l/LaylaHassan.java | 103 ++++++++++++++++++++ Mage.Sets/src/mage/sets/AssassinsCreed.java | 6 +- 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/l/LaylaHassan.java diff --git a/Mage.Sets/src/mage/cards/l/LaylaHassan.java b/Mage.Sets/src/mage/cards/l/LaylaHassan.java new file mode 100644 index 00000000000..b3eaf7c2a2e --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LaylaHassan.java @@ -0,0 +1,103 @@ +package mage.cards.l; + +import java.util.UUID; +import mage.MageInt; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.common.FilterHistoricCard; +import mage.game.Game; +import mage.game.events.DamagedBatchForOnePlayerEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCardInYourGraveyard; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.OneOrMoreDamagePlayerTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; + +/** + * @author balazskristof + */ +public final class LaylaHassan extends CardImpl { + + private static final FilterHistoricCard filter = new FilterHistoricCard("historic card from your graveyard"); + + public LaylaHassan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ASSASSIN); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // First strike + this.addAbility(FirstStrikeAbility.getInstance()); + + // When Layla Hassan enters the battlefield and whenever one or more Assassins you control deal combat damage to a player, return target historic card from your graveyard to your hand. + Ability ability = new LaylaHassanTriggeredAbility(new ReturnFromGraveyardToHandTargetEffect()); + ability.addTarget(new TargetCardInYourGraveyard(filter)); + this.addAbility(ability); + } + + private LaylaHassan(final LaylaHassan card) { + super(card); + } + + @Override + public LaylaHassan copy() { + return new LaylaHassan(this); + } +} + +class LaylaHassanTriggeredAbility extends OneOrMoreDamagePlayerTriggeredAbility { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("Assassins"); + + static { + filter.add(SubType.ASSASSIN.getPredicate()); + } + + public LaylaHassanTriggeredAbility(Effect effect) { + super(effect, filter, true, true); + } + + private LaylaHassanTriggeredAbility(final LaylaHassanTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD + || event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD + && event.getTargetId().equals(getSourceId())) { + return true; + } + if (event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PLAYER) { + return super.checkTrigger(event, game); + } + return false; + } + + @Override + public String getRule() { + return "When {this} enters the battlefield and whenever one or more Assassins you control deal combat damage to a player, return target historic card from your graveyard to your hand."; + } + + @Override + public LaylaHassanTriggeredAbility copy() { + return new LaylaHassanTriggeredAbility(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AssassinsCreed.java b/Mage.Sets/src/mage/sets/AssassinsCreed.java index 63b30bf8a39..7fb2fcf3838 100644 --- a/Mage.Sets/src/mage/sets/AssassinsCreed.java +++ b/Mage.Sets/src/mage/sets/AssassinsCreed.java @@ -189,9 +189,9 @@ public final class AssassinsCreed extends ExpansionSet { cards.add(new SetCardInfo("Kassandra, Eagle Bearer", 59, Rarity.MYTHIC, mage.cards.k.KassandraEagleBearer.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Keen-Eyed Raven", 279, Rarity.UNCOMMON, mage.cards.k.KeenEyedRaven.class)); cards.add(new SetCardInfo("Labyrinth Adversary", 290, Rarity.UNCOMMON, mage.cards.l.LabyrinthAdversary.class)); - //cards.add(new SetCardInfo("Layla Hassan", 127, Rarity.RARE, mage.cards.l.LaylaHassan.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Layla Hassan", 177, Rarity.RARE, mage.cards.l.LaylaHassan.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Layla Hassan", 7, Rarity.RARE, mage.cards.l.LaylaHassan.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Layla Hassan", 127, Rarity.RARE, mage.cards.l.LaylaHassan.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Layla Hassan", 177, Rarity.RARE, mage.cards.l.LaylaHassan.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Layla Hassan", 7, Rarity.RARE, mage.cards.l.LaylaHassan.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Leonardo da Vinci", "118z", Rarity.MYTHIC, mage.cards.l.LeonardoDaVinci.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Leonardo da Vinci", 118, Rarity.MYTHIC, mage.cards.l.LeonardoDaVinci.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Leonardo da Vinci", 193, Rarity.MYTHIC, mage.cards.l.LeonardoDaVinci.class, NON_FULL_USE_VARIOUS)); From cdebbe151bdc8266fc953e752574bd5dfcbd360e Mon Sep 17 00:00:00 2001 From: ssk97 Date: Sat, 5 Apr 2025 11:11:55 -0700 Subject: [PATCH 022/133] Fix for disconnect causing instant loss (#13390) --- Mage.Server/src/main/java/mage/server/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Server/src/main/java/mage/server/Main.java b/Mage.Server/src/main/java/mage/server/Main.java index 9503aacb383..8a6e06ac66b 100644 --- a/Mage.Server/src/main/java/mage/server/Main.java +++ b/Mage.Server/src/main/java/mage/server/Main.java @@ -370,7 +370,7 @@ public final class Main { // no need to keep session logger.info("CLIENT DISCONNECTED - " + sessionInfo); logger.debug("- cause: client called disconnect command"); - managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.DisconnectedByUser, true); + managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true); } else if (throwable == null) { // lease timeout (ping), so server lost connection with a client // must keep tables From 6f524b69c012352710e8c3a4e250f03b56a82966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Krist=C3=B3f?= <20043803+balazskristof@users.noreply.github.com> Date: Sat, 5 Apr 2025 20:13:23 +0200 Subject: [PATCH 023/133] [FIN] Implement Stiltzkin, Moogle Merchant (#13443) --- .../mage/cards/s/StiltzkinMoogleMerchant.java | 95 +++++++++++++++++++ Mage.Sets/src/mage/sets/FinalFantasy.java | 2 + .../src/main/java/mage/constants/SubType.java | 1 + 3 files changed, 98 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StiltzkinMoogleMerchant.java diff --git a/Mage.Sets/src/mage/cards/s/StiltzkinMoogleMerchant.java b/Mage.Sets/src/mage/cards/s/StiltzkinMoogleMerchant.java new file mode 100644 index 00000000000..c361ffaa710 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StiltzkinMoogleMerchant.java @@ -0,0 +1,95 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetControlledPermanent; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainControlTargetEffect; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; + +/** + * @author balazskristof + */ +public final class StiltzkinMoogleMerchant extends CardImpl { + + public StiltzkinMoogleMerchant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.MOOGLE); + this.power = new MageInt(1); + this.toughness = new MageInt(2); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // {2}, {T}: Target opponent gains control of another target permanent you control. If they do, you draw a card. + Ability ability = new SimpleActivatedAbility(new StiltzkinMoogleMerchantEffect(), new GenericManaCost(2)); + ability.addCost(new TapSourceCost()); + ability.addTarget(new TargetOpponent()); + ability.addTarget(new TargetControlledPermanent(StaticFilters.FILTER_CONTROLLED_ANOTHER_PERMANENT)); + this.addAbility(ability); + } + + private StiltzkinMoogleMerchant(final StiltzkinMoogleMerchant card) { + super(card); + } + + @Override + public StiltzkinMoogleMerchant copy() { + return new StiltzkinMoogleMerchant(this); + } +} + +class StiltzkinMoogleMerchantEffect extends OneShotEffect { + + StiltzkinMoogleMerchantEffect() { + super(Outcome.Benefit); + staticText = "Target opponent gains control of another target permanent you control. If they do, you draw a card."; + } + + private StiltzkinMoogleMerchantEffect(StiltzkinMoogleMerchantEffect effect) { + super(effect); + } + + @Override + public StiltzkinMoogleMerchantEffect copy() { + return new StiltzkinMoogleMerchantEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getTargets().get(1).getFirstTarget()); + if (permanent == null) { + return false; + } + UUID opponent = getTargetPointer().getFirst(game, source); + game.addEffect(new GainControlTargetEffect( + Duration.Custom, true, opponent + ).setTargetPointer(new FixedTarget(permanent.getId(), game)), source); + game.processAction(); + if (permanent.isControlledBy(opponent)) { + new DrawCardSourceControllerEffect(1).apply(game, source); + return true; + } + return false; + } +} + diff --git a/Mage.Sets/src/mage/sets/FinalFantasy.java b/Mage.Sets/src/mage/sets/FinalFantasy.java index bde77f989dc..463445ffd72 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasy.java +++ b/Mage.Sets/src/mage/sets/FinalFantasy.java @@ -26,6 +26,8 @@ public final class FinalFantasy extends ExpansionSet { cards.add(new SetCardInfo("Sin, Spira's Punishment", 242, Rarity.RARE, mage.cards.s.SinSpirasPunishment.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sin, Spira's Punishment", 348, Rarity.RARE, mage.cards.s.SinSpirasPunishment.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sin, Spira's Punishment", 508, Rarity.RARE, mage.cards.s.SinSpirasPunishment.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Stiltzkin, Moogle Merchant", 34, Rarity.RARE, mage.cards.s.StiltzkinMoogleMerchant.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Stiltzkin, Moogle Merchant", 327, Rarity.RARE, mage.cards.s.StiltzkinMoogleMerchant.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Summon: Shiva", 78, Rarity.UNCOMMON, mage.cards.s.SummonShiva.class)); cards.add(new SetCardInfo("Tonberry", 122, Rarity.UNCOMMON, mage.cards.t.Tonberry.class)); } diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index 5037f60d44a..0fcfafaa0c4 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -274,6 +274,7 @@ public enum SubType { MONGOOSE("Mongoose", SubTypeSet.CreatureType), MONK("Monk", SubTypeSet.CreatureType), MONKEY("Monkey", SubTypeSet.CreatureType), + MOOGLE("Moogle", SubTypeSet.CreatureType), MOONFOLK("Moonfolk", SubTypeSet.CreatureType), MOUNT("Mount", SubTypeSet.CreatureType), MOUSE("Mouse", SubTypeSet.CreatureType), From 6fcb592557c784376d75624d582be96f07b0c056 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Sat, 5 Apr 2025 15:47:31 -0400 Subject: [PATCH 024/133] [TDM] Implement Sidisi, Regent of the Mire --- .../mage/cards/s/SidisiRegentOfTheMire.java | 130 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 131 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SidisiRegentOfTheMire.java diff --git a/Mage.Sets/src/mage/cards/s/SidisiRegentOfTheMire.java b/Mage.Sets/src/mage/cards/s/SidisiRegentOfTheMire.java new file mode 100644 index 00000000000..ac37da8f169 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SidisiRegentOfTheMire.java @@ -0,0 +1,130 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.VariableCostImpl; +import mage.abilities.costs.VariableCostType; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.targetadjustment.ManaValueTargetAdjuster; +import mage.util.CardUtil; + +/** + * + * @author Grath + */ +public final class SidisiRegentOfTheMire extends CardImpl { + + public SidisiRegentOfTheMire(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.SNAKE); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // {T}, Sacrifice a creature you control with mana value X other than Sidisi: Return target creature card with mana value X plus 1 from your graveyard to the battlefield. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility( + Zone.BATTLEFIELD, new ReturnFromGraveyardToBattlefieldTargetEffect(), new TapSourceCost() + ); + ability.addCost(new SidisiRegentOfTheMireCost()); + ability.addTarget(new TargetCardInYourGraveyard(new FilterCreatureCard("creature card with mana value X plus 1 from your graveyard"))); + ability.setTargetAdjuster(new SidisiRegentOfTheMireAdjuster()); + this.addAbility(ability); + } + + private SidisiRegentOfTheMire(final SidisiRegentOfTheMire card) { + super(card); + } + + @Override + public SidisiRegentOfTheMire copy() { + return new SidisiRegentOfTheMire(this); + } +} + +enum GetXPlusOneValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0) + 1; + } + + @Override + public GetXPlusOneValue copy() { + return GetXPlusOneValue.instance; + } + + @Override + public String toString() { + return "X + 1"; + } + + @Override + public String getMessage() { + return ""; + } +} + +class SidisiRegentOfTheMireAdjuster extends ManaValueTargetAdjuster { + + public SidisiRegentOfTheMireAdjuster() { + super(GetXPlusOneValue.instance, ComparisonType.EQUAL_TO); + } + +} + +class SidisiRegentOfTheMireCost extends VariableCostImpl { + + public SidisiRegentOfTheMireCost() { + super(VariableCostType.NORMAL, "mana value X"); + this.text = "Sacrifice a creature with mana value X"; + } + + protected SidisiRegentOfTheMireCost(final SidisiRegentOfTheMireCost cost) { + super(cost); + } + + @Override + public SidisiRegentOfTheMireCost copy() { + return new SidisiRegentOfTheMireCost(this); + } + + @Override + public Cost getFixedCostsFromAnnouncedValue(int xValue) { + FilterPermanent filter = new FilterControlledCreaturePermanent("another creature with mana value X"); + filter.add(AnotherPredicate.instance); + filter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, xValue)); + return new SacrificeTargetCost(filter); + } + + @Override + public int getMaxValue(Ability source, Game game) { + return game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_CONTROLLED_ANOTHER_CREATURE, + source.getControllerId(), source, game + ).stream().mapToInt(MageObject::getManaValue).max().orElse(0); + } + +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index b5fe3c80cc2..878a2a6a5f4 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -187,6 +187,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Shock Brigade", 120, Rarity.COMMON, mage.cards.s.ShockBrigade.class)); cards.add(new SetCardInfo("Shocking Sharpshooter", 121, Rarity.UNCOMMON, mage.cards.s.ShockingSharpshooter.class)); cards.add(new SetCardInfo("Sibsig Appraiser", 56, Rarity.COMMON, mage.cards.s.SibsigAppraiser.class)); + cards.add(new SetCardInfo("Sidisi, Regent of the Mire", 92, Rarity.RARE, mage.cards.s.SidisiRegentOfTheMire.class)); cards.add(new SetCardInfo("Sinkhole Surveyor", 93, Rarity.RARE, mage.cards.s.SinkholeSurveyor.class)); cards.add(new SetCardInfo("Skirmish Rhino", 224, Rarity.UNCOMMON, mage.cards.s.SkirmishRhino.class)); cards.add(new SetCardInfo("Smile at Death", 24, Rarity.MYTHIC, mage.cards.s.SmileAtDeath.class)); From 95d5d373ce5d6e61fc5fb130dc49575766d478c9 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:47:00 -0400 Subject: [PATCH 025/133] Remove unnecessary custom Dynamic Value from SidisiRegentOfTheMire --- .../mage/cards/s/SidisiRegentOfTheMire.java | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/Mage.Sets/src/mage/cards/s/SidisiRegentOfTheMire.java b/Mage.Sets/src/mage/cards/s/SidisiRegentOfTheMire.java index ac37da8f169..ac7656de699 100644 --- a/Mage.Sets/src/mage/cards/s/SidisiRegentOfTheMire.java +++ b/Mage.Sets/src/mage/cards/s/SidisiRegentOfTheMire.java @@ -10,8 +10,9 @@ import mage.abilities.costs.VariableCostImpl; import mage.abilities.costs.VariableCostType; import mage.abilities.costs.common.SacrificeTargetCost; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.effects.Effect; +import mage.abilities.dynamicvalue.AdditiveDynamicValue; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; import mage.constants.*; import mage.cards.CardImpl; @@ -25,7 +26,6 @@ import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.game.Game; import mage.target.common.TargetCardInYourGraveyard; import mage.target.targetadjustment.ManaValueTargetAdjuster; -import mage.util.CardUtil; /** * @@ -63,34 +63,10 @@ public final class SidisiRegentOfTheMire extends CardImpl { } } -enum GetXPlusOneValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - return CardUtil.getSourceCostsTag(game, sourceAbility, "X", 0) + 1; - } - - @Override - public GetXPlusOneValue copy() { - return GetXPlusOneValue.instance; - } - - @Override - public String toString() { - return "X + 1"; - } - - @Override - public String getMessage() { - return ""; - } -} - class SidisiRegentOfTheMireAdjuster extends ManaValueTargetAdjuster { public SidisiRegentOfTheMireAdjuster() { - super(GetXPlusOneValue.instance, ComparisonType.EQUAL_TO); + super(new AdditiveDynamicValue(GetXValue.instance, StaticValue.get(1)), ComparisonType.EQUAL_TO); } } From 0ad8e8e181b03d95cc8e095782dc2307fbee1bff Mon Sep 17 00:00:00 2001 From: Mike Cunningham Date: Sun, 6 Apr 2025 01:14:36 -0400 Subject: [PATCH 026/133] Aggressive Negotiations implementation --- .../mage/cards/a/AggressiveNegotiations.java | 42 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 43 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java diff --git a/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java b/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java new file mode 100644 index 00000000000..92156e27a55 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java @@ -0,0 +1,42 @@ +package mage.cards.a; + +import java.util.UUID; +import mage.abilities.effects.common.ExileCardYouChooseTargetOpponentEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.TargetPlayer; +import mage.target.common.TargetCreaturePermanent; + +/** + * Aggressive Negotiations implementation + * Author: @mikejcunn + */ +public final class AggressiveNegotiations extends CardImpl { + + public AggressiveNegotiations(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}"); + + // Target opponent reveals their hand. You choose a nonland card from it. That player exiles that card. + this.getSpellAbility().addTarget(new TargetPlayer()); + this.getSpellAbility().addEffect(new ExileCardYouChooseTargetOpponentEffect(StaticFilters.FILTER_CARD_NON_LAND)); + + // Put a +1/+1 counter on target creature you control. + TargetCreaturePermanent targetCreature = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(targetCreature); + this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance(1), Outcome.BoostCreature)); + } + + private AggressiveNegotiations(final AggressiveNegotiations card) { + super(card); + } + + @Override + public AggressiveNegotiations copy() { + return new AggressiveNegotiations(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 878a2a6a5f4..a1a5e1d34e7 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -29,6 +29,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Adorned Crocodile", 69, Rarity.COMMON, mage.cards.a.AdornedCrocodile.class)); cards.add(new SetCardInfo("Aegis Sculptor", 35, Rarity.UNCOMMON, mage.cards.a.AegisSculptor.class)); cards.add(new SetCardInfo("Agent of Kotis", 36, Rarity.COMMON, mage.cards.a.AgentOfKotis.class)); + cards.add(new SetCardInfo("Aggressive Negotiations", 70, Rarity.COMMON, mage.cards.a.AggressiveNegotiations.class)); cards.add(new SetCardInfo("Ainok Wayfarer", 134, Rarity.COMMON, mage.cards.a.AinokWayfarer.class)); cards.add(new SetCardInfo("Alchemist's Assistant", 71, Rarity.UNCOMMON, mage.cards.a.AlchemistsAssistant.class)); cards.add(new SetCardInfo("Alesha's Legacy", 72, Rarity.COMMON, mage.cards.a.AleshasLegacy.class)); From a186a6f21e9c3024cd51f430691fb92974fb030e Mon Sep 17 00:00:00 2001 From: Mike Cunningham Date: Sun, 6 Apr 2025 09:13:36 -0400 Subject: [PATCH 027/133] Reusing Essence Capture +1/+1 counter effect --- .../src/mage/cards/a/AggressiveNegotiations.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java b/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java index 92156e27a55..24303f4c694 100644 --- a/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java +++ b/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java @@ -1,6 +1,8 @@ package mage.cards.a; import java.util.UUID; + +import mage.abilities.effects.Effect; import mage.abilities.effects.common.ExileCardYouChooseTargetOpponentEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.cards.CardImpl; @@ -10,7 +12,8 @@ import mage.constants.Outcome; import mage.counters.CounterType; import mage.filter.StaticFilters; import mage.target.TargetPlayer; -import mage.target.common.TargetCreaturePermanent; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.targetpointer.SecondTargetPointer; /** * Aggressive Negotiations implementation @@ -22,13 +25,16 @@ public final class AggressiveNegotiations extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}"); // Target opponent reveals their hand. You choose a nonland card from it. That player exiles that card. + Effect effect1 = new ExileCardYouChooseTargetOpponentEffect(StaticFilters.FILTER_CARD_NON_LAND); + effect1.setText("Target player reveals their hand. You choose a nonland card from it and exile that card."); + this.getSpellAbility().addEffect(effect1); this.getSpellAbility().addTarget(new TargetPlayer()); - this.getSpellAbility().addEffect(new ExileCardYouChooseTargetOpponentEffect(StaticFilters.FILTER_CARD_NON_LAND)); // Put a +1/+1 counter on target creature you control. - TargetCreaturePermanent targetCreature = new TargetCreaturePermanent(); - this.getSpellAbility().addTarget(targetCreature); - this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance(1), Outcome.BoostCreature)); + this.getSpellAbility().addEffect(new AddCountersTargetEffect( + CounterType.P1P1.createInstance() + ).setTargetPointer(new SecondTargetPointer())); + this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent(0, 1)); } private AggressiveNegotiations(final AggressiveNegotiations card) { From fd1e8144275b487122e3aeb15659557d115ffb84 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 6 Apr 2025 15:27:06 -0500 Subject: [PATCH 028/133] Update Ureni of the Unwritten replace custom effect with LookLibraryAndPickControllerEffect --- .../src/mage/cards/u/UreniOfTheUnwritten.java | 58 ++++--------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/Mage.Sets/src/mage/cards/u/UreniOfTheUnwritten.java b/Mage.Sets/src/mage/cards/u/UreniOfTheUnwritten.java index fa56c051e20..0c00e9b48b5 100644 --- a/Mage.Sets/src/mage/cards/u/UreniOfTheUnwritten.java +++ b/Mage.Sets/src/mage/cards/u/UreniOfTheUnwritten.java @@ -1,24 +1,18 @@ package mage.cards.u; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.cards.Cards; -import mage.cards.CardsImpl; import mage.constants.CardType; -import mage.constants.Outcome; +import mage.constants.PutCards; import mage.constants.SubType; import mage.constants.SuperType; -import mage.constants.Zone; import mage.filter.common.FilterCreatureCard; -import mage.game.Game; -import mage.players.Player; -import mage.target.TargetCard; import java.util.UUID; @@ -27,6 +21,12 @@ import java.util.UUID; */ public final class UreniOfTheUnwritten extends CardImpl { + static final FilterCreatureCard filter = new FilterCreatureCard("Dragon creature card"); + + static { + filter.add(SubType.DRAGON.getPredicate()); + } + public UreniOfTheUnwritten(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}{U}{R}"); @@ -43,7 +43,8 @@ public final class UreniOfTheUnwritten extends CardImpl { this.addAbility(TrampleAbility.getInstance()); // Whenever Ureni enters or attacks, look at the top eight cards of your library. You may put a Dragon creature card from among them onto the battlefield. Put the rest on the bottom of your library in a random order. - this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new UreniOfTheUnwrittenEffect(), false)); + Effect effect = new LookLibraryAndPickControllerEffect(8, 1, filter, PutCards.BATTLEFIELD, PutCards.BOTTOM_RANDOM); + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(effect, false)); } private UreniOfTheUnwritten(final UreniOfTheUnwritten card) { @@ -54,41 +55,4 @@ public final class UreniOfTheUnwritten extends CardImpl { public UreniOfTheUnwritten copy() { return new UreniOfTheUnwritten(this); } -} - -class UreniOfTheUnwrittenEffect extends OneShotEffect { - - UreniOfTheUnwrittenEffect() { - super(Outcome.Benefit); - this.staticText = "look at the top eight cards of your library. You may put a Dragon creature card from among them onto the battlefield. Put the rest on the bottom of your library in a random order."; - } - - private UreniOfTheUnwrittenEffect(final mage.cards.u.UreniOfTheUnwrittenEffect effect) { - super(effect); - } - - @Override - public mage.cards.u.UreniOfTheUnwrittenEffect copy() { - return new mage.cards.u.UreniOfTheUnwrittenEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { - return false; - } - Cards cards = new CardsImpl(controller.getLibrary().getTopCards(game, 8)); - if (!cards.isEmpty()) { - FilterCreatureCard filter = new FilterCreatureCard("Dragon creature cards"); - filter.add(SubType.DRAGON.getPredicate()); - TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, filter); - targetCard.withNotTarget(true); - controller.choose(Outcome.PutCreatureInPlay, cards, targetCard, source, game); - controller.moveCards(game.getCard(targetCard.getFirstTarget()), Zone.BATTLEFIELD, source, game); - cards.retainZone(Zone.LIBRARY, game); - controller.putCardsOnBottomOfLibrary(cards, game, source, false); - } - return true; - } } \ No newline at end of file From 39a4e7644e1a41ff0cf6dd000fbb71e87adf1077 Mon Sep 17 00:00:00 2001 From: Mike Cunningham Date: Mon, 7 Apr 2025 00:28:08 -0400 Subject: [PATCH 029/133] Removing setText, updating filter --- Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java b/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java index 24303f4c694..d4563bdb910 100644 --- a/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java +++ b/Mage.Sets/src/mage/cards/a/AggressiveNegotiations.java @@ -25,8 +25,7 @@ public final class AggressiveNegotiations extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}"); // Target opponent reveals their hand. You choose a nonland card from it. That player exiles that card. - Effect effect1 = new ExileCardYouChooseTargetOpponentEffect(StaticFilters.FILTER_CARD_NON_LAND); - effect1.setText("Target player reveals their hand. You choose a nonland card from it and exile that card."); + Effect effect1 = new ExileCardYouChooseTargetOpponentEffect(StaticFilters.FILTER_CARD_A_NON_LAND); this.getSpellAbility().addEffect(effect1); this.getSpellAbility().addTarget(new TargetPlayer()); From b84143bfca966be76b2383fad63d747da4eebb9c Mon Sep 17 00:00:00 2001 From: androosss <101566943+androosss@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:31:16 +0200 Subject: [PATCH 030/133] [TDM] Implement Lie in Wait (#13494) * Implemented Lie in Wait * code refactor * improve ability --- Mage.Sets/src/mage/cards/l/LieInWait.java | 86 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 87 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LieInWait.java diff --git a/Mage.Sets/src/mage/cards/l/LieInWait.java b/Mage.Sets/src/mage/cards/l/LieInWait.java new file mode 100644 index 00000000000..249818aa62a --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LieInWait.java @@ -0,0 +1,86 @@ +package mage.cards.l; + +import java.util.List; +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.targetpointer.EachTargetPointer; + +/** + * + * @author androosss + */ +public final class LieInWait extends CardImpl { + + public LieInWait(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}{G}{U}"); + + + // Return target creature card from your graveyard to your hand. Lie in Wait deals damage equal to that card's power to target creature. + this.getSpellAbility().addEffect(new LieInWaitTargetEffect()); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE)); + this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_CREATURE)); + } + + private LieInWait(final LieInWait card) { + super(card); + } + + @Override + public LieInWait copy() { + return new LieInWait(this); + } + +} + +class LieInWaitTargetEffect extends OneShotEffect { + + LieInWaitTargetEffect() { + super(Outcome.Benefit); + staticText = "Return target creature card from your graveyard to your hand. " + + "{this} deals damage equal to that card's power to target creature"; + setTargetPointer(new EachTargetPointer()); + } + + private LieInWaitTargetEffect(final LieInWaitTargetEffect effect) { + super(effect); + } + + @Override + public LieInWaitTargetEffect copy() { + return new LieInWaitTargetEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + List targets = getTargetPointer().getTargets(game, source); + Card card = game.getCard(targets.get(0)); + if (card == null) { + return false; + } + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + boolean result = card.moveToZone(Zone.HAND, source, game,false); + if (result && targets.size() >= 2) { + int power = card.getPower().getValue(); + Permanent permanent = game.getPermanent(targets.get(1)); + permanent.damage(power, source, game); + } + return result; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index a1a5e1d34e7..25de9c8f542 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -132,6 +132,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Knockout Maneuver", 147, Rarity.UNCOMMON, mage.cards.k.KnockoutManeuver.class)); cards.add(new SetCardInfo("Kotis, the Fangkeeper", 202, Rarity.RARE, mage.cards.k.KotisTheFangkeeper.class)); cards.add(new SetCardInfo("Krotiq Nestguard", 148, Rarity.COMMON, mage.cards.k.KrotiqNestguard.class)); + cards.add(new SetCardInfo("Lie in Wait", 203, Rarity.UNCOMMON, mage.cards.l.LieInWait.class)); cards.add(new SetCardInfo("Lightfoot Technique", 14, Rarity.COMMON, mage.cards.l.LightfootTechnique.class)); cards.add(new SetCardInfo("Lotuslight Dancers", 204, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Lotuslight Dancers", 363, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); From 6202e781bd9acc594f8d5b67490ddec69f398a49 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 09:01:36 -0400 Subject: [PATCH 031/133] [TDM] Implement Eshki Dragonclaw --- .../src/mage/cards/e/EshkiDragonclaw.java | 156 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 157 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/EshkiDragonclaw.java diff --git a/Mage.Sets/src/mage/cards/e/EshkiDragonclaw.java b/Mage.Sets/src/mage/cards/e/EshkiDragonclaw.java new file mode 100644 index 00000000000..695026c5fa7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EshkiDragonclaw.java @@ -0,0 +1,156 @@ +package mage.cards.e; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.abilities.keyword.WardAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.WatcherScope; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EshkiDragonclaw extends CardImpl { + + public EshkiDragonclaw(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Ward {1} + this.addAbility(new WardAbility(new ManaCostsImpl<>("{1}"))); + + // At the beginning of combat on your turn, if you've cast both a creature spell and a noncreature spell this turn, draw a card and put two +1/+1 counters on Eshki Dragonclaw. + Ability ability = new BeginningOfCombatTriggeredAbility(new DrawCardSourceControllerEffect(1)) + .withInterveningIf(EshkiDragonclawCondition.instance); + ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)).concatBy("and")); + this.addAbility(ability.addHint(EshkiDragonclawHint.instance), new EshkiDragonclawWatcher()); + } + + private EshkiDragonclaw(final EshkiDragonclaw card) { + super(card); + } + + @Override + public EshkiDragonclaw copy() { + return new EshkiDragonclaw(this); + } +} + +enum EshkiDragonclawCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return EshkiDragonclawWatcher.checkPlayer(source.getControllerId(), game) == 3; + } + + @Override + public String toString() { + return "you've cast both a creature spell and a noncreature spell this turn"; + } +} + +enum EshkiDragonclawHint implements Hint { + instance; + + @Override + public String getText(Game game, Ability ability) { + switch (EshkiDragonclawWatcher.checkPlayer(ability.getControllerId(), game)) { + case 0: + return null; + case 1: + return "You've cast a creature spell this turn"; + case 2: + return "You've cast a noncreature spell this turn"; + case 3: + return "You've cast a creature spell and a noncreature spell this turn"; + } + return null; + } + + @Override + public Hint copy() { + return this; + } +} + +class EshkiDragonclawWatcher extends Watcher { + + private final Map creatureCount = new HashMap<>(); + private final Map nonCreatureCount = new HashMap<>(); + + EshkiDragonclawWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.SPELL_CAST) { + return; + } + Spell spell = game.getSpell(event.getTargetId()); + if (spell == null) { + return; + } + if (spell.isCreature(game)) { + creatureCount.compute(spell.getControllerId(), CardUtil::setOrIncrementValue); + } else { + nonCreatureCount.compute(spell.getControllerId(), CardUtil::setOrIncrementValue); + } + } + + @Override + public void reset() { + super.reset(); + creatureCount.clear(); + nonCreatureCount.clear(); + } + + private int checkCreature(UUID playerId) { + return creatureCount.getOrDefault(playerId, 0) > 0 ? 1 : 0; + } + + private int checkNonCreature(UUID playerId) { + return nonCreatureCount.getOrDefault(playerId, 0) > 0 ? 2 : 0; + } + + private int check(UUID playerId) { + return checkCreature(playerId) + checkNonCreature(playerId); + } + + static int checkPlayer(UUID playerId, Game game) { + return game.getState().getWatcher(EshkiDragonclawWatcher.class).check(playerId); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 25de9c8f542..4ac988fe272 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -86,6 +86,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Embermouth Sentinel", 242, Rarity.COMMON, mage.cards.e.EmbermouthSentinel.class)); cards.add(new SetCardInfo("Encroaching Dragonstorm", 142, Rarity.UNCOMMON, mage.cards.e.EncroachingDragonstorm.class)); cards.add(new SetCardInfo("Equilibrium Adept", 106, Rarity.UNCOMMON, mage.cards.e.EquilibriumAdept.class)); + cards.add(new SetCardInfo("Eshki Dragonclaw", 182, Rarity.RARE, mage.cards.e.EshkiDragonclaw.class)); cards.add(new SetCardInfo("Essence Anchor", 44, Rarity.UNCOMMON, mage.cards.e.EssenceAnchor.class)); cards.add(new SetCardInfo("Evolving Wilds", 255, Rarity.COMMON, mage.cards.e.EvolvingWilds.class)); cards.add(new SetCardInfo("Fangkeeper's Familiar", 183, Rarity.RARE, mage.cards.f.FangkeepersFamiliar.class)); From 08e31b860e49166a7b8dcb750e513d6a88e3758b Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 09:14:42 -0400 Subject: [PATCH 032/133] [TDM] Implement Stillness in Motion --- .../src/mage/cards/s/StillnessInMotion.java | 80 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 81 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StillnessInMotion.java diff --git a/Mage.Sets/src/mage/cards/s/StillnessInMotion.java b/Mage.Sets/src/mage/cards/s/StillnessInMotion.java new file mode 100644 index 00000000000..7a436bf0a0c --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StillnessInMotion.java @@ -0,0 +1,80 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StillnessInMotion extends CardImpl { + + public StillnessInMotion(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}"); + + // At the beginning of your upkeep, mill three cards. Then if you have no cards in your library, exile this enchantment and put five cards from your graveyard on top of your library in any order. + Ability ability = new BeginningOfUpkeepTriggeredAbility(new MillCardsControllerEffect(3)); + ability.addEffect(new StillnessInMotionEffect()); + this.addAbility(ability); + } + + private StillnessInMotion(final StillnessInMotion card) { + super(card); + } + + @Override + public StillnessInMotion copy() { + return new StillnessInMotion(this); + } +} + +class StillnessInMotionEffect extends OneShotEffect { + + StillnessInMotionEffect() { + super(Outcome.Benefit); + staticText = "Then if you have no cards in your library, exile this enchantment " + + "and put five cards from your graveyard on top of your library in any order"; + } + + private StillnessInMotionEffect(final StillnessInMotionEffect effect) { + super(effect); + } + + @Override + public StillnessInMotionEffect copy() { + return new StillnessInMotionEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null || player.getLibrary().hasCards()) { + return false; + } + Optional.ofNullable(source.getSourcePermanentIfItStillExists(game)) + .ifPresent(permanent -> player.moveCards(permanent, Zone.EXILED, source, game)); + int graveCount = Math.min(player.getGraveyard().size(), 5); + if (graveCount < 1) { + return true; + } + TargetCard target = new TargetCardInYourGraveyard(graveCount); + target.withNotTarget(true); + player.choose(outcome, target, source, game); + player.putCardsOnTopOfLibrary(new CardsImpl(target.getTargets()), game, source, true); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 4ac988fe272..5ad3cf523f3 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -202,6 +202,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Stadium Headliner", 122, Rarity.RARE, mage.cards.s.StadiumHeadliner.class)); cards.add(new SetCardInfo("Starry-Eyed Skyrider", 25, Rarity.UNCOMMON, mage.cards.s.StarryEyedSkyrider.class)); cards.add(new SetCardInfo("Static Snare", 26, Rarity.UNCOMMON, mage.cards.s.StaticSnare.class)); + cards.add(new SetCardInfo("Stillness in Motion", 59, Rarity.RARE, mage.cards.s.StillnessInMotion.class)); cards.add(new SetCardInfo("Stormbeacon Blade", 27, Rarity.UNCOMMON, mage.cards.s.StormbeaconBlade.class)); cards.add(new SetCardInfo("Stormplain Detainment", 28, Rarity.COMMON, mage.cards.s.StormplainDetainment.class)); cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); From 325b4a046115f35cf2921c7c3653ba4333ccaa46 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 09:56:59 -0400 Subject: [PATCH 033/133] [TDM] Implement Lasyd Prowler --- Mage.Sets/src/mage/cards/l/LasydProwler.java | 67 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 68 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LasydProwler.java diff --git a/Mage.Sets/src/mage/cards/l/LasydProwler.java b/Mage.Sets/src/mage/cards/l/LasydProwler.java new file mode 100644 index 00000000000..1971addf543 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LasydProwler.java @@ -0,0 +1,67 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.common.ExileSourceFromGraveCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; +import mage.abilities.dynamicvalue.common.LandsYouControlCount; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LasydProwler extends CardImpl { + + private static final DynamicValue xValue = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_LAND, null); + private static final Hint hint = new ValueHint("Land cards in your graveyard", xValue); + + public LasydProwler(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{G}"); + + this.subtype.add(SubType.SNAKE); + this.subtype.add(SubType.RANGER); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // When this creature enters, you may mill cards equal to the number of lands you control. + this.addAbility(new EntersBattlefieldTriggeredAbility(new MillCardsControllerEffect(LandsYouControlCount.instance) + .setText("mill cards equal to the number of lands you control"), true)); + + // Renew -- {1}{G}, Exile this card from your graveyard: Put X +1/+1 counters on target creature, where X is the number of land cards in your graveyard. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility( + Zone.GRAVEYARD, + new AddCountersTargetEffect(CounterType.P1P1.createInstance(0), xValue), + new ManaCostsImpl<>("{1}{G}") + ); + ability.addCost(new ExileSourceFromGraveCost()); + ability.addTarget(new TargetCreaturePermanent()); + this.addAbility(ability.setAbilityWord(AbilityWord.RENEW).addHint(hint)); + } + + private LasydProwler(final LasydProwler card) { + super(card); + } + + @Override + public LasydProwler copy() { + return new LasydProwler(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 5ad3cf523f3..08f075c9440 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -133,6 +133,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Knockout Maneuver", 147, Rarity.UNCOMMON, mage.cards.k.KnockoutManeuver.class)); cards.add(new SetCardInfo("Kotis, the Fangkeeper", 202, Rarity.RARE, mage.cards.k.KotisTheFangkeeper.class)); cards.add(new SetCardInfo("Krotiq Nestguard", 148, Rarity.COMMON, mage.cards.k.KrotiqNestguard.class)); + cards.add(new SetCardInfo("Lasyd Prowler", 149, Rarity.RARE, mage.cards.l.LasydProwler.class)); cards.add(new SetCardInfo("Lie in Wait", 203, Rarity.UNCOMMON, mage.cards.l.LieInWait.class)); cards.add(new SetCardInfo("Lightfoot Technique", 14, Rarity.COMMON, mage.cards.l.LightfootTechnique.class)); cards.add(new SetCardInfo("Lotuslight Dancers", 204, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); From c9148aa2d32a3e3a40447b14349b4b166928c78d Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 10:03:19 -0400 Subject: [PATCH 034/133] [TDM] Implement Naga Fleshcrafter --- .../src/mage/cards/n/NagaFleshcrafter.java | 102 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 103 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/n/NagaFleshcrafter.java diff --git a/Mage.Sets/src/mage/cards/n/NagaFleshcrafter.java b/Mage.Sets/src/mage/cards/n/NagaFleshcrafter.java new file mode 100644 index 00000000000..f160e54b9f9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NagaFleshcrafter.java @@ -0,0 +1,102 @@ +package mage.cards.n; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.costs.common.ExileSourceFromGraveCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CopyPermanentEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NagaFleshcrafter extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledCreaturePermanent("nonlegendary creature you control"); + + static { + filter.add(Predicates.not(SuperType.LEGENDARY.getPredicate())); + } + + public NagaFleshcrafter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}"); + + this.subtype.add(SubType.SNAKE); + this.subtype.add(SubType.SHAPESHIFTER); + this.power = new MageInt(0); + this.toughness = new MageInt(0); + + // You may have this creature enter as a copy of any creature on the battlefield. + this.addAbility(new EntersBattlefieldAbility(new CopyPermanentEffect(), true)); + + // Renew -- {2}{U}, Exile this card from your graveyard: Put a +1/+1 counter on target nonlegendary creature you control. Each other creature you control becomes a copy of that creature until end of turn. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility( + Zone.GRAVEYARD, + new AddCountersTargetEffect(CounterType.P1P1.createInstance()), + new ManaCostsImpl<>("{2}{U}") + ); + ability.addCost(new ExileSourceFromGraveCost()); + ability.addEffect(new NagaFleshcrafterEffect()); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability.setAbilityWord(AbilityWord.RENEW)); + } + + private NagaFleshcrafter(final NagaFleshcrafter card) { + super(card); + } + + @Override + public NagaFleshcrafter copy() { + return new NagaFleshcrafter(this); + } +} + +class NagaFleshcrafterEffect extends OneShotEffect { + + NagaFleshcrafterEffect() { + super(Outcome.Benefit); + staticText = "each other creature you control becomes a copy of that creature until end of turn"; + } + + private NagaFleshcrafterEffect(final NagaFleshcrafterEffect effect) { + super(effect); + } + + @Override + public NagaFleshcrafterEffect copy() { + return new NagaFleshcrafterEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + for (Permanent creature : game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_CONTROLLED_CREATURE, + source.getControllerId(), source, game + )) { + if (!permanent.getId().equals(creature.getId())) { + game.copyPermanent(Duration.EndOfTurn, permanent, creature.getId(), source, null); + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 08f075c9440..0a2b0331323 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -149,6 +149,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Mountain", 283, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Mox Jasper", 246, Rarity.MYTHIC, mage.cards.m.MoxJasper.class)); cards.add(new SetCardInfo("Mystic Monastery", 262, Rarity.UNCOMMON, mage.cards.m.MysticMonastery.class)); + cards.add(new SetCardInfo("Naga Fleshcrafter", 52, Rarity.RARE, mage.cards.n.NagaFleshcrafter.class)); cards.add(new SetCardInfo("Narset's Rebuke", 114, Rarity.COMMON, mage.cards.n.NarsetsRebuke.class)); cards.add(new SetCardInfo("Narset, Jeskai Waymaster", 209, Rarity.RARE, mage.cards.n.NarsetJeskaiWaymaster.class)); cards.add(new SetCardInfo("Nature's Rhythm", 150, Rarity.RARE, mage.cards.n.NaturesRhythm.class)); From 97adeca9be8f07abfec8354f0c4255c30ed18b2b Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 10:13:53 -0400 Subject: [PATCH 035/133] [TDM] Implement Riverwheel Sweep --- .../src/mage/cards/r/RiverwheelSweep.java | 100 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 101 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RiverwheelSweep.java diff --git a/Mage.Sets/src/mage/cards/r/RiverwheelSweep.java b/Mage.Sets/src/mage/cards/r/RiverwheelSweep.java new file mode 100644 index 00000000000..94273f90e3b --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiverwheelSweep.java @@ -0,0 +1,100 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInExile; +import mage.target.common.TargetCreaturePermanent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RiverwheelSweep extends CardImpl { + + public RiverwheelSweep(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2/U}{2/R}{2/W}"); + + // Tap target creature. Put three stun counters on it. + this.getSpellAbility().addEffect(new TapTargetEffect()); + this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.STUN.createInstance(3)) + .setText("put three stun counters on it")); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + + // Exile the top two cards of your library. Choose one of them. Until the end of your next turn, you may play that card. + this.getSpellAbility().addEffect(new RiverwheelSweepEffect()); + } + + private RiverwheelSweep(final RiverwheelSweep card) { + super(card); + } + + @Override + public RiverwheelSweep copy() { + return new RiverwheelSweep(this); + } +} + +class RiverwheelSweepEffect extends OneShotEffect { + + RiverwheelSweepEffect() { + super(Outcome.Benefit); + staticText = "Exile the top two cards of your library. Choose one of them. " + + "Until the end of your next turn, you may play that card"; + this.concatBy("
"); + } + + private RiverwheelSweepEffect(final RiverwheelSweepEffect effect) { + super(effect); + } + + @Override + public RiverwheelSweepEffect copy() { + return new RiverwheelSweepEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 2)); + player.moveCards(cards, Zone.EXILED, source, game); + cards.retainZone(Zone.EXILED, game); + Card card; + switch (cards.size()) { + case 0: + return false; + case 1: + card = cards.getRandom(game); + break; + default: + TargetCard target = new TargetCardInExile(StaticFilters.FILTER_CARD); + target.withNotTarget(true); + player.choose(Outcome.DrawCard, cards, target, source, game); + card = game.getCard(target.getFirstTarget()); + } + if (card == null) { + return false; + } + CardUtil.makeCardPlayable( + game, source, card, false, + Duration.UntilEndOfYourNextTurn, false + ); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 0a2b0331323..38771af781b 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -174,6 +174,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Revival of the Ancestors", 218, Rarity.RARE, mage.cards.r.RevivalOfTheAncestors.class)); cards.add(new SetCardInfo("Ringing Strike Mastery", 53, Rarity.COMMON, mage.cards.r.RingingStrikeMastery.class)); cards.add(new SetCardInfo("Riverwalk Technique", 54, Rarity.COMMON, mage.cards.r.RiverwalkTechnique.class)); + cards.add(new SetCardInfo("Riverwheel Sweep", 219, Rarity.UNCOMMON, mage.cards.r.RiverwheelSweep.class)); cards.add(new SetCardInfo("Roamer's Routine", 154, Rarity.COMMON, mage.cards.r.RoamersRoutine.class)); cards.add(new SetCardInfo("Roar of Endless Song", 220, Rarity.RARE, mage.cards.r.RoarOfEndlessSong.class)); cards.add(new SetCardInfo("Roiling Dragonstorm", 55, Rarity.UNCOMMON, mage.cards.r.RoilingDragonstorm.class)); From c5b62eb620c706c624ac6e8c33783bd9bd24ef17 Mon Sep 17 00:00:00 2001 From: Mike Cunningham Date: Mon, 7 Apr 2025 14:57:18 -0400 Subject: [PATCH 036/133] [TDM] Implement Wail of War (#13504) * [TDM] Implement Wail of War * Updating BoostControlledEffect call * Resolving test errors * Update debuff ability, adjust setText, update graveyard filter * Removing setText, updating targetOpponent --- Mage.Sets/src/mage/cards/w/WailOfWar.java | 55 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 56 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WailOfWar.java diff --git a/Mage.Sets/src/mage/cards/w/WailOfWar.java b/Mage.Sets/src/mage/cards/w/WailOfWar.java new file mode 100644 index 00000000000..5c40127e78a --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WailOfWar.java @@ -0,0 +1,55 @@ +package mage.cards.w; + +import mage.abilities.Mode; +import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; +import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.TargetController; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.target.common.TargetCardInGraveyard; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * Represents the "Wail of War" instant card. + * Author: @mikejcunn + */ +public final class WailOfWar extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creatures target opponent controls"); + + static { + filter.add(TargetController.SOURCE_TARGETS.getControllerPredicate()); + } + + public WailOfWar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{B}"); + + // Mode 1: Creatures target player controls get -1/-1 until end of turn. + this.getSpellAbility().addEffect(new BoostAllEffect( + -1, -1, Duration.EndOfTurn, filter, false + )); + this.getSpellAbility().addTarget(new TargetOpponent()); + + // Mode 2: Return creatures from graveyard + Mode returnCreaturesFromGraveyardMode = new Mode(new ReturnFromGraveyardToHandTargetEffect()); + returnCreaturesFromGraveyardMode.addTarget(new TargetCardInGraveyard( + 0, 2, StaticFilters.FILTER_CARD_CREATURES_YOUR_GRAVEYARD + )); + this.getSpellAbility().addMode(returnCreaturesFromGraveyardMode); + } + + private WailOfWar(final WailOfWar card) { + super(card); + } + + @Override + public WailOfWar copy() { + return new WailOfWar(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 38771af781b..c61f700b718 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -241,6 +241,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Veteran Ice Climber", 64, Rarity.UNCOMMON, mage.cards.v.VeteranIceClimber.class)); cards.add(new SetCardInfo("Voice of Victory", 33, Rarity.RARE, mage.cards.v.VoiceOfVictory.class)); cards.add(new SetCardInfo("War Effort", 131, Rarity.UNCOMMON, mage.cards.w.WarEffort.class)); + cards.add(new SetCardInfo("Wail of War", 98, Rarity.UNCOMMON, mage.cards.w.WailOfWar.class)); cards.add(new SetCardInfo("Watcher of the Wayside", 249, Rarity.COMMON, mage.cards.w.WatcherOfTheWayside.class)); cards.add(new SetCardInfo("Wayspeaker Bodyguard", 34, Rarity.UNCOMMON, mage.cards.w.WayspeakerBodyguard.class)); cards.add(new SetCardInfo("Wild Ride", 132, Rarity.COMMON, mage.cards.w.WildRide.class)); From 49d5d6ded1e2e895c768452ec5583f4fdff24712 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 11:43:24 -0400 Subject: [PATCH 037/133] [TDM] Implement Tersa Lightshatter --- .../src/mage/cards/t/TersaLightshatter.java | 85 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 86 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TersaLightshatter.java diff --git a/Mage.Sets/src/mage/cards/t/TersaLightshatter.java b/Mage.Sets/src/mage/cards/t/TersaLightshatter.java new file mode 100644 index 00000000000..ea13914f87e --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TersaLightshatter.java @@ -0,0 +1,85 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.ThresholdCondition; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.discard.DiscardAndDrawThatManyEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TersaLightshatter extends CardImpl { + + public TersaLightshatter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ORC); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // When Tersa Lightshatter enters, discard up to two cards, then draw that many cards. + this.addAbility(new EntersBattlefieldTriggeredAbility(new DiscardAndDrawThatManyEffect(2))); + + // Whenever Tersa Lightshatter attacks, if there are seven or more cards in your graveyard, exile a card at random from your graveyard. You may play that card this turn. + this.addAbility(new AttacksTriggeredAbility(new TersaLightshatterEffect()).withInterveningIf(ThresholdCondition.instance)); + } + + private TersaLightshatter(final TersaLightshatter card) { + super(card); + } + + @Override + public TersaLightshatter copy() { + return new TersaLightshatter(this); + } +} + +class TersaLightshatterEffect extends OneShotEffect { + + TersaLightshatterEffect() { + super(Outcome.Benefit); + staticText = "exile a card at random from your graveyard. You may play that card this turn"; + } + + private TersaLightshatterEffect(final TersaLightshatterEffect effect) { + super(effect); + } + + @Override + public TersaLightshatterEffect copy() { + return new TersaLightshatterEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Card card = player.getGraveyard().getRandom(game); + if (card == null) { + return false; + } + player.moveCards(card, Zone.EXILED, source, game); + CardUtil.makeCardPlayable(game, source, card, false, Duration.EndOfTurn, false); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index c61f700b718..1909add4f1f 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -221,6 +221,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Temur Devotee", 61, Rarity.COMMON, mage.cards.t.TemurDevotee.class)); cards.add(new SetCardInfo("Temur Monument", 248, Rarity.UNCOMMON, mage.cards.t.TemurMonument.class)); cards.add(new SetCardInfo("Temur Tawnyback", 229, Rarity.COMMON, mage.cards.t.TemurTawnyback.class)); + cards.add(new SetCardInfo("Tersa Lightshatter", 127, Rarity.RARE, mage.cards.t.TersaLightshatter.class)); cards.add(new SetCardInfo("The Sibsig Ceremony", 91, Rarity.RARE, mage.cards.t.TheSibsigCeremony.class)); cards.add(new SetCardInfo("Thornwood Falls", 269, Rarity.COMMON, mage.cards.t.ThornwoodFalls.class)); cards.add(new SetCardInfo("Thunder of Unity", 231, Rarity.RARE, mage.cards.t.ThunderOfUnity.class)); From aad92581ce869afb6c2e91ea9ad6f2499915f448 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 12:31:29 -0400 Subject: [PATCH 038/133] [TDM] Implement Ureni, the Song Unending --- .../mage/cards/u/UreniTheSongUnending.java | 65 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 66 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/u/UreniTheSongUnending.java diff --git a/Mage.Sets/src/mage/cards/u/UreniTheSongUnending.java b/Mage.Sets/src/mage/cards/u/UreniTheSongUnending.java new file mode 100644 index 00000000000..b1bd25b98a3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UreniTheSongUnending.java @@ -0,0 +1,65 @@ +package mage.cards.u; + +import mage.MageInt; +import mage.ObjectColor; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.dynamicvalue.common.LandsYouControlCount; +import mage.abilities.effects.common.DamageMultiEffect; +import mage.abilities.hint.common.LandsYouControlHint; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.ProtectionAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent; +import mage.target.common.TargetPermanentAmount; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class UreniTheSongUnending extends CardImpl { + + private static final FilterPermanent filter + = new FilterCreatureOrPlaneswalkerPermanent("creatures and/or planeswalkers your opponents control"); + + static { + filter.add(TargetController.OPPONENT.getControllerPredicate()); + } + + public UreniTheSongUnending(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{G}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(10); + this.toughness = new MageInt(10); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Protection from white and from black + this.addAbility(ProtectionAbility.from(ObjectColor.WHITE, ObjectColor.BLACK)); + + // When Ureni enters, it deals X damage divided as you choose among any number of target creatures and/or planeswalkers your opponents control, where X is the number of lands you control. + Ability ability = new EntersBattlefieldTriggeredAbility(new DamageMultiEffect("it")); + ability.addTarget(new TargetPermanentAmount(LandsYouControlCount.instance, 0, filter)); + this.addAbility(ability.addHint(LandsYouControlHint.instance)); + } + + private UreniTheSongUnending(final UreniTheSongUnending card) { + super(card); + } + + @Override + public UreniTheSongUnending copy() { + return new UreniTheSongUnending(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 1909add4f1f..c28827e6fdc 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -238,6 +238,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Unrooted Ancestor", 96, Rarity.UNCOMMON, mage.cards.u.UnrootedAncestor.class)); cards.add(new SetCardInfo("Unsparing Boltcaster", 130, Rarity.UNCOMMON, mage.cards.u.UnsparingBoltcaster.class)); cards.add(new SetCardInfo("Ureni's Rebuff", 63, Rarity.UNCOMMON, mage.cards.u.UrenisRebuff.class)); + cards.add(new SetCardInfo("Ureni, the Song Unending", 233, Rarity.MYTHIC, mage.cards.u.UreniTheSongUnending.class)); cards.add(new SetCardInfo("Venerated Stormsinger", 97, Rarity.UNCOMMON, mage.cards.v.VeneratedStormsinger.class)); cards.add(new SetCardInfo("Veteran Ice Climber", 64, Rarity.UNCOMMON, mage.cards.v.VeteranIceClimber.class)); cards.add(new SetCardInfo("Voice of Victory", 33, Rarity.RARE, mage.cards.v.VoiceOfVictory.class)); From 31f152836007c3d863e539a2cc37b2c132f42af1 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 12:38:52 -0400 Subject: [PATCH 039/133] [TDM] Implement Sunpearl Kirin --- Mage.Sets/src/mage/cards/s/SunpearlKirin.java | 95 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 96 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SunpearlKirin.java diff --git a/Mage.Sets/src/mage/cards/s/SunpearlKirin.java b/Mage.Sets/src/mage/cards/s/SunpearlKirin.java new file mode 100644 index 00000000000..3283e9eac23 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SunpearlKirin.java @@ -0,0 +1,95 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterNonlandPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentToken; +import mage.players.Player; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SunpearlKirin extends CardImpl { + + private static final FilterPermanent filter = new FilterNonlandPermanent("other target nonland permanent you control"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(TargetController.YOU.getControllerPredicate()); + } + + public SunpearlKirin(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.KIRIN); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When this creature enters, return up to one other target nonland permanent you control to its owner's hand. If it was a token, draw a card. + Ability ability = new EntersBattlefieldTriggeredAbility(new SunpearlKirinEffect()); + ability.addTarget(new TargetPermanent(0, 1, filter)); + this.addAbility(ability); + } + + private SunpearlKirin(final SunpearlKirin card) { + super(card); + } + + @Override + public SunpearlKirin copy() { + return new SunpearlKirin(this); + } +} + +class SunpearlKirinEffect extends OneShotEffect { + + SunpearlKirinEffect() { + super(Outcome.Benefit); + staticText = "return up to one other target nonland permanent " + + "you control to its owner's hand. If it was a token, draw a card"; + } + + private SunpearlKirinEffect(final SunpearlKirinEffect effect) { + super(effect); + } + + @Override + public SunpearlKirinEffect copy() { + return new SunpearlKirinEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (player == null || permanent == null) { + return false; + } + boolean isToken = permanent instanceof PermanentToken; + player.moveCards(permanent, Zone.HAND, source, game); + if (isToken) { + player.drawCards(1, source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index c28827e6fdc..b17a1bd2d90 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -212,6 +212,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Sultai Devotee", 160, Rarity.COMMON, mage.cards.s.SultaiDevotee.class)); cards.add(new SetCardInfo("Sultai Monument", 247, Rarity.UNCOMMON, mage.cards.s.SultaiMonument.class)); cards.add(new SetCardInfo("Summit Intimidator", 125, Rarity.COMMON, mage.cards.s.SummitIntimidator.class)); + cards.add(new SetCardInfo("Sunpearl Kirin", 29, Rarity.UNCOMMON, mage.cards.s.SunpearlKirin.class)); cards.add(new SetCardInfo("Sunset Strikemaster", 126, Rarity.UNCOMMON, mage.cards.s.SunsetStrikemaster.class)); cards.add(new SetCardInfo("Swamp", 281, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Swiftwater Cliffs", 268, Rarity.COMMON, mage.cards.s.SwiftwaterCliffs.class)); From e3937e31c1b366337a7d9fe7353765d06815ba71 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Mon, 7 Apr 2025 14:55:46 -0400 Subject: [PATCH 040/133] [TDM] Implement Strategic Betrayal --- .../src/mage/cards/s/StrategicBetrayal.java | 78 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 79 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StrategicBetrayal.java diff --git a/Mage.Sets/src/mage/cards/s/StrategicBetrayal.java b/Mage.Sets/src/mage/cards/s/StrategicBetrayal.java new file mode 100644 index 00000000000..253659ce02d --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StrategicBetrayal.java @@ -0,0 +1,78 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StrategicBetrayal extends CardImpl { + + public StrategicBetrayal(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}"); + + // Target opponent exiles a creature they control and their graveyard. + this.getSpellAbility().addEffect(new StrategicBetrayalEffect()); + this.getSpellAbility().addTarget(new TargetOpponent()); + } + + private StrategicBetrayal(final StrategicBetrayal card) { + super(card); + } + + @Override + public StrategicBetrayal copy() { + return new StrategicBetrayal(this); + } +} + +class StrategicBetrayalEffect extends OneShotEffect { + + StrategicBetrayalEffect() { + super(Outcome.Benefit); + staticText = "target opponent exiles a creature they control and their graveyard"; + } + + private StrategicBetrayalEffect(final StrategicBetrayalEffect effect) { + super(effect); + } + + @Override + public StrategicBetrayalEffect copy() { + return new StrategicBetrayalEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(player.getGraveyard()); + if (game.getBattlefield().contains( + StaticFilters.FILTER_CONTROLLED_CREATURE, + player.getId(), source, game, 1 + )) { + TargetPermanent target = new TargetControlledCreaturePermanent(); + target.withNotTarget(true); + player.choose(Outcome.DestroyPermanent, target, source, game); + cards.add(target.getFirstTarget()); + } + return player.moveCards(cards, Zone.EXILED, source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index b17a1bd2d90..170bef109a3 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -209,6 +209,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Stormbeacon Blade", 27, Rarity.UNCOMMON, mage.cards.s.StormbeaconBlade.class)); cards.add(new SetCardInfo("Stormplain Detainment", 28, Rarity.COMMON, mage.cards.s.StormplainDetainment.class)); cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); + cards.add(new SetCardInfo("Strategic Betrayal", 94, Rarity.UNCOMMON, mage.cards.s.StrategicBetrayal.class)); cards.add(new SetCardInfo("Sultai Devotee", 160, Rarity.COMMON, mage.cards.s.SultaiDevotee.class)); cards.add(new SetCardInfo("Sultai Monument", 247, Rarity.UNCOMMON, mage.cards.s.SultaiMonument.class)); cards.add(new SetCardInfo("Summit Intimidator", 125, Rarity.COMMON, mage.cards.s.SummitIntimidator.class)); From 0df5f17603c3df35167a17353f64601fa913adac Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Tue, 8 Apr 2025 07:54:18 -0500 Subject: [PATCH 041/133] [TDM] Implement omen mechanic (#13501) * Abstract AdventureCard to SingleFaceSplitCard * Fix AdventureCardSpellImpl * Finish converting adventure card and adventure spell * Update Brightcap Badger change finalize call to adventure card * Update Darksteel Monolith being cast from hand condition referencing AdventureCardSpell * Update Tlincalli Hunter exiled creature condition referencing AdventureCardSpell * Update Twice Upon a Time finalizeAdventure called from Adventure card * Finish abstracting Adventure missed some more references to adventure cards * Implement Omen cards * Implement Dirgur Island Dragon * Missed some adventureSpellName references * OmenCardSpell had wrong comma symbol * Add tests for Omen Cards * Rename two part card components change from SingleFaceSplitCard to CardWithSpellOption * Update comments and variable name --- .../mage/card/arcane/ModernCardRenderer.java | 14 +- .../card/arcane/ModernSplitCardRenderer.java | 12 +- .../src/main/java/mage/view/CardView.java | 28 +-- .../src/mage/cards/b/BrightcapBadger.java | 2 +- .../src/mage/cards/d/DarksteelMonolith.java | 2 +- .../src/mage/cards/d/DirgurIslandDragon.java | 52 ++++++ .../src/mage/cards/t/TlincalliHunter.java | 2 +- .../src/mage/cards/t/TwiceUponATime.java | 2 +- .../src/mage/sets/TarkirDragonstorm.java | 1 + .../test/cards/cost/omen/OmenCardsTest.java | 77 ++++++++ .../java/mage/abilities/SpellAbility.java | 4 +- .../common/IsBeingCastFromHandCondition.java | 4 +- .../abilities/effects/ContinuousEffects.java | 6 +- .../common/ExileAdventureSpellEffect.java | 6 +- .../abilities/keyword/ForetellAbility.java | 16 +- .../mage/abilities/keyword/PlotAbility.java | 12 +- .../main/java/mage/cards/AdventureCard.java | 124 +------------ .../java/mage/cards/AdventureCardSpell.java | 12 -- ...SpellImpl.java => AdventureSpellCard.java} | 25 ++- Mage/src/main/java/mage/cards/CardImpl.java | 4 +- .../java/mage/cards/CardWithSpellOption.java | 131 +++++++++++++ Mage/src/main/java/mage/cards/OmenCard.java | 34 ++++ .../main/java/mage/cards/OmenSpellCard.java | 174 ++++++++++++++++++ .../main/java/mage/cards/SpellOptionCard.java | 18 ++ .../main/java/mage/cards/mock/MockCard.java | 14 +- .../java/mage/cards/repository/CardInfo.java | 26 +-- .../mage/cards/repository/CardRepository.java | 22 +-- .../java/mage/constants/SpellAbilityType.java | 3 +- .../src/main/java/mage/constants/SubType.java | 1 + .../predicate/card/CardTextPredicate.java | 13 +- Mage/src/main/java/mage/game/GameImpl.java | 4 +- Mage/src/main/java/mage/game/GameState.java | 8 +- Mage/src/main/java/mage/game/stack/Spell.java | 13 +- .../main/java/mage/players/PlayerImpl.java | 8 +- Mage/src/main/java/mage/util/CardUtil.java | 18 +- Mage/src/main/java/mage/util/ManaUtil.java | 9 +- 36 files changed, 637 insertions(+), 264 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/d/DirgurIslandDragon.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/cost/omen/OmenCardsTest.java delete mode 100644 Mage/src/main/java/mage/cards/AdventureCardSpell.java rename Mage/src/main/java/mage/cards/{AdventureCardSpellImpl.java => AdventureSpellCard.java} (88%) create mode 100644 Mage/src/main/java/mage/cards/CardWithSpellOption.java create mode 100644 Mage/src/main/java/mage/cards/OmenCard.java create mode 100644 Mage/src/main/java/mage/cards/OmenSpellCard.java create mode 100644 Mage/src/main/java/mage/cards/SpellOptionCard.java diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java index eb2643061dc..f188a9fbd61 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernCardRenderer.java @@ -123,6 +123,7 @@ public class ModernCardRenderer extends CardRenderer { public static final Color ERROR_COLOR = new Color(255, 0, 255); static String SUB_TYPE_ADVENTURE = "Adventure"; + static String SUB_TYPE_OMEN = "Omen"; /////////////////////////////////////////////////////////////////////////// // Layout metrics for modern border cards @@ -168,8 +169,8 @@ public class ModernCardRenderer extends CardRenderer { // Processed mana cost string protected String manaCostString; - // Is an adventure - protected boolean isAdventure = false; + // Is an adventure or omen + protected boolean isCardWithSpellOption = false; public ModernCardRenderer(CardView card) { // Pass off to parent @@ -179,12 +180,13 @@ public class ModernCardRenderer extends CardRenderer { manaCostString = ManaSymbols.getClearManaCost(cardView.getManaCostStr()); if (cardView.isSplitCard()) { - isAdventure = cardView.getRightSplitTypeLine().contains(SUB_TYPE_ADVENTURE); + isCardWithSpellOption = cardView.getRightSplitTypeLine().contains(SUB_TYPE_ADVENTURE) + || cardView.getRightSplitTypeLine().contains(SUB_TYPE_OMEN); } } - protected boolean isAdventure() { - return isAdventure; + protected boolean isCardWithSpellOption() { + return isCardWithSpellOption; } @Override @@ -660,7 +662,7 @@ public class ModernCardRenderer extends CardRenderer { drawRulesText(g, textboxKeywords, textboxRules, contentWidth / 2 + totalContentInset + 4, totalContentInset + boxHeight + 2, contentWidth / 2 - 8, typeLineY - totalContentInset - boxHeight - 6, false); - } else if (isAdventure) { + } else if (isCardWithSpellOption) { drawRulesText(g, textboxKeywords, textboxRules, contentWidth / 2 + totalContentInset + 4, typeLineY + boxHeight + 2, contentWidth / 2 - 8, cardHeight - typeLineY - boxHeight - 4 - borderWidth * 3, false); diff --git a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java index e43dfc10662..291dad2d4ec 100644 --- a/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java +++ b/Mage.Client/src/main/java/org/mage/card/arcane/ModernSplitCardRenderer.java @@ -56,7 +56,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { private boolean isAftermath = false; private static String trimAdventure(String rule) { - if (rule.startsWith("Adventure")) { + if (rule.startsWith("Adventure") || rule.startsWith("Omen")) { return rule.substring(rule.lastIndexOf("—") + 8); } return rule; @@ -71,7 +71,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { rightHalf.color = new ObjectColor(cardView.getRightSplitCostsStr()); leftHalf.color = new ObjectColor(cardView.getLeftSplitCostsStr()); - if (isAdventure()) { + if (isCardWithSpellOption()) { List trimmedRules = new ArrayList<>(); for (String rule : view.getRightSplitRules()) { trimmedRules.add(trimAdventure(rule)); @@ -95,7 +95,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { // they "rotate" in opposite directions making consquence and normal split cards // have the "right" vs "left" as the top half. // Adventures are treated differently and not rotated at all. - if (isAdventure()) { + if (isCardWithSpellOption()) { manaCostString = leftHalf.manaCostString; textboxKeywords = leftHalf.keywords; textboxRules = leftHalf.rules; @@ -159,7 +159,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { protected void drawBackground(Graphics2D g) { if (cardView.isFaceDown()) { drawCardBackTexture(g); - } if (isAdventure()) { + } if (isCardWithSpellOption()) { super.drawBackground(g); } else { { // Left half background (top of the card) @@ -204,7 +204,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { @Override protected void drawArt(Graphics2D g) { - if (isAdventure) { + if (isCardWithSpellOption) { super.drawArt(g); } else if (artImage != null) { if (isAftermath()) { @@ -318,7 +318,7 @@ public class ModernSplitCardRenderer extends ModernCardRenderer { @Override protected void drawFrame(Graphics2D g, CardPanelAttributes attribs, BufferedImage image, boolean lessOpaqueRulesTextBox) { - if (isAdventure()) { + if (isCardWithSpellOption()) { super.drawFrame(g, attribs, image, lessOpaqueRulesTextBox); CardPanelAttributes adventureAttribs = new CardPanelAttributes( diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java index c13f283c768..84d55c232fb 100644 --- a/Mage.Common/src/main/java/mage/view/CardView.java +++ b/Mage.Common/src/main/java/mage/view/CardView.java @@ -432,21 +432,21 @@ public class CardView extends SimpleCardView { fullCardName = mainCard.getLeftHalfCard().getName() + MockCard.MODAL_DOUBLE_FACES_NAME_SEPARATOR + mainCard.getRightHalfCard().getName(); this.manaCostLeftStr = mainCard.getLeftHalfCard().getManaCostSymbols(); this.manaCostRightStr = mainCard.getRightHalfCard().getManaCostSymbols(); - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { this.isSplitCard = true; - AdventureCard adventureCard = ((AdventureCard) card); - leftSplitName = adventureCard.getName(); - leftSplitCostsStr = String.join("", adventureCard.getManaCostSymbols()); - leftSplitRules = adventureCard.getSharedRules(game); - leftSplitTypeLine = getCardTypeLine(game, adventureCard); - AdventureCardSpell adventureCardSpell = adventureCard.getSpellCard(); - rightSplitName = adventureCardSpell.getName(); - rightSplitCostsStr = String.join("", adventureCardSpell.getManaCostSymbols()); - rightSplitRules = adventureCardSpell.getRules(game); - rightSplitTypeLine = getCardTypeLine(game, adventureCardSpell); - fullCardName = adventureCard.getName() + MockCard.ADVENTURE_NAME_SEPARATOR + adventureCardSpell.getName(); - this.manaCostLeftStr = adventureCard.getManaCostSymbols(); - this.manaCostRightStr = adventureCardSpell.getManaCostSymbols(); + CardWithSpellOption mainCard = ((CardWithSpellOption) card); + leftSplitName = mainCard.getName(); + leftSplitCostsStr = String.join("", mainCard.getManaCostSymbols()); + leftSplitRules = mainCard.getSharedRules(game); + leftSplitTypeLine = getCardTypeLine(game, mainCard); + SpellOptionCard splitCardSpell = mainCard.getSpellCard(); + rightSplitName = splitCardSpell.getName(); + rightSplitCostsStr = String.join("", splitCardSpell.getManaCostSymbols()); + rightSplitRules = splitCardSpell.getRules(game); + rightSplitTypeLine = getCardTypeLine(game, splitCardSpell); + fullCardName = mainCard.getName() + MockCard.CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + splitCardSpell.getName(); + this.manaCostLeftStr = mainCard.getManaCostSymbols(); + this.manaCostRightStr = splitCardSpell.getManaCostSymbols(); } else if (card instanceof MockCard) { // deck editor cards fullCardName = ((MockCard) card).getFullName(true); diff --git a/Mage.Sets/src/mage/cards/b/BrightcapBadger.java b/Mage.Sets/src/mage/cards/b/BrightcapBadger.java index 34e8561d800..14d1b89aee0 100644 --- a/Mage.Sets/src/mage/cards/b/BrightcapBadger.java +++ b/Mage.Sets/src/mage/cards/b/BrightcapBadger.java @@ -52,7 +52,7 @@ public final class BrightcapBadger extends AdventureCard { // Fungus Frolic // Create two 1/1 green Saproling creature tokens. this.getSpellCard().getSpellAbility().addEffect(new CreateTokenEffect(new SaprolingToken(), 2)); - this.getSpellCard().finalizeAdventure(); + this.finalizeAdventure(); } private BrightcapBadger(final BrightcapBadger card) { diff --git a/Mage.Sets/src/mage/cards/d/DarksteelMonolith.java b/Mage.Sets/src/mage/cards/d/DarksteelMonolith.java index 3ae3a10afc3..158d6a8747b 100644 --- a/Mage.Sets/src/mage/cards/d/DarksteelMonolith.java +++ b/Mage.Sets/src/mage/cards/d/DarksteelMonolith.java @@ -54,7 +54,7 @@ enum IsBeingCastFromHandCondition implements Condition { @Override public boolean apply(Game game, Ability source) { MageObject object = game.getObject(source); - if (object instanceof SplitCardHalf || object instanceof AdventureCardSpell || object instanceof ModalDoubleFacedCardHalf) { + if (object instanceof SplitCardHalf || object instanceof SpellOptionCard || object instanceof ModalDoubleFacedCardHalf) { UUID mainCardId = ((Card) object).getMainCard().getId(); object = game.getObject(mainCardId); } diff --git a/Mage.Sets/src/mage/cards/d/DirgurIslandDragon.java b/Mage.Sets/src/mage/cards/d/DirgurIslandDragon.java new file mode 100644 index 00000000000..bd4685d7d1b --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DirgurIslandDragon.java @@ -0,0 +1,52 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardSetInfo; +import mage.cards.OmenCard; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * + * @author Jmlundeen + */ +public final class DirgurIslandDragon extends OmenCard { + + public DirgurIslandDragon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.INSTANT}, "{5}{U}", "Skimming Strike", "{1}{U}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Ward {2} + this.addAbility(new WardAbility(new ManaCostsImpl<>("{2}"))); + + // Skimming Strike + // Tap up to one target creature. Draw a card. + this.getSpellCard().getSpellAbility().addEffect(new TapTargetEffect()); + this.getSpellCard().getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); + this.getSpellCard().getSpellAbility().addTarget(new TargetCreaturePermanent(0, 1)); + this.finalizeOmen(); + } + + private DirgurIslandDragon(final DirgurIslandDragon card) { + super(card); + } + + @Override + public DirgurIslandDragon copy() { + return new DirgurIslandDragon(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TlincalliHunter.java b/Mage.Sets/src/mage/cards/t/TlincalliHunter.java index 9cf8845dc43..732154314e4 100644 --- a/Mage.Sets/src/mage/cards/t/TlincalliHunter.java +++ b/Mage.Sets/src/mage/cards/t/TlincalliHunter.java @@ -65,7 +65,7 @@ enum ExiledCreatureSpellCondition implements Condition { @Override public boolean apply(Game game, Ability source) { MageObject object = game.getObject(source); - if (object instanceof SplitCardHalf || object instanceof AdventureCardSpell || object instanceof ModalDoubleFacedCardHalf) { + if (object instanceof SplitCardHalf || object instanceof SpellOptionCard || object instanceof ModalDoubleFacedCardHalf) { UUID mainCardId = ((Card) object).getMainCard().getId(); object = game.getObject(mainCardId); } diff --git a/Mage.Sets/src/mage/cards/t/TwiceUponATime.java b/Mage.Sets/src/mage/cards/t/TwiceUponATime.java index 9bf61c5f838..90a46602fbd 100644 --- a/Mage.Sets/src/mage/cards/t/TwiceUponATime.java +++ b/Mage.Sets/src/mage/cards/t/TwiceUponATime.java @@ -47,7 +47,7 @@ public final class TwiceUponATime extends AdventureCard { // Unlikely Meeting // Search your library for a Doctor card, reveal it, put it into your hand, then shuffle. this.getSpellCard().getSpellAbility().addEffect(new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter2), true)); - this.getSpellCard().finalizeAdventure(); + this.finalizeAdventure(); } private TwiceUponATime(final TwiceUponATime card) { diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 170bef109a3..9f498363e61 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -67,6 +67,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Delta Bloodflies", 77, Rarity.COMMON, mage.cards.d.DeltaBloodflies.class)); cards.add(new SetCardInfo("Descendant of Storms", 8, Rarity.UNCOMMON, mage.cards.d.DescendantOfStorms.class)); cards.add(new SetCardInfo("Devoted Duelist", 104, Rarity.COMMON, mage.cards.d.DevotedDuelist.class)); + cards.add(new SetCardInfo("Dirgur Island Dragon", 40, Rarity.COMMON, mage.cards.d.DirgurIslandDragon.class)); cards.add(new SetCardInfo("Dismal Backwater", 254, Rarity.COMMON, mage.cards.d.DismalBackwater.class)); cards.add(new SetCardInfo("Dispelling Exhale", 41, Rarity.COMMON, mage.cards.d.DispellingExhale.class)); cards.add(new SetCardInfo("Dracogenesis", 105, Rarity.MYTHIC, mage.cards.d.Dracogenesis.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/omen/OmenCardsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/omen/OmenCardsTest.java new file mode 100644 index 00000000000..e6572b126be --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/omen/OmenCardsTest.java @@ -0,0 +1,77 @@ +package org.mage.test.cards.cost.omen; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class OmenCardsTest extends CardTestPlayerBase { + + @Test + public void testDirgurIslandDragonShuffle() { + setStrictChooseMode(true); + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.HAND, playerA, "Dirgur Island Dragon"); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerB, "Bear Cub"); + addCard(Zone.LIBRARY, playerA, "Mountain"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Skimming Strike", "Bear Cub"); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertLibraryCount(playerA, "Dirgur Island Dragon", 1); + assertTapped("Bear Cub", true); + assertHandCount(playerA, 1); + } + + @Test + public void testDirgurIslandDragonShuffleAndPlay() { + setStrictChooseMode(true); + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + addCard(Zone.BATTLEFIELD, playerB, "Bear Cub"); + addCard(Zone.LIBRARY, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Dirgur Island Dragon"); + + castSpell(2, PhaseStep.BEGIN_COMBAT, playerA, "Skimming Strike", "Bear Cub"); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Dirgur Island Dragon"); + setStopAt(3, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, "Dirgur Island Dragon", 1); + } + + @Test + public void testCounteredInGraveyard() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerB,"Island", 4); + addCard(Zone.BATTLEFIELD, playerB,"Bear Cub"); + addCard(Zone.HAND, playerA, "Dirgur Island Dragon"); + addCard(Zone.HAND, playerB, "Counterspell"); + + castSpell(2, PhaseStep.BEGIN_COMBAT, playerA, "Skimming Strike", "Bear Cub"); + castSpell(2, PhaseStep.BEGIN_COMBAT, playerB, "Counterspell", "Skimming Strike", "Skimming Strike", StackClause.WHILE_ON_STACK); + attack(2, playerB, "Bear Cub"); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertGraveyardCount(playerA, "Dirgur Island Dragon", 1); + assertLife(playerA, 20 - 2); + } + + @Test + public void testGraveyardCast() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, "Kess, Dissident Mage"); + addCard(Zone.GRAVEYARD, playerA, "Dirgur Island Dragon"); + addCard(Zone.BATTLEFIELD, playerB, "Bear Cub"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Skimming Strike", "Bear Cub"); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLibraryCount(playerA, "Dirgur Island Dragon", 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/SpellAbility.java b/Mage/src/main/java/mage/abilities/SpellAbility.java index 2243a693b61..2c5c03ef220 100644 --- a/Mage/src/main/java/mage/abilities/SpellAbility.java +++ b/Mage/src/main/java/mage/abilities/SpellAbility.java @@ -6,8 +6,8 @@ import mage.MageObject; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.keyword.FlashAbility; -import mage.cards.AdventureCardSpell; import mage.cards.Card; +import mage.cards.SpellOptionCard; import mage.cards.SplitCard; import mage.constants.*; import mage.game.Game; @@ -99,7 +99,7 @@ public class SpellAbility extends ActivatedAbilityImpl { // forced to cast (can be part id or main id) Set idsToCheck = new HashSet<>(); idsToCheck.add(object.getId()); - if (object instanceof Card && !(object instanceof AdventureCardSpell)) { + if (object instanceof Card && !(object instanceof SpellOptionCard)) { idsToCheck.add(((Card) object).getMainCard().getId()); } for (UUID idToCheck : idsToCheck) { diff --git a/Mage/src/main/java/mage/abilities/condition/common/IsBeingCastFromHandCondition.java b/Mage/src/main/java/mage/abilities/condition/common/IsBeingCastFromHandCondition.java index fe3ca1e23ce..6e0e5b65174 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/IsBeingCastFromHandCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/IsBeingCastFromHandCondition.java @@ -4,7 +4,7 @@ package mage.abilities.condition.common; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.condition.Condition; -import mage.cards.AdventureCardSpell; +import mage.cards.SpellOptionCard; import mage.cards.Card; import mage.cards.ModalDoubleFacedCardHalf; import mage.cards.SplitCardHalf; @@ -20,7 +20,7 @@ public enum IsBeingCastFromHandCondition implements Condition { @Override public boolean apply(Game game, Ability source) { MageObject object = game.getObject(source); - if (object instanceof SplitCardHalf || object instanceof AdventureCardSpell || object instanceof ModalDoubleFacedCardHalf) { + if (object instanceof SplitCardHalf || object instanceof SpellOptionCard || object instanceof ModalDoubleFacedCardHalf) { UUID mainCardId = ((Card) object).getMainCard().getId(); object = game.getObject(mainCardId); } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index ea47fe4f39e..90e911e06c8 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -549,9 +549,9 @@ public class ContinuousEffects implements Serializable { // rules: // 708.4. In every zone except the stack, the characteristics of a split card are those of its two halves combined. idToCheck = ((SplitCardHalf) objectToCheck).getMainCard().getId(); - } else if (!type.needPlayCardAbility() && objectToCheck instanceof AdventureCardSpell) { - // adventure spell uses alternative characteristics for spell/stack, all other cases must use main card - idToCheck = ((AdventureCardSpell) objectToCheck).getMainCard().getId(); + } else if (!type.needPlayCardAbility() && objectToCheck instanceof CardWithSpellOption) { + // adventure/omen spell uses alternative characteristics for spell/stack, all other cases must use main card + idToCheck = ((CardWithSpellOption) objectToCheck).getMainCard().getId(); } else if (!type.needPlayCardAbility() && objectToCheck instanceof ModalDoubleFacedCardHalf) { // each mdf side uses own characteristics to check for playing, all other cases must use main card // rules: diff --git a/Mage/src/main/java/mage/abilities/effects/common/ExileAdventureSpellEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ExileAdventureSpellEffect.java index 907881dea21..706af3cd62a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ExileAdventureSpellEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ExileAdventureSpellEffect.java @@ -5,7 +5,7 @@ import mage.abilities.MageSingleton; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; -import mage.cards.AdventureCardSpell; +import mage.cards.AdventureSpellCard; import mage.cards.Card; import mage.constants.AsThoughEffectType; import mage.constants.Duration; @@ -51,10 +51,10 @@ public class ExileAdventureSpellEffect extends OneShotEffect implements MageSing Spell spell = game.getStack().getSpell(source.getId()); if (spell != null) { Card spellCard = spell.getCard(); - if (spellCard instanceof AdventureCardSpell) { + if (spellCard instanceof AdventureSpellCard) { UUID exileId = adventureExileId(controller.getId(), game); game.getExile().createZone(exileId, "On an Adventure from " + controller.getName()); - AdventureCardSpell adventureSpellCard = (AdventureCardSpell) spellCard; + AdventureSpellCard adventureSpellCard = (AdventureSpellCard) spellCard; Card parentCard = adventureSpellCard.getParentCard(); if (controller.moveCardsToExile(parentCard, source, game, true, exileId, "On an Adventure from " + controller.getName())) { ContinuousEffect effect = new AdventureCastFromExileEffect(); diff --git a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java index 9287ae99694..d094a49f537 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ForetellAbility.java @@ -257,7 +257,7 @@ public class ForetellAbility extends SpecialAction { game.getState().addOtherAbility(rightHalfCard, ability); } } - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { if (foretellCost != null) { Card creatureCard = card.getMainCard(); ForetellCostAbility ability = new ForetellCostAbility(foretellCost); @@ -268,7 +268,7 @@ public class ForetellAbility extends SpecialAction { game.getState().addOtherAbility(creatureCard, ability); } if (foretellSplitCost != null) { - Card spellCard = ((AdventureCard) card).getSpellCard(); + Card spellCard = ((CardWithSpellOption) card).getSpellCard(); ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost); ability.setSourceId(spellCard.getId()); ability.setControllerId(source.getControllerId()); @@ -360,11 +360,11 @@ public class ForetellAbility extends SpecialAction { } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); } - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { if (card.getMainCard().getName().equals(abilityName)) { return card.getMainCard().getSpellAbility().canActivate(playerId, game); - } else if (((AdventureCard) card).getSpellCard().getName().equals(abilityName)) { - return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game); + } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) { + return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game); } } return card.getSpellAbility().canActivate(playerId, game); @@ -391,11 +391,11 @@ public class ForetellAbility extends SpecialAction { } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy(); } - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { if (card.getMainCard().getName().equals(abilityName)) { spellAbilityCopy = card.getMainCard().getSpellAbility().copy(); - } else if (((AdventureCard) card).getSpellCard().getName().equals(abilityName)) { - spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy(); + } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) { + spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy(); } } else { spellAbilityCopy = card.getSpellAbility().copy(); diff --git a/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java b/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java index c91a6f2cbbd..93375e704b3 100644 --- a/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/PlotAbility.java @@ -268,11 +268,11 @@ class PlotSpellAbility extends SpellAbility { } else if (((CardWithHalves) mainCard).getRightHalfCard().getName().equals(faceCardName)) { return ((CardWithHalves) mainCard).getRightHalfCard().getSpellAbility().canActivate(playerId, game); } - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { if (card.getMainCard().getName().equals(faceCardName)) { return card.getMainCard().getSpellAbility().canActivate(playerId, game); - } else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) { - return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game); + } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(faceCardName)) { + return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game); } } return card.getSpellAbility().canActivate(playerId, game); @@ -294,11 +294,11 @@ class PlotSpellAbility extends SpellAbility { } else if (((CardWithHalves) card).getRightHalfCard().getName().equals(faceCardName)) { spellAbilityCopy = ((CardWithHalves) card).getRightHalfCard().getSpellAbility().copy(); } - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { if (card.getMainCard().getName().equals(faceCardName)) { spellAbilityCopy = card.getMainCard().getSpellAbility().copy(); - } else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) { - spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy(); + } else if (((CardWithSpellOption) card).getSpellCard().getName().equals(faceCardName)) { + spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy(); } } else { spellAbilityCopy = card.getSpellAbility().copy(); diff --git a/Mage/src/main/java/mage/cards/AdventureCard.java b/Mage/src/main/java/mage/cards/AdventureCard.java index 413d9d6e026..af6e67ca7c1 100644 --- a/Mage/src/main/java/mage/cards/AdventureCard.java +++ b/Mage/src/main/java/mage/cards/AdventureCard.java @@ -1,98 +1,29 @@ package mage.cards; -import mage.abilities.Abilities; -import mage.abilities.AbilitiesImpl; -import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.constants.CardType; import mage.constants.SpellAbilityType; import mage.constants.Zone; import mage.game.Game; -import mage.game.events.ZoneChangeEvent; -import mage.util.CardUtil; -import java.util.List; import java.util.UUID; /** * @author phulin */ -public abstract class AdventureCard extends CardImpl { - - /* The adventure spell card, i.e. Swift End. */ - protected AdventureCardSpell spellCard; +public abstract class AdventureCard extends CardWithSpellOption { public AdventureCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String adventureName, String costsSpell) { super(ownerId, setInfo, types, costs); - this.spellCard = new AdventureCardSpellImpl(ownerId, setInfo, adventureName, typesSpell, costsSpell, this); + this.spellCard = new AdventureSpellCard(ownerId, setInfo, adventureName, typesSpell, costsSpell, this); + } + + public AdventureCard(AdventureCard card) { + super(card); } public void finalizeAdventure() { - spellCard.finalizeAdventure(); - } - - protected AdventureCard(final AdventureCard card) { - super(card); - this.spellCard = card.getSpellCard().copy(); - this.spellCard.setParentCard(this); - } - - public AdventureCardSpell getSpellCard() { - return spellCard; - } - - public void setParts(AdventureCardSpell cardSpell) { - // for card copy only - set new parts - this.spellCard = cardSpell; - cardSpell.setParentCard(this); - } - - @Override - public void assignNewId() { - super.assignNewId(); - spellCard.assignNewId(); - } - - @Override - public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List appliedEffects) { - if (super.moveToZone(toZone, source, game, flag, appliedEffects)) { - Zone currentZone = game.getState().getZone(getId()); - game.getState().setZone(getSpellCard().getId(), currentZone); - return true; - } - return false; - } - - @Override - public void setZone(Zone zone, Game game) { - super.setZone(zone, game); - game.setZone(getSpellCard().getId(), zone); - } - - @Override - public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List appliedEffects) { - if (super.moveToExile(exileId, name, source, game, appliedEffects)) { - Zone currentZone = game.getState().getZone(getId()); - game.getState().setZone(getSpellCard().getId(), currentZone); - return true; - } - return false; - } - - @Override - public boolean removeFromZone(Game game, Zone fromZone, Ability source) { - // zone contains only one main card - return super.removeFromZone(game, fromZone, source); - } - - @Override - public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) { - if (isCopy()) { // same as meld cards - super.updateZoneChangeCounter(game, event); - return; - } - super.updateZoneChangeCounter(game, event); - getSpellCard().updateZoneChangeCounter(game, event); + spellCard.finalizeSpell(); } @Override @@ -103,45 +34,4 @@ public abstract class AdventureCard extends CardImpl { this.getSpellCard().getSpellAbility().setControllerId(controllerId); return super.cast(game, fromZone, ability, controllerId); } - - @Override - public Abilities getAbilities() { - Abilities allAbilities = new AbilitiesImpl<>(); - allAbilities.addAll(spellCard.getAbilities()); - allAbilities.addAll(super.getAbilities()); - return allAbilities; - } - - @Override - public Abilities getInitAbilities() { - // must init only parent related abilities, spell card must be init separately - return super.getAbilities(); - } - - @Override - public Abilities getAbilities(Game game) { - Abilities allAbilities = new AbilitiesImpl<>(); - allAbilities.addAll(spellCard.getAbilities(game)); - allAbilities.addAll(super.getAbilities(game)); - return allAbilities; - } - - public Abilities getSharedAbilities(Game game) { - // abilities without spellcard - return super.getAbilities(game); - } - - public List getSharedRules(Game game) { - // rules without spellcard - Abilities sourceAbilities = this.getSharedAbilities(game); - return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities); - } - - @Override - public void setOwnerId(UUID ownerId) { - super.setOwnerId(ownerId); - abilities.setControllerId(ownerId); - spellCard.getAbilities().setControllerId(ownerId); - spellCard.setOwnerId(ownerId); - } } diff --git a/Mage/src/main/java/mage/cards/AdventureCardSpell.java b/Mage/src/main/java/mage/cards/AdventureCardSpell.java deleted file mode 100644 index 27f14c5493a..00000000000 --- a/Mage/src/main/java/mage/cards/AdventureCardSpell.java +++ /dev/null @@ -1,12 +0,0 @@ -package mage.cards; - -/** - * @author phulin - */ -public interface AdventureCardSpell extends SubCard { - - @Override - AdventureCardSpell copy(); - - void finalizeAdventure(); -} diff --git a/Mage/src/main/java/mage/cards/AdventureCardSpellImpl.java b/Mage/src/main/java/mage/cards/AdventureSpellCard.java similarity index 88% rename from Mage/src/main/java/mage/cards/AdventureCardSpellImpl.java rename to Mage/src/main/java/mage/cards/AdventureSpellCard.java index 2062b27ca9e..ed6efa5f1f1 100644 --- a/Mage/src/main/java/mage/cards/AdventureCardSpellImpl.java +++ b/Mage/src/main/java/mage/cards/AdventureSpellCard.java @@ -19,11 +19,11 @@ import java.util.stream.Collectors; /** * @author phulin */ -public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpell { +public class AdventureSpellCard extends CardImpl implements SpellOptionCard { private AdventureCard adventureCardParent; - public AdventureCardSpellImpl(UUID ownerId, CardSetInfo setInfo, String adventureName, CardType[] cardTypes, String costs, AdventureCard adventureCardParent) { + public AdventureSpellCard(UUID ownerId, CardSetInfo setInfo, String adventureName, CardType[] cardTypes, String costs, AdventureCard adventureCardParent) { super(ownerId, setInfo, cardTypes, costs, SpellAbilityType.ADVENTURE_SPELL); this.subtype.add(SubType.ADVENTURE); @@ -35,13 +35,13 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe this.adventureCardParent = adventureCardParent; } - public void finalizeAdventure() { + public void finalizeSpell() { if (spellAbility instanceof AdventureCardSpellAbility) { ((AdventureCardSpellAbility) spellAbility).finalizeAdventure(); } } - protected AdventureCardSpellImpl(final AdventureCardSpellImpl card) { + protected AdventureSpellCard(final AdventureSpellCard card) { super(card); this.adventureCardParent = card.adventureCardParent; } @@ -83,13 +83,13 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe } @Override - public AdventureCardSpellImpl copy() { - return new AdventureCardSpellImpl(this); + public AdventureSpellCard copy() { + return new AdventureSpellCard(this); } @Override - public void setParentCard(AdventureCard card) { - this.adventureCardParent = card; + public void setParentCard(CardWithSpellOption card) { + this.adventureCardParent = (AdventureCard) card; } @Override @@ -102,6 +102,11 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe // id must send to main card (popup card hint in game logs) return getName() + " [" + adventureCardParent.getId().toString().substring(0, 3) + ']'; } + + @Override + public String getSpellType() { + return "Adventure"; + } } class AdventureCardSpellAbility extends SpellAbility { @@ -141,8 +146,8 @@ class AdventureCardSpellAbility extends SpellAbility { public ActivationStatus canActivate(UUID playerId, Game game) { ExileZone adventureExileZone = game.getExile().getExileZone(ExileAdventureSpellEffect.adventureExileId(playerId, game)); Card spellCard = game.getCard(this.getSourceId()); - if (spellCard instanceof AdventureCardSpell) { - Card card = ((AdventureCardSpell) spellCard).getParentCard(); + if (spellCard instanceof AdventureSpellCard) { + Card card = ((AdventureSpellCard) spellCard).getParentCard(); if (adventureExileZone != null && adventureExileZone.contains(card.getId())) { return ActivationStatus.getFalse(); } diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index e6c7aa4b9ae..2515d21c1fd 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -530,8 +530,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card { } } - if (stackObject == null && (this instanceof AdventureCard)) { - stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false); + if (stackObject == null && (this instanceof CardWithSpellOption)) { + stackObject = game.getStack().getSpell(((CardWithSpellOption) this).getSpellCard().getId(), false); } if (stackObject == null) { diff --git a/Mage/src/main/java/mage/cards/CardWithSpellOption.java b/Mage/src/main/java/mage/cards/CardWithSpellOption.java new file mode 100644 index 00000000000..b1470f7dedc --- /dev/null +++ b/Mage/src/main/java/mage/cards/CardWithSpellOption.java @@ -0,0 +1,131 @@ +package mage.cards; + +import mage.abilities.Abilities; +import mage.abilities.AbilitiesImpl; +import mage.abilities.Ability; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.ZoneChangeEvent; +import mage.util.CardUtil; + +import java.util.List; +import java.util.UUID; + +/** + * @author phulin, jmlundeen + */ +public abstract class CardWithSpellOption extends CardImpl { + + /* The adventure/omen spell card, i.e. Swift End. */ + protected SpellOptionCard spellCard; + + public CardWithSpellOption(UUID ownerId, CardSetInfo setInfo, CardType[] types, String costs) { + super(ownerId, setInfo, types, costs); + } + + public CardWithSpellOption(CardWithSpellOption card) { + super(card); + this.spellCard = card.getSpellCard().copy(); + this.spellCard.setParentCard(this); + } + + public SpellOptionCard getSpellCard() { + return spellCard; + } + + public void setParts(SpellOptionCard cardSpell) { + // for card copy only - set new parts + this.spellCard = cardSpell; + cardSpell.setParentCard(this); + } + + @Override + public void assignNewId() { + super.assignNewId(); + spellCard.assignNewId(); + } + + @Override + public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List appliedEffects) { + if (super.moveToZone(toZone, source, game, flag, appliedEffects)) { + Zone currentZone = game.getState().getZone(getId()); + game.getState().setZone(getSpellCard().getId(), currentZone); + return true; + } + return false; + } + + @Override + public void setZone(Zone zone, Game game) { + super.setZone(zone, game); + game.setZone(getSpellCard().getId(), zone); + } + + @Override + public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List appliedEffects) { + if (super.moveToExile(exileId, name, source, game, appliedEffects)) { + Zone currentZone = game.getState().getZone(getId()); + game.getState().setZone(getSpellCard().getId(), currentZone); + return true; + } + return false; + } + + @Override + public boolean removeFromZone(Game game, Zone fromZone, Ability source) { + // zone contains only one main card + return super.removeFromZone(game, fromZone, source); + } + + @Override + public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) { + if (isCopy()) { // same as meld cards + super.updateZoneChangeCounter(game, event); + return; + } + super.updateZoneChangeCounter(game, event); + getSpellCard().updateZoneChangeCounter(game, event); + } + + @Override + public Abilities getAbilities() { + Abilities allAbilities = new AbilitiesImpl<>(); + allAbilities.addAll(spellCard.getAbilities()); + allAbilities.addAll(super.getAbilities()); + return allAbilities; + } + + @Override + public Abilities getInitAbilities() { + // must init only parent related abilities, spell card must be init separately + return super.getAbilities(); + } + + @Override + public Abilities getAbilities(Game game) { + Abilities allAbilities = new AbilitiesImpl<>(); + allAbilities.addAll(spellCard.getAbilities(game)); + allAbilities.addAll(super.getAbilities(game)); + return allAbilities; + } + + public Abilities getSharedAbilities(Game game) { + // abilities without spellCard + return super.getAbilities(game); + } + + public List getSharedRules(Game game) { + // rules without spellCard + Abilities sourceAbilities = this.getSharedAbilities(game); + return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities); + } + + @Override + public void setOwnerId(UUID ownerId) { + super.setOwnerId(ownerId); + abilities.setControllerId(ownerId); + spellCard.getAbilities().setControllerId(ownerId); + spellCard.setOwnerId(ownerId); + } +} diff --git a/Mage/src/main/java/mage/cards/OmenCard.java b/Mage/src/main/java/mage/cards/OmenCard.java new file mode 100644 index 00000000000..82401304f12 --- /dev/null +++ b/Mage/src/main/java/mage/cards/OmenCard.java @@ -0,0 +1,34 @@ +package mage.cards; + +import mage.abilities.SpellAbility; +import mage.constants.CardType; +import mage.constants.SpellAbilityType; +import mage.constants.Zone; +import mage.game.Game; + +import java.util.UUID; + +public abstract class OmenCard extends CardWithSpellOption { + + public OmenCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String omenName, String costsSpell) { + super(ownerId, setInfo, types, costs); + this.spellCard = new OmenSpellCard(ownerId, setInfo, omenName, typesSpell, costsSpell, this); + } + + public OmenCard(OmenCard card) { + super(card); + } + + public void finalizeOmen() { + spellCard.finalizeSpell(); + } + + @Override + public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) { + if (ability.getSpellAbilityType() == SpellAbilityType.OMEN_SPELL) { + return this.getSpellCard().cast(game, fromZone, ability, controllerId); + } + this.getSpellCard().getSpellAbility().setControllerId(controllerId); + return super.cast(game, fromZone, ability, controllerId); + } +} diff --git a/Mage/src/main/java/mage/cards/OmenSpellCard.java b/Mage/src/main/java/mage/cards/OmenSpellCard.java new file mode 100644 index 00000000000..040bba51217 --- /dev/null +++ b/Mage/src/main/java/mage/cards/OmenSpellCard.java @@ -0,0 +1,174 @@ +package mage.cards; + +import mage.abilities.Ability; +import mage.abilities.Modes; +import mage.abilities.SpellAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.ShuffleIntoLibrarySourceEffect; +import mage.constants.CardType; +import mage.constants.SpellAbilityType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class OmenSpellCard extends CardImpl implements SpellOptionCard { + + private OmenCard omenCardParent; + + public OmenSpellCard(UUID ownerId, CardSetInfo setInfo, String omenName, CardType[] cardTypes, String costs, OmenCard omenCard) { + super(ownerId, setInfo, cardTypes, costs, SpellAbilityType.OMEN_SPELL); + this.subtype.add(SubType.OMEN); + + OmenCardSpellAbility newSpellAbility = new OmenCardSpellAbility(getSpellAbility(), omenName, cardTypes, costs); + this.replaceSpellAbility(newSpellAbility); + spellAbility = newSpellAbility; + + this.setName(omenName); + this.omenCardParent = omenCard; + } + + public void finalizeSpell() { + if (spellAbility instanceof OmenCardSpellAbility) { + ((OmenCardSpellAbility) spellAbility).finalizeOmen(); + } + } + + protected OmenSpellCard(final OmenSpellCard card) { + super(card); + this.omenCardParent = card.omenCardParent; + } + + @Override + public UUID getOwnerId() { + return omenCardParent.getOwnerId(); + } + + @Override + public String getExpansionSetCode() { + return omenCardParent.getExpansionSetCode(); + } + + @Override + public String getCardNumber() { + return omenCardParent.getCardNumber(); + } + + @Override + public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List appliedEffects) { + return omenCardParent.moveToZone(toZone, source, game, flag, appliedEffects); + } + + @Override + public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List appliedEffects) { + return omenCardParent.moveToExile(exileId, name, source, game, appliedEffects); + } + + @Override + public OmenCard getMainCard() { + return omenCardParent; + } + + @Override + public void setZone(Zone zone, Game game) { + game.setZone(omenCardParent.getId(), zone); + game.setZone(omenCardParent.getSpellCard().getId(), zone); + } + + @Override + public OmenSpellCard copy() { + return new OmenSpellCard(this); + } + + @Override + public void setParentCard(CardWithSpellOption card) { + this.omenCardParent = (OmenCard) card; + } + + @Override + public OmenCard getParentCard() { + return this.omenCardParent; + } + + @Override + public String getIdName() { + // id must send to main card (popup card hint in game logs) + return getName() + " [" + omenCardParent.getId().toString().substring(0, 3) + ']'; + } + + @Override + public String getSpellType() { + return "Omen"; + } +} + +class OmenCardSpellAbility extends SpellAbility { + + private String nameFull; + private boolean finalized = false; + + public OmenCardSpellAbility(final SpellAbility baseSpellAbility, String omenName, CardType[] cardTypes, String costs) { + super(baseSpellAbility); + this.setName(cardTypes, omenName, costs); + this.setCardName(omenName); + } + + public void finalizeOmen() { + if (finalized) { + throw new IllegalStateException("Wrong code usage. " + + "Omen (" + cardName + ") " + + "need to call finalizeOmen() exactly once."); + } + Effect effect = new ShuffleIntoLibrarySourceEffect(); + effect.setText(""); + this.addEffect(effect); + this.finalized = true; + } + + protected OmenCardSpellAbility(final OmenCardSpellAbility ability) { + super(ability); + this.nameFull = ability.nameFull; + if (!ability.finalized) { + throw new IllegalStateException("Wrong code usage. " + + "Omen (" + cardName + ") " + + "need to call finalizeOmen() at the very end of the card's constructor."); + } + this.finalized = true; + } + + public void setName(CardType[] cardTypes, String omenName, String costs) { + this.nameFull = "Omen " + Arrays.stream(cardTypes).map(CardType::toString).collect(Collectors.joining(" ")) + " — " + omenName; + this.name = this.nameFull + " " + costs; + } + + @Override + public String getRule(boolean all) { + return this.getRule(); + } + + @Override + public String getRule() { + StringBuilder sbRule = new StringBuilder(); + sbRule.append(this.nameFull); + sbRule.append(" "); + sbRule.append(getManaCosts().getText()); + sbRule.append(" — "); + Modes modes = this.getModes(); + if (modes.size() <= 1) { + sbRule.append(modes.getMode().getEffects().getTextStartingUpperCase(modes.getMode())); + } else { + sbRule.append(getModes().getText()); + } + sbRule.append(" (Then shuffle this card into its owner's library.)"); + return sbRule.toString(); + } + + @Override + public OmenCardSpellAbility copy() { + return new OmenCardSpellAbility(this); + } +} diff --git a/Mage/src/main/java/mage/cards/SpellOptionCard.java b/Mage/src/main/java/mage/cards/SpellOptionCard.java new file mode 100644 index 00000000000..0007d70fcbc --- /dev/null +++ b/Mage/src/main/java/mage/cards/SpellOptionCard.java @@ -0,0 +1,18 @@ +package mage.cards; + +public interface SpellOptionCard extends SubCard { + + @Override + SpellOptionCard copy(); + + /** + * Adds the final shared ability to the card. e.g. Adventure exile effect / Omen shuffle effect + */ + void finalizeSpell(); + + /** + * Used to get the card type text such as Adventure. Currently only used in {@link mage.game.stack.Spell#getSpellCastText Spell} for logging the spell + * being cast as part of the two part card. + */ + String getSpellType(); +} diff --git a/Mage/src/main/java/mage/cards/mock/MockCard.java b/Mage/src/main/java/mage/cards/mock/MockCard.java index ae406a39dca..6d768ef8b67 100644 --- a/Mage/src/main/java/mage/cards/mock/MockCard.java +++ b/Mage/src/main/java/mage/cards/mock/MockCard.java @@ -20,7 +20,7 @@ import java.util.List; */ public class MockCard extends CardImpl implements MockableCard { - public static String ADVENTURE_NAME_SEPARATOR = " // "; + public static String CARD_WITH_SPELL_OPTION_NAME_SEPARATOR = " // "; public static String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // "; // Needs to be here, as it is normally calculated from the @@ -34,7 +34,7 @@ public class MockCard extends CardImpl implements MockableCard { protected List manaCostLeftStr; protected List manaCostRightStr; protected List manaCostStr; - protected String adventureSpellName; + protected String spellOptionName; // adventure/omen spell name protected boolean isModalDoubleFacedCard; protected int manaValue; @@ -71,8 +71,8 @@ public class MockCard extends CardImpl implements MockableCard { this.secondSideCard = new MockCard(CardRepository.instance.findCardWithPreferredSetAndNumber(card.getSecondSideName(), card.getSetCode(), card.getCardNumber())); } - if (card.isAdventureCard()) { - this.adventureSpellName = card.getAdventureSpellName(); + if (card.isCardWithSpellOption()) { + this.spellOptionName = card.getSpellOptionCardName(); } if (card.isModalDoubleFacedCard()) { @@ -101,7 +101,7 @@ public class MockCard extends CardImpl implements MockableCard { this.manaCostLeftStr = new ArrayList<>(card.manaCostLeftStr); this.manaCostRightStr = new ArrayList<>(card.manaCostRightStr); this.manaCostStr = new ArrayList<>(card.manaCostStr); - this.adventureSpellName = card.adventureSpellName; + this.spellOptionName = card.spellOptionName; this.isModalDoubleFacedCard = card.isModalDoubleFacedCard; this.manaValue = card.manaValue; } @@ -155,8 +155,8 @@ public class MockCard extends CardImpl implements MockableCard { return getName(); } - if (adventureSpellName != null) { - return getName() + ADVENTURE_NAME_SEPARATOR + adventureSpellName; + if (spellOptionName != null) { + return getName() + CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + spellOptionName; } else if (isModalDoubleFacedCard) { return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.getSecondCardFace().getName(); } else { diff --git a/Mage/src/main/java/mage/cards/repository/CardInfo.java b/Mage/src/main/java/mage/cards/repository/CardInfo.java index 7477dc9bee3..d870673e76d 100644 --- a/Mage/src/main/java/mage/cards/repository/CardInfo.java +++ b/Mage/src/main/java/mage/cards/repository/CardInfo.java @@ -106,9 +106,9 @@ public class CardInfo { @DatabaseField protected String secondSideName; @DatabaseField - protected boolean adventureCard; + protected boolean cardWithSpellOption; @DatabaseField - protected String adventureSpellName; + protected String spellOptionCardName; @DatabaseField protected boolean modalDoubleFacedCard; @DatabaseField @@ -157,9 +157,9 @@ public class CardInfo { this.secondSideName = secondSide.getName(); } - if (card instanceof AdventureCard) { - this.adventureCard = true; - this.adventureSpellName = ((AdventureCard) card).getSpellCard().getName(); + if (card instanceof CardWithSpellOption) { + this.cardWithSpellOption = true; + this.spellOptionCardName = ((CardWithSpellOption) card).getSpellCard().getName(); } if (card instanceof ModalDoubleFacedCard) { @@ -189,8 +189,8 @@ public class CardInfo { List manaCostLeft = ((ModalDoubleFacedCard) card).getLeftHalfCard().getManaCostSymbols(); List manaCostRight = ((ModalDoubleFacedCard) card).getRightHalfCard().getManaCostSymbols(); this.setManaCosts(CardUtil.concatManaSymbols(SPLIT_MANA_SEPARATOR_FULL, manaCostLeft, manaCostRight)); - } else if (card instanceof AdventureCard) { - List manaCostLeft = ((AdventureCard) card).getSpellCard().getManaCostSymbols(); + } else if (card instanceof CardWithSpellOption) { + List manaCostLeft = ((CardWithSpellOption) card).getSpellCard().getManaCostSymbols(); List manaCostRight = card.getManaCostSymbols(); this.setManaCosts(CardUtil.concatManaSymbols(SPLIT_MANA_SEPARATOR_FULL, manaCostLeft, manaCostRight)); } else { @@ -469,12 +469,16 @@ public class CardInfo { return secondSideName; } - public boolean isAdventureCard() { - return adventureCard; + public boolean isCardWithSpellOption() { + return cardWithSpellOption; } - public String getAdventureSpellName() { - return adventureSpellName; + /** + * used for spell card portion of adventure/omen cards + * @return name of the spell + */ + public String getSpellOptionCardName() { + return spellOptionCardName; } public boolean isModalDoubleFacedCard() { diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java index 24484308724..88cb2fbadda 100644 --- a/Mage/src/main/java/mage/cards/repository/CardRepository.java +++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java @@ -147,8 +147,8 @@ public enum CardRepository { if (card.getMeldsToCardName() != null && !card.getMeldsToCardName().isEmpty()) { namesList.add(card.getMeldsToCardName()); } - if (card.getAdventureSpellName() != null && !card.getAdventureSpellName().isEmpty()) { - namesList.add(card.getAdventureSpellName()); + if (card.getSpellOptionCardName() != null && !card.getSpellOptionCardName().isEmpty()) { + namesList.add(card.getSpellOptionCardName()); } } @@ -160,7 +160,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName"); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); @@ -176,7 +176,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName"); qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%')); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { @@ -193,7 +193,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName"); Where where = qb.where(); where.and( where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'), @@ -214,7 +214,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName"); qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%')); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { @@ -231,7 +231,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName"); qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%')); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { @@ -248,7 +248,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName"); qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%')); List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { @@ -265,7 +265,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName"); Where where = qb.where(); where.and( where.not().like("types", '%' + CardType.CREATURE.name() + '%'), @@ -286,7 +286,7 @@ public enum CardRepository { Set names = new TreeSet<>(); try { QueryBuilder qb = cardsDao.queryBuilder(); - qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName"); + qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName"); Where where = qb.where(); where.and( where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'), @@ -511,7 +511,7 @@ public enum CardRepository { queryBuilder.where() .eq("flipCardName", new SelectArg(name)).or() .eq("secondSideName", new SelectArg(name)).or() - .eq("adventureSpellName", new SelectArg(name)).or() + .eq("spellOptionCardName", new SelectArg(name)).or() .eq("modalDoubleFacedSecondSideName", new SelectArg(name)); results = cardsDao.query(queryBuilder.prepare()); } else { diff --git a/Mage/src/main/java/mage/constants/SpellAbilityType.java b/Mage/src/main/java/mage/constants/SpellAbilityType.java index cb95677e060..fac7a218aef 100644 --- a/Mage/src/main/java/mage/constants/SpellAbilityType.java +++ b/Mage/src/main/java/mage/constants/SpellAbilityType.java @@ -15,7 +15,8 @@ public enum SpellAbilityType { MODAL_LEFT("LeftModal SpellAbility"), MODAL_RIGHT("RightModal SpellAbility"), SPLICE("Spliced SpellAbility"), - ADVENTURE_SPELL("Adventure SpellAbility"); + ADVENTURE_SPELL("Adventure SpellAbility"), + OMEN_SPELL("Omen SpellAbility"); private final String text; diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index 0fcfafaa0c4..cbf9f006c19 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -13,6 +13,7 @@ public enum SubType { ADVENTURE("Adventure", SubTypeSet.SpellType), ARCANE("Arcane", SubTypeSet.SpellType), LESSON("Lesson", SubTypeSet.SpellType), + OMEN("Omen", SubTypeSet.SpellType), TRAP("Trap", SubTypeSet.SpellType), // Battle subtypes diff --git a/Mage/src/main/java/mage/filter/predicate/card/CardTextPredicate.java b/Mage/src/main/java/mage/filter/predicate/card/CardTextPredicate.java index 7f412b0e45f..dc15882ba86 100644 --- a/Mage/src/main/java/mage/filter/predicate/card/CardTextPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/card/CardTextPredicate.java @@ -1,9 +1,6 @@ package mage.filter.predicate.card; -import mage.cards.AdventureCard; -import mage.cards.Card; -import mage.cards.ModalDoubleFacedCard; -import mage.cards.SplitCard; +import mage.cards.*; import mage.cards.mock.MockCard; import mage.constants.SubType; import mage.constants.SuperType; @@ -55,8 +52,8 @@ public class CardTextPredicate implements Predicate { fullName = ((MockCard) input).getFullName(true); } else if (input instanceof ModalDoubleFacedCard) { fullName = input.getName() + MockCard.MODAL_DOUBLE_FACES_NAME_SEPARATOR + ((ModalDoubleFacedCard) input).getRightHalfCard().getName(); - } else if (input instanceof AdventureCard) { - fullName = input.getName() + MockCard.ADVENTURE_NAME_SEPARATOR + ((AdventureCard) input).getSpellCard().getName(); + } else if (input instanceof CardWithSpellOption) { + fullName = input.getName() + MockCard.CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + ((CardWithSpellOption) input).getSpellCard().getName(); } if (fullName.toLowerCase(Locale.ENGLISH).contains(text.toLowerCase(Locale.ENGLISH))) { @@ -107,8 +104,8 @@ public class CardTextPredicate implements Predicate { } } - if (input instanceof AdventureCard) { - for (String rule : ((AdventureCard) input).getSpellCard().getRules(game)) { + if (input instanceof CardWithSpellOption) { + for (String rule : ((CardWithSpellOption) input).getSpellCard().getRules(game)) { if (rule.toLowerCase(Locale.ENGLISH).contains(token)) { found = true; break; diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 17238d3c262..7ff0e1610a7 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -334,8 +334,8 @@ public abstract class GameImpl implements Game { Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard(); rightCard.setOwnerId(ownerId); addCardToState(rightCard); - } else if (card instanceof AdventureCard) { - Card spellCard = ((AdventureCard) card).getSpellCard(); + } else if (card instanceof CardWithSpellOption) { + Card spellCard = ((CardWithSpellOption) card).getSpellCard(); spellCard.setOwnerId(ownerId); addCardToState(spellCard); } else if (card.isTransformable() && card.getSecondCardFace() != null) { diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index e0c3d41d452..3f13573471d 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1639,14 +1639,14 @@ public class GameState implements Serializable, Copyable { copiedParts.add(rightCopied); // sync parts ((ModalDoubleFacedCard) copiedCard).setParts(leftCopied, rightCopied); - } else if (copiedCard instanceof AdventureCard) { + } else if (copiedCard instanceof CardWithSpellOption) { // right - AdventureCardSpell rightOriginal = ((AdventureCard) copiedCard).getSpellCard(); - AdventureCardSpell rightCopied = rightOriginal.copy(); + SpellOptionCard rightOriginal = ((CardWithSpellOption) copiedCard).getSpellCard(); + SpellOptionCard rightCopied = rightOriginal.copy(); prepareCardForCopy(rightOriginal, rightCopied, newController); copiedParts.add(rightCopied); // sync parts - ((AdventureCard) copiedCard).setParts(rightCopied); + ((CardWithSpellOption) copiedCard).setParts(rightCopied); } // main part prepare (must be called after other parts cause it change ids for all) diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index c234e901643..149d8a6e92b 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -209,10 +209,11 @@ public class Spell extends StackObjectImpl implements Card { + " using " + this.getSpellAbility().getSpellAbilityCastMode(); } - if (card instanceof AdventureCardSpell) { - AdventureCard adventureCard = ((AdventureCardSpell) card).getParentCard(); - return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), adventureCard) - + " as Adventure spell of " + GameLog.getColoredObjectIdName(adventureCard); + if (card instanceof SpellOptionCard) { + CardWithSpellOption parentCard = ((SpellOptionCard) card).getParentCard(); + String type = ((SpellOptionCard) card).getSpellType(); + return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), parentCard) + + " as " + type + " spell of " + GameLog.getColoredObjectIdName(parentCard); } if (card instanceof ModalDoubleFacedCardHalf) { @@ -539,8 +540,8 @@ public class Spell extends StackObjectImpl implements Card { public String getIdName() { String idName; if (card != null) { - if (card instanceof AdventureCardSpell) { - idName = ((AdventureCardSpell) card).getParentCard().getId().toString().substring(0, 3); + if (card instanceof SpellOptionCard) { + idName = ((SpellOptionCard) card).getParentCard().getId().toString().substring(0, 3); } else { idName = card.getId().toString().substring(0, 3); } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index a6f3ac1dd88..0132cc7810c 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -4068,11 +4068,11 @@ public abstract class PlayerImpl implements Player, Serializable { getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output); getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof AdventureCard) { + } else if (object instanceof CardWithSpellOption) { // adventure must use different card characteristics for different spells (main or adventure) - AdventureCard adventureCard = (AdventureCard) object; - getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output); - getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output); + CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object; + getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption.getSpellCard(), cardWithSpellOption.getSpellCard().getAbilities(game), availableMana, output); + getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption, cardWithSpellOption.getSharedAbilities(game), availableMana, output); } else if (object instanceof Card) { getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output); } else if (object instanceof StackObject) { diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index b55d2a33da6..ee24d673186 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1272,7 +1272,7 @@ public final class CardUtil { Card permCard; if (card instanceof SplitCard) { permCard = card; - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { permCard = card; } else if (card instanceof ModalDoubleFacedCard) { permCard = ((ModalDoubleFacedCard) card).getLeftHalfCard(); @@ -1460,9 +1460,9 @@ public final class CardUtil { if (cardToCast instanceof CardWithHalves) { cards.add(((CardWithHalves) cardToCast).getLeftHalfCard()); cards.add(((CardWithHalves) cardToCast).getRightHalfCard()); - } else if (cardToCast instanceof AdventureCard) { + } else if (cardToCast instanceof CardWithSpellOption) { cards.add(cardToCast); - cards.add(((AdventureCard) cardToCast).getSpellCard()); + cards.add(((CardWithSpellOption) cardToCast).getSpellCard()); } else { cards.add(cardToCast); } @@ -1651,9 +1651,9 @@ public final class CardUtil { } // handle adventure cards - if (card instanceof AdventureCard) { + if (card instanceof CardWithSpellOption) { Card creatureCard = card.getMainCard(); - Card spellCard = ((AdventureCard) card).getSpellCard(); + Card spellCard = ((CardWithSpellOption) card).getSpellCard(); if (manaCost != null) { // get additional cost if any Costs additionalCostsCreature = creatureCard.getSpellAbility().getCosts(); @@ -1691,9 +1691,9 @@ public final class CardUtil { game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null); game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null); } - if (card instanceof AdventureCard) { + if (card instanceof CardWithSpellOption) { Card creatureCard = card.getMainCard(); - Card spellCard = ((AdventureCard) card).getSpellCard(); + Card spellCard = ((CardWithSpellOption) card).getSpellCard(); game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), null); game.getState().setValue("PlayFromNotOwnHandZone" + spellCard.getId(), null); } @@ -2078,8 +2078,8 @@ public final class CardUtil { res.add(mainCard); res.add(mainCard.getLeftHalfCard()); res.add(mainCard.getRightHalfCard()); - } else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) { - AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard(); + } else if (object instanceof CardWithSpellOption || object instanceof SpellOptionCard) { + CardWithSpellOption mainCard = (CardWithSpellOption) ((Card) object).getMainCard(); res.add(mainCard); res.add(mainCard.getSpellCard()); } else if (object instanceof Spell) { diff --git a/Mage/src/main/java/mage/util/ManaUtil.java b/Mage/src/main/java/mage/util/ManaUtil.java index 08b478d6e23..816353e13e9 100644 --- a/Mage/src/main/java/mage/util/ManaUtil.java +++ b/Mage/src/main/java/mage/util/ManaUtil.java @@ -13,10 +13,7 @@ import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.Effect; import mage.abilities.mana.*; -import mage.cards.AdventureCard; -import mage.cards.Card; -import mage.cards.ModalDoubleFacedCard; -import mage.cards.SplitCard; +import mage.cards.*; import mage.choices.Choice; import mage.constants.ColoredManaSymbol; import mage.constants.ManaType; @@ -644,8 +641,8 @@ public final class ManaUtil { Card secondSide; if (card instanceof SplitCard) { secondSide = ((SplitCard) card).getRightHalfCard(); - } else if (card instanceof AdventureCard) { - secondSide = ((AdventureCard) card).getSpellCard(); + } else if (card instanceof CardWithSpellOption) { + secondSide = ((CardWithSpellOption) card).getSpellCard(); } else if (card instanceof ModalDoubleFacedCard) { secondSide = ((ModalDoubleFacedCard) card).getRightHalfCard(); } else { From 235e5200d09fa605969f0a9d8fe22e24ae4d20b9 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 8 Apr 2025 08:44:31 -0400 Subject: [PATCH 042/133] [TDM] Implement Warden of the Grove --- .../src/mage/cards/w/WardenOfTheGrove.java | 91 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + .../effects/keyword/EndureSourceEffect.java | 15 ++- 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/w/WardenOfTheGrove.java diff --git a/Mage.Sets/src/mage/cards/w/WardenOfTheGrove.java b/Mage.Sets/src/mage/cards/w/WardenOfTheGrove.java new file mode 100644 index 00000000000..e612b21bb18 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WardenOfTheGrove.java @@ -0,0 +1,91 @@ +package mage.cards.w; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAllTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.keyword.EndureSourceEffect; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.counters.Counters; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.Game; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WardenOfTheGrove extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledCreaturePermanent("another nontoken creature you control"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(TokenPredicate.FALSE); + } + + public WardenOfTheGrove(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.HYDRA); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // At the beginning of your end step, put a +1/+1 counter on this creature. + this.addAbility(new BeginningOfEndStepTriggeredAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance()))); + + // Whenever another nontoken creature you control enters, it endures X, where X is the number of counters on this creature. + this.addAbility(new EntersBattlefieldAllTriggeredAbility( + Zone.BATTLEFIELD, new WardenOfTheGroveEffect(), + filter, false, SetTargetPointer.PERMANENT + )); + } + + private WardenOfTheGrove(final WardenOfTheGrove card) { + super(card); + } + + @Override + public WardenOfTheGrove copy() { + return new WardenOfTheGrove(this); + } +} + +class WardenOfTheGroveEffect extends OneShotEffect { + + WardenOfTheGroveEffect() { + super(Outcome.Benefit); + staticText = "it endures X, where X is the number of counters on {this}"; + } + + private WardenOfTheGroveEffect(final WardenOfTheGroveEffect effect) { + super(effect); + } + + @Override + public WardenOfTheGroveEffect copy() { + return new WardenOfTheGroveEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return EndureSourceEffect.doEndure( + this.getTargetPointer().getFirstTargetPermanentOrLKI(game, source), + Optional.ofNullable(source.getSourcePermanentOrLKI(game)) + .map(p -> p.getCounters(game)) + .map(Counters::getTotalCount) + .orElse(0), + game, source + ); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 9f498363e61..a581a542f55 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -247,6 +247,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Voice of Victory", 33, Rarity.RARE, mage.cards.v.VoiceOfVictory.class)); cards.add(new SetCardInfo("War Effort", 131, Rarity.UNCOMMON, mage.cards.w.WarEffort.class)); cards.add(new SetCardInfo("Wail of War", 98, Rarity.UNCOMMON, mage.cards.w.WailOfWar.class)); + cards.add(new SetCardInfo("Warden of the Grove", 166, Rarity.RARE, mage.cards.w.WardenOfTheGrove.class)); cards.add(new SetCardInfo("Watcher of the Wayside", 249, Rarity.COMMON, mage.cards.w.WatcherOfTheWayside.class)); cards.add(new SetCardInfo("Wayspeaker Bodyguard", 34, Rarity.UNCOMMON, mage.cards.w.WayspeakerBodyguard.class)); cards.add(new SetCardInfo("Wild Ride", 132, Rarity.COMMON, mage.cards.w.WildRide.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java index d6a8b4e9fea..9be3c36936d 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java @@ -39,12 +39,19 @@ public class EndureSourceEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - if (player == null) { + return doEndure(source.getSourcePermanentOrLKI(game), 1, game, source); + } + + public static boolean doEndure(Permanent permanent, int amount, Game game, Ability source) { + if (permanent == null || amount < 1) { return false; } - Permanent permanent = source.getSourcePermanentIfItStillExists(game); - if (permanent != null && player.chooseUse( + Player controller = game.getPlayer(permanent.getControllerId()); + if (controller == null) { + return false; + } + if (permanent.getZoneChangeCounter(game) == game.getState().getZoneChangeCounter(permanent.getId()) + && controller.chooseUse( Outcome.BoostCreature, "Put " + CardUtil.numberToText(amount, "a") + " +1/+1 counter" + (amount > 1 ? "s" : "") + " on " + permanent.getName() + " or create " + CardUtil.addArticle("" + amount) + ' ' + amount + '/' + amount + " Spirit token?", From 2e6e3cd4e7016397b93f7d5096e481b5148fd4ef Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 8 Apr 2025 09:16:50 -0400 Subject: [PATCH 043/133] [TDM] Implement Mardu Siegebreaker --- .../src/mage/cards/m/MarduSiegebreaker.java | 136 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + .../target/targetpointer/FixedTargets.java | 2 +- 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/m/MarduSiegebreaker.java diff --git a/Mage.Sets/src/mage/cards/m/MarduSiegebreaker.java b/Mage.Sets/src/mage/cards/m/MarduSiegebreaker.java new file mode 100644 index 00000000000..ead29290170 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MarduSiegebreaker.java @@ -0,0 +1,136 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileUntilSourceLeavesEffect; +import mage.abilities.effects.common.SacrificeTargetEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.abilities.keyword.HasteAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.GameState; +import mage.game.permanent.token.Token; +import mage.target.TargetCard; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInExile; +import mage.target.targetpointer.FixedTargets; +import mage.util.CardUtil; +import mage.util.functions.CopyTokenFunction; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MarduSiegebreaker extends CardImpl { + + public MarduSiegebreaker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}{W}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // When this creature enters, exile up to one other target creature you control until this creature leaves the battlefield. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect()); + ability.addTarget(new TargetPermanent(0, 1, StaticFilters.FILTER_OTHER_CONTROLLED_CREATURE)); + this.addAbility(ability); + + // Whenever this creature attacks, for each opponent, create a tapped token that's a copy of the exiled card attacking that opponent. At the beginning of your end step, sacrifice those tokens. + this.addAbility(new AttacksTriggeredAbility(new MarduSiegebreakerEffect())); + } + + private MarduSiegebreaker(final MarduSiegebreaker card) { + super(card); + } + + @Override + public MarduSiegebreaker copy() { + return new MarduSiegebreaker(this); + } +} + +class MarduSiegebreakerEffect extends OneShotEffect { + + MarduSiegebreakerEffect() { + super(Outcome.Benefit); + staticText = "for each opponent, create a tapped token that's a copy of the exiled card " + + "attacking that opponent. At the beginning of your end step, sacrifice those tokens"; + } + + private MarduSiegebreakerEffect(final MarduSiegebreakerEffect effect) { + super(effect); + } + + @Override + public MarduSiegebreakerEffect copy() { + return new MarduSiegebreakerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Cards cards = Optional + .ofNullable(game) + .map(Game::getState) + .map(GameState::getExile) + .map(exile -> exile.getExileZone(CardUtil.getExileZoneId(game, source))) + .orElse(null); + if (cards == null) { + return false; + } + Card card; + switch (cards.size()) { + case 0: + return false; + case 1: + card = cards.getRandom(game); + break; + default: + card = Optional.ofNullable(game.getPlayer(source.getControllerId())).map(player -> { + TargetCard target = new TargetCardInExile(StaticFilters.FILTER_CARD); + target.withNotTarget(true); + target.withChooseHint("to copy"); + player.choose(Outcome.Neutral, cards, target, source, game); + return game.getCard(target.getFirstTarget()); + }).orElse(null); + } + if (card == null) { + return false; + } + Set addedTokens = new HashSet<>(); + Token token = CopyTokenFunction.createTokenCopy(card, game); + for (UUID opponentId : game.getOpponents(source.getControllerId())) { + token.putOntoBattlefield(1, game, source, source.getControllerId(), true, true, opponentId); + token.getLastAddedTokenIds() + .stream() + .map(uuid -> new MageObjectReference(uuid, game)) + .forEach(addedTokens::add); + } + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility( + new SacrificeTargetEffect().setTargetPointer(new FixedTargets(addedTokens)) + ), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index a581a542f55..1354ac5ef04 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -143,6 +143,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Mammoth Bellow", 205, Rarity.UNCOMMON, mage.cards.m.MammothBellow.class)); cards.add(new SetCardInfo("Mardu Devotee", 16, Rarity.COMMON, mage.cards.m.MarduDevotee.class)); cards.add(new SetCardInfo("Mardu Monument", 245, Rarity.UNCOMMON, mage.cards.m.MarduMonument.class)); + cards.add(new SetCardInfo("Mardu Siegebreaker", 206, Rarity.RARE, mage.cards.m.MarduSiegebreaker.class)); cards.add(new SetCardInfo("Marshal of the Lost", 207, Rarity.UNCOMMON, mage.cards.m.MarshalOfTheLost.class)); cards.add(new SetCardInfo("Meticulous Artisan", 112, Rarity.COMMON, mage.cards.m.MeticulousArtisan.class)); cards.add(new SetCardInfo("Molten Exhale", 113, Rarity.COMMON, mage.cards.m.MoltenExhale.class)); diff --git a/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java b/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java index af3d80dbb31..1eb632fd1f6 100644 --- a/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java +++ b/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java @@ -50,7 +50,7 @@ public class FixedTargets extends TargetPointerImpl { .collect(Collectors.toList()), game); } - public FixedTargets(List morList) { + public FixedTargets(Collection morList) { super(); targets.addAll(morList); this.setInitialized(); // no need dynamic init From 44389cbbbfa27aa06a99006f58ff4628321043b9 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 11:02:17 -0500 Subject: [PATCH 044/133] [TDM] Implement Bloomvine Regent --- .../src/mage/cards/b/BloomvineRegent.java | 68 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 69 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BloomvineRegent.java diff --git a/Mage.Sets/src/mage/cards/b/BloomvineRegent.java b/Mage.Sets/src/mage/cards/b/BloomvineRegent.java new file mode 100644 index 00000000000..88f7e8cb72d --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BloomvineRegent.java @@ -0,0 +1,68 @@ +package mage.cards.b; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldThisOrAnotherTriggeredAbility; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; +import mage.cards.OmenCard; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SuperType; +import mage.filter.FilterCard; +import mage.filter.common.FilterBasicLandCard; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.target.Target; +import mage.target.common.TargetCardInLibrary; + +/** + * + * @author Jmlundeen + */ +public final class BloomvineRegent extends OmenCard { + + private static final FilterCard filter = new FilterCard("basic Forest cards"); + + static { + filter.add(SubType.FOREST.getPredicate()); + filter.add(SuperType.BASIC.getPredicate()); + } + + public BloomvineRegent(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{3}{G}{G}", "Claim Territory", "{2}{G}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever this creature or another Dragon you control enters, you gain 3 life. + this.addAbility(new EntersBattlefieldThisOrAnotherTriggeredAbility( + new GainLifeEffect(3), + new FilterCreaturePermanent(SubType.DRAGON, "Dragon"), + false, + true + )); + + // Claim Territory + // Search your library for up to two basic Forest cards, reveal them, put one onto the battlefield tapped and the other into your hand, then shuffle. (Also shuffle this card.) + TargetCardInLibrary target = new TargetCardInLibrary(0, 2, filter); + this.getSpellCard().getSpellAbility().addEffect(new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(target, 1)); + this.finalizeOmen(); + } + + private BloomvineRegent(final BloomvineRegent card) { + super(card); + } + + @Override + public BloomvineRegent copy() { + return new BloomvineRegent(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 1354ac5ef04..02decfc2e59 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -45,6 +45,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Bearer of Glory", 4, Rarity.COMMON, mage.cards.b.BearerOfGlory.class)); cards.add(new SetCardInfo("Bewildering Blizzard", 38, Rarity.UNCOMMON, mage.cards.b.BewilderingBlizzard.class)); cards.add(new SetCardInfo("Bloodfell Caves", 250, Rarity.COMMON, mage.cards.b.BloodfellCaves.class)); + cards.add(new SetCardInfo("Bloomvine Regent", 136, Rarity.RARE, mage.cards.b.BloomvineRegent.class)); cards.add(new SetCardInfo("Blossoming Sands", 251, Rarity.COMMON, mage.cards.b.BlossomingSands.class)); cards.add(new SetCardInfo("Bone-Cairn Butcher", 173, Rarity.UNCOMMON, mage.cards.b.BoneCairnButcher.class)); cards.add(new SetCardInfo("Boulderborn Dragon", 239, Rarity.COMMON, mage.cards.b.BoulderbornDragon.class)); From 076df16841e10e6e0921c8e3e2424ee4d41b3ac3 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 11:02:44 -0500 Subject: [PATCH 045/133] [TDM] Implement Disruptive Stormbrood --- .../mage/cards/d/DisruptiveStormbrood.java | 64 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 65 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DisruptiveStormbrood.java diff --git a/Mage.Sets/src/mage/cards/d/DisruptiveStormbrood.java b/Mage.Sets/src/mage/cards/d/DisruptiveStormbrood.java new file mode 100644 index 00000000000..382d66191e4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DisruptiveStormbrood.java @@ -0,0 +1,64 @@ +package mage.cards.d; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.cards.OmenCard; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class DisruptiveStormbrood extends OmenCard { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with power 3 or less"); + + static { + filter.add(new PowerPredicate(ComparisonType.OR_LESS, 3)); + } + + public DisruptiveStormbrood(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{4}{G}", "Petty Revenge", "{1}{B}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When this creature enters, destroy up to one target artifact or enchantment. + Ability ability = new EntersBattlefieldTriggeredAbility(new DestroyTargetEffect()); + ability.addTarget(new TargetPermanent(0, 1, StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT)); + this.addAbility(ability); + + // Petty Revenge + // Destroy target creature with power 3 or less. + Effect spellEffect = new DestroyTargetEffect(); + this.getSpellCard().getSpellAbility().addEffect(spellEffect); + this.getSpellCard().getSpellAbility().addTarget(new TargetCreaturePermanent(filter)); + this.finalizeOmen(); + } + + private DisruptiveStormbrood(final DisruptiveStormbrood card) { + super(card); + } + + @Override + public DisruptiveStormbrood copy() { + return new DisruptiveStormbrood(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 02decfc2e59..5c48ac76d54 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -71,6 +71,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Dirgur Island Dragon", 40, Rarity.COMMON, mage.cards.d.DirgurIslandDragon.class)); cards.add(new SetCardInfo("Dismal Backwater", 254, Rarity.COMMON, mage.cards.d.DismalBackwater.class)); cards.add(new SetCardInfo("Dispelling Exhale", 41, Rarity.COMMON, mage.cards.d.DispellingExhale.class)); + cards.add(new SetCardInfo("Disruptive Stormbrood", 178, Rarity.UNCOMMON, mage.cards.d.DisruptiveStormbrood.class)); cards.add(new SetCardInfo("Dracogenesis", 105, Rarity.MYTHIC, mage.cards.d.Dracogenesis.class)); cards.add(new SetCardInfo("Dragon Sniper", 139, Rarity.UNCOMMON, mage.cards.d.DragonSniper.class)); cards.add(new SetCardInfo("Dragon's Prey", 79, Rarity.COMMON, mage.cards.d.DragonsPrey.class)); From 2c13c30f5dcccb4b9fb81c537c3c39f0a8adf808 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 11:03:04 -0500 Subject: [PATCH 046/133] [TDM] Implement Feral Deathgorger --- .../src/mage/cards/f/FeralDeathgorger.java | 64 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 65 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FeralDeathgorger.java diff --git a/Mage.Sets/src/mage/cards/f/FeralDeathgorger.java b/Mage.Sets/src/mage/cards/f/FeralDeathgorger.java new file mode 100644 index 00000000000..9c44eaf81bc --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FeralDeathgorger.java @@ -0,0 +1,64 @@ +package mage.cards.f; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.OmenCard; +import mage.cards.o.Omen; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.target.common.TargetCardInASingleGraveyard; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class FeralDeathgorger extends OmenCard { + + public FeralDeathgorger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{5}{B}", "Dusk Sight", "{1}{B}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(3); + this.toughness = new MageInt(5); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // When this creature enters, exile up to two target cards from a single graveyard. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileTargetEffect()); + ability.addTarget(new TargetCardInASingleGraveyard(0, 2, new FilterCard("cards"))); + this.addAbility(ability); + + // Dusk Sight + // Put a +1/+1 counter on up to one target creature. Draw a card. + this.getSpellCard().getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance(), StaticValue.get(1))); + this.getSpellCard().getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); + this.getSpellCard().getSpellAbility().addTarget(new TargetCreaturePermanent(0, 1)); + this.finalizeOmen(); + } + + private FeralDeathgorger(final FeralDeathgorger card) { + super(card); + } + + @Override + public FeralDeathgorger copy() { + return new FeralDeathgorger(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 5c48ac76d54..8ed5fed4cb3 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -94,6 +94,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Evolving Wilds", 255, Rarity.COMMON, mage.cards.e.EvolvingWilds.class)); cards.add(new SetCardInfo("Fangkeeper's Familiar", 183, Rarity.RARE, mage.cards.f.FangkeepersFamiliar.class)); cards.add(new SetCardInfo("Felothar, Dawn of the Abzan", 184, Rarity.RARE, mage.cards.f.FelotharDawnOfTheAbzan.class)); + cards.add(new SetCardInfo("Feral Deathgorger", 80, Rarity.COMMON, mage.cards.f.FeralDeathgorger.class)); cards.add(new SetCardInfo("Fire-Rim Form", 107, Rarity.COMMON, mage.cards.f.FireRimForm.class)); cards.add(new SetCardInfo("Flamehold Grappler", 185, Rarity.RARE, mage.cards.f.FlameholdGrappler.class)); cards.add(new SetCardInfo("Fleeting Effigy", 108, Rarity.UNCOMMON, mage.cards.f.FleetingEffigy.class)); From aeffd36bcd040af559e49a82743337958498862f Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 11:33:02 -0500 Subject: [PATCH 047/133] [TDM] Implement Marang River Regent --- .../src/mage/cards/m/MarangRiverRegent.java | 53 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 54 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MarangRiverRegent.java diff --git a/Mage.Sets/src/mage/cards/m/MarangRiverRegent.java b/Mage.Sets/src/mage/cards/m/MarangRiverRegent.java new file mode 100644 index 00000000000..967ce00e83d --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MarangRiverRegent.java @@ -0,0 +1,53 @@ +package mage.cards.m; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.cards.OmenCard; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.TargetPermanent; +import mage.target.common.TargetNonlandPermanent; + +/** + * + * @author Jmlundeen + */ +public final class MarangRiverRegent extends OmenCard { + + public MarangRiverRegent(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.INSTANT}, "{4}{U}{U}", "Coil and Catch", "{3}{U}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(6); + this.toughness = new MageInt(7); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When this creature enters, return up to two other target nonland permanents to their owners' hands. + Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnToHandTargetEffect()); + ability.addTarget(new TargetNonlandPermanent(0, 2)); + this.addAbility(ability); + + // Coil and Catch + // Draw three cards, then discard a card. + this.getSpellCard().getSpellAbility().addEffect(new DrawDiscardControllerEffect(3, 1)); + this.finalizeOmen(); + } + + private MarangRiverRegent(final MarangRiverRegent card) { + super(card); + } + + @Override + public MarangRiverRegent copy() { + return new MarangRiverRegent(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 8ed5fed4cb3..1a18622449e 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -144,6 +144,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Lotuslight Dancers", 363, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Loxodon Battle Priest", 15, Rarity.UNCOMMON, mage.cards.l.LoxodonBattlePriest.class)); cards.add(new SetCardInfo("Mammoth Bellow", 205, Rarity.UNCOMMON, mage.cards.m.MammothBellow.class)); + cards.add(new SetCardInfo("Marang River Regent", 51, Rarity.RARE, mage.cards.m.MarangRiverRegent.class)); cards.add(new SetCardInfo("Mardu Devotee", 16, Rarity.COMMON, mage.cards.m.MarduDevotee.class)); cards.add(new SetCardInfo("Mardu Monument", 245, Rarity.UNCOMMON, mage.cards.m.MarduMonument.class)); cards.add(new SetCardInfo("Mardu Siegebreaker", 206, Rarity.RARE, mage.cards.m.MarduSiegebreaker.class)); From b9fc258777013642f2f303931e02642220b01a03 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 11:53:07 -0500 Subject: [PATCH 048/133] [TDM] Implement Purging Stormbrood --- .../src/mage/cards/p/PurgingStormbrood.java | 70 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 71 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/p/PurgingStormbrood.java diff --git a/Mage.Sets/src/mage/cards/p/PurgingStormbrood.java b/Mage.Sets/src/mage/cards/p/PurgingStormbrood.java new file mode 100644 index 00000000000..e1566facf10 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PurgingStormbrood.java @@ -0,0 +1,70 @@ +package mage.cards.p; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.counter.RemoveAllCountersPermanentTargetEffect; +import mage.abilities.keyword.HexproofAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.OmenCard; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class PurgingStormbrood extends OmenCard { + + public PurgingStormbrood(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.INSTANT}, "{4}{B}", "Absorb Essence", "{1}{W}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Ward--Pay 2 life. + this.addAbility(new WardAbility(new PayLifeCost(2))); + + // When this creature enters, remove all counters from up to one target creature. + Ability ability = new EntersBattlefieldTriggeredAbility(new RemoveAllCountersPermanentTargetEffect()); + ability.addTarget(new TargetCreaturePermanent(0, 1)); + this.addAbility(ability); + + // Absorb Essence + // Target creature gets +2/+2 and gains lifelink and hexproof until end of turn. + this.getSpellCard().getSpellAbility().addEffect(new BoostTargetEffect(2, 2) + .setText("Target creature gets +2/+2")); + this.getSpellCard().getSpellAbility().addEffect(new GainAbilityTargetEffect(LifelinkAbility.getInstance()) + .setText("gains lifelink") + .concatBy("and")); + this.getSpellCard().getSpellAbility().addEffect(new GainAbilityTargetEffect(HexproofAbility.getInstance()) + .setText("hexproof until end of turn") + .concatBy("and")); + this.getSpellCard().getSpellAbility().addTarget(new TargetCreaturePermanent()); + this.finalizeOmen(); + } + + private PurgingStormbrood(final PurgingStormbrood card) { + super(card); + } + + @Override + public PurgingStormbrood copy() { + return new PurgingStormbrood(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 1a18622449e..1f2ff4581a6 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -168,6 +168,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Piercing Exhale", 151, Rarity.COMMON, mage.cards.p.PiercingExhale.class)); cards.add(new SetCardInfo("Plains", 277, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Poised Practitioner", 18, Rarity.COMMON, mage.cards.p.PoisedPractitioner.class)); + cards.add(new SetCardInfo("Purging Stormbrood", 213, Rarity.UNCOMMON, mage.cards.p.PurgingStormbrood.class)); cards.add(new SetCardInfo("Qarsi Revenant", 86, Rarity.RARE, mage.cards.q.QarsiRevenant.class)); cards.add(new SetCardInfo("Rainveil Rejuvenator", 152, Rarity.UNCOMMON, mage.cards.r.RainveilRejuvenator.class)); cards.add(new SetCardInfo("Rakshasa's Bargain", 214, Rarity.UNCOMMON, mage.cards.r.RakshasasBargain.class)); From 3554fcd54031d0619b09dffa10caf4117f912c29 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 13:00:40 -0500 Subject: [PATCH 049/133] [TDM] Implement Riling Dawnbreaker --- .../src/mage/cards/r/RilingDawnbreaker.java | 66 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 67 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RilingDawnbreaker.java diff --git a/Mage.Sets/src/mage/cards/r/RilingDawnbreaker.java b/Mage.Sets/src/mage/cards/r/RilingDawnbreaker.java new file mode 100644 index 00000000000..21921919b2b --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RilingDawnbreaker.java @@ -0,0 +1,66 @@ +package mage.cards.r; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.cards.OmenCard; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.permanent.token.Soldier22Token; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class RilingDawnbreaker extends OmenCard { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("another target creature you control"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(TargetController.YOU.getControllerPredicate()); + } + + public RilingDawnbreaker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{4}{W}", "Signaling Roar", "{1}{W}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // At the beginning of combat on your turn, another target creature you control gets +1/+0 until end of turn. + Ability ability = new BeginningOfCombatTriggeredAbility(new BoostTargetEffect(1, 0)); + ability.addTarget(new TargetCreaturePermanent(filter)); + this.addAbility(ability); + + // Signaling Roar + // Create a 2/2 white Soldier creature token. + this.getSpellCard().getSpellAbility().addEffect(new CreateTokenEffect(new Soldier22Token())); + this.finalizeOmen(); + } + + private RilingDawnbreaker(final RilingDawnbreaker card) { + super(card); + } + + @Override + public RilingDawnbreaker copy() { + return new RilingDawnbreaker(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 1f2ff4581a6..0d4e8d1f01d 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -179,6 +179,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Reputable Merchant", 217, Rarity.COMMON, mage.cards.r.ReputableMerchant.class)); cards.add(new SetCardInfo("Rescue Leopard", 116, Rarity.COMMON, mage.cards.r.RescueLeopard.class)); cards.add(new SetCardInfo("Revival of the Ancestors", 218, Rarity.RARE, mage.cards.r.RevivalOfTheAncestors.class)); + cards.add(new SetCardInfo("Riling Dawnbreaker", 21, Rarity.COMMON, mage.cards.r.RilingDawnbreaker.class)); cards.add(new SetCardInfo("Ringing Strike Mastery", 53, Rarity.COMMON, mage.cards.r.RingingStrikeMastery.class)); cards.add(new SetCardInfo("Riverwalk Technique", 54, Rarity.COMMON, mage.cards.r.RiverwalkTechnique.class)); cards.add(new SetCardInfo("Riverwheel Sweep", 219, Rarity.UNCOMMON, mage.cards.r.RiverwheelSweep.class)); From ca04897028835570ad9d820723330b5a6ab58b48 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 13:22:37 -0500 Subject: [PATCH 050/133] [TDM] Implement Runescale Stormbrood --- .../src/mage/cards/r/RunescaleStormbrood.java | 65 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 66 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RunescaleStormbrood.java diff --git a/Mage.Sets/src/mage/cards/r/RunescaleStormbrood.java b/Mage.Sets/src/mage/cards/r/RunescaleStormbrood.java new file mode 100644 index 00000000000..bb106ea73e4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RunescaleStormbrood.java @@ -0,0 +1,65 @@ +package mage.cards.r; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.CounterTargetEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.cards.OmenCard; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.FilterSpell; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.target.TargetSpell; + +/** + * + * @author Jmlundeen + */ +public final class RunescaleStormbrood extends OmenCard { + + private static final FilterSpell filter = new FilterSpell("spell with mana value 2 or less"); + private static final FilterSpell castFilter = new FilterSpell("noncreature spell or Dragon spell"); + + static { + filter.add(new ManaValuePredicate(ComparisonType.OR_LESS, 2)); + castFilter.add(Predicates.or( + Predicates.not(CardType.CREATURE.getPredicate()), + SubType.DRAGON.getPredicate() + )); + } + + public RunescaleStormbrood(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.INSTANT}, "{3}{R}", "Chilling Screech", "{1}{U}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever you cast a noncreature spell or a Dragon spell, this creature gets +2/+0 until end of turn. + this.addAbility(new SpellCastControllerTriggeredAbility(new BoostSourceEffect(2, 0, Duration.EndOfTurn), castFilter, false)); + + // Chilling Screech + // Counter target spell with mana value 2 or less. + this.getSpellCard().getSpellAbility().addEffect(new CounterTargetEffect()); + this.getSpellCard().getSpellAbility().addTarget(new TargetSpell(filter)); + this.finalizeOmen(); + } + + private RunescaleStormbrood(final RunescaleStormbrood card) { + super(card); + } + + @Override + public RunescaleStormbrood copy() { + return new RunescaleStormbrood(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 0d4e8d1f01d..3d5fddfaff9 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -187,6 +187,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Roar of Endless Song", 220, Rarity.RARE, mage.cards.r.RoarOfEndlessSong.class)); cards.add(new SetCardInfo("Roiling Dragonstorm", 55, Rarity.UNCOMMON, mage.cards.r.RoilingDragonstorm.class)); cards.add(new SetCardInfo("Rugged Highlands", 265, Rarity.COMMON, mage.cards.r.RuggedHighlands.class)); + cards.add(new SetCardInfo("Runescale Stormbrood", 221, Rarity.UNCOMMON, mage.cards.r.RunescaleStormbrood.class)); cards.add(new SetCardInfo("Sage of the Fang", 155, Rarity.UNCOMMON, mage.cards.s.SageOfTheFang.class)); cards.add(new SetCardInfo("Sagu Pummeler", 156, Rarity.COMMON, mage.cards.s.SaguPummeler.class)); cards.add(new SetCardInfo("Salt Road Packbeast", 23, Rarity.COMMON, mage.cards.s.SaltRoadPackbeast.class)); From c9aa6c3152e923a736fd60b61db36e8c2725f0ba Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 13:31:14 -0500 Subject: [PATCH 051/133] [TDM] Implement Sagu Wildling --- Mage.Sets/src/mage/cards/s/SaguWildling.java | 50 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 51 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SaguWildling.java diff --git a/Mage.Sets/src/mage/cards/s/SaguWildling.java b/Mage.Sets/src/mage/cards/s/SaguWildling.java new file mode 100644 index 00000000000..8d08c6822c1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SaguWildling.java @@ -0,0 +1,50 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; +import mage.cards.OmenCard; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterBasicLandCard; +import mage.target.common.TargetCardInLibrary; + +/** + * + * @author Jmlundeen + */ +public final class SaguWildling extends OmenCard { + + public SaguWildling(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{4}{G}", "Roost Seek", "{G}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When this creature enters, you gain 3 life. + this.addAbility(new EntersBattlefieldTriggeredAbility(new GainLifeEffect(3))); + + // Roost Seek + // Search your library for a basic land card, reveal it, put it into your hand, then shuffle. + TargetCardInLibrary target = new TargetCardInLibrary(new FilterBasicLandCard()); + this.getSpellCard().getSpellAbility().addEffect(new SearchLibraryPutInHandEffect(target, true)); + this.finalizeOmen(); + } + + private SaguWildling(final SaguWildling card) { + super(card); + } + + @Override + public SaguWildling copy() { + return new SaguWildling(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 3d5fddfaff9..50176a483c0 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -190,6 +190,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Runescale Stormbrood", 221, Rarity.UNCOMMON, mage.cards.r.RunescaleStormbrood.class)); cards.add(new SetCardInfo("Sage of the Fang", 155, Rarity.UNCOMMON, mage.cards.s.SageOfTheFang.class)); cards.add(new SetCardInfo("Sagu Pummeler", 156, Rarity.COMMON, mage.cards.s.SaguPummeler.class)); + cards.add(new SetCardInfo("Sagu Wildling", 157, Rarity.COMMON, mage.cards.s.SaguWildling.class)); cards.add(new SetCardInfo("Salt Road Packbeast", 23, Rarity.COMMON, mage.cards.s.SaltRoadPackbeast.class)); cards.add(new SetCardInfo("Salt Road Skirmish", 88, Rarity.UNCOMMON, mage.cards.s.SaltRoadSkirmish.class)); cards.add(new SetCardInfo("Sandskitter Outrider", 89, Rarity.COMMON, mage.cards.s.SandskitterOutrider.class)); From f1a05db32c841f6521661f75ad64d0acbd6cd4d2 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 13:46:11 -0500 Subject: [PATCH 052/133] [TDM] Implement Scavenger Regent --- .../src/mage/cards/s/ScavengerRegent.java | 60 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 61 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/ScavengerRegent.java diff --git a/Mage.Sets/src/mage/cards/s/ScavengerRegent.java b/Mage.Sets/src/mage/cards/s/ScavengerRegent.java new file mode 100644 index 00000000000..3c4e6cb2996 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ScavengerRegent.java @@ -0,0 +1,60 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.costs.common.DiscardCardCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; +import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.cards.OmenCard; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; + +/** + * + * @author Jmlundeen + */ +public final class ScavengerRegent extends OmenCard { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("each non-Dragon creature"); + private static final DynamicValue xValue = new SignInversionDynamicValue(GetXValue.instance); + + static { + filter.add(Predicates.not(SubType.DRAGON.getPredicate())); + } + + public ScavengerRegent(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{3}{B}", "Exude Toxin", "{X}{B}{B}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Ward--Discard a card. + this.addAbility(new WardAbility(new DiscardCardCost())); + + // Exude Toxin + // Each non-Dragon creature gets -X/-X until end of turn. + this.getSpellCard().getSpellAbility().addEffect(new BoostAllEffect(xValue, xValue, Duration.EndOfTurn, filter, false)); + this.finalizeOmen(); + } + + private ScavengerRegent(final ScavengerRegent card) { + super(card); + } + + @Override + public ScavengerRegent copy() { + return new ScavengerRegent(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 50176a483c0..e1ec48d4f69 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -197,6 +197,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Sandsteppe Citadel", 266, Rarity.UNCOMMON, mage.cards.s.SandsteppeCitadel.class)); cards.add(new SetCardInfo("Sarkhan's Resolve", 158, Rarity.COMMON, mage.cards.s.SarkhansResolve.class)); cards.add(new SetCardInfo("Sarkhan, Dragon Ascendant", 118, Rarity.RARE, mage.cards.s.SarkhanDragonAscendant.class)); + cards.add(new SetCardInfo("Scavenger Regent", 90, Rarity.RARE, mage.cards.s.ScavengerRegent.class)); cards.add(new SetCardInfo("Scoured Barrens", 267, Rarity.COMMON, mage.cards.s.ScouredBarrens.class)); cards.add(new SetCardInfo("Seize Opportunity", 119, Rarity.COMMON, mage.cards.s.SeizeOpportunity.class)); cards.add(new SetCardInfo("Shiko, Paragon of the Way", 223, Rarity.MYTHIC, mage.cards.s.ShikoParagonOfTheWay.class)); From 797bc89c4bf784441f09cff6ee759782c55344bd Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 14:41:12 -0500 Subject: [PATCH 053/133] [TDM] Implement StormShriek Feral --- .../src/mage/cards/s/StormshriekFeral.java | 58 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 59 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StormshriekFeral.java diff --git a/Mage.Sets/src/mage/cards/s/StormshriekFeral.java b/Mage.Sets/src/mage/cards/s/StormshriekFeral.java new file mode 100644 index 00000000000..c6957a0f554 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StormshriekFeral.java @@ -0,0 +1,58 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.DiscardCardCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.cards.OmenCard; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardSetInfo; +import mage.constants.CardType; + +/** + * + * @author Jmlundeen + */ +public final class StormshriekFeral extends OmenCard { + + public StormshriekFeral(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{4}{R}", "Flush Out", "{1}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // {1}{R}: This creature gets +1/+0 until end of turn. + this.addAbility(new SimpleActivatedAbility(new BoostSourceEffect(1, 0 , Duration.EndOfTurn), new ManaCostsImpl<>("{1}{R}"))); + + // Flush Out + // Discard a card. If you do, draw two cards. + this.getSpellCard().getSpellAbility().addEffect(new DoIfCostPaid( + new DrawCardSourceControllerEffect(2), + null, new DiscardCardCost(), false + )); + this.finalizeOmen(); + } + + private StormshriekFeral(final StormshriekFeral card) { + super(card); + } + + @Override + public StormshriekFeral copy() { + return new StormshriekFeral(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index e1ec48d4f69..f537d79046d 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -220,6 +220,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Stormbeacon Blade", 27, Rarity.UNCOMMON, mage.cards.s.StormbeaconBlade.class)); cards.add(new SetCardInfo("Stormplain Detainment", 28, Rarity.COMMON, mage.cards.s.StormplainDetainment.class)); cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); + cards.add(new SetCardInfo("Stormshriek Feral", 124, Rarity.COMMON, mage.cards.s.StormshriekFeral.class)); cards.add(new SetCardInfo("Strategic Betrayal", 94, Rarity.UNCOMMON, mage.cards.s.StrategicBetrayal.class)); cards.add(new SetCardInfo("Sultai Devotee", 160, Rarity.COMMON, mage.cards.s.SultaiDevotee.class)); cards.add(new SetCardInfo("Sultai Monument", 247, Rarity.UNCOMMON, mage.cards.s.SultaiMonument.class)); From b205ac8472e928cbdc8e95a8b8f62bbef47cf0b9 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 14:53:24 -0500 Subject: [PATCH 054/133] [TDM] Implement Twinmaw Stormbrood --- .../src/mage/cards/t/TwinmawStormbrood.java | 59 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 60 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TwinmawStormbrood.java diff --git a/Mage.Sets/src/mage/cards/t/TwinmawStormbrood.java b/Mage.Sets/src/mage/cards/t/TwinmawStormbrood.java new file mode 100644 index 00000000000..41c021b2836 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TwinmawStormbrood.java @@ -0,0 +1,59 @@ +package mage.cards.t; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.DealsDamageToACreatureTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.cards.OmenCard; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class TwinmawStormbrood extends OmenCard { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature without flying"); + + static { + filter.add(Predicates.not(new AbilityPredicate(FlyingAbility.class))); + } + + public TwinmawStormbrood(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{5}{W}", "Charring Bite", "{1}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When this creature enters, you gain 5 life. + this.addAbility(new EntersBattlefieldTriggeredAbility(new GainLifeEffect(5))); + + // Charring Bite + // deals 5 damage to target creature without flying. + this.getSpellCard().getSpellAbility().addEffect(new DamageTargetEffect(5)); + this.getSpellCard().getSpellAbility().addTarget(new TargetCreaturePermanent(filter)); + this.finalizeOmen(); + } + + private TwinmawStormbrood(final TwinmawStormbrood card) { + super(card); + } + + @Override + public TwinmawStormbrood copy() { + return new TwinmawStormbrood(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index f537d79046d..c7ce0ec5c61 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -243,6 +243,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Tranquil Cove", 270, Rarity.COMMON, mage.cards.t.TranquilCove.class)); cards.add(new SetCardInfo("Traveling Botanist", 164, Rarity.UNCOMMON, mage.cards.t.TravelingBotanist.class)); cards.add(new SetCardInfo("Twin Bolt", 128, Rarity.COMMON, mage.cards.t.TwinBolt.class)); + cards.add(new SetCardInfo("Twinmaw Stormbrood", 232, Rarity.UNCOMMON, mage.cards.t.TwinmawStormbrood.class)); cards.add(new SetCardInfo("Ugin, Eye of the Storms", 1, Rarity.MYTHIC, mage.cards.u.UginEyeOfTheStorms.class)); cards.add(new SetCardInfo("Unburied Earthcarver", 95, Rarity.COMMON, mage.cards.u.UnburiedEarthcarver.class)); cards.add(new SetCardInfo("Underfoot Underdogs", 129, Rarity.COMMON, mage.cards.u.UnderfootUnderdogs.class)); From 7b94324871830c92143e30c24241e0a68d6f6c45 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Mon, 7 Apr 2025 15:09:47 -0500 Subject: [PATCH 055/133] [TDM] Implement Whirlwing Stormbrood --- .../src/mage/cards/w/WhirlwingStormbrood.java | 72 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 73 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WhirlwingStormbrood.java diff --git a/Mage.Sets/src/mage/cards/w/WhirlwingStormbrood.java b/Mage.Sets/src/mage/cards/w/WhirlwingStormbrood.java new file mode 100644 index 00000000000..aa95f5123da --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WhirlwingStormbrood.java @@ -0,0 +1,72 @@ +package mage.cards.w; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.common.continuous.CastAsThoughItHadFlashAllEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.OmenCard; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.FilterSpell; +import mage.filter.StaticFilters; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.common.TargetCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class WhirlwingStormbrood extends OmenCard { + + private static final FilterCard filter = new FilterCard("sorcery spells and Dragon spells"); + + static { + filter.add(Predicates.or( + CardType.SORCERY.getPredicate(), + SubType.DRAGON.getPredicate() + )); + } + + public WhirlwingStormbrood(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, new CardType[]{CardType.SORCERY}, "{4}{U}", "Dynamic Soar", "{2}{G}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // You may cast sorcery spells and Dragon spells as though they had flash. + this.addAbility(new SimpleStaticAbility(new CastAsThoughItHadFlashAllEffect(Duration.WhileOnBattlefield, filter))); + + // Dynamic Soar + // Put three +1/+1 counters on target creature you control. + this.getSpellCard().getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance(), StaticValue.get(3))); + this.getSpellCard().getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); + this.finalizeOmen(); + } + + private WhirlwingStormbrood(final WhirlwingStormbrood card) { + super(card); + } + + @Override + public WhirlwingStormbrood copy() { + return new WhirlwingStormbrood(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index c7ce0ec5c61..72c0adafb64 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -262,6 +262,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Warden of the Grove", 166, Rarity.RARE, mage.cards.w.WardenOfTheGrove.class)); cards.add(new SetCardInfo("Watcher of the Wayside", 249, Rarity.COMMON, mage.cards.w.WatcherOfTheWayside.class)); cards.add(new SetCardInfo("Wayspeaker Bodyguard", 34, Rarity.UNCOMMON, mage.cards.w.WayspeakerBodyguard.class)); + cards.add(new SetCardInfo("Whirlwing Stormbrood", 234, Rarity.UNCOMMON, mage.cards.w.WhirlwingStormbrood.class)); cards.add(new SetCardInfo("Wild Ride", 132, Rarity.COMMON, mage.cards.w.WildRide.class)); cards.add(new SetCardInfo("Wind-Scarred Crag", 271, Rarity.COMMON, mage.cards.w.WindScarredCrag.class)); cards.add(new SetCardInfo("Wingblade Disciple", 65, Rarity.UNCOMMON, mage.cards.w.WingbladeDisciple.class)); From 63781e9ca106c590c5fa4825ebef2955e0cb1746 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Tue, 8 Apr 2025 09:34:24 -0500 Subject: [PATCH 056/133] Missed some references to instanceof AdventureCard replaced with CardWithSpellOption --- .../src/mage/player/human/HumanPlayer.java | 2 +- Mage.Sets/src/mage/cards/d/DraugrNecromancer.java | 2 +- Mage.Sets/src/mage/cards/d/DreamDevourer.java | 4 ++-- Mage.Sets/src/mage/cards/e/EdginLarcenousLutenist.java | 4 ++-- Mage.Sets/src/mage/cards/e/EtherealValkyrie.java | 4 ++-- Mage.Sets/src/mage/cards/t/TinybonesBaubleBurglar.java | 2 +- .../src/test/java/mage/verify/VerifyCardDataTest.java | 10 +++++----- .../mage/abilities/effects/AsThoughEffectImpl.java | 7 +++---- 8 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 7115082ba57..2320626ce54 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -2379,7 +2379,7 @@ public class HumanPlayer extends PlayerImpl { Card mainCard = game.getCard(CardUtil.getMainCardId(game, ability.getSourceId())); if (mainCard != null && !Zone.BATTLEFIELD.equals(game.getState().getZone(mainCard.getId()))) { if (mainCard instanceof SplitCard - || mainCard instanceof AdventureCard + || mainCard instanceof CardWithSpellOption || mainCard instanceof ModalDoubleFacedCard) { return false; } diff --git a/Mage.Sets/src/mage/cards/d/DraugrNecromancer.java b/Mage.Sets/src/mage/cards/d/DraugrNecromancer.java index 4b48dddf5e1..59db469c383 100644 --- a/Mage.Sets/src/mage/cards/d/DraugrNecromancer.java +++ b/Mage.Sets/src/mage/cards/d/DraugrNecromancer.java @@ -180,7 +180,7 @@ class DraugrNecromancerSpendAnyManaEffect extends AsThoughEffectImpl implements CardState cardState; if (card instanceof SplitCard) { cardState = game.getLastKnownInformationCard(card.getId(), Zone.EXILED); - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { cardState = game.getLastKnownInformationCard(card.getId(), Zone.EXILED); } else if (card instanceof ModalDoubleFacedCard) { cardState = game.getLastKnownInformationCard(((ModalDoubleFacedCard) card).getLeftHalfCard().getId(), Zone.EXILED); diff --git a/Mage.Sets/src/mage/cards/d/DreamDevourer.java b/Mage.Sets/src/mage/cards/d/DreamDevourer.java index e9a00346504..0ed63bc1e30 100644 --- a/Mage.Sets/src/mage/cards/d/DreamDevourer.java +++ b/Mage.Sets/src/mage/cards/d/DreamDevourer.java @@ -103,9 +103,9 @@ class DreamDevourerAddAbilityEffect extends ContinuousEffectImpl { foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); } } - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { String creatureCost = CardUtil.reduceCost(card.getMainCard().getManaCost(), 2).getText(); - String spellCost = CardUtil.reduceCost(((AdventureCard) card).getSpellCard().getManaCost(), 2).getText(); + String spellCost = CardUtil.reduceCost(((CardWithSpellOption) card).getSpellCard().getManaCost(), 2).getText(); foretellAbility = new ForetellAbility(card, creatureCost, spellCost); } else { String costText = CardUtil.reduceCost(card.getManaCost(), 2).getText(); diff --git a/Mage.Sets/src/mage/cards/e/EdginLarcenousLutenist.java b/Mage.Sets/src/mage/cards/e/EdginLarcenousLutenist.java index 20a122cf63f..ada220bef82 100644 --- a/Mage.Sets/src/mage/cards/e/EdginLarcenousLutenist.java +++ b/Mage.Sets/src/mage/cards/e/EdginLarcenousLutenist.java @@ -100,9 +100,9 @@ class EdginLarcenousLutenistEffect extends ContinuousEffectImpl { foretellAbility = new ForetellAbility(card, leftHalfCost, rightHalfCost); } } - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { String creatureCost = CardUtil.reduceCost(card.getMainCard().getManaCost(), 2).getText(); - String spellCost = CardUtil.reduceCost(((AdventureCard) card).getSpellCard().getManaCost(), 2).getText(); + String spellCost = CardUtil.reduceCost(((CardWithSpellOption) card).getSpellCard().getManaCost(), 2).getText(); foretellAbility = new ForetellAbility(card, creatureCost, spellCost); } else { String costText = CardUtil.reduceCost(card.getManaCost(), 2).getText(); diff --git a/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java b/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java index 1cc5b9d3b91..b9f8d113128 100644 --- a/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java +++ b/Mage.Sets/src/mage/cards/e/EtherealValkyrie.java @@ -110,9 +110,9 @@ class EtherealValkyrieEffect extends OneShotEffect { foretellAbility = new ForetellAbility(exileCard, leftHalfCost, rightHalfCost); } } - } else if (exileCard instanceof AdventureCard) { + } else if (exileCard instanceof CardWithSpellOption) { String creatureCost = CardUtil.reduceCost(exileCard.getMainCard().getManaCost(), 2).getText(); - String spellCost = CardUtil.reduceCost(((AdventureCard) exileCard).getSpellCard().getManaCost(), 2).getText(); + String spellCost = CardUtil.reduceCost(((CardWithSpellOption) exileCard).getSpellCard().getManaCost(), 2).getText(); game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Cost", creatureCost); game.getState().setValue(exileCard.getMainCard().getId().toString() + "Foretell Split Cost", spellCost); foretellAbility = new ForetellAbility(exileCard, creatureCost, spellCost); diff --git a/Mage.Sets/src/mage/cards/t/TinybonesBaubleBurglar.java b/Mage.Sets/src/mage/cards/t/TinybonesBaubleBurglar.java index 0818d00cd1e..83473f261c3 100644 --- a/Mage.Sets/src/mage/cards/t/TinybonesBaubleBurglar.java +++ b/Mage.Sets/src/mage/cards/t/TinybonesBaubleBurglar.java @@ -211,7 +211,7 @@ class TinybonesBaubleBurglarSpendAnyManaEffect extends AsThoughEffectImpl implem CardState cardState; if (card instanceof SplitCard) { cardState = game.getLastKnownInformationCard(card.getId(), Zone.EXILED); - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { cardState = game.getLastKnownInformationCard(card.getId(), Zone.EXILED); } else if (card instanceof ModalDoubleFacedCard) { cardState = game.getLastKnownInformationCard(((ModalDoubleFacedCard) card).getLeftHalfCard().getId(), Zone.EXILED); diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 892bfadafbe..4c6e60a54de 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -306,9 +306,9 @@ public class VerifyCardDataTest { if (card instanceof CardWithHalves) { check(((CardWithHalves) card).getLeftHalfCard(), cardIndex); check(((CardWithHalves) card).getRightHalfCard(), cardIndex); - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { check(card, cardIndex); - check(((AdventureCard) card).getSpellCard(), cardIndex); + check(((CardWithSpellOption) card).getSpellCard(), cardIndex); } else { check(card, cardIndex); } @@ -2161,8 +2161,8 @@ public class VerifyCardDataTest { ); // card can contain rules text from both sides, so must search ref card for all sides too String additionalName; - if (card instanceof AdventureCard) { - additionalName = ((AdventureCard) card).getSpellCard().getName(); + if (card instanceof CardWithSpellOption) { + additionalName = ((CardWithSpellOption) card).getSpellCard().getName(); } else if (card.isTransformable() && !card.isNightCard()) { additionalName = card.getSecondCardFace().getName(); } else { @@ -2552,7 +2552,7 @@ public class VerifyCardDataTest { String[] cardRules = card .getRules() .stream() - .filter(s -> !(card instanceof AdventureCard) || !s.startsWith("Adventure ")) + .filter(s -> !(card instanceof CardWithSpellOption) || !s.startsWith("Adventure ") || !s.startsWith("Omen ")) .collect(Collectors.joining("\n")) .replace("
", "\n") .replace("
", "\n") diff --git a/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java index 09f1c74629b..6d8e2d6fea4 100644 --- a/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/AsThoughEffectImpl.java @@ -6,14 +6,13 @@ import mage.abilities.ActivatedAbility; import mage.cards.Card; import mage.cards.ModalDoubleFacedCard; import mage.cards.SplitCard; +import mage.cards.CardWithSpellOption; import mage.constants.*; import mage.game.Game; import mage.players.Player; import java.util.UUID; -import mage.cards.AdventureCard; - /** * @author BetaSteward_at_googlemail.com */ @@ -103,9 +102,9 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements if (!rightCard.isLand(game)) { player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier); } - } else if (card instanceof AdventureCard) { + } else if (card instanceof CardWithSpellOption) { Card creatureCard = card.getMainCard(); - Card spellCard = ((AdventureCard) card).getSpellCard(); + Card spellCard = ((CardWithSpellOption) card).getSpellCard(); player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts(), identifier); player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts(), identifier); } From 94c6b03dedfaf574ff2b15761b9afaa5396ad0e8 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Tue, 8 Apr 2025 09:41:50 -0500 Subject: [PATCH 057/133] Update showCardInfo for Adventure/Omen * formats implemented text to show main card followed by spell card * reference card also shows spell card information --- .../test/java/mage/verify/VerifyCardDataTest.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 4c6e60a54de..72b6d6dabad 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -2403,7 +2403,12 @@ public class VerifyCardDataTest { System.out.println(); System.out.println(card.getName() + " " + card.getManaCost().getText()); - if (card instanceof SplitCard || card instanceof ModalDoubleFacedCard) { + if (card instanceof CardWithSpellOption) { + // format to print main card then spell card + card.getInitAbilities().getRules().forEach(this::printAbilityText); + ((CardWithSpellOption) card).getSpellCard().getAbilities().getRules().forEach(r -> printAbilityText(r.replace("— ", "\n"))); + } + else if (card instanceof SplitCard || card instanceof ModalDoubleFacedCard) { card.getAbilities().getRules().forEach(this::printAbilityText); } else { card.getRules().forEach(this::printAbilityText); @@ -2412,12 +2417,20 @@ public class VerifyCardDataTest { // ref card System.out.println(); MtgJsonCard ref = MtgJsonService.card(card.getName()); + MtgJsonCard ref2 = null; + if (card instanceof CardWithSpellOption) { + ref2 = MtgJsonService.card(((CardWithSpellOption) card).getSpellCard().getName()); + } if (ref == null) { ref = MtgJsonService.cardByClassName(foundClassName); } if (ref != null) { System.out.println("ref: " + ref.getNameAsFace() + " " + ref.manaCost); System.out.println(ref.text); + if (ref2 != null) { + System.out.println(ref2.getNameAsFace() + " " + ref2.manaCost); + System.out.println(ref2.text); + } } else { System.out.println("WARNING, can't find mtgjson ref for " + card.getName()); } From 8b27b53fa72c055392f977bb8c1ec07d3fb499f5 Mon Sep 17 00:00:00 2001 From: androosss <101566943+androosss@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:20:41 +0200 Subject: [PATCH 058/133] Implemented Death Begets Life (#13513) --- .../src/mage/cards/d/DeathBegetsLife.java | 83 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 4 + 2 files changed, 87 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DeathBegetsLife.java diff --git a/Mage.Sets/src/mage/cards/d/DeathBegetsLife.java b/Mage.Sets/src/mage/cards/d/DeathBegetsLife.java new file mode 100644 index 00000000000..8c3efdd56d6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DeathBegetsLife.java @@ -0,0 +1,83 @@ +package mage.cards.d; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +/** + * + * @author androosss + */ +public final class DeathBegetsLife extends CardImpl { + + public DeathBegetsLife(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[] { CardType.SORCERY }, "{5}{B}{G}{U}"); + + // Destroy all creatures and enchantments. Draw a card for each permanent + // destroyed this way. + this.getSpellAbility().addEffect(new DeathBegetsLifeEffect()); + } + + private DeathBegetsLife(final DeathBegetsLife card) { + super(card); + } + + @Override + public DeathBegetsLife copy() { + return new DeathBegetsLife(this); + } +} + +class DeathBegetsLifeEffect extends OneShotEffect { + + private static final FilterPermanent filter = new FilterPermanent("creature or enchantment"); + static { + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + CardType.ENCHANTMENT.getPredicate())); + } + + DeathBegetsLifeEffect() { + super(Outcome.DestroyPermanent); + this.staticText = "Destroy all creatures and enchantments. Draw a card for each permanent destroyed this way"; + } + + private DeathBegetsLifeEffect(final DeathBegetsLifeEffect effect) { + super(effect); + } + + @Override + public DeathBegetsLifeEffect copy() { + return new DeathBegetsLifeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + int destroyedPermanent = 0; + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, + controller.getId(), game)) { + if (permanent.destroy(source, game)) { + destroyedPermanent++; + } + } + if (destroyedPermanent > 0) { + game.processAction(); + controller.drawCards(destroyedPermanent, source, game); + } + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 72c0adafb64..390d8f91aa8 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -64,6 +64,10 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Craterhoof Behemoth", 138, Rarity.MYTHIC, mage.cards.c.CraterhoofBehemoth.class)); cards.add(new SetCardInfo("Cruel Truths", 76, Rarity.COMMON, mage.cards.c.CruelTruths.class)); cards.add(new SetCardInfo("Dalkovan Packbeasts", 7, Rarity.UNCOMMON, mage.cards.d.DalkovanPackbeasts.class)); + cards.add(new SetCardInfo("Death Begets Life", 176, Rarity.MYTHIC, mage.cards.d.DeathBegetsLife.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Death Begets Life", 354, Rarity.MYTHIC, mage.cards.d.DeathBegetsLife.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Death Begets Life", 406, Rarity.MYTHIC, mage.cards.d.DeathBegetsLife.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Death Begets Life", 416, Rarity.MYTHIC, mage.cards.d.DeathBegetsLife.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Defibrillating Current", 177, Rarity.UNCOMMON, mage.cards.d.DefibrillatingCurrent.class)); cards.add(new SetCardInfo("Delta Bloodflies", 77, Rarity.COMMON, mage.cards.d.DeltaBloodflies.class)); cards.add(new SetCardInfo("Descendant of Storms", 8, Rarity.UNCOMMON, mage.cards.d.DescendantOfStorms.class)); From 78d21079034cc29c8d71b9d514ca9e60efa3a3e5 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:58:12 -0400 Subject: [PATCH 059/133] [TDM] Implement Teval, Arbiter of Virtue --- .../mage/cards/t/TevalArbiterOfVirtue.java | 98 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 99 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TevalArbiterOfVirtue.java diff --git a/Mage.Sets/src/mage/cards/t/TevalArbiterOfVirtue.java b/Mage.Sets/src/mage/cards/t/TevalArbiterOfVirtue.java new file mode 100644 index 00000000000..c172f35b82b --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TevalArbiterOfVirtue.java @@ -0,0 +1,98 @@ +package mage.cards.t; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledSpellsEffect; +import mage.abilities.keyword.DelveAbility; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.Game; +import mage.game.stack.Spell; + +/** + * + * @author Grath + */ +public final class TevalArbiterOfVirtue extends CardImpl { + + private static final FilterNonlandCard filter = new FilterNonlandCard("spells you cast"); + + static { + filter.add(Predicates.not(new AbilityPredicate(DelveAbility.class))); // So there are not redundant copies being added to each card + } + + public TevalArbiterOfVirtue(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{G}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(6); + this.toughness = new MageInt(6); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // Spells you cast have delve. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledSpellsEffect(new DelveAbility(false), filter))); + + // Whenever you cast a spell, you lose life equal to its mana value. + this.addAbility(new SpellCastControllerTriggeredAbility(new LoseLifeSourceControllerEffect(TevalArbiterOfVirtueValue.instance), false)); + } + + private TevalArbiterOfVirtue(final TevalArbiterOfVirtue card) { + super(card); + } + + @Override + public TevalArbiterOfVirtue copy() { + return new TevalArbiterOfVirtue(this); + } +} + +enum TevalArbiterOfVirtueValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return Optional + .ofNullable((Spell) effect.getValue("spellCast")) + .filter(Objects::nonNull) + .map(Spell::getManaValue) + .orElse(0); + } + + @Override + public TevalArbiterOfVirtueValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } + + @Override + public String toString() { + return "X"; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 390d8f91aa8..57bd15de2b3 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -240,6 +240,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Temur Monument", 248, Rarity.UNCOMMON, mage.cards.t.TemurMonument.class)); cards.add(new SetCardInfo("Temur Tawnyback", 229, Rarity.COMMON, mage.cards.t.TemurTawnyback.class)); cards.add(new SetCardInfo("Tersa Lightshatter", 127, Rarity.RARE, mage.cards.t.TersaLightshatter.class)); + cards.add(new SetCardInfo("Teval, Arbiter of Virtue", 230, Rarity.MYTHIC, mage.cards.t.TevalArbiterOfVirtue.class)); cards.add(new SetCardInfo("The Sibsig Ceremony", 91, Rarity.RARE, mage.cards.t.TheSibsigCeremony.class)); cards.add(new SetCardInfo("Thornwood Falls", 269, Rarity.COMMON, mage.cards.t.ThornwoodFalls.class)); cards.add(new SetCardInfo("Thunder of Unity", 231, Rarity.RARE, mage.cards.t.ThunderOfUnity.class)); From 13a832ae009a75616a812e074e98cc0e44ffd70d Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 8 Apr 2025 22:14:05 +0400 Subject: [PATCH 060/133] tests: removed outdated commands from init.txt example --- Mage.Server/release/config/init.example.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/Mage.Server/release/config/init.example.txt b/Mage.Server/release/config/init.example.txt index d645b731d57..03cbfd7e7b8 100644 --- a/Mage.Server/release/config/init.example.txt +++ b/Mage.Server/release/config/init.example.txt @@ -62,9 +62,6 @@ hand:Human:Angelic Edict:3 battlefield:Computer:Grizzly Bears:2 battlefield:Human:Grizzly Bears:2 -// special command, see SystemUtil for more special commands list -[@activate opponent ability] - [diff set codes example] battlefield:Human:XLN-Island:1 battlefield:Human:UST-Island:1 From bae3089abb41dcc48fda66e9d5047bcd2f0e42b3 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 8 Apr 2025 22:39:10 +0400 Subject: [PATCH 061/133] Reworked cost adjuster logic for better support of X and cost modification effects: Improves: * refactor: split CostAdjuster logic in multiple parts - prepare X, prepare cost, increase cost, reduce cost; * refactor: improved VariableManaCost to support min/max values, playable and AI calculations, test framework; * refactor: improved EarlyTargetCost to support mana costs too (related to #13023); * refactor: migrated some cards with CostAdjuster and X to EarlyTargetCost (Knollspine Invocation, etc - related to #13023); * refactor: added shared code for "As an additional cost to cast this spell, discard X creature cards"; * refactor: added shared code for "X is the converted mana cost of the exiled card"; * tests: added dozens tests with cost adjusters; Bug fixes: * game: fixed that some cards with CostAdjuster ignore min/max limits for X (allow to choose any X, example: Scorched Earth, Open The Way); * game: fixed that some cards ask to announce already defined X values (example: Bargaining Table); * game: fixed that some cards with CostAdjuster do not support combo with other cost modification effects; * game, gui: fixed missing game logs about predefined X values; * game, gui: fixed wrong X icon for predefined X values; Test framework: * test framework: added X min/max check for wrong values; * test framework: added X min/max info in miss X value announce; * test framework: added check to find duplicated effect bugs (see assertNoDuplicatedEffects); Cards: * Open The Way - fixed that it allow to choose any X without limits (close #12810); * Unbound Flourishing - improved combo support for activated abilities with predefined X mana costs like Bargaining Table; --- .../java/mage/player/ai/ComputerPlayer.java | 14 +- .../src/mage/player/human/HumanPlayer.java | 2 + Mage.Sets/src/mage/cards/a/AbandonHope.java | 35 +- Mage.Sets/src/mage/cards/a/AetherTide.java | 31 +- Mage.Sets/src/mage/cards/a/AladdinsLamp.java | 9 +- .../src/mage/cards/a/ArmMountedAnchor.java | 2 +- .../src/mage/cards/b/BargainingTable.java | 102 ++- .../src/mage/cards/b/BaruWurmspeaker.java | 7 +- .../src/mage/cards/b/BattlefieldButcher.java | 2 +- .../src/mage/cards/b/BeltOfGiantStrength.java | 45 +- .../src/mage/cards/b/BiteDownOnCrime.java | 2 +- .../src/mage/cards/c/CallerOfTheHunt.java | 3 +- .../cards/c/CaptainAmericaFirstAvenger.java | 3 +- .../src/mage/cards/c/ChanneledForce.java | 2 +- Mage.Sets/src/mage/cards/c/CrownOfGondor.java | 2 +- .../src/mage/cards/d/DeepwoodDenizen.java | 2 +- .../src/mage/cards/d/DevastatingDreams.java | 7 +- Mage.Sets/src/mage/cards/e/EliteArcanist.java | 29 +- .../src/mage/cards/e/EsquireOfTheKing.java | 2 +- .../src/mage/cards/e/EtheriumPteramander.java | 5 +- Mage.Sets/src/mage/cards/f/Fireball.java | 10 +- Mage.Sets/src/mage/cards/f/Firestorm.java | 5 +- .../src/mage/cards/f/FugitiveCodebreaker.java | 2 +- .../src/mage/cards/g/GhostfireBlade.java | 5 +- .../src/mage/cards/g/GrimGiganotosaurus.java | 2 +- Mage.Sets/src/mage/cards/h/HamletGlutton.java | 2 +- .../src/mage/cards/h/HelmOfObedience.java | 8 +- .../src/mage/cards/h/HinataDawnCrowned.java | 4 + .../mage/cards/h/HurkylsFinalMeditation.java | 2 +- .../src/mage/cards/h/HyldasCrownOfWinter.java | 2 +- Mage.Sets/src/mage/cards/i/IceOut.java | 2 +- .../src/mage/cards/i/InsidiousDreams.java | 2 +- .../src/mage/cards/j/JohannsStopgap.java | 2 +- .../src/mage/cards/k/KamiOfJealousThirst.java | 2 +- .../mage/cards/k/KnollspineInvocation.java | 124 +++- .../src/mage/cards/l/LoreseekersStone.java | 2 +- .../mage/cards/m/MariposaMilitaryBase.java | 2 +- .../src/mage/cards/m/MirrorOfGaladriel.java | 2 +- .../src/mage/cards/m/MobilizedDistrict.java | 2 +- Mage.Sets/src/mage/cards/n/NahirisWrath.java | 5 +- .../src/mage/cards/n/NecropolisFiend.java | 10 +- .../src/mage/cards/n/NemesisOfMortals.java | 2 +- .../src/mage/cards/n/NostalgicDreams.java | 4 +- Mage.Sets/src/mage/cards/o/OpenTheWay.java | 11 +- .../mage/cards/o/OsgirTheReconstructor.java | 13 +- .../src/mage/cards/p/PhyrexianPurge.java | 2 +- Mage.Sets/src/mage/cards/p/PlateArmor.java | 2 +- .../src/mage/cards/p/PrototypePortal.java | 34 +- Mage.Sets/src/mage/cards/p/Pteramander.java | 2 +- .../mage/cards/q/QuestForTheNecropolis.java | 2 +- .../mage/cards/r/RazorlashTransmogrant.java | 2 +- .../src/mage/cards/r/RestlessDreams.java | 2 +- .../mage/cards/s/SanctumOfTranquilLight.java | 2 +- Mage.Sets/src/mage/cards/s/ScorchedEarth.java | 34 +- .../src/mage/cards/s/SeafloorStalker.java | 2 +- .../src/mage/cards/s/SewerCrocodile.java | 2 +- .../src/mage/cards/s/SickeningDreams.java | 5 +- .../src/mage/cards/s/SkeletalScrying.java | 2 +- Mage.Sets/src/mage/cards/s/SoulFoundry.java | 32 +- .../src/mage/cards/t/TamiyosLogbook.java | 2 +- .../src/mage/cards/t/ThroneOfEldraine.java | 2 +- .../src/mage/cards/t/TowashiGuideBot.java | 2 +- .../src/mage/cards/t/TurbulentDreams.java | 2 +- .../src/mage/cards/u/UrgentNecropsy.java | 2 +- .../src/mage/cards/v/VengefulDreams.java | 4 +- .../mage/cards/v/VindictiveFlamestoker.java | 2 +- .../src/mage/cards/v/VoldarenEstate.java | 2 +- Mage.Sets/src/mage/cards/v/VoodooDoll.java | 2 +- .../src/mage/cards/w/WaytaTrainerProdigy.java | 2 +- .../cost/additional/AdjusterCostTest.java | 587 ++++++++++++++++++ .../single/afc/BeltOfGiantStrengthTest.java | 6 +- .../java/org/mage/test/player/TestPlayer.java | 23 +- .../base/impl/CardTestPlayerAPIImpl.java | 57 ++ .../src/main/java/mage/abilities/Ability.java | 33 +- .../main/java/mage/abilities/AbilityImpl.java | 114 +++- .../mage/abilities/costs/CostAdjuster.java | 82 ++- .../mage/abilities/costs/EarlyTargetCost.java | 17 +- .../abilities/costs/MinMaxVariableCost.java | 15 + .../costs/common/DiscardXTargetCost.java | 17 +- .../costs/common/PayLoyaltyCost.java | 4 +- .../costs/common/PayVariableLoyaltyCost.java | 4 +- .../CommanderManaValueAdjuster.java | 2 +- .../DiscardXCardsCostAdjuster.java | 73 +++ .../costs/costadjusters/DomainAdjuster.java | 2 +- .../ExileCardsFromHandAdjuster.java | 27 +- .../ImprintedManaValueXCostAdjuster.java | 37 ++ .../LegendaryCreatureCostAdjuster.java | 6 +- .../mage/abilities/costs/mana/ManaCost.java | 2 +- .../abilities/costs/mana/ManaCostImpl.java | 2 +- .../abilities/costs/mana/ManaCostsImpl.java | 2 +- .../costs/mana/VariableManaCost.java | 28 +- .../common/CardsInControllerHandCount.java | 26 +- .../abilities/effects/ContinuousEffects.java | 8 + .../LookTargetHandChooseDiscardEffect.java | 3 + .../abilities/keyword/SuspendAbility.java | 2 + .../java/mage/game/stack/StackAbility.java | 27 +- Mage/src/main/java/mage/players/Player.java | 8 +- .../main/java/mage/players/PlayerImpl.java | 15 +- .../TargetsCountAdjuster.java | 8 +- Mage/src/main/java/mage/util/CardUtil.java | 51 ++ 100 files changed, 1519 insertions(+), 449 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java create mode 100644 Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java create mode 100644 Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java create mode 100644 Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index d21ecf58811..71ec32b1c21 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -1863,8 +1863,11 @@ public class ComputerPlayer extends PlayerImpl { @Override public int announceXMana(int min, int max, String message, Game game, Ability ability) { - log.debug("announceXMana"); - //TODO: improve this + // current logic - use max possible mana + + // TODO: add good/bad effects support + // TODO: add simple game simulations like declare blocker? + int numAvailable = getAvailableManaProducers(game).size() - ability.getManaCosts().manaValue(); if (numAvailable < 0) { numAvailable = 0; @@ -1881,12 +1884,17 @@ public class ComputerPlayer extends PlayerImpl { @Override public int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variablCost) { - log.debug("announceXCost"); + // current logic - use random non-zero value + + // TODO: add good/bad effects support + // TODO: remove random logic + int value = RandomUtil.nextInt(CardUtil.overflowInc(max, 1)); if (value < min) { value = min; } if (value < max) { + // do not use zero values value++; } return value; diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 2320626ce54..46bafaf3eb4 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -1709,6 +1709,8 @@ public class HumanPlayer extends PlayerImpl { if (response.getInteger() != null) { break; } + + // TODO: add response verify here } if (response.getInteger() != null) { diff --git a/Mage.Sets/src/mage/cards/a/AbandonHope.java b/Mage.Sets/src/mage/cards/a/AbandonHope.java index 16365813b9d..0aeae4b6229 100644 --- a/Mage.Sets/src/mage/cards/a/AbandonHope.java +++ b/Mage.Sets/src/mage/cards/a/AbandonHope.java @@ -1,43 +1,30 @@ package mage.cards.a; -import mage.abilities.Ability; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster; import mage.abilities.dynamicvalue.common.GetXValue; -import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.discard.LookTargetHandChooseDiscardEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.target.common.TargetCardInHand; import mage.target.common.TargetOpponent; -import mage.util.CardUtil; import java.util.UUID; /** - * @author fireshoes + * @author fireshoes, JayDi85 */ public final class AbandonHope extends CardImpl { public AbandonHope(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{1}{B}"); - // As an additional cost to cast Abandon Hope, discard X cards. - Ability ability = new SimpleStaticAbility( - Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X cards") - ); - ability.setRuleAtTheTop(true); - this.addAbility(ability); + // As an additional cost to cast this spell, discard X cards. + DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_CARDS); // Look at target opponent's hand and choose X cards from it. That player discards those cards. this.getSpellAbility().addEffect(new LookTargetHandChooseDiscardEffect(false, GetXValue.instance, StaticFilters.FILTER_CARD_CARDS)); this.getSpellAbility().addTarget(new TargetOpponent()); - this.getSpellAbility().setCostAdjuster(AbandonHopeAdjuster.instance); } private AbandonHope(final AbandonHope card) { @@ -48,16 +35,4 @@ public final class AbandonHope extends CardImpl { public AbandonHope copy() { return new AbandonHope(this); } -} - -enum AbandonHopeAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); - if (xValue > 0) { - ability.addCost(new DiscardTargetCost(new TargetCardInHand(xValue, xValue, StaticFilters.FILTER_CARD_CARDS))); - } - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/AetherTide.java b/Mage.Sets/src/mage/cards/a/AetherTide.java index 885f74c69f0..eef2409f2b3 100644 --- a/Mage.Sets/src/mage/cards/a/AetherTide.java +++ b/Mage.Sets/src/mage/cards/a/AetherTide.java @@ -1,27 +1,18 @@ package mage.cards.a; -import mage.abilities.Ability; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster; import mage.abilities.effects.Effect; -import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.target.common.TargetCardInHand; import mage.target.common.TargetCreaturePermanent; import mage.target.targetadjustment.XTargetsCountAdjuster; -import mage.util.CardUtil; import java.util.UUID; /** - * * @author jeffwadsworth */ public final class AetherTide extends CardImpl { @@ -29,10 +20,8 @@ public final class AetherTide extends CardImpl { public AetherTide(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{U}"); - // As an additional cost to cast Aether Tide, discard X creature cards. - Ability ability = new SimpleStaticAbility(Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X creature cards")); - ability.setRuleAtTheTop(true); - this.addAbility(ability); + // As an additional cost to cast this spell, discard X creature cards. + DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_CREATURES); // Return X target creatures to their owners' hands. Effect effect = new ReturnToHandTargetEffect(); @@ -40,8 +29,6 @@ public final class AetherTide extends CardImpl { this.getSpellAbility().addEffect(effect); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); this.getSpellAbility().setTargetAdjuster(new XTargetsCountAdjuster()); - this.getSpellAbility().setCostAdjuster(AetherTideCostAdjuster.instance); - } private AetherTide(final AetherTide card) { @@ -53,15 +40,3 @@ public final class AetherTide extends CardImpl { return new AetherTide(this); } } - -enum AetherTideCostAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); - if (xValue > 0) { - ability.addCost(new DiscardTargetCost(new TargetCardInHand(xValue, xValue, StaticFilters.FILTER_CARD_CREATURES))); - } - } -} diff --git a/Mage.Sets/src/mage/cards/a/AladdinsLamp.java b/Mage.Sets/src/mage/cards/a/AladdinsLamp.java index 44614e7ec56..a4f7611e448 100644 --- a/Mage.Sets/src/mage/cards/a/AladdinsLamp.java +++ b/Mage.Sets/src/mage/cards/a/AladdinsLamp.java @@ -5,7 +5,6 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.effects.ReplacementEffectImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -36,12 +35,8 @@ public final class AladdinsLamp extends CardImpl { // {X}, {T}: The next time you would draw a card this turn, instead look at the top X cards of your library, put all but one of them on the bottom of your library in a random order, then draw a card. X can't be 0. Ability ability = new SimpleActivatedAbility(new AladdinsLampEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - for (Object cost : ability.getManaCosts()) { - if (cost instanceof VariableManaCost) { - ((VariableManaCost) cost).setMinX(1); - break; - } - } + ability.setVariableCostsMinMax(1, Integer.MAX_VALUE); + // TODO: add costAdjuster for min/max values due library size this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/a/ArmMountedAnchor.java b/Mage.Sets/src/mage/cards/a/ArmMountedAnchor.java index 1ee3a83718c..ca6455ad195 100644 --- a/Mage.Sets/src/mage/cards/a/ArmMountedAnchor.java +++ b/Mage.Sets/src/mage/cards/a/ArmMountedAnchor.java @@ -71,7 +71,7 @@ enum ArmMountedAnchorAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { // checking state if (HeckbentCondition.instance.apply(game, ability)) { CardUtil.reduceCost(ability, 2); diff --git a/Mage.Sets/src/mage/cards/b/BargainingTable.java b/Mage.Sets/src/mage/cards/b/BargainingTable.java index 3f280d7f552..62a928c6e58 100644 --- a/Mage.Sets/src/mage/cards/b/BargainingTable.java +++ b/Mage.Sets/src/mage/cards/b/BargainingTable.java @@ -1,25 +1,28 @@ package mage.cards.b; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.CostAdjuster; +import mage.abilities.costs.EarlyTargetCost; +import mage.abilities.costs.VariableCostType; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.InfoEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.Outcome; import mage.game.Game; import mage.players.Player; +import mage.target.Target; import mage.target.common.TargetOpponent; -import mage.util.CardUtil; + +import java.util.Objects; +import java.util.UUID; /** - * - * @author awjackson + * @author awjackson, JayDi85 */ public final class BargainingTable extends CardImpl { @@ -27,13 +30,10 @@ public final class BargainingTable extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{5}"); // {X}, {T}: Draw a card. X is the number of cards in an opponent's hand. - Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new ManaCostsImpl<>("{X}")); + Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new BargainingTableXCost()); ability.addCost(new TapSourceCost()); ability.addEffect(new InfoEffect("X is the number of cards in an opponent's hand")); - // You choose an opponent on announcement. This is not targeted, but a choice is still made. - // This choice is made before determining the value for X that is used in the cost. (2004-10-04) - ability.addTarget(new TargetOpponent(true)); - ability.setCostAdjuster(BargainingTableAdjuster.instance); + ability.setCostAdjuster(BargainingTableCostAdjuster.instance); this.addAbility(ability); } @@ -47,26 +47,70 @@ public final class BargainingTable extends CardImpl { } } -enum BargainingTableAdjuster implements CostAdjuster { +class BargainingTableXCost extends VariableManaCost implements EarlyTargetCost { + + // You choose an opponent on announcement. This is not targeted, but a choice is still made. + // This choice is made before determining the value for X that is used in the cost. + // (2004-10-04) + + public BargainingTableXCost() { + super(VariableCostType.NORMAL, 1); + } + + public BargainingTableXCost(final BargainingTableXCost cost) { + super(cost); + } + + @Override + public void chooseTarget(Game game, Ability source, Player controller) { + Target targetOpponent = new TargetOpponent(true); + controller.choose(Outcome.Benefit, targetOpponent, source, game); + addTarget(targetOpponent); + } + + @Override + public BargainingTableXCost copy() { + return new BargainingTableXCost(this); + } +} + +enum BargainingTableCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { - int handSize = Integer.MAX_VALUE; - if (game.inCheckPlayableState()) { - for (UUID playerId : CardUtil.getAllPossibleTargets(ability, game)) { - Player player = game.getPlayer(playerId); - if (player != null) { - handSize = Math.min(handSize, player.getHand().size()); - } - } - } else { - Player player = game.getPlayer(ability.getFirstTarget()); - if (player != null) { - handSize = player.getHand().size(); - } + public void prepareX(Ability ability, Game game) { + // make sure early target used + BargainingTableXCost cost = ability.getManaCostsToPay().getVariableCosts().stream() + .filter(c -> c instanceof BargainingTableXCost) + .map(c -> (BargainingTableXCost) c) + .findFirst() + .orElse(null); + if (cost == null) { + throw new IllegalArgumentException("Wrong code usage: cost item lost"); + } + + if (game.inCheckPlayableState()) { + // possible X + int minHandSize = game.getOpponents(ability.getControllerId(), true).stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .mapToInt(p -> p.getHand().size()) + .min() + .orElse(0); + int maxHandSize = game.getOpponents(ability.getControllerId(), true).stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .mapToInt(p -> p.getHand().size()) + .max() + .orElse(Integer.MAX_VALUE); + ability.setVariableCostsMinMax(minHandSize, maxHandSize); + } else { + // real X + Player opponent = game.getPlayer(cost.getTargets().getFirstTarget()); + if (opponent == null) { + throw new IllegalStateException("Wrong code usage: cost target lost"); + } + ability.setVariableCostsValue(opponent.getHand().size()); } - ability.clearManaCostsToPay(); - ability.addManaCostsToPay(new GenericManaCost(handSize)); } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java b/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java index d87af6d364b..c76fb6d540b 100644 --- a/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java +++ b/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java @@ -19,10 +19,7 @@ import mage.abilities.hint.ValueHint; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Duration; -import mage.constants.SubType; -import mage.constants.SuperType; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterCreaturePermanent; @@ -116,7 +113,7 @@ enum BaruWurmspeakerAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int value = BaruWurmspeakerValue.instance.calculate(game, ability, null); if (value > 0) { CardUtil.reduceCost(ability, value); diff --git a/Mage.Sets/src/mage/cards/b/BattlefieldButcher.java b/Mage.Sets/src/mage/cards/b/BattlefieldButcher.java index ab752a8e2cb..6251116133f 100644 --- a/Mage.Sets/src/mage/cards/b/BattlefieldButcher.java +++ b/Mage.Sets/src/mage/cards/b/BattlefieldButcher.java @@ -58,7 +58,7 @@ enum BattlefieldButcherAdjuster implements CostAdjuster { private static final Hint hint = new ValueHint("Creature cards in your graveyard", xValue); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, xValue.calculate(game, ability, null)); } diff --git a/Mage.Sets/src/mage/cards/b/BeltOfGiantStrength.java b/Mage.Sets/src/mage/cards/b/BeltOfGiantStrength.java index cd9693c66f6..40acc7e97f6 100644 --- a/Mage.Sets/src/mage/cards/b/BeltOfGiantStrength.java +++ b/Mage.Sets/src/mage/cards/b/BeltOfGiantStrength.java @@ -3,20 +3,21 @@ package mage.cards.b; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.CostAdjuster; +import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.common.continuous.SetBasePowerToughnessEnchantedEffect; import mage.abilities.keyword.EquipAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.Outcome; import mage.constants.SubType; import mage.game.Game; -import mage.game.permanent.Permanent; +import mage.target.common.TargetControlledCreaturePermanent; import mage.util.CardUtil; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; -import mage.abilities.costs.mana.GenericManaCost; -import mage.constants.Outcome; -import mage.target.common.TargetControlledCreaturePermanent; /** * @author TheElk801 @@ -35,7 +36,7 @@ public final class BeltOfGiantStrength extends CardImpl { // Equip {10}. This ability costs {X} less to activate where X is the power of the creature it targets. EquipAbility ability = new EquipAbility(Outcome.BoostCreature, new GenericManaCost(10), new TargetControlledCreaturePermanent(), false); ability.setCostReduceText("This ability costs {X} less to activate, where X is the power of the creature it targets."); - ability.setCostAdjuster(BeltOfGiantStrengthAdjuster.instance); + ability.setCostAdjuster(BeltOfGiantStrengthCostAdjuster.instance); this.addAbility(ability); } @@ -49,33 +50,23 @@ public final class BeltOfGiantStrength extends CardImpl { } } -enum BeltOfGiantStrengthAdjuster implements CostAdjuster { +enum BeltOfGiantStrengthCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { + int power; if (game.inCheckPlayableState()) { - int maxPower = 0; - for (UUID permId : CardUtil.getAllPossibleTargets(ability, game)) { - Permanent permanent = game.getPermanent(permId); - if (permanent != null) { - int power = permanent.getPower().getValue(); - if (power > maxPower) { - maxPower = power; - } - } - } - if (maxPower > 0) { - CardUtil.reduceCost(ability, maxPower); - } + power = CardUtil.getAllPossibleTargets(ability, game).stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .mapToInt(p -> p.getPower().getValue()) + .max().orElse(0); } else { - Permanent permanent = game.getPermanent(ability.getFirstTarget()); - if (permanent != null) { - int power = permanent.getPower().getValue(); - if (power > 0) { - CardUtil.reduceCost(ability, power); - } - } + power = Optional.ofNullable(game.getPermanent(ability.getFirstTarget())) + .map(p -> p.getPower().getValue()) + .orElse(0); } + CardUtil.reduceCost(ability, power); } } diff --git a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java index 98e460eadb2..b705cba0318 100644 --- a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java +++ b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java @@ -60,7 +60,7 @@ enum BiteDownOnCrimeAdjuster implements CostAdjuster { private static final OptionalAdditionalCost collectEvidenceCost = CollectEvidenceAbility.makeCost(6); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (CollectedEvidenceCondition.instance.apply(game, ability) || (game.inCheckPlayableState() && collectEvidenceCost.canPay(ability, null, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); diff --git a/Mage.Sets/src/mage/cards/c/CallerOfTheHunt.java b/Mage.Sets/src/mage/cards/c/CallerOfTheHunt.java index a486ea2e0b7..6b2043d66e7 100644 --- a/Mage.Sets/src/mage/cards/c/CallerOfTheHunt.java +++ b/Mage.Sets/src/mage/cards/c/CallerOfTheHunt.java @@ -59,7 +59,7 @@ enum CallerOfTheHuntAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { if (game.inCheckPlayableState()) { return; } @@ -103,6 +103,7 @@ enum CallerOfTheHuntAdjuster implements CostAdjuster { game.getState().setValue(sourceObject.getId() + "_type", maxSubType); } else { // human choose + // TODO: need early target cost instead dialog here Effect effect = new ChooseCreatureTypeEffect(Outcome.Benefit); effect.apply(game, ability); } diff --git a/Mage.Sets/src/mage/cards/c/CaptainAmericaFirstAvenger.java b/Mage.Sets/src/mage/cards/c/CaptainAmericaFirstAvenger.java index 34ecb776194..807a333291b 100644 --- a/Mage.Sets/src/mage/cards/c/CaptainAmericaFirstAvenger.java +++ b/Mage.Sets/src/mage/cards/c/CaptainAmericaFirstAvenger.java @@ -4,6 +4,7 @@ import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; +import mage.abilities.costs.CostImpl; import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.Cost; @@ -130,7 +131,7 @@ enum CaptainAmericaFirstAvengerValue implements DynamicValue { } } -class CaptainAmericaFirstAvengerUnattachCost extends EarlyTargetCost { +class CaptainAmericaFirstAvengerUnattachCost extends CostImpl implements EarlyTargetCost { private static final FilterPermanent filter = new FilterEquipmentPermanent("equipment attached to this creature"); private static final FilterPermanent subfilter = new FilterControlledPermanent("{this}"); diff --git a/Mage.Sets/src/mage/cards/c/ChanneledForce.java b/Mage.Sets/src/mage/cards/c/ChanneledForce.java index 74917f9627a..39a2d151ab1 100644 --- a/Mage.Sets/src/mage/cards/c/ChanneledForce.java +++ b/Mage.Sets/src/mage/cards/c/ChanneledForce.java @@ -25,7 +25,7 @@ public final class ChanneledForce extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{U}{R}"); // As an additional cost to cast this spell, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS)); + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Target player draws X cards. Channeled Force deals X damage to up to one target creature or planeswalker. this.getSpellAbility().addEffect(new ChanneledForceEffect()); diff --git a/Mage.Sets/src/mage/cards/c/CrownOfGondor.java b/Mage.Sets/src/mage/cards/c/CrownOfGondor.java index 27329de82bf..d9c7c0cb3f9 100644 --- a/Mage.Sets/src/mage/cards/c/CrownOfGondor.java +++ b/Mage.Sets/src/mage/cards/c/CrownOfGondor.java @@ -72,7 +72,7 @@ enum CrownOfGondorAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (MonarchIsSourceControllerCondition.instance.apply(game, ability)) { CardUtil.reduceCost(ability, 3); } diff --git a/Mage.Sets/src/mage/cards/d/DeepwoodDenizen.java b/Mage.Sets/src/mage/cards/d/DeepwoodDenizen.java index 5733ba08da2..a3097459e2f 100644 --- a/Mage.Sets/src/mage/cards/d/DeepwoodDenizen.java +++ b/Mage.Sets/src/mage/cards/d/DeepwoodDenizen.java @@ -62,7 +62,7 @@ enum DeepwoodDenizenAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, DeepwoodDenizenValue.instance.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/d/DevastatingDreams.java b/Mage.Sets/src/mage/cards/d/DevastatingDreams.java index 6847e84dfe6..7c42ed2a34f 100644 --- a/Mage.Sets/src/mage/cards/d/DevastatingDreams.java +++ b/Mage.Sets/src/mage/cards/d/DevastatingDreams.java @@ -5,6 +5,8 @@ import mage.abilities.costs.Cost; import mage.abilities.costs.VariableCostImpl; import mage.abilities.costs.VariableCostType; import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.common.DiscardXTargetCost; +import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.common.DamageAllEffect; import mage.abilities.effects.common.SacrificeAllEffect; @@ -12,6 +14,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.filter.common.FilterControlledLandPermanent; import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; @@ -28,8 +31,8 @@ public final class DevastatingDreams extends CardImpl { public DevastatingDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{R}{R}"); - // As an additional cost to cast Devastating Dreams, discard X cards at random. - this.getSpellAbility().addCost(new DevastatingDreamsAdditionalCost()); + // As an additional cost to cast this spell, discard X cards at random. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true).withRandom()); // Each player sacrifices X lands. this.getSpellAbility().addEffect(new SacrificeAllEffect(GetXValue.instance, new FilterControlledLandPermanent("lands"))); diff --git a/Mage.Sets/src/mage/cards/e/EliteArcanist.java b/Mage.Sets/src/mage/cards/e/EliteArcanist.java index ff2ad860b1e..d69a5fdc689 100644 --- a/Mage.Sets/src/mage/cards/e/EliteArcanist.java +++ b/Mage.Sets/src/mage/cards/e/EliteArcanist.java @@ -1,12 +1,13 @@ package mage.cards.e; +import mage.ApprovingObject; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; @@ -23,7 +24,6 @@ import mage.players.Player; import mage.target.TargetCard; import java.util.UUID; -import mage.ApprovingObject; /** * @author LevelX2 @@ -44,7 +44,7 @@ public final class EliteArcanist extends CardImpl { // {X}, {T}: Copy the exiled card. You may cast the copy without paying its mana cost. X is the converted mana cost of the exiled card. Ability ability = new SimpleActivatedAbility(new EliteArcanistCopyEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - ability.setCostAdjuster(EliteArcanistAdjuster.instance); + ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance); this.addAbility(ability); } @@ -58,29 +58,6 @@ public final class EliteArcanist extends CardImpl { } } -enum EliteArcanistAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - Permanent sourcePermanent = game.getPermanent(ability.getSourceId()); - if (sourcePermanent == null - || sourcePermanent.getImprinted() == null - || sourcePermanent.getImprinted().isEmpty()) { - return; - } - Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0)); - if (imprintedInstant == null) { - return; - } - int cmc = imprintedInstant.getManaValue(); - if (cmc > 0) { - ability.clearManaCostsToPay(); - ability.addManaCostsToPay(new GenericManaCost(cmc)); - } - } -} - class EliteArcanistImprintEffect extends OneShotEffect { private static final FilterCard filter = new FilterCard("instant card from your hand"); diff --git a/Mage.Sets/src/mage/cards/e/EsquireOfTheKing.java b/Mage.Sets/src/mage/cards/e/EsquireOfTheKing.java index 6f1ecf66002..86d81519f8d 100644 --- a/Mage.Sets/src/mage/cards/e/EsquireOfTheKing.java +++ b/Mage.Sets/src/mage/cards/e/EsquireOfTheKing.java @@ -64,7 +64,7 @@ enum EsquireOfTheKingAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_CREATURE_LEGENDARY, ability, game, 1)) { CardUtil.reduceCost(ability, 2); } diff --git a/Mage.Sets/src/mage/cards/e/EtheriumPteramander.java b/Mage.Sets/src/mage/cards/e/EtheriumPteramander.java index 2b29f88b3bd..681cdb1cb70 100644 --- a/Mage.Sets/src/mage/cards/e/EtheriumPteramander.java +++ b/Mage.Sets/src/mage/cards/e/EtheriumPteramander.java @@ -80,8 +80,7 @@ enum EtheriumPteramanderAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { - int count = artifactCount.calculate(game, ability, null); - CardUtil.reduceCost(ability, count); + public void reduceCost(Ability ability, Game game) { + CardUtil.reduceCost(ability, artifactCount.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/f/Fireball.java b/Mage.Sets/src/mage/cards/f/Fireball.java index acb030d570e..b55b90de1b9 100644 --- a/Mage.Sets/src/mage/cards/f/Fireball.java +++ b/Mage.Sets/src/mage/cards/f/Fireball.java @@ -2,11 +2,11 @@ package mage.cards.f; import mage.abilities.Ability; import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.CostModificationType; import mage.constants.Outcome; import mage.game.Game; import mage.game.permanent.Permanent; @@ -24,8 +24,8 @@ public final class Fireball extends CardImpl { public Fireball(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}"); - // Fireball deals X damage divided evenly, rounded down, among any number of target creatures and/or players. - // Fireball costs 1 more to cast for each target beyond the first. + // This spell costs {1} more to cast for each target beyond the first. + // Fireball deals X damage divided evenly, rounded down, among any number of targets. this.getSpellAbility().addTarget(new FireballTargetCreatureOrPlayer(0, Integer.MAX_VALUE)); this.getSpellAbility().addEffect(new FireballEffect()); this.getSpellAbility().setCostAdjuster(FireballAdjuster.instance); @@ -45,10 +45,10 @@ enum FireballAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { int numTargets = ability.getTargets().isEmpty() ? 0 : ability.getTargets().get(0).getTargets().size(); if (numTargets > 1) { - ability.addManaCostsToPay(new GenericManaCost(numTargets - 1)); + CardUtil.increaseCost(ability, numTargets - 1); } } } diff --git a/Mage.Sets/src/mage/cards/f/Firestorm.java b/Mage.Sets/src/mage/cards/f/Firestorm.java index a2f7f19f98f..2a01e592ac8 100644 --- a/Mage.Sets/src/mage/cards/f/Firestorm.java +++ b/Mage.Sets/src/mage/cards/f/Firestorm.java @@ -9,6 +9,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; @@ -25,8 +26,8 @@ public final class Firestorm extends CardImpl { public Firestorm(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}"); - // As an additional cost to cast Firestorm, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Firestorm deals X damage to each of X target creatures and/or players. this.getSpellAbility().addEffect(new FirestormEffect()); diff --git a/Mage.Sets/src/mage/cards/f/FugitiveCodebreaker.java b/Mage.Sets/src/mage/cards/f/FugitiveCodebreaker.java index e21d8c6fc0d..d38e97e54aa 100644 --- a/Mage.Sets/src/mage/cards/f/FugitiveCodebreaker.java +++ b/Mage.Sets/src/mage/cards/f/FugitiveCodebreaker.java @@ -92,7 +92,7 @@ enum FugitiveCodebreakerAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, FugitiveCodebreakerDisguiseAbility.xValue.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/g/GhostfireBlade.java b/Mage.Sets/src/mage/cards/g/GhostfireBlade.java index ad2537bc363..57b42d9ba57 100644 --- a/Mage.Sets/src/mage/cards/g/GhostfireBlade.java +++ b/Mage.Sets/src/mage/cards/g/GhostfireBlade.java @@ -55,9 +55,9 @@ enum GhostfireBladeAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { - // checking state + public void reduceCost(Ability ability, Game game) { if (game.inCheckPlayableState()) { + // possible if (CardUtil .getAllPossibleTargets(ability, game) .stream() @@ -67,6 +67,7 @@ enum GhostfireBladeAdjuster implements CostAdjuster { return; } } else { + // real Permanent permanent = game.getPermanent(ability.getFirstTarget()); if (permanent == null || !permanent.getColor(game).isColorless()) { return; diff --git a/Mage.Sets/src/mage/cards/g/GrimGiganotosaurus.java b/Mage.Sets/src/mage/cards/g/GrimGiganotosaurus.java index f67309915c5..8d4cb8ba558 100644 --- a/Mage.Sets/src/mage/cards/g/GrimGiganotosaurus.java +++ b/Mage.Sets/src/mage/cards/g/GrimGiganotosaurus.java @@ -74,7 +74,7 @@ enum GrimGiganotosaurusAdjuster implements CostAdjuster { private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { CardUtil.reduceCost(ability, xValue.calculate(game, ability, null)); diff --git a/Mage.Sets/src/mage/cards/h/HamletGlutton.java b/Mage.Sets/src/mage/cards/h/HamletGlutton.java index 2108c8ad0a4..703877d465b 100644 --- a/Mage.Sets/src/mage/cards/h/HamletGlutton.java +++ b/Mage.Sets/src/mage/cards/h/HamletGlutton.java @@ -61,7 +61,7 @@ enum HamletGluttonAdjuster implements CostAdjuster { private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost(); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); diff --git a/Mage.Sets/src/mage/cards/h/HelmOfObedience.java b/Mage.Sets/src/mage/cards/h/HelmOfObedience.java index a0498fd6251..469d26fcb8a 100644 --- a/Mage.Sets/src/mage/cards/h/HelmOfObedience.java +++ b/Mage.Sets/src/mage/cards/h/HelmOfObedience.java @@ -2,9 +2,8 @@ package mage.cards.h; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.VariableCostType; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.VariableManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.OneShotEffect; import mage.cards.*; @@ -29,9 +28,8 @@ public final class HelmOfObedience extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); // {X}, {T}: Target opponent puts cards from the top of their library into their graveyard until a creature card or X cards are put into that graveyard this way, whichever comes first. If a creature card is put into that graveyard this way, sacrifice Helm of Obedience and put that card onto the battlefield under your control. X can't be 0. - VariableManaCost xCosts = new VariableManaCost(VariableCostType.NORMAL); - xCosts.setMinX(1); - Ability ability = new SimpleActivatedAbility(new HelmOfObedienceEffect(), xCosts); + Ability ability = new SimpleActivatedAbility(new HelmOfObedienceEffect(), new ManaCostsImpl<>("{X}")); + ability.setVariableCostsMinMax(1, Integer.MAX_VALUE); ability.addCost(new TapSourceCost()); ability.addTarget(new TargetOpponent()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/h/HinataDawnCrowned.java b/Mage.Sets/src/mage/cards/h/HinataDawnCrowned.java index a685b776c34..27fa375465f 100644 --- a/Mage.Sets/src/mage/cards/h/HinataDawnCrowned.java +++ b/Mage.Sets/src/mage/cards/h/HinataDawnCrowned.java @@ -117,6 +117,10 @@ final class HinataDawnCrownedEffectUtility public static int getTargetCount(Game game, Ability abilityToModify) { if (game.inCheckPlayableState()) { + abilityToModify.getTargets().stream() + .mapToInt(a -> !a.isRequired() ? 0 : a.getMinNumberOfTargets()) + .min() + .orElse(0); Optional max = abilityToModify.getTargets().stream().map(x -> x.getMaxNumberOfTargets()).max(Integer::compare); int allPossibleSize = CardUtil.getAllPossibleTargets(abilityToModify, game).size(); return max.isPresent() ? diff --git a/Mage.Sets/src/mage/cards/h/HurkylsFinalMeditation.java b/Mage.Sets/src/mage/cards/h/HurkylsFinalMeditation.java index 4b3aa279774..3a7a896563f 100644 --- a/Mage.Sets/src/mage/cards/h/HurkylsFinalMeditation.java +++ b/Mage.Sets/src/mage/cards/h/HurkylsFinalMeditation.java @@ -49,7 +49,7 @@ enum HurkylsFinalMeditationAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { if (!game.isActivePlayer(ability.getControllerId())) { CardUtil.increaseCost(ability, 3); } diff --git a/Mage.Sets/src/mage/cards/h/HyldasCrownOfWinter.java b/Mage.Sets/src/mage/cards/h/HyldasCrownOfWinter.java index 51d266276c8..0dc2a950b17 100644 --- a/Mage.Sets/src/mage/cards/h/HyldasCrownOfWinter.java +++ b/Mage.Sets/src/mage/cards/h/HyldasCrownOfWinter.java @@ -82,7 +82,7 @@ enum HyldasCrownOfWinterAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (ability.getControllerId().equals(game.getActivePlayerId())) { CardUtil.reduceCost(ability, 1); } diff --git a/Mage.Sets/src/mage/cards/i/IceOut.java b/Mage.Sets/src/mage/cards/i/IceOut.java index f98ed44b76e..6d4dcf0eeb3 100644 --- a/Mage.Sets/src/mage/cards/i/IceOut.java +++ b/Mage.Sets/src/mage/cards/i/IceOut.java @@ -52,7 +52,7 @@ enum IceOutAdjuster implements CostAdjuster { private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost(); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 1); diff --git a/Mage.Sets/src/mage/cards/i/InsidiousDreams.java b/Mage.Sets/src/mage/cards/i/InsidiousDreams.java index d435205d5de..6ec4080d071 100644 --- a/Mage.Sets/src/mage/cards/i/InsidiousDreams.java +++ b/Mage.Sets/src/mage/cards/i/InsidiousDreams.java @@ -25,7 +25,7 @@ public final class InsidiousDreams extends CardImpl { public InsidiousDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{B}"); - // As an additional cost to cast Insidious Dreams, discard X cards. + // As an additional cost to cast this spell, discard X cards. this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Search your library for X cards. Then shuffle your library and put those cards on top of it in any order. diff --git a/Mage.Sets/src/mage/cards/j/JohannsStopgap.java b/Mage.Sets/src/mage/cards/j/JohannsStopgap.java index 8f08fa1a1ef..fc34f2aa974 100644 --- a/Mage.Sets/src/mage/cards/j/JohannsStopgap.java +++ b/Mage.Sets/src/mage/cards/j/JohannsStopgap.java @@ -54,7 +54,7 @@ enum JohannsStopgapAdjuster implements CostAdjuster { private static OptionalAdditionalCost bargainCost = BargainAbility.makeBargainCost(); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (BargainedCondition.instance.apply(game, ability) || (game.inCheckPlayableState() && bargainCost.canPay(ability, null, ability.getControllerId(), game))) { CardUtil.reduceCost(ability, 2); diff --git a/Mage.Sets/src/mage/cards/k/KamiOfJealousThirst.java b/Mage.Sets/src/mage/cards/k/KamiOfJealousThirst.java index fcd4bf15822..934bf79e8fb 100644 --- a/Mage.Sets/src/mage/cards/k/KamiOfJealousThirst.java +++ b/Mage.Sets/src/mage/cards/k/KamiOfJealousThirst.java @@ -60,7 +60,7 @@ enum KamiOfJealousThirstAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { int amount = CardsDrawnThisTurnDynamicValue.instance.calculate(game, ability, null); if (amount >= 3) { CardUtil.adjustCost(ability, new ManaCostsImpl<>("{4}{B}"), false); diff --git a/Mage.Sets/src/mage/cards/k/KnollspineInvocation.java b/Mage.Sets/src/mage/cards/k/KnollspineInvocation.java index f63a809d417..32b51409d80 100644 --- a/Mage.Sets/src/mage/cards/k/KnollspineInvocation.java +++ b/Mage.Sets/src/mage/cards/k/KnollspineInvocation.java @@ -1,40 +1,43 @@ package mage.cards.k; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.CostImpl; +import mage.abilities.costs.EarlyTargetCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.common.DamageTargetEffect; +import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.ComparisonType; -import mage.constants.Zone; +import mage.constants.Outcome; import mage.filter.FilterCard; -import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.game.Game; +import mage.players.Player; +import mage.target.Target; import mage.target.common.TargetAnyTarget; import mage.target.common.TargetCardInHand; -import mage.util.CardUtil; import java.util.UUID; /** - * @author anonymous + * @author JayDi85 */ public final class KnollspineInvocation extends CardImpl { - private static final FilterCard filter = new FilterCard("a card with mana value X"); + protected static final FilterCard filter = new FilterCard("a card with mana value X"); public KnollspineInvocation(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}{R}"); - // {X}, Discard a card with converted mana cost X: Knollspine Invocation deals X damage to any target. + // {X}, Discard a card with mana value X: This enchantment deals X damage to any target. Ability ability = new SimpleActivatedAbility(new DamageTargetEffect(GetXValue.instance, true), new ManaCostsImpl<>("{X}")); - ability.addCost(new DiscardTargetCost(new TargetCardInHand(filter))); + ability.addCost(new KnollspineInvocationDiscardCost()); ability.addTarget(new TargetAnyTarget()); ability.setCostAdjuster(KnollspineInvocationAdjuster.instance); this.addAbility(ability); @@ -50,22 +53,103 @@ public final class KnollspineInvocation extends CardImpl { } } +class KnollspineInvocationDiscardCost extends CostImpl implements EarlyTargetCost { + + // discard card with early target selection, so {X} mana cost can be setup after choose + + public KnollspineInvocationDiscardCost() { + super(); + this.text = "Discard a card with mana value X"; + } + + public KnollspineInvocationDiscardCost(final KnollspineInvocationDiscardCost cost) { + super(cost); + } + + @Override + public KnollspineInvocationDiscardCost copy() { + return new KnollspineInvocationDiscardCost(this); + } + + @Override + public void chooseTarget(Game game, Ability source, Player controller) { + Target target = new TargetCardInHand().withChooseHint("to discard with mana value for X"); + controller.choose(Outcome.Discard, target, source, game); + addTarget(target); + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + Player controller = game.getPlayer(controllerId); + return controller != null && !controller.getHand().isEmpty(); + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + this.paid = false; + + Player controller = game.getPlayer(controllerId); + if (controller == null) { + return false; + } + + Card card = controller.getHand().get(this.getTargets().getFirstTarget(), game); + if (card == null) { + return false; + } + + this.paid = controller.discard(card, true, source, game); + + return this.paid; + } +} + enum KnollspineInvocationAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { - int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); - for (Cost cost : ability.getCosts()) { - if (!(cost instanceof DiscardTargetCost)) { - continue; - } - DiscardTargetCost discardCost = (DiscardTargetCost) cost; - discardCost.getTargets().clear(); - FilterCard adjustedFilter = new FilterCard("a card with mana value X"); - adjustedFilter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, xValue)); - discardCost.addTarget(new TargetCardInHand(adjustedFilter)); + public void prepareX(Ability ability, Game game) { + Player controller = game.getPlayer(ability.getControllerId()); + if (controller == null) { return; } + + // make sure early target used + VariableManaCost costX = ability.getManaCostsToPay().stream() + .filter(c -> c instanceof VariableManaCost) + .map(c -> (VariableManaCost) c) + .findFirst() + .orElse(null); + if (costX == null) { + throw new IllegalArgumentException("Wrong code usage: costX lost"); + } + KnollspineInvocationDiscardCost costDiscard = ability.getCosts().stream() + .filter(c -> c instanceof KnollspineInvocationDiscardCost) + .map(c -> (KnollspineInvocationDiscardCost) c) + .findFirst() + .orElse(null); + if (costDiscard == null) { + throw new IllegalArgumentException("Wrong code usage: costDiscard lost"); + } + + if (game.inCheckPlayableState()) { + // possible X + int minManaValue = controller.getHand().getCards(game).stream() + .mapToInt(MageObject::getManaValue) + .min() + .orElse(0); + int maxManaValue = controller.getHand().getCards(game).stream() + .mapToInt(MageObject::getManaValue) + .max() + .orElse(0); + ability.setVariableCostsMinMax(minManaValue, maxManaValue); + } else { + // real X + Card card = controller.getHand().get(costDiscard.getTargets().getFirstTarget(), game); + if (card == null) { + throw new IllegalStateException("Wrong code usage: card to discard lost"); + } + ability.setVariableCostsValue(card.getManaValue()); + } } } diff --git a/Mage.Sets/src/mage/cards/l/LoreseekersStone.java b/Mage.Sets/src/mage/cards/l/LoreseekersStone.java index ce1a04b8533..f059d4cbfc3 100644 --- a/Mage.Sets/src/mage/cards/l/LoreseekersStone.java +++ b/Mage.Sets/src/mage/cards/l/LoreseekersStone.java @@ -48,7 +48,7 @@ enum LoreseekersStoneAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { Player player = game.getPlayer(ability.getControllerId()); if (player != null) { CardUtil.increaseCost(ability, player.getHand().size()); diff --git a/Mage.Sets/src/mage/cards/m/MariposaMilitaryBase.java b/Mage.Sets/src/mage/cards/m/MariposaMilitaryBase.java index a655b6d8db5..43b5ffb7483 100644 --- a/Mage.Sets/src/mage/cards/m/MariposaMilitaryBase.java +++ b/Mage.Sets/src/mage/cards/m/MariposaMilitaryBase.java @@ -64,7 +64,7 @@ enum MariposaMilitaryBaseAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, SourceControllerCountersCount.RAD.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/m/MirrorOfGaladriel.java b/Mage.Sets/src/mage/cards/m/MirrorOfGaladriel.java index d9c02ce3b65..da685a95e24 100644 --- a/Mage.Sets/src/mage/cards/m/MirrorOfGaladriel.java +++ b/Mage.Sets/src/mage/cards/m/MirrorOfGaladriel.java @@ -66,7 +66,7 @@ enum MirrorOfGaladrielAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int value = game.getBattlefield().count( StaticFilters.FILTER_CONTROLLED_CREATURE_LEGENDARY, ability.getControllerId(), ability, game diff --git a/Mage.Sets/src/mage/cards/m/MobilizedDistrict.java b/Mage.Sets/src/mage/cards/m/MobilizedDistrict.java index e6de269bb56..bff7732236f 100644 --- a/Mage.Sets/src/mage/cards/m/MobilizedDistrict.java +++ b/Mage.Sets/src/mage/cards/m/MobilizedDistrict.java @@ -74,7 +74,7 @@ enum MobilizedDistrictAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { int count = cardsCount.calculate(game, ability, null); diff --git a/Mage.Sets/src/mage/cards/n/NahirisWrath.java b/Mage.Sets/src/mage/cards/n/NahirisWrath.java index 015faadbce0..b7bb3ad30f7 100644 --- a/Mage.Sets/src/mage/cards/n/NahirisWrath.java +++ b/Mage.Sets/src/mage/cards/n/NahirisWrath.java @@ -8,6 +8,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.target.common.TargetCreatureOrPlaneswalker; import mage.target.targetadjustment.XTargetsCountAdjuster; @@ -21,8 +22,8 @@ public final class NahirisWrath extends CardImpl { public NahirisWrath(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{R}"); - // As an additional cost to cast Nahiri's Wrath, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Nahiri's Wrath deals damage equal to the total converted mana cost of the discarded cards to each of up to X target creatures and/or planeswalkers. Effect effect = new DamageTargetEffect(DiscardCostCardManaValue.instance); diff --git a/Mage.Sets/src/mage/cards/n/NecropolisFiend.java b/Mage.Sets/src/mage/cards/n/NecropolisFiend.java index 2f491e5ac22..930fb06c2b2 100644 --- a/Mage.Sets/src/mage/cards/n/NecropolisFiend.java +++ b/Mage.Sets/src/mage/cards/n/NecropolisFiend.java @@ -6,11 +6,9 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.VariableCost; import mage.abilities.costs.common.ExileFromGraveCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; @@ -86,16 +84,12 @@ enum NecropolisFiendCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareX(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller == null) { return; } - for (VariableCost variableCost : ability.getManaCostsToPay().getVariableCosts()) { - if (variableCost instanceof VariableManaCost) { - ((VariableManaCost) variableCost).setMaxX(controller.getGraveyard().size()); - } - } + ability.setVariableCostsMinMax(0, controller.getGraveyard().size()); } } diff --git a/Mage.Sets/src/mage/cards/n/NemesisOfMortals.java b/Mage.Sets/src/mage/cards/n/NemesisOfMortals.java index b238a6855e5..e6960031d07 100644 --- a/Mage.Sets/src/mage/cards/n/NemesisOfMortals.java +++ b/Mage.Sets/src/mage/cards/n/NemesisOfMortals.java @@ -63,7 +63,7 @@ enum NemesisOfMortalsAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { CardUtil.reduceCost(ability, controller.getGraveyard().count(StaticFilters.FILTER_CARD_CREATURE, game)); diff --git a/Mage.Sets/src/mage/cards/n/NostalgicDreams.java b/Mage.Sets/src/mage/cards/n/NostalgicDreams.java index 9bff99fef2d..bb453db207e 100644 --- a/Mage.Sets/src/mage/cards/n/NostalgicDreams.java +++ b/Mage.Sets/src/mage/cards/n/NostalgicDreams.java @@ -22,8 +22,8 @@ public final class NostalgicDreams extends CardImpl { public NostalgicDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{G}{G}"); - // As an additional cost to cast Nostalgic Dreams, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Return X target cards from your graveyard to your hand. Effect effect = new ReturnFromGraveyardToHandTargetEffect(); diff --git a/Mage.Sets/src/mage/cards/o/OpenTheWay.java b/Mage.Sets/src/mage/cards/o/OpenTheWay.java index 52f49155a0a..c1d7faebbc6 100644 --- a/Mage.Sets/src/mage/cards/o/OpenTheWay.java +++ b/Mage.Sets/src/mage/cards/o/OpenTheWay.java @@ -3,7 +3,6 @@ package mage.cards.o; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.InfoEffect; import mage.cards.*; @@ -28,7 +27,7 @@ public final class OpenTheWay extends CardImpl { this.addAbility(new SimpleStaticAbility( Zone.ALL, new InfoEffect("X can't be greater than the number of players in the game") ).setRuleAtTheTop(true)); - this.getSpellAbility().setCostAdjuster(OpenTheWayAdjuster.instance); + this.getSpellAbility().setCostAdjuster(OpenTheWayCostAdjuster.instance); // Reveal cards from the top of your library until you reveal X land cards. Put those land cards onto the battlefield tapped and the rest on the bottom of your library in a random order. this.getSpellAbility().addEffect(new OpenTheWayEffect()); @@ -44,14 +43,12 @@ public final class OpenTheWay extends CardImpl { } } -enum OpenTheWayAdjuster implements CostAdjuster { +enum OpenTheWayCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { - int playerCount = game.getPlayers().size(); - CardUtil.castStream(ability.getCosts().stream(), VariableManaCost.class) - .forEach(cost -> cost.setMaxX(playerCount)); + public void prepareX(Ability ability, Game game) { + ability.setVariableCostsMinMax(0, game.getState().getPlayersInRange(ability.getControllerId(), game, true).size()); } } diff --git a/Mage.Sets/src/mage/cards/o/OsgirTheReconstructor.java b/Mage.Sets/src/mage/cards/o/OsgirTheReconstructor.java index 5f2208a423b..6d72f04bcfb 100644 --- a/Mage.Sets/src/mage/cards/o/OsgirTheReconstructor.java +++ b/Mage.Sets/src/mage/cards/o/OsgirTheReconstructor.java @@ -33,7 +33,6 @@ import mage.util.CardUtil; import java.util.UUID; /** - * * @author Arketec */ public final class OsgirTheReconstructor extends CardImpl { @@ -81,15 +80,15 @@ enum OsgirTheReconstructorCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); Player controller = game.getPlayer(ability.getControllerId()); if (controller == null) { return; } - FilterCard filter = new FilterArtifactCard("an artifact card with mana value "+xValue+" from your graveyard"); + FilterCard filter = new FilterArtifactCard("an artifact card with mana value " + xValue + " from your graveyard"); filter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, xValue)); - for (Cost cost: ability.getCosts()) { + for (Cost cost : ability.getCosts()) { if (cost instanceof ExileFromGraveCost) { cost.getTargets().set(0, new TargetCardInYourGraveyard(filter)); } @@ -104,7 +103,7 @@ class OsgirTheReconstructorCreateArtifactTokensEffect extends OneShotEffect { this.staticText = "Create two tokens that are copies of the exiled card."; } - private OsgirTheReconstructorCreateArtifactTokensEffect(final OsgirTheReconstructorCreateArtifactTokensEffect effect) { + private OsgirTheReconstructorCreateArtifactTokensEffect(final OsgirTheReconstructorCreateArtifactTokensEffect effect) { super(effect); } @@ -127,11 +126,11 @@ class OsgirTheReconstructorCreateArtifactTokensEffect extends OneShotEffect { effect.setTargetPointer(new FixedTarget(card.getId(), game.getState().getZoneChangeCounter(card.getId()))); effect.apply(game, source); - return true; + return true; } @Override - public OsgirTheReconstructorCreateArtifactTokensEffect copy() { + public OsgirTheReconstructorCreateArtifactTokensEffect copy() { return new OsgirTheReconstructorCreateArtifactTokensEffect(this); } } diff --git a/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java b/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java index e0e5ac0e8b7..0e2b03b43a2 100644 --- a/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java +++ b/Mage.Sets/src/mage/cards/p/PhyrexianPurge.java @@ -46,7 +46,7 @@ enum PhyrexianPurgeCostAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void increaseCost(Ability ability, Game game) { int numTargets = ability.getTargets().get(0).getTargets().size(); if (numTargets > 0) { ability.addCost(new PayLifeCost(numTargets * 3)); diff --git a/Mage.Sets/src/mage/cards/p/PlateArmor.java b/Mage.Sets/src/mage/cards/p/PlateArmor.java index 3fe86c05be0..87f413a895d 100644 --- a/Mage.Sets/src/mage/cards/p/PlateArmor.java +++ b/Mage.Sets/src/mage/cards/p/PlateArmor.java @@ -81,7 +81,7 @@ enum PlateArmorAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { int count = equipmentCount.calculate(game, ability, null); diff --git a/Mage.Sets/src/mage/cards/p/PrototypePortal.java b/Mage.Sets/src/mage/cards/p/PrototypePortal.java index 9f2e4ea31f7..d8a92eb098f 100644 --- a/Mage.Sets/src/mage/cards/p/PrototypePortal.java +++ b/Mage.Sets/src/mage/cards/p/PrototypePortal.java @@ -4,11 +4,8 @@ import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.VariableCost; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; @@ -42,10 +39,10 @@ public final class PrototypePortal extends CardImpl { .setAbilityWord(AbilityWord.IMPRINT) ); - // {X}, {tap}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card. + // {X}, {T}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card. Ability ability = new SimpleActivatedAbility(new PrototypePortalCreateTokenEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - ability.setCostAdjuster(PrototypePortalAdjuster.instance); + ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance); this.addAbility(ability); } @@ -59,31 +56,6 @@ public final class PrototypePortal extends CardImpl { } } -enum PrototypePortalAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - Permanent card = game.getPermanent(ability.getSourceId()); - if (card != null) { - if (!card.getImprinted().isEmpty()) { - Card imprinted = game.getCard(card.getImprinted().get(0)); - if (imprinted != null) { - ability.clearManaCostsToPay(); - ability.addManaCostsToPay(new GenericManaCost(imprinted.getManaValue())); - } - } - } - - // no {X} anymore as we already have imprinted the card with defined manacost - for (ManaCost cost : ability.getManaCostsToPay()) { - if (cost instanceof VariableCost) { - cost.setPaid(); - } - } - } -} - class PrototypePortalEffect extends OneShotEffect { PrototypePortalEffect() { diff --git a/Mage.Sets/src/mage/cards/p/Pteramander.java b/Mage.Sets/src/mage/cards/p/Pteramander.java index c4fb5b9fe71..41a6acd784f 100644 --- a/Mage.Sets/src/mage/cards/p/Pteramander.java +++ b/Mage.Sets/src/mage/cards/p/Pteramander.java @@ -70,7 +70,7 @@ enum PteramanderAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int count = cardsCount.calculate(game, ability, null); CardUtil.reduceCost(ability, count); } diff --git a/Mage.Sets/src/mage/cards/q/QuestForTheNecropolis.java b/Mage.Sets/src/mage/cards/q/QuestForTheNecropolis.java index 3283a8631ec..8bcd9da1095 100644 --- a/Mage.Sets/src/mage/cards/q/QuestForTheNecropolis.java +++ b/Mage.Sets/src/mage/cards/q/QuestForTheNecropolis.java @@ -56,7 +56,7 @@ enum QuestForTheNecropolisAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int amount = Optional .ofNullable(ability.getSourcePermanentIfItStillExists(game)) .map(permanent -> permanent.getCounters(game).getCount(CounterType.QUEST)) diff --git a/Mage.Sets/src/mage/cards/r/RazorlashTransmogrant.java b/Mage.Sets/src/mage/cards/r/RazorlashTransmogrant.java index 22cdf9babf4..5ee5549c232 100644 --- a/Mage.Sets/src/mage/cards/r/RazorlashTransmogrant.java +++ b/Mage.Sets/src/mage/cards/r/RazorlashTransmogrant.java @@ -69,7 +69,7 @@ enum RazorlashTransmograntAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (makeMap(game, ability).values().stream().anyMatch(x -> x >= 4)) { CardUtil.reduceCost(ability, 4); } diff --git a/Mage.Sets/src/mage/cards/r/RestlessDreams.java b/Mage.Sets/src/mage/cards/r/RestlessDreams.java index 278a177431d..a81e7d2e06a 100644 --- a/Mage.Sets/src/mage/cards/r/RestlessDreams.java +++ b/Mage.Sets/src/mage/cards/r/RestlessDreams.java @@ -20,7 +20,7 @@ public final class RestlessDreams extends CardImpl { public RestlessDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}"); - // As an additional cost to cast Restless Dreams, discard X cards. + // As an additional cost to cast this spell, discard X cards. this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Return X target creature cards from your graveyard to your hand. diff --git a/Mage.Sets/src/mage/cards/s/SanctumOfTranquilLight.java b/Mage.Sets/src/mage/cards/s/SanctumOfTranquilLight.java index 11eb65f1810..ab18f92bf92 100644 --- a/Mage.Sets/src/mage/cards/s/SanctumOfTranquilLight.java +++ b/Mage.Sets/src/mage/cards/s/SanctumOfTranquilLight.java @@ -65,7 +65,7 @@ enum SanctumOfTranquilLightAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { CardUtil.reduceCost(ability, count.calculate(game, ability, null)); diff --git a/Mage.Sets/src/mage/cards/s/ScorchedEarth.java b/Mage.Sets/src/mage/cards/s/ScorchedEarth.java index 2eb04885c64..f9bfd97929a 100644 --- a/Mage.Sets/src/mage/cards/s/ScorchedEarth.java +++ b/Mage.Sets/src/mage/cards/s/ScorchedEarth.java @@ -1,23 +1,15 @@ - package mage.cards.s; -import mage.abilities.Ability; -import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.costadjusters.DiscardXCardsCostAdjuster; +import mage.abilities.dynamicvalue.common.CardsInControllerHandCount; import mage.abilities.effects.Effect; import mage.abilities.effects.common.DestroyTargetEffect; -import mage.abilities.effects.common.InfoEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Zone; -import mage.filter.common.FilterLandCard; -import mage.game.Game; -import mage.target.common.TargetCardInHand; +import mage.filter.StaticFilters; import mage.target.common.TargetLandPermanent; import mage.target.targetadjustment.XTargetsCountAdjuster; -import mage.util.CardUtil; import java.util.UUID; @@ -29,10 +21,8 @@ public final class ScorchedEarth extends CardImpl { public ScorchedEarth(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}"); - // As an additional cost to cast Scorched Earth, discard X land cards. - Ability ability = new SimpleStaticAbility(Zone.ALL, new InfoEffect("as an additional cost to cast this spell, discard X land cards")); - ability.setRuleAtTheTop(true); - this.addAbility(ability); + // As an additional cost to cast this spell, discard X land cards. + DiscardXCardsCostAdjuster.addAdjusterAndMessage(this, StaticFilters.FILTER_CARD_LANDS); // Destroy X target lands. Effect effect = new DestroyTargetEffect(); @@ -40,7 +30,7 @@ public final class ScorchedEarth extends CardImpl { this.getSpellAbility().addTarget(new TargetLandPermanent()); this.getSpellAbility().addEffect(effect); this.getSpellAbility().setTargetAdjuster(new XTargetsCountAdjuster()); - this.getSpellAbility().setCostAdjuster(ScorchedEarthCostAdjuster.instance); + this.getSpellAbility().addHint(CardsInControllerHandCount.LANDS.getHint()); } private ScorchedEarth(final ScorchedEarth card) { @@ -51,16 +41,4 @@ public final class ScorchedEarth extends CardImpl { public ScorchedEarth copy() { return new ScorchedEarth(this); } -} - -enum ScorchedEarthCostAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); - if (xValue > 0) { - ability.addCost(new DiscardTargetCost(new TargetCardInHand(xValue, xValue, new FilterLandCard("land cards")))); - } - } } \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SeafloorStalker.java b/Mage.Sets/src/mage/cards/s/SeafloorStalker.java index 443d5889fb3..21dcd5a12fa 100644 --- a/Mage.Sets/src/mage/cards/s/SeafloorStalker.java +++ b/Mage.Sets/src/mage/cards/s/SeafloorStalker.java @@ -60,7 +60,7 @@ enum SeafloorStalkerAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { Player controller = game.getPlayer(ability.getControllerId()); if (controller != null) { int count = PartyCount.instance.calculate(game, ability, null); diff --git a/Mage.Sets/src/mage/cards/s/SewerCrocodile.java b/Mage.Sets/src/mage/cards/s/SewerCrocodile.java index 71262d8589c..c7d5a2a3320 100644 --- a/Mage.Sets/src/mage/cards/s/SewerCrocodile.java +++ b/Mage.Sets/src/mage/cards/s/SewerCrocodile.java @@ -55,7 +55,7 @@ enum SewerCrocodileAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (DifferentManaValuesInGraveCondition.FIVE.apply(game, ability)) { CardUtil.reduceCost(ability, 3); } diff --git a/Mage.Sets/src/mage/cards/s/SickeningDreams.java b/Mage.Sets/src/mage/cards/s/SickeningDreams.java index 19079354d52..d8fd04a6bae 100644 --- a/Mage.Sets/src/mage/cards/s/SickeningDreams.java +++ b/Mage.Sets/src/mage/cards/s/SickeningDreams.java @@ -7,6 +7,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; import java.util.UUID; @@ -19,8 +20,8 @@ public final class SickeningDreams extends CardImpl { public SickeningDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}"); - // As an additional cost to cast Sickening Dreams, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Sickening Dreams deals X damage to each creature and each player. this.getSpellAbility().addEffect(new DamageEverythingEffect(GetXValue.instance, new FilterCreaturePermanent())); diff --git a/Mage.Sets/src/mage/cards/s/SkeletalScrying.java b/Mage.Sets/src/mage/cards/s/SkeletalScrying.java index f89a8dded84..9ea156bbf0b 100644 --- a/Mage.Sets/src/mage/cards/s/SkeletalScrying.java +++ b/Mage.Sets/src/mage/cards/s/SkeletalScrying.java @@ -61,7 +61,7 @@ enum SkeletalScryingAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { int xValue = CardUtil.getSourceCostsTag(game, ability, "X", 0); if (xValue > 0) { ability.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(xValue, xValue, StaticFilters.FILTER_CARDS_FROM_YOUR_GRAVEYARD))); diff --git a/Mage.Sets/src/mage/cards/s/SoulFoundry.java b/Mage.Sets/src/mage/cards/s/SoulFoundry.java index 050a8387362..f8301c8d43a 100644 --- a/Mage.Sets/src/mage/cards/s/SoulFoundry.java +++ b/Mage.Sets/src/mage/cards/s/SoulFoundry.java @@ -3,11 +3,8 @@ package mage.cards.s; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; -import mage.abilities.costs.CostAdjuster; -import mage.abilities.costs.VariableCost; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.costadjusters.ImprintedManaValueXCostAdjuster; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; @@ -45,7 +42,7 @@ public final class SoulFoundry extends CardImpl { // {X}, {T}: Create a token that's a copy of the exiled card. X is the converted mana cost of that card. Ability ability = new SimpleActivatedAbility(new SoulFoundryEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - ability.setCostAdjuster(SoulFoundryAdjuster.instance); + ability.setCostAdjuster(ImprintedManaValueXCostAdjuster.instance); this.addAbility(ability); } @@ -59,31 +56,6 @@ public final class SoulFoundry extends CardImpl { } } -enum SoulFoundryAdjuster implements CostAdjuster { - instance; - - @Override - public void adjustCosts(Ability ability, Game game) { - Permanent sourcePermanent = game.getPermanent(ability.getSourceId()); - if (sourcePermanent != null) { - if (!sourcePermanent.getImprinted().isEmpty()) { - Card imprinted = game.getCard(sourcePermanent.getImprinted().get(0)); - if (imprinted != null) { - ability.clearManaCostsToPay(); - ability.addManaCostsToPay(new GenericManaCost(imprinted.getManaValue())); - } - } - } - - // no {X} anymore as we already have imprinted the card with defined manacost - for (ManaCost cost : ability.getManaCostsToPay()) { - if (cost instanceof VariableCost) { - cost.setPaid(); - } - } - } -} - class SoulFoundryImprintEffect extends OneShotEffect { private static final FilterCard filter = new FilterCard("creature card from your hand"); diff --git a/Mage.Sets/src/mage/cards/t/TamiyosLogbook.java b/Mage.Sets/src/mage/cards/t/TamiyosLogbook.java index ea7780126be..1e50eae2df7 100644 --- a/Mage.Sets/src/mage/cards/t/TamiyosLogbook.java +++ b/Mage.Sets/src/mage/cards/t/TamiyosLogbook.java @@ -61,7 +61,7 @@ enum TamiyosLogbookAdjuster implements CostAdjuster { ); @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game); CardUtil.reduceCost(ability, count); } diff --git a/Mage.Sets/src/mage/cards/t/ThroneOfEldraine.java b/Mage.Sets/src/mage/cards/t/ThroneOfEldraine.java index 8c991ad97e2..5b503d04ec9 100644 --- a/Mage.Sets/src/mage/cards/t/ThroneOfEldraine.java +++ b/Mage.Sets/src/mage/cards/t/ThroneOfEldraine.java @@ -170,7 +170,7 @@ enum ThroneOfEldraineAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { ObjectColor color = (ObjectColor) game.getState().getValue(ability.getSourceId() + "_color"); if (color == null) { return; diff --git a/Mage.Sets/src/mage/cards/t/TowashiGuideBot.java b/Mage.Sets/src/mage/cards/t/TowashiGuideBot.java index 051da5a6ccc..15a26a67a7d 100644 --- a/Mage.Sets/src/mage/cards/t/TowashiGuideBot.java +++ b/Mage.Sets/src/mage/cards/t/TowashiGuideBot.java @@ -81,7 +81,7 @@ enum TowashiGuideBotAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, game.getBattlefield().count( filter, ability.getControllerId(), ability, game )); diff --git a/Mage.Sets/src/mage/cards/t/TurbulentDreams.java b/Mage.Sets/src/mage/cards/t/TurbulentDreams.java index eaf810befd6..db125c83c2f 100644 --- a/Mage.Sets/src/mage/cards/t/TurbulentDreams.java +++ b/Mage.Sets/src/mage/cards/t/TurbulentDreams.java @@ -20,7 +20,7 @@ public final class TurbulentDreams extends CardImpl { public TurbulentDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{U}{U}"); - // As an additional cost to cast Turbulent Dreams, discard X cards. + // As an additional cost to cast this spell, discard X cards. this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Return X target nonland permanents to their owners' hands. diff --git a/Mage.Sets/src/mage/cards/u/UrgentNecropsy.java b/Mage.Sets/src/mage/cards/u/UrgentNecropsy.java index 7358884883b..27643226474 100644 --- a/Mage.Sets/src/mage/cards/u/UrgentNecropsy.java +++ b/Mage.Sets/src/mage/cards/u/UrgentNecropsy.java @@ -61,7 +61,7 @@ enum UrgentNecropsyAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { int xValue = ability .getTargets() .stream() diff --git a/Mage.Sets/src/mage/cards/v/VengefulDreams.java b/Mage.Sets/src/mage/cards/v/VengefulDreams.java index a1d4a4c2363..47c70c73315 100644 --- a/Mage.Sets/src/mage/cards/v/VengefulDreams.java +++ b/Mage.Sets/src/mage/cards/v/VengefulDreams.java @@ -21,8 +21,8 @@ public final class VengefulDreams extends CardImpl { public VengefulDreams(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{W}{W}"); - // As an additional cost to cast Vengeful Dreams, discard X cards. - this.getSpellAbility().addCost(new DiscardXTargetCost(new FilterCard("cards"), true)); + // As an additional cost to cast this spell, discard X cards. + this.getSpellAbility().addCost(new DiscardXTargetCost(StaticFilters.FILTER_CARD_CARDS, true)); // Exile X target attacking creatures. Effect effect = new ExileTargetEffect(); diff --git a/Mage.Sets/src/mage/cards/v/VindictiveFlamestoker.java b/Mage.Sets/src/mage/cards/v/VindictiveFlamestoker.java index e52e393a821..75827a2ec5c 100644 --- a/Mage.Sets/src/mage/cards/v/VindictiveFlamestoker.java +++ b/Mage.Sets/src/mage/cards/v/VindictiveFlamestoker.java @@ -65,7 +65,7 @@ enum VindictiveFlamestokerAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { int amount = Optional .ofNullable(ability.getSourcePermanentIfItStillExists(game)) .filter(Objects::nonNull) diff --git a/Mage.Sets/src/mage/cards/v/VoldarenEstate.java b/Mage.Sets/src/mage/cards/v/VoldarenEstate.java index 96c78e9276c..84e6d25d367 100644 --- a/Mage.Sets/src/mage/cards/v/VoldarenEstate.java +++ b/Mage.Sets/src/mage/cards/v/VoldarenEstate.java @@ -132,7 +132,7 @@ enum VoldarenEstateCostAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, vampireCount.calculate(game, ability, null)); } } diff --git a/Mage.Sets/src/mage/cards/v/VoodooDoll.java b/Mage.Sets/src/mage/cards/v/VoodooDoll.java index c19eec025f0..1117c54da00 100644 --- a/Mage.Sets/src/mage/cards/v/VoodooDoll.java +++ b/Mage.Sets/src/mage/cards/v/VoodooDoll.java @@ -72,7 +72,7 @@ enum VoodooDollAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void prepareCost(Ability ability, Game game) { Permanent sourcePermanent = game.getPermanent(ability.getSourceId()); if (sourcePermanent != null) { int pin = sourcePermanent.getCounters(game).getCount(CounterType.PIN); diff --git a/Mage.Sets/src/mage/cards/w/WaytaTrainerProdigy.java b/Mage.Sets/src/mage/cards/w/WaytaTrainerProdigy.java index 780eaf2d97e..c7e7e20a89e 100644 --- a/Mage.Sets/src/mage/cards/w/WaytaTrainerProdigy.java +++ b/Mage.Sets/src/mage/cards/w/WaytaTrainerProdigy.java @@ -101,7 +101,7 @@ enum WaytaTrainerProdigyAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { if (game.inCheckPlayableState()) { int controllerTargets = 0; //number of possible targets controlled by the ability's controller for (UUID permId : CardUtil.getAllPossibleTargets(ability, game)) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java new file mode 100644 index 00000000000..7b9a1588ea6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java @@ -0,0 +1,587 @@ +package org.mage.test.cards.cost.additional; + +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.CostAdjuster; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.token.custom.CreatureToken; +import mage.util.CardUtil; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +import java.util.Arrays; + +/** + * @author JayDi85 + */ +public class AdjusterCostTest extends CardTestPlayerBaseWithAIHelps { + + private void prepareCustomCardInHand(String cardName, String spellManaCost, CostAdjuster costAdjuster) { + SpellAbility spellAbility = new SpellAbility(new ManaCostsImpl<>(spellManaCost), cardName); + if (costAdjuster != null) { + spellAbility.setCostAdjuster(costAdjuster); + } + addCustomCardWithAbility( + cardName, + playerA, + null, + spellAbility, + CardType.ENCHANTMENT, + spellManaCost, + Zone.HAND + ); + } + + private void prepareCustomPermanent(String cardName, String abilityName, String abilityManaCost, CostAdjuster costAdjuster) { + Ability ability = new SimpleActivatedAbility( + new CreateTokenEffect(new CreatureToken(1, 1).withName("test token")).setText(abilityName), + new ManaCostsImpl<>(abilityManaCost) + ); + if (costAdjuster != null) { + ability.setCostAdjuster(costAdjuster); + } + addCustomCardWithAbility(cardName, playerA, ability); + } + + @Test + public void test_DistributeValues() { + // make sure it can distribute values between min and max and skip useless values (example: mana optimization) + + Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, 0, 0)); + Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, -10, 10)); + Assert.assertEquals(Arrays.asList(), CardUtil.distributeValues(0, Integer.MIN_VALUE, Integer.MAX_VALUE)); + + Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 0)); + Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 1)); + Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 2)); + Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 3)); + Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(1, 0, 9)); + + Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(2, 0, 0)); + Assert.assertEquals(Arrays.asList(0, 1), CardUtil.distributeValues(2, 0, 1)); + Assert.assertEquals(Arrays.asList(0, 2), CardUtil.distributeValues(2, 0, 2)); + Assert.assertEquals(Arrays.asList(0, 3), CardUtil.distributeValues(2, 0, 3)); + Assert.assertEquals(Arrays.asList(0, 9), CardUtil.distributeValues(2, 0, 9)); + + Assert.assertEquals(Arrays.asList(0), CardUtil.distributeValues(3, 0, 0)); + Assert.assertEquals(Arrays.asList(0, 1), CardUtil.distributeValues(3, 0, 1)); + Assert.assertEquals(Arrays.asList(0, 1, 2), CardUtil.distributeValues(3, 0, 2)); + Assert.assertEquals(Arrays.asList(0, 2, 3), CardUtil.distributeValues(3, 0, 3)); + Assert.assertEquals(Arrays.asList(0, 5, 9), CardUtil.distributeValues(3, 0, 9)); + + Assert.assertEquals(Arrays.asList(10, 15, 20), CardUtil.distributeValues(3, 10, 20)); + Assert.assertEquals(Arrays.asList(10, 16, 21), CardUtil.distributeValues(3, 10, 21)); + Assert.assertEquals(Arrays.asList(10, 16, 22), CardUtil.distributeValues(3, 10, 22)); + Assert.assertEquals(Arrays.asList(10, 17, 23), CardUtil.distributeValues(3, 10, 23)); + Assert.assertEquals(Arrays.asList(10, 20, 29), CardUtil.distributeValues(3, 10, 29)); + + Assert.assertEquals(Arrays.asList(10), CardUtil.distributeValues(5, 10, 10)); + Assert.assertEquals(Arrays.asList(10, 11), CardUtil.distributeValues(5, 10, 11)); + Assert.assertEquals(Arrays.asList(10, 11, 12), CardUtil.distributeValues(5, 10, 12)); + Assert.assertEquals(Arrays.asList(10, 11, 12, 13), CardUtil.distributeValues(5, 10, 13)); + Assert.assertEquals(Arrays.asList(10, 11, 13, 14, 15), CardUtil.distributeValues(5, 10, 15)); + Assert.assertEquals(Arrays.asList(10, 13, 15, 18, 20), CardUtil.distributeValues(5, 10, 20)); + } + + @Test + public void test_X_SpellAbility() { + prepareCustomCardInHand("test card", "{X}{1}", null); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "test card"); + setChoice(playerA, "X=2"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "test card", 1); + } + + @Test + public void test_X_ActivatedAbility() { + prepareCustomPermanent("test card", "test ability", "{X}{1}", null); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}{1}:"); + setChoice(playerA, "X=2"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "test token", 1); + } + + @Test + public void test_prepareX_SpellAbility_TestFrameworkMustCatchLimits() { + prepareCustomCardInHand("test card", "{X}{1}", new CostAdjuster() { + @Override + public void prepareX(Ability ability, Game game) { + ability.setVariableCostsMinMax(0, 1); + } + }); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "test card"); + setChoice(playerA, "X=2"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + try { + execute(); + } catch (AssertionError e) { + Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("Found wrong X value = 2")); + Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("from 0 to 1")); + return; + } + Assert.fail("test must fail"); + } + + @Test + @Ignore // TODO: AI must support game simulations for X choice, see announceXMana + public void test_prepareX_SpellAbility_AI() { + prepareCustomCardInHand("test card", "{X}{1}", new CostAdjuster() { + @Override + public void prepareX(Ability ability, Game game) { + ability.setVariableCostsMinMax(0, 10); + } + }); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + + // AI must play card and use min good value + // it's bad to set X=1 for battlefield score cause card will give same score for X=0, X=1 + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "test card", 1); + assertTappedCount("Forest", true, 1); // must choose X=0 + } + + @Test + public void test_prepareX_ActivatedAbility_TestFrameworkMustCatchLimits() { + prepareCustomPermanent("test card", "test ability", "{X}{1}", new CostAdjuster() { + @Override + public void prepareX(Ability ability, Game game) { + ability.setVariableCostsMinMax(0, 1); + } + }); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}{1}:"); + setChoice(playerA, "X=2"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + try { + execute(); + } catch (AssertionError e) { + Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("Found wrong X value = 2")); + Assert.assertTrue("X must have limits: " + e.getMessage(), e.getMessage().contains("from 0 to 1")); + return; + } + Assert.fail("test must fail"); + } + + @Test + @Ignore // TODO: AI must support game simulations for X choice, see announceXMana + // TODO: implement AI and add tests for non-mana X values (announceXCost) + public void test_prepareX_ActivatedAbility_AI() { + prepareCustomPermanent("test card", "test ability", "{X}{1}", new CostAdjuster() { + @Override + public void prepareX(Ability ability, Game game) { + ability.setVariableCostsMinMax(0, 10); + } + }); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + + // AI must activate ability with min good value for X + // it's bad to set X=1 for battlefield score cause card will give same score for X=0, X=1 + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "test token", 1); + assertTappedCount("Forest", true, 1); // must choose X=0 + } + + @Test + public void test_prepareX_SpellAbility_ScorchedEarth_PayZero() { + // with X announce + + // As an additional cost to cast this spell, discard X land cards. + // Destroy X target lands. + addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 0 + 1); + addCard(Zone.HAND, playerA, "Island", 1); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 10); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth"); + setChoice(playerA, "X=0"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_prepareX_SpellAbility_ScorchedEarth_PaySome() { + // with X announce + + // As an additional cost to cast this spell, discard X land cards. + // Destroy X target lands. + addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2 + 1); + addCard(Zone.HAND, playerA, "Island", 10); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 10); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth"); + setChoice(playerA, "X=2"); + addTarget(playerA, "Forest", 2); // to destroy + setChoice(playerA, "Island", 2); // discard cost + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_prepareX_SpellAbility_ScorchedEarth_PayAll() { + // with X announce + + // As an additional cost to cast this spell, discard X land cards. + // Destroy X target lands. + addCard(Zone.HAND, playerA, "Scorched Earth"); // {X}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10 + 1); + addCard(Zone.HAND, playerA, "Island", 10); + addCard(Zone.BATTLEFIELD, playerB, "Forest", 10); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Scorched Earth"); + setChoice(playerA, "X=10"); + addTarget(playerA, "Forest", 10); // to destroy + setChoice(playerA, "Island", 10); // discard cost + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_prepareX_ActivatedAbility_BargainingTable_PayZero() { + // with direct X (without announce) + + // {X}, {T}: Draw a card. X is the number of cards in an opponent's hand. + addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + // + // no cards in opponent's hand + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:"); + setChoice(playerA, playerB.getName()); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 1); + } + + @Test + public void test_prepareX_ActivatedAbility_BargainingTable_PaySome() { + // with direct X (without announce) + + // {X}, {T}: Draw a card. X is the number of cards in an opponent's hand. + addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + // + addCard(Zone.HAND, playerB, "Forest", 3); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:"); + setChoice(playerA, playerB.getName()); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 1); + } + + @Test + public void test_prepareX_ActivatedAbility_BargainingTable_CantPay() { + // with direct X (without announce) + + // {X}, {T}: Draw a card. X is the number of cards in an opponent's hand. + addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + // + addCard(Zone.HAND, playerB, "Forest", 3); + + // must not request opponent choice because it must see min hand size as 3 + checkPlayableAbility("must not able to activate due lack of mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:", false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 0); + } + + @Test + public void test_prepareX_ActivatedAbility_BargainingTable_UnboundFlourishingMustCopy() { + // with direct X (without announce) + + // {X}, {T}: Draw a card. X is the number of cards in an opponent's hand. + addCard(Zone.BATTLEFIELD, playerA, "Bargaining Table"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + // + addCard(Zone.HAND, playerB, "Forest", 3); + // + // Whenever you cast a permanent spell with a mana cost that contains {X}, double the value of X. + // Whenever you cast an instant or sorcery spell or activate an ability, if that spell’s mana cost or that + // ability’s activation cost contains {X}, copy that spell or ability. You may choose new targets for the copy. + addCard(Zone.BATTLEFIELD, playerA, "Unbound Flourishing", 1); + + // Unbound Flourishing must see {X} mana cost and duplicate ability on stack + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}:"); + setChoice(playerA, playerB.getName()); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 2); // from original and copied abilities + } + + @Test + public void test_prepareX_NecropolisFiend() { + // {X}, {T}, Exile X cards from your graveyard: Target creature gets -X/-X until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Necropolis Fiend"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.GRAVEYARD, playerA, "Grizzly Bears", 3); + // + addCard(Zone.BATTLEFIELD, playerB, "Ancient Bronze Dragon"); // 7/7 + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}, Exile"); + setChoice(playerA, "X=2"); + addTarget(playerA, "Ancient Bronze Dragon"); // to -2/-2 + setChoice(playerA, "Grizzly Bears", 2); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerB, "Ancient Bronze Dragon", 7 - 2, 7 - 2); + } + + @Test + public void test_prepareX_OpenTheWay() { + skipInitShuffling(); + + // X can't be greater than the number of players in the game. + // Reveal cards from the top of your library until you reveal X land cards. + // Put those land cards onto the battlefield tapped and the rest on the bottom of your library in a random order. + addCard(Zone.HAND, playerA, "Open the Way"); // {X}{G}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 2); + addCard(Zone.LIBRARY, playerA, "Island", 5); + + // min/max test require multiple tests (see above), so just disable setChoice and look at logs for good limits + // example: Message: Announce the value for {X} (any value) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Open the Way"); + setChoice(playerA, "X=2"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Island", 2); + } + + @Test + public void test_prepareX_KnollspineInvocation() { + skipInitShuffling(); + + // {X}, Discard a card with mana value X: This enchantment deals X damage to any target. + addCard(Zone.BATTLEFIELD, playerA, "Knollspine Invocation"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + // + addCard(Zone.LIBRARY, playerA, "Grizzly Bears", 1); // {1}{G} + + // turn 1 - can't play due empty hand + checkPlayableAbility("no cards to discard", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", false); + + // turn 3 - can't play due no mana + activateManaAbility(3, PhaseStep.UPKEEP, playerA, "{T}: Add {G}", 2); + checkPlayableAbility("no mana to activate", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", false); + + // turn 5 - can play + checkPlayableAbility("must able to activate", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard", true); + activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, Discard"); + setChoice(playerA, "Grizzly Bears"); // discard + addTarget(playerA, playerB); // damage + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 2); + } + + @Test + public void test_prepareX_EliteArcanist() { + // When Elite Arcanist enters the battlefield, you may exile an instant card from your hand. + // {X}, {T}: Copy the exiled card. You may cast the copy without paying its mana cost. X is the converted mana cost of the exiled card. + addCard(Zone.HAND, playerA, "Elite Arcanist"); // {3}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 4 + 1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + + // turn 1 + // prepare arcanist + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Elite Arcanist"); + setChoice(playerA, true); // use exile + setChoice(playerA, "Lightning Bolt"); // to exile and copy later + + // turn 3 + // cast copy + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}: Copy"); + setChoice(playerA, true); // cast copy + addTarget(playerA, playerB); // damage + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertTappedCount("Island", true, 1); // used to cast copy for {1} + assertLife(playerB, 20 - 3); + } + + @Test + public void test_modifyCost_Fireball() { + // This spell costs {1} more to cast for each target beyond the first. + // Fireball deals X damage divided evenly, rounded down, among any number of targets. + addCard(Zone.HAND, playerA, "Fireball"); // {X}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 1); // 3 for x=2 cast, 1 for x2 targets + // + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 3); // 1/1 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Fireball"); + setChoice(playerA, "X=2"); + addTarget(playerA, "Arbor Elf^Arbor Elf"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertTappedCount("Mountain", true, 3 + 1); // 3 for x=2 cast, 1 for x2 targets + assertGraveyardCount(playerA, "Arbor Elf", 2); + assertPermanentCount(playerA, "Arbor Elf", 1); + } + + @Test + public void test_modifyCost_DeepwoodDenizen() { + // {5}{G}, {T}: Draw a card. This ability costs {1} less to activate for each +1/+1 counter on creatures you control. + addCard(Zone.BATTLEFIELD, playerA, "Deepwood Denizen"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 6 - 3); + // + // +1: Distribute three +1/+1 counters among one, two, or three target creatures you control + addCard(Zone.BATTLEFIELD, playerA, "Ajani, Mentor of Heroes", 1); + addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); + + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw", false); + + // add +3 counters and get -3 cost decrease + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Distribute"); + addTargetAmount(playerA, "Arbor Elf", 2); + addTargetAmount(playerA, "Deepwood Denizen", 1); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPlayableAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw", true); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}{G}, {T}: Draw"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 1); // +1 from draw ability + } + + @Test + public void test_modifyCost_BaruWurmspeaker() { + // Wurms you control get +2/+2 and have trample. + // {7}{G}, {T}: Create a 4/4 green Wurm creature token. This ability costs {X} less to activate, where X is the greatest power among Wurms you control. + addCard(Zone.BATTLEFIELD, playerA, "Baru, Wurmspeaker"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + // + // When this creature dies, create a 3/3 colorless Phyrexian Wurm artifact creature token with deathtouch + // and a 3/3 colorless Phyrexian Wurm artifact creature token with lifelink. + addCard(Zone.HAND, playerA, "Wurmcoil Engine", 1); // {6} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create", false); + + // turn 1 + // prepare wurm and get -8 cost decrease (6 wurm + 2 boost) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wurmcoil Engine"); + + // turn 3 + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN, playerA); + checkPlayableAbility("after", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create", true); + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{7}{G}, {T}: Create"); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertTokenCount(playerA, "Wurm Token", 1); + assertTappedCount("Forest", true, 1); + assertTappedCount("Mountain", true, 0); + } + + @Test + public void test_Other_AbandonHope() { + // used both modify and cost reduction in one cost adjuster + + // As an additional cost to cast this spell, discard X cards. + // Look at target opponent's hand and choose X cards from it. That player discards those cards. + addCard(Zone.HAND, playerA, "Abandon Hope"); // {X}{1}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2 + 2); + addCard(Zone.HAND, playerA, "Forest", 2); + addCard(Zone.HAND, playerB, "Grizzly Bears", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Abandon Hope"); + setChoice(playerA, "X=2"); + addTarget(playerA, playerB); + setChoice(playerA, "Forest^Forest"); // discard cost + setChoice(playerA, "Grizzly Bears^Grizzly Bears"); // discard from hand + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Forest", 2); + assertGraveyardCount(playerB, "Grizzly Bears", 2); + } + + // additional tasks to improve code base: + // TODO: OsgirTheReconstructorCostAdjuster - migrate to EarlyTargetCost + // TODO: SkeletalScryingAdjuster - migrate to EarlyTargetCost + // TODO: NecropolisFiend - migrate to EarlyTargetCost + // TODO: KnollspineInvocation - migrate to EarlyTargetCost + // TODO: ExileCardsFromHandAdjuster - need rework to remove dialog from inside, e.g. migrate to EarlyTargetCost? + // TODO: CallerOfTheHuntAdjuster - research and add test + // TODO: VoodooDoll - research and add test +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java index f79679ec443..d10b3497e0f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java @@ -13,8 +13,12 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class BeltOfGiantStrengthTest extends CardTestPlayerBase { + /** + * Equipped creature has base power and toughness 10/10. + * Equip {10}. This ability costs {X} less to activate where X is the power of the creature it targets. + */ private static final String belt = "Belt of Giant Strength"; - private static final String gigantosauras = "Gigantosaurus"; + private static final String gigantosauras = "Gigantosaurus"; // 10/10 @Test public void testWithManaAvailable() { diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index e33796e3a0f..2238c6dffeb 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -2927,6 +2927,7 @@ public class TestPlayer implements Player { for (String choice : choices) { if (choice.startsWith("X=")) { int xValue = Integer.parseInt(choice.substring(2)); + assertXMinMaxValue(game, ability, xValue, min, max); choices.remove(choice); return xValue; } @@ -2934,7 +2935,7 @@ public class TestPlayer implements Player { } this.chooseStrictModeFailed("choice", game, getInfo(ability, game) - + "\nMessage: " + message); + + "\nMessage: " + message + prepareXMaxInfo(min, max)); return computerPlayer.announceXMana(min, max, message, game, ability); } @@ -2944,16 +2945,34 @@ public class TestPlayer implements Player { if (!choices.isEmpty()) { if (choices.get(0).startsWith("X=")) { int xValue = Integer.parseInt(choices.get(0).substring(2)); + assertXMinMaxValue(game, ability, xValue, min, max); choices.remove(0); return xValue; } } this.chooseStrictModeFailed("choice", game, getInfo(ability, game) - + "\nMessage: " + message); + + "\nMessage: " + message + prepareXMaxInfo(min, max)); return computerPlayer.announceXCost(min, max, message, game, ability, null); } + private String prepareXMaxInfo(int min, int max) { + if (min == 0 && max == Integer.MAX_VALUE) { + return " (any value)"; + } else { + return String.format(" (from %s to %s)", + min, + (max == Integer.MAX_VALUE) ? "any" : String.valueOf(max) + ); + } + } + + private void assertXMinMaxValue(Game game, Ability source, int xValue, int min, int max) { + if (xValue < min || xValue > max) { + Assert.fail("Found wrong X value = " + xValue + ", for " + CardUtil.getSourceName(game, source) + ", must" + prepareXMaxInfo(min, max)); + } + } + @Override public int getAmount(int min, int max, String message, Game game) { assertAliasSupportInChoices(false); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index c78ea5c0fd7..228e51880ab 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -4,6 +4,9 @@ import mage.MageObject; import mage.Mana; import mage.ObjectColor; import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ContinuousEffectsList; +import mage.abilities.effects.Effect; import mage.cards.Card; import mage.cards.decks.Deck; import mage.cards.decks.DeckCardLists; @@ -29,6 +32,7 @@ import mage.players.Player; import mage.server.game.GameSessionPlayer; import mage.util.CardUtil; import mage.util.ThreadUtils; +import mage.utils.StreamUtils; import mage.utils.SystemUtil; import mage.view.GameView; import org.junit.Assert; @@ -320,6 +324,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } assertAllCommandsUsed(); + + //assertNoDuplicatedEffects(); } protected TestPlayer createNewPlayer(String playerName, RangeOfInfluence rangeOfInfluence) { @@ -1702,6 +1708,57 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } } + /** + * Make sure game state do not contain any duplicated effects, e.g. all effect's durations are fine + */ + private void assertNoDuplicatedEffects() { + // to find bugs like https://github.com/magefree/mage/issues/12932 + + // TODO: simulate full end turn to end all effects and make it default? + + // some effects can generate duplicated effects by design + // example: Tamiyo, Inquisitive Student + x2 attack in TamiyoInquisitiveStudentTest.test_PlusTwo + // +2: Until your next turn, whenever a creature attacks you or a planeswalker you control, it gets -1/-0 until end of turn. + // TODO: add targets and affected objects check to unique key? + + // one effect can be used multiple times by different sources + // so use group key like: effect + ability + source + Map> groups = new HashMap<>(); + for (ContinuousEffectsList layer : currentGame.getState().getContinuousEffects().allEffectsLists) { + for (Object effectObj : layer) { + ContinuousEffect effect = (ContinuousEffect) effectObj; + for (Object abilityObj : layer.getAbility(effect.getId())) { + Ability ability = (Ability) abilityObj; + MageObject sourceObject = currentGame.getObject(ability.getSourceId()); + String groupKey = "effectClass_" + effect.getClass().getCanonicalName() + + "_abilityClass_" + ability.getClass().getCanonicalName() + + "_sourceName_" + (sourceObject == null ? "null" : sourceObject.getIdName()); + List groupList = groups.getOrDefault(groupKey, null); + if (groupList == null) { + groupList = new ArrayList<>(); + groups.put(groupKey, groupList); + } + groupList.add(effect); + } + } + } + + // analyse + List duplicatedGroups = groups.keySet().stream() + .filter(groupKey -> groups.get(groupKey).size() > 1) + .collect(Collectors.toList()); + if (duplicatedGroups.size() > 0) { + System.out.println("Duplicated effect groups: " + duplicatedGroups.size()); + duplicatedGroups.forEach(groupKey -> { + System.out.println("group " + groupKey + ": "); + groups.get(groupKey).forEach(e -> { + System.out.println(" - " + e.getId() + " - " + e.getDuration() + " - " + e); + }); + }); + Assert.fail("Found duplicated effects: " + duplicatedGroups.size()); + } + } + public void assertActivePlayer(TestPlayer player) { Assert.assertEquals("message", currentGame.getState().getActivePlayerId(), player.getId()); } diff --git a/Mage/src/main/java/mage/abilities/Ability.java b/Mage/src/main/java/mage/abilities/Ability.java index 96f1a194195..e1bb0a5618b 100644 --- a/Mage/src/main/java/mage/abilities/Ability.java +++ b/Mage/src/main/java/mage/abilities/Ability.java @@ -126,7 +126,7 @@ public interface Ability extends Controllable, Serializable { /** * Gets all {@link ManaCosts} associated with this ability. These returned * costs should never be modified as they represent the base costs before - * any modifications. + * any modifications (only cost adjusters can change it, e.g. set min/max values) * * @return All {@link ManaCosts} that must be paid. */ @@ -151,6 +151,17 @@ public interface Ability extends Controllable, Serializable { void addManaCostsToPay(ManaCost manaCost); + /** + * Helper method to setup actual min/max limits of current X costs BEFORE player's X announcement + */ + void setVariableCostsMinMax(int min, int max); + + /** + * Helper method to replace X by direct value BEFORE player's X announcement + * If you need additional target for X then use CostAdjuster + EarlyTargetCost (example: Bargaining Table) + */ + void setVariableCostsValue(int xValue); + /** * Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet. * Does NOT return the source permanent's tags. @@ -356,7 +367,7 @@ public interface Ability extends Controllable, Serializable { * - for dies triggers - override and use TriggeredAbilityImpl.isInUseableZoneDiesTrigger inside + set setLeavesTheBattlefieldTrigger(true) * * @param sourceObject can be null for static continues effects checking like rules modification (example: Yixlid Jailer) - * @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder) + * @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder) */ boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event); @@ -533,11 +544,25 @@ public interface Ability extends Controllable, Serializable { void adjustTargets(Game game); + /** + * Dynamic X and cost modification, see CostAdjuster for more details on usage + */ Ability setCostAdjuster(CostAdjuster costAdjuster); - CostAdjuster getCostAdjuster(); + /** + * Prepare {X} settings for announce + */ + void adjustX(Game game); - void adjustCosts(Game game); + /** + * Prepare costs (generate due game state or announce) + */ + void adjustCostsPrepare(Game game); + + /** + * Apply additional cost modifications logic/effects + */ + void adjustCostsModify(Game game, CostModificationType costModificationType); List getHints(); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 114e88343f5..95d13c2d59e 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -291,6 +291,8 @@ public abstract class AbilityImpl implements Ability { // fused or spliced spells contain multiple abilities (e.g. fused, left, right) // optional costs and cost modification must be applied only to the first/main ability + // TODO: need tests with X announced costs, cost modification effects, CostAdjuster, early cost target, etc + // can be bugged due multiple calls (not all code parts below use isMainPartAbility) boolean isMainPartAbility = !CardUtil.isFusedPartAbility(this, game); /* 20220908 - 601.2b @@ -326,6 +328,16 @@ public abstract class AbilityImpl implements Ability { return false; } + // 20241022 - 601.2b + // Choose targets for costs that have to be chosen early + // Not yet included in 601.2b but this is where it will be + handleChooseCostTargets(game, controller); + + // prepare dynamic costs (must be called before any x announce) + if (isMainPartAbility) { + adjustX(game); + } + // 20121001 - 601.2b // If the spell has a variable cost that will be paid as it's being cast (such as an {X} in // its mana cost; see rule 107.3), the player announces the value of that variable. @@ -402,7 +414,7 @@ public abstract class AbilityImpl implements Ability { // Note: ActivatedAbility does include SpellAbility & PlayLandAbility, but those should be able to be canceled too. boolean canCancel = this instanceof ActivatedAbility && controller.isHuman(); if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) { - // was canceled during targer selection + // was canceled during target selection return false; } } @@ -428,7 +440,7 @@ public abstract class AbilityImpl implements Ability { //20101001 - 601.2e if (isMainPartAbility) { - adjustCosts(game); // still needed for CostAdjuster objects (to handle some types of dynamic costs) + // adjustX already called before any announces game.getContinuousEffects().costModification(this, game); } @@ -726,6 +738,11 @@ public abstract class AbilityImpl implements Ability { ((EarlyTargetCost) cost).chooseTarget(game, this, controller); } } + for (ManaCost cost : getManaCostsToPay()) { + if (cost instanceof EarlyTargetCost && cost.getTargets().isEmpty()) { + ((EarlyTargetCost) cost).chooseTarget(game, this, controller); + } + } } /** @@ -764,8 +781,15 @@ public abstract class AbilityImpl implements Ability { if (!variableManaCost.isPaid()) { // should only happen for human players int xValue; if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) { - xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(), - "Announce the value for " + variableManaCost.getText(), game, this); + if (variableManaCost.wasAnnounced()) { + // announce by rules + xValue = variableManaCost.getAmount(); + } else { + // announce by player + xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(), + "Announce the value for " + variableManaCost.getText(), game, this); + } + int amountMana = xValue * variableManaCost.getXInstancesCount(); StringBuilder manaString = threadLocalBuilder.get(); if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) { @@ -1072,6 +1096,60 @@ public abstract class AbilityImpl implements Ability { } } + @Override + public void setVariableCostsMinMax(int min, int max) { + // modify all values (mtg rules allow only one type of X, so min/max must be shared between all X instances) + + // base cost + for (ManaCost cost : getManaCosts()) { + if (cost instanceof MinMaxVariableCost) { + MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost; + minMaxCost.setMinX(min); + minMaxCost.setMaxX(max); + } + } + + // prepared cost + for (ManaCost cost : getManaCostsToPay()) { + if (cost instanceof MinMaxVariableCost) { + MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost; + minMaxCost.setMinX(min); + minMaxCost.setMaxX(max); + } + } + } + + @Override + public void setVariableCostsValue(int xValue) { + // only mana cost supported + + // base cost + boolean foundBaseCost = false; + for (ManaCost cost : getManaCosts()) { + if (cost instanceof VariableManaCost) { + foundBaseCost = true; + ((VariableManaCost) cost).setMinX(xValue); + ((VariableManaCost) cost).setMaxX(xValue); + ((VariableManaCost) cost).setAmount(xValue, xValue, false); + } + } + + // prepared cost + boolean foundPreparedCost = false; + for (ManaCost cost : getManaCostsToPay()) { + if (cost instanceof VariableManaCost) { + foundPreparedCost = true; + ((VariableManaCost) cost).setMinX(xValue); + ((VariableManaCost) cost).setMaxX(xValue); + ((VariableManaCost) cost).setAmount(xValue, xValue, false); + } + } + + if (!foundPreparedCost || !foundBaseCost) { + throw new IllegalArgumentException("Wrong code usage: auto-announced X values allowed in mana costs only"); + } + } + @Override public void addEffect(Effect effect) { if (effect != null) { @@ -1676,17 +1754,6 @@ public abstract class AbilityImpl implements Ability { } } - /** - * Dynamic cost modification for ability.
- * Example: if it need stack related info (like real targets) then must - * check two states (game.inCheckPlayableState):
- * 1. In playable state it must check all possible use cases (e.g. allow to - * reduce on any available target and modes)
- * 2. In real cast state it must check current use case (e.g. real selected - * targets and modes) - * - * @param costAdjuster - */ @Override public AbilityImpl setCostAdjuster(CostAdjuster costAdjuster) { this.costAdjuster = costAdjuster; @@ -1694,14 +1761,23 @@ public abstract class AbilityImpl implements Ability { } @Override - public CostAdjuster getCostAdjuster() { - return costAdjuster; + public void adjustX(Game game) { + if (costAdjuster != null) { + costAdjuster.prepareX(this, game); + } } @Override - public void adjustCosts(Game game) { + public void adjustCostsPrepare(Game game) { if (costAdjuster != null) { - costAdjuster.adjustCosts(this, game); + costAdjuster.prepareCost(this, game); + } + } + + @Override + public void adjustCostsModify(Game game, CostModificationType costModificationType) { + if (costAdjuster != null) { + costAdjuster.modifyCost(this, game, costModificationType); } } diff --git a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java index ea1d77449d0..e522779f851 100644 --- a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java @@ -1,24 +1,86 @@ package mage.abilities.costs; import mage.abilities.Ability; +import mage.constants.CostModificationType; import mage.game.Game; import java.io.Serializable; /** - * @author TheElk801 + * Dynamic costs implementation to control {X} or other costs, can be used in spells and abilities + *

+ * Possible use cases: + * - define {X} costs like X cards to discard (mana and non-mana values); + * - define {X} limits before announce (to help in UX and AI logic) + * - define any dynamic costs + * - use as simple cost increase/reduce effect + *

+ * Calls order by game engine: + * - ... early cost target selection for EarlyTargetCost ... + * - prepareX + * - ... x announce ... + * - prepareCost + * - increaseCost + * - reduceCost + * - ... normal target selection and payment ... + * + * @author TheElk801, JayDi85 */ -@FunctionalInterface public interface CostAdjuster extends Serializable { /** - * Must check playable and real cast states. - * Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState): - * 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes) - * 2. In real cast state it must check current use case (e.g. real selected targets and modes) - * - * @param ability - * @param game + * Prepare {X} costs settings or define auto-announced mana values + *

+ * Usage example: + * - define auto-announced mana value {X} by ability.setVariableCostsValue + * - define possible {X} settings by ability.setVariableCostsMinMax */ - void adjustCosts(Ability ability, Game game); + default void prepareX(Ability ability, Game game) { + // do nothing + } + + /** + * Prepare any dynamic costs + *

+ * Usage example: + * - add real cost after {X} mana value announce by CardUtil.getSourceCostsTagX + * - add dynamic cost from game data + */ + default void prepareCost(Ability ability, Game game) { + // do nothing + } + + /** + * Simple cost reduction effect + */ + default void reduceCost(Ability ability, Game game) { + // do nothing + } + + /** + * Simple cost increase effect + */ + default void increaseCost(Ability ability, Game game) { + // do nothing + } + + /** + * Default implementation. Override reduceCost or increaseCost instead + * TODO: make it private after java 9+ migrate + */ + default void modifyCost(Ability ability, Game game, CostModificationType costModificationType) { + switch (costModificationType) { + case REDUCE_COST: + reduceCost(ability, game); + break; + case INCREASE_COST: + increaseCost(ability, game); + break; + case SET_COST: + // do nothing + break; + default: + throw new IllegalArgumentException("Unknown mod type: " + costModificationType); + } + } } diff --git a/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java b/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java index 359188f7fdc..23e71faa203 100644 --- a/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/EarlyTargetCost.java @@ -6,20 +6,11 @@ import mage.players.Player; /** * @author Grath - * Costs which extend this class need to have targets chosen, and those targets must be chosen during 601.2b step. + *

+ * Support 601.2b rules for ealry target choice before X announce and other actions */ -public abstract class EarlyTargetCost extends CostImpl { +public interface EarlyTargetCost { - protected EarlyTargetCost() { - super(); - } + void chooseTarget(Game game, Ability source, Player controller); - protected EarlyTargetCost(final EarlyTargetCost cost) { - super(cost); - } - - @Override - public abstract EarlyTargetCost copy(); - - public abstract void chooseTarget(Game game, Ability source, Player controller); } diff --git a/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java b/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java new file mode 100644 index 00000000000..80b8a1d12ed --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/MinMaxVariableCost.java @@ -0,0 +1,15 @@ +package mage.abilities.costs; + +/** + * @author jayDi85 + */ +public interface MinMaxVariableCost extends VariableCost { + + int getMinX(); + + void setMinX(int minX); + + int getMaxX(); + + void setMaxX(int maxX); +} diff --git a/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java b/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java index 8fc089b12de..ac863a0e819 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/DiscardXTargetCost.java @@ -10,11 +10,20 @@ import mage.players.Player; import mage.target.common.TargetCardInHand; /** + * Used to setup discard cost WITHOUT {X} mana cost + *

+ * If you have {X} in spell's mana cost then use DiscardXCardsCostAdjuster instead + *

+ * Example: + * - {2}{U}{R} + * - As an additional cost to cast this spell, discard X cards. + * * @author LevelX2 */ public class DiscardXTargetCost extends VariableCostImpl { protected FilterCard filter; + protected boolean isRandom = false; public DiscardXTargetCost(FilterCard filter) { this(filter, false); @@ -30,6 +39,12 @@ public class DiscardXTargetCost extends VariableCostImpl { protected DiscardXTargetCost(final DiscardXTargetCost cost) { super(cost); this.filter = cost.filter; + this.isRandom = cost.isRandom; + } + + public DiscardXTargetCost withRandom() { + this.isRandom = true; + return this; } @Override @@ -49,6 +64,6 @@ public class DiscardXTargetCost extends VariableCostImpl { @Override public Cost getFixedCostsFromAnnouncedValue(int xValue) { TargetCardInHand target = new TargetCardInHand(xValue, filter); - return new DiscardTargetCost(target); + return new DiscardTargetCost(target, this.isRandom); } } 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 2b8885471c0..fff0baae6bb 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PayLoyaltyCost.java @@ -35,10 +35,10 @@ public class PayLoyaltyCost extends CostImpl { int loyaltyCost = amount; - // apply cost modification + // apply dynamic costs and cost modification if (ability instanceof LoyaltyAbility) { LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy(); - copiedAbility.adjustCosts(game); + copiedAbility.adjustX(game); game.getContinuousEffects().costModification(copiedAbility, game); loyaltyCost = 0; for (Cost cost : copiedAbility.getCosts()) { diff --git a/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java b/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java index 42bca28413b..4806e359f85 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/PayVariableLoyaltyCost.java @@ -59,10 +59,10 @@ public class PayVariableLoyaltyCost extends VariableCostImpl { int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY); - // apply cost modification + // apply dynamic costs and cost modification if (source instanceof LoyaltyAbility) { LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy(); - copiedAbility.adjustCosts(game); + copiedAbility.adjustX(game); game.getContinuousEffects().costModification(copiedAbility, game); for (Cost cost : copiedAbility.getCosts()) { if (cost instanceof PayVariableLoyaltyCost) { diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java index cd018459433..d168aa46850 100644 --- a/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/CommanderManaValueAdjuster.java @@ -13,7 +13,7 @@ public enum CommanderManaValueAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, GreatestCommanderManaValue.instance.calculate(game, ability, null)); } } diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java new file mode 100644 index 00000000000..302d4d7e354 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/DiscardXCardsCostAdjuster.java @@ -0,0 +1,73 @@ +package mage.abilities.costs.costadjusters; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.CostAdjuster; +import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.effects.common.InfoEffect; +import mage.cards.Card; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInHand; +import mage.util.CardUtil; + +/** + * Used to setup discard cost with {X} mana cost + *

+ * If you don't have {X} then use DiscardXTargetCost instead + *

+ * Example: + * - {X}{1}{B} + * - As an additional cost to cast this spell, discard X cards. + * + * @author JayDi85 + */ +public class DiscardXCardsCostAdjuster implements CostAdjuster { + + private final FilterCard filter; + + private DiscardXCardsCostAdjuster(FilterCard filter) { + this.filter = filter; + } + + @Override + public void prepareX(Ability ability, Game game) { + Player controller = game.getPlayer(ability.getControllerId()); + if (controller == null) { + return; + } + + int minX = 0; + int maxX = controller.getHand().getCards(this.filter, ability.getControllerId(), ability, game).size(); + ability.setVariableCostsMinMax(minX, maxX); + } + + @Override + public void prepareCost(Ability ability, Game game) { + int x = CardUtil.getSourceCostsTagX(game, ability, -1); + if (x >= 0) { + ability.addCost(new DiscardTargetCost(new TargetCardInHand(x, x, this.filter))); + } + } + + public static void addAdjusterAndMessage(Card card, FilterCard filter) { + addAdjusterAndMessage(card, filter, false); + } + + public static void addAdjusterAndMessage(Card card, FilterCard filter, boolean isRandom) { + if (card.getSpellAbility().getManaCosts().getVariableCosts().isEmpty()) { + // how to fix: use DiscardXTargetCost + throw new IllegalArgumentException("Wrong code usage: that's cost adjuster must be used with {X} in mana costs only - " + card); + } + + Ability ability = new SimpleStaticAbility( + Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X " + filter.getMessage()) + ); + ability.setRuleAtTheTop(true); + card.addAbility(ability); + + card.getSpellAbility().setCostAdjuster(new DiscardXCardsCostAdjuster(filter)); + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java index 37cf5e704a2..5037eedb47d 100644 --- a/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/DomainAdjuster.java @@ -13,7 +13,7 @@ public enum DomainAdjuster implements CostAdjuster { instance; @Override - public void adjustCosts(Ability ability, Game game) { + public void reduceCost(Ability ability, Game game) { CardUtil.reduceCost(ability, DomainValue.REGULAR.calculate(game, ability, null)); } } diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java index 742e315efcb..9d9d287eae2 100644 --- a/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/ExileCardsFromHandAdjuster.java @@ -25,22 +25,29 @@ public class ExileCardsFromHandAdjuster implements CostAdjuster { } @Override - public void adjustCosts(Ability ability, Game game) { - if (game.inCheckPlayableState()) { - return; - } + public void reduceCost(Ability ability, Game game) { Player player = game.getPlayer(ability.getControllerId()); if (player == null) { return; } + int cardCount = player.getHand().count(filter, game); - int toExile = cardCount > 0 ? player.getAmount( - 0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game - ) : 0; - if (toExile > 0) { - ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter))); - CardUtil.reduceCost(ability, 2 * toExile); + int reduceCount; + if (game.inCheckPlayableState()) { + // possible + reduceCount = 2 * cardCount; + } else { + // real - need to choose + // TODO: need early target cost instead dialog here + int toExile = cardCount == 0 ? 0 : player.getAmount( + 0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game + ); + reduceCount = 2 * toExile; + if (toExile > 0) { + ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter))); + } } + CardUtil.reduceCost(ability, 2 * reduceCount); } public static final void addAdjusterAndMessage(Card card, FilterCard filter) { diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java new file mode 100644 index 00000000000..76ac9ca2a0a --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/ImprintedManaValueXCostAdjuster.java @@ -0,0 +1,37 @@ +package mage.abilities.costs.costadjusters; + +import mage.abilities.Ability; +import mage.abilities.costs.CostAdjuster; +import mage.cards.Card; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * Used for {X} mana cost that must be replaced by imprinted mana value + *

+ * Example: + * - Elite Arcanist + * - {X}, {T}: Copy the exiled card. ... X is the converted mana cost of the exiled card. + * + * @author JayDi85 + */ +public enum ImprintedManaValueXCostAdjuster implements CostAdjuster { + instance; + + @Override + public void prepareX(Ability ability, Game game) { + int manaValue = Integer.MAX_VALUE; + + Permanent sourcePermanent = game.getPermanent(ability.getSourceId()); + if (sourcePermanent != null + && sourcePermanent.getImprinted() != null + && !sourcePermanent.getImprinted().isEmpty()) { + Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0)); + if (imprintedInstant != null) { + manaValue = imprintedInstant.getManaValue(); + } + } + + ability.setVariableCostsValue(manaValue); + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java index cd2406ed5f3..3d4f08fc785 100644 --- a/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/costadjusters/LegendaryCreatureCostAdjuster.java @@ -29,10 +29,8 @@ public enum LegendaryCreatureCostAdjuster implements CostAdjuster { ); @Override - public void adjustCosts(Ability ability, Game game) { - int count = game.getBattlefield().count( - filter, ability.getControllerId(), ability, game - ); + public void reduceCost(Ability ability, Game game) { + int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game); if (count > 0) { CardUtil.reduceCost(ability, count); } diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java index b0d51dca448..f72b7fa0a86 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCost.java @@ -32,7 +32,7 @@ public interface ManaCost extends Cost { ManaOptions getOptions(); /** - * Return all options for paying the mana cost (this) while taking into accoutn if the player can pay life. + * Return all options for paying the mana cost (this) while taking into account if the player can pay life. * Used to correctly highlight (or not) spells with Phyrexian mana depending on if the player can pay life costs. *

* E.g. Tezzeret's Gambit has a cost of {3}{U/P}. diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java index 1fd9bdd5f90..c557d260326 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostImpl.java @@ -72,7 +72,7 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost { } @Override - public ManaOptions getOptions() { + public final ManaOptions getOptions() { return getOptions(true); } diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index 118379ccef8..70ba8044ff5 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -439,7 +439,7 @@ public class ManaCostsImpl extends ArrayList implements M this.add(new ColoredManaCost(ColoredManaSymbol.lookup(symbol.charAt(0)))); } else // check X wasn't added before if (modifierForX == 0) { - // count X occurence + // count X occurrence for (String s : symbols) { if (s.equals("X")) { modifierForX++; diff --git a/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java b/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java index d2fa2b16f75..8a8d3e25a00 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/VariableManaCost.java @@ -3,17 +3,19 @@ package mage.abilities.costs.mana; import mage.Mana; import mage.abilities.Ability; import mage.abilities.costs.Cost; -import mage.abilities.costs.VariableCost; +import mage.abilities.costs.MinMaxVariableCost; import mage.abilities.costs.VariableCostType; +import mage.abilities.mana.ManaOptions; import mage.constants.ColoredManaSymbol; import mage.filter.FilterMana; import mage.game.Game; import mage.players.ManaPool; +import mage.util.CardUtil; /** * @author BetaSteward_at_googlemail.com, JayDi85 */ -public final class VariableManaCost extends ManaCostImpl implements VariableCost { +public class VariableManaCost extends ManaCostImpl implements MinMaxVariableCost { // variable mana cost usage on 2019-06-20: // 1. as X value in spell/ability cast (announce X, set VariableManaCost as paid and add generic mana to pay instead) @@ -23,7 +25,7 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost protected int xInstancesCount; // number of {X} instances in cost like {X} or {X}{X} protected int xValue = 0; // final X value after announce and replace events protected int xPay = 0; // final/total need pay after announce and replace events (example: {X}{X}, X=3, xPay = 6) - protected boolean wasAnnounced = false; + protected boolean wasAnnounced = false; // X was announced by player or auto-defined by CostAdjuster protected FilterMana filter; // mana filter that can be used for that cost protected int minX = 0; @@ -59,6 +61,18 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost return 0; } + @Override + public ManaOptions getOptions(boolean canPayLifeCost) { + ManaOptions res = new ManaOptions(); + + // limit mana options for better performance + CardUtil.distributeValues(10, getMinX(), getMaxX()).forEach(value -> { + res.add(Mana.GenericMana(value)); + }); + + return res; + } + @Override public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) { // X mana cost always pays as generic mana @@ -86,6 +100,10 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost return this.isColorlessPaid(xPay); } + public boolean wasAnnounced() { + return this.wasAnnounced; + } + @Override public VariableManaCost getUnpaid() { return this; @@ -122,18 +140,22 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost return this.xInstancesCount; } + @Override public int getMinX() { return minX; } + @Override public void setMinX(int minX) { this.minX = minX; } + @Override public int getMaxX() { return maxX; } + @Override public void setMaxX(int maxX) { this.maxX = maxX; } diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java index ea8cde0572d..a7fe0661157 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java @@ -3,11 +3,27 @@ package mage.abilities.dynamicvalue.common; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; import mage.game.Game; import mage.players.Player; public enum CardsInControllerHandCount implements DynamicValue { - instance; + + instance(StaticFilters.FILTER_CARD_CARDS), // TODO: replace usage to ANY + ANY(StaticFilters.FILTER_CARD_CARDS), + CREATURES(StaticFilters.FILTER_CARD_CREATURES), + LANDS(StaticFilters.FILTER_CARD_LANDS); + + FilterCard filter; + ValueHint hint; + + CardsInControllerHandCount(FilterCard filter) { + this.filter = filter; + this.hint = new ValueHint(filter.getMessage() + " in your hand", this); + } @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { @@ -22,16 +38,20 @@ public enum CardsInControllerHandCount implements DynamicValue { @Override public CardsInControllerHandCount copy() { - return CardsInControllerHandCount.instance; + return this; } @Override public String getMessage() { - return "cards in your hand"; + return this.filter.getMessage() + " in your hand"; } @Override public String toString() { return "1"; } + + public Hint getHint() { + return this.hint; + } } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 90e911e06c8..a60322ba64a 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -688,6 +688,8 @@ public class ContinuousEffects implements Serializable { * the battlefield for * {@link CostModificationEffect cost modification effects} and applies them * if necessary. + *

+ * Warning, don't forget to call ability.adjustX before any cost modifications * * @param abilityToModify * @param game @@ -695,6 +697,10 @@ public class ContinuousEffects implements Serializable { public void costModification(Ability abilityToModify, Game game) { List costEffects = getApplicableCostModificationEffects(game); + // add dynamic costs from X and other places + abilityToModify.adjustCostsPrepare(game); + + abilityToModify.adjustCostsModify(game, CostModificationType.INCREASE_COST); for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.INCREASE_COST) { Set abilities = costModificationEffects.getAbility(effect.getId()); @@ -706,6 +712,7 @@ public class ContinuousEffects implements Serializable { } } + abilityToModify.adjustCostsModify(game, CostModificationType.REDUCE_COST); for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.REDUCE_COST) { Set abilities = costModificationEffects.getAbility(effect.getId()); @@ -717,6 +724,7 @@ public class ContinuousEffects implements Serializable { } } + abilityToModify.adjustCostsModify(game, CostModificationType.SET_COST); for (CostModificationEffect effect : costEffects) { if (effect.getModificationType() == CostModificationType.SET_COST) { Set abilities = costModificationEffects.getAbility(effect.getId()); diff --git a/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java b/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java index e8dc09bd813..7cacad39c33 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/discard/LookTargetHandChooseDiscardEffect.java @@ -62,6 +62,9 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect { } TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter); if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) { + // TODO: must fizzle discard effect on not full choice + // - tests: affected (allow to choose and discard 1 instead 2) + // - real game: need to check player.discard(new CardsImpl(target.getTargets()), false, source, game); } return true; diff --git a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java index 94972323c83..72d5cbcbde4 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java @@ -132,6 +132,8 @@ public class SuspendAbility extends SpecialAction { this.addEffect(new SuspendExileEffect(suspend)); this.usesStack = false; if (suspend == Integer.MAX_VALUE) { + // example: Suspend X-{X}{W}{W}. X can't be 0. + // TODO: replace by costAdjuster for shared logic VariableManaCost xCosts = new VariableManaCost(VariableCostType.ALTERNATIVE); xCosts.setMinX(1); this.addCost(xCosts); diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index 4f9244234ba..34fb4333ab9 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -426,6 +426,16 @@ public class StackAbility extends StackObjectImpl implements Ability { // Do nothing } + @Override + public void setVariableCostsMinMax(int min, int max) { + ability.setVariableCostsMinMax(min, max); + } + + @Override + public void setVariableCostsValue(int xValue) { + ability.setVariableCostsValue(xValue); + } + @Override public Map getCostsTagMap() { return ability.getCostsTagMap(); @@ -778,14 +788,23 @@ public class StackAbility extends StackObjectImpl implements Ability { } @Override - public CostAdjuster getCostAdjuster() { - return costAdjuster; + public void adjustX(Game game) { + if (costAdjuster != null) { + costAdjuster.prepareX(this, game); + } } @Override - public void adjustCosts(Game game) { + public void adjustCostsPrepare(Game game) { if (costAdjuster != null) { - costAdjuster.adjustCosts(this, game); + costAdjuster.prepareCost(this, game); + } + } + + @Override + public void adjustCostsModify(Game game, CostModificationType costModificationType) { + if (costAdjuster != null) { + costAdjuster.modifyCost(this, game, costModificationType); } } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index c6c7d14039c..987b47e37f8 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -743,10 +743,14 @@ public interface Player extends MageItem, Copyable { boolean shuffleCardsToLibrary(Card card, Game game, Ability source); - // set the value for X mana spells and abilities + /** + * Set the value for X mana spells and abilities + */ int announceXMana(int min, int max, String message, Game game, Ability ability); - // set the value for non mana X costs + /** + * Set the value for non mana X costs + */ int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost); // TODO: rework to use pair's list of effect + ability instead string's map diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 0132cc7810c..88e36b7c3fb 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3679,8 +3679,11 @@ public abstract class PlayerImpl implements Player, Serializable { if (!copy.canActivate(playerId, game).canActivate()) { return false; } + + // apply dynamic costs and cost modification + copy.adjustX(game); if (availableMana != null) { - copy.adjustCosts(game); + // TODO: need research, why it look at availableMana here - can delete condition? game.getContinuousEffects().costModification(copy, game); } boolean canBeCastRegularly = true; @@ -3891,7 +3894,8 @@ public abstract class PlayerImpl implements Player, Serializable { copyAbility = ability.copy(); copyAbility.clearManaCostsToPay(); copyAbility.addManaCostsToPay(manaCosts.copy()); - copyAbility.adjustCosts(game); + // apply dynamic costs and cost modification + copyAbility.adjustX(game); game.getContinuousEffects().costModification(copyAbility, game); // reduced all cost @@ -3963,12 +3967,9 @@ public abstract class PlayerImpl implements Player, Serializable { // alternative cost reduce copyAbility = ability.copy(); copyAbility.clearManaCostsToPay(); - // TODO: IDE warning: - // Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to - // 'java.util.Collection'. - // Reason: 'manaCosts' has raw type, so result of copy is erased copyAbility.addManaCostsToPay(manaCosts.copy()); - copyAbility.adjustCosts(game); + // apply dynamic costs and cost modification + copyAbility.adjustX(game); game.getContinuousEffects().costModification(copyAbility, game); // reduced all cost diff --git a/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java b/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java index c84fbd949d5..c8ffbea7271 100644 --- a/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java +++ b/Mage/src/main/java/mage/target/targetadjustment/TargetsCountAdjuster.java @@ -25,8 +25,13 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster { @Override public void adjustTargets(Ability ability, Game game) { - Target newTarget = blueprintTarget.copy(); int count = dynamicValue.calculate(game, ability, ability.getEffects().get(0)); + ability.getTargets().clear(); + if (count <= 0) { + return; + } + + Target newTarget = blueprintTarget.copy(); newTarget.setMaxNumberOfTargets(count); Filter filter = newTarget.getFilter(); if (blueprintTarget.getMinNumberOfTargets() != 0) { @@ -35,7 +40,6 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster { } else { newTarget.withTargetName(filter.getMessage() + " (up to " + count + " targets)"); } - ability.getTargets().clear(); ability.addTarget(newTarget); } } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index ee24d673186..4356de3d90f 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1079,6 +1079,53 @@ public final class CardUtil { .collect(Collectors.toSet()); } + public static Set getAllPossibleTargets(Cost cost, Game game, Ability source) { + return cost.getTargets() + .stream() + .map(t -> t.possibleTargets(source.getControllerId(), source, game)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Distribute values between min and max and make sure that the values will be evenly distributed + * Use it to limit possible values list like mana options + */ + public static List distributeValues(int count, int min, int max) { + List res = new ArrayList<>(); + if (count <= 0 || min > max) { + return res; + } + + if (min == max) { + res.add(min); + return res; + } + + int range = max - min + 1; + + // low possible amount + if (range <= count) { + for (int i = 0; i < range; i++) { + res.add(min + i); + } + return res; + } + + // big possible amount, so skip some values + double step = (double) (max - min) / (count - 1); + for (int i = 0; i < count; i++) { + res.add(min + (int) Math.round(i * step)); + } + // make sure first and last elements are good + res.set(0, min); + if (res.size() > 1) { + res.set(res.size() - 1, max); + } + + return res; + } + /** * For finding the spell or ability on the stack for "becomes the target" triggers. * @@ -1908,6 +1955,10 @@ public final class CardUtil { return defaultValue; } + public static int getSourceCostsTagX(Game game, Ability source, int defaultValue) { + return getSourceCostsTag(game, source, "X", defaultValue); + } + public static String addCostVerb(String text) { if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) { return text; From 36cec8961e6d2ab17c40cab74d96594ae67e98a6 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 8 Apr 2025 22:42:31 +0400 Subject: [PATCH 062/133] refactor: better naming in CardsInControllerHandCount --- Mage.Sets/src/mage/cards/a/AeonChronicler.java | 2 +- Mage.Sets/src/mage/cards/a/AlandraSkyDreamer.java | 8 ++++---- Mage.Sets/src/mage/cards/a/AncientExcavation.java | 2 +- Mage.Sets/src/mage/cards/b/BaldinCenturyHerdmaster.java | 2 +- Mage.Sets/src/mage/cards/b/BodyOfKnowledge.java | 2 +- Mage.Sets/src/mage/cards/c/CastleLocthwain.java | 2 +- Mage.Sets/src/mage/cards/d/DamiaSageOfStone.java | 3 +-- Mage.Sets/src/mage/cards/d/DemonicLore.java | 2 +- Mage.Sets/src/mage/cards/d/DescendantOfSoramaro.java | 3 +-- Mage.Sets/src/mage/cards/d/DivinersPortent.java | 2 +- Mage.Sets/src/mage/cards/d/DreadSlag.java | 3 +-- Mage.Sets/src/mage/cards/d/DugganPrivateDetective.java | 2 +- Mage.Sets/src/mage/cards/e/EmpyrialArmor.java | 3 +-- Mage.Sets/src/mage/cards/e/EmpyrialPlate.java | 3 +-- Mage.Sets/src/mage/cards/e/EndlessSwarm.java | 2 +- Mage.Sets/src/mage/cards/f/FatefulShowdown.java | 2 +- Mage.Sets/src/mage/cards/f/FiresOfVictory.java | 2 +- Mage.Sets/src/mage/cards/g/GeralfsMasterpiece.java | 3 +-- Mage.Sets/src/mage/cards/g/GerrardsWisdom.java | 2 +- Mage.Sets/src/mage/cards/g/GrimStrider.java | 3 +-- Mage.Sets/src/mage/cards/h/HandOfVecna.java | 2 +- Mage.Sets/src/mage/cards/i/InnerCalmOuterStrength.java | 2 +- Mage.Sets/src/mage/cards/i/InnerFire.java | 2 +- Mage.Sets/src/mage/cards/j/JolraelMwonvuliRecluse.java | 2 +- Mage.Sets/src/mage/cards/j/JushiApprentice.java | 3 +-- Mage.Sets/src/mage/cards/k/KagemaroFirstToSuffer.java | 2 +- Mage.Sets/src/mage/cards/k/KagemarosClutch.java | 3 +-- Mage.Sets/src/mage/cards/k/KitsuneLoreweaver.java | 3 +-- Mage.Sets/src/mage/cards/k/KiyomaroFirstToStand.java | 2 +- Mage.Sets/src/mage/cards/l/LeonardoDaVinci.java | 2 +- Mage.Sets/src/mage/cards/m/Malignus.java | 2 +- Mage.Sets/src/mage/cards/m/ManifestationSage.java | 2 +- Mage.Sets/src/mage/cards/m/Maro.java | 2 +- Mage.Sets/src/mage/cards/m/MasterTheWay.java | 2 +- Mage.Sets/src/mage/cards/m/MasumaroFirstToLive.java | 2 +- Mage.Sets/src/mage/cards/m/MeishinTheMindCage.java | 3 +-- Mage.Sets/src/mage/cards/m/MinasTirithGarrison.java | 2 +- Mage.Sets/src/mage/cards/n/NightmarishEnd.java | 2 +- Mage.Sets/src/mage/cards/o/OboroEnvoy.java | 2 +- Mage.Sets/src/mage/cards/o/OverbeingOfMyth.java | 3 +-- Mage.Sets/src/mage/cards/p/PresenceOfTheWise.java | 2 +- Mage.Sets/src/mage/cards/p/PsychosisCrawler.java | 2 +- Mage.Sets/src/mage/cards/r/RalsStaticaster.java | 2 +- Mage.Sets/src/mage/cards/r/RobobrainWarMind.java | 2 +- Mage.Sets/src/mage/cards/s/SageOfAncientLore.java | 2 +- Mage.Sets/src/mage/cards/s/SeaGateRestoration.java | 2 +- Mage.Sets/src/mage/cards/s/SkirgeFamiliar.java | 2 +- Mage.Sets/src/mage/cards/s/SokenzanSpellblade.java | 3 +-- Mage.Sets/src/mage/cards/s/SophicCentaur.java | 3 +-- Mage.Sets/src/mage/cards/s/SoramaroFirstToDream.java | 2 +- Mage.Sets/src/mage/cards/s/SpiralingEmbers.java | 2 +- Mage.Sets/src/mage/cards/s/SpontaneousGeneration.java | 2 +- Mage.Sets/src/mage/cards/s/StingerbackTerror.java | 2 +- Mage.Sets/src/mage/cards/s/Sturmgeist.java | 2 +- Mage.Sets/src/mage/cards/s/SunbringersTouch.java | 2 +- Mage.Sets/src/mage/cards/s/SwordOfWarAndPeace.java | 2 +- Mage.Sets/src/mage/cards/s/SylvanYeti.java | 2 +- Mage.Sets/src/mage/cards/s/SyrElenoraTheDiscerning.java | 2 +- Mage.Sets/src/mage/cards/t/TheArchimandrite.java | 2 +- Mage.Sets/src/mage/cards/t/TheGreatSynthesis.java | 2 +- Mage.Sets/src/mage/cards/t/TheRoyalScions.java | 2 +- Mage.Sets/src/mage/cards/t/TishanaVoiceOfThunder.java | 2 +- Mage.Sets/src/mage/cards/u/UnionOfTheThirdPath.java | 2 +- Mage.Sets/src/mage/cards/v/VensersJournal.java | 3 +-- Mage.Sets/src/mage/cards/v/VisionOfTheUnspeakable.java | 4 ++-- Mage.Sets/src/mage/cards/w/WarpedPhysique.java | 4 ++-- .../dynamicvalue/common/CardsInControllerHandCount.java | 1 - 67 files changed, 71 insertions(+), 87 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AeonChronicler.java b/Mage.Sets/src/mage/cards/a/AeonChronicler.java index b18938e90a4..291b1c7af64 100644 --- a/Mage.Sets/src/mage/cards/a/AeonChronicler.java +++ b/Mage.Sets/src/mage/cards/a/AeonChronicler.java @@ -31,7 +31,7 @@ public final class AeonChronicler extends CardImpl { this.toughness = new MageInt(0); // Aeon Chronicler's power and toughness are each equal to the number of cards in your hand. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.instance))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.ANY))); // Suspend X-{X}{3}{U}. X can't be 0. this.addAbility(new SuspendAbility(Integer.MAX_VALUE, new ManaCostsImpl<>("{3}{U}"), this, true)); diff --git a/Mage.Sets/src/mage/cards/a/AlandraSkyDreamer.java b/Mage.Sets/src/mage/cards/a/AlandraSkyDreamer.java index 520be3501a2..a78555c9360 100644 --- a/Mage.Sets/src/mage/cards/a/AlandraSkyDreamer.java +++ b/Mage.Sets/src/mage/cards/a/AlandraSkyDreamer.java @@ -47,8 +47,8 @@ public final class AlandraSkyDreamer extends CardImpl { // Whenever you draw your fifth card each turn, Alandra, Sky Dreamer and Drakes you control each get +X/+X until end of turn, where X is the number of cards in your hand. DrawNthCardTriggeredAbility drawNthCardTriggeredAbility = new DrawNthCardTriggeredAbility( new BoostSourceEffect( - CardsInControllerHandCount.instance, - CardsInControllerHandCount.instance, + CardsInControllerHandCount.ANY, + CardsInControllerHandCount.ANY, Duration.EndOfTurn ).setText("{this}"), false, @@ -56,8 +56,8 @@ public final class AlandraSkyDreamer extends CardImpl { ); drawNthCardTriggeredAbility.addEffect( new BoostControlledEffect( - CardsInControllerHandCount.instance, - CardsInControllerHandCount.instance, + CardsInControllerHandCount.ANY, + CardsInControllerHandCount.ANY, Duration.EndOfTurn, filter, false diff --git a/Mage.Sets/src/mage/cards/a/AncientExcavation.java b/Mage.Sets/src/mage/cards/a/AncientExcavation.java index 9feb0e5b44a..b8f990713ab 100644 --- a/Mage.Sets/src/mage/cards/a/AncientExcavation.java +++ b/Mage.Sets/src/mage/cards/a/AncientExcavation.java @@ -61,7 +61,7 @@ class AncientExcavationEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); if (player != null) { - DynamicValue numCards = CardsInControllerHandCount.instance; + DynamicValue numCards = CardsInControllerHandCount.ANY; int amount = numCards.calculate(game, source, this); int cardsDrawn = player.drawCards(amount, source, game); player.discard(cardsDrawn, false, false, source, game); diff --git a/Mage.Sets/src/mage/cards/b/BaldinCenturyHerdmaster.java b/Mage.Sets/src/mage/cards/b/BaldinCenturyHerdmaster.java index 0a545ef0130..544aac236fa 100644 --- a/Mage.Sets/src/mage/cards/b/BaldinCenturyHerdmaster.java +++ b/Mage.Sets/src/mage/cards/b/BaldinCenturyHerdmaster.java @@ -43,7 +43,7 @@ public final class BaldinCenturyHerdmaster extends CardImpl { // Whenever Baldin, Century Herdmaster attacks, up to one hundred target creatures each get +0/+X until end of turn, where X is the number of cards in your hand. Ability ability = new AttacksTriggeredAbility(new BoostTargetEffect( - StaticValue.get(0), CardsInControllerHandCount.instance, Duration.EndOfTurn + StaticValue.get(0), CardsInControllerHandCount.ANY, Duration.EndOfTurn ).setText("up to one hundred target creatures each get +0/+X until end of turn, where X is the number of cards in your hand")); ability.addTarget(new TargetCreaturePermanent(0, 100)); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/b/BodyOfKnowledge.java b/Mage.Sets/src/mage/cards/b/BodyOfKnowledge.java index 675a461e032..34e37cfb6b8 100644 --- a/Mage.Sets/src/mage/cards/b/BodyOfKnowledge.java +++ b/Mage.Sets/src/mage/cards/b/BodyOfKnowledge.java @@ -33,7 +33,7 @@ public final class BodyOfKnowledge extends CardImpl { this.addAbility(new SimpleStaticAbility( Zone.ALL, new SetBasePowerToughnessSourceEffect( - CardsInControllerHandCount.instance + CardsInControllerHandCount.ANY ) )); diff --git a/Mage.Sets/src/mage/cards/c/CastleLocthwain.java b/Mage.Sets/src/mage/cards/c/CastleLocthwain.java index 4f11e611bba..7fa016e86e8 100644 --- a/Mage.Sets/src/mage/cards/c/CastleLocthwain.java +++ b/Mage.Sets/src/mage/cards/c/CastleLocthwain.java @@ -40,7 +40,7 @@ public final class CastleLocthwain extends CardImpl { Ability ability = new SimpleActivatedAbility( new DrawCardSourceControllerEffect(1).setText("draw a card,"), new ManaCostsImpl<>("{1}{B}{B}") ); - ability.addEffect(new LoseLifeSourceControllerEffect(CardsInControllerHandCount.instance) + ability.addEffect(new LoseLifeSourceControllerEffect(CardsInControllerHandCount.ANY) .setText("then you lose life equal to the number of cards in your hand")); ability.addCost(new TapSourceCost()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/d/DamiaSageOfStone.java b/Mage.Sets/src/mage/cards/d/DamiaSageOfStone.java index e689e8989a8..effdfb78226 100644 --- a/Mage.Sets/src/mage/cards/d/DamiaSageOfStone.java +++ b/Mage.Sets/src/mage/cards/d/DamiaSageOfStone.java @@ -17,7 +17,6 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; import mage.constants.TargetController; -import mage.constants.Zone; import mage.game.Game; import mage.players.Player; @@ -59,7 +58,7 @@ public final class DamiaSageOfStone extends CardImpl { class DamiaSageOfStoneTriggeredAbility extends BeginningOfUpkeepTriggeredAbility { DamiaSageOfStoneTriggeredAbility() { - super(TargetController.YOU, new DrawCardSourceControllerEffect(new IntPlusDynamicValue(7, new MultipliedValue(CardsInControllerHandCount.instance, -1))), false); + super(TargetController.YOU, new DrawCardSourceControllerEffect(new IntPlusDynamicValue(7, new MultipliedValue(CardsInControllerHandCount.ANY, -1))), false); } private DamiaSageOfStoneTriggeredAbility(final DamiaSageOfStoneTriggeredAbility ability) { diff --git a/Mage.Sets/src/mage/cards/d/DemonicLore.java b/Mage.Sets/src/mage/cards/d/DemonicLore.java index be9f94ed3da..3cdc30eb088 100644 --- a/Mage.Sets/src/mage/cards/d/DemonicLore.java +++ b/Mage.Sets/src/mage/cards/d/DemonicLore.java @@ -18,7 +18,7 @@ import java.util.UUID; */ public final class DemonicLore extends CardImpl { - private static final DynamicValue xValue = new MultipliedValue(CardsInControllerHandCount.instance, 2); + private static final DynamicValue xValue = new MultipliedValue(CardsInControllerHandCount.ANY, 2); public DemonicLore(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}"); diff --git a/Mage.Sets/src/mage/cards/d/DescendantOfSoramaro.java b/Mage.Sets/src/mage/cards/d/DescendantOfSoramaro.java index 41a9c725d78..7dd093feff4 100644 --- a/Mage.Sets/src/mage/cards/d/DescendantOfSoramaro.java +++ b/Mage.Sets/src/mage/cards/d/DescendantOfSoramaro.java @@ -12,7 +12,6 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; /** * @@ -28,7 +27,7 @@ public final class DescendantOfSoramaro extends CardImpl { this.power = new MageInt(2); this.toughness = new MageInt(3); // {1}{U}: Look at the top X cards of your library, where X is the number of cards in your hand, then put them back in any order. - Effect effect = new LookLibraryControllerEffect(CardsInControllerHandCount.instance); + Effect effect = new LookLibraryControllerEffect(CardsInControllerHandCount.ANY); effect.setText("Look at the top X cards of your library, where X is the number of cards in your hand, then put them back in any order"); this.addAbility(new SimpleActivatedAbility( effect, new ManaCostsImpl<>("{1}{U}"))); diff --git a/Mage.Sets/src/mage/cards/d/DivinersPortent.java b/Mage.Sets/src/mage/cards/d/DivinersPortent.java index 0795197206e..4efb58a46f3 100644 --- a/Mage.Sets/src/mage/cards/d/DivinersPortent.java +++ b/Mage.Sets/src/mage/cards/d/DivinersPortent.java @@ -27,7 +27,7 @@ public final class DivinersPortent extends CardImpl { // Roll a d20 and add the number of cards in your hand. RollDieWithResultTableEffect effect = new RollDieWithResultTableEffect( 20, "roll a d20 and add the number " + - "of cards in your hand", CardsInControllerHandCount.instance, 0 + "of cards in your hand", CardsInControllerHandCount.ANY, 0 ); this.getSpellAbility().addEffect(effect); diff --git a/Mage.Sets/src/mage/cards/d/DreadSlag.java b/Mage.Sets/src/mage/cards/d/DreadSlag.java index ac89d84771f..90704bab302 100644 --- a/Mage.Sets/src/mage/cards/d/DreadSlag.java +++ b/Mage.Sets/src/mage/cards/d/DreadSlag.java @@ -15,7 +15,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @@ -32,7 +31,7 @@ public final class DreadSlag extends CardImpl { // Trample this.addAbility(TrampleAbility.getInstance()); // Dread Slag gets -4/-4 for each card in your hand. - DynamicValue amount = new MultipliedValue(CardsInControllerHandCount.instance, -4); + DynamicValue amount = new MultipliedValue(CardsInControllerHandCount.ANY, -4); Effect effect = new BoostSourceEffect(amount, amount, Duration.WhileOnBattlefield); effect.setText("{this} gets -4/-4 for each card in your hand"); this.addAbility(new SimpleStaticAbility(effect)); diff --git a/Mage.Sets/src/mage/cards/d/DugganPrivateDetective.java b/Mage.Sets/src/mage/cards/d/DugganPrivateDetective.java index 0bcd954c207..f2f19b21ccf 100644 --- a/Mage.Sets/src/mage/cards/d/DugganPrivateDetective.java +++ b/Mage.Sets/src/mage/cards/d/DugganPrivateDetective.java @@ -36,7 +36,7 @@ public final class DugganPrivateDetective extends CardImpl { this.toughness = new MageInt(0); // Duggan's power and toughness are each equal to the number of cards in your hand. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.instance))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.ANY))); // Whenever Duggan enters the battlefield or attacks, investigate. this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new InvestigateEffect().setText("investigate"))); diff --git a/Mage.Sets/src/mage/cards/e/EmpyrialArmor.java b/Mage.Sets/src/mage/cards/e/EmpyrialArmor.java index 8ddd1e03472..333e3294023 100644 --- a/Mage.Sets/src/mage/cards/e/EmpyrialArmor.java +++ b/Mage.Sets/src/mage/cards/e/EmpyrialArmor.java @@ -15,7 +15,6 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; @@ -38,7 +37,7 @@ public final class EmpyrialArmor extends CardImpl { this.addAbility(ability); // Enchanted creature gets +1/+1 for each card in your hand. - DynamicValue xValue = CardsInControllerHandCount.instance; + DynamicValue xValue = CardsInControllerHandCount.ANY; this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(xValue, xValue, Duration.WhileOnBattlefield))); } diff --git a/Mage.Sets/src/mage/cards/e/EmpyrialPlate.java b/Mage.Sets/src/mage/cards/e/EmpyrialPlate.java index 8437ac9dbed..0010967bbc3 100644 --- a/Mage.Sets/src/mage/cards/e/EmpyrialPlate.java +++ b/Mage.Sets/src/mage/cards/e/EmpyrialPlate.java @@ -12,7 +12,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Outcome; -import mage.constants.Zone; /** * @@ -25,7 +24,7 @@ public final class EmpyrialPlate extends CardImpl { this.subtype.add(SubType.EQUIPMENT); // Equipped creature gets +1/+1 for each card in your hand. - this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(CardsInControllerHandCount.instance, CardsInControllerHandCount.instance))); + this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(CardsInControllerHandCount.ANY, CardsInControllerHandCount.ANY))); // Equip {2} this.addAbility(new EquipAbility(Outcome.BoostCreature, new GenericManaCost(2), false)); diff --git a/Mage.Sets/src/mage/cards/e/EndlessSwarm.java b/Mage.Sets/src/mage/cards/e/EndlessSwarm.java index 230b1379a56..acb59f93f31 100644 --- a/Mage.Sets/src/mage/cards/e/EndlessSwarm.java +++ b/Mage.Sets/src/mage/cards/e/EndlessSwarm.java @@ -22,7 +22,7 @@ public final class EndlessSwarm extends CardImpl { // Create a 1/1 green Snake creature token for each card in your hand. - this.getSpellAbility().addEffect(new CreateTokenEffect(new SnakeToken(), CardsInControllerHandCount.instance).setText("create a 1/1 green Snake creature token for each card in your hand")); + this.getSpellAbility().addEffect(new CreateTokenEffect(new SnakeToken(), CardsInControllerHandCount.ANY).setText("create a 1/1 green Snake creature token for each card in your hand")); // Epic this.getSpellAbility().addEffect(new EpicEffect()); diff --git a/Mage.Sets/src/mage/cards/f/FatefulShowdown.java b/Mage.Sets/src/mage/cards/f/FatefulShowdown.java index f6c43b5a40f..f2166040b7f 100644 --- a/Mage.Sets/src/mage/cards/f/FatefulShowdown.java +++ b/Mage.Sets/src/mage/cards/f/FatefulShowdown.java @@ -21,7 +21,7 @@ public final class FatefulShowdown extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{2}{R}{R}"); // Fateful Showdown deals damage to any target equal to the number of cards in your hand. Discard all the cards in your hand, then draw that many cards. - Effect effect = new DamageTargetEffect(CardsInControllerHandCount.instance); + Effect effect = new DamageTargetEffect(CardsInControllerHandCount.ANY); effect.setText("{this} deals damage to any target equal to the number of cards in your hand"); this.getSpellAbility().addEffect(effect); this.getSpellAbility().addTarget(new TargetAnyTarget()); diff --git a/Mage.Sets/src/mage/cards/f/FiresOfVictory.java b/Mage.Sets/src/mage/cards/f/FiresOfVictory.java index 7b7fe0c75a7..d2c5b87c2af 100644 --- a/Mage.Sets/src/mage/cards/f/FiresOfVictory.java +++ b/Mage.Sets/src/mage/cards/f/FiresOfVictory.java @@ -31,7 +31,7 @@ public final class FiresOfVictory extends CardImpl { KickedCondition.ONCE, "If this spell was kicked, draw a card." )); - this.getSpellAbility().addEffect(new DamageTargetEffect(CardsInControllerHandCount.instance) + this.getSpellAbility().addEffect(new DamageTargetEffect(CardsInControllerHandCount.ANY) .setText("{this} deals damage to target creature or planeswalker equal to the number of cards in your hand.")); this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker()); } diff --git a/Mage.Sets/src/mage/cards/g/GeralfsMasterpiece.java b/Mage.Sets/src/mage/cards/g/GeralfsMasterpiece.java index 8a9f2d34e33..3cb638e14df 100644 --- a/Mage.Sets/src/mage/cards/g/GeralfsMasterpiece.java +++ b/Mage.Sets/src/mage/cards/g/GeralfsMasterpiece.java @@ -21,7 +21,6 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; import mage.constants.Zone; -import mage.filter.FilterCard; import mage.filter.StaticFilters; import mage.target.common.TargetCardInHand; @@ -42,7 +41,7 @@ public final class GeralfsMasterpiece extends CardImpl { this.addAbility(FlyingAbility.getInstance()); // Geralf's Masterpiece gets -1/-1 for each card in your hand. - DynamicValue count = new SignInversionDynamicValue(CardsInControllerHandCount.instance); + DynamicValue count = new SignInversionDynamicValue(CardsInControllerHandCount.ANY); Effect effect = new BoostSourceEffect(count, count, Duration.WhileOnBattlefield); effect.setText("{this} gets -1/-1 for each card in your hand"); this.addAbility(new SimpleStaticAbility(effect)); diff --git a/Mage.Sets/src/mage/cards/g/GerrardsWisdom.java b/Mage.Sets/src/mage/cards/g/GerrardsWisdom.java index f474ecfb227..aa4008901c2 100644 --- a/Mage.Sets/src/mage/cards/g/GerrardsWisdom.java +++ b/Mage.Sets/src/mage/cards/g/GerrardsWisdom.java @@ -20,7 +20,7 @@ public final class GerrardsWisdom extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{2}{W}{W}"); // You gain 2 life for each card in your hand. - this.getSpellAbility().addEffect(new GainLifeEffect(new MultipliedValue(CardsInControllerHandCount.instance, 2), + this.getSpellAbility().addEffect(new GainLifeEffect(new MultipliedValue(CardsInControllerHandCount.ANY, 2), "You gain 2 life for each card in your hand")); } diff --git a/Mage.Sets/src/mage/cards/g/GrimStrider.java b/Mage.Sets/src/mage/cards/g/GrimStrider.java index d1bea4bd1ee..34fb24c0a18 100644 --- a/Mage.Sets/src/mage/cards/g/GrimStrider.java +++ b/Mage.Sets/src/mage/cards/g/GrimStrider.java @@ -14,7 +14,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @@ -29,7 +28,7 @@ public final class GrimStrider extends CardImpl { this.toughness = new MageInt(6); // Grim Strider gets -1/-1 for each card in your hand. - DynamicValue count = new SignInversionDynamicValue(CardsInControllerHandCount.instance); + DynamicValue count = new SignInversionDynamicValue(CardsInControllerHandCount.ANY); Effect effect = new BoostSourceEffect(count, count, Duration.WhileOnBattlefield); effect.setText("{this} gets -1/-1 for each card in your hand"); this.addAbility(new SimpleStaticAbility(effect)); diff --git a/Mage.Sets/src/mage/cards/h/HandOfVecna.java b/Mage.Sets/src/mage/cards/h/HandOfVecna.java index 34a8024e9db..1e35ab4b8a2 100644 --- a/Mage.Sets/src/mage/cards/h/HandOfVecna.java +++ b/Mage.Sets/src/mage/cards/h/HandOfVecna.java @@ -47,7 +47,7 @@ public final class HandOfVecna extends CardImpl { this.addAbility(new EquipAbility( Outcome.Benefit, new PayLifeCost( - CardsInControllerHandCount.instance, "1 life for each card in your hand"), + CardsInControllerHandCount.ANY, "1 life for each card in your hand"), false )); diff --git a/Mage.Sets/src/mage/cards/i/InnerCalmOuterStrength.java b/Mage.Sets/src/mage/cards/i/InnerCalmOuterStrength.java index a38e294cc14..5d95823f88b 100644 --- a/Mage.Sets/src/mage/cards/i/InnerCalmOuterStrength.java +++ b/Mage.Sets/src/mage/cards/i/InnerCalmOuterStrength.java @@ -24,7 +24,7 @@ public final class InnerCalmOuterStrength extends CardImpl { this.subtype.add(SubType.ARCANE); // Target creature gets +X/+X until end of turn, where X is the number of cards in your hand. - DynamicValue xValue= CardsInControllerHandCount.instance; + DynamicValue xValue= CardsInControllerHandCount.ANY; Effect effect = new BoostTargetEffect(xValue, xValue, Duration.EndOfTurn); effect.setText("Target creature gets +X/+X until end of turn, where X is the number of cards in your hand"); this.getSpellAbility().addEffect(effect); diff --git a/Mage.Sets/src/mage/cards/i/InnerFire.java b/Mage.Sets/src/mage/cards/i/InnerFire.java index f7d9754164b..1e286f7e104 100644 --- a/Mage.Sets/src/mage/cards/i/InnerFire.java +++ b/Mage.Sets/src/mage/cards/i/InnerFire.java @@ -20,7 +20,7 @@ public final class InnerFire extends CardImpl { // Add {R} for each card in your hand. - this.getSpellAbility().addEffect(new DynamicManaEffect(Mana.RedMana(1), CardsInControllerHandCount.instance)); + this.getSpellAbility().addEffect(new DynamicManaEffect(Mana.RedMana(1), CardsInControllerHandCount.ANY)); } private InnerFire(final InnerFire card) { diff --git a/Mage.Sets/src/mage/cards/j/JolraelMwonvuliRecluse.java b/Mage.Sets/src/mage/cards/j/JolraelMwonvuliRecluse.java index 05e8ecc72e3..c3f0d6fc4ad 100644 --- a/Mage.Sets/src/mage/cards/j/JolraelMwonvuliRecluse.java +++ b/Mage.Sets/src/mage/cards/j/JolraelMwonvuliRecluse.java @@ -37,7 +37,7 @@ public final class JolraelMwonvuliRecluse extends CardImpl { // {4}{G}{G}: Until end of turn, creatures you control have base power and toughness X/X, where X is the number of cards in your hand. this.addAbility(new SimpleActivatedAbility(new SetBasePowerToughnessAllEffect( - CardsInControllerHandCount.instance, CardsInControllerHandCount.instance, + CardsInControllerHandCount.ANY, CardsInControllerHandCount.ANY, Duration.EndOfTurn, StaticFilters.FILTER_CONTROLLED_CREATURES ).setText("until end of turn, creatures you control have base power and toughness X/X, " + "where X is the number of cards in your hand"), new ManaCostsImpl<>("{4}{G}{G}"))); diff --git a/Mage.Sets/src/mage/cards/j/JushiApprentice.java b/Mage.Sets/src/mage/cards/j/JushiApprentice.java index d8162a7c2a9..3f6e98d144f 100644 --- a/Mage.Sets/src/mage/cards/j/JushiApprentice.java +++ b/Mage.Sets/src/mage/cards/j/JushiApprentice.java @@ -19,7 +19,6 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.ComparisonType; import mage.constants.SuperType; -import mage.constants.Zone; import mage.game.permanent.token.TokenImpl; import mage.target.TargetPlayer; @@ -70,7 +69,7 @@ class TomoyaTheRevealer extends TokenImpl { toughness = new MageInt(3); // {3}{U}{U},{T} : Target player draws X cards, where X is the number of cards in your hand. - Ability ability = new SimpleActivatedAbility(new DrawCardTargetEffect(CardsInControllerHandCount.instance), new ManaCostsImpl<>("{3}{U}{U}")); + Ability ability = new SimpleActivatedAbility(new DrawCardTargetEffect(CardsInControllerHandCount.ANY), new ManaCostsImpl<>("{3}{U}{U}")); ability.addCost(new TapSourceCost()); ability.addTarget(new TargetPlayer()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/k/KagemaroFirstToSuffer.java b/Mage.Sets/src/mage/cards/k/KagemaroFirstToSuffer.java index 15349f4586f..fc6bb0e6d8a 100644 --- a/Mage.Sets/src/mage/cards/k/KagemaroFirstToSuffer.java +++ b/Mage.Sets/src/mage/cards/k/KagemaroFirstToSuffer.java @@ -37,7 +37,7 @@ public final class KagemaroFirstToSuffer extends CardImpl { this.power = new MageInt(0); this.toughness = new MageInt(0); - DynamicValue xValue = CardsInControllerHandCount.instance; + DynamicValue xValue = CardsInControllerHandCount.ANY; // Kagemaro, First to Suffer's power and toughness are each equal to the number of cards in your hand. this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(xValue))); // {B}, Sacrifice Kagemaro: All creatures get -X/-X until end of turn, where X is the number of cards in your hand. diff --git a/Mage.Sets/src/mage/cards/k/KagemarosClutch.java b/Mage.Sets/src/mage/cards/k/KagemarosClutch.java index e90a6ef1026..e5c94d58fcf 100644 --- a/Mage.Sets/src/mage/cards/k/KagemarosClutch.java +++ b/Mage.Sets/src/mage/cards/k/KagemarosClutch.java @@ -17,7 +17,6 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; @@ -40,7 +39,7 @@ public final class KagemarosClutch extends CardImpl { this.addAbility(ability); // Enchanted creature gets -X/-X, where X is the number of cards in your hand. - DynamicValue xMinusValue = new SignInversionDynamicValue(CardsInControllerHandCount.instance); + DynamicValue xMinusValue = new SignInversionDynamicValue(CardsInControllerHandCount.ANY); Effect effect = new BoostEnchantedEffect(xMinusValue, xMinusValue, Duration.WhileOnBattlefield); effect.setText("Enchanted creature gets -X/-X, where X is the number of cards in your hand"); this.addAbility(new SimpleStaticAbility(effect)); diff --git a/Mage.Sets/src/mage/cards/k/KitsuneLoreweaver.java b/Mage.Sets/src/mage/cards/k/KitsuneLoreweaver.java index 2ac7d9148d3..4aa8eb389a9 100644 --- a/Mage.Sets/src/mage/cards/k/KitsuneLoreweaver.java +++ b/Mage.Sets/src/mage/cards/k/KitsuneLoreweaver.java @@ -14,7 +14,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @@ -30,7 +29,7 @@ public final class KitsuneLoreweaver extends CardImpl { this.toughness = new MageInt(1); // {1}{W}: Kitsune Loreweaver gets +0/+X until end of turn, where X is the number of cards in your hand. - Effect effect = new BoostSourceEffect(StaticValue.get(0), CardsInControllerHandCount.instance, Duration.EndOfTurn); + Effect effect = new BoostSourceEffect(StaticValue.get(0), CardsInControllerHandCount.ANY, Duration.EndOfTurn); effect.setText("{this} gets +0/+X until end of turn, where X is the number of cards in your hand"); this.addAbility(new SimpleActivatedAbility(effect, new ManaCostsImpl<>("{1}{W}"))); } diff --git a/Mage.Sets/src/mage/cards/k/KiyomaroFirstToStand.java b/Mage.Sets/src/mage/cards/k/KiyomaroFirstToStand.java index 48a897d7fd5..dd95e476221 100644 --- a/Mage.Sets/src/mage/cards/k/KiyomaroFirstToStand.java +++ b/Mage.Sets/src/mage/cards/k/KiyomaroFirstToStand.java @@ -31,7 +31,7 @@ public final class KiyomaroFirstToStand extends CardImpl { this.toughness = new MageInt(0); // Kiyomaro, First to Stand's power and toughness are each equal to the number of cards in your hand. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.instance))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.ANY))); // As long as you have four or more cards in hand, Kiyomaro has vigilance. Condition condition = new CardsInHandCondition(ComparisonType.MORE_THAN, 3); diff --git a/Mage.Sets/src/mage/cards/l/LeonardoDaVinci.java b/Mage.Sets/src/mage/cards/l/LeonardoDaVinci.java index e29391f7e08..b21f2ecdb99 100644 --- a/Mage.Sets/src/mage/cards/l/LeonardoDaVinci.java +++ b/Mage.Sets/src/mage/cards/l/LeonardoDaVinci.java @@ -43,7 +43,7 @@ public final class LeonardoDaVinci extends CardImpl { this.power = new MageInt(3); this.toughness = new MageInt(3); - DynamicValue xValue = CardsInControllerHandCount.instance; + DynamicValue xValue = CardsInControllerHandCount.ANY; // {3}{U}{U}: Until end of turn, Thopters you control have base power and toughness X/X, where X is the number of cards in your hand. this.addAbility(new SimpleActivatedAbility(new BoostControlledEffect(xValue, xValue, Duration.EndOfTurn, filter, false).setText( "Until end of turn, Thopters you control have base power and toughness X/X, where X is the number of cards in your hand." diff --git a/Mage.Sets/src/mage/cards/m/Malignus.java b/Mage.Sets/src/mage/cards/m/Malignus.java index a0d51a95248..e8381a6154a 100644 --- a/Mage.Sets/src/mage/cards/m/Malignus.java +++ b/Mage.Sets/src/mage/cards/m/Malignus.java @@ -74,7 +74,7 @@ class HighestLifeTotalAmongOpponentsCount implements DynamicValue { @Override public DynamicValue copy() { - return CardsInControllerHandCount.instance; + return CardsInControllerHandCount.ANY; } @Override diff --git a/Mage.Sets/src/mage/cards/m/ManifestationSage.java b/Mage.Sets/src/mage/cards/m/ManifestationSage.java index fe1f6c90507..7adfcc0d4ef 100644 --- a/Mage.Sets/src/mage/cards/m/ManifestationSage.java +++ b/Mage.Sets/src/mage/cards/m/ManifestationSage.java @@ -26,7 +26,7 @@ public final class ManifestationSage extends CardImpl { // When Manifestation Sage enters the battlefield, create a 0/0 green and blue Fractal creature token. Put X +1/+1 counters on it, where X is the number of cards in your hand. this.addAbility(new EntersBattlefieldTriggeredAbility(FractalToken.getEffect( - CardsInControllerHandCount.instance, "Put X +1/+1 counters on it, " + + CardsInControllerHandCount.ANY, "Put X +1/+1 counters on it, " + "where X is the number of cards in your hand" ))); } diff --git a/Mage.Sets/src/mage/cards/m/Maro.java b/Mage.Sets/src/mage/cards/m/Maro.java index 4e1aff0a3f6..b05ccffcbd8 100644 --- a/Mage.Sets/src/mage/cards/m/Maro.java +++ b/Mage.Sets/src/mage/cards/m/Maro.java @@ -26,7 +26,7 @@ public final class Maro extends CardImpl { this.toughness = new MageInt(0); // Maro's power and toughness are each equal to the number of cards in your hand. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.instance))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.ANY))); } private Maro(final Maro card) { diff --git a/Mage.Sets/src/mage/cards/m/MasterTheWay.java b/Mage.Sets/src/mage/cards/m/MasterTheWay.java index 526cf4fc1d4..ba07b492f7c 100644 --- a/Mage.Sets/src/mage/cards/m/MasterTheWay.java +++ b/Mage.Sets/src/mage/cards/m/MasterTheWay.java @@ -23,7 +23,7 @@ public final class MasterTheWay extends CardImpl { // Draw a card. Master the Way deals damage to any target equal to the number of cards in your hand. this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); - Effect effect = new DamageTargetEffect(CardsInControllerHandCount.instance); + Effect effect = new DamageTargetEffect(CardsInControllerHandCount.ANY); effect.setText("{this} deals damage to any target equal to the number of cards in your hand"); this.getSpellAbility().addEffect(effect); this.getSpellAbility().addTarget(new TargetAnyTarget()); diff --git a/Mage.Sets/src/mage/cards/m/MasumaroFirstToLive.java b/Mage.Sets/src/mage/cards/m/MasumaroFirstToLive.java index 1771acc7653..7c4f870b4bc 100644 --- a/Mage.Sets/src/mage/cards/m/MasumaroFirstToLive.java +++ b/Mage.Sets/src/mage/cards/m/MasumaroFirstToLive.java @@ -31,7 +31,7 @@ public final class MasumaroFirstToLive extends CardImpl { this.toughness = new MageInt(0); // Masumaro, First to Live's power and toughness are each equal to twice the number of cards in your hand. - DynamicValue xValue= new MultipliedValue(CardsInControllerHandCount.instance, 2); + DynamicValue xValue= new MultipliedValue(CardsInControllerHandCount.ANY, 2); Effect effect = new SetBasePowerToughnessSourceEffect(xValue); effect.setText("{this}'s power and toughness are each equal to twice the number of cards in your hand"); this.addAbility(new SimpleStaticAbility(Zone.ALL, effect)); diff --git a/Mage.Sets/src/mage/cards/m/MeishinTheMindCage.java b/Mage.Sets/src/mage/cards/m/MeishinTheMindCage.java index 2261a7e6a4b..21b514a9162 100644 --- a/Mage.Sets/src/mage/cards/m/MeishinTheMindCage.java +++ b/Mage.Sets/src/mage/cards/m/MeishinTheMindCage.java @@ -12,7 +12,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.SuperType; -import mage.constants.Zone; import mage.filter.StaticFilters; /** @@ -26,7 +25,7 @@ public final class MeishinTheMindCage extends CardImpl { this.supertype.add(SuperType.LEGENDARY); // All creatures get -X/-0, where X is the number of cards in your hand. - this.addAbility(new SimpleStaticAbility(new BoostAllEffect(new SignInversionDynamicValue(CardsInControllerHandCount.instance), StaticValue.get(0), Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURE, false, "All creatures get -X/-0, where X is the number of cards in your hand"))); + this.addAbility(new SimpleStaticAbility(new BoostAllEffect(new SignInversionDynamicValue(CardsInControllerHandCount.ANY), StaticValue.get(0), Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURE, false, "All creatures get -X/-0, where X is the number of cards in your hand"))); } private MeishinTheMindCage(final MeishinTheMindCage card) { diff --git a/Mage.Sets/src/mage/cards/m/MinasTirithGarrison.java b/Mage.Sets/src/mage/cards/m/MinasTirithGarrison.java index 20c4ac70836..920205e9d57 100644 --- a/Mage.Sets/src/mage/cards/m/MinasTirithGarrison.java +++ b/Mage.Sets/src/mage/cards/m/MinasTirithGarrison.java @@ -39,7 +39,7 @@ public final class MinasTirithGarrison extends CardImpl { // Minas Tirith Garrison's power is equal to the number of cards in your hand. this.addAbility(new SimpleStaticAbility( - Zone.ALL, new SetBasePowerSourceEffect(CardsInControllerHandCount.instance) + Zone.ALL, new SetBasePowerSourceEffect(CardsInControllerHandCount.ANY) )); // Whenever Minas Tirith Garrison attacks, you may tap any number of untapped Humans you control. Draw a card for each Human tapped this way. diff --git a/Mage.Sets/src/mage/cards/n/NightmarishEnd.java b/Mage.Sets/src/mage/cards/n/NightmarishEnd.java index fb5f9587c9e..d2a939fc308 100644 --- a/Mage.Sets/src/mage/cards/n/NightmarishEnd.java +++ b/Mage.Sets/src/mage/cards/n/NightmarishEnd.java @@ -18,7 +18,7 @@ import mage.target.common.TargetCreaturePermanent; */ public final class NightmarishEnd extends CardImpl { - private static final DynamicValue xValue = new SignInversionDynamicValue(CardsInControllerHandCount.instance); + private static final DynamicValue xValue = new SignInversionDynamicValue(CardsInControllerHandCount.ANY); public NightmarishEnd(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{2}{B}"); diff --git a/Mage.Sets/src/mage/cards/o/OboroEnvoy.java b/Mage.Sets/src/mage/cards/o/OboroEnvoy.java index d44b232d62e..0fcb580a0f5 100644 --- a/Mage.Sets/src/mage/cards/o/OboroEnvoy.java +++ b/Mage.Sets/src/mage/cards/o/OboroEnvoy.java @@ -28,7 +28,7 @@ import mage.target.common.TargetCreaturePermanent; */ public final class OboroEnvoy extends CardImpl { - private static final DynamicValue xValue = new SignInversionDynamicValue(CardsInControllerHandCount.instance); + private static final DynamicValue xValue = new SignInversionDynamicValue(CardsInControllerHandCount.ANY); public OboroEnvoy(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}"); diff --git a/Mage.Sets/src/mage/cards/o/OverbeingOfMyth.java b/Mage.Sets/src/mage/cards/o/OverbeingOfMyth.java index 1f8fe4c40e2..3f9c8546f86 100644 --- a/Mage.Sets/src/mage/cards/o/OverbeingOfMyth.java +++ b/Mage.Sets/src/mage/cards/o/OverbeingOfMyth.java @@ -13,7 +13,6 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.TargetController; import mage.constants.Zone; /** @@ -32,7 +31,7 @@ public final class OverbeingOfMyth extends CardImpl { this.toughness = new MageInt(0); // Overbeing of Myth's power and toughness are each equal to the number of cards in your hand. - DynamicValue number = CardsInControllerHandCount.instance; + DynamicValue number = CardsInControllerHandCount.ANY; this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(number))); // At the beginning of your draw step, draw an additional card. diff --git a/Mage.Sets/src/mage/cards/p/PresenceOfTheWise.java b/Mage.Sets/src/mage/cards/p/PresenceOfTheWise.java index de5b4794bad..433a2791e80 100644 --- a/Mage.Sets/src/mage/cards/p/PresenceOfTheWise.java +++ b/Mage.Sets/src/mage/cards/p/PresenceOfTheWise.java @@ -20,7 +20,7 @@ public final class PresenceOfTheWise extends CardImpl { // You gain 2 life for each card in your hand. this.getSpellAbility().addEffect(new GainLifeEffect( - new MultipliedValue(CardsInControllerHandCount.instance, 2),"You gain 2 life for each card in your hand")); + new MultipliedValue(CardsInControllerHandCount.ANY, 2),"You gain 2 life for each card in your hand")); } private PresenceOfTheWise(final PresenceOfTheWise card) { diff --git a/Mage.Sets/src/mage/cards/p/PsychosisCrawler.java b/Mage.Sets/src/mage/cards/p/PsychosisCrawler.java index 4fbd2dd259f..bd99c7adad1 100644 --- a/Mage.Sets/src/mage/cards/p/PsychosisCrawler.java +++ b/Mage.Sets/src/mage/cards/p/PsychosisCrawler.java @@ -28,7 +28,7 @@ public final class PsychosisCrawler extends CardImpl { this.power = new MageInt(0); this.toughness = new MageInt(0); - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.instance))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.ANY))); this.addAbility(new DrawCardControllerTriggeredAbility(new LoseLifeOpponentsEffect(1), false)); } diff --git a/Mage.Sets/src/mage/cards/r/RalsStaticaster.java b/Mage.Sets/src/mage/cards/r/RalsStaticaster.java index 0a7a502a778..d05013c785b 100644 --- a/Mage.Sets/src/mage/cards/r/RalsStaticaster.java +++ b/Mage.Sets/src/mage/cards/r/RalsStaticaster.java @@ -45,7 +45,7 @@ public final class RalsStaticaster extends CardImpl { // Whenever Ral's Staticaster attacks, if you control a Ral planeswalker, Ral's Staticaster gets +1/+0 for each card in your hand until end of turn. this.addAbility(new ConditionalInterveningIfTriggeredAbility( new AttacksTriggeredAbility(new BoostSourceEffect( - CardsInControllerHandCount.instance, StaticValue.get(0), + CardsInControllerHandCount.ANY, StaticValue.get(0), Duration.EndOfTurn), false), new PermanentsOnTheBattlefieldCondition(filter), "Whenever {this} attacks, if you control a Ral planeswalker, " diff --git a/Mage.Sets/src/mage/cards/r/RobobrainWarMind.java b/Mage.Sets/src/mage/cards/r/RobobrainWarMind.java index 5040637c5a4..c827b72d06e 100644 --- a/Mage.Sets/src/mage/cards/r/RobobrainWarMind.java +++ b/Mage.Sets/src/mage/cards/r/RobobrainWarMind.java @@ -42,7 +42,7 @@ public final class RobobrainWarMind extends CardImpl { this.toughness = new MageInt(5); // Robobrain War Mind's power is equal to the number of cards in your hand. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerSourceEffect(CardsInControllerHandCount.instance))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerSourceEffect(CardsInControllerHandCount.ANY))); // When Robobrain War Mind enters the battlefield, you get an amount of {E} equal to the number of artifact creatures you control. this.addAbility(new EntersBattlefieldTriggeredAbility(new GetEnergyCountersControllerEffect(new PermanentsOnBattlefieldCount(filter)) diff --git a/Mage.Sets/src/mage/cards/s/SageOfAncientLore.java b/Mage.Sets/src/mage/cards/s/SageOfAncientLore.java index 91d14fcd33c..36105114896 100644 --- a/Mage.Sets/src/mage/cards/s/SageOfAncientLore.java +++ b/Mage.Sets/src/mage/cards/s/SageOfAncientLore.java @@ -35,7 +35,7 @@ public final class SageOfAncientLore extends CardImpl { this.secondSideCardClazz = mage.cards.w.WerewolfOfAncientHunger.class; // Sage of Ancient Lore's power and toughness are each equal to the number of cards in your hand. - DynamicValue xValue = CardsInControllerHandCount.instance; + DynamicValue xValue = CardsInControllerHandCount.ANY; this.addAbility(new SimpleStaticAbility(Zone.ALL, new ConditionalContinuousEffect(new SetBasePowerToughnessSourceEffect(xValue), new TransformedCondition(true), "{this}'s power and toughness are each equal to the total number of cards in your hand"))); diff --git a/Mage.Sets/src/mage/cards/s/SeaGateRestoration.java b/Mage.Sets/src/mage/cards/s/SeaGateRestoration.java index 208dea20e35..25752198cd4 100644 --- a/Mage.Sets/src/mage/cards/s/SeaGateRestoration.java +++ b/Mage.Sets/src/mage/cards/s/SeaGateRestoration.java @@ -22,7 +22,7 @@ import java.util.UUID; */ public final class SeaGateRestoration extends ModalDoubleFacedCard { - private static final DynamicValue xValue = new IntPlusDynamicValue(1, CardsInControllerHandCount.instance); + private static final DynamicValue xValue = new IntPlusDynamicValue(1, CardsInControllerHandCount.ANY); public SeaGateRestoration(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, diff --git a/Mage.Sets/src/mage/cards/s/SkirgeFamiliar.java b/Mage.Sets/src/mage/cards/s/SkirgeFamiliar.java index b7a95acc200..99398811bba 100644 --- a/Mage.Sets/src/mage/cards/s/SkirgeFamiliar.java +++ b/Mage.Sets/src/mage/cards/s/SkirgeFamiliar.java @@ -37,7 +37,7 @@ public final class SkirgeFamiliar extends CardImpl { new DiscardCardCost(false), // not perfect but for hand cards correct, activated abilities on Battlefield will miss one possible available mana // to solve this we have to do possible mana calculation per pell/ability to use. - new IntPlusDynamicValue(-1, CardsInControllerHandCount.instance) + new IntPlusDynamicValue(-1, CardsInControllerHandCount.ANY) )); } diff --git a/Mage.Sets/src/mage/cards/s/SokenzanSpellblade.java b/Mage.Sets/src/mage/cards/s/SokenzanSpellblade.java index 768031cdac3..2d6542e2054 100644 --- a/Mage.Sets/src/mage/cards/s/SokenzanSpellblade.java +++ b/Mage.Sets/src/mage/cards/s/SokenzanSpellblade.java @@ -15,7 +15,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; /** * @@ -35,7 +34,7 @@ public final class SokenzanSpellblade extends CardImpl { // Bushido 1 this.addAbility(new BushidoAbility(1)); // {1}{R}: Sokenzan Spellblade gets +X/+0 until end of turn, where X is the number of cards in your hand. - Effect effect = new BoostSourceEffect(CardsInControllerHandCount.instance, StaticValue.get(0), Duration.EndOfTurn); + Effect effect = new BoostSourceEffect(CardsInControllerHandCount.ANY, StaticValue.get(0), Duration.EndOfTurn); effect.setText("{this} gets +X/+0 until end of turn, where X is the number of cards in your hand"); this.addAbility(new SimpleActivatedAbility( effect, new ManaCostsImpl<>("{1}{R}") diff --git a/Mage.Sets/src/mage/cards/s/SophicCentaur.java b/Mage.Sets/src/mage/cards/s/SophicCentaur.java index 23c59fe3e70..8ec74b56c33 100644 --- a/Mage.Sets/src/mage/cards/s/SophicCentaur.java +++ b/Mage.Sets/src/mage/cards/s/SophicCentaur.java @@ -17,7 +17,6 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; /** * @@ -33,7 +32,7 @@ public final class SophicCentaur extends CardImpl { this.toughness = new MageInt(1); // {2}{G}{G}, {tap}, Discard a card: You gain 2 life for each card in your hand. - DynamicValue lifeToGainAmount = new MultipliedValue(CardsInControllerHandCount.instance, 2); + DynamicValue lifeToGainAmount = new MultipliedValue(CardsInControllerHandCount.ANY, 2); Effect effect = new GainLifeEffect(lifeToGainAmount); effect.setText("You gain 2 life for each card in your hand"); Ability ability = new SimpleActivatedAbility(effect, new ManaCostsImpl<>("{2}{G}{G}")); diff --git a/Mage.Sets/src/mage/cards/s/SoramaroFirstToDream.java b/Mage.Sets/src/mage/cards/s/SoramaroFirstToDream.java index 97af2c85091..1650d440d7c 100644 --- a/Mage.Sets/src/mage/cards/s/SoramaroFirstToDream.java +++ b/Mage.Sets/src/mage/cards/s/SoramaroFirstToDream.java @@ -38,7 +38,7 @@ public final class SoramaroFirstToDream extends CardImpl { // Flying this.addAbility(FlyingAbility.getInstance()); // Soramaro, First to Dream's power and toughness are each equal to the number of cards in your hand. - DynamicValue xValue= CardsInControllerHandCount.instance; + DynamicValue xValue= CardsInControllerHandCount.ANY; this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(xValue))); // {4}, Return a land you control to its owner's hand: Draw a card. diff --git a/Mage.Sets/src/mage/cards/s/SpiralingEmbers.java b/Mage.Sets/src/mage/cards/s/SpiralingEmbers.java index d96cd42fa3a..5efbbc14f47 100644 --- a/Mage.Sets/src/mage/cards/s/SpiralingEmbers.java +++ b/Mage.Sets/src/mage/cards/s/SpiralingEmbers.java @@ -23,7 +23,7 @@ public final class SpiralingEmbers extends CardImpl { // Spiraling Embers deals damage to any target equal to the number of cards in your hand. - Effect effect = new DamageTargetEffect(CardsInControllerHandCount.instance); + Effect effect = new DamageTargetEffect(CardsInControllerHandCount.ANY); effect.setText("{this} deals damage to any target equal to the number of cards in your hand."); this.getSpellAbility().addEffect(effect); this.getSpellAbility().addTarget(new TargetAnyTarget()); diff --git a/Mage.Sets/src/mage/cards/s/SpontaneousGeneration.java b/Mage.Sets/src/mage/cards/s/SpontaneousGeneration.java index c57c475b00b..13a4fb5ea05 100644 --- a/Mage.Sets/src/mage/cards/s/SpontaneousGeneration.java +++ b/Mage.Sets/src/mage/cards/s/SpontaneousGeneration.java @@ -19,7 +19,7 @@ public final class SpontaneousGeneration extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{3}{G}"); // Create a 1/1 green Saproling creature token for each card in your hand. - this.getSpellAbility().addEffect(new CreateTokenEffect(new SaprolingToken(), CardsInControllerHandCount.instance)); + this.getSpellAbility().addEffect(new CreateTokenEffect(new SaprolingToken(), CardsInControllerHandCount.ANY)); } private SpontaneousGeneration(final SpontaneousGeneration card) { diff --git a/Mage.Sets/src/mage/cards/s/StingerbackTerror.java b/Mage.Sets/src/mage/cards/s/StingerbackTerror.java index dcef50e3f04..78a699c7efd 100644 --- a/Mage.Sets/src/mage/cards/s/StingerbackTerror.java +++ b/Mage.Sets/src/mage/cards/s/StingerbackTerror.java @@ -22,7 +22,7 @@ import java.util.UUID; */ public final class StingerbackTerror extends CardImpl { - private static final DynamicValue xValue = new SignInversionDynamicValue(CardsInControllerHandCount.instance); + private static final DynamicValue xValue = new SignInversionDynamicValue(CardsInControllerHandCount.ANY); public StingerbackTerror(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}"); diff --git a/Mage.Sets/src/mage/cards/s/Sturmgeist.java b/Mage.Sets/src/mage/cards/s/Sturmgeist.java index 966a26ebba5..a3e67a820e6 100644 --- a/Mage.Sets/src/mage/cards/s/Sturmgeist.java +++ b/Mage.Sets/src/mage/cards/s/Sturmgeist.java @@ -30,7 +30,7 @@ public final class Sturmgeist extends CardImpl { this.addAbility(FlyingAbility.getInstance()); // Sturmgeist's power and toughness are each equal to the number of cards in your hand. - this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.instance))); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(CardsInControllerHandCount.ANY))); // Whenever Sturmgeist deals combat damage to a player, draw a card. this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(new DrawCardSourceControllerEffect(1), false)); } diff --git a/Mage.Sets/src/mage/cards/s/SunbringersTouch.java b/Mage.Sets/src/mage/cards/s/SunbringersTouch.java index 5d27a705998..63efe489b8e 100644 --- a/Mage.Sets/src/mage/cards/s/SunbringersTouch.java +++ b/Mage.Sets/src/mage/cards/s/SunbringersTouch.java @@ -23,7 +23,7 @@ public final class SunbringersTouch extends CardImpl { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{2}{G}{G}"); // Bolster X, where X is the number of cards in your hand. - this.getSpellAbility().addEffect(new BolsterEffect(CardsInControllerHandCount.instance).setText("Bolster X, where X is the number of cards in your hand.")); + this.getSpellAbility().addEffect(new BolsterEffect(CardsInControllerHandCount.ANY).setText("Bolster X, where X is the number of cards in your hand.")); // Each creature you control with a +1/+1 counter on it gains trample until end of turn. Effect effect = new GainAbilityControlledEffect( diff --git a/Mage.Sets/src/mage/cards/s/SwordOfWarAndPeace.java b/Mage.Sets/src/mage/cards/s/SwordOfWarAndPeace.java index 0556562a059..e42008056d8 100644 --- a/Mage.Sets/src/mage/cards/s/SwordOfWarAndPeace.java +++ b/Mage.Sets/src/mage/cards/s/SwordOfWarAndPeace.java @@ -62,7 +62,7 @@ class SwordOfWarAndPeaceAbility extends TriggeredAbilityImpl { public SwordOfWarAndPeaceAbility() { super(Zone.BATTLEFIELD, new SwordOfWarAndPeaceDamageEffect()); - this.addEffect(new GainLifeEffect(CardsInControllerHandCount.instance)); + this.addEffect(new GainLifeEffect(CardsInControllerHandCount.ANY)); } private SwordOfWarAndPeaceAbility(final SwordOfWarAndPeaceAbility ability) { diff --git a/Mage.Sets/src/mage/cards/s/SylvanYeti.java b/Mage.Sets/src/mage/cards/s/SylvanYeti.java index 936b1dfb8f9..1dc359027c1 100644 --- a/Mage.Sets/src/mage/cards/s/SylvanYeti.java +++ b/Mage.Sets/src/mage/cards/s/SylvanYeti.java @@ -26,7 +26,7 @@ public final class SylvanYeti extends CardImpl { this.toughness = new MageInt(4); // Sylvan Yeti's power is equal to the number of cards in your hand. - Effect effect = new SetBasePowerSourceEffect(CardsInControllerHandCount.instance); + Effect effect = new SetBasePowerSourceEffect(CardsInControllerHandCount.ANY); this.addAbility(new SimpleStaticAbility(Zone.ALL, effect)); } diff --git a/Mage.Sets/src/mage/cards/s/SyrElenoraTheDiscerning.java b/Mage.Sets/src/mage/cards/s/SyrElenoraTheDiscerning.java index aa7cacedc07..4e02196d807 100644 --- a/Mage.Sets/src/mage/cards/s/SyrElenoraTheDiscerning.java +++ b/Mage.Sets/src/mage/cards/s/SyrElenoraTheDiscerning.java @@ -30,7 +30,7 @@ public final class SyrElenoraTheDiscerning extends CardImpl { // Syr Elenora the Discerning's power is equal to the number of cards in your hand. this.addAbility(new SimpleStaticAbility( - Zone.ALL, new SetBasePowerSourceEffect(CardsInControllerHandCount.instance) + Zone.ALL, new SetBasePowerSourceEffect(CardsInControllerHandCount.ANY) )); // When Syr Elenora enters the battlefield, draw a card. diff --git a/Mage.Sets/src/mage/cards/t/TheArchimandrite.java b/Mage.Sets/src/mage/cards/t/TheArchimandrite.java index 9127f3b5ae4..bad5a75b710 100644 --- a/Mage.Sets/src/mage/cards/t/TheArchimandrite.java +++ b/Mage.Sets/src/mage/cards/t/TheArchimandrite.java @@ -36,7 +36,7 @@ import java.util.UUID; public final class TheArchimandrite extends CardImpl { private static final DynamicValue xValue = new AdditiveDynamicValue( - CardsInControllerHandCount.instance, StaticValue.get(-4) + CardsInControllerHandCount.ANY, StaticValue.get(-4) ); private static final FilterPermanent filter = new FilterControlledPermanent(); private static final FilterCreaturePermanent filter2 = new FilterCreaturePermanent(); diff --git a/Mage.Sets/src/mage/cards/t/TheGreatSynthesis.java b/Mage.Sets/src/mage/cards/t/TheGreatSynthesis.java index ce1e78245dc..160a80da2a6 100644 --- a/Mage.Sets/src/mage/cards/t/TheGreatSynthesis.java +++ b/Mage.Sets/src/mage/cards/t/TheGreatSynthesis.java @@ -41,7 +41,7 @@ public class TheGreatSynthesis extends CardImpl { //I — Draw cards equal to the number of cards in your hand. You have no maximum hand size for as long as you //control The Great Synthesis. sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I, - new DrawCardSourceControllerEffect(CardsInControllerHandCount.instance) + new DrawCardSourceControllerEffect(CardsInControllerHandCount.ANY) .setText("draw cards equal to the number of cards in your hand"), new MaximumHandSizeControllerEffect(Integer.MAX_VALUE, Duration.WhileOnBattlefield, MaximumHandSizeControllerEffect.HandSizeModification.SET) diff --git a/Mage.Sets/src/mage/cards/t/TheRoyalScions.java b/Mage.Sets/src/mage/cards/t/TheRoyalScions.java index 790e8df37ed..52897d907de 100644 --- a/Mage.Sets/src/mage/cards/t/TheRoyalScions.java +++ b/Mage.Sets/src/mage/cards/t/TheRoyalScions.java @@ -88,7 +88,7 @@ class TheRoyalScionsCreateReflexiveTriggerEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { effect.apply(game, source); ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( - new DamageTargetEffect(CardsInControllerHandCount.instance), false, + new DamageTargetEffect(CardsInControllerHandCount.ANY), false, "{this} deals damage to any target equal to the number of cards in your hand" ); ability.addTarget(new TargetAnyTarget()); diff --git a/Mage.Sets/src/mage/cards/t/TishanaVoiceOfThunder.java b/Mage.Sets/src/mage/cards/t/TishanaVoiceOfThunder.java index 2fbb6ac1215..f7b2d96c3c6 100644 --- a/Mage.Sets/src/mage/cards/t/TishanaVoiceOfThunder.java +++ b/Mage.Sets/src/mage/cards/t/TishanaVoiceOfThunder.java @@ -32,7 +32,7 @@ public final class TishanaVoiceOfThunder extends CardImpl { this.toughness = new MageInt(0); // Tishana, Voice of Thunder's power and toughness are each equal to the number of cards in your hand. - DynamicValue xValue = CardsInControllerHandCount.instance; + DynamicValue xValue = CardsInControllerHandCount.ANY; this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessSourceEffect(xValue))); // You have no maximum hand size. diff --git a/Mage.Sets/src/mage/cards/u/UnionOfTheThirdPath.java b/Mage.Sets/src/mage/cards/u/UnionOfTheThirdPath.java index bd944e32708..b9e2707ef49 100644 --- a/Mage.Sets/src/mage/cards/u/UnionOfTheThirdPath.java +++ b/Mage.Sets/src/mage/cards/u/UnionOfTheThirdPath.java @@ -20,7 +20,7 @@ public final class UnionOfTheThirdPath extends CardImpl { // Draw a card, then you gain life equal to the number of cards in your hand. this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1)); - this.getSpellAbility().addEffect(new GainLifeEffect(CardsInControllerHandCount.instance).concatBy(", then")); + this.getSpellAbility().addEffect(new GainLifeEffect(CardsInControllerHandCount.ANY).concatBy(", then")); } private UnionOfTheThirdPath(final UnionOfTheThirdPath card) { diff --git a/Mage.Sets/src/mage/cards/v/VensersJournal.java b/Mage.Sets/src/mage/cards/v/VensersJournal.java index 717b3365950..bf96037fdf3 100644 --- a/Mage.Sets/src/mage/cards/v/VensersJournal.java +++ b/Mage.Sets/src/mage/cards/v/VensersJournal.java @@ -13,7 +13,6 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.constants.Zone; /** * @@ -29,7 +28,7 @@ public final class VensersJournal extends CardImpl { this.addAbility(new SimpleStaticAbility(effect)); // At the beginning of your upkeep, you gain 1 life for each card in your hand. - this.addAbility(new BeginningOfUpkeepTriggeredAbility(new GainLifeEffect(CardsInControllerHandCount.instance) + this.addAbility(new BeginningOfUpkeepTriggeredAbility(new GainLifeEffect(CardsInControllerHandCount.ANY) .setText("you gain 1 life for each card in your hand"))); } diff --git a/Mage.Sets/src/mage/cards/v/VisionOfTheUnspeakable.java b/Mage.Sets/src/mage/cards/v/VisionOfTheUnspeakable.java index 4d8752569c1..a93dc245b06 100644 --- a/Mage.Sets/src/mage/cards/v/VisionOfTheUnspeakable.java +++ b/Mage.Sets/src/mage/cards/v/VisionOfTheUnspeakable.java @@ -36,8 +36,8 @@ public final class VisionOfTheUnspeakable extends CardImpl { // Vision of the Unspeakable gets +1/+1 for each card in your hand. this.addAbility(new SimpleStaticAbility(new BoostSourceEffect( - CardsInControllerHandCount.instance, - CardsInControllerHandCount.instance, + CardsInControllerHandCount.ANY, + CardsInControllerHandCount.ANY, Duration.WhileOnBattlefield ))); } diff --git a/Mage.Sets/src/mage/cards/w/WarpedPhysique.java b/Mage.Sets/src/mage/cards/w/WarpedPhysique.java index 9e63b29a6a2..2a4a66a23d4 100644 --- a/Mage.Sets/src/mage/cards/w/WarpedPhysique.java +++ b/Mage.Sets/src/mage/cards/w/WarpedPhysique.java @@ -18,13 +18,13 @@ import mage.target.common.TargetCreaturePermanent; public final class WarpedPhysique extends CardImpl { - private static final DynamicValue xValue = new SignInversionDynamicValue(CardsInControllerHandCount.instance); + private static final DynamicValue xValue = new SignInversionDynamicValue(CardsInControllerHandCount.ANY); public WarpedPhysique(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{U}{B}"); // Target creature gets +X/-X until end of turn, where X is the number of cards in your hand. - this.getSpellAbility().addEffect(new BoostTargetEffect(CardsInControllerHandCount.instance, xValue, Duration.EndOfTurn)); + this.getSpellAbility().addEffect(new BoostTargetEffect(CardsInControllerHandCount.ANY, xValue, Duration.EndOfTurn)); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); } diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java index a7fe0661157..881e747fb6b 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java @@ -12,7 +12,6 @@ import mage.players.Player; public enum CardsInControllerHandCount implements DynamicValue { - instance(StaticFilters.FILTER_CARD_CARDS), // TODO: replace usage to ANY ANY(StaticFilters.FILTER_CARD_CARDS), CREATURES(StaticFilters.FILTER_CARD_CREATURES), LANDS(StaticFilters.FILTER_CARD_LANDS); From 6cf1f4af3dd50bd83f21ecf4f07a33c397d4fe2f Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 8 Apr 2025 23:02:49 +0400 Subject: [PATCH 063/133] refactor: improved Aladdin's Lamp --- Mage.Sets/src/mage/cards/a/AladdinsLamp.java | 19 ++++++++++-- Mage.Sets/src/mage/cards/a/Archivist.java | 3 +- .../cost/additional/AdjusterCostTest.java | 30 +++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AladdinsLamp.java b/Mage.Sets/src/mage/cards/a/AladdinsLamp.java index a4f7611e448..d2f261f0ca9 100644 --- a/Mage.Sets/src/mage/cards/a/AladdinsLamp.java +++ b/Mage.Sets/src/mage/cards/a/AladdinsLamp.java @@ -3,6 +3,7 @@ package mage.cards.a; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.ReplacementEffectImpl; @@ -35,8 +36,8 @@ public final class AladdinsLamp extends CardImpl { // {X}, {T}: The next time you would draw a card this turn, instead look at the top X cards of your library, put all but one of them on the bottom of your library in a random order, then draw a card. X can't be 0. Ability ability = new SimpleActivatedAbility(new AladdinsLampEffect(), new ManaCostsImpl<>("{X}")); ability.addCost(new TapSourceCost()); - ability.setVariableCostsMinMax(1, Integer.MAX_VALUE); - // TODO: add costAdjuster for min/max values due library size + ability.setCostAdjuster(AladdinsLampCostAdjuster.instance); + this.addAbility(ability); } @@ -96,3 +97,17 @@ class AladdinsLampEffect extends ReplacementEffectImpl { return source.isControlledBy(event.getPlayerId()); } } + +enum AladdinsLampCostAdjuster implements CostAdjuster { + instance; + + @Override + public void prepareX(Ability ability, Game game) { + Player controller = game.getPlayer(ability.getControllerId()); + if (controller == null) { + return; + } + + ability.setVariableCostsMinMax(1, Math.max(1, controller.getLibrary().size())); + } +} diff --git a/Mage.Sets/src/mage/cards/a/Archivist.java b/Mage.Sets/src/mage/cards/a/Archivist.java index b0cde1b880f..688acbb500c 100644 --- a/Mage.Sets/src/mage/cards/a/Archivist.java +++ b/Mage.Sets/src/mage/cards/a/Archivist.java @@ -1,4 +1,3 @@ - package mage.cards.a; import java.util.UUID; @@ -26,7 +25,7 @@ public final class Archivist extends CardImpl { this.power = new MageInt(1); this.toughness = new MageInt(1); - //{T}: Draw a card. + // {T}: Draw a card. this.addAbility(new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new TapSourceCost())); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java index 7b9a1588ea6..35c05e4c0ff 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/additional/AdjusterCostTest.java @@ -467,6 +467,36 @@ public class AdjusterCostTest extends CardTestPlayerBaseWithAIHelps { assertLife(playerB, 20 - 3); } + @Test + public void test_prepareX_AladdinsLamp() { + skipInitShuffling(); + + // {X}, {T}: The next time you would draw a card this turn, instead look at the top X cards of your library, + // put all but one of them on the bottom of your library in a random order, then draw a card. X can't be 0. + addCard(Zone.BATTLEFIELD, playerA, "Aladdin's Lamp"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + // {T}: Draw a card. + addCard(Zone.BATTLEFIELD, playerA, "Archivist"); + addCard(Zone.LIBRARY, playerA, "Island", 1); + addCard(Zone.LIBRARY, playerA, "Grizzly Bears", 1); + addCard(Zone.LIBRARY, playerA, "Island", 3); + + // prepare effect + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{X}, {T}: The next time"); + setChoice(playerA, "X=5"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // improved draw + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Draw"); + setChoice(playerA, "Grizzly Bears"); // keep on top and draw + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, "Grizzly Bears", 1); + } + @Test public void test_modifyCost_Fireball() { // This spell costs {1} more to cast for each target beyond the first. From d674c592ea2925d7003841e25550d299663229b3 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 8 Apr 2025 23:02:49 +0400 Subject: [PATCH 064/133] refactor: improved Aladdin's Lamp --- Mage/src/main/java/mage/abilities/costs/CostAdjuster.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java index e522779f851..86bc96ee985 100644 --- a/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java +++ b/Mage/src/main/java/mage/abilities/costs/CostAdjuster.java @@ -11,9 +11,9 @@ import java.io.Serializable; *

* Possible use cases: * - define {X} costs like X cards to discard (mana and non-mana values); - * - define {X} limits before announce (to help in UX and AI logic) - * - define any dynamic costs - * - use as simple cost increase/reduce effect + * - define {X} limits before announce (to help in UX and AI logic); + * - define any dynamic costs; + * - use as simple cost increase/reduce effect; *

* Calls order by game engine: * - ... early cost target selection for EarlyTargetCost ... From 1568189c1bab77425a992fc3d3e483559cdcf6e0 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 9 Apr 2025 01:22:33 +0400 Subject: [PATCH 065/133] other: fixed typos in omen related code (#13501); --- .../plugins/card/dl/sources/ScryfallApiCard.java | 6 +++--- .../test/java/mage/verify/VerifyCardDataTest.java | 12 ++++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallApiCard.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallApiCard.java index b864f74a21d..4ea496665d0 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallApiCard.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallApiCard.java @@ -54,7 +54,7 @@ public class ScryfallApiCard { this.card_faces.forEach(ScryfallApiCardFace::prepareCompatibleData); } - // workaround for adventure card name fix: + // workaround for adventure/omen card name fix: // - scryfall: Ondu Knotmaster // Throw a Line // - xmage: Ondu Knotmaster if (this.layout.equals("adventure")) { @@ -100,12 +100,12 @@ public class ScryfallApiCard { } this.name = this.card_faces.get(0).name; } else if (this.card_faces.get(0).layout.equals("adventure")) { - // adventure card + // adventure/omen card // Bloomvine Regent // Claim Territory // https://scryfall.com/card/tdm/381/bloomvine-regent-claim-territory-bloomvine-regent this.name = this.card_faces.get(0).name; if (this.card_faces.get(0).name.equals(this.card_faces.get(1).name)) { - throw new IllegalArgumentException("Scryfall: unsupported data type, adventure's reversible_card must have diff names in faces " + throw new IllegalArgumentException("Scryfall: unsupported data type, adventure/omen's reversible_card must have diff names in faces " + this.set + " - " + this.collector_number + " - " + this.name); } } else if (this.card_faces.get(0).layout.equals("token")) { diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 72b6d6dabad..0bd47644157 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -2560,12 +2560,20 @@ public class VerifyCardDataTest { refRules[i]; } } - + if (ref.subtypes.contains("Omen")) { + for (int i = 0; i < refRules.length; i++) { + refRules[i] = "Omen " + + ref.types.get(0) + " - " + + ref.faceName + ' ' + + ref.manaCost + " - " + + refRules[i]; + } + } String[] cardRules = card .getRules() .stream() - .filter(s -> !(card instanceof CardWithSpellOption) || !s.startsWith("Adventure ") || !s.startsWith("Omen ")) + .filter(s -> !(card instanceof CardWithSpellOption) || !(s.startsWith("Adventure ") || s.startsWith("Omen "))) .collect(Collectors.joining("\n")) .replace("
", "\n") .replace("
", "\n") From a29dbc17ebcc06a67d6aec965a3b433bdf237138 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 9 Apr 2025 01:24:30 +0400 Subject: [PATCH 066/133] tests: added ref rules compare in test_showCardInfo --- .../java/mage/verify/VerifyCardDataTest.java | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 0bd47644157..f1da5a49128 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -1696,7 +1696,7 @@ public class VerifyCardDataTest { checkCardCanBeCopied(card); } } - checkWrongAbilitiesText(card, ref, cardIndex); + checkWrongAbilitiesText(card, ref, cardIndex, false); } private void checkColors(Card card, MtgJsonCard ref) { @@ -2416,24 +2416,31 @@ public class VerifyCardDataTest { // ref card System.out.println(); - MtgJsonCard ref = MtgJsonService.card(card.getName()); - MtgJsonCard ref2 = null; + MtgJsonCard refMain = MtgJsonService.card(card.getName()); + MtgJsonCard refSpell = null; if (card instanceof CardWithSpellOption) { - ref2 = MtgJsonService.card(((CardWithSpellOption) card).getSpellCard().getName()); + refSpell = MtgJsonService.card(((CardWithSpellOption) card).getSpellCard().getName()); } - if (ref == null) { - ref = MtgJsonService.cardByClassName(foundClassName); + if (refMain == null) { + refMain = MtgJsonService.cardByClassName(foundClassName); } - if (ref != null) { - System.out.println("ref: " + ref.getNameAsFace() + " " + ref.manaCost); - System.out.println(ref.text); - if (ref2 != null) { - System.out.println(ref2.getNameAsFace() + " " + ref2.manaCost); - System.out.println(ref2.text); + if (refMain != null) { + System.out.println("ref: " + refMain.getNameAsFace() + " " + refMain.manaCost); + System.out.println(refMain.text); + if (refSpell != null) { + System.out.println(refSpell.getNameAsFace() + " " + refSpell.manaCost); + System.out.println(refSpell.text); } } else { System.out.println("WARNING, can't find mtgjson ref for " + card.getName()); } + + // additional check to simulate diff in rules + if (refMain != null) { + checkWrongAbilitiesText(card, refMain, 0, true); + } else if (refSpell != null) { + checkWrongAbilitiesText(((CardWithSpellOption) card).getSpellCard(), refSpell, 0, true); + } }); } @@ -2484,9 +2491,9 @@ public class VerifyCardDataTest { System.out.println(); } - private void checkWrongAbilitiesText(Card card, MtgJsonCard ref, int cardIndex) { + private void checkWrongAbilitiesText(Card card, MtgJsonCard ref, int cardIndex, boolean forceToCheck) { // checks missing or wrong text - if (!FULL_ABILITIES_CHECK_SET_CODES.equals("*") && !FULL_ABILITIES_CHECK_SET_CODES.contains(card.getExpansionSetCode())) { + if (!forceToCheck && !FULL_ABILITIES_CHECK_SET_CODES.equals("*") && !FULL_ABILITIES_CHECK_SET_CODES.contains(card.getExpansionSetCode())) { return; } if (wasCheckedByAbilityText(ref)) { @@ -2628,10 +2635,10 @@ public class VerifyCardDataTest { } // extra message for easy checks - if (!isFine) { + if (forceToCheck || !isFine) { System.out.println(); - System.out.println("Wrong card " + cardIndex + ": " + card.getName()); + System.out.println((isFine ? "Good" : "Wrong") + " card " + cardIndex + ": " + card.getName()); Arrays.sort(cardRules); for (String s : cardRules) { System.out.println(s); From 04f9ab75bab382e8965761d751261419658247fb Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 8 Apr 2025 11:26:38 -0400 Subject: [PATCH 067/133] [TDM] Implement Krumar Initiate --- .../src/mage/cards/k/KrumarInitiate.java | 48 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + .../effects/keyword/EndureSourceEffect.java | 14 +++++- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/k/KrumarInitiate.java diff --git a/Mage.Sets/src/mage/cards/k/KrumarInitiate.java b/Mage.Sets/src/mage/cards/k/KrumarInitiate.java new file mode 100644 index 00000000000..6d04e88cde8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KrumarInitiate.java @@ -0,0 +1,48 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.effects.keyword.EndureSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class KrumarInitiate extends CardImpl { + + public KrumarInitiate(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // {X}{B}, {T}, Pay X life: This creature endures X. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility( + new EndureSourceEffect(GetXValue.instance, "{this}"), new ManaCostsImpl<>("{X}{B}") + ); + ability.addCost(new TapSourceCost()); + ability.addCost(new PayLifeCost(GetXValue.instance, "X life")); + this.addAbility(ability); + } + + private KrumarInitiate(final KrumarInitiate card) { + super(card); + } + + @Override + public KrumarInitiate copy() { + return new KrumarInitiate(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 57bd15de2b3..5ae3005a566 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -141,6 +141,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Knockout Maneuver", 147, Rarity.UNCOMMON, mage.cards.k.KnockoutManeuver.class)); cards.add(new SetCardInfo("Kotis, the Fangkeeper", 202, Rarity.RARE, mage.cards.k.KotisTheFangkeeper.class)); cards.add(new SetCardInfo("Krotiq Nestguard", 148, Rarity.COMMON, mage.cards.k.KrotiqNestguard.class)); + cards.add(new SetCardInfo("Krumar Initiate", 84, Rarity.UNCOMMON, mage.cards.k.KrumarInitiate.class)); cards.add(new SetCardInfo("Lasyd Prowler", 149, Rarity.RARE, mage.cards.l.LasydProwler.class)); cards.add(new SetCardInfo("Lie in Wait", 203, Rarity.UNCOMMON, mage.cards.l.LieInWait.class)); cards.add(new SetCardInfo("Lightfoot Technique", 14, Rarity.COMMON, mage.cards.l.LightfootTechnique.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java index 9be3c36936d..a9f23f7bcea 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/EndureSourceEffect.java @@ -1,6 +1,8 @@ package mage.abilities.effects.keyword; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.OneShotEffect; import mage.constants.Outcome; import mage.counters.CounterType; @@ -15,13 +17,17 @@ import mage.util.CardUtil; */ public class EndureSourceEffect extends OneShotEffect { - private final int amount; + private final DynamicValue amount; public EndureSourceEffect(int amount) { this(amount, "it"); } public EndureSourceEffect(int amount, String selfText) { + this(StaticValue.get(amount), selfText); + } + + public EndureSourceEffect(DynamicValue amount, String selfText) { super(Outcome.Benefit); staticText = selfText + " endures " + amount; this.amount = amount; @@ -39,7 +45,11 @@ public class EndureSourceEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - return doEndure(source.getSourcePermanentOrLKI(game), 1, game, source); + return doEndure( + source.getSourcePermanentOrLKI(game), + amount.calculate(game, source, this), + game, source + ); } public static boolean doEndure(Permanent permanent, int amount, Game game, Ability source) { From 0a1bf474349d6e600cfddeb8dc30c727f1ed774f Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 9 Apr 2025 02:21:16 +0400 Subject: [PATCH 068/133] AI: fixed game freeze on cards with combat triggers (close #13342) --- .../src/mage/player/human/HumanPlayer.java | 60 ++++++++++--------- .../src/mage/cards/f/FurnacePunisher.java | 7 +-- .../test/AI/basic/BlockSimulationAITest.java | 43 +++++++++++++ 3 files changed, 78 insertions(+), 32 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 46bafaf3eb4..9f64a00117c 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -380,9 +380,13 @@ public class HumanPlayer extends PlayerImpl { } } + private boolean canCallFeedback(Game game) { + return !gameInCheckPlayableState(game) && !game.isSimulation(); + } + @Override public boolean chooseMulligan(Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } @@ -509,7 +513,7 @@ public class HumanPlayer extends PlayerImpl { @Override public int chooseReplacementEffect(Map effectsMap, Map objectsMap, Game game) { - if (gameInCheckPlayableState(game, true)) { // ignore warning logs until double call for TAPPED_FOR_MANA will be fix + if (gameInCheckPlayableState(game, true) || game.isSimulation()) { // TODO: ignore warning logs until double call for TAPPED_FOR_MANA will be fix return 0; } @@ -615,7 +619,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choose(Outcome outcome, Choice choice, Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } @@ -678,7 +682,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map options) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } @@ -782,7 +786,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } @@ -862,7 +866,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } @@ -944,7 +948,7 @@ public class HumanPlayer extends PlayerImpl { // choose one or multiple target cards @Override public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } @@ -1025,7 +1029,7 @@ public class HumanPlayer extends PlayerImpl { public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) { // choose amount // human can choose or un-choose MULTIPLE targets at once - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } @@ -1486,8 +1490,8 @@ public class HumanPlayer extends PlayerImpl { @Override public TriggeredAbility chooseTriggeredAbility(java.util.List abilities, Game game) { // choose triggered abilitity from list - if (gameInCheckPlayableState(game)) { - return null; + if (!canCallFeedback(game)) { + return abilities.isEmpty() ? null : abilities.get(0); } // automatically order triggers with same ability, rules text, and targets @@ -1615,7 +1619,7 @@ public class HumanPlayer extends PlayerImpl { protected boolean playManaHandling(Ability abilityToCast, ManaCost unpaid, String promptText, Game game) { // choose mana to pay (from permanents or from pool) - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } @@ -1661,7 +1665,7 @@ public class HumanPlayer extends PlayerImpl { * @return */ public int announceRepetitions(Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return 0; } @@ -1694,8 +1698,8 @@ public class HumanPlayer extends PlayerImpl { */ @Override public int announceXMana(int min, int max, String message, Game game, Ability ability) { - if (gameInCheckPlayableState(game)) { - return 0; + if (!canCallFeedback(game)) { + return min; } int xValue = 0; @@ -1721,8 +1725,8 @@ public class HumanPlayer extends PlayerImpl { @Override public int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost) { - if (gameInCheckPlayableState(game)) { - return 0; + if (!canCallFeedback(game)) { + return min; } int xValue = 0; @@ -1794,7 +1798,7 @@ public class HumanPlayer extends PlayerImpl { @Override public void selectAttackers(Game game, UUID attackingPlayerId) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return; } @@ -2064,7 +2068,7 @@ public class HumanPlayer extends PlayerImpl { @Override public void selectBlockers(Ability source, Game game, UUID defendingPlayerId) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return; } @@ -2126,7 +2130,7 @@ public class HumanPlayer extends PlayerImpl { } protected void selectCombatGroup(UUID defenderId, UUID blockerId, Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return; } TargetAttackingCreature target = new TargetAttackingCreature(); @@ -2180,8 +2184,8 @@ public class HumanPlayer extends PlayerImpl { @Override public int getAmount(int min, int max, String message, Game game) { - if (gameInCheckPlayableState(game)) { - return 0; + if (!canCallFeedback(game)) { + return min; } while (canRespond()) { @@ -2220,7 +2224,7 @@ public class HumanPlayer extends PlayerImpl { return defaultList; } - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return defaultList; } @@ -2288,7 +2292,7 @@ public class HumanPlayer extends PlayerImpl { * @param unpaidForManaAction - set unpaid for mana actions like convoke */ protected void activateSpecialAction(Game game, ManaCost unpaidForManaAction) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return; } @@ -2319,7 +2323,7 @@ public class HumanPlayer extends PlayerImpl { } protected void activateAbility(Map abilities, MageObject object, Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return; } @@ -2404,7 +2408,7 @@ public class HumanPlayer extends PlayerImpl { @Override public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return null; } @@ -2444,7 +2448,7 @@ public class HumanPlayer extends PlayerImpl { @Override public ActivatedAbility chooseLandOrSpellAbility(Card card, Game game, boolean noMana) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return null; } @@ -2491,7 +2495,7 @@ public class HumanPlayer extends PlayerImpl { @Override public Mode chooseMode(Modes modes, Ability source, Game game) { // choose mode to activate - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return null; } @@ -2619,7 +2623,7 @@ public class HumanPlayer extends PlayerImpl { @Override public boolean choosePile(Outcome outcome, String message, java.util.List pile1, java.util.List pile2, Game game) { - if (gameInCheckPlayableState(game)) { + if (!canCallFeedback(game)) { return true; } diff --git a/Mage.Sets/src/mage/cards/f/FurnacePunisher.java b/Mage.Sets/src/mage/cards/f/FurnacePunisher.java index a086a692657..88d6854e5db 100644 --- a/Mage.Sets/src/mage/cards/f/FurnacePunisher.java +++ b/Mage.Sets/src/mage/cards/f/FurnacePunisher.java @@ -2,9 +2,9 @@ package mage.cards.f; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.MenaceAbility; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -23,11 +23,10 @@ public class FurnacePunisher extends CardImpl { this.power = new MageInt(3); this.toughness = new MageInt(3); - //Menace + // Menace this.addAbility(new MenaceAbility(false)); - //At the beginning of each player’s upkeep, Furnace Punisher deals 2 damage to that player unless they control - //two or more basic lands. + // At the beginning of each player’s upkeep, Furnace Punisher deals 2 damage to that player unless they control two or more basic lands. this.addAbility(new BeginningOfUpkeepTriggeredAbility(TargetController.EACH_PLAYER, new FurnacePunisherEffect(), false ).setTriggerPhrase("At the beginning of each player's upkeep, ")); diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java index af1e99a689e..f7c0158b60d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java @@ -309,4 +309,47 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { assertGraveyardCount(playerA, "Balduvian Bears", 1); assertDamageReceived(playerB, "Graveblade Marauder", 2); } + + @Test + public void test_Block_deathtouch_attacker_vs_menace() { + // possible bug: AI freeze, see https://github.com/magefree/mage/issues/13342 + // it's only HumanPlayer related and can't be tested here + + // First strike, Deathtouch + // Whenever Glissa Sunslayer deals combat damage to a player, choose one — + // • You draw a card and you lose 1 life. + // • Destroy target enchantment. + // • Remove up to three counters from target permanent. + addCard(Zone.BATTLEFIELD, playerA, "Glissa Sunslayer", 1); // 3/3 + // Deathtouch + // Whenever a creature you control with deathtouch deals combat damage to a player, that player gets two poison counters. + addCard(Zone.BATTLEFIELD, playerA, "Fynn, the Fangbearer", 1); // 1/3 + // Deathtouch + // Toxic 1 (Players dealt combat damage by this creature also get a poison counter.) + addCard(Zone.BATTLEFIELD, playerA, "Bilious Skulldweller", 1); // 1/1 + // + // Menace + // At the beginning of each player’s upkeep, Furnace Punisher deals 2 damage to that player unless they control + // two or more basic lands. + addCard(Zone.BATTLEFIELD, playerB, "Furnace Punisher", 1); // 3/3 + + attack(1, playerA, "Glissa Sunslayer"); + attack(1, playerA, "Fynn, the Fangbearer"); + attack(1, playerA, "Bilious Skulldweller"); + setChoice(playerA, "Whenever a creature you control", 2); // x3 triggers + setModeChoice(playerA, "1"); // you draw a card and you lose 1 life + + // ai must not block attacker with Deathtouch + aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("no blockers", 1, playerB, ""); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20 - 1 - 2); // from draw, from furnace damage + assertLife(playerB, 20 - 3 - 1 - 1); + assertPermanentCount(playerA, "Glissa Sunslayer", 1); + assertGraveyardCount(playerB, 0); + } } From 504a2c2bf893d3dc2550625c3ef291a6b947f274 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 8 Apr 2025 21:04:33 -0400 Subject: [PATCH 069/133] temporary verify fix --- .../src/test/java/mage/verify/VerifyCardDataTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index f1da5a49128..a258288f503 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -2161,10 +2161,13 @@ public class VerifyCardDataTest { ); // card can contain rules text from both sides, so must search ref card for all sides too String additionalName; - if (card instanceof CardWithSpellOption) { - additionalName = ((CardWithSpellOption) card).getSpellCard().getName(); + if (card instanceof AdventureCard) { // temporary, change when mtgjson is fixed + additionalName = ((AdventureCard) card).getSpellCard().getName(); } else if (card.isTransformable() && !card.isNightCard()) { additionalName = card.getSecondCardFace().getName(); + } else if (card instanceof OmenCard) { // temporary, change when mtgjson is fixed + checkMissNonTargeted = false; + additionalName = null; } else { additionalName = null; } @@ -2407,8 +2410,7 @@ public class VerifyCardDataTest { // format to print main card then spell card card.getInitAbilities().getRules().forEach(this::printAbilityText); ((CardWithSpellOption) card).getSpellCard().getAbilities().getRules().forEach(r -> printAbilityText(r.replace("— ", "\n"))); - } - else if (card instanceof SplitCard || card instanceof ModalDoubleFacedCard) { + } else if (card instanceof SplitCard || card instanceof ModalDoubleFacedCard) { card.getAbilities().getRules().forEach(this::printAbilityText); } else { card.getRules().forEach(this::printAbilityText); From db8102a94c7d922a8515ef3ea92ef65b5234a394 Mon Sep 17 00:00:00 2001 From: androosss <101566943+androosss@users.noreply.github.com> Date: Wed, 9 Apr 2025 03:18:55 +0200 Subject: [PATCH 070/133] [TDM] Implement Betor, Kin to All (#13508) * Implemented Betor, Kin to All * intervenening if in ability * update betor * reduce ident --- Mage.Sets/src/mage/cards/b/BetorKinToAll.java | 165 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 9 +- 2 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/b/BetorKinToAll.java diff --git a/Mage.Sets/src/mage/cards/b/BetorKinToAll.java b/Mage.Sets/src/mage/cards/b/BetorKinToAll.java new file mode 100644 index 00000000000..923f3db7a30 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BetorKinToAll.java @@ -0,0 +1,165 @@ +package mage.cards.b; + +import java.util.UUID; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +/** + * + * @author androosss + */ +public final class BetorKinToAll extends CardImpl { + + private static final Hint hint = new ValueHint( + "Total toughness of creatures you control", ControlledCreaturesToughnessValue.instance); + + public BetorKinToAll(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[] { CardType.CREATURE }, "{2}{W}{B}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(5); + this.toughness = new MageInt(7); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // At the beginning of your end step, if creatures you control have total + // toughness 10 or greater, draw a card. Then if creatures you control have + // total toughness 20 or greater, untap each creature you control. Then if + // creatures you control have total toughness 40 or greater, each opponent loses + // half their life, rounded up. + Ability betorAbility = new BeginningOfEndStepTriggeredAbility(new DrawCardSourceControllerEffect(1)) + .withInterveningIf(BetorKinToAllCondition.instance).addHint(hint); + betorAbility.addEffect(new BetorKinToAllEffect()); + this.addAbility(betorAbility); + } + + private BetorKinToAll(final BetorKinToAll card) { + super(card); + } + + @Override + public BetorKinToAll copy() { + return new BetorKinToAll(this); + } +} + +enum BetorKinToAllCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return ControlledCreaturesToughnessValue.instance.calculate(game, source, null) >= 10; + } + +} + +class BetorKinToAllEffect extends OneShotEffect { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(); + + BetorKinToAllEffect() { + super(Outcome.Benefit); + this.staticText = "Then if creatures you control have total toughness 20 or greater, untap each creature you control. Then if creatures you control have total toughness 40 or greater, each opponent loses half their life, rounded up."; + } + + private BetorKinToAllEffect(final BetorKinToAllEffect effect) { + super(effect); + } + + @Override + public BetorKinToAllEffect copy() { + return new BetorKinToAllEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + + int sumToughness = ControlledCreaturesToughnessValue.instance.calculate(game, source, null); + + if (sumToughness < 20) { + return true; + } + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, source.getControllerId(), + game)) { + permanent.untap(game); + } + + if (sumToughness < 40) { + return true; + } + + for (UUID playerId : game.getOpponents(controller.getId())) { + Player opponent = game.getPlayer(playerId); + if (opponent == null) { + continue; + } + int amount = (int) Math.ceil(opponent.getLife() / 2f); + if (amount > 0) { + opponent.loseLife(amount, game, source, false); + } + } + + return true; + } +} + +enum ControlledCreaturesToughnessValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return game + .getBattlefield() + .getActivePermanents( + StaticFilters.FILTER_CONTROLLED_CREATURE, + sourceAbility.getControllerId(), sourceAbility, game) + .stream() + .map(MageObject::getToughness) + .mapToInt(MageInt::getValue) + .sum(); + } + + @Override + public ControlledCreaturesToughnessValue copy() { + return this; + } + + @Override + public String getMessage() { + return "total toughness of creatures you control"; + } + + @Override + public String toString() { + return "X"; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 5ae3005a566..e9e25436f1b 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -1,12 +1,12 @@ package mage.sets; +import java.util.Arrays; +import java.util.List; + import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ @@ -43,6 +43,9 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Awaken the Honored Dead", 170, Rarity.RARE, mage.cards.a.AwakenTheHonoredDead.class)); cards.add(new SetCardInfo("Barrensteppe Siege", 171, Rarity.RARE, mage.cards.b.BarrensteppeSiege.class)); cards.add(new SetCardInfo("Bearer of Glory", 4, Rarity.COMMON, mage.cards.b.BearerOfGlory.class)); + cards.add(new SetCardInfo("Betor, Kin to All", 172, Rarity.MYTHIC, mage.cards.b.BetorKinToAll.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Betor, Kin to All", 308, Rarity.MYTHIC, mage.cards.b.BetorKinToAll.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Betor, Kin to All", 353, Rarity.MYTHIC, mage.cards.b.BetorKinToAll.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Bewildering Blizzard", 38, Rarity.UNCOMMON, mage.cards.b.BewilderingBlizzard.class)); cards.add(new SetCardInfo("Bloodfell Caves", 250, Rarity.COMMON, mage.cards.b.BloodfellCaves.class)); cards.add(new SetCardInfo("Bloomvine Regent", 136, Rarity.RARE, mage.cards.b.BloomvineRegent.class)); From a09930bb6c66ac686e20ac79e0f8cc3210425f8e Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 08:09:32 -0400 Subject: [PATCH 071/133] [TDC] Implement Adaptive Training Post --- .../mage/cards/a/AdaptiveTrainingPost.java | 50 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 51 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AdaptiveTrainingPost.java diff --git a/Mage.Sets/src/mage/cards/a/AdaptiveTrainingPost.java b/Mage.Sets/src/mage/cards/a/AdaptiveTrainingPost.java new file mode 100644 index 00000000000..332776d8e14 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AdaptiveTrainingPost.java @@ -0,0 +1,50 @@ +package mage.cards.a; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.common.delayed.CopyNextSpellDelayedTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SourceHasCounterCondition; +import mage.abilities.costs.common.RemoveCountersSourceCost; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AdaptiveTrainingPost extends CardImpl { + + private static final Condition condition = new SourceHasCounterCondition(CounterType.CHARGE, 0, 2); + + public AdaptiveTrainingPost(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{U}"); + + // Whenever you cast an instant or sorcery spell, if this artifact has fewer than three charge counters on it, put a charge counter on it. + this.addAbility(new SpellCastControllerTriggeredAbility( + new AddCountersSourceEffect(CounterType.CHARGE.createInstance()), + StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false + ).withInterveningIf(condition)); + + // Remove three charge counters from this artifact: When you next cast an instant or sorcery spell this turn, copy it and you may choose new targets for the copy. + this.addAbility(new SimpleActivatedAbility( + new CreateDelayedTriggeredAbilityEffect(new CopyNextSpellDelayedTriggeredAbility()), + new RemoveCountersSourceCost(CounterType.CHARGE.createInstance(3)) + )); + } + + private AdaptiveTrainingPost(final AdaptiveTrainingPost card) { + super(card); + } + + @Override + public AdaptiveTrainingPost copy() { + return new AdaptiveTrainingPost(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 8cba642f9e3..a34c8418b8e 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -21,6 +21,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Abrade", 203, Rarity.UNCOMMON, mage.cards.a.Abrade.class)); cards.add(new SetCardInfo("Access Tunnel", 337, Rarity.UNCOMMON, mage.cards.a.AccessTunnel.class)); + cards.add(new SetCardInfo("Adaptive Training Post", 18, Rarity.RARE, mage.cards.a.AdaptiveTrainingPost.class)); cards.add(new SetCardInfo("Adarkar Wastes", 338, Rarity.RARE, mage.cards.a.AdarkarWastes.class)); cards.add(new SetCardInfo("Adeline, Resplendent Cathar", 108, Rarity.RARE, mage.cards.a.AdelineResplendentCathar.class)); cards.add(new SetCardInfo("Amphin Mutineer", 143, Rarity.RARE, mage.cards.a.AmphinMutineer.class)); From d567582cf04a496581abf0c0c4f67054d2cf4181 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 08:12:34 -0400 Subject: [PATCH 072/133] [TDC] Implement Aligned Heart --- Mage.Sets/src/mage/cards/a/AlignedHeart.java | 42 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + .../main/java/mage/counters/CounterType.java | 1 + 3 files changed, 44 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AlignedHeart.java diff --git a/Mage.Sets/src/mage/cards/a/AlignedHeart.java b/Mage.Sets/src/mage/cards/a/AlignedHeart.java new file mode 100644 index 00000000000..f031fd1ead0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AlignedHeart.java @@ -0,0 +1,42 @@ +package mage.cards.a; + +import mage.abilities.Ability; +import mage.abilities.common.FlurryAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CountersSourceCount; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.game.permanent.token.MonasteryMentorToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AlignedHeart extends CardImpl { + + private static final DynamicValue xValue = new CountersSourceCount(CounterType.RALLY); + + public AlignedHeart(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}"); + + // Flurry -- Whenever you cast your second spell each turn, put a rally counter on this enchantment. Then create a 1/1 white Monk creature token with prowess for each rally counter on it. + Ability ability = new FlurryAbility(new AddCountersSourceEffect(CounterType.RALLY.createInstance())); + ability.addEffect(new CreateTokenEffect(new MonasteryMentorToken(), xValue) + .setText("then create a 1/1 white Monk creature token with prowess for each rally counter on it")); + this.addAbility(ability); + } + + private AlignedHeart(final AlignedHeart card) { + super(card); + } + + @Override + public AlignedHeart copy() { + return new AlignedHeart(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index a34c8418b8e..d35465407f9 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -24,6 +24,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Adaptive Training Post", 18, Rarity.RARE, mage.cards.a.AdaptiveTrainingPost.class)); cards.add(new SetCardInfo("Adarkar Wastes", 338, Rarity.RARE, mage.cards.a.AdarkarWastes.class)); cards.add(new SetCardInfo("Adeline, Resplendent Cathar", 108, Rarity.RARE, mage.cards.a.AdelineResplendentCathar.class)); + cards.add(new SetCardInfo("Aligned Heart", 12, Rarity.RARE, mage.cards.a.AlignedHeart.class)); cards.add(new SetCardInfo("Amphin Mutineer", 143, Rarity.RARE, mage.cards.a.AmphinMutineer.class)); cards.add(new SetCardInfo("Ancestral Vision", 144, Rarity.RARE, mage.cards.a.AncestralVision.class)); cards.add(new SetCardInfo("Angel of Invention", 109, Rarity.MYTHIC, mage.cards.a.AngelOfInvention.class)); diff --git a/Mage/src/main/java/mage/counters/CounterType.java b/Mage/src/main/java/mage/counters/CounterType.java index dcb5353e8b3..20f71f266d2 100644 --- a/Mage/src/main/java/mage/counters/CounterType.java +++ b/Mage/src/main/java/mage/counters/CounterType.java @@ -184,6 +184,7 @@ public enum CounterType { PREY("prey"), PUPA("pupa"), RAD("rad"), + RALLY("rally"), REACH("reach"), REJECTION("rejection"), REPAIR("repair"), From f781a3e7d71accdddf9b0e3f91fa44ff75818175 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 08:16:32 -0400 Subject: [PATCH 073/133] [TDC] Implement Canopy Gargantuan --- .../src/mage/cards/c/CanopyGargantuan.java | 83 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 84 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CanopyGargantuan.java diff --git a/Mage.Sets/src/mage/cards/c/CanopyGargantuan.java b/Mage.Sets/src/mage/cards/c/CanopyGargantuan.java new file mode 100644 index 00000000000..c7763975ded --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CanopyGargantuan.java @@ -0,0 +1,83 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.WardAbility; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CanopyGargantuan extends CardImpl { + + public CanopyGargantuan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{G}{G}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(7); + this.toughness = new MageInt(7); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Ward {2} + this.addAbility(new WardAbility(new ManaCostsImpl<>("{2}"))); + + // At the beginning of your upkeep, put a number of +1/+1 counters on each other creature you control equal to that creature's toughness. + this.addAbility(new BeginningOfUpkeepTriggeredAbility(new CanopyGargantuanEffect())); + } + + private CanopyGargantuan(final CanopyGargantuan card) { + super(card); + } + + @Override + public CanopyGargantuan copy() { + return new CanopyGargantuan(this); + } +} + +class CanopyGargantuanEffect extends OneShotEffect { + + CanopyGargantuanEffect() { + super(Outcome.Benefit); + staticText = "put a number of +1/+1 counters on each other creature you control equal to that creature's toughness"; + } + + private CanopyGargantuanEffect(final CanopyGargantuanEffect effect) { + super(effect); + } + + @Override + public CanopyGargantuanEffect copy() { + return new CanopyGargantuanEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (Permanent permanent : game.getBattlefield().getActivePermanents( + StaticFilters.FILTER_OTHER_CONTROLLED_CREATURE, + source.getControllerId(), source, game + )) { + int toughness = permanent.getToughness().getValue(); + if (toughness > 0) { + permanent.addCounters(CounterType.P1P1.createInstance(toughness), source, game); + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index d35465407f9..6e49e548eb5 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -59,6 +59,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Boros Signet", 314, Rarity.UNCOMMON, mage.cards.b.BorosSignet.class)); cards.add(new SetCardInfo("Bountiful Landscape", 342, Rarity.COMMON, mage.cards.b.BountifulLandscape.class)); cards.add(new SetCardInfo("Caldera Pyremaw", 33, Rarity.RARE, mage.cards.c.CalderaPyremaw.class)); + cards.add(new SetCardInfo("Canopy Gargantuan", 45, Rarity.RARE, mage.cards.c.CanopyGargantuan.class)); cards.add(new SetCardInfo("Canopy Vista", 343, Rarity.RARE, mage.cards.c.CanopyVista.class)); cards.add(new SetCardInfo("Canyon Slough", 344, Rarity.RARE, mage.cards.c.CanyonSlough.class)); cards.add(new SetCardInfo("Carven Caryatid", 250, Rarity.UNCOMMON, mage.cards.c.CarvenCaryatid.class)); From dec4970fb465c8fefa83f95cd95e79e9a283897f Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 08:18:47 -0400 Subject: [PATCH 074/133] [TDC] Implement Goldlust Triad --- Mage.Sets/src/mage/cards/g/GoldlustTriad.java | 46 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 47 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/g/GoldlustTriad.java diff --git a/Mage.Sets/src/mage/cards/g/GoldlustTriad.java b/Mage.Sets/src/mage/cards/g/GoldlustTriad.java new file mode 100644 index 00000000000..68e84e4f8cf --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GoldlustTriad.java @@ -0,0 +1,46 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.MyriadAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.game.permanent.token.TreasureToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GoldlustTriad extends CardImpl { + + public GoldlustTriad(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Myriad + this.addAbility(new MyriadAbility()); + + // Whenever this creature deals combat damage to a player, create a Treasure token. + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(new CreateTokenEffect(new TreasureToken()))); + } + + private GoldlustTriad(final GoldlustTriad card) { + super(card); + } + + @Override + public GoldlustTriad copy() { + return new GoldlustTriad(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 6e49e548eb5..5d893c9d0f8 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -136,6 +136,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Glacial Fortress", 367, Rarity.RARE, mage.cards.g.GlacialFortress.class)); cards.add(new SetCardInfo("Glorybringer", 215, Rarity.RARE, mage.cards.g.Glorybringer.class)); cards.add(new SetCardInfo("Goblin Electromancer", 99, Rarity.COMMON, mage.cards.g.GoblinElectromancer.class)); + cards.add(new SetCardInfo("Goldlust Triad", 34, Rarity.RARE, mage.cards.g.GoldlustTriad.class)); cards.add(new SetCardInfo("Goldnight Commander", 117, Rarity.UNCOMMON, mage.cards.g.GoldnightCommander.class)); cards.add(new SetCardInfo("Golgari Rot Farm", 368, Rarity.UNCOMMON, mage.cards.g.GolgariRotFarm.class)); cards.add(new SetCardInfo("Grand Crescendo", 118, Rarity.RARE, mage.cards.g.GrandCrescendo.class)); From 339d7d615060d7698a7482ca6f1f5d39d2ccbb8c Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 08:23:53 -0400 Subject: [PATCH 075/133] [TDC] Implement Jaws of Defeat --- Mage.Sets/src/mage/cards/j/JawsOfDefeat.java | 70 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 71 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/j/JawsOfDefeat.java diff --git a/Mage.Sets/src/mage/cards/j/JawsOfDefeat.java b/Mage.Sets/src/mage/cards/j/JawsOfDefeat.java new file mode 100644 index 00000000000..91ec5094f99 --- /dev/null +++ b/Mage.Sets/src/mage/cards/j/JawsOfDefeat.java @@ -0,0 +1,70 @@ +package mage.cards.j; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAllTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class JawsOfDefeat extends CardImpl { + + public JawsOfDefeat(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{B}"); + + // Whenever a creature you control enters, target opponent loses life equal to the difference between that creature's power and its toughness. + Ability ability = new EntersBattlefieldAllTriggeredAbility( + new JawsOfDefeatEffect(), StaticFilters.FILTER_CONTROLLED_A_CREATURE + ); + ability.addTarget(new TargetOpponent()); + this.addAbility(ability); + } + + private JawsOfDefeat(final JawsOfDefeat card) { + super(card); + } + + @Override + public JawsOfDefeat copy() { + return new JawsOfDefeat(this); + } +} + +class JawsOfDefeatEffect extends OneShotEffect { + + JawsOfDefeatEffect() { + super(Outcome.Benefit); + staticText = "target opponent loses life equal to the difference between that creature's power and its toughness"; + } + + private JawsOfDefeatEffect(final JawsOfDefeatEffect effect) { + super(effect); + } + + @Override + public JawsOfDefeatEffect copy() { + return new JawsOfDefeatEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(getTargetPointer().getFirst(game, source)); + Permanent permanent = (Permanent) getValue("permanentEnteringBattlefield"); + if (player == null || permanent == null) { + return false; + } + int diff = Math.abs(permanent.getToughness().getValue() - permanent.getPower().getValue()); + return diff > 0 && player.loseLife(diff, game, source, false) > 0; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 5d893c9d0f8..540f71b2aa5 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -166,6 +166,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Izzet Signet", 320, Rarity.COMMON, mage.cards.i.IzzetSignet.class)); cards.add(new SetCardInfo("Jaddi Offshoot", 260, Rarity.UNCOMMON, mage.cards.j.JaddiOffshoot.class)); cards.add(new SetCardInfo("Jarad, Golgari Lich Lord", 293, Rarity.MYTHIC, mage.cards.j.JaradGolgariLichLord.class)); + cards.add(new SetCardInfo("Jaws of Defeat", 27, Rarity.RARE, mage.cards.j.JawsOfDefeat.class)); cards.add(new SetCardInfo("Junji, the Midnight Sky", 183, Rarity.MYTHIC, mage.cards.j.JunjiTheMidnightSky.class)); cards.add(new SetCardInfo("Karplusan Forest", 374, Rarity.RARE, mage.cards.k.KarplusanForest.class)); cards.add(new SetCardInfo("Kaya, Geist Hunter", 294, Rarity.MYTHIC, mage.cards.k.KayaGeistHunter.class)); From fb173a8543dcfcbe99b36d7c4f1745f3223a64b9 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 08:28:29 -0400 Subject: [PATCH 076/133] [TDC] Implement Floral Evoker --- Mage.Sets/src/mage/cards/f/FloralEvoker.java | 55 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 56 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FloralEvoker.java diff --git a/Mage.Sets/src/mage/cards/f/FloralEvoker.java b/Mage.Sets/src/mage/cards/f/FloralEvoker.java new file mode 100644 index 00000000000..29a9d87389b --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FloralEvoker.java @@ -0,0 +1,55 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.LandfallAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.DiscardTargetCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.common.TargetCardInHand; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FloralEvoker extends CardImpl { + + public FloralEvoker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.SNAKE); + this.subtype.add(SubType.DRUID); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Landfall -- Whenever a land you control enters, put a +1/+1 counter on this creature. + this.addAbility(new LandfallAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance()))); + + // {G}, Discard a creature card: Return target land card from your graveyard to the battlefield tapped. + Ability ability = new SimpleActivatedAbility( + new ReturnFromGraveyardToBattlefieldTargetEffect(true), new ManaCostsImpl<>("{G}") + ); + ability.addCost(new DiscardTargetCost(new TargetCardInHand(StaticFilters.FILTER_CARD_CREATURE_A))); + ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_LAND)); + this.addAbility(ability); + } + + private FloralEvoker(final FloralEvoker card) { + super(card); + } + + @Override + public FloralEvoker copy() { + return new FloralEvoker(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 540f71b2aa5..006b8ff492a 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -125,6 +125,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Fetid Heath", 362, Rarity.RARE, mage.cards.f.FetidHeath.class)); cards.add(new SetCardInfo("Fetid Pools", 363, Rarity.RARE, mage.cards.f.FetidPools.class)); cards.add(new SetCardInfo("Flooded Grove", 364, Rarity.RARE, mage.cards.f.FloodedGrove.class)); + cards.add(new SetCardInfo("Floral Evoker", 46, Rarity.RARE, mage.cards.f.FloralEvoker.class)); cards.add(new SetCardInfo("Forbidden Alchemy", 152, Rarity.COMMON, mage.cards.f.ForbiddenAlchemy.class)); cards.add(new SetCardInfo("Foreboding Landscape", 365, Rarity.COMMON, mage.cards.f.ForebodingLandscape.class)); cards.add(new SetCardInfo("Fortified Village", 366, Rarity.RARE, mage.cards.f.FortifiedVillage.class)); From 3c6e055f419147c32e0c36e4599cadcb89f7daa8 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 08:36:38 -0400 Subject: [PATCH 077/133] [TDC] Implement Become the Avalanche --- .../src/mage/cards/b/BecomeTheAvalanche.java | 55 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + .../common/CardsInControllerHandCount.java | 22 +++++--- 3 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/b/BecomeTheAvalanche.java diff --git a/Mage.Sets/src/mage/cards/b/BecomeTheAvalanche.java b/Mage.Sets/src/mage/cards/b/BecomeTheAvalanche.java new file mode 100644 index 00000000000..b8c2d218264 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BecomeTheAvalanche.java @@ -0,0 +1,55 @@ +package mage.cards.b; + +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CardsInControllerHandCount; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Duration; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BecomeTheAvalanche extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledCreaturePermanent("creature you control with power 4 or greater"); + + static { + filter.add(new PowerPredicate(ComparisonType.MORE_THAN, 3)); + } + + private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter); + private static final Hint hint = new ValueHint("Creatures you control with power 4 or greater", xValue); + + public BecomeTheAvalanche(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{G}{G}"); + + // Draw a card for each creature you control with power 4 or greater. Then creatures you control get +X/+X until end of turn, where X is the number of cards in your hand. + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(xValue)); + this.getSpellAbility().addEffect(new BoostControlledEffect( + CardsInControllerHandCount.ANY, CardsInControllerHandCount.ANY, Duration.EndOfTurn + )); + this.getSpellAbility().addHint(hint); + } + + private BecomeTheAvalanche(final BecomeTheAvalanche card) { + super(card); + } + + @Override + public BecomeTheAvalanche copy() { + return new BecomeTheAvalanche(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 006b8ff492a..88724f24d04 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -47,6 +47,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Bastion of Remembrance", 171, Rarity.UNCOMMON, mage.cards.b.BastionOfRemembrance.class)); cards.add(new SetCardInfo("Battlefield Forge", 340, Rarity.RARE, mage.cards.b.BattlefieldForge.class)); cards.add(new SetCardInfo("Beast Within", 249, Rarity.UNCOMMON, mage.cards.b.BeastWithin.class)); + cards.add(new SetCardInfo("Become the Avalanche", 43, Rarity.RARE, mage.cards.b.BecomeTheAvalanche.class)); cards.add(new SetCardInfo("Beetleback Chief", 205, Rarity.UNCOMMON, mage.cards.b.BeetlebackChief.class)); cards.add(new SetCardInfo("Behind the Scenes", 172, Rarity.UNCOMMON, mage.cards.b.BehindTheScenes.class)); cards.add(new SetCardInfo("Betor, Ancestor's Voice", 1, Rarity.MYTHIC, mage.cards.b.BetorAncestorsVoice.class)); diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java index 881e747fb6b..6163ffdf6a2 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInControllerHandCount.java @@ -7,17 +7,21 @@ import mage.abilities.hint.Hint; import mage.abilities.hint.ValueHint; import mage.filter.FilterCard; import mage.filter.StaticFilters; +import mage.game.Controllable; import mage.game.Game; import mage.players.Player; +import java.util.Optional; +import java.util.Set; + public enum CardsInControllerHandCount implements DynamicValue { ANY(StaticFilters.FILTER_CARD_CARDS), CREATURES(StaticFilters.FILTER_CARD_CREATURES), LANDS(StaticFilters.FILTER_CARD_LANDS); - FilterCard filter; - ValueHint hint; + private final FilterCard filter; + private final ValueHint hint; CardsInControllerHandCount(FilterCard filter) { this.filter = filter; @@ -26,13 +30,13 @@ public enum CardsInControllerHandCount implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - if (sourceAbility != null) { - Player controller = game.getPlayer(sourceAbility.getControllerId()); - if (controller != null) { - return controller.getHand().size(); - } - } - return 0; + return Optional + .ofNullable(sourceAbility) + .map(Controllable::getControllerId) + .map(game::getPlayer) + .map(Player::getHand) + .map(Set::size) + .orElse(0); } @Override From 56a05a78436911d21d637b9e9b3776d9a92860b4 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 9 Apr 2025 13:24:57 -0400 Subject: [PATCH 078/133] Rework implementation of anchor words (#13518) * refactor anchor word implementation * fix error * [TDM] Implement Windcrag Siege --- .../src/mage/cards/a/AshlingsPrerogative.java | 104 ++++++------------ .../src/mage/cards/b/BarrensteppeSiege.java | 32 ++---- .../src/mage/cards/b/BattleOfHooverDam.java | 47 ++++---- Mage.Sets/src/mage/cards/c/CitadelSiege.java | 45 +++----- Mage.Sets/src/mage/cards/f/FrontierSiege.java | 34 +++--- .../src/mage/cards/f/FrostcliffSiege.java | 53 ++++----- .../src/mage/cards/g/GlacierwoodSiege.java | 49 +++------ .../mage/cards/i/IsshinTwoHeavensAsOne.java | 64 +---------- .../src/mage/cards/l/LavabrinkVenturer.java | 23 ++-- .../src/mage/cards/m/MirrodinBesieged.java | 37 +++---- .../src/mage/cards/m/MonasterySiege.java | 31 +++--- Mage.Sets/src/mage/cards/o/OutpostSiege.java | 46 ++++---- Mage.Sets/src/mage/cards/p/PalaceSiege.java | 38 +++---- .../mage/cards/p/PhenomenonInvestigators.java | 37 +++---- Mage.Sets/src/mage/cards/r/RootsOfLife.java | 56 +++++----- .../cards/s/StruggleForProjectPurity.java | 29 ++--- Mage.Sets/src/mage/cards/w/WindcragSiege.java | 87 +++++++++++++++ .../mage/cards/w/WulfgarOfIcewindDale.java | 76 +------------ .../src/mage/sets/TarkirDragonstorm.java | 1 + .../single/c19/PramikonSkyRampartTest.java | 20 ++-- .../common/ModeChoiceSourceCondition.java | 26 ----- .../effects/common/ChooseModeEffect.java | 67 ++++++----- ...ermanentsEnterBattlefieldTappedEffect.java | 4 +- .../GainAnchorWordAbilitySourceEffect.java | 52 +++++++++ ...nlyAttackInDirectionRestrictionEffect.java | 35 ++---- ...nalTriggersAttackingReplacementEffect.java | 80 ++++++++++++++ .../main/java/mage/constants/ModeChoice.java | 75 +++++++++++++ 27 files changed, 610 insertions(+), 638 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/w/WindcragSiege.java delete mode 100644 Mage/src/main/java/mage/abilities/condition/common/ModeChoiceSourceCondition.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/continuous/GainAnchorWordAbilitySourceEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/replacement/AdditionalTriggersAttackingReplacementEffect.java create mode 100644 Mage/src/main/java/mage/constants/ModeChoice.java diff --git a/Mage.Sets/src/mage/cards/a/AshlingsPrerogative.java b/Mage.Sets/src/mage/cards/a/AshlingsPrerogative.java index 84d3a3c59a1..56e862bef7e 100644 --- a/Mage.Sets/src/mage/cards/a/AshlingsPrerogative.java +++ b/Mage.Sets/src/mage/cards/a/AshlingsPrerogative.java @@ -1,11 +1,8 @@ package mage.cards.a; -import java.util.UUID; -import mage.abilities.Ability; -import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.PermanentsEnterBattlefieldTappedEffect; import mage.abilities.effects.common.continuous.GainAbilityAllEffect; @@ -14,31 +11,44 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.constants.Zone; +import mage.constants.ModeChoice; +import mage.filter.FilterPermanent; import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; -import mage.game.events.EntersTheBattlefieldEvent; -import mage.game.events.GameEvent; import mage.game.permanent.Permanent; +import java.util.UUID; + /** - * * @author Eirkei */ public final class AshlingsPrerogative extends CardImpl { + private static final FilterPermanent filterMatch + = new FilterCreaturePermanent("each creature with mana value of the chosen quality"); + private static final FilterPermanent filterNotMatch + = new FilterCreaturePermanent("each creature without mana value of the chosen quality"); + + static { + filterMatch.add(AshlingsPrerogativePredicate.WITH); + filterNotMatch.add(AshlingsPrerogativePredicate.WITHOUT); + } + public AshlingsPrerogative(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ENCHANTMENT},"{1}{R}"); + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}"); // As Ashling's Prerogative enters the battlefield, choose odd or even. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Odd or even?", "Odd", "Even"), null, "As {this} enters, choose odd or even. (Zero is even.)", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.ODD, ModeChoice.EVEN))); // Each creature with converted mana cost of the chosen value has haste. - this.addAbility(new SimpleStaticAbility(new AshlingsPrerogativeCorrectOddityEffect())); + this.addAbility(new SimpleStaticAbility(new GainAbilityAllEffect( + HasteAbility.getInstance(), Duration.WhileOnBattlefield, filterMatch + ))); // Each creature without converted mana cost of the chosen value enters the battlefield tapped. - this.addAbility(new SimpleStaticAbility(new AshlingsPrerogativeIncorrectOddityEffect())); - + this.addAbility(new SimpleStaticAbility(new PermanentsEnterBattlefieldTappedEffect(filterNotMatch))); } private AshlingsPrerogative(final AshlingsPrerogative card) { @@ -51,67 +61,23 @@ public final class AshlingsPrerogative extends CardImpl { } } -class AshlingsPrerogativeIncorrectOddityEffect extends PermanentsEnterBattlefieldTappedEffect { +enum AshlingsPrerogativePredicate implements ObjectSourcePlayerPredicate { + WITH(true), + WITHOUT(false); + private final boolean match; - private static final FilterCreaturePermanent creaturefilter = new FilterCreaturePermanent("Each creature without mana value of the chosen quality"); - private static final ModeChoiceSourceCondition oddCondition = new ModeChoiceSourceCondition("Odd"); - - public AshlingsPrerogativeIncorrectOddityEffect() { - super(creaturefilter); - staticText = "Each creature without mana value of the chosen quality enters the battlefield tapped."; - } - - private AshlingsPrerogativeIncorrectOddityEffect(final AshlingsPrerogativeIncorrectOddityEffect effect) { - super(effect); + AshlingsPrerogativePredicate(boolean match) { + this.match = match; } @Override - public boolean applies(GameEvent event, Ability source, Game game) { - int incorrectModResult; - - if (oddCondition.apply(game, source)) { - incorrectModResult = 0; + public boolean apply(ObjectSourcePlayer input, Game game) { + if (ModeChoice.ODD.checkMode(game, input.getSource())) { + return (input.getObject().getManaValue() % 2 == 1) == match; + } else if (ModeChoice.EVEN.checkMode(game, input.getSource())) { + return (input.getObject().getManaValue() % 2 == 0) == match; } else { - incorrectModResult = 1; + return false; } - - Permanent permanent = ((EntersTheBattlefieldEvent) event).getTarget(); - - return permanent != null && creaturefilter.match(permanent, game) && permanent.getManaValue() % 2 == incorrectModResult; - } - - @Override - public AshlingsPrerogativeIncorrectOddityEffect copy() { - return new AshlingsPrerogativeIncorrectOddityEffect(this); - } -} - -class AshlingsPrerogativeCorrectOddityEffect extends GainAbilityAllEffect { - - private static final FilterCreaturePermanent creaturefilter = new FilterCreaturePermanent("Each creature with mana value of the chosen quality"); - private static final ModeChoiceSourceCondition oddCondition = new ModeChoiceSourceCondition("Odd"); - - public AshlingsPrerogativeCorrectOddityEffect() { - super(HasteAbility.getInstance(), Duration.WhileOnBattlefield, creaturefilter); - staticText = "Each creature with mana value of the chosen quality has haste."; - } - private AshlingsPrerogativeCorrectOddityEffect(final AshlingsPrerogativeCorrectOddityEffect effect) { - super(effect); - } - - @Override - protected boolean selectedByRuntimeData(Permanent permanent, Ability source, Game game) { - int correctModResult; - if (oddCondition.apply(game, source)) { - correctModResult = 1; - } else { - correctModResult = 0; - } - return permanent != null && creaturefilter.match(permanent, game) && permanent.getManaValue() % 2 == correctModResult; - } - - @Override - public AshlingsPrerogativeCorrectOddityEffect copy() { - return new AshlingsPrerogativeCorrectOddityEffect(this); } } diff --git a/Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java b/Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java index 5bac6c137fd..b4370e0ab64 100644 --- a/Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java +++ b/Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java @@ -1,17 +1,17 @@ package mage.cards.b; -import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.condition.Condition; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.condition.common.CreatureDiedControlledCondition; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.SacrificeOpponentsEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.effects.common.counter.AddCountersAllEffect; import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.ModeChoice; import mage.counters.CounterType; import mage.filter.StaticFilters; @@ -22,35 +22,25 @@ import java.util.UUID; */ public final class BarrensteppeSiege extends CardImpl { - private static final Condition condition1 = new ModeChoiceSourceCondition("Abzan"); - private static final String rule1 = "&bull Abzan — At the beginning of your end step, " + - "put a +1/+1 counter on each creature you control."; - private static final Condition condition2 = new ModeChoiceSourceCondition("Mardu"); - private static final String rule2 = "&bull Mardu — At the beginning of your end step, " + - "if a creature died under your control this turn, each opponent sacrifices a creature of their choice."; - public BarrensteppeSiege(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}{B}"); // As this enchantment enters, choose Abzan or Mardu. - this.addAbility(new EntersBattlefieldAbility( - new ChooseModeEffect("Abzan or Mardu?", "Abzan", "Mardu"), - null, "As {this} enters, choose Abzan or Mardu.", "" - )); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.ABZAN, ModeChoice.MARDU))); // * Abzan -- At the beginning of your end step, put a +1/+1 counter on each creature you control. - this.addAbility(new ConditionalTriggeredAbility( + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( new BeginningOfEndStepTriggeredAbility(new AddCountersAllEffect( CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE - )), condition1, rule1 - )); + )), ModeChoice.ABZAN + ))); // * Mardu -- At the beginning of your end step, if a creature died under your control this turn, each opponent sacrifices a creature of their choice. - this.addAbility(new ConditionalTriggeredAbility( + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( new BeginningOfEndStepTriggeredAbility( new SacrificeOpponentsEffect(StaticFilters.FILTER_PERMANENT_CREATURE) - ).withInterveningIf(CreatureDiedControlledCondition.instance), condition2, rule2 - )); + ).withInterveningIf(CreatureDiedControlledCondition.instance), ModeChoice.MARDU + ))); } private BarrensteppeSiege(final BarrensteppeSiege card) { diff --git a/Mage.Sets/src/mage/cards/b/BattleOfHooverDam.java b/Mage.Sets/src/mage/cards/b/BattleOfHooverDam.java index fa2a607a372..b8e39e72ee1 100644 --- a/Mage.Sets/src/mage/cards/b/BattleOfHooverDam.java +++ b/Mage.Sets/src/mage/cards/b/BattleOfHooverDam.java @@ -1,33 +1,35 @@ package mage.cards.b; -import java.util.UUID; - import mage.abilities.Ability; import mage.abilities.common.AsEntersBattlefieldAbility; -import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; import mage.abilities.common.DiesCreatureTriggeredAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldWithCounterTargetEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.ComparisonType; +import mage.constants.ModeChoice; import mage.counters.CounterType; +import mage.filter.FilterCard; import mage.filter.StaticFilters; import mage.filter.common.FilterCreatureCard; import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.target.common.TargetCardInYourGraveyard; import mage.target.common.TargetControlledCreaturePermanent; +import java.util.UUID; + /** * @author Cguy7777 */ public final class BattleOfHooverDam extends CardImpl { - private static final FilterCreatureCard filter = new FilterCreatureCard(); + private static final FilterCard filter = new FilterCreatureCard(); static { filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 4)); @@ -37,32 +39,23 @@ public final class BattleOfHooverDam extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{W}"); // As Battle of Hoover Dam enters the battlefield, choose NCR or Legion. - this.addAbility(new AsEntersBattlefieldAbility( - new ChooseModeEffect("NCR or Legion?", "NCR", "Legion"))); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.NCR, ModeChoice.LEGION))); // * NCR -- At the beginning of your end step, return target creature card with mana value 3 or less // from your graveyard to the battlefield with a finality counter on it. - Ability ncrAbility = new ConditionalTriggeredAbility( - new BeginningOfEndStepTriggeredAbility( - new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(CounterType.FINALITY.createInstance()) - ), - new ModeChoiceSourceCondition("NCR"), - "&bull NCR — At the beginning of your end step, return target creature card with " + - "mana value 3 or less from your graveyard to the battlefield with a finality counter on it."); - ncrAbility.addTarget(new TargetCardInYourGraveyard(filter)); - this.addAbility(ncrAbility); + Ability ability = new BeginningOfEndStepTriggeredAbility( + new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(CounterType.FINALITY.createInstance()) + ); + ability.addTarget(new TargetCardInYourGraveyard(filter)); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.NCR))); // * Legion -- Whenever a creature you control dies, put two +1/+1 counters on target creature you control. - Ability legionAbility = new ConditionalTriggeredAbility( - new DiesCreatureTriggeredAbility( - new AddCountersTargetEffect(CounterType.P1P1.createInstance(2)), - false, - StaticFilters.FILTER_CONTROLLED_A_CREATURE), - new ModeChoiceSourceCondition("Legion"), - "&bull Legion — Whenever a creature you control dies, " + - "put two +1/+1 counters on target creature you control."); - legionAbility.addTarget(new TargetControlledCreaturePermanent()); - this.addAbility(legionAbility); + ability = new DiesCreatureTriggeredAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance(2)), + false, StaticFilters.FILTER_CONTROLLED_A_CREATURE + ); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.LEGION))); } private BattleOfHooverDam(final BattleOfHooverDam card) { diff --git a/Mage.Sets/src/mage/cards/c/CitadelSiege.java b/Mage.Sets/src/mage/cards/c/CitadelSiege.java index 793ee47f43d..edbafe2556e 100644 --- a/Mage.Sets/src/mage/cards/c/CitadelSiege.java +++ b/Mage.Sets/src/mage/cards/c/CitadelSiege.java @@ -1,23 +1,20 @@ - package mage.cards.c; import mage.abilities.Ability; -import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; -import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.ModeChoice; import mage.constants.TargetController; import mage.counters.CounterType; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.Predicate; -import mage.game.Game; -import mage.game.permanent.Permanent; import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetCreaturePermanent; @@ -28,36 +25,29 @@ import java.util.UUID; */ public final class CitadelSiege extends CardImpl { - private static final String ruleTrigger1 = "&bull Khans — At the beginning of combat on your turn, put two +1/+1 counters on target creature you control."; - private static final String ruleTrigger2 = "&bull Dragons — At the beginning of combat on each opponent's turn, tap target creature that player controls."; private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature controlled by the active player"); static { - filter.add(CitadelSiegePredicate.instance); + filter.add(TargetController.ACTIVE.getControllerPredicate()); } public CitadelSiege(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}{W}"); // As Citadel Siege enters the battlefield, choose Khans or Dragons. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Khans or Dragons?", "Khans", "Dragons"), null, - "As {this} enters, choose Khans or Dragons.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.KHANS, ModeChoice.DRAGONS))); // * Khans - At the beginning of combat on your turn, put two +1/+1 counters on target creature you control. - Ability ability = new ConditionalTriggeredAbility( - new BeginningOfCombatTriggeredAbility(new AddCountersTargetEffect(CounterType.P1P1.createInstance(2))), - new ModeChoiceSourceCondition("Khans"), - ruleTrigger1); + Ability ability = new BeginningOfCombatTriggeredAbility(new AddCountersTargetEffect(CounterType.P1P1.createInstance(2))); ability.addTarget(new TargetControlledCreaturePermanent()); - this.addAbility(ability); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.KHANS))); // * Dragons - At the beginning of combat on each opponent's turn, tap target creature that player controls. - ability = new ConditionalTriggeredAbility( - new BeginningOfCombatTriggeredAbility(TargetController.OPPONENT, new TapTargetEffect(), false), - new ModeChoiceSourceCondition("Dragons"), - ruleTrigger2); + ability = new BeginningOfCombatTriggeredAbility( + TargetController.OPPONENT, new TapTargetEffect("tap target creature that player controls"), false + ); ability.addTarget(new TargetCreaturePermanent(filter)); - this.addAbility(ability); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.DRAGONS))); } private CitadelSiege(final CitadelSiege card) { @@ -69,12 +59,3 @@ public final class CitadelSiege extends CardImpl { return new CitadelSiege(this); } } - -enum CitadelSiegePredicate implements Predicate { - instance; - - @Override - public boolean apply(Permanent input, Game game) { - return input.getControllerId().equals(game.getActivePlayerId()); - } -} diff --git a/Mage.Sets/src/mage/cards/f/FrontierSiege.java b/Mage.Sets/src/mage/cards/f/FrontierSiege.java index 87b2e5bfcfa..c18360d64e2 100644 --- a/Mage.Sets/src/mage/cards/f/FrontierSiege.java +++ b/Mage.Sets/src/mage/cards/f/FrontierSiege.java @@ -3,17 +3,18 @@ package mage.cards.f; import mage.Mana; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.EntersBattlefieldAllTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.effects.mana.AddManaToManaPoolSourceControllerEffect; import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; +import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.AbilityPredicate; @@ -29,37 +30,28 @@ import java.util.UUID; */ public final class FrontierSiege extends CardImpl { - private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("a creature with flying"); + private static final FilterPermanent filter = new FilterCreaturePermanent("a creature with flying you control"); static { filter.add(TargetController.YOU.getControllerPredicate()); filter.add(new AbilityPredicate(FlyingAbility.class)); } - private static final String ruleTrigger1 = "&bull Khans — At the beginning of each of your main phases, add {G}{G}."; - private static final String ruleTrigger2 = "&bull Dragons — Whenever a creature with flying you control enters, you may have it fight target creature you don't control."; - public FrontierSiege(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}"); // As Frontier Siege enters the battlefield, choose Khans or Dragons. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Khans or Dragons?", "Khans", "Dragons"), null, - "As {this} enters, choose Khans or Dragons.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.KHANS, ModeChoice.DRAGONS))); // * Khans - At the beginning of each of your main phases, add {G}{G}. - this.addAbility(new ConditionalTriggeredAbility( - new FrontierSiegeKhansTriggeredAbility(), - new ModeChoiceSourceCondition("Khans"), - ruleTrigger1)); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(new FrontierSiegeKhansTriggeredAbility(), ModeChoice.KHANS))); // * Dragons - Whenever a creature with flying you control enters, you may have it fight target creature you don't control. - Ability ability2 = new ConditionalTriggeredAbility( - new EntersBattlefieldControlledTriggeredAbility(Zone.BATTLEFIELD, new FrontierSiegeFightEffect(), filter, true, SetTargetPointer.PERMANENT), - new ModeChoiceSourceCondition("Dragons"), - ruleTrigger2); - ability2.addTarget(new TargetCreaturePermanent(StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); - this.addAbility(ability2); - + Ability ability = new EntersBattlefieldAllTriggeredAbility( + Zone.BATTLEFIELD, new FrontierSiegeFightEffect(), filter, true, SetTargetPointer.PERMANENT + ); + ability.addTarget(new TargetCreaturePermanent(StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.DRAGONS))); } private FrontierSiege(final FrontierSiege card) { diff --git a/Mage.Sets/src/mage/cards/f/FrostcliffSiege.java b/Mage.Sets/src/mage/cards/f/FrostcliffSiege.java index 61ce64fc2b5..1f4199d77e6 100644 --- a/Mage.Sets/src/mage/cards/f/FrostcliffSiege.java +++ b/Mage.Sets/src/mage/cards/f/FrostcliffSiege.java @@ -1,65 +1,54 @@ package mage.cards.f; -import java.util.UUID; - import mage.abilities.Ability; -import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.OneOrMoreCombatDamagePlayerTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalContinuousEffect; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.continuous.BoostControlledEffect; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.keyword.HasteAbility; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.ModeChoice; import mage.filter.StaticFilters; +import java.util.UUID; + /** - * * @author androosss */ public final class FrostcliffSiege extends CardImpl { - private static final String rule1Trigger = "• Jeskai — Whenever one or more creatures you control deal combat damage to a player, draw a card."; - public FrostcliffSiege(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{U}{R}"); - // As this enchantment enters, choose Jeskai or Temur. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Jeskai or Temur?", "Jeskai", "Temur"), null, - "As {this} enters, choose Jeskai or Temur.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.JESKAI, ModeChoice.TEMUR))); // * Jeskai -- Whenever one or more creatures you control deal combat damage to a player, draw a card. - this.addAbility(new ConditionalTriggeredAbility( - new OneOrMoreCombatDamagePlayerTriggeredAbility(new DrawCardSourceControllerEffect(1)), - new ModeChoiceSourceCondition("Jeskai"), - rule1Trigger)); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new OneOrMoreCombatDamagePlayerTriggeredAbility(new DrawCardSourceControllerEffect(1)), ModeChoice.JESKAI + ))); // * Temur -- Creatures you control get +1/+0 and have trample and haste. - Ability temurAbility = new SimpleStaticAbility(new ConditionalContinuousEffect( - new BoostControlledEffect(1, 0, Duration.WhileOnBattlefield), - new ModeChoiceSourceCondition("Temur"), - "• Temur — Creatures you control get +1/+0" - )); - temurAbility.addEffect(new ConditionalContinuousEffect( - new GainAbilityControlledEffect(TrampleAbility.getInstance(), Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURES), - new ModeChoiceSourceCondition("Temur"), - "and have trample" - )); - temurAbility.addEffect(new ConditionalContinuousEffect( - new GainAbilityControlledEffect(HasteAbility.getInstance(), Duration.WhileOnBattlefield, StaticFilters.FILTER_PERMANENT_CREATURES), - new ModeChoiceSourceCondition("Temur"), - "and haste." - )); - this.addAbility(temurAbility); + Ability ability = new SimpleStaticAbility( + new BoostControlledEffect(1, 0, Duration.WhileOnBattlefield) + ); + ability.addEffect(new GainAbilityControlledEffect( + TrampleAbility.getInstance(), Duration.WhileOnBattlefield, + StaticFilters.FILTER_PERMANENT_CREATURES + ).setText("and have trample")); + ability.addEffect(new GainAbilityControlledEffect( + HasteAbility.getInstance(), Duration.WhileOnBattlefield, + StaticFilters.FILTER_PERMANENT_CREATURES + ).setText("and haste")); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.TEMUR))); } private FrostcliffSiege(final FrostcliffSiege card) { diff --git a/Mage.Sets/src/mage/cards/g/GlacierwoodSiege.java b/Mage.Sets/src/mage/cards/g/GlacierwoodSiege.java index 693845f7cd6..6452cc88eb6 100644 --- a/Mage.Sets/src/mage/cards/g/GlacierwoodSiege.java +++ b/Mage.Sets/src/mage/cards/g/GlacierwoodSiege.java @@ -1,58 +1,45 @@ package mage.cards.g; -import java.util.UUID; - -import mage.abilities.TriggeredAbility; -import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.Ability; +import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalAsThoughEffect; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.MillCardsTargetEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.effects.common.ruleModifying.PlayFromGraveyardControllerEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.filter.FilterSpell; -import mage.filter.predicate.Predicates; +import mage.constants.ModeChoice; +import mage.filter.StaticFilters; import mage.target.TargetPlayer; +import java.util.UUID; + /** - * * @author androosss */ public final class GlacierwoodSiege extends CardImpl { - private static final FilterSpell filter = new FilterSpell("an instant or sorcery spell"); - - static { - filter.add(Predicates.or( - CardType.INSTANT.getPredicate(), - CardType.SORCERY.getPredicate())); - } - public GlacierwoodSiege(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{G}{U}"); - + // As this enchantment enters, choose Temur or Sultai. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Temur or Sultai?", "Temur", "Sultai"), null, - "As {this} enters, choose Temur or Sultai.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.TEMUR, ModeChoice.SULTAI))); // * Temur -- Whenever you cast an instant or sorcery spell, target player mills four cards. - TriggeredAbility temurAbility = new SpellCastControllerTriggeredAbility(new MillCardsTargetEffect(4), filter, false); - temurAbility.addTarget(new TargetPlayer()); - this.addAbility(new ConditionalTriggeredAbility( - temurAbility, - new ModeChoiceSourceCondition("Temur"), - "• Temur — Whenever you cast an instant or sorcery spell, target player mills four cards.")); + Ability ability = new SpellCastControllerTriggeredAbility( + new MillCardsTargetEffect(4), + StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false + ); + ability.addTarget(new TargetPlayer()); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.TEMUR))); // * Sultai -- You may play lands from your graveyard. - this.addAbility(new SimpleStaticAbility(new ConditionalAsThoughEffect( - PlayFromGraveyardControllerEffect.playLands(), - new ModeChoiceSourceCondition("Sultai")).setText("• Sultai — You may play lands from your graveyard.") - )); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + PlayFromGraveyardControllerEffect.playLands(), ModeChoice.SULTAI + ))); } private GlacierwoodSiege(final GlacierwoodSiege card) { diff --git a/Mage.Sets/src/mage/cards/i/IsshinTwoHeavensAsOne.java b/Mage.Sets/src/mage/cards/i/IsshinTwoHeavensAsOne.java index 31f9cf1c370..689ed56b8b3 100644 --- a/Mage.Sets/src/mage/cards/i/IsshinTwoHeavensAsOne.java +++ b/Mage.Sets/src/mage/cards/i/IsshinTwoHeavensAsOne.java @@ -1,16 +1,13 @@ package mage.cards.i; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.replacement.AdditionalTriggersAttackingReplacementEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.NumberOfTriggersEvent; -import mage.game.permanent.Permanent; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; import java.util.UUID; @@ -29,7 +26,7 @@ public final class IsshinTwoHeavensAsOne extends CardImpl { this.toughness = new MageInt(4); // If a creature attacking causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time. - this.addAbility(new SimpleStaticAbility(new IsshinTwoHeavensAsOneEffect())); + this.addAbility(new SimpleStaticAbility(new AdditionalTriggersAttackingReplacementEffect(false))); } private IsshinTwoHeavensAsOne(final IsshinTwoHeavensAsOne card) { @@ -41,54 +38,3 @@ public final class IsshinTwoHeavensAsOne extends CardImpl { return new IsshinTwoHeavensAsOne(this); } } - -class IsshinTwoHeavensAsOneEffect extends ReplacementEffectImpl { - - IsshinTwoHeavensAsOneEffect() { - super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "if a creature attacking causes a triggered ability " + - "of a permanent you control to trigger, that ability triggers an additional time"; - } - - private IsshinTwoHeavensAsOneEffect(final IsshinTwoHeavensAsOneEffect effect) { - super(effect); - } - - @Override - public IsshinTwoHeavensAsOneEffect copy() { - return new IsshinTwoHeavensAsOneEffect(this); - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.NUMBER_OF_TRIGGERS; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - NumberOfTriggersEvent numberOfTriggersEvent = (NumberOfTriggersEvent) event; - Permanent sourcePermanent = game.getPermanent(numberOfTriggersEvent.getSourceId()); - if (sourcePermanent == null || !sourcePermanent.isControlledBy(source.getControllerId())) { - return false; - } - - GameEvent sourceEvent = numberOfTriggersEvent.getSourceEvent(); - if (sourceEvent == null) { - return false; - } - - switch (sourceEvent.getType()) { - case ATTACKER_DECLARED: - case DECLARED_ATTACKERS: - case DEFENDER_ATTACKED: - return true; - } - return false; - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - event.setAmount(event.getAmount() + 1); - return false; - } -} diff --git a/Mage.Sets/src/mage/cards/l/LavabrinkVenturer.java b/Mage.Sets/src/mage/cards/l/LavabrinkVenturer.java index 97c807b7924..a51d0ff8407 100644 --- a/Mage.Sets/src/mage/cards/l/LavabrinkVenturer.java +++ b/Mage.Sets/src/mage/cards/l/LavabrinkVenturer.java @@ -10,6 +10,7 @@ import mage.abilities.keyword.ProtectionAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.ModeChoice; import mage.constants.SubType; import mage.filter.FilterObject; import mage.filter.predicate.mageobject.ManaValueParityPredicate; @@ -31,10 +32,7 @@ public final class LavabrinkVenturer extends CardImpl { this.toughness = new MageInt(3); // As Lavabrink Venturer enters the battlefield, choose odd or even. - this.addAbility(new AsEntersBattlefieldAbility( - new ChooseModeEffect("Odd or even?", "Odd", "Even"), - "choose odd or even. (Zero is even.)" - )); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.ODD, ModeChoice.EVEN))); // Lavabrink Venturer has protection from each converted mana cost of the chosen value. this.addAbility(new SimpleStaticAbility(new LavabrinkVenturerEffect())); @@ -75,20 +73,13 @@ class LavabrinkVenturerEffect extends GainAbilitySourceEffect { @Override public boolean apply(Game game, Ability source) { - String chosenMode = (String) game.getState().getValue(source.getSourceId() + "_modeChoice"); - if (chosenMode == null) { + if (ModeChoice.ODD.checkMode(game, source)) { + this.ability = new ProtectionAbility(oddFilter); + } else if (ModeChoice.EVEN.checkMode(game, source)) { + this.ability = new ProtectionAbility(evenFilter); + } else { return false; } - switch (chosenMode) { - case "Odd": - this.ability = new ProtectionAbility(oddFilter); - break; - case "Even": - this.ability = new ProtectionAbility(evenFilter); - break; - default: - return false; - } return super.apply(game, source); } diff --git a/Mage.Sets/src/mage/cards/m/MirrodinBesieged.java b/Mage.Sets/src/mage/cards/m/MirrodinBesieged.java index 6af67534394..459afeaa1d6 100644 --- a/Mage.Sets/src/mage/cards/m/MirrodinBesieged.java +++ b/Mage.Sets/src/mage/cards/m/MirrodinBesieged.java @@ -1,22 +1,21 @@ package mage.cards.m; import mage.abilities.Ability; -import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; -import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.ModeChoice; import mage.constants.Outcome; -import mage.filter.FilterSpell; import mage.filter.StaticFilters; -import mage.filter.common.FilterArtifactSpell; import mage.game.Game; import mage.game.permanent.token.MyrToken; import mage.players.Player; @@ -29,33 +28,23 @@ import java.util.UUID; */ public final class MirrodinBesieged extends CardImpl { - private static final String ruleTrigger1 = "&bull Mirran — Whenever you cast an artifact spell, " + - "create a 1/1 colorless Myr artifact creature token."; - private static final String ruleTrigger2 = "&bull Phyrexian — At the beginning of your end step, " + - "draw a card, then discard a card. Then if there are fifteen or more artifact cards in your graveyard, " + - "target opponent loses the game."; - private static final FilterSpell filter = new FilterArtifactSpell(); - public MirrodinBesieged(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}"); // As Mirrodin Besieged enters the battlefield, choose Mirran or Phyrexian. - this.addAbility(new EntersBattlefieldAbility( - new ChooseModeEffect("Mirran or Phyrexian?", "Mirran", "Phyrexian"), - null, "As {this} enters, choose Mirran or Phyrexian.", "" - )); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.MIRRAN, ModeChoice.PHYREXIAN))); // • Mirran — Whenever you cast an artifact spell, create a 1/1 colorless Myr artifact creature token. - this.addAbility(new ConditionalTriggeredAbility(new SpellCastControllerTriggeredAbility( - new CreateTokenEffect(new MyrToken()), filter, false - ), new ModeChoiceSourceCondition("Mirran"), ruleTrigger1)); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new SpellCastControllerTriggeredAbility( + new CreateTokenEffect(new MyrToken()), StaticFilters.FILTER_SPELL_AN_ARTIFACT, false + ), ModeChoice.MIRRAN + ))); // • Phyrexian — At the beginning of your end step, draw a card, then discard a card. Then if there are fifteen or more artifact cards in your graveyard, target opponent loses the game. - Ability ability = new ConditionalTriggeredAbility(new BeginningOfEndStepTriggeredAbility( - new MirrodinBesiegedEffect() - ), new ModeChoiceSourceCondition("Phyrexian"), ruleTrigger2); + Ability ability = new BeginningOfEndStepTriggeredAbility(new MirrodinBesiegedEffect()); ability.addTarget(new TargetOpponent()); - this.addAbility(ability); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.PHYREXIAN))); } private MirrodinBesieged(final MirrodinBesieged card) { diff --git a/Mage.Sets/src/mage/cards/m/MonasterySiege.java b/Mage.Sets/src/mage/cards/m/MonasterySiege.java index ab0074437dd..69bab804c13 100644 --- a/Mage.Sets/src/mage/cards/m/MonasterySiege.java +++ b/Mage.Sets/src/mage/cards/m/MonasterySiege.java @@ -2,14 +2,13 @@ package mage.cards.m; import mage.abilities.Ability; import mage.abilities.SpellAbility; -import mage.abilities.triggers.BeginningOfDrawTriggeredAbility; -import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.triggers.BeginningOfDrawTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -31,17 +30,19 @@ public final class MonasterySiege extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}"); // As Monastery Siege enters the battlefield, choose Khans or Dragons. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Khans or Dragons?", "Khans", "Dragons"), null, - "As {this} enters, choose Khans or Dragons.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.KHANS, ModeChoice.DRAGONS))); // * Khans - At the beginning of your draw step, draw an additional card, then discard a card. - this.addAbility(new ConditionalTriggeredAbility( - new BeginningOfDrawTriggeredAbility(new DrawDiscardControllerEffect(1, 1), false), - new ModeChoiceSourceCondition("Khans"), - "• Khans — At the beginning of your draw step, draw an additional card, then discard a card.")); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new BeginningOfDrawTriggeredAbility( + new DrawDiscardControllerEffect(1, 1), false + ), ModeChoice.KHANS + ))); // * Dragons - Spells your opponents cast that target you or a permanent you control cost {2} more to cast. - this.addAbility(new SimpleStaticAbility(new MonasterySiegeCostIncreaseEffect())); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new MonasterySiegeCostIncreaseEffect(), ModeChoice.DRAGONS + ))); } private MonasterySiege(final MonasterySiege card) { @@ -56,11 +57,9 @@ public final class MonasterySiege extends CardImpl { class MonasterySiegeCostIncreaseEffect extends CostModificationEffectImpl { - private static final ModeChoiceSourceCondition modeDragons = new ModeChoiceSourceCondition("Dragons"); - MonasterySiegeCostIncreaseEffect() { super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.INCREASE_COST); - staticText = "• Dragons — Spells your opponents cast that target you or a permanent you control cost {2} more to cast"; + staticText = "spells your opponents cast that target you or a permanent you control cost {2} more to cast"; } private MonasterySiegeCostIncreaseEffect(final MonasterySiegeCostIncreaseEffect effect) { @@ -75,10 +74,6 @@ class MonasterySiegeCostIncreaseEffect extends CostModificationEffectImpl { @Override public boolean applies(Ability abilityToModify, Ability source, Game game) { - if (!modeDragons.apply(game, source)) { - return false; - } - if (!(abilityToModify instanceof SpellAbility)) { return false; } diff --git a/Mage.Sets/src/mage/cards/o/OutpostSiege.java b/Mage.Sets/src/mage/cards/o/OutpostSiege.java index 449ac8d4711..47827489fe1 100644 --- a/Mage.Sets/src/mage/cards/o/OutpostSiege.java +++ b/Mage.Sets/src/mage/cards/o/OutpostSiege.java @@ -1,54 +1,48 @@ package mage.cards.o; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; -import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.common.ZoneChangeAllTriggeredAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.LeavesBattlefieldAllTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.ExileTopXMayPlayUntilEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.constants.Zone; -import mage.filter.common.FilterControlledCreaturePermanent; +import mage.constants.ModeChoice; +import mage.filter.StaticFilters; import mage.target.common.TargetAnyTarget; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class OutpostSiege extends CardImpl { - private static final String ruleTrigger1 = "&bull Khans — At the beginning of your upkeep, exile the top card of your library. Until end of turn, you may play that card."; - private static final String ruleTrigger2 = "&bull Dragons — Whenever a creature you control leaves the battlefield, {this} deals 1 damage to any target."; - public OutpostSiege(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{R}"); // As Outpost Siege enters the battlefield, choose Khans or Dragons. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Khans or Dragons?", "Khans", "Dragons"), null, - "As {this} enters, choose Khans or Dragons.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.KHANS, ModeChoice.DRAGONS))); // * Khans - At the beginning of your upkeep, exile the top card of your library. Until end of turn, you may play that card. - this.addAbility(new ConditionalTriggeredAbility( - new BeginningOfUpkeepTriggeredAbility(new ExileTopXMayPlayUntilEffect(1, Duration.EndOfTurn)), - new ModeChoiceSourceCondition("Khans"), - ruleTrigger1)); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new BeginningOfUpkeepTriggeredAbility( + new ExileTopXMayPlayUntilEffect(1, Duration.EndOfTurn) + ), ModeChoice.KHANS + ))); // * Dragons - Whenever a creature you control leaves the battlefield, Outpost Siege deals 1 damage to any target. - Ability ability2 = new ConditionalTriggeredAbility( - new ZoneChangeAllTriggeredAbility(Zone.BATTLEFIELD, Zone.BATTLEFIELD, null, new DamageTargetEffect(1), - new FilterControlledCreaturePermanent(), "", false), - new ModeChoiceSourceCondition("Dragons"), - ruleTrigger2); - ability2.addTarget(new TargetAnyTarget()); - this.addAbility(ability2); - + Ability ability = new LeavesBattlefieldAllTriggeredAbility( + new DamageTargetEffect(1), StaticFilters.FILTER_CONTROLLED_A_CREATURE + ); + ability.addTarget(new TargetAnyTarget()); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability, ModeChoice.DRAGONS))); } private OutpostSiege(final OutpostSiege card) { diff --git a/Mage.Sets/src/mage/cards/p/PalaceSiege.java b/Mage.Sets/src/mage/cards/p/PalaceSiege.java index 445a1411d5d..24b4a86a524 100644 --- a/Mage.Sets/src/mage/cards/p/PalaceSiege.java +++ b/Mage.Sets/src/mage/cards/p/PalaceSiege.java @@ -1,55 +1,43 @@ package mage.cards.p; -import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; -import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; -import mage.abilities.effects.Effect; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.GainLifeEffect; import mage.abilities.effects.common.LoseLifeOpponentsEffect; import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.ModeChoice; import mage.filter.StaticFilters; import mage.target.common.TargetCardInYourGraveyard; +import java.util.UUID; + /** - * * @author LevelX2 */ public final class PalaceSiege extends CardImpl { - private static final String ruleTrigger1 = "&bull Khans — At the beginning of your upkeep, return target creature card from your graveyard to your hand."; - private static final String ruleTrigger2 = "&bull Dragons — At the beginning of your upkeep, each opponent loses 2 life and you gain 2 life."; - public PalaceSiege(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{B}{B}"); // As Palace Siege enters the battlefield, choose Khans or Dragons. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Khans or Dragons?", "Khans", "Dragons"), null, - "As {this} enters, choose Khans or Dragons.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.KHANS, ModeChoice.DRAGONS))); // * Khans - At the beginning of your upkeep, return target creature card from your graveyard to your hand. - Ability ability1 = new ConditionalTriggeredAbility( - new BeginningOfUpkeepTriggeredAbility(new ReturnFromGraveyardToHandTargetEffect()), - new ModeChoiceSourceCondition("Khans"), - ruleTrigger1); + Ability ability1 = new BeginningOfUpkeepTriggeredAbility(new ReturnFromGraveyardToHandTargetEffect()); ability1.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE)); - this.addAbility(ability1); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability1, ModeChoice.KHANS))); // * Dragons - At the beginning of your upkeep, each opponent loses 2 life and you gain 2 life. - Ability ability2 = new ConditionalTriggeredAbility( - new BeginningOfUpkeepTriggeredAbility(new LoseLifeOpponentsEffect(2)), - new ModeChoiceSourceCondition("Dragons"), - ruleTrigger2); - Effect effect = new GainLifeEffect(2); - effect.setText("and you gain 2 life"); - ability2.addEffect(effect); - this.addAbility(ability2); + Ability ability2 = new BeginningOfUpkeepTriggeredAbility(new LoseLifeOpponentsEffect(2)); + ability2.addEffect(new GainLifeEffect(2).setText("and you gain 2 life")); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(ability2, ModeChoice.DRAGONS))); } diff --git a/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java b/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java index 366fe7c8fa9..2170f980cb3 100644 --- a/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java +++ b/Mage.Sets/src/mage/cards/p/PhenomenonInvestigators.java @@ -1,23 +1,21 @@ package mage.cards.p; -import java.util.UUID; - import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.DiesCreatureTriggeredAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.CostImpl; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; -import mage.constants.*; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.filter.common.FilterNonlandPermanent; @@ -27,6 +25,8 @@ import mage.game.permanent.token.HorrorEnchantmentCreatureToken; import mage.players.Player; import mage.target.TargetPermanent; +import java.util.UUID; + /** * @author Cguy7777 */ @@ -41,28 +41,23 @@ public final class PhenomenonInvestigators extends CardImpl { this.toughness = new MageInt(4); // As Phenomenon Investigators enters, choose Believe or Doubt. - this.addAbility(new AsEntersBattlefieldAbility( - new ChooseModeEffect("Believe or Doubt?", "Believe", "Doubt"))); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.BELIEVE, ModeChoice.DOUBT))); // * Believe -- Whenever a nontoken creature you control dies, create a 2/2 black Horror enchantment creature token. - this.addAbility(new ConditionalTriggeredAbility( + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( new DiesCreatureTriggeredAbility( new CreateTokenEffect(new HorrorEnchantmentCreatureToken()), - false, - StaticFilters.FILTER_CONTROLLED_CREATURE_NON_TOKEN), - new ModeChoiceSourceCondition("Believe"), - "&bull Believe — Whenever a nontoken creature you control dies, " + - "create a 2/2 black Horror enchantment creature token.")); + false, StaticFilters.FILTER_CONTROLLED_CREATURE_NON_TOKEN + ), ModeChoice.BELIEVE + ))); // * Doubt -- At the beginning of your end step, you may return a nonland permanent you own to your hand. If you do, draw a card. - this.addAbility(new ConditionalTriggeredAbility( - new BeginningOfEndStepTriggeredAbility( - new DoIfCostPaid( - new DrawCardSourceControllerEffect(1), - new PhenomenonInvestigatorsReturnCost())), - new ModeChoiceSourceCondition("Doubt"), - "&bull Doubt — At the beginning of your end step, you may return a nonland permanent " + - "you own to your hand. If you do, draw a card.")); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new BeginningOfEndStepTriggeredAbility(new DoIfCostPaid( + new DrawCardSourceControllerEffect(1), + new PhenomenonInvestigatorsReturnCost() + )), ModeChoice.DOUBT + ))); } private PhenomenonInvestigators(final PhenomenonInvestigators card) { diff --git a/Mage.Sets/src/mage/cards/r/RootsOfLife.java b/Mage.Sets/src/mage/cards/r/RootsOfLife.java index facf5d46af3..933133ec13b 100644 --- a/Mage.Sets/src/mage/cards/r/RootsOfLife.java +++ b/Mage.Sets/src/mage/cards/r/RootsOfLife.java @@ -1,57 +1,44 @@ package mage.cards.r; -import java.util.UUID; +import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.BecomesTappedTriggeredAbility; -import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.common.ChooseModeEffect; import mage.abilities.effects.common.GainLifeEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.ModeChoice; import mage.constants.SubType; import mage.constants.TargetController; import mage.filter.FilterPermanent; +import mage.filter.common.FilterLandPermanent; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.UUID; /** - * * @author fubs */ public final class RootsOfLife extends CardImpl { - private static final String ruleTrigger1 = "&bull Island — Whenever an Island an opponent controls becomes tapped, you gain 1 life"; - private static final String ruleTrigger2 = "&bull Swamp — Whenever a Swamp an opponent controls becomes tapped, you gain 1 life"; - - private static final FilterPermanent islandFilter = new FilterPermanent("an Island an opponent controls"); - private static final FilterPermanent swampFilter = new FilterPermanent("a Swamp an opponent controls"); + private static final FilterPermanent filter = new FilterLandPermanent("a land of the chosen type an opponent controls"); static { - islandFilter.add(SubType.ISLAND.getPredicate()); - islandFilter.add(TargetController.OPPONENT.getControllerPredicate()); - swampFilter.add(SubType.SWAMP.getPredicate()); - swampFilter.add(TargetController.OPPONENT.getControllerPredicate()); + filter.add(TargetController.OPPONENT.getControllerPredicate()); + filter.add(RootsOfLifePredicate.instance); } public RootsOfLife(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{G}{G}"); // As Roots of Life enters the battlefield, choose Island or Swamp. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Island or Swamp?", "Island", "Swamp"), null, - "As {this} enters, choose Island or Swamp.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.ISLAND, ModeChoice.SWAMP))); // Whenever a land of the chosen type an opponent controls becomes tapped, you gain 1 life. - // * Island chosen - this.addAbility(new ConditionalTriggeredAbility( - new BecomesTappedTriggeredAbility(new GainLifeEffect(1), false, islandFilter), - new ModeChoiceSourceCondition("Island"), - ruleTrigger1)); - - // * Swamp chosen - this.addAbility(new ConditionalTriggeredAbility( - new BecomesTappedTriggeredAbility(new GainLifeEffect(1), false, swampFilter), - new ModeChoiceSourceCondition("Swamp"), - ruleTrigger2)); + this.addAbility(new BecomesTappedTriggeredAbility(new GainLifeEffect(1), false, filter)); } private RootsOfLife(final RootsOfLife card) { @@ -63,3 +50,18 @@ public final class RootsOfLife extends CardImpl { return new RootsOfLife(this); } } + +enum RootsOfLifePredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + if (ModeChoice.ISLAND.checkMode(game, input.getSource())) { + return input.getObject().hasSubtype(SubType.ISLAND, game); + } else if (ModeChoice.SWAMP.checkMode(game, input.getSource())) { + return input.getObject().hasSubtype(SubType.SWAMP, game); + } else { + return false; + } + } +} diff --git a/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java b/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java index 1599ab4bb2c..5a46b36aeb4 100644 --- a/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java +++ b/Mage.Sets/src/mage/cards/s/StruggleForProjectPurity.java @@ -2,18 +2,19 @@ package mage.cards.s; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.condition.common.ModeChoiceSourceCondition; -import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.ModeChoice; import mage.constants.Outcome; import mage.constants.Zone; import mage.counters.CounterType; @@ -34,29 +35,21 @@ import java.util.stream.Collectors; */ public final class StruggleForProjectPurity extends CardImpl { - private static final String ruleTrigger1 = "&bull Brotherhood — At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way."; - private static final String ruleTrigger2 = "&bull Enclave — Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters."; - public StruggleForProjectPurity(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{U}"); // As Struggle for Project Purity enters, choose Brotherhood or Enclave. - this.addAbility(new EntersBattlefieldAbility(new ChooseModeEffect("Brotherhood or Enclave?", "Brotherhood", "Enclave"), null, - "As {this} enters, choose Brotherhood or Enclave.", "")); + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.BROTHERHOOD, ModeChoice.ENCLAVE))); // * Brotherhood - At the beginning of your upkeep, each opponent draws a card. You draw a card for each card drawn this way. - Ability ability = new ConditionalTriggeredAbility( - new BeginningOfUpkeepTriggeredAbility(new StruggleForProjectDrawEffect()), - new ModeChoiceSourceCondition("Brotherhood"), - ruleTrigger1); - this.addAbility(ability); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new BeginningOfUpkeepTriggeredAbility(new StruggleForProjectDrawEffect()), ModeChoice.BROTHERHOOD + ))); // * Enclave - Whenever a player attacks you with one or more creatures, that player gets twice that many rad counters. - ability = new ConditionalTriggeredAbility( - new StruggleForProjectRadCountersTriggeredAbility(), - new ModeChoiceSourceCondition("Enclave"), - ruleTrigger2); - this.addAbility(ability); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new StruggleForProjectRadCountersTriggeredAbility(), ModeChoice.ENCLAVE + ))); } private StruggleForProjectPurity(final StruggleForProjectPurity card) { diff --git a/Mage.Sets/src/mage/cards/w/WindcragSiege.java b/Mage.Sets/src/mage/cards/w/WindcragSiege.java new file mode 100644 index 00000000000..dac881a8419 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WindcragSiege.java @@ -0,0 +1,87 @@ +package mage.cards.w; + +import mage.abilities.Ability; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; +import mage.abilities.effects.common.replacement.AdditionalTriggersAttackingReplacementEffect; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.ModeChoice; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.permanent.token.GoblinToken; +import mage.game.permanent.token.Token; +import mage.target.targetpointer.FixedTargets; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WindcragSiege extends CardImpl { + + public WindcragSiege(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}{W}"); + + // As this enchantment enters, choose Mardu or Jeskai. + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.MARDU, ModeChoice.JESKAI))); + + // * Mardu -- If a creature attacking causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time. + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new AdditionalTriggersAttackingReplacementEffect(false), ModeChoice.MARDU + ))); + + // * Jeskai -- At the beginning of your upkeep, create a 1/1 red Goblin creature token. It gains lifelink and haste until end of turn. + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect( + new BeginningOfUpkeepTriggeredAbility(new WindcragSiegeEffect()), ModeChoice.JESKAI + ))); + } + + private WindcragSiege(final WindcragSiege card) { + super(card); + } + + @Override + public WindcragSiege copy() { + return new WindcragSiege(this); + } +} + +class WindcragSiegeEffect extends OneShotEffect { + + WindcragSiegeEffect() { + super(Outcome.Benefit); + staticText = "create a 1/1 red Goblin creature token. It gains lifelink and haste until end of turn"; + } + + private WindcragSiegeEffect(final WindcragSiegeEffect effect) { + super(effect); + } + + @Override + public WindcragSiegeEffect copy() { + return new WindcragSiegeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Token token = new GoblinToken(); + token.putOntoBattlefield(1, game, source); + game.addEffect(new GainAbilityTargetEffect( + LifelinkAbility.getInstance(), Duration.EndOfTurn + ).setTargetPointer(new FixedTargets(token, game)), source); + game.addEffect(new GainAbilityTargetEffect( + HasteAbility.getInstance(), Duration.EndOfTurn + ).setTargetPointer(new FixedTargets(token, game)), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/w/WulfgarOfIcewindDale.java b/Mage.Sets/src/mage/cards/w/WulfgarOfIcewindDale.java index 6b4c622c27d..798da523453 100644 --- a/Mage.Sets/src/mage/cards/w/WulfgarOfIcewindDale.java +++ b/Mage.Sets/src/mage/cards/w/WulfgarOfIcewindDale.java @@ -1,19 +1,14 @@ package mage.cards.w; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.replacement.AdditionalTriggersAttackingReplacementEffect; import mage.abilities.keyword.MeleeAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; -import mage.game.Controllable; -import mage.game.Game; -import mage.game.events.DefenderAttackedEvent; -import mage.game.events.GameEvent; -import mage.game.events.NumberOfTriggersEvent; -import mage.game.permanent.Permanent; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; import java.util.UUID; @@ -35,7 +30,7 @@ public final class WulfgarOfIcewindDale extends CardImpl { this.addAbility(new MeleeAbility()); // If a creature you control attacking would cause a triggered ability of a permanent you control to trigger, that ability triggers an additional time. - this.addAbility(new SimpleStaticAbility(new WulfgarOfIcewindDaleEffect())); + this.addAbility(new SimpleStaticAbility(new AdditionalTriggersAttackingReplacementEffect(true))); } private WulfgarOfIcewindDale(final WulfgarOfIcewindDale card) { @@ -47,64 +42,3 @@ public final class WulfgarOfIcewindDale extends CardImpl { return new WulfgarOfIcewindDale(this); } } - -class WulfgarOfIcewindDaleEffect extends ReplacementEffectImpl { - - WulfgarOfIcewindDaleEffect() { - super(Duration.WhileOnBattlefield, Outcome.Benefit); - staticText = "if a creature you control attacking causes a triggered ability " + - "of a permanent you control to trigger, that ability triggers an additional time"; - } - - private WulfgarOfIcewindDaleEffect(final WulfgarOfIcewindDaleEffect effect) { - super(effect); - } - - @Override - public WulfgarOfIcewindDaleEffect copy() { - return new WulfgarOfIcewindDaleEffect(this); - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.NUMBER_OF_TRIGGERS; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - NumberOfTriggersEvent numberOfTriggersEvent = (NumberOfTriggersEvent) event; - Permanent sourcePermanent = game.getPermanent(numberOfTriggersEvent.getSourceId()); - if (sourcePermanent == null || !sourcePermanent.isControlledBy(source.getControllerId())) { - return false; - } - GameEvent sourceEvent = numberOfTriggersEvent.getSourceEvent(); - if (sourceEvent == null) { - return false; - } - - switch (sourceEvent.getType()) { - case ATTACKER_DECLARED: - return source.isControlledBy(sourceEvent.getPlayerId()); - case DECLARED_ATTACKERS: - return game - .getCombat() - .getAttackers() - .stream() - .map(game::getControllerId) - .anyMatch(source::isControlledBy); - case DEFENDER_ATTACKED: - return ((DefenderAttackedEvent) sourceEvent) - .getAttackers(game) - .stream() - .map(Controllable::getControllerId) - .anyMatch(source::isControlledBy); - } - return false; - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - event.setAmount(event.getAmount() + 1); - return false; - } -} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index e9e25436f1b..74b45fe1e2d 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -274,6 +274,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Whirlwing Stormbrood", 234, Rarity.UNCOMMON, mage.cards.w.WhirlwingStormbrood.class)); cards.add(new SetCardInfo("Wild Ride", 132, Rarity.COMMON, mage.cards.w.WildRide.class)); cards.add(new SetCardInfo("Wind-Scarred Crag", 271, Rarity.COMMON, mage.cards.w.WindScarredCrag.class)); + cards.add(new SetCardInfo("Windcrag Siege", 235, Rarity.RARE, mage.cards.w.WindcragSiege.class)); cards.add(new SetCardInfo("Wingblade Disciple", 65, Rarity.UNCOMMON, mage.cards.w.WingbladeDisciple.class)); cards.add(new SetCardInfo("Wingspan Stride", 66, Rarity.COMMON, mage.cards.w.WingspanStride.class)); cards.add(new SetCardInfo("Winternight Stories", 67, Rarity.RARE, mage.cards.w.WinternightStories.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/PramikonSkyRampartTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/PramikonSkyRampartTest.java index f51b08d613c..8e5f1b9cc16 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/PramikonSkyRampartTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c19/PramikonSkyRampartTest.java @@ -1,10 +1,6 @@ package org.mage.test.cards.single.c19; -import mage.abilities.effects.common.continuous.PlayerCanOnlyAttackInDirectionRestrictionEffect; -import mage.constants.MultiplayerAttackOption; -import mage.constants.PhaseStep; -import mage.constants.RangeOfInfluence; -import mage.constants.Zone; +import mage.constants.*; import mage.game.FreeForAll; import mage.game.Game; import mage.game.GameException; @@ -63,7 +59,7 @@ public class PramikonSkyRampartTest extends CardTestMultiPlayerBase { addCard(Zone.BATTLEFIELD, playerD, devil); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pramikon); - setChoice(playerA, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_LEFT); + setChoice(playerA, ModeChoice.LEFT.toString()); // A has pramikon, and chose left. // @@ -128,7 +124,7 @@ public class PramikonSkyRampartTest extends CardTestMultiPlayerBase { addCard(Zone.BATTLEFIELD, playerD, devil); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pramikon); - setChoice(playerA, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_RIGHT); + setChoice(playerA, ModeChoice.RIGHT.toString()); // A has pramikon, and chose right. // @@ -197,10 +193,10 @@ public class PramikonSkyRampartTest extends CardTestMultiPlayerBase { addCard(Zone.BATTLEFIELD, playerD, devil); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pramikon); - setChoice(playerA, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_RIGHT); + setChoice(playerA, ModeChoice.RIGHT.toString()); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, pramikon); - setChoice(playerD, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_LEFT); + setChoice(playerD, ModeChoice.LEFT.toString()); // A has pramikon, and chose right. // D has pramikon, and chose left. @@ -270,10 +266,10 @@ public class PramikonSkyRampartTest extends CardTestMultiPlayerBase { addCard(Zone.BATTLEFIELD, playerD, devil); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, pramikon); - setChoice(playerA, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_LEFT); + setChoice(playerA, ModeChoice.LEFT.toString()); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, pramikon); - setChoice(playerD, PlayerCanOnlyAttackInDirectionRestrictionEffect.ALLOW_ATTACKING_RIGHT); + setChoice(playerD, ModeChoice.RIGHT.toString()); // A has pramikon, and chose left. // D has pramikon, and chose right. @@ -323,4 +319,4 @@ public class PramikonSkyRampartTest extends CardTestMultiPlayerBase { setStopAt(5, PhaseStep.END_TURN); execute(); } -} \ No newline at end of file +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/ModeChoiceSourceCondition.java b/Mage/src/main/java/mage/abilities/condition/common/ModeChoiceSourceCondition.java deleted file mode 100644 index 8ce77f24633..00000000000 --- a/Mage/src/main/java/mage/abilities/condition/common/ModeChoiceSourceCondition.java +++ /dev/null @@ -1,26 +0,0 @@ - -package mage.abilities.condition.common; - -import mage.abilities.Ability; -import mage.abilities.condition.Condition; -import mage.game.Game; - -/** - * - * @author LevelX2 - */ -public class ModeChoiceSourceCondition implements Condition { - - private final String mode; - - - public ModeChoiceSourceCondition(String mode) { - this.mode = mode; - } - - @Override - public boolean apply(Game game, Ability source) { - String chosenMode = (String) game.getState().getValue(source.getSourceId() + "_modeChoice"); - return chosenMode != null && chosenMode.equals(mode); - } -} diff --git a/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java index 5ca8e531c68..978e0cb8f2e 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ChooseModeEffect.java @@ -1,39 +1,41 @@ package mage.abilities.effects.common; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.choices.Choice; import mage.choices.ChoiceImpl; +import mage.constants.ModeChoice; import mage.constants.Outcome; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + /** * @author LevelX2 */ public class ChooseModeEffect extends OneShotEffect { - protected final List modes = new ArrayList<>(); - protected final String choiceMessage; + protected final List modes = new ArrayList<>(); + protected final String message; - public ChooseModeEffect(String choiceMessage, String... modes) { + public ChooseModeEffect(ModeChoice... modes) { super(Outcome.Neutral); - this.choiceMessage = choiceMessage; this.modes.addAll(Arrays.asList(modes)); - this.staticText = setText(); + this.message = makeMessage(this.modes); + this.staticText = "choose " + this.message; } protected ChooseModeEffect(final ChooseModeEffect effect) { super(effect); this.modes.addAll(effect.modes); - this.choiceMessage = effect.choiceMessage; + this.message = effect.message; } @Override @@ -48,34 +50,27 @@ public class ChooseModeEffect extends OneShotEffect { if (sourcePermanent == null) { sourcePermanent = game.getPermanentEntering(source.getSourceId()); } - if (controller != null && sourcePermanent != null) { - Choice choice = new ChoiceImpl(true); - choice.setMessage(choiceMessage + CardUtil.getSourceLogName(game, source)); - choice.getChoices().addAll(modes); - if (controller.choose(Outcome.Neutral, choice, game)) { - if (!game.isSimulation()) { - game.informPlayers(sourcePermanent.getLogName() + ": " + controller.getLogName() + " has chosen " + choice.getChoice()); - } - game.getState().setValue(source.getSourceId() + "_modeChoice", choice.getChoice()); - sourcePermanent.addInfo("_modeChoice", "Chosen mode: " + choice.getChoice() + "", game); - return true; - } + if (controller == null || sourcePermanent == null) { + return false; } - return false; + Choice choice = new ChoiceImpl(true); + choice.setMessage(message + "? (" + CardUtil.getSourceLogName(game, source) + ')'); + choice.getChoices().addAll(modes.stream().map(ModeChoice::toString).collect(Collectors.toList())); + if (!controller.choose(Outcome.Neutral, choice, game)) { + return false; + } + game.informPlayers(sourcePermanent.getLogName() + ": " + controller.getLogName() + " has chosen " + choice.getChoice()); + game.getState().setValue(source.getSourceId() + "_modeChoice", choice.getChoice()); + sourcePermanent.addInfo("_modeChoice", "Chosen mode: " + choice.getChoice() + "", game); + return true; } - private String setText() { - StringBuilder sb = new StringBuilder("choose "); - int count = 0; - for (String choice : modes) { - count++; - sb.append(choice); - if (count + 1 < modes.size()) { - sb.append(", "); - } else if (count < modes.size()) { - sb.append(" or "); - } - } - return sb.toString(); + private static String makeMessage(List modeChoices) { + return CardUtil.concatWithOr( + modeChoices + .stream() + .map(ModeChoice::toString) + .collect(Collectors.toList()) + ); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/PermanentsEnterBattlefieldTappedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/PermanentsEnterBattlefieldTappedEffect.java index e9d949ac4ec..7eb63a0be7f 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/PermanentsEnterBattlefieldTappedEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/PermanentsEnterBattlefieldTappedEffect.java @@ -63,7 +63,7 @@ public class PermanentsEnterBattlefieldTappedEffect extends ReplacementEffectImp return staticText; } return filter.getMessage() - + " enter tapped" - + (duration == Duration.EndOfTurn ? " this turn" : ""); + + " enter" + (filter.getMessage().startsWith("each") ? "s" : "") + + " tapped" + (duration == Duration.EndOfTurn ? " this turn" : ""); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAnchorWordAbilitySourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAnchorWordAbilitySourceEffect.java new file mode 100644 index 00000000000..f4af6963691 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAnchorWordAbilitySourceEffect.java @@ -0,0 +1,52 @@ +package mage.abilities.effects.common.continuous; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.Effect; +import mage.constants.*; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * @author TheElk801 + */ +public class GainAnchorWordAbilitySourceEffect extends ContinuousEffectImpl { + + private final Ability ability; + private final ModeChoice modeChoice; + + public GainAnchorWordAbilitySourceEffect(Effect effect, ModeChoice modeChoice) { + this(new SimpleStaticAbility(effect), modeChoice); + } + + public GainAnchorWordAbilitySourceEffect(Ability ability, ModeChoice modeChoice) { + super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.staticText = "&bull " + modeChoice + " — " + ability.getRule(); + this.ability = ability; + this.modeChoice = modeChoice; + this.ability.setRuleVisible(false); + this.generateGainAbilityDependencies(ability, null); + } + + private GainAnchorWordAbilitySourceEffect(final GainAnchorWordAbilitySourceEffect effect) { + super(effect); + this.modeChoice = effect.modeChoice; + this.ability = effect.ability; + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null || !modeChoice.checkMode(game, source)) { + return false; + } + permanent.addAbility(ability, source.getSourceId(), game); + return true; + } + + @Override + public GainAnchorWordAbilitySourceEffect copy() { + return new GainAnchorWordAbilitySourceEffect(this); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java index b74ab65717c..8aaefded6da 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayerCanOnlyAttackInDirectionRestrictionEffect.java @@ -6,6 +6,7 @@ import mage.abilities.effects.RestrictionEffect; import mage.abilities.effects.common.ChooseModeEffect; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.ModeChoice; import mage.constants.Outcome; import mage.game.Game; import mage.game.permanent.Permanent; @@ -19,9 +20,6 @@ import java.util.UUID; */ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends RestrictionEffect { - public static final String ALLOW_ATTACKING_LEFT = "Allow attacking left"; - public static final String ALLOW_ATTACKING_RIGHT = "Allow attacking right"; - public PlayerCanOnlyAttackInDirectionRestrictionEffect(Duration duration, String directionText) { super(duration, Outcome.Neutral); staticText = duration + (duration.toString().isEmpty() ? "" : ", ") @@ -39,10 +37,7 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction } public static Effect choiceEffect() { - return new ChooseModeEffect( - "Choose a direction to allow attacking in.", - ALLOW_ATTACKING_LEFT, ALLOW_ATTACKING_RIGHT - ).setText("choose left or right"); + return new ChooseModeEffect(ModeChoice.LEFT, ModeChoice.RIGHT); } @Override @@ -55,10 +50,13 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction if (defenderId == null) { return true; } - - String allowedDirection = (String) game.getState().getValue(source.getSourceId() + "_modeChoice"); - if (allowedDirection == null) { - return true; // If no choice was made, the ability has no effect. + boolean left; + if (ModeChoice.LEFT.checkMode(game, source)) { + left = true; + } else if (ModeChoice.RIGHT.checkMode(game, source)) { + left = false; + } else { + return false; // If no choice was made, the ability has no effect. } Player playerAttacking = game.getPlayer(attacker.getControllerId()); @@ -83,18 +81,7 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction } PlayerList playerList = game.getState().getPlayerList(playerAttacking.getId()); - if (allowedDirection.equals(ALLOW_ATTACKING_LEFT) - && !playerList.getNext().equals(playerDefending.getId())) { - // the defender is not the player to the left - return false; - } - if (allowedDirection.equals(ALLOW_ATTACKING_RIGHT) - && !playerList.getPrevious().equals(playerDefending.getId())) { - // the defender is not the player to the right - return false; - } - - return true; + return (!left || playerList.getNext().equals(playerDefending.getId())) + && (left || playerList.getPrevious().equals(playerDefending.getId())); } - } diff --git a/Mage/src/main/java/mage/abilities/effects/common/replacement/AdditionalTriggersAttackingReplacementEffect.java b/Mage/src/main/java/mage/abilities/effects/common/replacement/AdditionalTriggersAttackingReplacementEffect.java new file mode 100644 index 00000000000..85516648f74 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/replacement/AdditionalTriggersAttackingReplacementEffect.java @@ -0,0 +1,80 @@ +package mage.abilities.effects.common.replacement; + +import mage.abilities.Ability; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Controllable; +import mage.game.Game; +import mage.game.events.DefenderAttackedEvent; +import mage.game.events.GameEvent; +import mage.game.events.NumberOfTriggersEvent; +import mage.game.permanent.Permanent; + +/** + * @author TheElk801 + */ +public class AdditionalTriggersAttackingReplacementEffect extends ReplacementEffectImpl { + + private final boolean onlyControlled; + + public AdditionalTriggersAttackingReplacementEffect(boolean onlyControlled) { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + this.onlyControlled = onlyControlled; + staticText = "if a creature " + (onlyControlled ? "you control " : "") + "attacking causes a triggered ability " + + "of a permanent you control to trigger, that ability triggers an additional time"; + } + + private AdditionalTriggersAttackingReplacementEffect(final AdditionalTriggersAttackingReplacementEffect effect) { + super(effect); + this.onlyControlled = effect.onlyControlled; + } + + @Override + public AdditionalTriggersAttackingReplacementEffect copy() { + return new AdditionalTriggersAttackingReplacementEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.NUMBER_OF_TRIGGERS; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + NumberOfTriggersEvent numberOfTriggersEvent = (NumberOfTriggersEvent) event; + Permanent sourcePermanent = game.getPermanent(numberOfTriggersEvent.getSourceId()); + if (sourcePermanent == null || !sourcePermanent.isControlledBy(source.getControllerId())) { + return false; + } + GameEvent sourceEvent = numberOfTriggersEvent.getSourceEvent(); + if (sourceEvent == null) { + return false; + } + + switch (sourceEvent.getType()) { + case ATTACKER_DECLARED: + return !onlyControlled || source.isControlledBy(sourceEvent.getPlayerId()); + case DECLARED_ATTACKERS: + return !onlyControlled || game + .getCombat() + .getAttackers() + .stream() + .map(game::getControllerId) + .anyMatch(source::isControlledBy); + case DEFENDER_ATTACKED: + return !onlyControlled || ((DefenderAttackedEvent) sourceEvent) + .getAttackers(game) + .stream() + .map(Controllable::getControllerId) + .anyMatch(source::isControlledBy); + } + return false; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + event.setAmount(event.getAmount() + 1); + return false; + } +} diff --git a/Mage/src/main/java/mage/constants/ModeChoice.java b/Mage/src/main/java/mage/constants/ModeChoice.java new file mode 100644 index 00000000000..1759d2dece8 --- /dev/null +++ b/Mage/src/main/java/mage/constants/ModeChoice.java @@ -0,0 +1,75 @@ +package mage.constants; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; + +import java.util.Objects; + +public enum ModeChoice { + + KHANS("Khans"), + DRAGONS("Dragons"), + + MARDU("Mardu"), + TEMUR("Temur"), + ABZAN("Abzan"), + JESKAI("Jeskai"), + SULTAI("Sultai"), + + MIRRAN("Mirran"), + PHYREXIAN("Phyrexian "), + + ODD("odd"), + EVEN("even"), + + BELIEVE("Believe"), + DOUBT("Doubt"), + + NCR("NCR"), + LEGION("Legion"), + + BROTHERHOOD("Brotherhood"), + ENCLAVE("Enclave"), + + ISLAND("Island"), + SWAMP("Swamp"), + + LEFT("left"), + RIGHT("right"); + + private static class ModeChoiceCondition implements Condition { + + private final ModeChoice modeChoice; + + ModeChoiceCondition(ModeChoice modeChoice) { + this.modeChoice = modeChoice; + } + + @Override + public boolean apply(Game game, Ability source) { + return modeChoice.checkMode(game, source); + } + } + + private final String name; + private final ModeChoiceCondition condition; + + ModeChoice(String name) { + this.name = name; + this.condition = new ModeChoiceCondition(this); + } + + @Override + public String toString() { + return name; + } + + public ModeChoiceCondition getCondition() { + return condition; + } + + public boolean checkMode(Game game, Ability source) { + return Objects.equals(game.getState().getValue(source.getSourceId() + "_modeChoice"), name); + } +} From 162365269d96410e17998db30c10bd2fe844fa3d Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 13:29:54 -0400 Subject: [PATCH 079/133] [TDM] Implement Herd Heirloom --- Mage.Sets/src/mage/cards/h/HerdHeirloom.java | 72 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 73 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HerdHeirloom.java diff --git a/Mage.Sets/src/mage/cards/h/HerdHeirloom.java b/Mage.Sets/src/mage/cards/h/HerdHeirloom.java new file mode 100644 index 00000000000..e3c47f7abdf --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HerdHeirloom.java @@ -0,0 +1,72 @@ +package mage.cards.h; + +import mage.ConditionalMana; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.mana.ConditionalAnyColorManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.abilities.mana.conditional.CreatureCastConditionalMana; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HerdHeirloom extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledCreaturePermanent("creature you control with power 4 or greater"); + + static { + filter.add(new PowerPredicate(ComparisonType.MORE_THAN, 3)); + } + + public HerdHeirloom(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{G}"); + + // {T}: Add one mana of any color. Spend this mana only to cast a creature spell. + this.addAbility(new ConditionalAnyColorManaAbility(1, new HerdHeirloomManaBuilder())); + + // {T}: Until end of turn, target creature you control with power 4 or greater gains trample and "Whenever this creature deals combat damage to a player, draw a card." + Ability ability = new SimpleActivatedAbility(new GainAbilityTargetEffect(TrampleAbility.getInstance()) + .setText("until end of turn, target creature you control with power 4 or greater gains trample"), new TapSourceCost()); + ability.addEffect(new GainAbilityTargetEffect(new DealsCombatDamageToAPlayerTriggeredAbility(new DrawCardSourceControllerEffect(1))) + .setText("and \"Whenever this creature deals combat damage to a player, draw a card.\"")); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private HerdHeirloom(final HerdHeirloom card) { + super(card); + } + + @Override + public HerdHeirloom copy() { + return new HerdHeirloom(this); + } +} + +class HerdHeirloomManaBuilder extends ConditionalManaBuilder { + @Override + public ConditionalMana build(Object... options) { + return new CreatureCastConditionalMana(this.mana); + } + + @Override + public String getRule() { + return "Spend this mana only to cast a creature spell"; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 74b45fe1e2d..6cf9e9b1569 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -120,6 +120,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Gurmag Nightwatch", 190, Rarity.COMMON, mage.cards.g.GurmagNightwatch.class)); cards.add(new SetCardInfo("Gurmag Rakshasa", 81, Rarity.UNCOMMON, mage.cards.g.GurmagRakshasa.class)); cards.add(new SetCardInfo("Hardened Tactician", 191, Rarity.UNCOMMON, mage.cards.h.HardenedTactician.class)); + cards.add(new SetCardInfo("Herd Heirloom", 144, Rarity.RARE, mage.cards.h.HerdHeirloom.class)); cards.add(new SetCardInfo("Heritage Reclamation", 145, Rarity.COMMON, mage.cards.h.HeritageReclamation.class)); cards.add(new SetCardInfo("Highspire Bell-Ringer", 47, Rarity.COMMON, mage.cards.h.HighspireBellRinger.class)); cards.add(new SetCardInfo("Humbling Elder", 48, Rarity.COMMON, mage.cards.h.HumblingElder.class)); From 5e23afc47731d97d709d410da2a76f22ab6dd56e Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 13:38:44 -0400 Subject: [PATCH 080/133] [TDM] Implement Synchronized Charge --- .../src/mage/cards/s/SynchronizedCharge.java | 60 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 61 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SynchronizedCharge.java diff --git a/Mage.Sets/src/mage/cards/s/SynchronizedCharge.java b/Mage.Sets/src/mage/cards/s/SynchronizedCharge.java new file mode 100644 index 00000000000..2c7d78315b3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SynchronizedCharge.java @@ -0,0 +1,60 @@ +package mage.cards.s; + +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.effects.common.counter.DistributeCountersEffect; +import mage.abilities.keyword.HarmonizeAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.CounterAnyPredicate; +import mage.target.common.TargetCreaturePermanentAmount; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SynchronizedCharge extends CardImpl { + + private static final FilterPermanent filter = new FilterCreaturePermanent(); + + static { + filter.add(CounterAnyPredicate.instance); + } + + public SynchronizedCharge(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{G}"); + + // Distribute two +1/+1 counters among one or two target creatures you control. Creatures you control with counters on them gain vigilance and trample until end of turn. + this.getSpellAbility().addEffect(new DistributeCountersEffect(CounterType.P1P1)); + this.getSpellAbility().addTarget(new TargetCreaturePermanentAmount( + 2, 1, 2, + StaticFilters.FILTER_CONTROLLED_CREATURES + )); + this.getSpellAbility().addEffect(new GainAbilityControlledEffect( + VigilanceAbility.getInstance(), Duration.EndOfTurn, filter + ).setText("creatures you control with counters on them gain vigilance")); + this.getSpellAbility().addEffect(new GainAbilityControlledEffect( + TrampleAbility.getInstance(), Duration.EndOfTurn, filter + ).setText("and trample until end of turn")); + + // Harmonize {4}{G} + this.addAbility(new HarmonizeAbility(this, "{4}{G}")); + } + + private SynchronizedCharge(final SynchronizedCharge card) { + super(card); + } + + @Override + public SynchronizedCharge copy() { + return new SynchronizedCharge(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 6cf9e9b1569..caabc721197 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -238,6 +238,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Sunset Strikemaster", 126, Rarity.UNCOMMON, mage.cards.s.SunsetStrikemaster.class)); cards.add(new SetCardInfo("Swamp", 281, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Swiftwater Cliffs", 268, Rarity.COMMON, mage.cards.s.SwiftwaterCliffs.class)); + cards.add(new SetCardInfo("Synchronized Charge", 162, Rarity.UNCOMMON, mage.cards.s.SynchronizedCharge.class)); cards.add(new SetCardInfo("Teeming Dragonstorm", 30, Rarity.UNCOMMON, mage.cards.t.TeemingDragonstorm.class)); cards.add(new SetCardInfo("Tempest Hawk", 31, Rarity.COMMON, mage.cards.t.TempestHawk.class)); cards.add(new SetCardInfo("Temur Battlecrier", 228, Rarity.RARE, mage.cards.t.TemurBattlecrier.class)); From 4c8337e0e558a57a09d1d37930ec6a7fb457bf8b Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 13:52:07 -0400 Subject: [PATCH 081/133] [TDM] Implement Rot-Curse Rakshasa --- .../src/mage/cards/r/RotCurseRakshasa.java | 61 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + .../main/java/mage/counters/CounterType.java | 3 + Utils/keywords.txt | 1 + 4 files changed, 66 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java diff --git a/Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java b/Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java new file mode 100644 index 00000000000..555110515f8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java @@ -0,0 +1,61 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.ActivateAsSorceryActivatedAbility; +import mage.abilities.costs.common.ExileSourceFromGraveCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.DecayedAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.target.targetadjustment.TargetsCountAdjuster; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RotCurseRakshasa extends CardImpl { + + public RotCurseRakshasa(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.DEMON); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Decayed + this.addAbility(new DecayedAbility()); + + // Renew -- {X}{B}{B}, Exile this card from your graveyard: Put a decayed counter on each of X target creatures. Activate only as a sorcery. + Ability ability = new ActivateAsSorceryActivatedAbility( + Zone.GRAVEYARD, + new AddCountersTargetEffect(CounterType.DECAYED.createInstance(1)) + .setText("put a decayed counter on each of X target creatures"), + new ManaCostsImpl<>("{X}{B}{B}") + ); + ability.addCost(new ExileSourceFromGraveCost()); + ability.setTargetAdjuster(new TargetsCountAdjuster(GetXValue.instance)); + this.addAbility(ability.setAbilityWord(AbilityWord.RENEW)); + } + + private RotCurseRakshasa(final RotCurseRakshasa card) { + super(card); + } + + @Override + public RotCurseRakshasa copy() { + return new RotCurseRakshasa(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index caabc721197..8eda800022a 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -195,6 +195,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Roamer's Routine", 154, Rarity.COMMON, mage.cards.r.RoamersRoutine.class)); cards.add(new SetCardInfo("Roar of Endless Song", 220, Rarity.RARE, mage.cards.r.RoarOfEndlessSong.class)); cards.add(new SetCardInfo("Roiling Dragonstorm", 55, Rarity.UNCOMMON, mage.cards.r.RoilingDragonstorm.class)); + cards.add(new SetCardInfo("Rot-Curse Rakshasa", 87, Rarity.MYTHIC, mage.cards.r.RotCurseRakshasa.class)); cards.add(new SetCardInfo("Rugged Highlands", 265, Rarity.COMMON, mage.cards.r.RuggedHighlands.class)); cards.add(new SetCardInfo("Runescale Stormbrood", 221, Rarity.UNCOMMON, mage.cards.r.RunescaleStormbrood.class)); cards.add(new SetCardInfo("Sage of the Fang", 155, Rarity.UNCOMMON, mage.cards.s.SageOfTheFang.class)); diff --git a/Mage/src/main/java/mage/counters/CounterType.java b/Mage/src/main/java/mage/counters/CounterType.java index 20f71f266d2..0afd7f62cb5 100644 --- a/Mage/src/main/java/mage/counters/CounterType.java +++ b/Mage/src/main/java/mage/counters/CounterType.java @@ -54,6 +54,7 @@ public enum CounterType { CURRENCY("currency"), DEATH("death"), DEATHTOUCH("deathtouch"), + DECAYED("decayed"), DEFENSE("defense"), DELAY("delay"), DEPLETION("depletion"), @@ -315,6 +316,8 @@ public enum CounterType { return new BoostCounter(-2, -2, amount); case DEATHTOUCH: return new AbilityCounter(DeathtouchAbility.getInstance(), amount); + case DECAYED: + return new AbilityCounter(new DecayedAbility(), amount); case DOUBLE_STRIKE: return new AbilityCounter(DoubleStrikeAbility.getInstance(), amount); case EXALTED: diff --git a/Utils/keywords.txt b/Utils/keywords.txt index a3eea683be2..fb6f00141ca 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -25,6 +25,7 @@ Cycling|cost| Dash|manaString| Daybound|new| Deathtouch|instance| +Decayed|new| Demonstrate|new| Delve|new| Dethrone|new| From 32e99536bc9482380d27025ba88679c144802425 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 13:58:08 -0400 Subject: [PATCH 082/133] [TDM] Implement Reverberating Summons --- .../mage/cards/r/ReverberatingSummons.java | 73 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 74 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/ReverberatingSummons.java diff --git a/Mage.Sets/src/mage/cards/r/ReverberatingSummons.java b/Mage.Sets/src/mage/cards/r/ReverberatingSummons.java new file mode 100644 index 00000000000..4ca6b91c46f --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/ReverberatingSummons.java @@ -0,0 +1,73 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.common.DiscardHandCost; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.BecomesCreatureSourceEffect; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.game.Game; +import mage.game.permanent.token.custom.CreatureToken; +import mage.watchers.common.SpellsCastWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ReverberatingSummons extends CardImpl { + + public ReverberatingSummons(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}"); + + // At the beginning of each combat, if you've cast two or more spells this turn, this enchantment becomes a 3/3 Monk creature with haste in addition to its other types until end of turn. + this.addAbility(new BeginningOfCombatTriggeredAbility( + TargetController.ANY, + new BecomesCreatureSourceEffect( + new CreatureToken(3, 3, "3/3 Monk creature with haste") + .withSubType(SubType.MONK) + .withAbility(HasteAbility.getInstance()), + CardType.ENCHANTMENT, Duration.EndOfTurn + ), false + ).withInterveningIf(ReverberatingSummonsCondition.instance)); + + // {1}{R}, Discard your hand, Sacrifice this enchantment: Draw two cards. + Ability ability = new SimpleActivatedAbility( + new DrawCardSourceControllerEffect(2), new ManaCostsImpl<>("{1}{R}") + ); + ability.addCost(new DiscardHandCost()); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(ability); + } + + private ReverberatingSummons(final ReverberatingSummons card) { + super(card); + } + + @Override + public ReverberatingSummons copy() { + return new ReverberatingSummons(this); + } +} + +enum ReverberatingSummonsCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return game + .getState() + .getWatcher(SpellsCastWatcher.class) + .getCount(source.getControllerId()) >= 2; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 8eda800022a..b9d025d607c 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -187,6 +187,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Reigning Victor", 216, Rarity.COMMON, mage.cards.r.ReigningVictor.class)); cards.add(new SetCardInfo("Reputable Merchant", 217, Rarity.COMMON, mage.cards.r.ReputableMerchant.class)); cards.add(new SetCardInfo("Rescue Leopard", 116, Rarity.COMMON, mage.cards.r.RescueLeopard.class)); + cards.add(new SetCardInfo("Reverberating Summons", 117, Rarity.UNCOMMON, mage.cards.r.ReverberatingSummons.class)); cards.add(new SetCardInfo("Revival of the Ancestors", 218, Rarity.RARE, mage.cards.r.RevivalOfTheAncestors.class)); cards.add(new SetCardInfo("Riling Dawnbreaker", 21, Rarity.COMMON, mage.cards.r.RilingDawnbreaker.class)); cards.add(new SetCardInfo("Ringing Strike Mastery", 53, Rarity.COMMON, mage.cards.r.RingingStrikeMastery.class)); From e30cdb6c7f86e74b4cea781bb381b78803446a65 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 14:09:18 -0400 Subject: [PATCH 083/133] [TDM] Implement Maelstrom of the Spirit Dragon --- .../cards/m/MaelstromOfTheSpiritDragon.java | 65 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 66 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MaelstromOfTheSpiritDragon.java diff --git a/Mage.Sets/src/mage/cards/m/MaelstromOfTheSpiritDragon.java b/Mage.Sets/src/mage/cards/m/MaelstromOfTheSpiritDragon.java new file mode 100644 index 00000000000..1f17bf216b1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MaelstromOfTheSpiritDragon.java @@ -0,0 +1,65 @@ +package mage.cards.m; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; +import mage.abilities.mana.ColorlessManaAbility; +import mage.abilities.mana.ConditionalAnyColorManaAbility; +import mage.abilities.mana.conditional.ConditionalSpellManaBuilder; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.FilterSpell; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MaelstromOfTheSpiritDragon extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("a Dragon spell or an Omen spell"); + private static final FilterCard filter2 = new FilterCard("a Dragon card"); + + static { + filter.add(Predicates.or( + SubType.DRAGON.getPredicate(), + SubType.OMEN.getPredicate() + )); + filter2.add(SubType.DRAGON.getPredicate()); + } + + public MaelstromOfTheSpiritDragon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + // {T}: Add {C}. + this.addAbility(new ColorlessManaAbility()); + + // {T}: Add one mana of any color. Spend this mana only to cast a Dragon spell or an Omen spell. + this.addAbility(new ConditionalAnyColorManaAbility(1, new ConditionalSpellManaBuilder(filter))); + + // {4}, {T}, Sacrifice this land: Search your library for a Dragon card, reveal it, put it into your hand, then shuffle. + Ability ability = new SimpleActivatedAbility( + new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter2), true), new GenericManaCost(4) + ); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(ability); + } + + private MaelstromOfTheSpiritDragon(final MaelstromOfTheSpiritDragon card) { + super(card); + } + + @Override + public MaelstromOfTheSpiritDragon copy() { + return new MaelstromOfTheSpiritDragon(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index b9d025d607c..795fcd0a293 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -152,6 +152,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Lotuslight Dancers", 204, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Lotuslight Dancers", 363, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Loxodon Battle Priest", 15, Rarity.UNCOMMON, mage.cards.l.LoxodonBattlePriest.class)); + cards.add(new SetCardInfo("Maelstrom of the Spirit Dragon", 260, Rarity.RARE, mage.cards.m.MaelstromOfTheSpiritDragon.class)); cards.add(new SetCardInfo("Mammoth Bellow", 205, Rarity.UNCOMMON, mage.cards.m.MammothBellow.class)); cards.add(new SetCardInfo("Marang River Regent", 51, Rarity.RARE, mage.cards.m.MarangRiverRegent.class)); cards.add(new SetCardInfo("Mardu Devotee", 16, Rarity.COMMON, mage.cards.m.MarduDevotee.class)); From 79bf1851e5af5ae62907a8a4b1bdf1f904cf3871 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Wed, 9 Apr 2025 10:34:52 -0500 Subject: [PATCH 084/133] [TDM] Implement Furious Forebear --- .../src/mage/cards/f/FuriousForebear.java | 54 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 55 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FuriousForebear.java diff --git a/Mage.Sets/src/mage/cards/f/FuriousForebear.java b/Mage.Sets/src/mage/cards/f/FuriousForebear.java new file mode 100644 index 00000000000..852f999adf1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FuriousForebear.java @@ -0,0 +1,54 @@ +package mage.cards.f; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.ReturnSourceFromGraveyardToHandEffect; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.common.FilterCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class FuriousForebear extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("a creature you control"); + + static { + filter.add(TargetController.YOU.getControllerPredicate()); + } + + public FuriousForebear(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Whenever a creature you control dies while this card is in your graveyard, you may pay {1}{W}. If you do, return this card from your graveyard to your hand. + Effect effect = new DoIfCostPaid(new ReturnSourceFromGraveyardToHandEffect(), new ManaCostsImpl<>("{1}{W}")); + Ability ability = new DiesCreatureTriggeredAbility(Zone.GRAVEYARD, effect,false, filter, false) + .setTriggerPhrase("Whenever a creature you control dies while this card is in your graveyard, "); + this.addAbility(ability); + } + + private FuriousForebear(final FuriousForebear card) { + super(card); + } + + @Override + public FuriousForebear copy() { + return new FuriousForebear(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 795fcd0a293..42ee85092ff 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -113,6 +113,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Frontline Rush", 186, Rarity.UNCOMMON, mage.cards.f.FrontlineRush.class)); cards.add(new SetCardInfo("Frostcliff Siege", 187, Rarity.RARE, mage.cards.f.FrostcliffSiege.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Frostcliff Siege", 385, Rarity.RARE, mage.cards.f.FrostcliffSiege.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Furious Forebear", 13, Rarity.UNCOMMON, mage.cards.f.FuriousForebear.class)); cards.add(new SetCardInfo("Glacial Dragonhunt", 188, Rarity.UNCOMMON, mage.cards.g.GlacialDragonhunt.class)); cards.add(new SetCardInfo("Glacierwood Siege", 189, Rarity.RARE, mage.cards.g.GlacierwoodSiege.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Glacierwood Siege", 386, Rarity.RARE, mage.cards.g.GlacierwoodSiege.class, NON_FULL_USE_VARIOUS)); From 178bd22025814e89882491f6e5d6894c8462e80a Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Wed, 9 Apr 2025 13:11:04 -0500 Subject: [PATCH 085/133] [TDM] Implement Host of the Hereafter --- .../src/mage/cards/h/HostOfTheHereafter.java | 99 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + .../single/tdm/HostOfTheHereafterTest.java | 94 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HostOfTheHereafter.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/HostOfTheHereafterTest.java diff --git a/Mage.Sets/src/mage/cards/h/HostOfTheHereafter.java b/Mage.Sets/src/mage/cards/h/HostOfTheHereafter.java new file mode 100644 index 00000000000..d2d9ffd0d69 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HostOfTheHereafter.java @@ -0,0 +1,99 @@ +package mage.cards.h; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.condition.common.SourceHasCounterCondition; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.EntersBattlefieldWithXCountersEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.common.counter.MoveCountersFromSourceToTargetEffect; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.counters.Counters; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.CounterAnyPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetControlledCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class HostOfTheHereafter extends CardImpl { + + private static final FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent("this creature or another creature you control"); + + static { + filter.add(CounterAnyPredicate.instance); + } + + public HostOfTheHereafter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{G}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // This creature enters with two +1/+1 counters on it. + this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), + null, "This creature enters with two +1/+1 counters on it.", "") + ); + + // Whenever this creature or another creature you control dies, if it had counters on it, put its counters on up to one target creature you control. + Ability ability = new DiesCreatureTriggeredAbility(new HostOfTheHereafterDiesEffect(), false, filter) + .setTriggerPhrase("Whenever " + filter.getMessage() + " dies, if it had counters on it, "); + ability.addTarget(new TargetControlledCreaturePermanent(0, 1)); + this.addAbility(ability); + } + + private HostOfTheHereafter(final HostOfTheHereafter card) { + super(card); + } + + @Override + public HostOfTheHereafter copy() { + return new HostOfTheHereafter(this); + } +} + +class HostOfTheHereafterDiesEffect extends OneShotEffect { + + HostOfTheHereafterDiesEffect() { + super(Outcome.Benefit); + staticText = "put its counters on up to one target creature you control"; + } + + private HostOfTheHereafterDiesEffect(final HostOfTheHereafterDiesEffect effect) { + super(effect); + } + + @Override + public HostOfTheHereafterDiesEffect copy() { + return new HostOfTheHereafterDiesEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + Permanent deadCreature = (Permanent) this.getValue("creatureDied"); + if (permanent == null || deadCreature == null) { + return false; + } + Counters counters = deadCreature.getCounters(game); + counters.values().stream() + .filter(counter -> counter.getCount() > 0) + .forEach(counter -> permanent.addCounters(counter, source, game)); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 42ee85092ff..d1cfbf160d2 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -124,6 +124,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Herd Heirloom", 144, Rarity.RARE, mage.cards.h.HerdHeirloom.class)); cards.add(new SetCardInfo("Heritage Reclamation", 145, Rarity.COMMON, mage.cards.h.HeritageReclamation.class)); cards.add(new SetCardInfo("Highspire Bell-Ringer", 47, Rarity.COMMON, mage.cards.h.HighspireBellRinger.class)); + cards.add(new SetCardInfo("Host of the Hereafter", 193, Rarity.UNCOMMON, mage.cards.h.HostOfTheHereafter.class)); cards.add(new SetCardInfo("Humbling Elder", 48, Rarity.COMMON, mage.cards.h.HumblingElder.class)); cards.add(new SetCardInfo("Iceridge Serpent", 49, Rarity.COMMON, mage.cards.i.IceridgeSerpent.class)); cards.add(new SetCardInfo("Inevitable Defeat", 194, Rarity.RARE, mage.cards.i.InevitableDefeat.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/HostOfTheHereafterTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/HostOfTheHereafterTest.java new file mode 100644 index 00000000000..69624983da0 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/HostOfTheHereafterTest.java @@ -0,0 +1,94 @@ +package org.mage.test.cards.single.tdm; + + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class HostOfTheHereafterTest extends CardTestPlayerBase { + + public static final String HOST = "Host of the Hereafter"; + public static final String BAYOU = "Bayou"; + public static final String CUB = "Bear Cub"; + public static final String DOWNFALL = "Hero's Downfall"; + public static final String FEED = "Feed the Serpent"; + + @Test + public void testEntersWithCounters() { + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, HOST); + addCard(Zone.BATTLEFIELD, playerA, BAYOU, 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, HOST); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, HOST, 1); + assertCounterCount(playerA, HOST, CounterType.P1P1, 2); + } + + @Test + public void testDiesMoveCounters() { + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, HOST); + addCard(Zone.HAND, playerA, DOWNFALL); + addCard(Zone.BATTLEFIELD, playerA, BAYOU, 7); + addCard(Zone.BATTLEFIELD, playerA, CUB); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, HOST); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, DOWNFALL, HOST); + addTarget(playerA, CUB); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, CUB, 1); + assertCounterCount(playerA, CUB, CounterType.P1P1, 2); + } + + @Test + public void exileNoCounters() { + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, HOST); + addCard(Zone.HAND, playerA, FEED); + addCard(Zone.BATTLEFIELD, playerA, BAYOU, 8); + addCard(Zone.BATTLEFIELD, playerA, CUB); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, HOST); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, FEED, HOST); + + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, CUB, 1); + assertCounterCount(playerA, CUB, CounterType.P1P1, 0); + } + + @Test + public void testOtherCounters() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, HOST); + addCard(Zone.HAND, playerA, DOWNFALL); + addCard(Zone.BATTLEFIELD, playerA, BAYOU, 4); + addCard(Zone.BATTLEFIELD, playerA, CUB); + + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, CUB, CounterType.FLYING, 1); + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, CUB, CounterType.LIFELINK, 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, DOWNFALL, CUB); + addTarget(playerA, HOST); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, HOST, 1); + assertCounterCount(playerA, HOST, CounterType.FLYING, 1); + assertCounterCount(playerA, HOST, CounterType.LIFELINK, 1); + } + +} \ No newline at end of file From 30ffe9a1593d0985df0da15a74bdf020d2afef46 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Wed, 9 Apr 2025 13:11:33 -0500 Subject: [PATCH 086/133] Fix Chitin Gravestalker hint text --- Mage.Sets/src/mage/cards/c/ChitinGravestalker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/c/ChitinGravestalker.java b/Mage.Sets/src/mage/cards/c/ChitinGravestalker.java index 1499612e08e..f33c4dd99fa 100644 --- a/Mage.Sets/src/mage/cards/c/ChitinGravestalker.java +++ b/Mage.Sets/src/mage/cards/c/ChitinGravestalker.java @@ -34,7 +34,7 @@ public final class ChitinGravestalker extends CardImpl { } private static final DynamicValue xValue = new CardsInControllerGraveyardCount(filter); - private static final Hint hint = new ValueHint("Instant and sorcery card in your graveyard", xValue); + private static final Hint hint = new ValueHint("Artifact and/or creature cards in your graveyard", xValue); public ChitinGravestalker(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{B}"); From 0848382dcd6d55cf84dbc9c95ffc94634855f97a Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:15:59 -0500 Subject: [PATCH 087/133] [TDM] Implement Dragonfire Blade (#13522) * [TDM] Implement Dragonfire Blade --- .../src/mage/cards/d/DragonfireBlade.java | 83 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 2 + .../cards/single/tdm/DragonfireBladeTest.java | 49 +++++++++++ 3 files changed, 134 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DragonfireBlade.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/DragonfireBladeTest.java diff --git a/Mage.Sets/src/mage/cards/d/DragonfireBlade.java b/Mage.Sets/src/mage/cards/d/DragonfireBlade.java new file mode 100644 index 00000000000..c417986a982 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DragonfireBlade.java @@ -0,0 +1,83 @@ +package mage.cards.d; + +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.CostAdjuster; +import mage.abilities.effects.common.continuous.BoostEquippedEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.EquipAbility; +import mage.abilities.keyword.HexproofFromMonocoloredAbility; +import mage.abilities.mana.ManaOptions; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.util.CardUtil; + +/** + * + * @author Jmlundeen + */ +public final class DragonfireBlade extends CardImpl { + + public DragonfireBlade(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Equipped creature gets +2/+2 and has hexproof from monocolored. + Ability ability = new SimpleStaticAbility(new BoostEquippedEffect(2, 2)); + ability.addEffect(new GainAbilityAttachedEffect(HexproofFromMonocoloredAbility.getInstance(), AttachmentType.EQUIPMENT) + .setText("and has hexproof from monocolored")); + this.addAbility(ability); + + // Equip {4}. This ability costs {1} less to activate for each color of the creature it targets. + EquipAbility equipAbility = new EquipAbility(4); + equipAbility.setCostAdjuster(DragonfireBladeCostAdjuster.instance); + equipAbility.setCostReduceText("This ability costs {1} less to activate for each color of the creature it targets."); + this.addAbility(equipAbility); + } + + private DragonfireBlade(final DragonfireBlade card) { + super(card); + } + + @Override + public DragonfireBlade copy() { + return new DragonfireBlade(this); + } +} + +enum DragonfireBladeCostAdjuster implements CostAdjuster { + instance; + + @Override + public void reduceCost(Ability ability, Game game) { + int reduceCount = 0; + if (game.inCheckPlayableState()) { + reduceCount = game.getBattlefield().getAllActivePermanents(ability.getControllerId()) + .stream() + .filter(Permanent::isCreature) + .mapToInt(permanent -> permanent.getColor(game).getColorCount()) + .max() + .orElse(reduceCount); + } + else { + Permanent target = game.getPermanent(ability.getFirstTarget()); + if (target != null) { + reduceCount = target.getColor(game).getColorCount(); + } + + } + CardUtil.reduceCost(ability, reduceCount); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index d1cfbf160d2..c5b32875aef 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -86,6 +86,8 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Dragonback Lancer", 9, Rarity.COMMON, mage.cards.d.DragonbackLancer.class)); cards.add(new SetCardInfo("Dragonbroods' Relic", 140, Rarity.UNCOMMON, mage.cards.d.DragonbroodsRelic.class)); cards.add(new SetCardInfo("Dragonclaw Strike", 180, Rarity.UNCOMMON, mage.cards.d.DragonclawStrike.class)); + cards.add(new SetCardInfo("Dragonfire Blade", 240, Rarity.RARE, mage.cards.d.DragonfireBlade.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Dragonfire Blade", 324, Rarity.RARE, mage.cards.d.DragonfireBlade.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Dragonologist", 42, Rarity.RARE, mage.cards.d.Dragonologist.class)); cards.add(new SetCardInfo("Dragonstorm Forecaster", 43, Rarity.UNCOMMON, mage.cards.d.DragonstormForecaster.class)); cards.add(new SetCardInfo("Dragonstorm Globe", 241, Rarity.COMMON, mage.cards.d.DragonstormGlobe.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/DragonfireBladeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/DragonfireBladeTest.java new file mode 100644 index 00000000000..81c117e21ba --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/DragonfireBladeTest.java @@ -0,0 +1,49 @@ +package org.mage.test.cards.single.tdm; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import static org.junit.jupiter.api.Assertions.*; + +public class DragonfireBladeTest extends CardTestPlayerBase { + + private static final String blade = "Dragonfire Blade"; + private static final String equipText = "Equip {4}"; + public static final String ornithopter = "Ornithopter"; // colorless 0/1 creature + private static final String turtle = "Aegis Turtle"; // U 0/5 creature + private static final String leotau = "Grizzled Leotau"; // GW 1/5 creature + private static final String mantis = "Mantis Rider"; // URW 3/3 creature + private static final String glint = "Glint-Eye Nephilim"; // UBRG 2/2 creature + + @Test + public void colorsTest() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, blade); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4 + 3 + 2 + 1); + addCard(Zone.BATTLEFIELD, playerA, ornithopter); + addCard(Zone.BATTLEFIELD, playerA, turtle); + addCard(Zone.BATTLEFIELD, playerA, leotau); + addCard(Zone.BATTLEFIELD, playerA, mantis); + addCard(Zone.BATTLEFIELD, playerA, glint); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, equipText, ornithopter); + waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, equipText, turtle); + waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, equipText, leotau); + waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, equipText, mantis); + waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, equipText, glint); + waitStackResolved(1,PhaseStep.PRECOMBAT_MAIN); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAttachedTo(playerA, blade, glint, true); + assertPowerToughness(playerA, glint, 4, 4); + } +} \ No newline at end of file From e552a54e6f246d781ff4556ef5a4097bcc05e986 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:16:48 -0500 Subject: [PATCH 088/133] [TDM] Focus the Mind and Sage of the Skies (#13524) * [TDM] Implement Focus the Mind * [TDM] Implement Sage of the Skies * Extracted cast another spell this turn condition from Rally the Monastery * Add condition to Rally the Monastery and Slick Sequence --- Mage.Sets/src/mage/cards/f/FocusTheMind.java | 44 ++++++++++ .../src/mage/cards/r/RallyTheMonastery.java | 41 +--------- .../src/mage/cards/s/SageOfTheSkies.java | 54 ++++++++++++ Mage.Sets/src/mage/cards/s/SlickSequence.java | 82 +------------------ .../src/mage/sets/TarkirDragonstorm.java | 3 + .../CastAnotherSpellThisTurnCondition.java | 44 ++++++++++ 6 files changed, 152 insertions(+), 116 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/f/FocusTheMind.java create mode 100644 Mage.Sets/src/mage/cards/s/SageOfTheSkies.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/CastAnotherSpellThisTurnCondition.java diff --git a/Mage.Sets/src/mage/cards/f/FocusTheMind.java b/Mage.Sets/src/mage/cards/f/FocusTheMind.java new file mode 100644 index 00000000000..cdce75924b6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FocusTheMind.java @@ -0,0 +1,44 @@ +package mage.cards.f; + +import java.util.UUID; + +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.CastAnotherSpellThisTurnCondition; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; + +/** + * + * @author Jmlundeen + */ +public final class FocusTheMind extends CardImpl { + + public FocusTheMind(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{U}"); + + + // This spell costs {2} less to cast if you've cast another spell this turn. + Effect effect = new SpellCostReductionSourceEffect(2, CastAnotherSpellThisTurnCondition.instance) + .setCanWorksOnStackOnly(true); + this.addAbility(new SimpleStaticAbility(Zone.ALL, effect) + .setRuleAtTheTop(true) + .addHint(CastAnotherSpellThisTurnCondition.instance.getHint())); + + // Draw three cards, then discard a card. + this.getSpellAbility().addEffect(new DrawDiscardControllerEffect(3, 1)); + } + + private FocusTheMind(final FocusTheMind card) { + super(card); + } + + @Override + public FocusTheMind copy() { + return new FocusTheMind(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RallyTheMonastery.java b/Mage.Sets/src/mage/cards/r/RallyTheMonastery.java index 335b01cee14..7867c2c0c48 100644 --- a/Mage.Sets/src/mage/cards/r/RallyTheMonastery.java +++ b/Mage.Sets/src/mage/cards/r/RallyTheMonastery.java @@ -1,19 +1,14 @@ package mage.cards.r; -import java.util.List; -import java.util.Objects; import java.util.UUID; -import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CastAnotherSpellThisTurnCondition; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.DestroyTargetEffect; import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; -import mage.abilities.hint.ConditionHint; -import mage.abilities.hint.Hint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -22,23 +17,15 @@ import mage.constants.Zone; import mage.filter.StaticFilters; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.PowerPredicate; -import mage.game.Game; -import mage.game.permanent.token.BirdToken; import mage.game.permanent.token.MonasteryMentorToken; -import mage.game.stack.Spell; import mage.target.TargetPermanent; import mage.target.common.TargetControlledCreaturePermanent; -import mage.watchers.common.SpellsCastWatcher; /** * @author balazskristof */ public final class RallyTheMonastery extends CardImpl { - private static final Hint hint = new ConditionHint( - RallyTheMonasteryCondition.instance, "You've cast a spell this turn" - ); - private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with power 4 or greater"); static { @@ -51,8 +38,8 @@ public final class RallyTheMonastery extends CardImpl { // This spell costs {2} less to cast if you've cast another spell this turn. this.addAbility(new SimpleStaticAbility( Zone.ALL, - new SpellCostReductionSourceEffect(2, RallyTheMonasteryCondition.instance).setCanWorksOnStackOnly(true) - ).setRuleAtTheTop(true).addHint(hint)); + new SpellCostReductionSourceEffect(2, CastAnotherSpellThisTurnCondition.instance).setCanWorksOnStackOnly(true) + ).setRuleAtTheTop(true).addHint(CastAnotherSpellThisTurnCondition.instance.getHint())); // Choose one — this.getSpellAbility().getModes().setMinModes(1); @@ -79,25 +66,3 @@ public final class RallyTheMonastery extends CardImpl { } } -enum RallyTheMonasteryCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher == null) { - return false; - } - List spells = watcher.getSpellsCastThisTurn(source.getControllerId()); - return spells != null && spells - .stream() - .filter(Objects::nonNull) - .anyMatch(spell -> !spell.getSourceId().equals(source.getSourceId())); - } - - @Override - public String toString() { - return "you've cast another spell this turn"; - } -} - diff --git a/Mage.Sets/src/mage/cards/s/SageOfTheSkies.java b/Mage.Sets/src/mage/cards/s/SageOfTheSkies.java new file mode 100644 index 00000000000..f39973e8f30 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SageOfTheSkies.java @@ -0,0 +1,54 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.condition.common.CastAnotherSpellThisTurnCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CastSourceTriggeredAbility; +import mage.abilities.effects.common.CopySourceSpellEffect; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; + +/** + * + * @author Jmlundeen + */ +public final class SageOfTheSkies extends CardImpl { + + public SageOfTheSkies(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.MONK); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // When you cast this spell, if you've cast another spell this turn, copy this spell. + OneShotEffect effect = new CopySourceSpellEffect().setText("copy this spell. (The copy becomes a token.)"); + this.addAbility(new CastSourceTriggeredAbility(effect) + .withInterveningIf(CastAnotherSpellThisTurnCondition.instance) + .addHint(CastAnotherSpellThisTurnCondition.instance.getHint()) + ); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + } + + private SageOfTheSkies(final SageOfTheSkies card) { + super(card); + } + + @Override + public SageOfTheSkies copy() { + return new SageOfTheSkies(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SlickSequence.java b/Mage.Sets/src/mage/cards/s/SlickSequence.java index 4c5d96c7bd0..852d441cf0c 100644 --- a/Mage.Sets/src/mage/cards/s/SlickSequence.java +++ b/Mage.Sets/src/mage/cards/s/SlickSequence.java @@ -1,22 +1,13 @@ package mage.cards.s; -import mage.MageObjectReference; -import mage.abilities.Ability; -import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CastAnotherSpellThisTurnCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; -import mage.abilities.hint.ConditionHint; -import mage.abilities.hint.Hint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.WatcherScope; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.stack.Spell; import mage.target.common.TargetAnyTarget; -import mage.watchers.Watcher; import java.util.*; @@ -34,11 +25,10 @@ public final class SlickSequence extends CardImpl { this.getSpellAbility().addEffect( new ConditionalOneShotEffect( new DrawCardSourceControllerEffect(1), - SlickSequenceCondition.instance + CastAnotherSpellThisTurnCondition.instance ) ); - this.getSpellAbility().addHint(SlickSequenceCondition.hint); - this.getSpellAbility().addWatcher(new SlickSequenceWatcher()); + this.getSpellAbility().addHint(CastAnotherSpellThisTurnCondition.instance.getHint()); } private SlickSequence(final SlickSequence card) { @@ -49,68 +39,4 @@ public final class SlickSequence extends CardImpl { public SlickSequence copy() { return new SlickSequence(this); } -} - - -enum SlickSequenceCondition implements Condition { - instance; - static final Hint hint = new ConditionHint(instance, "you've cast another spell this turn"); - - @Override - public boolean apply(Game game, Ability source) { - SlickSequenceWatcher watcher = game.getState().getWatcher(SlickSequenceWatcher.class); - if (watcher == null) { - return false; - } - // may be null, watcher will handle that. - Spell sourceSpell = game.getSpell(source.getSourceId()); - return watcher.didPlayerCastAnotherSpellThisTurn(source.getControllerId(), sourceSpell, game); - } - - @Override - public String toString() { - return "you've cast another spell this turn"; - } -} - -class SlickSequenceWatcher extends Watcher { - - // Per player, MOR of the spells cast this turn. - private final Map> spellsCastThisTurn = new HashMap<>(); - - /** - * Game default watcher - */ - public SlickSequenceWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (event.getType() == GameEvent.EventType.SPELL_CAST) { - UUID playerId = event.getPlayerId(); - if (playerId != null) { - MageObjectReference mor = new MageObjectReference(event.getTargetId(), game); - spellsCastThisTurn.computeIfAbsent(playerId, x -> new HashSet<>()).add(mor); - } - } - } - - @Override - public void reset() { - super.reset(); - spellsCastThisTurn.clear(); - } - - boolean didPlayerCastAnotherSpellThisTurn(UUID playerId, Spell spell, Game game) { - Set spells = spellsCastThisTurn.getOrDefault(playerId, new HashSet<>()); - if (spell == null) { - // Not currently a spell, so any spell recorded does count as another. - return !spells.isEmpty(); - } else { - // Is currently a spell, so need to exclude that one. - MageObjectReference spellMOR = new MageObjectReference(spell.getId(), game); - return spells.stream().anyMatch(mor -> !mor.equals(spellMOR)); - } - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index c5b32875aef..f00f98c0d99 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -107,6 +107,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Fire-Rim Form", 107, Rarity.COMMON, mage.cards.f.FireRimForm.class)); cards.add(new SetCardInfo("Flamehold Grappler", 185, Rarity.RARE, mage.cards.f.FlameholdGrappler.class)); cards.add(new SetCardInfo("Fleeting Effigy", 108, Rarity.UNCOMMON, mage.cards.f.FleetingEffigy.class)); + cards.add(new SetCardInfo("Focus the Mind", 45, Rarity.COMMON, mage.cards.f.FocusTheMind.class)); cards.add(new SetCardInfo("Forest", 285, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Formation Breaker", 143, Rarity.UNCOMMON, mage.cards.f.FormationBreaker.class)); cards.add(new SetCardInfo("Fortress Kin-Guard", 12, Rarity.COMMON, mage.cards.f.FortressKinGuard.class)); @@ -205,6 +206,8 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Rugged Highlands", 265, Rarity.COMMON, mage.cards.r.RuggedHighlands.class)); cards.add(new SetCardInfo("Runescale Stormbrood", 221, Rarity.UNCOMMON, mage.cards.r.RunescaleStormbrood.class)); cards.add(new SetCardInfo("Sage of the Fang", 155, Rarity.UNCOMMON, mage.cards.s.SageOfTheFang.class)); + cards.add(new SetCardInfo("Sage of the Skies", 22, Rarity.RARE, mage.cards.s.SageOfTheSkies.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sage of the Skies", 328, Rarity.RARE, mage.cards.s.SageOfTheSkies.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sagu Pummeler", 156, Rarity.COMMON, mage.cards.s.SaguPummeler.class)); cards.add(new SetCardInfo("Sagu Wildling", 157, Rarity.COMMON, mage.cards.s.SaguWildling.class)); cards.add(new SetCardInfo("Salt Road Packbeast", 23, Rarity.COMMON, mage.cards.s.SaltRoadPackbeast.class)); diff --git a/Mage/src/main/java/mage/abilities/condition/common/CastAnotherSpellThisTurnCondition.java b/Mage/src/main/java/mage/abilities/condition/common/CastAnotherSpellThisTurnCondition.java new file mode 100644 index 00000000000..10ee9bdd27f --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/CastAnotherSpellThisTurnCondition.java @@ -0,0 +1,44 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.watchers.common.SpellsCastWatcher; + +import java.util.List; +import java.util.Objects; + +/** + * @author balazskristof, Jmlundeen + */ +public enum CastAnotherSpellThisTurnCondition implements Condition { + instance; + private final Hint hint = new ConditionHint( + this, "You've cast another spell this turn" + ); + + @Override + public boolean apply(Game game, Ability source) { + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (watcher == null) { + return false; + } + List spells = watcher.getSpellsCastThisTurn(source.getControllerId()); + return spells != null && spells + .stream() + .filter(Objects::nonNull) + .anyMatch(spell -> !spell.getSourceId().equals(source.getSourceId()) || spell.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter()); + } + + public Hint getHint() { + return hint; + } + + @Override + public String toString() { + return "you've cast another spell this turn"; + } +} From 7b472e28f31cd9c7b58e0f41967c20395c0f0c94 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 15:03:44 -0400 Subject: [PATCH 089/133] fix test failure --- Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java b/Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java index 555110515f8..e510fe217cd 100644 --- a/Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java +++ b/Mage.Sets/src/mage/cards/r/RotCurseRakshasa.java @@ -16,6 +16,7 @@ import mage.constants.CardType; import mage.constants.SubType; import mage.constants.Zone; import mage.counters.CounterType; +import mage.target.common.TargetCreaturePermanent; import mage.target.targetadjustment.TargetsCountAdjuster; import java.util.UUID; @@ -46,6 +47,7 @@ public final class RotCurseRakshasa extends CardImpl { new ManaCostsImpl<>("{X}{B}{B}") ); ability.addCost(new ExileSourceFromGraveCost()); + ability.addTarget(new TargetCreaturePermanent()); ability.setTargetAdjuster(new TargetsCountAdjuster(GetXValue.instance)); this.addAbility(ability.setAbilityWord(AbilityWord.RENEW)); } From aa637b414b4d18d298341cac9a17455d0230f02a Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:34:44 -0400 Subject: [PATCH 090/133] [TDC] Implement Colossal Grave-Reaver --- .../src/mage/cards/c/ColossalGraveReaver.java | 164 ++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 165 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java diff --git a/Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java b/Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java new file mode 100644 index 00000000000..f7b35440a66 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java @@ -0,0 +1,164 @@ +package mage.cards.c; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.cards.*; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.abilities.keyword.FlyingAbility; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeBatchEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.targetpointer.FixedTargets; + +/** + * + * @author Grath + */ +public final class ColossalGraveReaver extends CardImpl { + + public ColossalGraveReaver(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{6}{B}{G}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(7); + this.toughness = new MageInt(6); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever this creature enters or attacks, mill three cards. + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new MillCardsControllerEffect(3))); + + // Whenever one or more creature cards are put into your graveyard from your library, put one of them onto the battlefield. + this.addAbility(new ColossalGraveReaverTriggeredAbility()); + } + + private ColossalGraveReaver(final ColossalGraveReaver card) { + super(card); + } + + @Override + public ColossalGraveReaver copy() { + return new ColossalGraveReaver(this); + } +} + +class ColossalGraveReaverEffect extends OneShotEffect { + + private static final FilterCard defaultFilter = new FilterCard("creature to return to battlefield"); + + public ColossalGraveReaverEffect() { + super(Outcome.PutCreatureInPlay); + staticText = "put one of them onto the battlefield"; + } + + protected ColossalGraveReaverEffect(final ColossalGraveReaverEffect effect) { + super(effect); + } + + @Override + public ColossalGraveReaverEffect copy() { + return new ColossalGraveReaverEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + Cards cardsToChooseFrom = new CardsImpl(); + for (UUID targetId : getTargetPointer().getTargets(game, source)) { + Card card = game.getCard(targetId); + if (card != null && game.getState().getZone(card.getId()) == Zone.GRAVEYARD) { + cardsToChooseFrom.add(card); + } + } + Set cardsToMove; + switch (cardsToChooseFrom.size()) { + case 0: + return false; + case 1: + cardsToMove = new HashSet<>(cardsToChooseFrom.getCards(game)); + break; + default: + cardsToMove = new HashSet<>(); + TargetCard target = new TargetCard(0, 1, Zone.ALL, defaultFilter); + target.withNotTarget(true); + controller.choose(Outcome.PlayForFree, cardsToChooseFrom, target, source, game); + cardsToMove.add(cardsToChooseFrom.get(target.getFirstTarget(), game)); + + } + controller.moveCards(cardsToMove, Zone.BATTLEFIELD, source, game, false, false, false, null); + return true; + } + return false; + } +} + +class ColossalGraveReaverTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { + + ColossalGraveReaverTriggeredAbility() { + super(Zone.BATTLEFIELD, new ColossalGraveReaverEffect()); + } + + private ColossalGraveReaverTriggeredAbility(final ColossalGraveReaverTriggeredAbility ability) { + super(ability); + } + + @Override + public ColossalGraveReaverTriggeredAbility copy() { + return new ColossalGraveReaverTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; + } + + @Override + public boolean checkEvent(ZoneChangeEvent event, Game game) { + if (event.getFromZone() != Zone.LIBRARY || event.getToZone() != Zone.GRAVEYARD) { + return false; + } + Card card = game.getCard(event.getTargetId()); + return card != null && card.isCreature(game) && card.isOwnedBy(getControllerId()); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Set set = getFilteredEvents((ZoneChangeBatchEvent) event, game) + .stream() + .map(ZoneChangeEvent::getTargetId) + .map(game::getCard) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (set.isEmpty()) { + return false; + } + this.getEffects().setTargetPointer(new FixedTargets(set, game)); + return true; + } + + @Override + public String getRule() { + return "Whenever one or more creature cards are put into your graveyard " + + "from your library, put one of them onto the battlefield tapped."; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 88724f24d04..8a45e6ac406 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -75,6 +75,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Cinder Glade", 350, Rarity.RARE, mage.cards.c.CinderGlade.class)); cards.add(new SetCardInfo("Clifftop Retreat", 351, Rarity.RARE, mage.cards.c.ClifftopRetreat.class)); cards.add(new SetCardInfo("Colfenor's Urn", 315, Rarity.RARE, mage.cards.c.ColfenorsUrn.class)); + cards.add(new SetCardInfo("Colossal Grave-Reaver", 50, Rarity.RARE, mage.cards.c.ColossalGraveReaver.class)); cards.add(new SetCardInfo("Command Beacon", 352, Rarity.RARE, mage.cards.c.CommandBeacon.class)); cards.add(new SetCardInfo("Command Tower", 107, Rarity.COMMON, mage.cards.c.CommandTower.class)); cards.add(new SetCardInfo("Commander's Insignia", 111, Rarity.RARE, mage.cards.c.CommandersInsignia.class)); From e00d5359b1300128b7b089f51dec549b0348e83c Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:36:24 -0400 Subject: [PATCH 091/133] Fix minimum targets to 1 for non-optional effect of Colossal Grave-Reaver. --- Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java b/Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java index f7b35440a66..3b130b45c27 100644 --- a/Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java +++ b/Mage.Sets/src/mage/cards/c/ColossalGraveReaver.java @@ -99,7 +99,7 @@ class ColossalGraveReaverEffect extends OneShotEffect { break; default: cardsToMove = new HashSet<>(); - TargetCard target = new TargetCard(0, 1, Zone.ALL, defaultFilter); + TargetCard target = new TargetCard(1, 1, Zone.ALL, defaultFilter); target.withNotTarget(true); controller.choose(Outcome.PlayForFree, cardsToChooseFrom, target, source, game); cardsToMove.add(cardsToChooseFrom.get(target.getFirstTarget(), game)); From 4ffb0ff01419823f07d5241c13b3870e4b14d10a Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 19:19:22 -0400 Subject: [PATCH 092/133] fix verify failure --- Mage.Sets/src/mage/sets/TarkirDragonstorm.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index f00f98c0d99..2254f89e5f6 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -1,12 +1,12 @@ package mage.sets; -import java.util.Arrays; -import java.util.List; - import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; +import java.util.Arrays; +import java.util.List; + /** * @author TheElk801 */ @@ -24,6 +24,8 @@ public final class TarkirDragonstorm extends ExpansionSet { this.blockName = "Tarkir: Dragonstorm"; // for sorting in GUI this.hasBasicLands = true; + this.enablePlayBooster(Integer.MAX_VALUE); + cards.add(new SetCardInfo("Abzan Devotee", 68, Rarity.COMMON, mage.cards.a.AbzanDevotee.class)); cards.add(new SetCardInfo("Abzan Monument", 238, Rarity.UNCOMMON, mage.cards.a.AbzanMonument.class)); cards.add(new SetCardInfo("Adorned Crocodile", 69, Rarity.COMMON, mage.cards.a.AdornedCrocodile.class)); From dcdf0ca4a5291a5c0d52afcc78b5e907bcf69b6a Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Thu, 10 Apr 2025 12:36:39 +0400 Subject: [PATCH 093/133] Choose a player at random - fixed that it wrongly choose same player (example: Scrambleverse, close #12679, close #13526); Inniaz, the Gale Force - fixed that it wrongly choose left/right player (close #13526); --- .../multiplayer/PlayersListAndOrderTest.java | 30 +++++++++++++++++++ .../src/main/java/mage/util/CircularList.java | 4 +-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayersListAndOrderTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayersListAndOrderTest.java index 0e4d6bed1f1..d0809f3d3d0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayersListAndOrderTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayersListAndOrderTest.java @@ -115,6 +115,36 @@ public class PlayersListAndOrderTest extends CardTestMultiPlayerBase { execute(); } + @Test + public void test_Game_PlayerListMustHaveAccessByIndex() { + // make sure CircularList return same data by index, see #13526 + + PlayerList players = new PlayerList(); + players.add(playerA.getId()); + players.add(playerB.getId()); + players.add(playerC.getId()); + players.add(playerD.getId()); + Assert.assertEquals("last added player must be current", players.get(0), playerD.getId()); + List staticList = new ArrayList<>(players); + + // normal + Assert.assertEquals(players.get(0), staticList.get(0)); + Assert.assertEquals(players.get(1), staticList.get(1)); + Assert.assertEquals(players.get(2), staticList.get(2)); + Assert.assertEquals(players.get(3), staticList.get(3)); + Assert.assertEquals(players.get(2), staticList.get(2)); // make sure no depends on calls order + + // make sure CircularList keeps inner structure + players.setCurrent(playerC.getId()); + Assert.assertEquals(players.get(0), staticList.get(0)); + Assert.assertEquals(players.get(1), staticList.get(1)); + Assert.assertEquals(players.get(2), staticList.get(2)); + Assert.assertEquals(players.get(3), staticList.get(3)); + Assert.assertEquals(players.get(2), staticList.get(2)); // make sure no depends on calls order + + Assert.assertNull("must return null on non existing item", players.get(999)); + } + @Test public void test_Game_GetPlayerList() { // game.getPlayerList() - APNAP order, for cards usage diff --git a/Mage/src/main/java/mage/util/CircularList.java b/Mage/src/main/java/mage/util/CircularList.java index 5fb6a09c6d6..45890ad68f1 100644 --- a/Mage/src/main/java/mage/util/CircularList.java +++ b/Mage/src/main/java/mage/util/CircularList.java @@ -82,8 +82,8 @@ public class CircularList implements List, Iterable, Serializable { */ @Override public E get(int index) { - if (list.size() > this.index) { - return list.get(this.index); + if (list.size() > index) { + return list.get(index); } return null; } From 0b4c9d90b75fd25e838cdbb14f8f0e0f11579d24 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Thu, 10 Apr 2025 12:57:49 +0400 Subject: [PATCH 094/133] merge fix --- .../java/org/mage/test/multiplayer/PlayersListAndOrderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayersListAndOrderTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayersListAndOrderTest.java index d0809f3d3d0..1d1fbc6ad87 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayersListAndOrderTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayersListAndOrderTest.java @@ -124,8 +124,8 @@ public class PlayersListAndOrderTest extends CardTestMultiPlayerBase { players.add(playerB.getId()); players.add(playerC.getId()); players.add(playerD.getId()); - Assert.assertEquals("last added player must be current", players.get(0), playerD.getId()); List staticList = new ArrayList<>(players); + Assert.assertEquals("last added player must be current", staticList.get(0), playerD.getId()); // normal Assert.assertEquals(players.get(0), staticList.get(0)); From 5165282d7d7ea482487a28e28cd9d556acbd5a12 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 9 Apr 2025 19:56:35 -0400 Subject: [PATCH 095/133] revert verify skip --- .../src/test/java/mage/verify/VerifyCardDataTest.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index a258288f503..7013bdd1ab3 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -2161,13 +2161,10 @@ public class VerifyCardDataTest { ); // card can contain rules text from both sides, so must search ref card for all sides too String additionalName; - if (card instanceof AdventureCard) { // temporary, change when mtgjson is fixed - additionalName = ((AdventureCard) card).getSpellCard().getName(); + if (card instanceof CardWithSpellOption) { + additionalName = ((CardWithSpellOption) card).getSpellCard().getName(); } else if (card.isTransformable() && !card.isNightCard()) { additionalName = card.getSecondCardFace().getName(); - } else if (card instanceof OmenCard) { // temporary, change when mtgjson is fixed - checkMissNonTargeted = false; - additionalName = null; } else { additionalName = null; } From 2363fd639052ee665ed8ee5d4f5654f8c1ad55ed Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 07:44:23 -0400 Subject: [PATCH 096/133] [TDC] Implement Arbor Adherent --- Mage.Sets/src/mage/cards/a/AbzanMonument.java | 13 +---- Mage.Sets/src/mage/cards/a/ArborAdherent.java | 48 ++++++++++++++++ .../src/mage/cards/b/BighornerRancher.java | 7 ++- .../src/mage/cards/f/FlourishingHunter.java | 55 +++---------------- Mage.Sets/src/mage/cards/g/GlintWeaver.java | 4 +- .../src/mage/cards/h/HuatliTheSunsHeart.java | 4 +- .../src/mage/cards/l/LastMarchOfTheEnts.java | 3 +- .../mage/sets/TarkirDragonstormCommander.java | 1 + ...oughnessAmongControlledCreaturesValue.java | 26 ++++++--- 9 files changed, 88 insertions(+), 73 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/a/ArborAdherent.java diff --git a/Mage.Sets/src/mage/cards/a/AbzanMonument.java b/Mage.Sets/src/mage/cards/a/AbzanMonument.java index fc1899a2030..b9c120e1fba 100644 --- a/Mage.Sets/src/mage/cards/a/AbzanMonument.java +++ b/Mage.Sets/src/mage/cards/a/AbzanMonument.java @@ -9,8 +9,6 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.GreatestToughnessAmongControlledCreaturesValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; -import mage.abilities.hint.Hint; -import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -41,11 +39,6 @@ public final class AbzanMonument extends CardImpl { )); } - private static final Hint hint = new ValueHint( - "Greatest toughness among creatures you control", - GreatestToughnessAmongControlledCreaturesValue.instance - ); - public AbzanMonument(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}"); @@ -60,7 +53,7 @@ public final class AbzanMonument extends CardImpl { ); ability.addCost(new TapSourceCost()); ability.addCost(new SacrificeSourceCost()); - this.addAbility(ability.addHint(hint)); + this.addAbility(ability.addHint(GreatestToughnessAmongControlledCreaturesValue.ALL.getHint())); } private AbzanMonument(final AbzanMonument card) { @@ -93,9 +86,7 @@ class AbzanMonumentEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { return new SpiritXXToken( - GreatestToughnessAmongControlledCreaturesValue - .instance - .calculate(game, source, this) + GreatestToughnessAmongControlledCreaturesValue.ALL.calculate(game, source, this) ).putOntoBattlefield(1, game, source); } } diff --git a/Mage.Sets/src/mage/cards/a/ArborAdherent.java b/Mage.Sets/src/mage/cards/a/ArborAdherent.java new file mode 100644 index 00000000000..686eb403b1b --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/ArborAdherent.java @@ -0,0 +1,48 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.Mana; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.dynamicvalue.common.GreatestToughnessAmongControlledCreaturesValue; +import mage.abilities.mana.AnyColorManaAbility; +import mage.abilities.mana.DynamicManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ArborAdherent extends CardImpl { + + public ArborAdherent(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}"); + + this.subtype.add(SubType.DOG); + this.subtype.add(SubType.DRUID); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // {T}: Add one mana of any color. + this.addAbility(new AnyColorManaAbility()); + + // {T}: Add X mana of any one color, where X is the greatest toughness among other creatures you control. + this.addAbility(new DynamicManaAbility( + Mana.AnyMana(1), GreatestToughnessAmongControlledCreaturesValue.OTHER, + new TapSourceCost(), "add X mana of any one color, where X is the " + + "greatest toughness among other creatures you control", true + ).addHint(GreatestToughnessAmongControlledCreaturesValue.OTHER.getHint())); + } + + private ArborAdherent(final ArborAdherent card) { + super(card); + } + + @Override + public ArborAdherent copy() { + return new ArborAdherent(this); + } +} diff --git a/Mage.Sets/src/mage/cards/b/BighornerRancher.java b/Mage.Sets/src/mage/cards/b/BighornerRancher.java index 7532b135ed4..e5967f1599f 100644 --- a/Mage.Sets/src/mage/cards/b/BighornerRancher.java +++ b/Mage.Sets/src/mage/cards/b/BighornerRancher.java @@ -14,7 +14,6 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; import java.util.UUID; @@ -42,8 +41,10 @@ public final class BighornerRancher extends CardImpl { // Sacrifice Bighorner Rancher: You gain life equal to the greatest toughness among other creatures you control. this.addAbility(new SimpleActivatedAbility( - new GainLifeEffect(GreatestToughnessAmongControlledCreaturesValue.instance).setText("You gain life equal to the greatest toughness among other creatures you control."), - new SacrificeSourceCost())); + new GainLifeEffect(GreatestToughnessAmongControlledCreaturesValue.OTHER) + .setText("You gain life equal to the greatest toughness among other creatures you control."), + new SacrificeSourceCost() + ).addHint(GreatestToughnessAmongControlledCreaturesValue.OTHER.getHint())); } private BighornerRancher(final BighornerRancher card) { diff --git a/Mage.Sets/src/mage/cards/f/FlourishingHunter.java b/Mage.Sets/src/mage/cards/f/FlourishingHunter.java index 0f4979e105a..89fc37c1059 100644 --- a/Mage.Sets/src/mage/cards/f/FlourishingHunter.java +++ b/Mage.Sets/src/mage/cards/f/FlourishingHunter.java @@ -1,21 +1,17 @@ package mage.cards.f; -import java.util.UUID; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.effects.OneShotEffect; -import mage.constants.Outcome; -import mage.constants.SubType; +import mage.abilities.dynamicvalue.common.GreatestToughnessAmongControlledCreaturesValue; +import mage.abilities.effects.common.GainLifeEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.players.Player; +import mage.constants.SubType; + +import java.util.UUID; /** - * * @author weirddan455 */ public final class FlourishingHunter extends CardImpl { @@ -29,7 +25,10 @@ public final class FlourishingHunter extends CardImpl { this.toughness = new MageInt(6); // When Flourishing Hunter enters the battlefield, you gain life equal to the greatest toughness among other creatures you control. - this.addAbility(new EntersBattlefieldTriggeredAbility(new FlourishingHunterEffect())); + this.addAbility(new EntersBattlefieldTriggeredAbility( + new GainLifeEffect(GreatestToughnessAmongControlledCreaturesValue.OTHER) + .setText("you gain life equal to the greatest toughness among other creatures you control") + ).addHint(GreatestToughnessAmongControlledCreaturesValue.OTHER.getHint())); } private FlourishingHunter(final FlourishingHunter card) { @@ -41,39 +40,3 @@ public final class FlourishingHunter extends CardImpl { return new FlourishingHunter(this); } } - -class FlourishingHunterEffect extends OneShotEffect { - - FlourishingHunterEffect() { - super(Outcome.GainLife); - staticText = "you gain life equal to the greatest toughness among other creatures you control"; - } - - private FlourishingHunterEffect(final FlourishingHunterEffect effect) { - super(effect); - } - - @Override - public FlourishingHunterEffect copy() { - return new FlourishingHunterEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { - return false; - } - int greatestToughness = 0; - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(controller.getId())) { - if (permanent.isCreature(game) && !permanent.getId().equals(source.getSourceId())) { - int toughness = permanent.getToughness().getValue(); - if (toughness > greatestToughness) { - greatestToughness = toughness; - } - } - } - controller.gainLife(greatestToughness, game, source); - return true; - } -} diff --git a/Mage.Sets/src/mage/cards/g/GlintWeaver.java b/Mage.Sets/src/mage/cards/g/GlintWeaver.java index f99e13ef97a..626cd6f719c 100644 --- a/Mage.Sets/src/mage/cards/g/GlintWeaver.java +++ b/Mage.Sets/src/mage/cards/g/GlintWeaver.java @@ -32,10 +32,10 @@ public final class GlintWeaver extends CardImpl { // When Glint Weaver enters the battlefield, distribute three +1/+1 counters among one, two, or three target creatures, then you gain life equal to the greatest toughness among creatures you control. Ability ability = new EntersBattlefieldTriggeredAbility(new DistributeCountersEffect()); - ability.addEffect(new GainLifeEffect(GreatestToughnessAmongControlledCreaturesValue.instance) + ability.addEffect(new GainLifeEffect(GreatestToughnessAmongControlledCreaturesValue.ALL) .setText(", then you gain life equal to the greatest toughness among creatures you control")); ability.addTarget(new TargetCreaturePermanentAmount(3)); - this.addAbility(ability); + this.addAbility(ability.addHint(GreatestToughnessAmongControlledCreaturesValue.ALL.getHint())); } private GlintWeaver(final GlintWeaver card) { diff --git a/Mage.Sets/src/mage/cards/h/HuatliTheSunsHeart.java b/Mage.Sets/src/mage/cards/h/HuatliTheSunsHeart.java index d184a7ef852..37853526cdd 100644 --- a/Mage.Sets/src/mage/cards/h/HuatliTheSunsHeart.java +++ b/Mage.Sets/src/mage/cards/h/HuatliTheSunsHeart.java @@ -30,9 +30,9 @@ public final class HuatliTheSunsHeart extends CardImpl { // -3: You gain life equal to the greatest toughness among creatures you control. this.addAbility(new LoyaltyAbility(new GainLifeEffect( - GreatestToughnessAmongControlledCreaturesValue.instance, + GreatestToughnessAmongControlledCreaturesValue.ALL, "You gain life equal to the greatest toughness among creatures you control" - ), -3)); + ), -3).addHint(GreatestToughnessAmongControlledCreaturesValue.ALL.getHint())); } private HuatliTheSunsHeart(final HuatliTheSunsHeart card) { diff --git a/Mage.Sets/src/mage/cards/l/LastMarchOfTheEnts.java b/Mage.Sets/src/mage/cards/l/LastMarchOfTheEnts.java index b13c0c85eb7..95efa9a8b04 100644 --- a/Mage.Sets/src/mage/cards/l/LastMarchOfTheEnts.java +++ b/Mage.Sets/src/mage/cards/l/LastMarchOfTheEnts.java @@ -32,9 +32,10 @@ public final class LastMarchOfTheEnts extends CardImpl { // Draw cards equal to the greatest toughness among creatures you control, then put any number of creature cards from your hand onto the battlefield. this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect( - GreatestToughnessAmongControlledCreaturesValue.instance + GreatestToughnessAmongControlledCreaturesValue.ALL ).setText("draw cards equal to the greatest toughness among creatures you control")); this.getSpellAbility().addEffect(new LastMarchOfTheEntsEffect()); + this.getSpellAbility().addHint(GreatestToughnessAmongControlledCreaturesValue.ALL.getHint()); } private LastMarchOfTheEnts(final LastMarchOfTheEnts card) { diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 8a45e6ac406..abd3e3ebeff 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -30,6 +30,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Angel of Invention", 109, Rarity.MYTHIC, mage.cards.a.AngelOfInvention.class)); cards.add(new SetCardInfo("Anguished Unmaking", 279, Rarity.RARE, mage.cards.a.AnguishedUnmaking.class)); cards.add(new SetCardInfo("Arasta of the Endless Web", 244, Rarity.RARE, mage.cards.a.ArastaOfTheEndlessWeb.class)); + cards.add(new SetCardInfo("Arbor Adherent", 42, Rarity.RARE, mage.cards.a.ArborAdherent.class)); cards.add(new SetCardInfo("Arboreal Grazer", 245, Rarity.COMMON, mage.cards.a.ArborealGrazer.class)); cards.add(new SetCardInfo("Arcane Signet", 105, Rarity.UNCOMMON, mage.cards.a.ArcaneSignet.class)); cards.add(new SetCardInfo("Archmage Emeritus", 145, Rarity.RARE, mage.cards.a.ArchmageEmeritus.class)); diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java index bd60ddb158f..de7e9252c28 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java @@ -1,4 +1,3 @@ - package mage.abilities.dynamicvalue.common; import mage.MageInt; @@ -6,6 +5,9 @@ import mage.MageObject; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.game.Game; @@ -13,16 +15,21 @@ import mage.game.Game; * @author TheElk801 */ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicValue { - instance; + ALL(StaticFilters.FILTER_CONTROLLED_CREATURES), + OTHER(StaticFilters.FILTER_OTHER_CONTROLLED_CREATURES); + private final FilterPermanent filter; + private final Hint hint; + + GreatestToughnessAmongControlledCreaturesValue(FilterPermanent filter) { + this.filter = filter; + this.hint = new ValueHint("The greatest toughness among " + filter.getMessage(), this); + } @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { return game .getBattlefield() - .getActivePermanents( - StaticFilters.FILTER_CONTROLLED_CREATURE, - sourceAbility.getControllerId(), game - ) + .getActivePermanents(filter, sourceAbility.getControllerId(), game) .stream() .map(MageObject::getToughness) .mapToInt(MageInt::getValue) @@ -32,12 +39,12 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal @Override public GreatestToughnessAmongControlledCreaturesValue copy() { - return GreatestToughnessAmongControlledCreaturesValue.instance; + return GreatestToughnessAmongControlledCreaturesValue.ALL; } @Override public String getMessage() { - return "the greatest toughness among creatures you control"; + return "the greatest toughness among " + filter.getMessage(); } @Override @@ -45,4 +52,7 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal return "X"; } + public Hint getHint() { + return hint; + } } From 0abef9c5ef52f0d3fab0a0b674da51dc2f5570e9 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 07:50:41 -0400 Subject: [PATCH 097/133] [TDC] Implement Infantry Shield --- .../src/mage/cards/i/InfantryShield.java | 49 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 50 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/i/InfantryShield.java diff --git a/Mage.Sets/src/mage/cards/i/InfantryShield.java b/Mage.Sets/src/mage/cards/i/InfantryShield.java new file mode 100644 index 00000000000..7c776a83fd7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/InfantryShield.java @@ -0,0 +1,49 @@ +package mage.cards.i; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.SourcePermanentPowerValue; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.EquipAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.abilities.keyword.MobilizeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AttachmentType; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class InfantryShield extends CardImpl { + + public InfantryShield(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{R}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Equipped creature has menace and mobilize X, where X is its power. + Ability ability = new SimpleStaticAbility(new GainAbilityAttachedEffect( + new MenaceAbility(false), AttachmentType.EQUIPMENT + )); + ability.addEffect(new GainAbilityAttachedEffect( + new MobilizeAbility(SourcePermanentPowerValue.NOT_NEGATIVE), AttachmentType.EQUIPMENT + ).setText("and mobilize X, where X is its power")); + this.addAbility(ability); + + // Equip {2} + this.addAbility(new EquipAbility(2)); + } + + private InfantryShield(final InfantryShield card) { + super(card); + } + + @Override + public InfantryShield copy() { + return new InfantryShield(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index abd3e3ebeff..8808a412c92 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -164,6 +164,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Ikra Shidiqi, the Usurper", 100, Rarity.MYTHIC, mage.cards.i.IkraShidiqiTheUsurper.class)); cards.add(new SetCardInfo("Indomitable Ancients", 121, Rarity.RARE, mage.cards.i.IndomitableAncients.class)); cards.add(new SetCardInfo("Indulging Patrician", 292, Rarity.UNCOMMON, mage.cards.i.IndulgingPatrician.class)); + cards.add(new SetCardInfo("Infantry Shield", 35, Rarity.RARE, mage.cards.i.InfantryShield.class)); cards.add(new SetCardInfo("Infernal Grasp", 182, Rarity.UNCOMMON, mage.cards.i.InfernalGrasp.class)); cards.add(new SetCardInfo("Irrigated Farmland", 372, Rarity.RARE, mage.cards.i.IrrigatedFarmland.class)); cards.add(new SetCardInfo("Isolated Chapel", 373, Rarity.RARE, mage.cards.i.IsolatedChapel.class)); From e334f595397c6c6864cca7e29353cb9da0a7b242 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 07:57:58 -0400 Subject: [PATCH 098/133] [TDC] Implement Hammerhead Tyrant --- .../src/mage/cards/h/HammerheadTyrant.java | 87 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 88 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HammerheadTyrant.java diff --git a/Mage.Sets/src/mage/cards/h/HammerheadTyrant.java b/Mage.Sets/src/mage/cards/h/HammerheadTyrant.java new file mode 100644 index 00000000000..2ddd62db7c0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HammerheadTyrant.java @@ -0,0 +1,87 @@ +package mage.cards.h; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterNonlandPermanent; +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.TargetPermanent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HammerheadTyrant extends CardImpl { + + private static final FilterPermanent filter = new FilterNonlandPermanent( + "nonland permanent an opponent controls with mana value less than or equal to that spell's mana value" + ); + + static { + filter.add(TargetController.OPPONENT.getControllerPredicate()); + } + + public HammerheadTyrant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{U}{U}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(6); + this.toughness = new MageInt(6); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever you cast a spell, return up to one target nonland permanent an opponent controls with mana value less than or equal to that spell's mana value to its owner's hand. + Ability ability = new SpellCastControllerTriggeredAbility( + new ReturnToHandTargetEffect(), StaticFilters.FILTER_SPELL_A, false + ); + ability.addTarget(new TargetPermanent(0, 1, filter)); + this.addAbility(ability); + } + + private HammerheadTyrant(final HammerheadTyrant card) { + super(card); + } + + @Override + public HammerheadTyrant copy() { + return new HammerheadTyrant(this); + } +} + +enum HammerheadTyrantPredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return input + .getObject() + .getManaValue() + <= CardUtil + .castStream( + input.getSource() + .getEffects() + .stream() + .map(effect -> effect.getValue("spellCast")), + Spell.class + ) + .findFirst() + .map(Spell::getManaValue) + .orElse(-1); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 8808a412c92..cc2897e8b18 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -149,6 +149,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Grenzo, Havoc Raiser", 216, Rarity.RARE, mage.cards.g.GrenzoHavocRaiser.class)); cards.add(new SetCardInfo("Grisly Salvage", 290, Rarity.COMMON, mage.cards.g.GrislySalvage.class)); cards.add(new SetCardInfo("Guttersnipe", 217, Rarity.UNCOMMON, mage.cards.g.Guttersnipe.class)); + cards.add(new SetCardInfo("Hammerhead Tyrant", 21, Rarity.RARE, mage.cards.h.HammerheadTyrant.class)); cards.add(new SetCardInfo("Harbinger of the Hunt", 291, Rarity.RARE, mage.cards.h.HarbingerOfTheHunt.class)); cards.add(new SetCardInfo("Harrow", 258, Rarity.COMMON, mage.cards.h.Harrow.class)); cards.add(new SetCardInfo("Haughty Djinn", 154, Rarity.RARE, mage.cards.h.HaughtyDjinn.class)); From e4ffeba3e7f901ee43b5c664d09eee5cfec54569 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 08:04:23 -0400 Subject: [PATCH 099/133] [TDC] Implement Ironwill Forger --- .../src/mage/cards/i/IronwillForger.java | 57 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 58 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/i/IronwillForger.java diff --git a/Mage.Sets/src/mage/cards/i/IronwillForger.java b/Mage.Sets/src/mage/cards/i/IronwillForger.java new file mode 100644 index 00000000000..e933eeef99e --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/IronwillForger.java @@ -0,0 +1,57 @@ +package mage.cards.i; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.condition.common.ControlYourCommanderCondition; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.MyriadAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class IronwillForger extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledCreaturePermanent("nonlegendary creature you control"); + + static { + filter.add(Predicates.not(SuperType.LEGENDARY.getPredicate())); + } + + public IronwillForger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.subtype.add(SubType.ORC); + this.subtype.add(SubType.ARTIFICER); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Lieutenant -- At the beginning of combat on your turn, if you control your commander, target nonlegendary creature you control gains myriad until end of turn. + Ability ability = new BeginningOfCombatTriggeredAbility( + new GainAbilityTargetEffect(new MyriadAbility(false)) + ).withInterveningIf(ControlYourCommanderCondition.instance); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability.setAbilityWord(AbilityWord.LIEUTENANT)); + } + + private IronwillForger(final IronwillForger card) { + super(card); + } + + @Override + public IronwillForger copy() { + return new IronwillForger(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index cc2897e8b18..0c779be18bc 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -167,6 +167,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Indulging Patrician", 292, Rarity.UNCOMMON, mage.cards.i.IndulgingPatrician.class)); cards.add(new SetCardInfo("Infantry Shield", 35, Rarity.RARE, mage.cards.i.InfantryShield.class)); cards.add(new SetCardInfo("Infernal Grasp", 182, Rarity.UNCOMMON, mage.cards.i.InfernalGrasp.class)); + cards.add(new SetCardInfo("Ironwill Forger", 13, Rarity.RARE, mage.cards.i.IronwillForger.class)); cards.add(new SetCardInfo("Irrigated Farmland", 372, Rarity.RARE, mage.cards.i.IrrigatedFarmland.class)); cards.add(new SetCardInfo("Isolated Chapel", 373, Rarity.RARE, mage.cards.i.IsolatedChapel.class)); cards.add(new SetCardInfo("Izzet Signet", 320, Rarity.COMMON, mage.cards.i.IzzetSignet.class)); From 3b959c25b9d0ab070079996c55d717db7331acf7 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 08:23:18 -0400 Subject: [PATCH 100/133] fix verify failure --- .../common/GreatestToughnessAmongControlledCreaturesValue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java index de7e9252c28..4ec730c0f08 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/GreatestToughnessAmongControlledCreaturesValue.java @@ -39,7 +39,7 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal @Override public GreatestToughnessAmongControlledCreaturesValue copy() { - return GreatestToughnessAmongControlledCreaturesValue.ALL; + return this; } @Override From ac6c8d39ed754487c0a67d964a444dcad9c223b4 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 08:31:21 -0400 Subject: [PATCH 101/133] [TDC] Implement Within Range --- Mage.Sets/src/mage/cards/w/WithinRange.java | 80 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 81 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WithinRange.java diff --git a/Mage.Sets/src/mage/cards/w/WithinRange.java b/Mage.Sets/src/mage/cards/w/WithinRange.java new file mode 100644 index 00000000000..e75296feaa7 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WithinRange.java @@ -0,0 +1,80 @@ +package mage.cards.w; + +import mage.abilities.Ability; +import mage.abilities.common.AttacksWithCreaturesTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.combat.CombatGroup; +import mage.game.permanent.token.RedWarriorToken; +import mage.players.Player; + +import java.util.List; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WithinRange extends CardImpl { + + public WithinRange(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{B}"); + + // When this enchantment enters, create two 1/1 red Warrior creature tokens. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new RedWarriorToken()))); + + // Whenever you attack, each opponent loses life equal to the number of creatures attacking them. + this.addAbility(new AttacksWithCreaturesTriggeredAbility(new WithinRangeEffect(), 1)); + } + + private WithinRange(final WithinRange card) { + super(card); + } + + @Override + public WithinRange copy() { + return new WithinRange(this); + } +} + +class WithinRangeEffect extends OneShotEffect { + + WithinRangeEffect() { + super(Outcome.Benefit); + staticText = "each opponent loses life equal to the number of creatures attacking them"; + } + + private WithinRangeEffect(final WithinRangeEffect effect) { + super(effect); + } + + @Override + public WithinRangeEffect copy() { + return new WithinRangeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID playerId : game.getOpponents(source.getControllerId())) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + int count = game + .getCombat() + .getGroups() + .stream() + .filter(combatGroup -> player.getId().equals(combatGroup.getDefenderId())) + .map(CombatGroup::getAttackers) + .mapToInt(List::size) + .sum(); + player.loseLife(count, game, source, false); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 0c779be18bc..751a182c141 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -355,6 +355,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Whirlwind of Thought", 311, Rarity.RARE, mage.cards.w.WhirlwindOfThought.class)); cards.add(new SetCardInfo("Windbrisk Heights", 411, Rarity.RARE, mage.cards.w.WindbriskHeights.class)); cards.add(new SetCardInfo("Wingmantle Chaplain", 141, Rarity.UNCOMMON, mage.cards.w.WingmantleChaplain.class)); + cards.add(new SetCardInfo("Within Range", 32, Rarity.RARE, mage.cards.w.WithinRange.class)); cards.add(new SetCardInfo("Woe Strider", 201, Rarity.RARE, mage.cards.w.WoeStrider.class)); cards.add(new SetCardInfo("Wonder", 170, Rarity.UNCOMMON, mage.cards.w.Wonder.class)); cards.add(new SetCardInfo("Woodland Cemetery", 412, Rarity.RARE, mage.cards.w.WoodlandCemetery.class)); From 9e6987ab56565585fc9631e43c1c24311d8a2b8d Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 08:38:04 -0400 Subject: [PATCH 102/133] [TDC] Implement Tip the Scales --- Mage.Sets/src/mage/cards/t/TipTheScales.java | 84 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 85 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TipTheScales.java diff --git a/Mage.Sets/src/mage/cards/t/TipTheScales.java b/Mage.Sets/src/mage/cards/t/TipTheScales.java new file mode 100644 index 00000000000..7cc4d12c271 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TipTheScales.java @@ -0,0 +1,84 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetSacrifice; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TipTheScales extends CardImpl { + + public TipTheScales(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}"); + + // Sacrifice a creature. When you do, all creatures get -X/-X until end of turn, where X is the sacrificed creature's toughness. + this.getSpellAbility().addEffect(new TipTheScalesEffect()); + } + + private TipTheScales(final TipTheScales card) { + super(card); + } + + @Override + public TipTheScales copy() { + return new TipTheScales(this); + } +} + +class TipTheScalesEffect extends OneShotEffect { + + TipTheScalesEffect() { + super(Outcome.Benefit); + staticText = "sacrifice a creature. When you do, all creatures get " + + "-X/-X until end of turn, where X is the sacrificed creature's toughness"; + } + + private TipTheScalesEffect(final TipTheScalesEffect effect) { + super(effect); + } + + @Override + public TipTheScalesEffect copy() { + return new TipTheScalesEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetPermanent target = new TargetSacrifice(StaticFilters.FILTER_CONTROLLED_CREATURE); + if (!target.canChoose(source.getControllerId(), source, game)) { + return false; + } + player.choose(Outcome.Sacrifice, target, source, game); + Permanent permanent = game.getPermanent(target.getFirstTarget()); + if (permanent == null) { + return false; + } + int value = permanent.getToughness().getValue(); + if (!permanent.sacrifice(source, game) || value < 1) { + return false; + } + game.fireReflexiveTriggeredAbility(new ReflexiveTriggeredAbility( + new BoostAllEffect(-value, -value, Duration.EndOfTurn), false + ), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 751a182c141..1f4241a7d9c 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -326,6 +326,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Thunderbreak Regent", 241, Rarity.RARE, mage.cards.t.ThunderbreakRegent.class)); cards.add(new SetCardInfo("Time Wipe", 308, Rarity.RARE, mage.cards.t.TimeWipe.class)); cards.add(new SetCardInfo("Timeless Witness", 274, Rarity.UNCOMMON, mage.cards.t.TimelessWitness.class)); + cards.add(new SetCardInfo("Tip the Scales", 29, Rarity.RARE, mage.cards.t.TipTheScales.class)); cards.add(new SetCardInfo("Tocasia's Welcome", 135, Rarity.RARE, mage.cards.t.TocasiasWelcome.class)); cards.add(new SetCardInfo("Tower Defense", 275, Rarity.UNCOMMON, mage.cards.t.TowerDefense.class)); cards.add(new SetCardInfo("Towering Titan", 276, Rarity.MYTHIC, mage.cards.t.ToweringTitan.class)); From f3ffca26fa57ddeee68e3b5be809b96f46c7c044 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 08:47:59 -0400 Subject: [PATCH 103/133] [TDC] Implement Redoubled Stormsinger --- .../mage/cards/r/RedoubledStormsinger.java | 101 ++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + .../target/targetpointer/FixedTargets.java | 9 +- 3 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/r/RedoubledStormsinger.java diff --git a/Mage.Sets/src/mage/cards/r/RedoubledStormsinger.java b/Mage.Sets/src/mage/cards/r/RedoubledStormsinger.java new file mode 100644 index 00000000000..ead0ddedaa9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RedoubledStormsinger.java @@ -0,0 +1,101 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.abilities.effects.common.SacrificeTargetEffect; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.permanent.EnteredThisTurnPredicate; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.targetpointer.FixedTargets; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RedoubledStormsinger extends CardImpl { + + public RedoubledStormsinger(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); + + this.subtype.add(SubType.ORC); + this.subtype.add(SubType.WIZARD); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // First strike + this.addAbility(FirstStrikeAbility.getInstance()); + + // Whenever this creature attacks, for each creature token you control that entered this turn, create a tapped and attacking token that's a copy of that token. At the beginning of the next end step, sacrifice those tokens. + this.addAbility(new AttacksTriggeredAbility(new RedoubledStormsingerEffect())); + } + + private RedoubledStormsinger(final RedoubledStormsinger card) { + super(card); + } + + @Override + public RedoubledStormsinger copy() { + return new RedoubledStormsinger(this); + } +} + +class RedoubledStormsingerEffect extends OneShotEffect { + + private static final FilterPermanent filter = new FilterControlledCreaturePermanent(); + + static { + filter.add(TokenPredicate.TRUE); + filter.add(EnteredThisTurnPredicate.instance); + } + + RedoubledStormsingerEffect() { + super(Outcome.Benefit); + staticText = "for each creature token you control that entered this turn, " + + "create a tapped and attacking token that's a copy of that token. " + + "At the beginning of the next end step, sacrifice those tokens"; + } + + private RedoubledStormsingerEffect(final RedoubledStormsingerEffect effect) { + super(effect); + } + + @Override + public RedoubledStormsingerEffect copy() { + return new RedoubledStormsingerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Set addedTokens = new HashSet<>(); + for (Permanent permanent : game.getBattlefield().getActivePermanents( + filter, source.getControllerId(), source, game + )) { + CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect( + null, null, false, + 1, true, true + ).setSavedPermanent(permanent); + effect.apply(game, source); + addedTokens.addAll(effect.getAddedPermanents()); + } + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility( + new SacrificeTargetEffect().setTargetPointer(new FixedTargets(addedTokens, game)) + ), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 1f4241a7d9c..4ae44b69fe3 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -241,6 +241,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Rapid Hybridization", 162, Rarity.UNCOMMON, mage.cards.r.RapidHybridization.class)); cards.add(new SetCardInfo("Reality Shift", 163, Rarity.UNCOMMON, mage.cards.r.RealityShift.class)); cards.add(new SetCardInfo("Reassembling Skeleton", 195, Rarity.UNCOMMON, mage.cards.r.ReassemblingSkeleton.class)); + cards.add(new SetCardInfo("Redoubled Stormsinger", 37, Rarity.RARE, mage.cards.r.RedoubledStormsinger.class)); cards.add(new SetCardInfo("Reflections of Littjara", 164, Rarity.RARE, mage.cards.r.ReflectionsOfLittjara.class)); cards.add(new SetCardInfo("Release the Dogs", 127, Rarity.UNCOMMON, mage.cards.r.ReleaseTheDogs.class)); cards.add(new SetCardInfo("Reliquary Tower", 386, Rarity.UNCOMMON, mage.cards.r.ReliquaryTower.class)); diff --git a/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java b/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java index 1eb632fd1f6..1a3b3d926bd 100644 --- a/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java +++ b/Mage/src/main/java/mage/target/targetpointer/FixedTargets.java @@ -22,14 +22,7 @@ public class FixedTargets extends TargetPointerImpl { final ArrayList targets = new ArrayList<>(); - public FixedTargets(List objects, Game game) { - this(objects - .stream() - .map(o -> new MageObjectReference(o.getId(), game)) - .collect(Collectors.toList())); - } - - public FixedTargets(Set objects, Game game) { + public FixedTargets(Collection objects, Game game) { this(objects .stream() .map(o -> new MageObjectReference(o.getId(), game)) From 8787eacd41c09637c798f19419d014938b00f9a0 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 12:29:40 -0400 Subject: [PATCH 104/133] [TDC] fix Hammerhead Tyrant filter missing predicate --- Mage.Sets/src/mage/cards/h/HammerheadTyrant.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Mage.Sets/src/mage/cards/h/HammerheadTyrant.java b/Mage.Sets/src/mage/cards/h/HammerheadTyrant.java index 2ddd62db7c0..0e10903c3a9 100644 --- a/Mage.Sets/src/mage/cards/h/HammerheadTyrant.java +++ b/Mage.Sets/src/mage/cards/h/HammerheadTyrant.java @@ -34,6 +34,7 @@ public final class HammerheadTyrant extends CardImpl { static { filter.add(TargetController.OPPONENT.getControllerPredicate()); + filter.add(HammerheadTyrantPredicate.instance); } public HammerheadTyrant(UUID ownerId, CardSetInfo setInfo) { From c46e37a99708014ad32ac00f6a3d7f7c6cbaae55 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 12:36:35 -0400 Subject: [PATCH 105/133] [TDC] Implement Deceptive Frostkite --- .../src/mage/cards/d/DeceptiveFrostkite.java | 74 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 75 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DeceptiveFrostkite.java diff --git a/Mage.Sets/src/mage/cards/d/DeceptiveFrostkite.java b/Mage.Sets/src/mage/cards/d/DeceptiveFrostkite.java new file mode 100644 index 00000000000..c3090a452b3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DeceptiveFrostkite.java @@ -0,0 +1,74 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.EntersBattlefieldEffect; +import mage.abilities.effects.common.CopyPermanentEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.game.Game; +import mage.util.functions.CopyApplier; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DeceptiveFrostkite extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledCreaturePermanent("a creature you control with power 4 or greater"); + + static { + filter.add(new PowerPredicate(ComparisonType.MORE_THAN, 3)); + } + + private static final CopyApplier applier = new CopyApplier() { + @Override + public boolean apply(Game game, MageObject blueprint, Ability source, UUID copyToObjectId) { + blueprint.addSubType(SubType.DRAGON); + blueprint.getAbilities().add(FlyingAbility.getInstance()); + return true; + } + }; + + public DeceptiveFrostkite(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{U}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // You may have this creature enter as a copy of a creature you control with power 4 or greater, except it's a Dragon in addition to its other types and it has flying. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, + new EntersBattlefieldEffect( + new CopyPermanentEffect(filter, applier), "you may have this creature enter " + + "as a copy of a creature you control with power 4 or greater, " + + "except it's a Dragon in addition to its other types and it has flying", true + ) + )); + } + + private DeceptiveFrostkite(final DeceptiveFrostkite card) { + super(card); + } + + @Override + public DeceptiveFrostkite copy() { + return new DeceptiveFrostkite(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 4ae44b69fe3..352a29dee1b 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -94,6 +94,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Darkwater Catacombs", 355, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class)); cards.add(new SetCardInfo("Dauthi Voidwalker", 176, Rarity.RARE, mage.cards.d.DauthiVoidwalker.class)); cards.add(new SetCardInfo("Deadly Dispute", 177, Rarity.COMMON, mage.cards.d.DeadlyDispute.class)); + cards.add(new SetCardInfo("Deceptive Frostkite", 19, Rarity.RARE, mage.cards.d.DeceptiveFrostkite.class)); cards.add(new SetCardInfo("Deceptive Landscape", 356, Rarity.COMMON, mage.cards.d.DeceptiveLandscape.class)); cards.add(new SetCardInfo("Deep Analysis", 150, Rarity.COMMON, mage.cards.d.DeepAnalysis.class)); cards.add(new SetCardInfo("Despark", 284, Rarity.UNCOMMON, mage.cards.d.Despark.class)); From 9c5134f858272ad29664855ef7a40e97b270b6b1 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 12:44:47 -0400 Subject: [PATCH 106/133] [TDC] Implement Eshki, Temur's Roar --- .../src/mage/cards/e/EshkiTemursRoar.java | 89 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 90 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/EshkiTemursRoar.java diff --git a/Mage.Sets/src/mage/cards/e/EshkiTemursRoar.java b/Mage.Sets/src/mage/cards/e/EshkiTemursRoar.java new file mode 100644 index 00000000000..2e8c907d1ef --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EshkiTemursRoar.java @@ -0,0 +1,89 @@ +package mage.cards.e; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.SourcePermanentPowerValue; +import mage.abilities.effects.common.DamagePlayersEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.TargetController; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EshkiTemursRoar extends CardImpl { + + public EshkiTemursRoar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Whenever you cast a creature spell, put a +1/+1 counter on Eshki. If that spell's power is 4 or greater, draw a card. If that spell's power is 6 or greater, Eshki deals damage equal to Eshki's power to each opponent. + Ability ability = new SpellCastControllerTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), + StaticFilters.FILTER_SPELL_A_CREATURE, false + ); + ability.addEffect(new ConditionalOneShotEffect( + new DrawCardSourceControllerEffect(1), EshkiTemursRoarCondition.FOUR, + "If that spell's power is 4 or greater, draw a card" + )); + ability.addEffect(new ConditionalOneShotEffect( + new DamagePlayersEffect(SourcePermanentPowerValue.NOT_NEGATIVE, TargetController.OPPONENT), + EshkiTemursRoarCondition.SIX, "If that spell's power is 6 or greater, " + + "{this} deals damage equal to {this}'s power to each opponent" + )); + this.addAbility(ability); + } + + private EshkiTemursRoar(final EshkiTemursRoar card) { + super(card); + } + + @Override + public EshkiTemursRoar copy() { + return new EshkiTemursRoar(this); + } +} + +enum EshkiTemursRoarCondition implements Condition { + FOUR(4), + SIX(6); + private final int amount; + + EshkiTemursRoarCondition(int amount) { + this.amount = amount; + } + + @Override + public boolean apply(Game game, Ability source) { + return CardUtil.castStream( + source.getEffects() + .stream() + .map(effect -> effect.getValue("spellCast")), + Spell.class + ) + .findFirst() + .map(Spell::getPower) + .map(MageInt::getValue) + .orElse(0) >= amount; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 352a29dee1b..ae3bb20c1e0 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -116,6 +116,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Eliminate the Competition", 179, Rarity.RARE, mage.cards.e.EliminateTheCompetition.class)); cards.add(new SetCardInfo("Elsha, Threefold Master", 2, Rarity.MYTHIC, mage.cards.e.ElshaThreefoldMaster.class)); cards.add(new SetCardInfo("Emeria Angel", 114, Rarity.RARE, mage.cards.e.EmeriaAngel.class)); + cards.add(new SetCardInfo("Eshki, Temur's Roar", 3, Rarity.MYTHIC, mage.cards.e.EshkiTemursRoar.class)); cards.add(new SetCardInfo("Exotic Orchard", 360, Rarity.RARE, mage.cards.e.ExoticOrchard.class)); cards.add(new SetCardInfo("Expansion // Explosion", 287, Rarity.RARE, mage.cards.e.ExpansionExplosion.class)); cards.add(new SetCardInfo("Expel the Interlopers", 115, Rarity.RARE, mage.cards.e.ExpelTheInterlopers.class)); From 94ce7f8380ebebf4aab189946f0aa1dca646faa4 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 13:04:33 -0400 Subject: [PATCH 107/133] [TDC] Implement Felothar, the Steadfast --- .../mage/cards/f/FelotharTheSteadfast.java | 99 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 100 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FelotharTheSteadfast.java diff --git a/Mage.Sets/src/mage/cards/f/FelotharTheSteadfast.java b/Mage.Sets/src/mage/cards/f/FelotharTheSteadfast.java new file mode 100644 index 00000000000..449c92ac516 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FelotharTheSteadfast.java @@ -0,0 +1,99 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.combat.CanAttackAsThoughItDidntHaveDefenderAllEffect; +import mage.abilities.effects.common.ruleModifying.CombatDamageByToughnessControlledEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.Collection; +import java.util.Objects; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FelotharTheSteadfast extends CardImpl { + + public FelotharTheSteadfast(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}{B}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(0); + this.toughness = new MageInt(5); + + // Each creature you control assigns combat damage equal to its toughness rather than its power. + this.addAbility(new SimpleStaticAbility(new CombatDamageByToughnessControlledEffect())); + + // Creatures you control can attack as though they didn't have defender. + this.addAbility(new SimpleStaticAbility(new CanAttackAsThoughItDidntHaveDefenderAllEffect( + Duration.WhileOnBattlefield, StaticFilters.FILTER_CONTROLLED_CREATURES + ))); + + // {3}, {T}, Sacrifice another creature: Draw cards equal to the sacrificed creature's toughness, then discard cards equal to its power. + Ability ability = new SimpleActivatedAbility(new FelotharTheSteadfastEffect(), new GenericManaCost(3)); + ability.addCost(new TapSourceCost()); + ability.addCost(new SacrificeTargetCost(StaticFilters.FILTER_ANOTHER_CREATURE)); + this.addAbility(ability); + } + + private FelotharTheSteadfast(final FelotharTheSteadfast card) { + super(card); + } + + @Override + public FelotharTheSteadfast copy() { + return new FelotharTheSteadfast(this); + } +} + +class FelotharTheSteadfastEffect extends OneShotEffect { + + FelotharTheSteadfastEffect() { + super(Outcome.Benefit); + staticText = "draw cards equal to the sacrificed creature's toughness, then discard cards equal to its power"; + } + + private FelotharTheSteadfastEffect(final FelotharTheSteadfastEffect effect) { + super(effect); + } + + @Override + public FelotharTheSteadfastEffect copy() { + return new FelotharTheSteadfastEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Permanent permanent = CardUtil + .castStream(source.getCosts(), SacrificeTargetCost.class) + .filter(Objects::nonNull) + .map(SacrificeTargetCost::getPermanents) + .flatMap(Collection::stream) + .findFirst() + .orElse(null); + if (player == null || permanent == null) { + return false; + } + player.drawCards(permanent.getToughness().getValue(), source, game); + game.processAction(); + player.discard(permanent.getPower().getValue(), false, false, source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index ae3bb20c1e0..9237c807aff 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -126,6 +126,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Farseek", 255, Rarity.COMMON, mage.cards.f.Farseek.class)); cards.add(new SetCardInfo("Feed the Swarm", 180, Rarity.COMMON, mage.cards.f.FeedTheSwarm.class)); cards.add(new SetCardInfo("Fellwar Stone", 318, Rarity.UNCOMMON, mage.cards.f.FellwarStone.class)); + cards.add(new SetCardInfo("Felothar the Steadfast", 4, Rarity.MYTHIC, mage.cards.f.FelotharTheSteadfast.class)); cards.add(new SetCardInfo("Ferrous Lake", 361, Rarity.RARE, mage.cards.f.FerrousLake.class)); cards.add(new SetCardInfo("Fetid Heath", 362, Rarity.RARE, mage.cards.f.FetidHeath.class)); cards.add(new SetCardInfo("Fetid Pools", 363, Rarity.RARE, mage.cards.f.FetidPools.class)); From 2f57cdb0a132ff0d42a32723b17adcaebe732736 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 13:27:28 -0400 Subject: [PATCH 108/133] [TDC] Implement Reunion of the House --- .../src/mage/cards/r/ReunionOfTheHouse.java | 86 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 87 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/ReunionOfTheHouse.java diff --git a/Mage.Sets/src/mage/cards/r/ReunionOfTheHouse.java b/Mage.Sets/src/mage/cards/r/ReunionOfTheHouse.java new file mode 100644 index 00000000000..34f954a2b09 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/ReunionOfTheHouse.java @@ -0,0 +1,86 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.target.common.TargetCardInYourGraveyard; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ReunionOfTheHouse extends CardImpl { + + public ReunionOfTheHouse(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{5}{W}{W}"); + + // Return any number of target creature cards with total power 10 or less from your graveyard to the battlefield. Exile Reunion of the House. + this.getSpellAbility().addEffect(new ReturnFromGraveyardToBattlefieldTargetEffect()); + this.getSpellAbility().addTarget(new ReunionOfTheHouseTarget()); + } + + private ReunionOfTheHouse(final ReunionOfTheHouse card) { + super(card); + } + + @Override + public ReunionOfTheHouse copy() { + return new ReunionOfTheHouse(this); + } +} + +class ReunionOfTheHouseTarget extends TargetCardInYourGraveyard { + + private static final FilterCard filterStatic + = new FilterCreatureCard("creature cards with total power 10 or less from your graveyard"); + + ReunionOfTheHouseTarget() { + super(0, Integer.MAX_VALUE, filterStatic); + } + + private ReunionOfTheHouseTarget(final ReunionOfTheHouseTarget target) { + super(target); + } + + @Override + public ReunionOfTheHouseTarget copy() { + return new ReunionOfTheHouseTarget(this); + } + + @Override + public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) { + return super.canTarget(controllerId, id, source, game) + && CardUtil.checkCanTargetTotalValueLimit( + this.getTargets(), id, m -> m.getPower().getValue(), 10, game); + } + + @Override + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + return CardUtil.checkPossibleTargetsTotalValueLimit(this.getTargets(), + super.possibleTargets(sourceControllerId, source, game), + m -> m.getPower().getValue(), 10, game); + } + + @Override + public String getMessage(Game game) { + // shows selected total + int selectedValue = this.getTargets().stream() + .map(game::getObject) + .filter(Objects::nonNull) + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .sum(); + return super.getMessage(game) + " (selected total power " + selectedValue + ")"; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 9237c807aff..272086c8bb5 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -248,6 +248,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Reflections of Littjara", 164, Rarity.RARE, mage.cards.r.ReflectionsOfLittjara.class)); cards.add(new SetCardInfo("Release the Dogs", 127, Rarity.UNCOMMON, mage.cards.r.ReleaseTheDogs.class)); cards.add(new SetCardInfo("Reliquary Tower", 386, Rarity.UNCOMMON, mage.cards.r.ReliquaryTower.class)); + cards.add(new SetCardInfo("Reunion of the House", 15, Rarity.RARE, mage.cards.r.ReunionOfTheHouse.class)); cards.add(new SetCardInfo("Rhox Faithmender", 128, Rarity.RARE, mage.cards.r.RhoxFaithmender.class)); cards.add(new SetCardInfo("Rite of Replication", 165, Rarity.RARE, mage.cards.r.RiteOfReplication.class)); cards.add(new SetCardInfo("River Kelpie", 166, Rarity.RARE, mage.cards.r.RiverKelpie.class)); From 4d8028adb999ce98719c4f728a571cfa22d94741 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 10 Apr 2025 13:39:21 -0400 Subject: [PATCH 109/133] [TDM] Implement harmonize mechanic (#13475) * add initial ability * remove skip list * update harmonize ability * add reminder text * remove skip list * add harmonize test * update tests * create base class for flashback-like abilities --- .../src/mage/sets/TarkirDragonstorm.java | 6 - .../abilities/keywords/HarmonizeTest.java | 55 +++++ .../main/java/mage/abilities/AbilityImpl.java | 1 + .../keyword/CastFromGraveyardAbility.java | 197 ++++++++++++++++++ .../abilities/keyword/FlashbackAbility.java | 180 +--------------- .../abilities/keyword/HarmonizeAbility.java | 162 +++++++++++++- .../mage/constants/SpellAbilityCastMode.java | 2 + 7 files changed, 415 insertions(+), 188 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HarmonizeTest.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 2254f89e5f6..72e2735a641 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -4,15 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class TarkirDragonstorm extends ExpansionSet { - private static final List unfinished = Arrays.asList("Channeled Dragonfire", "Glacial Dragonhunt", "Mammoth Bellow", "Nature's Rhythm", "Roamer's Routine", "Songcrafter Mage", "Synchronized Charge", "Unending Whisper", "Ureni's Rebuff", "Wild Ride", "Winternight Stories"); private static final TarkirDragonstorm instance = new TarkirDragonstorm(); public static TarkirDragonstorm getInstance() { @@ -297,7 +293,5 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Yathan Tombguard", 100, Rarity.UNCOMMON, mage.cards.y.YathanTombguard.class)); cards.add(new SetCardInfo("Zurgo's Vanguard", 133, Rarity.UNCOMMON, mage.cards.z.ZurgosVanguard.class)); cards.add(new SetCardInfo("Zurgo, Thunder's Decree", 237, Rarity.RARE, mage.cards.z.ZurgoThundersDecree.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HarmonizeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HarmonizeTest.java new file mode 100644 index 00000000000..5508f1e612c --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/HarmonizeTest.java @@ -0,0 +1,55 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class HarmonizeTest extends CardTestPlayerBase { + + private static final String bear = "Grizzly Bears"; + private static final String dragonfire = "Channeled Dragonfire"; + + @Test + public void testNoTap() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 7); + addCard(Zone.BATTLEFIELD, playerA, bear); + addCard(Zone.GRAVEYARD, playerA, dragonfire); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Harmonize", playerB); + setChoice(playerA, false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTapped(bear, false); + assertLife(playerB, 20 - 2); + assertExileCount(playerA, dragonfire, 1); + assertTappedCount("Mountain", true, 7); + } + + @Test + public void testTap() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, bear); + addCard(Zone.GRAVEYARD, playerA, dragonfire); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Harmonize", playerB); + setChoice(playerA, true); + setChoice(playerA, bear); + setChoice(playerA, bear); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTapped(bear, true); + assertLife(playerB, 20 - 2); + assertExileCount(playerA, dragonfire, 1); + assertTappedCount("Mountain", true, 5); + } +} diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 95d13c2d59e..2fd281c59a8 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -495,6 +495,7 @@ public abstract class AbilityImpl implements Ability { // A player can't apply two alternative methods of casting or two alternative costs to a single spell. switch (((SpellAbility) this).getSpellAbilityCastMode()) { case FLASHBACK: + case HARMONIZE: case MADNESS: case TRANSFORMED: case DISTURB: diff --git a/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java b/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java new file mode 100644 index 00000000000..73de96cf5e7 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/CastFromGraveyardAbility.java @@ -0,0 +1,197 @@ +package mage.abilities.keyword; + +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.cards.Card; +import mage.cards.ModalDoubleFacedCard; +import mage.cards.SplitCard; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * Base class for Flashback and Harmonize and any future ability which works similarly + * + * @author TheElk801 + */ +public abstract class CastFromGraveyardAbility extends SpellAbility { + + protected String abilityName; + private SpellAbility spellAbilityToResolve; + + protected CastFromGraveyardAbility(Card card, Cost cost, SpellAbilityCastMode spellAbilityCastMode) { + super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, spellAbilityCastMode); + this.setAdditionalCostsRuleVisible(false); + this.name = spellAbilityCastMode + " " + cost.getText(); + this.addCost(cost); + this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT; + } + + protected CastFromGraveyardAbility(final CastFromGraveyardAbility ability) { + super(ability); + this.abilityName = ability.abilityName; + this.spellAbilityToResolve = ability.spellAbilityToResolve; + } + + @Override + public ActivationStatus canActivate(UUID playerId, Game game) { + // flashback ability dynamicly added to all card's parts (split cards) + if (!super.canActivate(playerId, game).canActivate()) { + return ActivationStatus.getFalse(); + } + Card card = game.getCard(getSourceId()); + if (card == null) { + return ActivationStatus.getFalse(); + } + // Card must be in the graveyard zone + if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) { + return ActivationStatus.getFalse(); + } + // Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision) + if (card.getManaCost().isEmpty()) { + return ActivationStatus.getFalse(); + } + // CastFromGraveyard can never cast a split card by Fuse, because Fuse only works from hand + // https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/ + if (card instanceof SplitCard) { + if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { + return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); + } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { + return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); + } + } else if (card instanceof ModalDoubleFacedCard) { + if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { + return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); + } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { + return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); + } + } + return card.getSpellAbility().canActivate(playerId, game); + } + + @Override + public SpellAbility getSpellAbilityToResolve(Game game) { + Card card = game.getCard(getSourceId()); + if (card == null || spellAbilityToResolve != null) { + return spellAbilityToResolve; + } + SpellAbility spellAbilityCopy; + if (card instanceof SplitCard) { + if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy(); + } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy(); + } else { + spellAbilityCopy = null; + } + } else if (card instanceof ModalDoubleFacedCard) { + if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy(); + } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { + spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy(); + } else { + spellAbilityCopy = null; + } + } else { + spellAbilityCopy = card.getSpellAbility().copy(); + } + if (spellAbilityCopy == null) { + return null; + } + spellAbilityCopy.setId(this.getId()); + spellAbilityCopy.clearManaCosts(); + spellAbilityCopy.clearManaCostsToPay(); + spellAbilityCopy.addCost(this.getCosts().copy()); + spellAbilityCopy.addCost(this.getManaCosts().copy()); + spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode()); + spellAbilityToResolve = spellAbilityCopy; + ContinuousEffect effect = new CastFromGraveyardReplacementEffect(); + effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId()))); + game.addEffect(effect, this); + return spellAbilityToResolve; + } + + @Override + public Costs getCosts() { + if (spellAbilityToResolve == null) { + return super.getCosts(); + } + return spellAbilityToResolve.getCosts(); + } + + @Override + public String getRule(boolean all) { + return this.getRule(); + } + + /** + * Used for split card in PlayerImpl method: + * getOtherUseableActivatedAbilities + * + * @param abilityName + */ + public CastFromGraveyardAbility setAbilityName(String abilityName) { + this.abilityName = abilityName; + return this; + } +} + +class CastFromGraveyardReplacementEffect extends ReplacementEffectImpl { + + public CastFromGraveyardReplacementEffect() { + super(Duration.OneUse, Outcome.Exile); + } + + protected CastFromGraveyardReplacementEffect(final CastFromGraveyardReplacementEffect effect) { + super(effect); + } + + @Override + public CastFromGraveyardReplacementEffect copy() { + return new CastFromGraveyardReplacementEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Card card = game.getCard(event.getTargetId()); + if (card == null) { + return false; + } + discard(); + return controller.moveCards( + card, Zone.EXILED, source, game, false, + false, false, event.getAppliedEffects() + ); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards + if (!cardId.equals(event.getTargetId()) + || ((ZoneChangeEvent) event).getFromZone() != Zone.STACK + || ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) { + return false; + } + int zcc = game.getState().getZoneChangeCounter(cardId); + return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc; + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java index e88cf333684..4b7dae1d709 100644 --- a/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/FlashbackAbility.java @@ -1,23 +1,8 @@ package mage.abilities.keyword; -import mage.abilities.Ability; -import mage.abilities.SpellAbility; import mage.abilities.costs.Cost; -import mage.abilities.costs.Costs; -import mage.abilities.effects.ContinuousEffect; -import mage.abilities.effects.ReplacementEffectImpl; import mage.cards.Card; -import mage.cards.ModalDoubleFacedCard; -import mage.cards.SplitCard; -import mage.constants.*; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; -import mage.players.Player; -import mage.target.targetpointer.FixedTarget; -import mage.util.CardUtil; - -import java.util.UUID; +import mage.constants.SpellAbilityCastMode; /** * 702.32. Flashback @@ -33,106 +18,14 @@ import java.util.UUID; * * @author nantuko */ -public class FlashbackAbility extends SpellAbility { - - private String abilityName; - private SpellAbility spellAbilityToResolve; +public class FlashbackAbility extends CastFromGraveyardAbility { public FlashbackAbility(Card card, Cost cost) { - super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.FLASHBACK); - this.setAdditionalCostsRuleVisible(false); - this.name = "Flashback " + cost.getText(); - this.addCost(cost); - this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT; + super(card, cost, SpellAbilityCastMode.FLASHBACK); } protected FlashbackAbility(final FlashbackAbility ability) { super(ability); - this.spellAbilityType = ability.spellAbilityType; - this.abilityName = ability.abilityName; - this.spellAbilityToResolve = ability.spellAbilityToResolve; - } - - @Override - public ActivationStatus canActivate(UUID playerId, Game game) { - // flashback ability dynamicly added to all card's parts (split cards) - if (super.canActivate(playerId, game).canActivate()) { - Card card = game.getCard(getSourceId()); - if (card != null) { - // Card must be in the graveyard zone - if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) { - return ActivationStatus.getFalse(); - } - // Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision) - if (card.getManaCost().isEmpty()) { - return ActivationStatus.getFalse(); - } - // Flashback can never cast a split card by Fuse, because Fuse only works from hand - // https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/ - if (card instanceof SplitCard) { - if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { - return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); - } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { - return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); - } - } else if (card instanceof ModalDoubleFacedCard) { - if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { - return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game); - } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { - return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game); - } - } - return card.getSpellAbility().canActivate(playerId, game); - } - } - return ActivationStatus.getFalse(); - } - - @Override - public SpellAbility getSpellAbilityToResolve(Game game) { - Card card = game.getCard(getSourceId()); - if (card != null) { - if (spellAbilityToResolve == null) { - SpellAbility spellAbilityCopy = null; - if (card instanceof SplitCard) { - if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy(); - } else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy(); - } - } else if (card instanceof ModalDoubleFacedCard) { - if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy(); - } else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) { - spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy(); - } - } else { - spellAbilityCopy = card.getSpellAbility().copy(); - } - if (spellAbilityCopy == null) { - return null; - } - spellAbilityCopy.setId(this.getId()); - spellAbilityCopy.clearManaCosts(); - spellAbilityCopy.clearManaCostsToPay(); - spellAbilityCopy.addCost(this.getCosts().copy()); - spellAbilityCopy.addCost(this.getManaCosts().copy()); - spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode()); - spellAbilityToResolve = spellAbilityCopy; - ContinuousEffect effect = new FlashbackReplacementEffect(); - effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId()))); - game.addEffect(effect, this); - } - } - return spellAbilityToResolve; - } - - @Override - public Costs getCosts() { - if (spellAbilityToResolve == null) { - return super.getCosts(); - } - return spellAbilityToResolve.getCosts(); } @Override @@ -140,11 +33,6 @@ public class FlashbackAbility extends SpellAbility { return new FlashbackAbility(this); } - @Override - public String getRule(boolean all) { - return this.getRule(); - } - @Override public String getRule() { StringBuilder sbRule = new StringBuilder("Flashback"); @@ -170,66 +58,4 @@ public class FlashbackAbility extends SpellAbility { sbRule.append(" (You may cast this card from your graveyard for its flashback cost. Then exile it.)"); return sbRule.toString(); } - - /** - * Used for split card in PlayerImpl method: - * getOtherUseableActivatedAbilities - * - * @param abilityName - */ - public FlashbackAbility setAbilityName(String abilityName) { - this.abilityName = abilityName; - return this; - } - -} - -class FlashbackReplacementEffect extends ReplacementEffectImpl { - - public FlashbackReplacementEffect() { - super(Duration.OneUse, Outcome.Exile); - staticText = "(If the flashback cost was paid, exile this card instead of putting it anywhere else any time it would leave the stack)"; - } - - protected FlashbackReplacementEffect(final FlashbackReplacementEffect effect) { - super(effect); - } - - @Override - public FlashbackReplacementEffect copy() { - return new FlashbackReplacementEffect(this); - } - - @Override - public boolean replaceEvent(GameEvent event, Ability source, Game game) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Card card = game.getCard(event.getTargetId()); - if (card != null) { - discard(); - return controller.moveCards( - card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects()); - } - } - return false; - } - - @Override - public boolean checksEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ZONE_CHANGE; - } - - @Override - public boolean applies(GameEvent event, Ability source, Game game) { - UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards - if (cardId.equals(event.getTargetId()) - && ((ZoneChangeEvent) event).getFromZone() == Zone.STACK - && ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) { - - int zcc = game.getState().getZoneChangeCounter(cardId); - return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc; - - } - return false; - } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java b/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java index 92f6310401c..d18de35de8c 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HarmonizeAbility.java @@ -1,19 +1,43 @@ package mage.abilities.keyword; +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.VariableCostImpl; +import mage.abilities.costs.VariableCostType; +import mage.abilities.costs.common.TapTargetCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.cost.CostModificationEffectImpl; import mage.cards.Card; -import mage.constants.Zone; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.permanent.PermanentIdPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledPermanent; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.UUID; /** - * TODO: Implement this - * * @author TheElk801 */ -public class HarmonizeAbility extends SpellAbility { +public class HarmonizeAbility extends CastFromGraveyardAbility { + + private String abilityName; + private SpellAbility spellAbilityToResolve; public HarmonizeAbility(Card card, String manaString) { - super(new ManaCostsImpl<>(manaString), card.getName(), Zone.GRAVEYARD); + super(card, new ManaCostsImpl<>(manaString), SpellAbilityCastMode.HARMONIZE); + this.addCost(new HarmonizeCost()); + this.addSubAbility(new SimpleStaticAbility(Zone.ALL, new HarmonizeCostReductionEffect()).setRuleVisible(false)); } private HarmonizeAbility(final HarmonizeAbility ability) { @@ -24,4 +48,132 @@ public class HarmonizeAbility extends SpellAbility { public HarmonizeAbility copy() { return new HarmonizeAbility(this); } + + @Override + public String getRule() { + return name + " (You may cast this card from your graveyard for its harmonize cost. " + + "You may tap a creature you control to reduce that cost by {X}, " + + "where X is its power. Then exile this spell.)"; + } +} + +class HarmonizeCostReductionEffect extends CostModificationEffectImpl { + + HarmonizeCostReductionEffect() { + super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST); + } + + private HarmonizeCostReductionEffect(final HarmonizeCostReductionEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source, Ability abilityToModify) { + SpellAbility spellAbility = (SpellAbility) abilityToModify; + int power; + if (game.inCheckPlayableState()) { + power = game + .getBattlefield() + .getActivePermanents( + StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, + source.getControllerId(), source, game + ).stream() + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .max() + .orElse(0); + } else { + power = CardUtil + .castStream(spellAbility.getCosts().stream(), HarmonizeCost.class) + .map(HarmonizeCost::getChosenCreature) + .map(game::getPermanent) + .filter(Objects::nonNull) + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .map(x -> Math.max(x, 0)) + .sum(); + } + if (power > 0) { + CardUtil.adjustCost(spellAbility, power); + } + return true; + } + + @Override + public boolean applies(Ability abilityToModify, Ability source, Game game) { + return abilityToModify instanceof SpellAbility + && abilityToModify.getSourceId().equals(source.getSourceId()); + } + + @Override + public HarmonizeCostReductionEffect copy() { + return new HarmonizeCostReductionEffect(this); + } +} + +class HarmonizeCost extends VariableCostImpl { + + private UUID chosenCreature = null; + + HarmonizeCost() { + super(VariableCostType.ADDITIONAL, "", ""); + } + + private HarmonizeCost(final HarmonizeCost cost) { + super(cost); + this.chosenCreature = cost.chosenCreature; + } + + @Override + public HarmonizeCost copy() { + return new HarmonizeCost(this); + } + + @Override + public void clearPaid() { + super.clearPaid(); + chosenCreature = null; + } + + @Override + public int getMaxValue(Ability source, Game game) { + return game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1) ? 1 : 0; + } + + @Override + public int announceXValue(Ability source, Game game) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null || !game.getBattlefield().contains( + StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1 + ) || !player.chooseUse( + Outcome.Benefit, "Tap an untapped creature you control for harmonize?", source, game + )) { + return 0; + } + TargetPermanent target = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE); + target.withNotTarget(true); + target.withChooseHint("for harmonize"); + player.choose(Outcome.PlayForFree, target, source, game); + Permanent permanent = game.getPermanent(target.getFirstTarget()); + if (permanent == null) { + return 0; + } + chosenCreature = permanent.getId(); + return 1; + } + + private FilterControlledPermanent makeFilter() { + FilterControlledPermanent filter = new FilterControlledPermanent("tap the chosen creature"); + filter.add(new PermanentIdPredicate(chosenCreature)); + return filter; + } + + @Override + public Cost getFixedCostsFromAnnouncedValue(int xValue) { + return new TapTargetCost(new TargetControlledPermanent(xValue, xValue, makeFilter(), true)); + } + + public UUID getChosenCreature() { + return chosenCreature; + } } diff --git a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java index a57e28325f6..6eedb18741f 100644 --- a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java +++ b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java @@ -15,6 +15,7 @@ public enum SpellAbilityCastMode { NORMAL("Normal"), MADNESS("Madness"), FLASHBACK("Flashback"), + HARMONIZE("Harmonize"), BESTOW("Bestow"), PROTOTYPE("Prototype"), MORPH("Morph", false, true), // and megamorph @@ -91,6 +92,7 @@ public enum SpellAbilityCastMode { case NORMAL: case MADNESS: case FLASHBACK: + case HARMONIZE: case DISTURB: case PLOT: case MORE_THAN_MEETS_THE_EYE: From 0e44fcbcd4fd0347b955ea730f4ac8714179c68f Mon Sep 17 00:00:00 2001 From: androosss <101566943+androosss@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:19:35 +0200 Subject: [PATCH 110/133] [TDM] Hollowmurk Siege (#13519) * implemented hollowmurk siege * Update to work with new anchorword card implementation * Update rule text --- .../src/mage/cards/h/HollowmurkSiege.java | 94 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 5 + 2 files changed, 99 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HollowmurkSiege.java diff --git a/Mage.Sets/src/mage/cards/h/HollowmurkSiege.java b/Mage.Sets/src/mage/cards/h/HollowmurkSiege.java new file mode 100644 index 00000000000..30836b1ff79 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HollowmurkSiege.java @@ -0,0 +1,94 @@ +package mage.cards.h; + +import java.util.Optional; +import java.util.UUID; + +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.AttacksWithCreaturesTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.continuous.GainAnchorWordAbilitySourceEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ModeChoice; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; + +/** + * + * @author androosss + */ +public final class HollowmurkSiege extends CardImpl { + + public HollowmurkSiege(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{B}{G}"); + + // As this enchantment enters, choose Sultai or Abzan. + this.addAbility(new AsEntersBattlefieldAbility(new ChooseModeEffect(ModeChoice.SULTAI, ModeChoice.ABZAN))); + + // * Sultai -- Whenever a counter is put on a creature you control, draw a card. + // This ability triggers only once each turn. + this.addAbility(new SimpleStaticAbility( + new GainAnchorWordAbilitySourceEffect(new HollowmurkSiegeSultaiTriggeredAbility(), ModeChoice.SULTAI))); + + // * Abzan -- Whenever you attack, put a +1/+1 counter on target attacking + // creature. It gains menace until end of turn. + TriggeredAbility abzanAbility = new AttacksWithCreaturesTriggeredAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance(1)), 1); + abzanAbility.addEffect( + new GainAbilityTargetEffect(new MenaceAbility(false)).setText("It gains menace until end of turn")); + abzanAbility.addTarget(new TargetPermanent(StaticFilters.FILTER_ATTACKING_CREATURE)); + this.addAbility(new SimpleStaticAbility(new GainAnchorWordAbilitySourceEffect(abzanAbility, ModeChoice.ABZAN))); + } + + private HollowmurkSiege(final HollowmurkSiege card) { + super(card); + } + + @Override + public HollowmurkSiege copy() { + return new HollowmurkSiege(this); + } +} + +class HollowmurkSiegeSultaiTriggeredAbility extends TriggeredAbilityImpl { + + HollowmurkSiegeSultaiTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1)); + setTriggerPhrase("Whenever a counter is put on a creature you control, "); + setTriggersLimitEachTurn(1); + } + + private HollowmurkSiegeSultaiTriggeredAbility(final HollowmurkSiegeSultaiTriggeredAbility ability) { + super(ability); + } + + @Override + public HollowmurkSiegeSultaiTriggeredAbility copy() { + return new HollowmurkSiegeSultaiTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.COUNTER_ADDED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Permanent permanent = Optional.ofNullable(game.getPermanentOrLKIBattlefield(event.getTargetId())).orElse(game.getPermanentEntering(event.getTargetId())); + + return permanent != null && permanent.isCreature(game) && permanent.isControlledBy(getControllerId()); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 72e2735a641..fd5d4d498bc 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -1,5 +1,8 @@ package mage.sets; +import java.util.Arrays; +import java.util.List; + import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; @@ -125,6 +128,8 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Herd Heirloom", 144, Rarity.RARE, mage.cards.h.HerdHeirloom.class)); cards.add(new SetCardInfo("Heritage Reclamation", 145, Rarity.COMMON, mage.cards.h.HeritageReclamation.class)); cards.add(new SetCardInfo("Highspire Bell-Ringer", 47, Rarity.COMMON, mage.cards.h.HighspireBellRinger.class)); + cards.add(new SetCardInfo("Hollowmurk Siege", 192, Rarity.RARE, mage.cards.h.HollowmurkSiege.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Hollowmurk Siege", 387, Rarity.RARE, mage.cards.h.HollowmurkSiege.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Host of the Hereafter", 193, Rarity.UNCOMMON, mage.cards.h.HostOfTheHereafter.class)); cards.add(new SetCardInfo("Humbling Elder", 48, Rarity.COMMON, mage.cards.h.HumblingElder.class)); cards.add(new SetCardInfo("Iceridge Serpent", 49, Rarity.COMMON, mage.cards.i.IceridgeSerpent.class)); From 5f7bb6ed9f4ad994da256dd3c7eb4a5d6a6d0dbb Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 15:51:43 -0400 Subject: [PATCH 111/133] [TDC] Implement Zenith Festival --- .../src/mage/cards/z/ZenithFestival.java | 38 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 39 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/z/ZenithFestival.java diff --git a/Mage.Sets/src/mage/cards/z/ZenithFestival.java b/Mage.Sets/src/mage/cards/z/ZenithFestival.java new file mode 100644 index 00000000000..f37ef6e10e0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/z/ZenithFestival.java @@ -0,0 +1,38 @@ +package mage.cards.z; + +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.effects.common.ExileTopXMayPlayUntilEffect; +import mage.abilities.keyword.HarmonizeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ZenithFestival extends CardImpl { + + public ZenithFestival(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{R}{R}"); + + // Exile the top X cards of your library. You may play them until the end of your next turn. + this.getSpellAbility().addEffect(new ExileTopXMayPlayUntilEffect( + GetXValue.instance, false, Duration.UntilEndOfYourNextTurn + )); + + // Harmonize {X}{R}{R} + this.addAbility(new HarmonizeAbility(this, "{X}{R}{R}")); + } + + private ZenithFestival(final ZenithFestival card) { + super(card); + } + + @Override + public ZenithFestival copy() { + return new ZenithFestival(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 272086c8bb5..9f08ccccef7 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -368,6 +368,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Yahenni, Undying Partisan", 202, Rarity.RARE, mage.cards.y.YahenniUndyingPartisan.class)); cards.add(new SetCardInfo("Yavimaya Coast", 413, Rarity.RARE, mage.cards.y.YavimayaCoast.class)); cards.add(new SetCardInfo("Young Pyromancer", 95, Rarity.UNCOMMON, mage.cards.y.YoungPyromancer.class)); + cards.add(new SetCardInfo("Zenith Festival", 41, Rarity.RARE, mage.cards.z.ZenithFestival.class)); cards.add(new SetCardInfo("Zetalpa, Primal Dawn", 142, Rarity.RARE, mage.cards.z.ZetalpaPrimalDawn.class)); } } From 61c771a905a0406441d6211aa5faedca5aa1beb0 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 15:51:57 -0400 Subject: [PATCH 112/133] [TDC] Implement Welcome the Dead --- .../src/mage/cards/w/WelcomeTheDead.java | 126 ++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 127 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WelcomeTheDead.java diff --git a/Mage.Sets/src/mage/cards/w/WelcomeTheDead.java b/Mage.Sets/src/mage/cards/w/WelcomeTheDead.java new file mode 100644 index 00000000000..c813928f5b3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WelcomeTheDead.java @@ -0,0 +1,126 @@ +package mage.cards.w; + +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.FlashbackAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.WatcherScope; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.token.ZombieDruidToken; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WelcomeTheDead extends CardImpl { + + public WelcomeTheDead(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}"); + + // Draw two cards, then discard a card and you lose 2 life. Create X tapped 2/2 black Zombie Druid creature tokens, where X is the number of cards that were put into your graveyard from your hand or library this turn. + this.getSpellAbility().addEffect(new DrawDiscardControllerEffect(2, 1)); + this.getSpellAbility().addEffect(new LoseLifeSourceControllerEffect(2).concatBy("and")); + this.getSpellAbility().addEffect(new CreateTokenEffect( + new ZombieDruidToken(), WelcomeTheDeadValue.instance, true, false + )); + this.getSpellAbility().addHint(WelcomeTheDeadValue.getHint()); + this.getSpellAbility().addWatcher(new WelcomeTheDeadWatcher()); + + // Flashback {5}{B} + this.addAbility(new FlashbackAbility(this, new ManaCostsImpl<>("{5}{B}"))); + } + + private WelcomeTheDead(final WelcomeTheDead card) { + super(card); + } + + @Override + public WelcomeTheDead copy() { + return new WelcomeTheDead(this); + } +} + +enum WelcomeTheDeadValue implements DynamicValue { + instance; + private static final Hint hint = new ValueHint("Cards put into your graveyard from your hand or library", instance); + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return WelcomeTheDeadWatcher.getValue(game, sourceAbility); + } + + @Override + public WelcomeTheDeadValue copy() { + return this; + } + + @Override + public String getMessage() { + return "the number of cards that were put into your graveyard from your hand or library this turn"; + } + + @Override + public String toString() { + return "X"; + } + + public static Hint getHint() { + return hint; + } +} + +class WelcomeTheDeadWatcher extends Watcher { + + private final Map map = new HashMap<>(); + + WelcomeTheDeadWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.ZONE_CHANGE) { + return; + } + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + if (!Zone.GRAVEYARD.match(zEvent.getToZone())) { + return; + } + switch (zEvent.getFromZone()) { + case HAND: + case LIBRARY: + map.compute(zEvent.getPlayerId(), CardUtil::setOrIncrementValue); + } + } + + @Override + public void reset() { + super.reset(); + map.clear(); + } + + static int getValue(Game game, Ability source) { + return game + .getState() + .getWatcher(WelcomeTheDeadWatcher.class) + .map + .getOrDefault(source.getControllerId(), 0); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 9f08ccccef7..b297edccf64 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -357,6 +357,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Wall of Roots", 278, Rarity.COMMON, mage.cards.w.WallOfRoots.class)); cards.add(new SetCardInfo("Wayfarer's Bauble", 335, Rarity.COMMON, mage.cards.w.WayfarersBauble.class)); cards.add(new SetCardInfo("Weathered Sentinels", 336, Rarity.RARE, mage.cards.w.WeatheredSentinels.class)); + cards.add(new SetCardInfo("Welcome the Dead", 30, Rarity.RARE, mage.cards.w.WelcomeTheDead.class)); cards.add(new SetCardInfo("Welcoming Vampire", 140, Rarity.RARE, mage.cards.w.WelcomingVampire.class)); cards.add(new SetCardInfo("Whirlwind of Thought", 311, Rarity.RARE, mage.cards.w.WhirlwindOfThought.class)); cards.add(new SetCardInfo("Windbrisk Heights", 411, Rarity.RARE, mage.cards.w.WindbriskHeights.class)); From 5ae4d7c0bc5f5b3be4a1cde91465ade04b0eae46 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 15:52:11 -0400 Subject: [PATCH 113/133] [TDM] Implement Severance Priest --- .../src/mage/cards/s/SeverancePriest.java | 98 +++++++++++++++++++ .../src/mage/cards/s/SkyclaveApparition.java | 66 ++----------- .../src/mage/sets/TarkirDragonstorm.java | 4 +- ...ateXXTokenExiledEffectManaValueEffect.java | 74 ++++++++++++++ 4 files changed, 181 insertions(+), 61 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/SeverancePriest.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/CreateXXTokenExiledEffectManaValueEffect.java diff --git a/Mage.Sets/src/mage/cards/s/SeverancePriest.java b/Mage.Sets/src/mage/cards/s/SeverancePriest.java new file mode 100644 index 00000000000..ff17d0efc7b --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SeverancePriest.java @@ -0,0 +1,98 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.LeavesBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateXXTokenExiledEffectManaValueEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.token.SpiritXXToken; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInHand; +import mage.target.common.TargetOpponent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SeverancePriest extends CardImpl { + + public SeverancePriest(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}{B}{G}"); + + this.subtype.add(SubType.DJINN); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // When this creature enters, target opponent reveals their hand. You may choose a nonland card from it. If you do, exile that card. + Ability ability = new EntersBattlefieldTriggeredAbility(new SeverancePriestEffect()); + ability.addTarget(new TargetOpponent()); + this.addAbility(ability); + + // When this creature leaves the battlefield, the exiled card's owner creates an X/X white Spirit creature token, where X is the mana value of the exiled card. + this.addAbility(new LeavesBattlefieldTriggeredAbility( + new CreateXXTokenExiledEffectManaValueEffect(SpiritXXToken::new, "white Spirit") + )); + } + + private SeverancePriest(final SeverancePriest card) { + super(card); + } + + @Override + public SeverancePriest copy() { + return new SeverancePriest(this); + } +} + +class SeverancePriestEffect extends OneShotEffect { + + SeverancePriestEffect() { + super(Outcome.Benefit); + staticText = "target opponent reveals their hand. You may choose " + + "a nonland card from it. If you do, exile that card"; + } + + private SeverancePriestEffect(final SeverancePriestEffect effect) { + super(effect); + } + + @Override + public SeverancePriestEffect copy() { + return new SeverancePriestEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (controller == null || opponent == null || opponent.getHand().isEmpty()) { + return false; + } + opponent.revealCards(source, opponent.getHand(), game); + TargetCard target = new TargetCardInHand(0, 1, StaticFilters.FILTER_CARD_NON_LAND); + controller.choose(Outcome.Discard, opponent.getHand(), target, source, game); + Card card = game.getCard(target.getFirstTarget()); + return card != null && controller.moveCardsToExile( + card, source, game, true, + CardUtil.getExileZoneId(game, source), + CardUtil.getSourceName(game, source) + ); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SkyclaveApparition.java b/Mage.Sets/src/mage/cards/s/SkyclaveApparition.java index a4997e653f1..5b17917216b 100644 --- a/Mage.Sets/src/mage/cards/s/SkyclaveApparition.java +++ b/Mage.Sets/src/mage/cards/s/SkyclaveApparition.java @@ -1,29 +1,24 @@ package mage.cards.s; import mage.MageInt; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.LeavesBattlefieldTriggeredAbility; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateXXTokenExiledEffectManaValueEffect; import mage.abilities.effects.common.ExileTargetForSourceEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.constants.TargetController; import mage.filter.FilterPermanent; import mage.filter.common.FilterNonlandPermanent; import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.filter.predicate.permanent.TokenPredicate; -import mage.game.ExileZone; -import mage.game.Game; -import mage.game.permanent.Permanent; import mage.game.permanent.token.CustomIllusionToken; import mage.target.TargetPermanent; -import mage.util.CardUtil; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; import java.util.UUID; /** @@ -55,7 +50,9 @@ public final class SkyclaveApparition extends CardImpl { this.addAbility(ability); // When Skyclave Apparition leaves the battlefield, the exiled card's owner creates an X/X blue Illusion creature token, where X is the converted mana cost of the exiled card. - this.addAbility(new LeavesBattlefieldTriggeredAbility(new SkyclaveApparitionEffect(), false)); + this.addAbility(new LeavesBattlefieldTriggeredAbility( + new CreateXXTokenExiledEffectManaValueEffect(CustomIllusionToken::new, "blue Illusion") + )); } private SkyclaveApparition(final SkyclaveApparition card) { @@ -67,50 +64,3 @@ public final class SkyclaveApparition extends CardImpl { return new SkyclaveApparition(this); } } - -class SkyclaveApparitionEffect extends OneShotEffect { - - SkyclaveApparitionEffect() { - super(Outcome.Benefit); - staticText = "the exiled card's owner creates an X/X blue Illusion creature token, " + - "where X is the mana value of the exiled card"; - } - - private SkyclaveApparitionEffect(final SkyclaveApparitionEffect effect) { - super(effect); - } - - @Override - public SkyclaveApparitionEffect copy() { - return new SkyclaveApparitionEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Permanent permanentLeftBattlefield = (Permanent) getValue("permanentLeftBattlefield"); - ExileZone exile = game.getExile().getExileZone( - CardUtil.getExileZoneId(game, source.getSourceId(), permanentLeftBattlefield.getZoneChangeCounter(game)) - ); - if (exile == null || exile.isEmpty()) { - return false; - } - // From ZNR Release Notes: - // https://magic.wizards.com/en/articles/archive/feature/zendikar-rising-release-notes-2020-09-10 - // If Skyclave Apparition's first ability exiled more than one card owned by a single player, - // that player creates a token with power and toughness equal to the sum of those cards' converted mana costs. - // If the first ability exiled cards owned by more than one player, each of those players creates a token - // with power and toughness equal to the sum of the converted mana costs of all cards exiled by the first ability. - Set owners = new HashSet<>(); - int totalCMC = exile - .getCards(game) - .stream() - .filter(Objects::nonNull) - .map(card -> owners.add(card.getOwnerId()) ? card : card) - .mapToInt(MageObject::getManaValue) - .sum(); - for (UUID playerId : owners) { - new CustomIllusionToken(totalCMC).putOntoBattlefield(1, game, source, playerId); - } - return true; - } -} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index fd5d4d498bc..28c91b3358f 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -1,8 +1,5 @@ package mage.sets; -import java.util.Arrays; -import java.util.List; - import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; @@ -222,6 +219,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Scavenger Regent", 90, Rarity.RARE, mage.cards.s.ScavengerRegent.class)); cards.add(new SetCardInfo("Scoured Barrens", 267, Rarity.COMMON, mage.cards.s.ScouredBarrens.class)); cards.add(new SetCardInfo("Seize Opportunity", 119, Rarity.COMMON, mage.cards.s.SeizeOpportunity.class)); + cards.add(new SetCardInfo("Severance Priest", 222, Rarity.RARE, mage.cards.s.SeverancePriest.class)); cards.add(new SetCardInfo("Shiko, Paragon of the Way", 223, Rarity.MYTHIC, mage.cards.s.ShikoParagonOfTheWay.class)); cards.add(new SetCardInfo("Shock Brigade", 120, Rarity.COMMON, mage.cards.s.ShockBrigade.class)); cards.add(new SetCardInfo("Shocking Sharpshooter", 121, Rarity.UNCOMMON, mage.cards.s.ShockingSharpshooter.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateXXTokenExiledEffectManaValueEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateXXTokenExiledEffectManaValueEffect.java new file mode 100644 index 00000000000..3b58617a27c --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateXXTokenExiledEffectManaValueEffect.java @@ -0,0 +1,74 @@ +package mage.abilities.effects.common; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.constants.Outcome; +import mage.game.ExileZone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.Token; +import mage.util.CardUtil; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; + +/** + * @author TheElk801 + */ +public class CreateXXTokenExiledEffectManaValueEffect extends OneShotEffect { + + private final Function tokenMaker; + + public CreateXXTokenExiledEffectManaValueEffect(Function tokenMaker, String description) { + super(Outcome.Benefit); + this.tokenMaker = tokenMaker; + staticText = "the exiled card's owner creates an X/X " + description + + "creature token, where X is the mana value of the exiled card"; + } + + private CreateXXTokenExiledEffectManaValueEffect(final CreateXXTokenExiledEffectManaValueEffect effect) { + super(effect); + this.tokenMaker = effect.tokenMaker; + } + + @Override + public CreateXXTokenExiledEffectManaValueEffect copy() { + return new CreateXXTokenExiledEffectManaValueEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanentLeftBattlefield = (Permanent) getValue("permanentLeftBattlefield"); + ExileZone exile = game.getExile().getExileZone( + CardUtil.getExileZoneId(game, source.getSourceId(), permanentLeftBattlefield.getZoneChangeCounter(game)) + ); + if (exile == null || exile.isEmpty()) { + return false; + } + // From ZNR Release Notes: + // https://magic.wizards.com/en/articles/archive/feature/zendikar-rising-release-notes-2020-09-10 + // If Skyclave Apparition's first ability exiled more than one card owned by a single player, + // that player creates a token with power and toughness equal to the sum of those cards' converted mana costs. + // If the first ability exiled cards owned by more than one player, each of those players creates a token + // with power and toughness equal to the sum of the converted mana costs of all cards exiled by the first ability. + Set owners = new HashSet<>(); + int totalCMC = exile + .getCards(game) + .stream() + .filter(Objects::nonNull) + .map(card -> { + owners.add(card.getOwnerId()); + return card; + }) + .mapToInt(MageObject::getManaValue) + .sum(); + for (UUID playerId : owners) { + tokenMaker.apply(totalCMC).putOntoBattlefield(1, game, source, playerId); + } + return true; + } +} From cb6c64bc0af6880f5734c7087d967957cca8191f Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 15:57:07 -0400 Subject: [PATCH 114/133] [TDM] Implement Jeskai Revelation --- .../src/mage/cards/j/JeskaiRevelation.java | 42 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 43 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/j/JeskaiRevelation.java diff --git a/Mage.Sets/src/mage/cards/j/JeskaiRevelation.java b/Mage.Sets/src/mage/cards/j/JeskaiRevelation.java new file mode 100644 index 00000000000..5a4a488cc07 --- /dev/null +++ b/Mage.Sets/src/mage/cards/j/JeskaiRevelation.java @@ -0,0 +1,42 @@ +package mage.cards.j; + +import mage.abilities.effects.common.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.MonasteryMentorToken; +import mage.target.common.TargetAnyTarget; +import mage.target.common.TargetSpellOrPermanent; +import mage.target.targetpointer.SecondTargetPointer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class JeskaiRevelation extends CardImpl { + + public JeskaiRevelation(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{U}{R}{W}"); + + // Return target spell or permanent to its owner's hand. Jeskai Revelation deals 4 damage to any target. Create two 1/1 white Monk creature tokens with prowess. Draw two cards. You gain 4 life. + this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); + this.getSpellAbility().addTarget(new TargetSpellOrPermanent()); + this.getSpellAbility().addEffect(new DamageTargetEffect( + 4, true, "any target", true + ).setTargetPointer(new SecondTargetPointer())); + this.getSpellAbility().addTarget(new TargetAnyTarget()); + this.getSpellAbility().addEffect(new CreateTokenEffect(new MonasteryMentorToken(), 2)); + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(2)); + this.getSpellAbility().addEffect(new GainLifeEffect(4)); + } + + private JeskaiRevelation(final JeskaiRevelation card) { + super(card); + } + + @Override + public JeskaiRevelation copy() { + return new JeskaiRevelation(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 28c91b3358f..b88c9fe710d 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -138,6 +138,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Jeskai Brushmaster", 195, Rarity.UNCOMMON, mage.cards.j.JeskaiBrushmaster.class)); cards.add(new SetCardInfo("Jeskai Devotee", 110, Rarity.COMMON, mage.cards.j.JeskaiDevotee.class)); cards.add(new SetCardInfo("Jeskai Monument", 244, Rarity.UNCOMMON, mage.cards.j.JeskaiMonument.class)); + cards.add(new SetCardInfo("Jeskai Revelation", 196, Rarity.MYTHIC, mage.cards.j.JeskaiRevelation.class)); cards.add(new SetCardInfo("Jeskai Shrinekeeper", 197, Rarity.UNCOMMON, mage.cards.j.JeskaiShrinekeeper.class)); cards.add(new SetCardInfo("Jungle Hollow", 258, Rarity.COMMON, mage.cards.j.JungleHollow.class)); cards.add(new SetCardInfo("Karakyk Guardian", 198, Rarity.UNCOMMON, mage.cards.k.KarakykGuardian.class)); From cf970ec0199de4ca3d81df7bd32de9052f14041a Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Thu, 10 Apr 2025 16:42:43 -0500 Subject: [PATCH 115/133] [TDM] Implement Taigam, Master Opportunist --- .../mage/cards/t/TaigamMasterOpportunist.java | 94 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 2 + 2 files changed, 96 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TaigamMasterOpportunist.java diff --git a/Mage.Sets/src/mage/cards/t/TaigamMasterOpportunist.java b/Mage.Sets/src/mage/cards/t/TaigamMasterOpportunist.java new file mode 100644 index 00000000000..025b1df41c3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TaigamMasterOpportunist.java @@ -0,0 +1,94 @@ +package mage.cards.t; + +import java.util.UUID; +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.FlurryAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainSuspendEffect; +import mage.abilities.keyword.SuspendAbility; +import mage.cards.Card; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.players.Player; + +/** + * + * @author Jmlundeen + */ +public final class TaigamMasterOpportunist extends CardImpl { + + public TaigamMasterOpportunist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.MONK); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Flurry -- Whenever you cast your second spell each turn, copy it, then exile the spell you cast with four time counters on it. If it doesn't have suspend, it gains suspend. + this.addAbility(new FlurryAbility(new TaigamMasterOpportunistEffect())); + } + + private TaigamMasterOpportunist(final TaigamMasterOpportunist card) { + super(card); + } + + @Override + public TaigamMasterOpportunist copy() { + return new TaigamMasterOpportunist(this); + } +} + +class TaigamMasterOpportunistEffect extends OneShotEffect { + + public TaigamMasterOpportunistEffect() { + super(Outcome.Copy); + this.staticText = "copy it, then exile the spell you cast with four time counters on it. " + + "If it doesn't have suspend, it gains suspend."; + } + + public TaigamMasterOpportunistEffect(final TaigamMasterOpportunistEffect effect) { + super(effect); + } + + @Override + public TaigamMasterOpportunistEffect copy() { + return new TaigamMasterOpportunistEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Spell spell = (Spell) this.getValue("spellCast"); + Player controller = game.getPlayer(source.getControllerId()); + if (spell == null || controller == null) { + return false; + } + + // copy it + spell.createCopyOnStack(game, source, source.getControllerId(), false); + // exile it, if it doesn't have suspend, it gains suspend + // get main card to work with adventure/omen/split + Card card = spell.getMainCard(); + if (card == null) { + return false; + } + UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game); + if (controller.moveCardsToExile(card, source, game, true, exileId, "Suspended cards of " + controller.getName())) { + boolean hasSuspend = card.getAbilities(game).containsClass(SuspendAbility.class); + card.addCounters(CounterType.TIME.createInstance(4), source, game); + game.informPlayers(controller.getLogName() + " exiles " + spell.getLogName() + " with 3 time counters on it"); + if (!hasSuspend) { + game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source); + } + return true; + } + return false; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index b88c9fe710d..e1984864701 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -251,6 +251,8 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Swamp", 281, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Swiftwater Cliffs", 268, Rarity.COMMON, mage.cards.s.SwiftwaterCliffs.class)); cards.add(new SetCardInfo("Synchronized Charge", 162, Rarity.UNCOMMON, mage.cards.s.SynchronizedCharge.class)); + cards.add(new SetCardInfo("Taigam, Master Opportunist", 60, Rarity.MYTHIC, mage.cards.t.TaigamMasterOpportunist.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Taigam, Master Opportunist", 335, Rarity.MYTHIC, mage.cards.t.TaigamMasterOpportunist.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Teeming Dragonstorm", 30, Rarity.UNCOMMON, mage.cards.t.TeemingDragonstorm.class)); cards.add(new SetCardInfo("Tempest Hawk", 31, Rarity.COMMON, mage.cards.t.TempestHawk.class)); cards.add(new SetCardInfo("Temur Battlecrier", 228, Rarity.RARE, mage.cards.t.TemurBattlecrier.class)); From 9285b7e78a0e2c9a86898cb69b454c746eafb98c Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 18:26:13 -0400 Subject: [PATCH 116/133] [TDM] Implement Perennation --- .../src/mage/cards/g/GracefulRestoration.java | 2 +- Mage.Sets/src/mage/cards/p/Perennation.java | 36 +++++++++++++++ Mage.Sets/src/mage/cards/r/RakdosJoinsUp.java | 2 +- .../mage/cards/t/TheRevelationsOfEzio.java | 4 +- .../src/mage/sets/TarkirDragonstorm.java | 1 + ...dToBattlefieldWithCounterTargetEffect.java | 44 +++++++++++-------- 6 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/p/Perennation.java diff --git a/Mage.Sets/src/mage/cards/g/GracefulRestoration.java b/Mage.Sets/src/mage/cards/g/GracefulRestoration.java index fed8e25db2f..3421be44ed7 100644 --- a/Mage.Sets/src/mage/cards/g/GracefulRestoration.java +++ b/Mage.Sets/src/mage/cards/g/GracefulRestoration.java @@ -32,7 +32,7 @@ public final class GracefulRestoration extends CardImpl { // Choose one — // • Return target creature card from your graveyard to the battlefield with an additional +1/+1 counter on it. - this.getSpellAbility().addEffect(new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(CounterType.P1P1.createInstance(), true)); + this.getSpellAbility().addEffect(new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(true, CounterType.P1P1.createInstance())); this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); // • Return up to two target creature cards with power 2 or less from your graveyard to the battlefield. diff --git a/Mage.Sets/src/mage/cards/p/Perennation.java b/Mage.Sets/src/mage/cards/p/Perennation.java new file mode 100644 index 00000000000..ad266c96dce --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/Perennation.java @@ -0,0 +1,36 @@ +package mage.cards.p; + +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldWithCounterTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class Perennation extends CardImpl { + + public Perennation(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{W}{B}{G}"); + + // Return target permanent card from your graveyard to the battlefield with a hexproof counter and an indestructible counter on it. + this.getSpellAbility().addEffect(new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect( + CounterType.HEXPROOF.createInstance(), CounterType.INDESTRUCTIBLE.createInstance() + )); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_PERMANENT)); + } + + private Perennation(final Perennation card) { + super(card); + } + + @Override + public Perennation copy() { + return new Perennation(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RakdosJoinsUp.java b/Mage.Sets/src/mage/cards/r/RakdosJoinsUp.java index 6750ef7d2c9..493077261e8 100644 --- a/Mage.Sets/src/mage/cards/r/RakdosJoinsUp.java +++ b/Mage.Sets/src/mage/cards/r/RakdosJoinsUp.java @@ -43,7 +43,7 @@ public final class RakdosJoinsUp extends CardImpl { // When Rakdos Joins Up enters the battlefield, return target creature card from your graveyard to the battlefield with two additional +1/+1 counters on it. Ability ability = new EntersBattlefieldTriggeredAbility( - new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(CounterType.P1P1.createInstance(2), true) + new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(true, CounterType.P1P1.createInstance(2)) ); ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/t/TheRevelationsOfEzio.java b/Mage.Sets/src/mage/cards/t/TheRevelationsOfEzio.java index 28664c0c876..cc52367e98d 100644 --- a/Mage.Sets/src/mage/cards/t/TheRevelationsOfEzio.java +++ b/Mage.Sets/src/mage/cards/t/TheRevelationsOfEzio.java @@ -61,7 +61,7 @@ public final class TheRevelationsOfEzio extends CardImpl { sagaAbility.addChapterEffect( this, SagaChapter.CHAPTER_III, SagaChapter.CHAPTER_III, ability -> { - ability.addEffect(new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(CounterType.P1P1.createInstance(), true)); + ability.addEffect(new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(true, CounterType.P1P1.createInstance())); ability.addTarget(new TargetCardInYourGraveyard(assassinFilter)); } ); @@ -116,4 +116,4 @@ class TheRevelationsOfEzioTriggeredAbility extends DelayedTriggeredAbility { public String getRule() { return "Whenever an Assassin you control attacks this turn, put a +1/+1 counter on it."; } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index e1984864701..f797ae0ada3 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -181,6 +181,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Opulent Palace", 264, Rarity.UNCOMMON, mage.cards.o.OpulentPalace.class)); cards.add(new SetCardInfo("Osseous Exhale", 17, Rarity.COMMON, mage.cards.o.OsseousExhale.class)); cards.add(new SetCardInfo("Overwhelming Surge", 115, Rarity.UNCOMMON, mage.cards.o.OverwhelmingSurge.class)); + cards.add(new SetCardInfo("Perennation", 212, Rarity.MYTHIC, mage.cards.p.Perennation.class)); cards.add(new SetCardInfo("Piercing Exhale", 151, Rarity.COMMON, mage.cards.p.PiercingExhale.class)); cards.add(new SetCardInfo("Plains", 277, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Poised Practitioner", 18, Rarity.COMMON, mage.cards.p.PoisedPractitioner.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldWithCounterTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldWithCounterTargetEffect.java index 2bf4ce8bc1f..7d85a7a1a30 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldWithCounterTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ReturnFromGraveyardToBattlefieldWithCounterTargetEffect.java @@ -7,6 +7,8 @@ import mage.counters.Counters; import mage.game.Game; import mage.util.CardUtil; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; /** @@ -17,15 +19,17 @@ public class ReturnFromGraveyardToBattlefieldWithCounterTargetEffect extends Ret private final Counters counters; private final String counterText; - public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter counter) { - this(counter, false); + public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter... counters) { + this(false, counters); } - public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter counter, boolean additional) { + public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(boolean additional, Counter... counters) { super(false); this.counters = new Counters(); - this.counters.addCounter(counter); - this.counterText = makeText(counter, additional); + for (Counter counter : counters) { + this.counters.addCounter(counter); + } + this.counterText = makeText(additional, counters); } protected ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(final ReturnFromGraveyardToBattlefieldWithCounterTargetEffect effect) { @@ -47,22 +51,26 @@ public class ReturnFromGraveyardToBattlefieldWithCounterTargetEffect extends Ret return super.apply(game, source); } - private String makeText(Counter counter, boolean additional) { - StringBuilder sb = new StringBuilder(" with "); - if (counter.getCount() == 1) { - if (additional) { - sb.append("an additional ").append(counter.getName()); + private static String makeText(boolean additional, Counter... counters) { + List strings = new ArrayList<>(); + for (Counter counter : counters) { + StringBuilder sb = new StringBuilder(); + if (counter.getCount() == 1) { + if (additional) { + sb.append("an additional ").append(counter.getName()); + } else { + sb.append(CardUtil.addArticle(counter.getName())); + } + sb.append(" counter"); } else { - sb.append(CardUtil.addArticle(counter.getName())); + sb.append(CardUtil.numberToText(counter.getCount())); + sb.append(additional ? " additional " : " "); + sb.append(counter.getName()); + sb.append(" counters"); } - sb.append(" counter"); - } else { - sb.append(CardUtil.numberToText(counter.getCount())); - sb.append(additional ? " additional " : " "); - sb.append(counter.getName()); - sb.append(" counters"); + strings.add(sb.toString()); } - return sb.toString(); + return " with " + CardUtil.concatWithAnd(strings); } @Override From 937fe8630b004bbcb0149a4e2e81b7c1f5e39eb5 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 18:45:08 -0400 Subject: [PATCH 117/133] [TDM] Implement Stalwart Successor --- .../src/mage/cards/s/StalwartSuccessor.java | 123 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 124 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StalwartSuccessor.java diff --git a/Mage.Sets/src/mage/cards/s/StalwartSuccessor.java b/Mage.Sets/src/mage/cards/s/StalwartSuccessor.java new file mode 100644 index 00000000000..ae0fbeacb18 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StalwartSuccessor.java @@ -0,0 +1,123 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.WatcherScope; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.targetpointer.FixedTarget; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StalwartSuccessor extends CardImpl { + + public StalwartSuccessor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{G}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Menace + this.addAbility(new MenaceAbility()); + + // Whenever one or more counters are put on a creature you control, if it's the first time counters have been put on that creature this turn, put a +1/+1 counter on that creature. + this.addAbility(new StalwartSuccessorTriggeredAbility()); + } + + private StalwartSuccessor(final StalwartSuccessor card) { + super(card); + } + + @Override + public StalwartSuccessor copy() { + return new StalwartSuccessor(this); + } +} + +class StalwartSuccessorTriggeredAbility extends TriggeredAbilityImpl { + + StalwartSuccessorTriggeredAbility() { + super(Zone.BATTLEFIELD, new AddCountersTargetEffect(CounterType.P1P1.createInstance())); + this.setTriggerPhrase("Whenever one or more counters are put on a creature you control, " + + "if it's the first time counters have been put on that creature this turn, "); + this.addWatcher(new StalwartSuccessorWatcher()); + } + + private StalwartSuccessorTriggeredAbility(final StalwartSuccessorTriggeredAbility ability) { + super(ability); + } + + @Override + public StalwartSuccessorTriggeredAbility copy() { + return new StalwartSuccessorTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.COUNTERS_ADDED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Permanent permanent = game.getPermanent(event.getTargetId()); + if (permanent == null + || !permanent.isCreature(game) + || !permanent.isControlledBy(getControllerId()) + || !StalwartSuccessorWatcher.checkCreature(permanent, event, game)) { + return false; + } + this.getEffects().setTargetPointer(new FixedTarget(permanent, game)); + return true; + } +} + +class StalwartSuccessorWatcher extends Watcher { + + private final Map map = new HashMap<>(); + + StalwartSuccessorWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.COUNTERS_ADDED) { + map.putIfAbsent(new MageObjectReference(event.getTargetId(), game), event.getId()); + } + } + + @Override + public void reset() { + super.reset(); + map.clear(); + } + + static boolean checkCreature(Permanent permanent, GameEvent event, Game game) { + return Objects.equals( + event.getId(), + game.getState() + .getWatcher(StalwartSuccessorWatcher.class) + .map + .getOrDefault(new MageObjectReference(permanent, game), null) + ); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index f797ae0ada3..41fc545735f 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -236,6 +236,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Sonic Shrieker", 226, Rarity.UNCOMMON, mage.cards.s.SonicShrieker.class)); cards.add(new SetCardInfo("Spectral Denial", 58, Rarity.UNCOMMON, mage.cards.s.SpectralDenial.class)); cards.add(new SetCardInfo("Stadium Headliner", 122, Rarity.RARE, mage.cards.s.StadiumHeadliner.class)); + cards.add(new SetCardInfo("Stalwart Successor", 227, Rarity.UNCOMMON, mage.cards.s.StalwartSuccessor.class)); cards.add(new SetCardInfo("Starry-Eyed Skyrider", 25, Rarity.UNCOMMON, mage.cards.s.StarryEyedSkyrider.class)); cards.add(new SetCardInfo("Static Snare", 26, Rarity.UNCOMMON, mage.cards.s.StaticSnare.class)); cards.add(new SetCardInfo("Stillness in Motion", 59, Rarity.RARE, mage.cards.s.StillnessInMotion.class)); From 86fe23e396302a7b76641f14e8cd6515a5deb43b Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 19:07:34 -0400 Subject: [PATCH 118/133] [TDM] Implement New Way Forward --- .../src/mage/cards/d/DeflectingPalm.java | 41 +++----- Mage.Sets/src/mage/cards/n/NewWayForward.java | 97 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 3 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/n/NewWayForward.java diff --git a/Mage.Sets/src/mage/cards/d/DeflectingPalm.java b/Mage.Sets/src/mage/cards/d/DeflectingPalm.java index 7189cbe5329..a9f8bdccb89 100644 --- a/Mage.Sets/src/mage/cards/d/DeflectingPalm.java +++ b/Mage.Sets/src/mage/cards/d/DeflectingPalm.java @@ -1,6 +1,5 @@ package mage.cards.d; -import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.PreventionEffectData; import mage.abilities.effects.PreventionEffectImpl; @@ -11,8 +10,6 @@ import mage.constants.Duration; import mage.constants.Outcome; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; -import mage.game.stack.Spell; import mage.players.Player; import mage.target.TargetSource; @@ -44,9 +41,11 @@ class DeflectingPalmEffect extends PreventionEffectImpl { private final TargetSource target; - public DeflectingPalmEffect() { + DeflectingPalmEffect() { super(Duration.EndOfTurn, Integer.MAX_VALUE, false, false); - this.staticText = "The next time a source of your choice would deal damage to you this turn, prevent that damage. If damage is prevented this way, {this} deals that much damage to that source's controller"; + this.staticText = "the next time a source of your choice would deal damage to you this turn, " + + "prevent that damage. If damage is prevented this way, " + + "{this} deals that much damage to that source's controller"; this.target = new TargetSource(); } @@ -71,31 +70,23 @@ class DeflectingPalmEffect extends PreventionEffectImpl { PreventionEffectData preventionData = preventDamageAction(event, source, game); this.used = true; this.discard(); // only one use - if (preventionData.getPreventedDamage() > 0) { - MageObject damageDealingObject = game.getObject(target.getFirstTarget()); - UUID objectControllerId = null; - if (damageDealingObject instanceof Permanent) { - objectControllerId = ((Permanent) damageDealingObject).getControllerId(); - } else if (damageDealingObject instanceof Ability) { - objectControllerId = ((Ability) damageDealingObject).getControllerId(); - } else if (damageDealingObject instanceof Spell) { - objectControllerId = ((Spell) damageDealingObject).getControllerId(); - } - if (objectControllerId != null) { - Player objectController = game.getPlayer(objectControllerId); - if (objectController != null) { - objectController.damage(preventionData.getPreventedDamage(), source.getSourceId(), source, game); - } - } + if (preventionData.getPreventedDamage() < 1) { + return true; } + UUID objectControllerId = game.getControllerId(target.getFirstTarget()); + Player objectController = game.getPlayer(objectControllerId); + if (objectController == null) { + return true; + } + objectController.damage(preventionData.getPreventedDamage(), source.getSourceId(), source, game); return true; } @Override public boolean applies(GameEvent event, Ability source, Game game) { - if (!this.used && super.applies(event, source, game)) { - return event.getTargetId().equals(source.getControllerId()) && event.getSourceId().equals(target.getFirstTarget()); - } - return false; + return !this.used + && super.applies(event, source, game) + && event.getTargetId().equals(source.getControllerId()) + && event.getSourceId().equals(target.getFirstTarget()); } } diff --git a/Mage.Sets/src/mage/cards/n/NewWayForward.java b/Mage.Sets/src/mage/cards/n/NewWayForward.java new file mode 100644 index 00000000000..7b359e2b9ea --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NewWayForward.java @@ -0,0 +1,97 @@ +package mage.cards.n; + +import mage.abilities.Ability; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.effects.PreventionEffectData; +import mage.abilities.effects.PreventionEffectImpl; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.target.TargetSource; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NewWayForward extends CardImpl { + + public NewWayForward(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{U}{R}{W}"); + + // The next time a source of your choice would deal damage to you this turn, prevent that damage. When damage is prevented this way, New Way Forward deals that much damage to that source's controller and you draw that many cards. + this.getSpellAbility().addEffect(new NewWayForwardEffect()); + } + + private NewWayForward(final NewWayForward card) { + super(card); + } + + @Override + public NewWayForward copy() { + return new NewWayForward(this); + } +} + +class NewWayForwardEffect extends PreventionEffectImpl { + + private final TargetSource target; + + NewWayForwardEffect() { + super(Duration.EndOfTurn, Integer.MAX_VALUE, false, false); + this.staticText = "the next time a source of your choice would deal damage to you this turn, " + + "prevent that damage. When damage is prevented this way, " + + "{this} deals that much damage to that source's controller and you draw that many cards"; + this.target = new TargetSource(); + } + + private NewWayForwardEffect(final NewWayForwardEffect effect) { + super(effect); + this.target = effect.target.copy(); + } + + @Override + public NewWayForwardEffect copy() { + return new NewWayForwardEffect(this); + } + + @Override + public void init(Ability source, Game game) { + super.init(source, game); + this.target.choose(Outcome.PreventDamage, source.getControllerId(), source.getSourceId(), source, game); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + PreventionEffectData preventionData = preventDamageAction(event, source, game); + this.used = true; + this.discard(); // only one use + if (preventionData.getPreventedDamage() < 1) { + return true; + } + UUID objectControllerId = game.getControllerId(target.getFirstTarget()); + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( + new DamageTargetEffect(preventionData.getPreventedDamage()) + .setTargetPointer(new FixedTarget(objectControllerId)), + false + ); + ability.addEffect(new DrawCardSourceControllerEffect(preventionData.getPreventedDamage())); + game.fireReflexiveTriggeredAbility(ability, source); + return true; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return !this.used + && super.applies(event, source, game) + && event.getTargetId().equals(source.getControllerId()) + && event.getSourceId().equals(target.getFirstTarget()); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 41fc545735f..2c7e22fd57c 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -176,6 +176,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Narset, Jeskai Waymaster", 209, Rarity.RARE, mage.cards.n.NarsetJeskaiWaymaster.class)); cards.add(new SetCardInfo("Nature's Rhythm", 150, Rarity.RARE, mage.cards.n.NaturesRhythm.class)); cards.add(new SetCardInfo("Neriv, Heart of the Storm", 210, Rarity.MYTHIC, mage.cards.n.NerivHeartOfTheStorm.class)); + cards.add(new SetCardInfo("New Way Forward", 211, Rarity.RARE, mage.cards.n.NewWayForward.class)); cards.add(new SetCardInfo("Nightblade Brigade", 85, Rarity.COMMON, mage.cards.n.NightbladeBrigade.class)); cards.add(new SetCardInfo("Nomad Outpost", 263, Rarity.UNCOMMON, mage.cards.n.NomadOutpost.class)); cards.add(new SetCardInfo("Opulent Palace", 264, Rarity.UNCOMMON, mage.cards.o.OpulentPalace.class)); From 8dd8953a85280fa411086c4be7659e7e79b87b7a Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 10 Apr 2025 21:33:42 -0400 Subject: [PATCH 119/133] [TDM] Implement Magmatic Hellkite --- .../src/mage/cards/m/MagmaticHellkite.java | 108 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 109 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MagmaticHellkite.java diff --git a/Mage.Sets/src/mage/cards/m/MagmaticHellkite.java b/Mage.Sets/src/mage/cards/m/MagmaticHellkite.java new file mode 100644 index 00000000000..6d7685aba95 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MagmaticHellkite.java @@ -0,0 +1,108 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.counters.Counters; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterLandPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MagmaticHellkite extends CardImpl { + + private static final FilterPermanent filter = new FilterLandPermanent("nonbasic land an opponent controls"); + + static { + filter.add(Predicates.not(SuperType.BASIC.getPredicate())); + filter.add(TargetController.OPPONENT.getControllerPredicate()); + } + + public MagmaticHellkite(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When this creature enters, destroy target nonbasic land an opponent controls. Its controller searches their library for a basic land card, puts it onto the battlefield tapped with a stun counter on it, then shuffles. + Ability ability = new EntersBattlefieldTriggeredAbility(new MagmaticHellkiteEffect()); + ability.addTarget(new TargetPermanent(filter)); + this.addAbility(ability); + } + + private MagmaticHellkite(final MagmaticHellkite card) { + super(card); + } + + @Override + public MagmaticHellkite copy() { + return new MagmaticHellkite(this); + } +} + +class MagmaticHellkiteEffect extends OneShotEffect { + + MagmaticHellkiteEffect() { + super(Outcome.Benefit); + staticText = "destroy target nonbasic land an opponent controls. " + + "Its controller searches their library for a basic land card, " + + "puts it onto the battlefield tapped with a stun counter on it, then shuffles"; + } + + private MagmaticHellkiteEffect(final MagmaticHellkiteEffect effect) { + super(effect); + } + + @Override + public MagmaticHellkiteEffect copy() { + return new MagmaticHellkiteEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + Player player = game.getPlayer(permanent.getControllerId()); + permanent.destroy(source, game); + if (player == null) { + return true; + } + TargetCardInLibrary target = new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND); + player.searchLibrary(target, source, game); + Card card = player.getLibrary().getCard(target.getFirstTarget(), game); + if (card == null) { + player.shuffleLibrary(source, game); + return true; + } + game.setEnterWithCounters(card.getId(), new Counters().addCounter(CounterType.STUN.createInstance())); + player.moveCards( + card, Zone.BATTLEFIELD, source, game, true, + false, false, null + ); + player.shuffleLibrary(source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 2c7e22fd57c..b6d20f107df 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -159,6 +159,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Lotuslight Dancers", 363, Rarity.RARE, mage.cards.l.LotuslightDancers.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Loxodon Battle Priest", 15, Rarity.UNCOMMON, mage.cards.l.LoxodonBattlePriest.class)); cards.add(new SetCardInfo("Maelstrom of the Spirit Dragon", 260, Rarity.RARE, mage.cards.m.MaelstromOfTheSpiritDragon.class)); + cards.add(new SetCardInfo("Magmatic Hellkite", 111, Rarity.RARE, mage.cards.m.MagmaticHellkite.class)); cards.add(new SetCardInfo("Mammoth Bellow", 205, Rarity.UNCOMMON, mage.cards.m.MammothBellow.class)); cards.add(new SetCardInfo("Marang River Regent", 51, Rarity.RARE, mage.cards.m.MarangRiverRegent.class)); cards.add(new SetCardInfo("Mardu Devotee", 16, Rarity.COMMON, mage.cards.m.MarduDevotee.class)); From 1b06813997e554301e045653e30f927e7fa0ca76 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Fri, 11 Apr 2025 06:22:13 -0500 Subject: [PATCH 120/133] Reworked Suspend ability: (#13527) * Updated Delay and Gandalf Of The Secret Fire to get the main card since they target spells * Suspend now properly lets you play either side of mdfc and spell parts from adventure/omen cards utilizing CardUtil.castSpellWithAttributesForFree method * Removed extra code in SuspendPlayCardEffect since the referenced bug for Epochrasite does not seem to appear. Removed related gainedTemporary variable also. * Added tests for Omen and Suspend With Taigam, Master Opportunists as well as an Epochrasite test for recasting after suspend. --- Mage.Sets/src/mage/cards/d/Delay.java | 2 +- .../mage/cards/g/GandalfOfTheSecretFire.java | 2 +- .../cards/abilities/keywords/SuspendTest.java | 35 ++++++ .../tdm/TaigamMasterOpportunistTest.java | 105 ++++++++++++++++++ .../abilities/keyword/SuspendAbility.java | 57 +++------- 5 files changed, 158 insertions(+), 43 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/TaigamMasterOpportunistTest.java diff --git a/Mage.Sets/src/mage/cards/d/Delay.java b/Mage.Sets/src/mage/cards/d/Delay.java index 0e43d711f0f..ee21e510be2 100644 --- a/Mage.Sets/src/mage/cards/d/Delay.java +++ b/Mage.Sets/src/mage/cards/d/Delay.java @@ -68,7 +68,7 @@ class DelayEffect extends OneShotEffect { if (controller != null && spell != null) { Effect effect = new CounterTargetWithReplacementEffect(PutCards.EXILED); effect.setTargetPointer(this.getTargetPointer().copy()); - Card card = game.getCard(spell.getSourceId()); + Card card = spell.getMainCard(); if (card != null && effect.apply(game, source) && game.getState().getZone(card.getId()) == Zone.EXILED) { boolean hasSuspend = card.getAbilities(game).containsClass(SuspendAbility.class); UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game); diff --git a/Mage.Sets/src/mage/cards/g/GandalfOfTheSecretFire.java b/Mage.Sets/src/mage/cards/g/GandalfOfTheSecretFire.java index 165793c9f6a..b7b93aa887f 100644 --- a/Mage.Sets/src/mage/cards/g/GandalfOfTheSecretFire.java +++ b/Mage.Sets/src/mage/cards/g/GandalfOfTheSecretFire.java @@ -119,7 +119,7 @@ class GandalfOfTheSecretFireEffect extends ReplacementEffectImpl { game.informPlayers(controller.getLogName() + " exiles " + sourceSpell.getLogName() + " with 3 time counters on it"); } if (!sourceSpell.getAbilities(game).containsClass(SuspendAbility.class)) { - game.addEffect(new GainSuspendEffect(new MageObjectReference(sourceSpell.getCard(), game)), source); + game.addEffect(new GainSuspendEffect(new MageObjectReference(sourceSpell.getMainCard(), game)), source); } return true; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SuspendTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SuspendTest.java index 7cd178aa7e6..dd292c7229a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SuspendTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SuspendTest.java @@ -42,6 +42,40 @@ public class SuspendTest extends CardTestPlayerBase { } + /** + * Tests bug that was mentioned in suspend ability, but does not appear to still be an issue. + * Epochrasite being unable to be cast after casting from suspend and returning to hand. + */ + @Test + public void test_Single_Epochrasite_Recast_After_Suspend() { + // Bug was mentioned in suspend ability, but does not appear to still be an issue + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + // Epochrasite enters the battlefield with three +1/+1 counters on it if you didn't cast it from your hand. + // When Epochrasite dies, exile it with three time counters on it and it gains suspend. + addCard(Zone.HAND, playerA, "Epochrasite", 1); + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); + addCard(Zone.HAND, playerB, "Boomerang", 1); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Epochrasite"); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", "Epochrasite"); + castSpell(7, PhaseStep.DRAW, playerB, "Boomerang", "Epochrasite"); + castSpell(7, PhaseStep.PRECOMBAT_MAIN, playerA, "Epochrasite"); + + + setChoice(playerA, true); // choose yes to cast + + setStrictChooseMode(true); + setStopAt(7, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertGraveyardCount(playerB, "Lightning Bolt", 1); + assertPermanentCount(playerA, "Epochrasite", 1); // returned on turn 7 and cast again after going to hand + assertPowerToughness(playerA, "Epochrasite", 1, 1); + assertAbility(playerA, "Epochrasite", HasteAbility.getInstance(), false); + } + /** * Tests Jhoira of the Ghitu works (give suspend to a exiled card) {2}, * Exile a nonland card from your hand: Put four time counters on the exiled @@ -275,6 +309,7 @@ public class SuspendTest extends CardTestPlayerBase { // 3 time counters removes on upkeep (3, 5, 7) and cast again setChoice(playerA, true); // choose yes to cast + setChoice(playerA, "Cast Wear"); addTarget(playerA, "Bident of Thassa"); checkPermanentCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, "Bident of Thassa", 0); checkPermanentCount("after suspend", 7, PhaseStep.PRECOMBAT_MAIN, playerB, "Bow of Nylea", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/TaigamMasterOpportunistTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/TaigamMasterOpportunistTest.java new file mode 100644 index 00000000000..9cda471dcce --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/TaigamMasterOpportunistTest.java @@ -0,0 +1,105 @@ +package org.mage.test.cards.single.tdm; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class TaigamMasterOpportunistTest extends CardTestPlayerBase { + + private static final String TAIGAM = "Taigam, Master Opportunist"; + private static final String ORNITHOPTER = "Ornithopter"; + private static final String TWINMAW = "Twinmaw Stormbrood"; + private static final String BITE = "Charring Bite"; + private static final String TURTLE = "Aegis Turtle"; + private static final String AKOUM = "Akoum Warrior"; + + @Test + public void testCardWithSpellOption() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, TAIGAM); + addCard(Zone.HAND, playerA, ORNITHOPTER); + addCard(Zone.HAND, playerA, TWINMAW); + addCard(Zone.BATTLEFIELD, playerA, "Plateau", 6); + addCard(Zone.BATTLEFIELD, playerB, TURTLE, 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ORNITHOPTER, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, BITE, TURTLE); + checkCardCounters("time counters", 1, PhaseStep.BEGIN_COMBAT, playerA, TWINMAW, CounterType.TIME, 4); + setChoice(playerA, true); + setChoice(playerA, "Cast " + BITE); + addTarget(playerA, TURTLE); + + setStopAt(9, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLibraryCount(playerA, TWINMAW, 1); + assertGraveyardCount(playerB, TURTLE, 2); + } + + @Test + public void testMDFC() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, TAIGAM); + addCard(Zone.BATTLEFIELD, playerA, "Plateau", 6); + addCard(Zone.HAND, playerA, ORNITHOPTER); + addCard(Zone.HAND, playerA, AKOUM); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ORNITHOPTER, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, AKOUM); + setChoice(playerA, true); + setChoice(playerA, "Play Akoum Teeth"); + + setStopAt(9, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Akoum Teeth", 1); + assertTapped("Akoum Teeth", true); + } + + @Test + public void testMDFC2() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plateau", 6); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.HAND, playerB, "Delay"); + addCard(Zone.HAND, playerA, AKOUM); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, AKOUM); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Delay", AKOUM); + setChoice(playerA, true); + setChoice(playerA, "Play Akoum Teeth"); + + setStopAt(7, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Akoum Teeth", 1); + assertTapped("Akoum Teeth", true); + } + + @Test + public void test() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plateau", 6); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.BATTLEFIELD, playerB, TURTLE); + addCard(Zone.HAND, playerB, "Delay"); + addCard(Zone.HAND, playerA, TWINMAW); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, TWINMAW); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Delay", TWINMAW); + setChoice(playerA, true); + setChoice(playerA, "Cast " + BITE); + addTarget(playerA, TURTLE); + + setStopAt(7, PhaseStep.PRECOMBAT_MAIN); + execute(); + + } + +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java index 72d5cbcbde4..1114ecebc64 100644 --- a/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/SuspendAbility.java @@ -1,6 +1,5 @@ package mage.abilities.keyword; -import mage.ApprovingObject; import mage.MageIdentifier; import mage.abilities.Ability; import mage.abilities.SpecialAction; @@ -17,8 +16,11 @@ import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; import mage.cards.Card; +import mage.cards.CardsImpl; +import mage.cards.ModalDoubleFacedCard; import mage.constants.*; import mage.counters.CounterType; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; @@ -26,8 +28,6 @@ import mage.players.Player; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import java.util.UUID; @@ -112,7 +112,6 @@ import java.util.UUID; public class SuspendAbility extends SpecialAction { private final String ruleText; - private boolean gainedTemporary; /** * Gives the card the SuspendAbility @@ -177,6 +176,11 @@ public class SuspendAbility extends SpecialAction { * or added by Jhoira of the Ghitu */ public static void addSuspendTemporaryToCard(Card card, Ability source, Game game) { + if (card instanceof ModalDoubleFacedCard) { + // Need to ensure the suspend ability gets put on the left side card + // since counters get added to this card. + card = ((ModalDoubleFacedCard) card).getLeftHalfCard(); + } SuspendAbility ability = new SuspendAbility(0, null, card, false); ability.setSourceId(card.getId()); ability.setControllerId(card.getOwnerId()); @@ -206,7 +210,6 @@ public class SuspendAbility extends SpecialAction { private SuspendAbility(final SuspendAbility ability) { super(ability); this.ruleText = ability.ruleText; - this.gainedTemporary = ability.gainedTemporary; } @Override @@ -232,10 +235,6 @@ public class SuspendAbility extends SpecialAction { return ruleText; } - public boolean isGainedTemporary() { - return gainedTemporary; - } - @Override public SuspendAbility copy() { return new SuspendAbility(this); @@ -345,40 +344,16 @@ class SuspendPlayCardEffect extends OneShotEffect { if (player == null || card == null) { return false; } - if (!player.chooseUse(Outcome.Benefit, "Play " + card.getLogName() + " without paying its mana cost?", source, game)) { + // ensure we're getting the main card when passing to CardUtil to check all parts of card + // MDFC points to left half card + card = card.getMainCard(); + // cast/play the card for free + if (!CardUtil.castSpellWithAttributesForFree(player, source, game, new CardsImpl(card), + StaticFilters.FILTER_CARD, null, true)) { return true; } - // remove temporary suspend ability (used e.g. for Epochrasite) - // TODO: isGainedTemporary is not set or use in other places, so it can be deleted?! - List abilitiesToRemove = new ArrayList<>(); - for (Ability ability : card.getAbilities(game)) { - if (ability instanceof SuspendAbility && (((SuspendAbility) ability).isGainedTemporary())) { - abilitiesToRemove.add(ability); - } - } - if (!abilitiesToRemove.isEmpty()) { - for (Ability ability : card.getAbilities(game)) { - if (ability instanceof SuspendBeginningOfUpkeepInterveningIfTriggeredAbility - || ability instanceof SuspendPlayCardAbility) { - abilitiesToRemove.add(ability); - } - } - // remove the abilities from the card - // TODO: will not work with Adventure Cards and another auto-generated abilities list - // TODO: is it work after blink or return to hand? - /* - bug example: - Epochrasite bug: It comes out of suspend, is cast and enters the battlefield. THEN if it's returned to - its owner's hand from battlefield, the bounced Epochrasite can't be cast for the rest of the game. - */ - card.getAbilities().removeAll(abilitiesToRemove); - } - // cast the card for free - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); - boolean cardWasCast = player.cast(player.chooseAbilityForCast(card, game, true), - game, true, new ApprovingObject(source, game)); - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); - if (cardWasCast && (card.isCreature(game))) { + // creatures cast from suspend gain haste + if ((card.isCreature(game))) { ContinuousEffect effect = new GainHasteEffect(); effect.setTargetPointer(new FixedTarget(card.getId(), card.getZoneChangeCounter(game) + 1)); game.addEffect(effect, source); From b81954574417ae82d12c94a521d5938405f446dd Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 11 Apr 2025 17:50:12 +0400 Subject: [PATCH 121/133] bump version --- Mage.Client/pom.xml | 2 +- Mage.Common/pom.xml | 2 +- Mage.Common/src/main/java/mage/utils/MageVersion.java | 4 ++-- Mage.Plugins/Mage.Counter.Plugin/pom.xml | 2 +- Mage.Plugins/pom.xml | 2 +- Mage.Reports/pom.xml | 2 +- Mage.Server.Console/pom.xml | 2 +- Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml | 2 +- Mage.Server.Plugins/Mage.Deck.Limited/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml | 2 +- .../Mage.Game.CustomPillarOfTheParunsDuel/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml | 2 +- .../Mage.Game.FreeformCommanderFreeForAll/pom.xml | 2 +- .../Mage.Game.FreeformUnlimitedCommander/pom.xml | 4 ++-- Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml | 4 ++-- Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml | 2 +- .../Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml | 2 +- Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml | 2 +- Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml | 2 +- Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml | 2 +- Mage.Server.Plugins/Mage.Player.AI/pom.xml | 2 +- Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml | 2 +- Mage.Server.Plugins/Mage.Player.Human/pom.xml | 2 +- Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml | 2 +- Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml | 2 +- Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml | 2 +- Mage.Server.Plugins/pom.xml | 2 +- Mage.Server/pom.xml | 2 +- Mage.Sets/pom.xml | 2 +- Mage.Tests/pom.xml | 2 +- Mage.Verify/pom.xml | 2 +- Mage/pom.xml | 2 +- pom.xml | 4 ++-- 41 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Mage.Client/pom.xml b/Mage.Client/pom.xml index c573fb21f68..d27d1ff605e 100644 --- a/Mage.Client/pom.xml +++ b/Mage.Client/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-client diff --git a/Mage.Common/pom.xml b/Mage.Common/pom.xml index 2a0c7e1bf6a..36fe0dc82ca 100644 --- a/Mage.Common/pom.xml +++ b/Mage.Common/pom.xml @@ -7,7 +7,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-common diff --git a/Mage.Common/src/main/java/mage/utils/MageVersion.java b/Mage.Common/src/main/java/mage/utils/MageVersion.java index 79dfcb49f43..07ba1547c1a 100644 --- a/Mage.Common/src/main/java/mage/utils/MageVersion.java +++ b/Mage.Common/src/main/java/mage/utils/MageVersion.java @@ -17,8 +17,8 @@ public class MageVersion implements Serializable, Comparable { // * launcher gives priority to 1.4.48 instead 1.4.48-any-text, so don't use empty release info public static final int MAGE_VERSION_MAJOR = 1; public static final int MAGE_VERSION_MINOR = 4; - public static final int MAGE_VERSION_RELEASE = 56; - public static final String MAGE_VERSION_RELEASE_INFO = "V3"; // V1, V1a, V1b for releases; V1-beta3, V1-beta4 for betas + public static final int MAGE_VERSION_RELEASE = 57; + public static final String MAGE_VERSION_RELEASE_INFO = "V1"; // V1, V1a, V1b for releases; V1-beta3, V1-beta4 for betas // strict mode // Each update requires a strict version diff --git a/Mage.Plugins/Mage.Counter.Plugin/pom.xml b/Mage.Plugins/Mage.Counter.Plugin/pom.xml index 54eba397b58..01eb53581af 100644 --- a/Mage.Plugins/Mage.Counter.Plugin/pom.xml +++ b/Mage.Plugins/Mage.Counter.Plugin/pom.xml @@ -7,7 +7,7 @@ org.mage mage-plugins - 1.4.56 + 1.4.57 mage-counter-plugin diff --git a/Mage.Plugins/pom.xml b/Mage.Plugins/pom.xml index b3d08009be2..bb34aa35fd1 100644 --- a/Mage.Plugins/pom.xml +++ b/Mage.Plugins/pom.xml @@ -7,7 +7,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-plugins diff --git a/Mage.Reports/pom.xml b/Mage.Reports/pom.xml index 2e98574191d..ad07b817fe5 100644 --- a/Mage.Reports/pom.xml +++ b/Mage.Reports/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-reports diff --git a/Mage.Server.Console/pom.xml b/Mage.Server.Console/pom.xml index a2192534a39..83cc4306e94 100644 --- a/Mage.Server.Console/pom.xml +++ b/Mage.Server.Console/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-server-console diff --git a/Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml b/Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml index 57d5a53b0f1..c7e540478b8 100644 --- a/Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml +++ b/Mage.Server.Plugins/Mage.Deck.Constructed/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-deck-constructed diff --git a/Mage.Server.Plugins/Mage.Deck.Limited/pom.xml b/Mage.Server.Plugins/Mage.Deck.Limited/pom.xml index 3d53e554370..d4dd4f092b5 100644 --- a/Mage.Server.Plugins/Mage.Deck.Limited/pom.xml +++ b/Mage.Server.Plugins/Mage.Deck.Limited/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-deck-limited diff --git a/Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml index b865a976eaa..a9b9a9dff43 100644 --- a/Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.BrawlDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-brawlduel diff --git a/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml index eb5c5913080..924b17f9daf 100644 --- a/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.BrawlFreeForAll/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-brawlfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml index d9defa0ebad..f269b29868d 100644 --- a/Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.CanadianHighlanderDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-canadianhighlanderduel diff --git a/Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml index 55d69cf5c38..18459996ee6 100644 --- a/Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.CommanderDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-commanderduel diff --git a/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml index f5c7b42c0ca..90440eae253 100644 --- a/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.CommanderFreeForAll/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-commanderfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.CustomPillarOfTheParunsDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.CustomPillarOfTheParunsDuel/pom.xml index f2986e22ff7..61c4eee4421 100644 --- a/Mage.Server.Plugins/Mage.Game.CustomPillarOfTheParunsDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.CustomPillarOfTheParunsDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-custompillaroftheparunsduel diff --git a/Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml index 5a9acb210ec..33c2188d519 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.FreeForAll/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-freeforall diff --git a/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml index d68adb9f8a3..3b3b8d02a2c 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.FreeformCommanderDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-freeformcommanderduel diff --git a/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/pom.xml index 1c0016007c7..a8fdeac89a2 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.FreeformCommanderFreeForAll/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-freeformcommanderfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.FreeformUnlimitedCommander/pom.xml b/Mage.Server.Plugins/Mage.Game.FreeformUnlimitedCommander/pom.xml index e0fc0dbc6ad..9db9b8620b5 100644 --- a/Mage.Server.Plugins/Mage.Game.FreeformUnlimitedCommander/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.FreeformUnlimitedCommander/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-freeformunlimitedcommander @@ -23,7 +23,7 @@ org.mage mage-game-freeformcommanderfreeforall - 1.4.56 + 1.4.57 compile diff --git a/Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml index dce83069cad..07d7171de27 100644 --- a/Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.MomirDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-momirduel diff --git a/Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml b/Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml index 0451da80628..8a9d7ea5d46 100644 --- a/Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.MomirGame/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-momirfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml index 1c1fc8cf0cc..4cb658b9b42 100644 --- a/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerDuel/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-oathbreakerduel @@ -22,7 +22,7 @@ org.mage mage-game-oathbreakerfreeforall - 1.4.56 + 1.4.57 compile diff --git a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml index a87f4e138ad..47cb4ce8bb9 100644 --- a/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.OathbreakerFreeForAll/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-oathbreakerfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml index 9e1365bbe12..91a3176b885 100644 --- a/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.PennyDreadfulCommanderFreeForAll/pom.xml @@ -6,7 +6,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-pennydreadfulcommanderfreeforall diff --git a/Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml index bf33a290fbc..0f53a19a16e 100644 --- a/Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.TinyLeadersDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-tinyleadersduel diff --git a/Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml b/Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml index 8a0b37c4bdc..205565820ef 100644 --- a/Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml +++ b/Mage.Server.Plugins/Mage.Game.TwoPlayerDuel/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-game-twoplayerduel diff --git a/Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml b/Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml index 4599885dfe9..71a45bc1577 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AI.DraftBot/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-player-ai-draftbot diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml b/Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml index dc71986f494..5dc6a5b7b7a 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-player-ai-ma diff --git a/Mage.Server.Plugins/Mage.Player.AI/pom.xml b/Mage.Server.Plugins/Mage.Player.AI/pom.xml index 04cf099d905..80eda4b4025 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AI/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-player-ai diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml index d488c1ed04f..51e7608305d 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-player-ai-mcts diff --git a/Mage.Server.Plugins/Mage.Player.Human/pom.xml b/Mage.Server.Plugins/Mage.Player.Human/pom.xml index 930c9c7787c..921c136dd8a 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/pom.xml +++ b/Mage.Server.Plugins/Mage.Player.Human/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-player-human diff --git a/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml b/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml index 520028b4293..8501c5e0d54 100644 --- a/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml +++ b/Mage.Server.Plugins/Mage.Tournament.BoosterDraft/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-tournament-boosterdraft diff --git a/Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml b/Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml index 4ec19f4a47d..e14dff2efdb 100644 --- a/Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml +++ b/Mage.Server.Plugins/Mage.Tournament.Constructed/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-tournament-constructed diff --git a/Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml b/Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml index 3d00fdd278b..e2ad7473e1b 100644 --- a/Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml +++ b/Mage.Server.Plugins/Mage.Tournament.Sealed/pom.xml @@ -7,7 +7,7 @@ org.mage mage-server-plugins - 1.4.56 + 1.4.57 mage-tournament-sealed diff --git a/Mage.Server.Plugins/pom.xml b/Mage.Server.Plugins/pom.xml index b22f00b7348..e216bbc0eea 100644 --- a/Mage.Server.Plugins/pom.xml +++ b/Mage.Server.Plugins/pom.xml @@ -7,7 +7,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-server-plugins diff --git a/Mage.Server/pom.xml b/Mage.Server/pom.xml index 9774edf32ca..ff104474d0e 100644 --- a/Mage.Server/pom.xml +++ b/Mage.Server/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-server diff --git a/Mage.Sets/pom.xml b/Mage.Sets/pom.xml index c55e8bb1d3b..b04a9bb367e 100644 --- a/Mage.Sets/pom.xml +++ b/Mage.Sets/pom.xml @@ -7,7 +7,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-sets diff --git a/Mage.Tests/pom.xml b/Mage.Tests/pom.xml index feba3cf97ca..a99b1948896 100644 --- a/Mage.Tests/pom.xml +++ b/Mage.Tests/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-tests diff --git a/Mage.Verify/pom.xml b/Mage.Verify/pom.xml index 31b9493ede0..9ce6635cd66 100644 --- a/Mage.Verify/pom.xml +++ b/Mage.Verify/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage-verify diff --git a/Mage/pom.xml b/Mage/pom.xml index 6713c55dccb..f148552c45f 100644 --- a/Mage/pom.xml +++ b/Mage/pom.xml @@ -6,7 +6,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 mage diff --git a/pom.xml b/pom.xml index feb6a458482..54f614ad154 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.mage mage-root - 1.4.56 + 1.4.57 pom Mage Root Mage Root POM @@ -16,7 +16,7 @@ ${project.basedir} - 1.4.56 + 1.4.57 -Dfile.encoding=UTF-8 UTF-8 yyyy-MM-dd'T'HH:mm:ss'Z' From 7964b10a4f86dcc9790d29d552ce851412856b2e Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 10:08:06 -0400 Subject: [PATCH 122/133] [TDM] Implement Surrak, Elusive Hunter --- .../src/mage/cards/s/SurrakElusiveHunter.java | 98 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 99 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java diff --git a/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java b/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java new file mode 100644 index 00000000000..0e49ac98e56 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java @@ -0,0 +1,98 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.CantBeCounteredSourceAbility; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SurrakElusiveHunter extends CardImpl { + + public SurrakElusiveHunter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // This spell can't be countered. + this.addAbility(new CantBeCounteredSourceAbility()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Whenever a creature you control or a creature spell you control becomes the target of a spell or ability an opponent controls, draw a card. + this.addAbility(new SurrakElusiveHunterTriggeredAbility()); + } + + private SurrakElusiveHunter(final SurrakElusiveHunter card) { + super(card); + } + + @Override + public SurrakElusiveHunter copy() { + return new SurrakElusiveHunter(this); + } +} + +class SurrakElusiveHunterTriggeredAbility extends TriggeredAbilityImpl { + + SurrakElusiveHunterTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1)); + this.setTriggerPhrase("Whenever a creature you control or a creature spell you control " + + "becomes the target of a spell or ability an opponent controls, "); + } + + private SurrakElusiveHunterTriggeredAbility(final SurrakElusiveHunterTriggeredAbility ability) { + super(ability); + } + + @Override + public SurrakElusiveHunterTriggeredAbility copy() { + return new SurrakElusiveHunterTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.TARGETED; + } + + private boolean checkTargeted(UUID targetId, Game game) { + Permanent permanent = game.getPermanentOrLKIBattlefield(targetId); + if (permanent != null) { + return permanent.isCreature(game) && permanent.isControlledBy(getControllerId()); + } + Spell spell = game.getSpellOrLKIStack(targetId); + return spell != null && spell.isCreature(game) && spell.isControlledBy(getControllerId()); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!checkTargeted(event.getTargetId(), game)) { + return false; + } + StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); + return targetingObject != null + && !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId()) + && !CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game) + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index b6d20f107df..017f3d1b874 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -252,6 +252,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Summit Intimidator", 125, Rarity.COMMON, mage.cards.s.SummitIntimidator.class)); cards.add(new SetCardInfo("Sunpearl Kirin", 29, Rarity.UNCOMMON, mage.cards.s.SunpearlKirin.class)); cards.add(new SetCardInfo("Sunset Strikemaster", 126, Rarity.UNCOMMON, mage.cards.s.SunsetStrikemaster.class)); + cards.add(new SetCardInfo("Surrak, Elusive Hunter", 161, Rarity.RARE, mage.cards.s.SurrakElusiveHunter.class)); cards.add(new SetCardInfo("Swamp", 281, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Swiftwater Cliffs", 268, Rarity.COMMON, mage.cards.s.SwiftwaterCliffs.class)); cards.add(new SetCardInfo("Synchronized Charge", 162, Rarity.UNCOMMON, mage.cards.s.SynchronizedCharge.class)); From efa1cd0a07fbcb163ef068f3de9830b4352d9543 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 10:12:55 -0400 Subject: [PATCH 123/133] fix error --- Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java b/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java index 0e49ac98e56..41916688957 100644 --- a/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java +++ b/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java @@ -93,6 +93,6 @@ class SurrakElusiveHunterTriggeredAbility extends TriggeredAbilityImpl { StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); return targetingObject != null && !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId()) - && !CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game) + && !CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game); } } From 089d8c1be0013c75507dd4e110aa53849081b53e Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Fri, 11 Apr 2025 08:33:49 -0500 Subject: [PATCH 124/133] [TDM] Implement Mistrise Village --- .../src/mage/cards/m/MistriseVillage.java | 112 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + .../cards/single/tdm/MistriseVillageTest.java | 42 +++++++ 3 files changed, 155 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MistriseVillage.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/MistriseVillageTest.java diff --git a/Mage.Sets/src/mage/cards/m/MistriseVillage.java b/Mage.Sets/src/mage/cards/m/MistriseVillage.java new file mode 100644 index 00000000000..ea2132cfd54 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MistriseVillage.java @@ -0,0 +1,112 @@ +package mage.cards.m; + +import java.util.UUID; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.CantBeCounteredSourceAbility; +import mage.abilities.common.EntersBattlefieldTappedUnlessAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.common.YouControlPermanentCondition; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.AddContinuousEffectToGame; +import mage.abilities.effects.common.continuous.NextSpellCastHasAbilityEffect; +import mage.abilities.mana.BlueManaAbility; +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.filter.common.FilterLandPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; + +/** + * + * @author Jmlundeen + */ +public final class MistriseVillage extends CardImpl { + + private static final FilterLandPermanent filter = new FilterLandPermanent("Mountain or a Forest"); + + static { + filter.add(Predicates.or(SubType.MOUNTAIN.getPredicate(), SubType.FOREST.getPredicate())); + } + + private static final YouControlPermanentCondition condition = new YouControlPermanentCondition(filter); + + public MistriseVillage(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + + // This land enters tapped unless you control a Mountain or a Forest. + this.addAbility(new EntersBattlefieldTappedUnlessAbility(condition).addHint(condition.getHint())); + + // {T}: Add {U}. + this.addAbility(new BlueManaAbility()); + + // {U}, {T}: The next spell you cast this turn can't be countered. + Effect effect = new AddContinuousEffectToGame(new MistriseCantBeCounteredEffect()); + Ability ability = new SimpleActivatedAbility(effect, new ManaCostsImpl<>("{U}")); + ability.addCost(new TapSourceCost()); + this.addAbility(ability); + } + + private MistriseVillage(final MistriseVillage card) { + super(card); + } + + @Override + public MistriseVillage copy() { + return new MistriseVillage(this); + } +} + +class MistriseCantBeCounteredEffect extends ContinuousRuleModifyingEffectImpl { + + public MistriseCantBeCounteredEffect() { + super(Duration.OneUse, Outcome.Benefit, false, true); + staticText = "the next spell you cast this turn can't be countered"; + } + + protected MistriseCantBeCounteredEffect(final MistriseCantBeCounteredEffect effect) { + super(effect); + } + + @Override + public MistriseCantBeCounteredEffect copy() { + return new MistriseCantBeCounteredEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.COUNTER; + } + + @Override + public String getInfoMessage(Ability source, GameEvent event, Game game) { + StackObject sourceObject = game.getStack().getStackObject(event.getSourceId()); + StackObject targetObject = game.getStack().getStackObject(event.getTargetId()); + if (sourceObject != null && targetObject != null) { + return targetObject.getName() + " cannot be countered by " + sourceObject.getName(); + } + return staticText; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Spell spell = game.getStack().getSpell(event.getTargetId()); + boolean res = spell != null && spell.isControlledBy(source.getControllerId()); + if (res) { + discard(); + } + return res; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 017f3d1b874..3abb24fc5ae 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -167,6 +167,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Mardu Siegebreaker", 206, Rarity.RARE, mage.cards.m.MarduSiegebreaker.class)); cards.add(new SetCardInfo("Marshal of the Lost", 207, Rarity.UNCOMMON, mage.cards.m.MarshalOfTheLost.class)); cards.add(new SetCardInfo("Meticulous Artisan", 112, Rarity.COMMON, mage.cards.m.MeticulousArtisan.class)); + cards.add(new SetCardInfo("Mistrise Village", 261, Rarity.RARE, mage.cards.m.MistriseVillage.class)); cards.add(new SetCardInfo("Molten Exhale", 113, Rarity.COMMON, mage.cards.m.MoltenExhale.class)); cards.add(new SetCardInfo("Monastery Messenger", 208, Rarity.COMMON, mage.cards.m.MonasteryMessenger.class)); cards.add(new SetCardInfo("Mountain", 283, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/MistriseVillageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/MistriseVillageTest.java new file mode 100644 index 00000000000..98c2cbee505 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/MistriseVillageTest.java @@ -0,0 +1,42 @@ +package org.mage.test.cards.single.tdm; + + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class MistriseVillageTest extends CardTestPlayerBase { + + private static final String MISTRISE = "Mistrise Village"; + private static final String COUNTER = "Counterspell"; + private static final String CUB = "Bear Cub"; + private static final String BEARS = "Balduvian Bears"; + + @Test + public void testCounter() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, MISTRISE); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.BATTLEFIELD, playerB, "Island", 4); + addCard(Zone.HAND, playerB, COUNTER, 2); + addCard(Zone.HAND, playerA, CUB); + addCard(Zone.HAND, playerA, BEARS); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{U}, {T}"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, CUB, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, COUNTER, CUB, CUB); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, BEARS, true); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, COUNTER, BEARS, BEARS); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, CUB, 1); + assertGraveyardCount(playerA, BEARS, 1); + assertGraveyardCount(playerB, COUNTER, 2); + } +} \ No newline at end of file From 5cf0d8ad10b608d7c01b80921d880fed6795956f Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Fri, 11 Apr 2025 07:13:52 -0500 Subject: [PATCH 125/133] [TDM] Implement Hundred-Battle Veteran --- .../mage/cards/h/HundredBattleVeteran.java | 109 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + .../single/tdm/HundredBattleVeteranTest.java | 51 ++++++++ 3 files changed, 161 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HundredBattleVeteran.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/HundredBattleVeteranTest.java diff --git a/Mage.Sets/src/mage/cards/h/HundredBattleVeteran.java b/Mage.Sets/src/mage/cards/h/HundredBattleVeteran.java new file mode 100644 index 00000000000..e56ac094eca --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HundredBattleVeteran.java @@ -0,0 +1,109 @@ +package mage.cards.h; + +import java.util.UUID; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.AbilityImpl; +import mage.abilities.SpellAbility; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.MayCastFromGraveyardSourceAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CastFromGraveyardSourceCondition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.EntersBattlefieldEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.Card; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; + +/** + * + * @author Jmlundeen + */ +public final class HundredBattleVeteran extends CardImpl { + + private static final Hint hint = new ValueHint("different kinds of counters among creatures you control", CounterTypeCount.instance); + + public HundredBattleVeteran(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}"); + + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(4); + this.toughness = new MageInt(2); + + // As long as there are three or more different kinds of counters among creatures you control, this creature gets +2/+4. + Effect effect = new ConditionalContinuousEffect( + new BoostSourceEffect(2, 4, Duration.WhileOnBattlefield), + HundredBattleVeteranCondition.instance, + "As long as there are three or more different kinds of counters among creatures you control, this creature gets +2/+4" + ); + this.addAbility(new SimpleStaticAbility(effect).addHint(hint)); + + // You may cast this card from your graveyard. If you do, it enters with a finality counter on it. + AbilityImpl ability = new MayCastFromGraveyardSourceAbility(); + ability.appendToRule(" If you do, it enters with a finality counter on it."); + Effect effect1 = new AddCountersSourceEffect(CounterType.FINALITY.createInstance(), StaticValue.get(1)); + ability.addSubAbility(new EntersBattlefieldAbility(effect1, CastFromGraveyardSourceCondition.instance, "", "") + .setRuleVisible(false)); + this.addAbility(ability); + + } + + private HundredBattleVeteran(final HundredBattleVeteran card) { + super(card); + } + + @Override + public HundredBattleVeteran copy() { + return new HundredBattleVeteran(this); + } +} + +enum CounterTypeCount implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return game.getBattlefield().getAllActivePermanents(StaticFilters.FILTER_CONTROLLED_CREATURE, sourceAbility.getControllerId(), game) + .stream() + .filter(permanent -> !permanent.getCounters(game).isEmpty()) + .mapToInt(permanent -> permanent.getCounters(game).size()) + .sum(); + } + + @Override + public CounterTypeCount copy() { + return this; + } + + @Override + public String getMessage() { + return "different kinds of counters among creatures you control"; + } +} + +enum HundredBattleVeteranCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + int sum = CounterTypeCount.instance.calculate(game, source, null); + return sum >= 3; + } +} + + diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 3abb24fc5ae..866dfa8c84f 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -129,6 +129,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Hollowmurk Siege", 387, Rarity.RARE, mage.cards.h.HollowmurkSiege.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Host of the Hereafter", 193, Rarity.UNCOMMON, mage.cards.h.HostOfTheHereafter.class)); cards.add(new SetCardInfo("Humbling Elder", 48, Rarity.COMMON, mage.cards.h.HumblingElder.class)); + cards.add(new SetCardInfo("Hundred-Battle Veteran", 82, Rarity.UNCOMMON, mage.cards.h.HundredBattleVeteran.class)); cards.add(new SetCardInfo("Iceridge Serpent", 49, Rarity.COMMON, mage.cards.i.IceridgeSerpent.class)); cards.add(new SetCardInfo("Inevitable Defeat", 194, Rarity.RARE, mage.cards.i.InevitableDefeat.class)); cards.add(new SetCardInfo("Inspirited Vanguard", 146, Rarity.UNCOMMON, mage.cards.i.InspiritedVanguard.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/HundredBattleVeteranTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/HundredBattleVeteranTest.java new file mode 100644 index 00000000000..bab36a7880d --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/HundredBattleVeteranTest.java @@ -0,0 +1,51 @@ +package org.mage.test.cards.single.tdm; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +public class HundredBattleVeteranTest extends CardTestPlayerBase { + + private static final String VETERAN = "Hundred-Battle Veteran"; // 4/2 + private static final String CUB = "Bear Cub"; // 2/2 + + @Test + public void testBoostEffect() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, VETERAN); + addCard(Zone.BATTLEFIELD, playerA, CUB); + + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, CUB, CounterType.FINALITY, 1); + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, CUB, CounterType.P1P1, 3); + checkPT("no boost", 1, PhaseStep.BEGIN_COMBAT, playerA, VETERAN, 4, 2); + addCounters(1, PhaseStep.POSTCOMBAT_MAIN, playerA, CUB, CounterType.LIFELINK, 1); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, CUB, 1); + assertCounterCount(playerA, VETERAN, CounterType.FINALITY, 0); + assertPowerToughness(playerA, VETERAN, 6, 6); + } + + @Test + public void testCastFromGraveyard() { + setStrictChooseMode(true); + + addCard(Zone.GRAVEYARD, playerA, VETERAN); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, VETERAN); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, VETERAN, 1); + assertCounterCount(playerA, VETERAN, CounterType.FINALITY, 1); + } + +} \ No newline at end of file From 3b90fb428ad77ba7de64445f8805a64e9c993e76 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Fri, 11 Apr 2025 09:27:25 -0500 Subject: [PATCH 126/133] [TDM] fix Surrak, Elusive Hunter typo --- Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java b/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java index 41916688957..42b189c9e3c 100644 --- a/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java +++ b/Mage.Sets/src/mage/cards/s/SurrakElusiveHunter.java @@ -92,7 +92,7 @@ class SurrakElusiveHunterTriggeredAbility extends TriggeredAbilityImpl { } StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game); return targetingObject != null - && !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId()) + && game.getOpponents(getControllerId()).contains(targetingObject.getControllerId()) && !CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game); } } From 42c4aa8d2f7df4c405c308bc46905ae3fc78974b Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 10:54:20 -0400 Subject: [PATCH 127/133] [TDC] Implement Bone Devourer --- Mage.Sets/src/mage/cards/b/BoneDevourer.java | 63 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 64 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BoneDevourer.java diff --git a/Mage.Sets/src/mage/cards/b/BoneDevourer.java b/Mage.Sets/src/mage/cards/b/BoneDevourer.java new file mode 100644 index 00000000000..79921a60ede --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BoneDevourer.java @@ -0,0 +1,63 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CountersSourceCount; +import mage.abilities.dynamicvalue.common.CreaturesDiedThisTurnCount; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.common.CreaturesDiedThisTurnHint; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BoneDevourer extends CardImpl { + + private static final DynamicValue xValue = new CountersSourceCount(null); + + public BoneDevourer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // This creature enters with a number of +1/+1 counters on it equal to the number of creatures that died this turn. + this.addAbility(new EntersBattlefieldAbility(new AddCountersSourceEffect( + CounterType.P1P1.createInstance(), CreaturesDiedThisTurnCount.instance, false + ), "with a number of +1/+1 counters on it equal to the number of creatures that died this turn").addHint(CreaturesDiedThisTurnHint.instance)); + + // When this creature dies, you draw X cards and you lose X life, where X is the number of +1/+1 counters on it. + Ability ability = new DiesSourceTriggeredAbility(new DrawCardSourceControllerEffect(xValue).setText("you draw X cards")); + ability.addEffect(new LoseLifeSourceControllerEffect(xValue)); + this.addAbility(ability); + } + + private BoneDevourer(final BoneDevourer card) { + super(card); + } + + @Override + public BoneDevourer copy() { + return new BoneDevourer(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index b297edccf64..7a7a4d12ae5 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -58,6 +58,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Blasphemous Act", 207, Rarity.RARE, mage.cards.b.BlasphemousAct.class)); cards.add(new SetCardInfo("Blight Pile", 174, Rarity.UNCOMMON, mage.cards.b.BlightPile.class)); cards.add(new SetCardInfo("Bojuka Bog", 341, Rarity.COMMON, mage.cards.b.BojukaBog.class)); + cards.add(new SetCardInfo("Bone Devourer", 26, Rarity.RARE, mage.cards.b.BoneDevourer.class)); cards.add(new SetCardInfo("Boros Signet", 314, Rarity.UNCOMMON, mage.cards.b.BorosSignet.class)); cards.add(new SetCardInfo("Bountiful Landscape", 342, Rarity.COMMON, mage.cards.b.BountifulLandscape.class)); cards.add(new SetCardInfo("Caldera Pyremaw", 33, Rarity.RARE, mage.cards.c.CalderaPyremaw.class)); From d89735034e2537e4bcefb8325c358f0ce9bbc644 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 11:05:26 -0400 Subject: [PATCH 128/133] [TDC] Implement Transforming Flourish --- .../mage/cards/t/TransformingFlourish.java | 97 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 98 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TransformingFlourish.java diff --git a/Mage.Sets/src/mage/cards/t/TransformingFlourish.java b/Mage.Sets/src/mage/cards/t/TransformingFlourish.java new file mode 100644 index 00000000000..d11ff2b4f88 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TransformingFlourish.java @@ -0,0 +1,97 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.DemonstrateAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TransformingFlourish extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("artifact or creature you don't control"); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate() + )); + filter.add(TargetController.NOT_YOU.getControllerPredicate()); + } + + public TransformingFlourish(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{R}"); + + // Demonstrate + this.addAbility(new DemonstrateAbility()); + + // Destroy target artifact or creature you don't control. If that permanent is destroyed this way, its controller exiles cards from the top of their library until they exile a nonland card, then they may cast that card without paying its mana cost. + this.getSpellAbility().addEffect(new TransformingFlourishEffect()); + this.getSpellAbility().addTarget(new TargetPermanent(filter)); + } + + private TransformingFlourish(final TransformingFlourish card) { + super(card); + } + + @Override + public TransformingFlourish copy() { + return new TransformingFlourish(this); + } +} + +class TransformingFlourishEffect extends OneShotEffect { + + TransformingFlourishEffect() { + super(Outcome.Benefit); + staticText = "destroy target artifact or creature you don't control. " + + "If that permanent is destroyed this way, its controller exiles cards " + + "from the top of their library until they exile a nonland card, " + + "then they may cast that card without paying its mana cost"; + } + + private TransformingFlourishEffect(final TransformingFlourishEffect effect) { + super(effect); + } + + @Override + public TransformingFlourishEffect copy() { + return new TransformingFlourishEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null || !permanent.destroy(source, game)) { + return false; + } + Player player = game.getPlayer(permanent.getControllerId()); + if (player == null || !player.getLibrary().hasCards()) { + return true; + } + for (Card card : player.getLibrary().getCards(game)) { + player.moveCards(card, Zone.EXILED, source, game); + if (!card.isLand(game)) { + CardUtil.castSpellWithAttributesForFree(player, source, game, card); + return true; + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 7a7a4d12ae5..19e93b61224 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -336,6 +336,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Tocasia's Welcome", 135, Rarity.RARE, mage.cards.t.TocasiasWelcome.class)); cards.add(new SetCardInfo("Tower Defense", 275, Rarity.UNCOMMON, mage.cards.t.TowerDefense.class)); cards.add(new SetCardInfo("Towering Titan", 276, Rarity.MYTHIC, mage.cards.t.ToweringTitan.class)); + cards.add(new SetCardInfo("Transforming Flourish", 39, Rarity.RARE, mage.cards.t.TransformingFlourish.class)); cards.add(new SetCardInfo("Treasure Cruise", 169, Rarity.COMMON, mage.cards.t.TreasureCruise.class)); cards.add(new SetCardInfo("Tree of Redemption", 97, Rarity.MYTHIC, mage.cards.t.TreeOfRedemption.class)); cards.add(new SetCardInfo("Twilight Drover", 136, Rarity.RARE, mage.cards.t.TwilightDrover.class)); From 3afc233c6cd38abd9d842e596c45ee950ec3b39f Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 11:49:06 -0400 Subject: [PATCH 129/133] [TDC] Implement Will of the Temur --- .../src/mage/cards/w/WillOfTheTemur.java | 132 ++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 133 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WillOfTheTemur.java diff --git a/Mage.Sets/src/mage/cards/w/WillOfTheTemur.java b/Mage.Sets/src/mage/cards/w/WillOfTheTemur.java new file mode 100644 index 00000000000..5588d9bf35d --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WillOfTheTemur.java @@ -0,0 +1,132 @@ +package mage.cards.w; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.condition.common.ControlACommanderCondition; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.abilities.effects.common.DrawCardTargetEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; +import mage.target.TargetPlayer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WillOfTheTemur extends CardImpl { + + public WillOfTheTemur(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{5}{U}"); + + // Choose one. If you control a commander as you cast this spell, you may choose both instead. + this.getSpellAbility().getModes().setChooseText( + "Choose one. If you control a commander as you cast this spell, you may choose both instead." + ); + this.getSpellAbility().getModes().setMoreCondition(2, ControlACommanderCondition.instance); + + // * Create a token that's a copy of target permanent, except it's a 4/4 Dragon creature with flying in addition to its other types. + this.getSpellAbility().addEffect(new WillOfTheTemurEffect()); + this.getSpellAbility().addTarget(new TargetPermanent()); + + // * Target player draws cards equal to the greatest mana value among permanents you control. + this.getSpellAbility().addMode(new Mode(new DrawCardTargetEffect(WillOfTheTemurValue.instance) + .setText("target player draws cards equal to the greatest mana value among permanents you control") + ).addTarget(new TargetPlayer())); + this.getSpellAbility().addHint(WillOfTheTemurValue.getHint()); + } + + private WillOfTheTemur(final WillOfTheTemur card) { + super(card); + } + + @Override + public WillOfTheTemur copy() { + return new WillOfTheTemur(this); + } +} + +enum WillOfTheTemurValue implements DynamicValue { + instance; + private static final Hint hint = new ValueHint("Greatest mana value among permanents you control", instance); + + public static Hint getHint() { + return hint; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return game + .getBattlefield() + .getActivePermanents( + StaticFilters.FILTER_CONTROLLED_PERMANENT, + sourceAbility.getControllerId(), sourceAbility, game + ) + .stream() + .mapToInt(MageObject::getManaValue) + .max() + .orElse(0); + } + + @Override + public WillOfTheTemurValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } + + @Override + public String toString() { + return "1"; + } +} + +class WillOfTheTemurEffect extends OneShotEffect { + + WillOfTheTemurEffect() { + super(Outcome.Benefit); + staticText = "create a token that's a copy of target permanent, " + + "except it's a 4/4 Dragon creature with flying in addition to its other types"; + } + + private WillOfTheTemurEffect(final WillOfTheTemurEffect effect) { + super(effect); + } + + @Override + public WillOfTheTemurEffect copy() { + return new WillOfTheTemurEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + return new CreateTokenCopyTargetEffect().setPermanentModifier(token -> { + token.addCardType(CardType.CREATURE); + token.addSubType(SubType.DRAGON); + token.setPower(4); + token.setToughness(4); + token.addAbility(FlyingAbility.getInstance()); + }).setSavedPermanent(permanent).apply(game, source); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 19e93b61224..adb2ba286e6 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -362,6 +362,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Welcome the Dead", 30, Rarity.RARE, mage.cards.w.WelcomeTheDead.class)); cards.add(new SetCardInfo("Welcoming Vampire", 140, Rarity.RARE, mage.cards.w.WelcomingVampire.class)); cards.add(new SetCardInfo("Whirlwind of Thought", 311, Rarity.RARE, mage.cards.w.WhirlwindOfThought.class)); + cards.add(new SetCardInfo("Will of the Temur", 24, Rarity.RARE, mage.cards.w.WillOfTheTemur.class)); cards.add(new SetCardInfo("Windbrisk Heights", 411, Rarity.RARE, mage.cards.w.WindbriskHeights.class)); cards.add(new SetCardInfo("Wingmantle Chaplain", 141, Rarity.UNCOMMON, mage.cards.w.WingmantleChaplain.class)); cards.add(new SetCardInfo("Within Range", 32, Rarity.RARE, mage.cards.w.WithinRange.class)); From 69a8a1c041c9bd29dc8e3c301f10c61815d5d6f3 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 11:52:32 -0400 Subject: [PATCH 130/133] [TDC] Implement Will of the Sultai --- .../src/mage/cards/w/WillOfTheSultai.java | 57 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 58 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WillOfTheSultai.java diff --git a/Mage.Sets/src/mage/cards/w/WillOfTheSultai.java b/Mage.Sets/src/mage/cards/w/WillOfTheSultai.java new file mode 100644 index 00000000000..fa49ce6513b --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WillOfTheSultai.java @@ -0,0 +1,57 @@ +package mage.cards.w; + +import mage.abilities.Mode; +import mage.abilities.condition.common.ControlACommanderCondition; +import mage.abilities.dynamicvalue.common.LandsYouControlCount; +import mage.abilities.effects.common.MillCardsTargetEffect; +import mage.abilities.effects.common.ReturnFromYourGraveyardToBattlefieldAllEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.hint.common.LandsYouControlHint; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.target.TargetPlayer; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WillOfTheSultai extends CardImpl { + + public WillOfTheSultai(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{G}"); + + // Choose one. If you control a commander as you cast this spell, you may choose both instead. + this.getSpellAbility().getModes().setChooseText( + "Choose one. If you control a commander as you cast this spell, you may choose both instead." + ); + this.getSpellAbility().getModes().setMoreCondition(2, ControlACommanderCondition.instance); + + // * Target player mills three cards. Return all land cards from your graveyard to the battlefield tapped. + this.getSpellAbility().addEffect(new MillCardsTargetEffect(3)); + this.getSpellAbility().addTarget(new TargetPlayer()); + this.getSpellAbility().addEffect(new ReturnFromYourGraveyardToBattlefieldAllEffect(StaticFilters.FILTER_CARD_LANDS)); + + // * Put X +1/+1 counters on target creature, where X is the number of lands you control. It gains trample until end of turn. + this.getSpellAbility().addMode(new Mode(new AddCountersTargetEffect( + CounterType.P1P1.createInstance(0), LandsYouControlCount.instance + )).addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance()) + .setText("it gains trample until end of turn")).addTarget(new TargetCreaturePermanent())); + this.getSpellAbility().addHint(LandsYouControlHint.instance); + } + + private WillOfTheSultai(final WillOfTheSultai card) { + super(card); + } + + @Override + public WillOfTheSultai copy() { + return new WillOfTheSultai(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index adb2ba286e6..022875588b2 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -362,6 +362,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Welcome the Dead", 30, Rarity.RARE, mage.cards.w.WelcomeTheDead.class)); cards.add(new SetCardInfo("Welcoming Vampire", 140, Rarity.RARE, mage.cards.w.WelcomingVampire.class)); cards.add(new SetCardInfo("Whirlwind of Thought", 311, Rarity.RARE, mage.cards.w.WhirlwindOfThought.class)); + cards.add(new SetCardInfo("Will of the Sultai", 49, Rarity.RARE, mage.cards.w.WillOfTheSultai.class)); cards.add(new SetCardInfo("Will of the Temur", 24, Rarity.RARE, mage.cards.w.WillOfTheTemur.class)); cards.add(new SetCardInfo("Windbrisk Heights", 411, Rarity.RARE, mage.cards.w.WindbriskHeights.class)); cards.add(new SetCardInfo("Wingmantle Chaplain", 141, Rarity.UNCOMMON, mage.cards.w.WingmantleChaplain.class)); From 1ace9580577683ad3080b01fdf59a3a602cb9bb9 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 12:19:45 -0400 Subject: [PATCH 131/133] [TDC] Implement Broodcaller Scourge --- .../src/mage/cards/b/BroodcallerScourge.java | 76 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + Mage/src/main/java/mage/util/CardUtil.java | 13 ++++ 3 files changed, 90 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BroodcallerScourge.java diff --git a/Mage.Sets/src/mage/cards/b/BroodcallerScourge.java b/Mage.Sets/src/mage/cards/b/BroodcallerScourge.java new file mode 100644 index 00000000000..4a8145bbece --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BroodcallerScourge.java @@ -0,0 +1,76 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.common.OneOrMoreDamagePlayerTriggeredAbility; +import mage.abilities.effects.common.PutCardFromHandOntoBattlefieldEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.common.FilterPermanentCard; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BroodcallerScourge extends CardImpl { + + private static final FilterCard filter + = new FilterPermanentCard("a permanent card with mana value less than or equal to that damage"); + private static final FilterCreaturePermanent filter2 = new FilterCreaturePermanent(SubType.DRAGON, "Dragons"); + + static { + filter.add(BroodcallerScourgePredicate.instance); + } + + public BroodcallerScourge(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{G}{G}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(5); + this.toughness = new MageInt(7); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever one or more Dragons you control deal combat damage to a player, you may put a permanent card with mana value less than or equal to that damage from your hand onto the battlefield. + this.addAbility(new OneOrMoreDamagePlayerTriggeredAbility( + new PutCardFromHandOntoBattlefieldEffect(filter), + filter2, true, true + )); + } + + private BroodcallerScourge(final BroodcallerScourge card) { + super(card); + } + + @Override + public BroodcallerScourge copy() { + return new BroodcallerScourge(this); + } +} + +enum BroodcallerScourgePredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return input + .getObject() + .getManaValue() + <= CardUtil + .getEffectValueFromAbility( + input.getSource(), "damage", + Integer.class, 0 + ); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 022875588b2..6d0138635c2 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -61,6 +61,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Bone Devourer", 26, Rarity.RARE, mage.cards.b.BoneDevourer.class)); cards.add(new SetCardInfo("Boros Signet", 314, Rarity.UNCOMMON, mage.cards.b.BorosSignet.class)); cards.add(new SetCardInfo("Bountiful Landscape", 342, Rarity.COMMON, mage.cards.b.BountifulLandscape.class)); + cards.add(new SetCardInfo("Broodcaller Scourge", 44, Rarity.RARE, mage.cards.b.BroodcallerScourge.class)); cards.add(new SetCardInfo("Caldera Pyremaw", 33, Rarity.RARE, mage.cards.c.CalderaPyremaw.class)); cards.add(new SetCardInfo("Canopy Gargantuan", 45, Rarity.RARE, mage.cards.c.CanopyGargantuan.class)); cards.add(new SetCardInfo("Canopy Vista", 343, Rarity.RARE, mage.cards.c.CanopyVista.class)); diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 4356de3d90f..eb2a2e8d513 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -2236,6 +2236,19 @@ public final class CardUtil { return sb.toString(); } + public static T getEffectValueFromAbility(Ability ability, String key, Class clazz) { + return getEffectValueFromAbility(ability, key, clazz, null); + } + + public static T getEffectValueFromAbility(Ability ability, String key, Class clazz, T defaultValue) { + return castStream( + ability.getAllEffects() + .stream() + .map(effect -> effect.getValue(key)), + clazz + ).findFirst().orElse(defaultValue); + } + public static Stream castStream(Collection collection, Class clazz) { return castStream(collection.stream(), clazz); } From 69710cac3573feb5a0b86824e6d84042254ef173 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 12:27:46 -0400 Subject: [PATCH 132/133] [TDC] Implement Will of the Abzan --- .../src/mage/cards/w/WillOfTheAbzan.java | 61 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + 2 files changed, 62 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WillOfTheAbzan.java diff --git a/Mage.Sets/src/mage/cards/w/WillOfTheAbzan.java b/Mage.Sets/src/mage/cards/w/WillOfTheAbzan.java new file mode 100644 index 00000000000..3bbcf532696 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WillOfTheAbzan.java @@ -0,0 +1,61 @@ +package mage.cards.w; + +import mage.abilities.Mode; +import mage.abilities.condition.common.ControlACommanderCondition; +import mage.abilities.effects.common.LoseLifeTargetControllerEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.effects.common.SacrificeEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.permanent.GreatestPowerControlledPredicate; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WillOfTheAbzan extends CardImpl { + + private static final FilterPermanent filter + = new FilterControlledCreaturePermanent("a creature with the greatest power among creatures you control"); + + static { + filter.add(GreatestPowerControlledPredicate.instance); + } + + public WillOfTheAbzan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}"); + + // Choose one. If you control a commander as you cast this spell, you may choose both instead. + this.getSpellAbility().getModes().setChooseText( + "Choose one. If you control a commander as you cast this spell, you may choose both instead." + ); + this.getSpellAbility().getModes().setMoreCondition(2, ControlACommanderCondition.instance); + + // * Any number of target opponents each sacrifice a creature with the greatest power among creatures that player controls and lose 3 life. + this.getSpellAbility().addEffect(new SacrificeEffect(filter, 1, "") + .setText("any number of target opponents each sacrifice a creature " + + "with the greatest power among creatures that player controls")); + this.getSpellAbility().addEffect(new LoseLifeTargetControllerEffect(3).setText("and lose 3 life")); + this.getSpellAbility().addTarget(new TargetOpponent(0, 1, false)); + + // * Return target creature card from your graveyard to the battlefield. + this.getSpellAbility().addMode(new Mode(new ReturnFromGraveyardToBattlefieldTargetEffect()) + .addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD))); + } + + private WillOfTheAbzan(final WillOfTheAbzan card) { + super(card); + } + + @Override + public WillOfTheAbzan copy() { + return new WillOfTheAbzan(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index 6d0138635c2..277b0c89500 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -363,6 +363,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { cards.add(new SetCardInfo("Welcome the Dead", 30, Rarity.RARE, mage.cards.w.WelcomeTheDead.class)); cards.add(new SetCardInfo("Welcoming Vampire", 140, Rarity.RARE, mage.cards.w.WelcomingVampire.class)); cards.add(new SetCardInfo("Whirlwind of Thought", 311, Rarity.RARE, mage.cards.w.WhirlwindOfThought.class)); + cards.add(new SetCardInfo("Will of the Abzan", 31, Rarity.RARE, mage.cards.w.WillOfTheAbzan.class)); cards.add(new SetCardInfo("Will of the Sultai", 49, Rarity.RARE, mage.cards.w.WillOfTheSultai.class)); cards.add(new SetCardInfo("Will of the Temur", 24, Rarity.RARE, mage.cards.w.WillOfTheTemur.class)); cards.add(new SetCardInfo("Windbrisk Heights", 411, Rarity.RARE, mage.cards.w.WindbriskHeights.class)); From f76c3943dc4166f8c53cbd211ccd425bbf67d1fe Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 11 Apr 2025 15:25:13 -0400 Subject: [PATCH 133/133] [TDM] fix Kotis, the Fangkeeper ability --- Mage.Sets/src/mage/cards/k/KotisTheFangkeeper.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/k/KotisTheFangkeeper.java b/Mage.Sets/src/mage/cards/k/KotisTheFangkeeper.java index 131dce312ea..ff609acbfcb 100644 --- a/Mage.Sets/src/mage/cards/k/KotisTheFangkeeper.java +++ b/Mage.Sets/src/mage/cards/k/KotisTheFangkeeper.java @@ -3,7 +3,6 @@ package mage.cards.k; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; -import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.IndestructibleAbility; import mage.cards.CardImpl; @@ -73,7 +72,7 @@ class KotisTheFangkeeperEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(getTargetPointer().getFirst(game, source)); - int xValue = GetXValue.instance.calculate(game, source, this); + int xValue = (Integer) getValue("damage"); if (controller == null || player == null || xValue < 1) { return false; }