From 6a9340f1aa9cd5ea1274209068f01a383495ffa6 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Thu, 13 Jul 2023 01:40:27 +0200 Subject: [PATCH] Introduce Duration.UntilYourNextUpkeepStep (#10600) * add new Duration * refactor cards with new Duration. * fix both Durations and add unit tests. * fix text --- .../src/mage/cards/b/BrazenCannonade.java | 2 +- Mage.Sets/src/mage/cards/e/ElkinBottle.java | 60 ++------- Mage.Sets/src/mage/cards/e/ErhnamDjinn.java | 6 +- .../src/mage/cards/g/GabrielAngelfire.java | 22 +--- Mage.Sets/src/mage/cards/g/GrinningTotem.java | 52 +------- Mage.Sets/src/mage/cards/h/Halfdane.java | 36 ++---- Mage.Sets/src/mage/cards/s/SoulEcho.java | 23 +--- .../UntilEndCombatYourNextTurnTest.java | 118 ++++++++++++++++++ .../cards/continuous/UntilYourNextUpkeep.java | 97 ++++++++++++++ .../abilities/effects/ContinuousEffect.java | 2 + .../effects/ContinuousEffectImpl.java | 45 +++++-- .../effects/ContinuousEffectsList.java | 3 +- .../main/java/mage/constants/Duration.java | 3 +- .../java/mage/game/permanent/Permanent.java | 2 +- 14 files changed, 291 insertions(+), 180 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilEndCombatYourNextTurnTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilYourNextUpkeep.java diff --git a/Mage.Sets/src/mage/cards/b/BrazenCannonade.java b/Mage.Sets/src/mage/cards/b/BrazenCannonade.java index 911429fb089..a803c724bee 100644 --- a/Mage.Sets/src/mage/cards/b/BrazenCannonade.java +++ b/Mage.Sets/src/mage/cards/b/BrazenCannonade.java @@ -45,7 +45,7 @@ public final class BrazenCannonade extends CardImpl { Ability ability = new ConditionalInterveningIfTriggeredAbility( new BeginningOfPostCombatMainTriggeredAbility( new ExileTopXMayPlayUntilEndOfTurnEffect( - 1, false, Duration.UntilYourNextEndCombatStep + 1, false, Duration.UntilEndCombatOfYourNextTurn ), TargetController.YOU, false ), RaidCondition.instance, "At the beginning of your postcombat main phase, " + "if you attacked with a creature this turn, exile the top card of your library. " + diff --git a/Mage.Sets/src/mage/cards/e/ElkinBottle.java b/Mage.Sets/src/mage/cards/e/ElkinBottle.java index 0fb1583933c..b97f1031209 100644 --- a/Mage.Sets/src/mage/cards/e/ElkinBottle.java +++ b/Mage.Sets/src/mage/cards/e/ElkinBottle.java @@ -1,22 +1,23 @@ package mage.cards.e; -import java.util.UUID; 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.AsThoughEffectImpl; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; import mage.game.Game; import mage.players.Player; -import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import java.util.UUID; + /** * * @author L_J @@ -65,55 +66,10 @@ class ElkinBottleExileEffect extends OneShotEffect { Card card = controller.getLibrary().getFromTop(game); if (card != null) { controller.moveCardsToExile(card, source, game, true, source.getSourceId(), CardUtil.createObjectRealtedWindowTitle(source, game, null)); - ContinuousEffect effect = new ElkinBottleCastFromExileEffect(); - effect.setTargetPointer(new FixedTarget(card.getId(), game)); - game.addEffect(effect, source); + CardUtil.makeCardPlayable(game, source, card, Duration.UntilYourNextUpkeepStep, false); } return true; } return false; } -} - -class ElkinBottleCastFromExileEffect extends AsThoughEffectImpl { - - private boolean sameStep = true; - - public ElkinBottleCastFromExileEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.Benefit); - this.staticText = "Until the beginning of your next upkeep, you may play that card."; - } - - public ElkinBottleCastFromExileEffect(final ElkinBottleCastFromExileEffect effect) { - super(effect); - } - - @Override - public ElkinBottleCastFromExileEffect copy() { - return new ElkinBottleCastFromExileEffect(this); - } - - @Override - public boolean isInactive(Ability source, Game game) { - if (game.getPhase().getStep().getType() == PhaseStep.UPKEEP) { - if (!sameStep && game.isActivePlayer(source.getControllerId()) || game.getPlayer(source.getControllerId()).hasReachedNextTurnAfterLeaving()) { - return true; - } - } else { - sameStep = false; - } - return false; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - return source.isControlledBy(affectedControllerId) - && sourceId.equals(getTargetPointer().getFirst(game, source)); - } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/e/ErhnamDjinn.java b/Mage.Sets/src/mage/cards/e/ErhnamDjinn.java index cf86e1a4e75..2961dfe2bfe 100644 --- a/Mage.Sets/src/mage/cards/e/ErhnamDjinn.java +++ b/Mage.Sets/src/mage/cards/e/ErhnamDjinn.java @@ -1,7 +1,6 @@ package mage.cards.e; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; @@ -14,6 +13,8 @@ import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.Predicates; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** * * @author LevelX2 @@ -35,9 +36,8 @@ public final class ErhnamDjinn extends CardImpl { this.toughness = new MageInt(5); // At the beginning of your upkeep, target non-Wall creature an opponent controls gains forestwalk until your next upkeep. - GainAbilityTargetEffect effect = new GainAbilityTargetEffect(new ForestwalkAbility(false), Duration.Custom, + GainAbilityTargetEffect effect = new GainAbilityTargetEffect(new ForestwalkAbility(false), Duration.UntilYourNextUpkeepStep, "target non-Wall creature an opponent controls gains forestwalk until your next upkeep"); - effect.setDurationToPhase(PhaseStep.UPKEEP); Ability ability = new BeginningOfUpkeepTriggeredAbility(effect, TargetController.YOU, false); ability.addTarget(new TargetCreaturePermanent(filter)); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/g/GabrielAngelfire.java b/Mage.Sets/src/mage/cards/g/GabrielAngelfire.java index cde0616d073..504a0e0048a 100644 --- a/Mage.Sets/src/mage/cards/g/GabrielAngelfire.java +++ b/Mage.Sets/src/mage/cards/g/GabrielAngelfire.java @@ -1,9 +1,6 @@ package mage.cards.g; -import java.util.LinkedHashSet; -import java.util.Set; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; @@ -20,6 +17,10 @@ import mage.constants.*; import mage.game.Game; import mage.players.Player; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; + /** * * @author Styxo & L_J @@ -50,7 +51,6 @@ public final class GabrielAngelfire extends CardImpl { class GabrielAngelfireGainAbilityEffect extends GainAbilitySourceEffect { private static final Set choices = new LinkedHashSet<>(); - private boolean sameStep = true; static { choices.add("Flying"); @@ -60,7 +60,7 @@ class GabrielAngelfireGainAbilityEffect extends GainAbilitySourceEffect { } public GabrielAngelfireGainAbilityEffect() { - super(FlyingAbility.getInstance(), Duration.Custom); + super(FlyingAbility.getInstance(), Duration.UntilYourNextUpkeepStep); staticText = "choose flying, first strike, trample, or rampage 3. {this} gains that ability until your next upkeep"; } @@ -74,18 +74,6 @@ class GabrielAngelfireGainAbilityEffect extends GainAbilitySourceEffect { return new GabrielAngelfireGainAbilityEffect(this); } - @Override - public boolean isInactive(Ability source, Game game) { - if (game.getPhase().getStep().getType() == PhaseStep.UPKEEP) { - if (!sameStep && game.isActivePlayer(source.getControllerId()) || game.getPlayer(source.getControllerId()).hasReachedNextTurnAfterLeaving()) { - return true; - } - } else { - sameStep = false; - } - return false; - } - @Override public void init(Ability source, Game game) { super.init(source, game); diff --git a/Mage.Sets/src/mage/cards/g/GrinningTotem.java b/Mage.Sets/src/mage/cards/g/GrinningTotem.java index 65c4191f305..6b9eaa1c9d9 100644 --- a/Mage.Sets/src/mage/cards/g/GrinningTotem.java +++ b/Mage.Sets/src/mage/cards/g/GrinningTotem.java @@ -6,19 +6,20 @@ import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.SacrificeSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; import mage.game.ExileZone; import mage.game.Game; import mage.game.events.GameEvent; import mage.players.Player; import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetOpponent; -import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; import java.util.UUID; @@ -82,7 +83,7 @@ class GrinningTotemSearchAndExileEffect extends OneShotEffect { if (card != null) { UUID exileZoneId = CardUtil.getCardExileZoneId(game, source); you.moveCardsToExile(card, source, game, true, exileZoneId, CardUtil.getSourceName(game, source)); - game.addEffect(new GrinningTotemMayPlayEffect().setTargetPointer(new FixedTarget(card.getId())), source); + CardUtil.makeCardPlayable(game, source, card, Duration.UntilYourNextUpkeepStep, false); game.addDelayedTriggeredAbility(new GrinningTotemDelayedTriggeredAbility(exileZoneId), source); } targetOpponent.shuffleLibrary(source, game); @@ -91,49 +92,6 @@ class GrinningTotemSearchAndExileEffect extends OneShotEffect { } -class GrinningTotemMayPlayEffect extends AsThoughEffectImpl { - - private boolean sameStep = true; - - public GrinningTotemMayPlayEffect() { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.Benefit); - this.staticText = "Until the beginning of your next upkeep, you may play that card."; - } - - public GrinningTotemMayPlayEffect(final GrinningTotemMayPlayEffect effect) { - super(effect); - } - - @Override - public GrinningTotemMayPlayEffect copy() { - return new GrinningTotemMayPlayEffect(this); - } - - @Override - public boolean isInactive(Ability source, Game game) { - if (game.getPhase().getStep().getType() == PhaseStep.UPKEEP) { - if (!sameStep && game.isActivePlayer(source.getControllerId()) || game.getPlayer(source.getControllerId()).hasReachedNextTurnAfterLeaving()) { - return true; - } - } else { - sameStep = false; - } - return false; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - return source.isControlledBy(affectedControllerId) - && sourceId.equals(getTargetPointer().getFirst(game, source)); - } - -} - class GrinningTotemDelayedTriggeredAbility extends DelayedTriggeredAbility { private final UUID exileZoneId; diff --git a/Mage.Sets/src/mage/cards/h/Halfdane.java b/Mage.Sets/src/mage/cards/h/Halfdane.java index 8136cb13b18..210408bdb50 100644 --- a/Mage.Sets/src/mage/cards/h/Halfdane.java +++ b/Mage.Sets/src/mage/cards/h/Halfdane.java @@ -1,7 +1,6 @@ package mage.cards.h; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; @@ -18,6 +17,8 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCreaturePermanent; +import java.util.UUID; + /** * * @author L_J @@ -77,33 +78,12 @@ class HalfdaneUpkeepEffect extends OneShotEffect { return false; } - ContinuousEffect effect = new HalfdaneSetBasePowerToughnessEffect(permanent.getPower().getValue(), permanent.getToughness().getValue()); + ContinuousEffect effect = new SetBasePowerToughnessSourceEffect( + permanent.getPower().getValue(), + permanent.getToughness().getValue(), + Duration.UntilYourNextUpkeepStep, + SubLayer.SetPT_7b); game.addEffect(effect, source); return true; } -} - -class HalfdaneSetBasePowerToughnessEffect extends SetBasePowerToughnessSourceEffect { - - public HalfdaneSetBasePowerToughnessEffect(int power, int toughness) { - super(power, toughness, Duration.UntilYourNextTurn, SubLayer.SetPT_7b); - } - - public HalfdaneSetBasePowerToughnessEffect(final HalfdaneSetBasePowerToughnessEffect effect) { - super(effect); - } - - @Override - public boolean isInactive(Ability source, Game game) { - if (super.isInactive(source, game) && game.getTurnStepType().isAfter(PhaseStep.UPKEEP)) { - return true; - } - return false; - } - - @Override - public HalfdaneSetBasePowerToughnessEffect copy() { - return new HalfdaneSetBasePowerToughnessEffect(this); - } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SoulEcho.java b/Mage.Sets/src/mage/cards/s/SoulEcho.java index eb227a537d3..96d179b2868 100644 --- a/Mage.Sets/src/mage/cards/s/SoulEcho.java +++ b/Mage.Sets/src/mage/cards/s/SoulEcho.java @@ -1,7 +1,6 @@ package mage.cards.s; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.EntersBattlefieldAbility; @@ -21,11 +20,12 @@ import mage.counters.CounterType; import mage.game.Game; import mage.game.events.DamageEvent; import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetOpponent; +import java.util.UUID; + /** * * @author L_J @@ -63,7 +63,8 @@ class SoulEchoOpponentsChoiceEffect extends OneShotEffect { public SoulEchoOpponentsChoiceEffect() { super(Outcome.PreventDamage); - staticText = "target opponent may choose that for each 1 damage that would be dealt to you until your next upkeep, you remove an echo counter from {this} instead"; + staticText = "target opponent may choose that for each 1 damage that would be dealt to you " + + "until your next upkeep, you remove an echo counter from {this} instead"; } public SoulEchoOpponentsChoiceEffect(final SoulEchoOpponentsChoiceEffect effect) { @@ -92,29 +93,15 @@ class SoulEchoOpponentsChoiceEffect extends OneShotEffect { } class SoulEchoReplacementEffect extends ReplacementEffectImpl { - - private boolean sameStep = true; SoulEchoReplacementEffect() { - super(Duration.Custom, Outcome.PreventDamage); + super(Duration.UntilYourNextUpkeepStep, Outcome.PreventDamage); } SoulEchoReplacementEffect(final SoulEchoReplacementEffect effect) { super(effect); } - @Override - public boolean isInactive(Ability source, Game game) { - if (game.getPhase().getStep().getType() == PhaseStep.UPKEEP) { - if (!sameStep && game.isActivePlayer(source.getControllerId()) || game.getPlayer(source.getControllerId()).hasReachedNextTurnAfterLeaving()) { - return true; - } - } else { - sameStep = false; - } - return false; - } - @Override public boolean replaceEvent(GameEvent event, Ability source, Game game) { DamageEvent damageEvent = (DamageEvent) event; diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilEndCombatYourNextTurnTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilEndCombatYourNextTurnTest.java new file mode 100644 index 00000000000..7a5d18535bc --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilEndCombatYourNextTurnTest.java @@ -0,0 +1,118 @@ +package org.mage.test.cards.continuous; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class UntilEndCombatYourNextTurnTest extends CardTestPlayerBase { + + public interface AdditionalSetup { + void init(UntilEndCombatYourNextTurnTest test); + } + + public void doTest(AdditionalSetup additionalSetup, int endTurnNum, PhaseStep endPhaseStep, boolean stillActive) { + addCustomCardWithAbility( + "tester", playerA, + new SimpleActivatedAbility(new BoostSourceEffect( + 1, 1, Duration.UntilEndCombatOfYourNextTurn + ), new ManaCostsImpl<>("{0}")), null, + CardType.CREATURE, "", Zone.BATTLEFIELD + ); + + if(additionalSetup != null){ + additionalSetup.init(this); + } + + activateAbility(1, PhaseStep.UPKEEP, playerA, "{0}"); + + setStrictChooseMode(true); + setStopAt(endTurnNum, endPhaseStep); + execute(); + + int powerToughness = stillActive ? 2 : 1; + assertPowerToughness(playerA, "tester", powerToughness, powerToughness); + } + + @Test + public void testSameTurnPre() { + doTest(null, 1, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testSameTurnPost() { + doTest(null,1, PhaseStep.POSTCOMBAT_MAIN, true); + } + + @Test + public void testOppTurnPre() { + doTest(null, 2, PhaseStep.PRECOMBAT_MAIN, true); } + + @Test + public void testOppTurnPost() { + doTest(null, 2, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testTurnCyclePre() { + doTest(null, 3, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testTurnCycleFalse() { + + doTest(null, 3, PhaseStep.POSTCOMBAT_MAIN, false); + } + + // Relevant rulings: + // + // 614.10. An effect that causes a player to skip an event, step, phase, or turn + // is a replacement effect. “Skip [something]” is the same as “Instead of doing + // [something], do nothing.” Once a step, phase, or turn has started, it can no + // longer be skipped—any skip effects will wait until the next occurrence. + // + // 614.10a Anything scheduled for a skipped step, phase, or turn won’t happen. + // Anything scheduled for the “next” occurrence of something waits for the first + // occurrence that isn’t skipped. If two effects each cause a player to skip + // their next occurrence, that player must skip the next two; one effect will + // be satisfied in skipping the first occurrence, while the other will remain + // until another occurrence can be skipped + private static void timeStopOn3(UntilEndCombatYourNextTurnTest test) { + // End the turn. + test.addCard(Zone.HAND, test.playerA, "Time Stop"); + test.addCard(Zone.BATTLEFIELD, test.playerA, "Island", 6); + test.castSpell(3,PhaseStep.PRECOMBAT_MAIN,test.playerA,"Time Stop"); + } + + @Test + public void testTimeStopTurnCyclePre() { + + doTest(test -> timeStopOn3(test), 3, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testTimeStopTurnCycleFalse() { + + doTest(test -> timeStopOn3(test), 3, PhaseStep.CLEANUP, true); + } + + @Test + public void testTimeStop2TurnCyclePre() { + + doTest(test -> timeStopOn3(test), 5, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testTimeStop2TurnCycleFalse() { + doTest(test -> timeStopOn3(test), 5, PhaseStep.POSTCOMBAT_MAIN, true); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilYourNextUpkeep.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilYourNextUpkeep.java new file mode 100644 index 00000000000..5d0107c2abc --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilYourNextUpkeep.java @@ -0,0 +1,97 @@ +package org.mage.test.cards.continuous; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class UntilYourNextUpkeep extends CardTestPlayerBase { + + public interface AdditionalSetup { + void init(UntilYourNextUpkeep test); + } + + public void doTest(AdditionalSetup additionalSetup, + int startTurnNum, PhaseStep startPhaseStep, + int endTurnNum, PhaseStep endPhaseStep, + boolean stillActive) { + setStrictChooseMode(true); + + addCustomCardWithAbility( + "tester", playerA, + new SimpleActivatedAbility(new BoostSourceEffect( + 1, 1, Duration.UntilYourNextUpkeepStep + ), new ManaCostsImpl<>("{0}")), null, + CardType.CREATURE, "", Zone.BATTLEFIELD + ); + + if(additionalSetup != null){ + additionalSetup.init(this); + } + + activateAbility(startTurnNum, startPhaseStep, playerA, "{0}"); + + setStopAt(endTurnNum, endPhaseStep); + execute(); + + int powerToughness = stillActive ? 2 : 1; + assertPowerToughness(playerA, "tester", powerToughness, powerToughness); + } + + @Test + public void testSameTurn() { + doTest(null, 1, PhaseStep.UPKEEP, 1, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testOppTurn() { + doTest(null, 1, PhaseStep.UPKEEP, 2, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testTurnCycle() { + doTest(null, 1, PhaseStep.UPKEEP, 3, PhaseStep.PRECOMBAT_MAIN, false); + } + + private static void initParadoxHaze(UntilYourNextUpkeep test) { + // At the beginning of enchanted player’s first upkeep each turn, + // that player gets an additional upkeep step after this step. + test.addCard(Zone.HAND, test.playerA, "Paradox Haze"); + test.addCard(Zone.BATTLEFIELD, test.playerA, "Island", 3); + test.castSpell(1, PhaseStep.PRECOMBAT_MAIN, test.playerA, "Paradox Haze", test.playerA); + } + + @Test + public void testParadoxHazeOppSameTurn() { + doTest(test -> initParadoxHaze(test), 2, PhaseStep.UPKEEP, 2, PhaseStep.PRECOMBAT_MAIN, true); + } + + // Activating at first upkeep, at second upkeep the effect wears off. + @Test + public void testParadoxHazeSameTurn() { + doTest(test -> initParadoxHaze(test), 3, PhaseStep.UPKEEP, 3, PhaseStep.PRECOMBAT_MAIN, false); + } + + private static void initEonHub(UntilYourNextUpkeep test) { + // Players skip their upkeep step. + test.addCard(Zone.BATTLEFIELD, test.playerA, "Eon Hub"); + } + + @Test + public void testEonHubSameTurn() { + doTest(test -> initEonHub(test), 1, PhaseStep.PRECOMBAT_MAIN, 1, PhaseStep.POSTCOMBAT_MAIN, true); + } + + @Test + public void testEonHubCycleTurn() { + doTest(test -> initEonHub(test), 1, PhaseStep.PRECOMBAT_MAIN, 3, PhaseStep.POSTCOMBAT_MAIN, true); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java index 03b551a9a71..78dc130d62f 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java @@ -69,6 +69,8 @@ public interface ContinuousEffect extends Effect { boolean isYourNextEndStep(Game game); + boolean isYourNextUpkeepStep(Game game); + @Override void newId(); diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index bcbb57432bf..35a8a5297f5 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -4,10 +4,6 @@ import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.CompoundAbility; import mage.abilities.MageSingleton; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.dynamicvalue.common.DomainValue; -import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; -import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.keyword.ChangelingAbility; import mage.constants.*; import mage.filter.Filter; @@ -62,6 +58,10 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu private boolean startingTurnWasActive; // effect started during related players turn and related players turn was already active private int effectStartingOnTurn = 0; // turn the effect started private int effectStartingEndStep = 0; + private int nextTurnNumber = Integer.MAX_VALUE; // effect is waiting for a step during your next turn, we store it if found. + // set to the turn number on your next turn. + private int effectStartingStepNum = 0; // Some continuous are waiting for the next step of a kind. + // Avoid miscancelling if the start step is of that kind. public ContinuousEffectImpl(Duration duration, Outcome outcome) { super(outcome); @@ -96,6 +96,8 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu this.dependencyTypes = effect.dependencyTypes; this.dependendToTypes = effect.dependendToTypes; this.characterDefining = effect.characterDefining; + this.nextTurnNumber = effect.nextTurnNumber; + this.effectStartingStepNum = effect.effectStartingStepNum; } @Override @@ -215,6 +217,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu && activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too this.effectStartingOnTurn = game.getTurnNum(); this.effectStartingEndStep = EndStepCountWatcher.getCount(startingController, game); + this.effectStartingStepNum = game.getState().getStepNum(); } @Override @@ -228,10 +231,24 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu return EndStepCountWatcher.getCount(startingControllerId, game) > effectStartingEndStep; } - public boolean isYourNextEndCombatStep(Game game) { - return effectStartingOnTurn < game.getTurnNum() - && game.isActivePlayer(startingControllerId) - && game.getPhase().getType() == TurnPhase.POSTCOMBAT_MAIN; + public boolean isEndCombatOfYourNextTurn(Game game) { + int currentTurn = game.getTurnNum(); + if(nextTurnNumber != Integer.MAX_VALUE && nextTurnNumber < currentTurn){ + return false; // This is a turn after your next turn. + } + if(nextTurnNumber == Integer.MAX_VALUE && isYourNextTurn(game)) { + nextTurnNumber = currentTurn; + } + + return isYourNextTurn(game) + && game.getPhase().getType() == TurnPhase.POSTCOMBAT_MAIN; + } + + public boolean isYourNextUpkeepStep(Game game) { + return (effectStartingOnTurn < game.getTurnNum() || + effectStartingStepNum < game.getState().getStepNum()) + && game.isActivePlayer(startingControllerId) + && game.getStep().getType() == PhaseStep.UPKEEP; } @Override @@ -243,7 +260,8 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu case UntilYourNextTurn: case UntilEndOfYourNextTurn: case UntilYourNextEndStep: - case UntilYourNextEndCombatStep: + case UntilEndCombatOfYourNextTurn: + case UntilYourNextUpkeepStep: break; default: return false; @@ -286,9 +304,14 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu return this.isYourNextEndStep(game); } break; - case UntilYourNextEndCombatStep: + case UntilEndCombatOfYourNextTurn: if (player != null && player.isInGame()) { - return this.isYourNextEndCombatStep(game); + return this.isEndCombatOfYourNextTurn(game); + } + break; + case UntilYourNextUpkeepStep: + if (player != null && player.isInGame()) { + return this.isYourNextUpkeepStep(game); } break; } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java index 6e48c6cda87..c8707a7289f 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java @@ -154,8 +154,9 @@ public class ContinuousEffectsList extends ArrayList case Custom: case UntilYourNextTurn: case UntilEndOfYourNextTurn: - case UntilYourNextEndCombatStep: + case UntilEndCombatOfYourNextTurn: case UntilYourNextEndStep: + case UntilYourNextUpkeepStep: // until your turn effects continue until real turn reached, their used it's own inactive method // 514.2 Second, the following actions happen simultaneously: all damage marked on permanents // (including phased-out permanents) is removed and all "until end of turn" and "this turn" effects end. diff --git a/Mage/src/main/java/mage/constants/Duration.java b/Mage/src/main/java/mage/constants/Duration.java index db4cd221b3a..dd14431a6b5 100644 --- a/Mage/src/main/java/mage/constants/Duration.java +++ b/Mage/src/main/java/mage/constants/Duration.java @@ -13,7 +13,8 @@ public enum Duration { EndOfTurn("until end of turn", true, true), UntilYourNextTurn("until your next turn", true, true), UntilYourNextEndStep("until your next end step", true, true), - UntilYourNextEndCombatStep("until your next end of combat step", false, true), + UntilEndCombatOfYourNextTurn("until end of combat on your next turn", true, true), + UntilYourNextUpkeepStep("until your next upkeep", true, true), UntilEndOfYourNextTurn("until the end of your next turn", true, true), UntilSourceLeavesBattlefield("until {this} leaves the battlefield", true, false), // supported for continuous layered effects EndOfCombat("until end of combat", true, true), diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 463af7368c1..bd402c7a286 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -115,7 +115,7 @@ public interface Permanent extends Card, Controllable { int getAttachedToZoneChangeCounter(); - void attachTo(UUID permanentId, Ability source, Game game); + void attachTo(UUID attachToObjectId, Ability source, Game game); void unattach(Game game);