From 158212bbebb56525558619d40584f8ab9ddcbd54 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:57:26 -0600 Subject: [PATCH] refactor: create common DynamicValue for instant and sorcery spells cast in a turn (#13374) --- Mage.Sets/src/mage/cards/d/Demilich.java | 38 +--------- Mage.Sets/src/mage/cards/l/LockAndLoad.java | 48 +----------- .../src/mage/cards/r/RalLeylineProdigy.java | 42 ++-------- .../src/mage/cards/r/RalMonsoonMage.java | 4 +- .../src/mage/cards/r/RionyaFireDancer.java | 60 +-------------- .../src/mage/cards/s/ShowOfConfidence.java | 14 +--- Mage.Sets/src/mage/cards/s/SorcererClass.java | 42 +--------- .../common/InstantAndSorceryCastThisTurn.java | 76 +++++++++++++++++++ 8 files changed, 103 insertions(+), 221 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/dynamicvalue/common/InstantAndSorceryCastThisTurn.java diff --git a/Mage.Sets/src/mage/cards/d/Demilich.java b/Mage.Sets/src/mage/cards/d/Demilich.java index 329903847a3..df95d282491 100644 --- a/Mage.Sets/src/mage/cards/d/Demilich.java +++ b/Mage.Sets/src/mage/cards/d/Demilich.java @@ -10,21 +10,17 @@ import mage.abilities.costs.Costs; import mage.abilities.costs.CostsImpl; import mage.abilities.costs.common.ExileFromGraveCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.Effect; import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; -import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.stack.Spell; import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; -import mage.watchers.common.SpellsCastWatcher; import java.util.UUID; @@ -43,8 +39,8 @@ public final class Demilich extends CardImpl { // This spell costs {U} less to cast for each instant and sorcery you've cast this turn. this.addAbility(new SimpleStaticAbility(Zone.ALL, new SpellCostReductionForEachSourceEffect( - new ManaCostsImpl<>("{U}"), DemilichValue.instance - )).addHint(new ValueHint("Instants and sorceries you've cast this turn", DemilichValue.instance))); + new ManaCostsImpl<>("{U}"), InstantAndSorceryCastThisTurn.YOU + )).addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); // Whenever Demilich attacks, exile up to one target instant or sorcery card from your graveyard. Copy it. You may cast the copy. Ability ability = new AttacksTriggeredAbility(new ExileTargetCardCopyAndCastEffect(false).setText( @@ -66,34 +62,6 @@ public final class Demilich extends CardImpl { } } -enum DemilichValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - int spells = 0; - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher != null) { - for (Spell spell : watcher.getSpellsCastThisTurn(sourceAbility.getControllerId())) { - if (spell.isInstantOrSorcery(game)) { - spells++; - } - } - } - return spells; - } - - @Override - public DemilichValue copy() { - return instance; - } - - @Override - public String getMessage() { - return "instant and sorcery spell you've cast this turn"; - } -} - class DemilichPlayEffect extends AsThoughEffectImpl { DemilichPlayEffect() { diff --git a/Mage.Sets/src/mage/cards/l/LockAndLoad.java b/Mage.Sets/src/mage/cards/l/LockAndLoad.java index c249aec13ac..0d92c96e37a 100644 --- a/Mage.Sets/src/mage/cards/l/LockAndLoad.java +++ b/Mage.Sets/src/mage/cards/l/LockAndLoad.java @@ -1,18 +1,12 @@ package mage.cards.l; -import mage.abilities.Ability; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.dynamicvalue.IntPlusDynamicValue; -import mage.abilities.effects.Effect; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.keyword.PlotAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.game.Game; -import mage.watchers.common.SpellsCastWatcher; -import java.util.Objects; import java.util.UUID; /** @@ -20,15 +14,14 @@ import java.util.UUID; */ public final class LockAndLoad extends CardImpl { - private static final DynamicValue xValue = new IntPlusDynamicValue(1, LockAndLoadValue.instance); - public LockAndLoad(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{U}"); // Draw a card, then draw a card for each other instant and sorcery spell you've cast this turn. - this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(xValue) + this.getSpellAbility() + .addHint(InstantAndSorceryCastThisTurn.YOU.getHint()) + .addEffect(new DrawCardSourceControllerEffect(InstantAndSorceryCastThisTurn.YOU) .setText("Draw a card, then draw a card for each other instant and sorcery spell you've cast this turn")); - // Plot {3}{U} this.addAbility(new PlotAbility("{3}{U}")); } @@ -41,37 +34,4 @@ public final class LockAndLoad extends CardImpl { public LockAndLoad copy() { return new LockAndLoad(this); } -} - -enum LockAndLoadValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - return watcher == null ? 0 : - watcher.getSpellsCastThisTurn(sourceAbility.getControllerId()) - .stream() - .filter(Objects::nonNull) - .filter(s -> s.isInstantOrSorcery(game)) - .filter(s -> !s.getSourceId().equals(sourceAbility.getSourceId()) - || s.getZoneChangeCounter(game) != sourceAbility.getSourceObjectZoneChangeCounter()) - .mapToInt(x -> 1) - .sum(); - } - - @Override - public LockAndLoadValue copy() { - return this; - } - - @Override - public String toString() { - return "X"; - } - - @Override - public String getMessage() { - return "Number of other instant and sorcery spell you've cast this turn"; - } } \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RalLeylineProdigy.java b/Mage.Sets/src/mage/cards/r/RalLeylineProdigy.java index d5bd0c3d84c..a900ac858f3 100644 --- a/Mage.Sets/src/mage/cards/r/RalLeylineProdigy.java +++ b/Mage.Sets/src/mage/cards/r/RalLeylineProdigy.java @@ -10,9 +10,8 @@ import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.condition.Condition; import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; import mage.abilities.decorator.ConditionalOneShotEffect; -import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DamageMultiEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; @@ -34,9 +33,7 @@ import mage.game.Game; import mage.players.Player; import mage.target.common.TargetAnyTargetAmount; import mage.util.CardUtil; -import mage.watchers.common.SpellsCastWatcher; -import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -67,9 +64,11 @@ public final class RalLeylineProdigy extends CardImpl { // Ral, Leyline Prodigy enters the battlefield with an additional loyalty counter on him for each instant and sorcery spell you've cast this turn. this.addAbility(new EntersBattlefieldAbility( - new AddCountersSourceEffect(CounterType.LOYALTY.createInstance(), RalLeylineProdigyValue.instance, false) - .setText("with an additional loyalty counter on him for each instant and sorcery spell you've cast this turn") - )); + new AddCountersSourceEffect(CounterType.LOYALTY.createInstance(), InstantAndSorceryCastThisTurn.YOU, + false) + .setText("with an additional loyalty counter on him for each instant and sorcery spell you've cast this turn")) + .addHint(InstantAndSorceryCastThisTurn.YOU.getHint()) + ); // +1: Until your next turn, instant and sorcery spells you cast cost {1} less to cast. this.addAbility(new LoyaltyAbility(new RalLeylineProdigyCostReductionEffect(), 1)); @@ -126,35 +125,6 @@ class RalLeylineProdigyCostReductionEffect extends OneShotEffect { } } -enum RalLeylineProdigyValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher == null) { - return 0; - } - return watcher - .getSpellsCastThisTurn(sourceAbility.getControllerId()) - .stream() - .filter(Objects::nonNull) - .filter(spell -> spell.isInstantOrSorcery(game)) - .mapToInt(spell -> 1) - .sum(); - } - - @Override - public RalLeylineProdigyValue copy() { - return instance; - } - - @Override - public String getMessage() { - return "instant and sorcery spell you've cast this turn"; - } -} - class RalLeylineProdigyMinusEightEffect extends OneShotEffect { RalLeylineProdigyMinusEightEffect() { diff --git a/Mage.Sets/src/mage/cards/r/RalMonsoonMage.java b/Mage.Sets/src/mage/cards/r/RalMonsoonMage.java index 01d6a767f89..c579098b09c 100644 --- a/Mage.Sets/src/mage/cards/r/RalMonsoonMage.java +++ b/Mage.Sets/src/mage/cards/r/RalMonsoonMage.java @@ -2,6 +2,7 @@ package mage.cards.r; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.constants.Pronoun; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility; @@ -45,7 +46,8 @@ public final class RalMonsoonMage extends CardImpl { // Whenever you cast an instant or sorcery spell during your turn, flip a coin. If you lose the flip, Ral, Monsoon Mage deals 1 damage to you. If you win the flip, you may exile Ral. If you do, return him to the battlefield transformed under his owner control. this.addAbility(new TransformAbility()); - this.addAbility(new RalMonsoonMageTriggeredAbility()); + this.addAbility(new RalMonsoonMageTriggeredAbility() + .addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); } private RalMonsoonMage(final RalMonsoonMage card) { diff --git a/Mage.Sets/src/mage/cards/r/RionyaFireDancer.java b/Mage.Sets/src/mage/cards/r/RionyaFireDancer.java index f60d70ea09a..5dc76c5a02c 100644 --- a/Mage.Sets/src/mage/cards/r/RionyaFireDancer.java +++ b/Mage.Sets/src/mage/cards/r/RionyaFireDancer.java @@ -2,23 +2,17 @@ package mage.cards.r; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; -import mage.abilities.hint.Hint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.stack.Spell; import mage.target.TargetPermanent; -import mage.util.CardUtil; -import mage.watchers.Watcher; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -40,7 +34,7 @@ public final class RionyaFireDancer extends CardImpl { new RionyaFireDancerEffect() ); ability.addTarget(new TargetPermanent(StaticFilters.FILTER_ANOTHER_CREATURE_YOU_CONTROL)); - this.addAbility(ability.addHint(RionyaFireDancerHint.instance), new RionyaFireDancerWatcher()); + this.addAbility(ability.addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); } private RionyaFireDancer(final RionyaFireDancer card) { @@ -53,21 +47,6 @@ public final class RionyaFireDancer extends CardImpl { } } -enum RionyaFireDancerHint implements Hint { - instance; - - @Override - public String getText(Game game, Ability ability) { - return "Instants and sorceries you've cast this turn: " - + RionyaFireDancerWatcher.getValue(ability.getControllerId(), game); - } - - @Override - public RionyaFireDancerHint copy() { - return instance; - } -} - class RionyaFireDancerEffect extends OneShotEffect { RionyaFireDancerEffect() { @@ -90,41 +69,10 @@ class RionyaFireDancerEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect( source.getControllerId(), null, true, - RionyaFireDancerWatcher.getValue(source.getControllerId(), game) + 1 + InstantAndSorceryCastThisTurn.YOU.calculate(game, source, this) + 1 ); effect.apply(game, source); effect.exileTokensCreatedAtNextEndStep(game, source); return true; } -} - -class RionyaFireDancerWatcher extends Watcher { - - private final Map playerMap = new HashMap<>(); - - RionyaFireDancerWatcher() { - 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 && spell.isInstantOrSorcery(game)) { - playerMap.compute(spell.getControllerId(), CardUtil::setOrIncrementValue); - } - } - - @Override - public void reset() { - super.reset(); - playerMap.clear(); - } - - static int getValue(UUID playerId, Game game) { - RionyaFireDancerWatcher watcher = game.getState().getWatcher(RionyaFireDancerWatcher.class); - return watcher == null ? 0 : watcher.playerMap.getOrDefault(playerId, 0); - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/ShowOfConfidence.java b/Mage.Sets/src/mage/cards/s/ShowOfConfidence.java index 3672f5637ae..a9c501e41e0 100644 --- a/Mage.Sets/src/mage/cards/s/ShowOfConfidence.java +++ b/Mage.Sets/src/mage/cards/s/ShowOfConfidence.java @@ -1,6 +1,7 @@ package mage.cards.s; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CastSourceTriggeredAbility; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; @@ -17,7 +18,6 @@ import mage.game.stack.Spell; import mage.target.common.TargetCreaturePermanent; import mage.watchers.common.SpellsCastWatcher; -import java.util.Objects; import java.util.UUID; /** @@ -29,7 +29,8 @@ public final class ShowOfConfidence extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}"); // When you cast this spell, copy it for each other instant or sorcery spell you've cast this turn. You may choose new targets for the copies. - this.addAbility(new CastSourceTriggeredAbility(new ShowOfConfidenceEffect())); + this.addAbility(new CastSourceTriggeredAbility(new ShowOfConfidenceEffect()) + .addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); // Put a +1/+1 counter on target creature. It gains vigilance until end of turn. this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance())); @@ -73,14 +74,7 @@ class ShowOfConfidenceEffect extends OneShotEffect { if (spell == null || watcher == null) { return false; } - int copies = watcher.getSpellsCastThisTurn(source.getControllerId()) - .stream() - .filter(Objects::nonNull) - .filter(spell1 -> spell1.isInstantOrSorcery(game)) - .filter(s -> !s.getSourceId().equals(source.getSourceId()) - || s.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter()) - .mapToInt(x -> 1) - .sum(); + int copies = InstantAndSorceryCastThisTurn.YOU.calculate(game, source, this) - 1; if (copies > 0) { spell.createCopyOnStack(game, source, source.getControllerId(), true, copies); } diff --git a/Mage.Sets/src/mage/cards/s/SorcererClass.java b/Mage.Sets/src/mage/cards/s/SorcererClass.java index e46d532f5a8..b1d333628b6 100644 --- a/Mage.Sets/src/mage/cards/s/SorcererClass.java +++ b/Mage.Sets/src/mage/cards/s/SorcererClass.java @@ -10,6 +10,7 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility; import mage.abilities.condition.Condition; import mage.abilities.costs.Cost; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DrawDiscardControllerEffect; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; @@ -24,14 +25,9 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.events.GameEvent; import mage.game.stack.Spell; import mage.players.Player; -import mage.util.CardUtil; -import mage.watchers.Watcher; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -76,7 +72,7 @@ public final class SorcererClass extends CardImpl { StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false, SetTargetPointer.SPELL ), 3 - )), new SorcererClassWatcher()); + )).addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); } private SorcererClass(final SorcererClass card) { @@ -151,7 +147,7 @@ class SorcererClassEffect extends OneShotEffect { if (spell == null) { return false; } - int count = SorcererClassWatcher.spellCount(source.getControllerId(), game); + int count = InstantAndSorceryCastThisTurn.YOU.calculate(game, source, this); if (count < 1) { return false; } @@ -165,35 +161,3 @@ class SorcererClassEffect extends OneShotEffect { return true; } } - -class SorcererClassWatcher extends Watcher { - - private final Map spellMap = new HashMap<>(); - - SorcererClassWatcher() { - 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 || !spell.isInstantOrSorcery(game)) { - return; - } - spellMap.compute(spell.getControllerId(), CardUtil::setOrIncrementValue); - } - - @Override - public void reset() { - spellMap.clear(); - super.reset(); - } - - static int spellCount(UUID playerId, Game game) { - SorcererClassWatcher watcher = game.getState().getWatcher(SorcererClassWatcher.class); - return watcher != null ? watcher.spellMap.getOrDefault(playerId, 0) : 0; - } -} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/InstantAndSorceryCastThisTurn.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/InstantAndSorceryCastThisTurn.java new file mode 100644 index 00000000000..6ffe2ace4b0 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/InstantAndSorceryCastThisTurn.java @@ -0,0 +1,76 @@ +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.game.Game; +import mage.watchers.common.SpellsCastWatcher; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.UUID; + +public enum InstantAndSorceryCastThisTurn implements DynamicValue +{ + YOU("you've cast"), + ALL("all players have cast"), + OPPONENTS("your opponents have cast"); + + private final String message; + private final ValueHint hint; + + InstantAndSorceryCastThisTurn(String message) { + this.message = "Instant and sorcery spells " + message + " this turn"; + this.hint = new ValueHint(this.message, this); + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return getSpellsCastThisTurn(game, sourceAbility); + } + + @Override + public InstantAndSorceryCastThisTurn copy() { + return this; + } + + @Override + public String getMessage() { + return this.message; + } + + public Hint getHint() { + return this.hint; + } + + private int getSpellsCastThisTurn(Game game, Ability ability) { + Collection playerIds; + switch (this) { + case YOU: + playerIds = Collections.singletonList(ability.getControllerId()); + break; + case ALL: + playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game); + break; + case OPPONENTS: + playerIds = game.getOpponents(ability.getControllerId()); + break; + default: + return 0; + } + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (watcher == null) { + return 0; + } + return (int) playerIds.stream() + .map(watcher::getSpellsCastThisTurn) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .filter(spell -> spell.isInstantOrSorcery(game)) + .count(); + } +} \ No newline at end of file