Implemented "Until your next end step" duration (#8831)

* initial implementation of until next end step duration

* added test, reworked effect duration
This commit is contained in:
Evan Kranzler 2022-04-10 17:57:58 -04:00 committed by GitHub
parent 1807565ef0
commit 6e65db284c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 160 additions and 90 deletions

View file

@ -1,26 +1,19 @@
package mage.cards.r; package mage.cards.r;
import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.Mode;
import mage.abilities.condition.Condition;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ExileGraveyardAllTargetPlayerEffect; import mage.abilities.effects.common.ExileGraveyardAllTargetPlayerEffect;
import mage.abilities.effects.common.ExileTopXMayPlayUntilEndOfTurnEffect;
import mage.abilities.effects.common.SacrificeEffect; import mage.abilities.effects.common.SacrificeEffect;
import mage.cards.*; import mage.cards.CardImpl;
import mage.constants.*; import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.filter.FilterPermanent; import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent; import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent;
import mage.filter.predicate.permanent.MaxManaValueControlledPermanentPredicate; import mage.filter.predicate.permanent.MaxManaValueControlledPermanentPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.target.TargetPlayer; import mage.target.TargetPlayer;
import mage.target.common.TargetOpponent; import mage.target.common.TargetOpponent;
import mage.util.CardUtil;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
@ -46,8 +39,9 @@ public final class RiveteersCharm extends CardImpl {
this.getSpellAbility().addTarget(new TargetOpponent()); this.getSpellAbility().addTarget(new TargetOpponent());
// Exile the top three cards of your library. Until your next end step, you may play those cards. // Exile the top three cards of your library. Until your next end step, you may play those cards.
this.getSpellAbility().addMode(new Mode(new RiveteersCharmEffect())); this.getSpellAbility().addMode(new Mode(new ExileTopXMayPlayUntilEndOfTurnEffect(
this.getSpellAbility().addWatcher(new RiveteersCharmWatcher()); 3, false, Duration.UntilYourNextEndStep
)));
// Exile target player's graveyard. // Exile target player's graveyard.
this.getSpellAbility().addMode(new Mode(new ExileGraveyardAllTargetPlayerEffect()).addTarget(new TargetPlayer())); this.getSpellAbility().addMode(new Mode(new ExileGraveyardAllTargetPlayerEffect()).addTarget(new TargetPlayer()));
@ -62,69 +56,3 @@ public final class RiveteersCharm extends CardImpl {
return new RiveteersCharm(this); return new RiveteersCharm(this);
} }
} }
class RiveteersCharmEffect extends OneShotEffect {
RiveteersCharmEffect() {
super(Outcome.Benefit);
staticText = "exile the top three cards of your library. Until your next end step, you may play those cards";
}
private RiveteersCharmEffect(final RiveteersCharmEffect effect) {
super(effect);
}
@Override
public RiveteersCharmEffect copy() {
return new RiveteersCharmEffect(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, 3));
if (cards.isEmpty()) {
return false;
}
player.moveCards(cards, Zone.EXILED, source, game);
int count = RiveteersCharmWatcher.getCount(game, source);
Condition condition = (g, s) -> RiveteersCharmWatcher.getCount(g, s) == count;
for (Card card : cards.getCards(game)) {
CardUtil.makeCardPlayable(game, source, card, Duration.Custom, false, null, condition);
}
return true;
}
}
class RiveteersCharmWatcher extends Watcher {
private final Map<UUID, Integer> playerMap = new HashMap<>();
RiveteersCharmWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
switch (event.getType()) {
case END_TURN_STEP_PRE:
playerMap.compute(game.getActivePlayerId(), CardUtil::setOrIncrementValue);
return;
case BEGINNING_PHASE_PRE:
if (game.getTurnNum() == 1) {
playerMap.clear();
}
}
}
static int getCount(Game game, Ability source) {
return game
.getState()
.getWatcher(RiveteersCharmWatcher.class)
.playerMap
.getOrDefault(source.getControllerId(), 0);
}
}

View file

@ -0,0 +1,77 @@
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 TheElk801
*/
public class UntilNextEndStepTest extends CardTestPlayerBase {
public void doTest(int startTurnNum, PhaseStep startPhaseStep, int endTurnNum, PhaseStep endPhaseStep, boolean stillActive) {
addCustomCardWithAbility(
"tester", playerA,
new SimpleActivatedAbility(new BoostSourceEffect(
1, 1, Duration.UntilYourNextEndStep
), new ManaCostsImpl<>("{0}")), null,
CardType.CREATURE, "", Zone.BATTLEFIELD
);
activateAbility(startTurnNum, startPhaseStep, playerA, "{0}");
setStrictChooseMode(true);
setStopAt(endTurnNum, endPhaseStep);
execute();
assertAllCommandsUsed();
int powerToughness = stillActive ? 2 : 1;
assertPowerToughness(playerA, "tester", powerToughness, powerToughness);
}
@Test
public void testSameTurnTrue() {
doTest(1, PhaseStep.PRECOMBAT_MAIN, 1, PhaseStep.POSTCOMBAT_MAIN, true);
}
@Test
public void testSameTurnFalse() {
doTest(1, PhaseStep.PRECOMBAT_MAIN, 1, PhaseStep.END_TURN, false);
}
@Test
public void testNextTurnTrue() {
doTest(1, PhaseStep.END_TURN, 2, PhaseStep.PRECOMBAT_MAIN, true);
}
@Test
public void testNextTurnFalse() {
doTest(1, PhaseStep.PRECOMBAT_MAIN, 2, PhaseStep.PRECOMBAT_MAIN, false);
}
@Test
public void testTurnCycleTrue() {
doTest(1, PhaseStep.END_TURN, 3, PhaseStep.PRECOMBAT_MAIN, true);
}
@Test
public void testTurnCycleFalse() {
doTest(1, PhaseStep.END_TURN, 3, PhaseStep.END_TURN, false);
}
@Test
public void testOpponentTurnTrue() {
doTest(2, PhaseStep.PRECOMBAT_MAIN, 3, PhaseStep.PRECOMBAT_MAIN, true);
}
@Test
public void testOpponentTurnFalse() {
doTest(2, PhaseStep.PRECOMBAT_MAIN, 3, PhaseStep.END_TURN, false);
}
}

View file

@ -67,6 +67,8 @@ public interface ContinuousEffect extends Effect {
boolean isYourNextTurn(Game game); boolean isYourNextTurn(Game game);
boolean isYourNextEndStep(Game game);
@Override @Override
void newId(); void newId();

View file

@ -4,7 +4,6 @@ import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.CompoundAbility; import mage.abilities.CompoundAbility;
import mage.abilities.MageSingleton; import mage.abilities.MageSingleton;
import mage.abilities.costs.mana.ActivationManaAbilityStep;
import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.DomainValue; import mage.abilities.dynamicvalue.common.DomainValue;
import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; import mage.abilities.dynamicvalue.common.SignInversionDynamicValue;
@ -19,6 +18,7 @@ import mage.game.stack.Spell;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.target.targetpointer.TargetPointer; import mage.target.targetpointer.TargetPointer;
import mage.watchers.common.EndStepCountWatcher;
import java.util.*; import java.util.*;
@ -61,6 +61,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
private UUID startingControllerId; // player to check for turn duration (can't different with real controller ability) private UUID startingControllerId; // player to check for turn duration (can't different with real controller ability)
private boolean startingTurnWasActive; // effect started during related players turn and related players turn was already active 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 effectStartingOnTurn = 0; // turn the effect started
private int effectStartingEndStep = 0;
public ContinuousEffectImpl(Duration duration, Outcome outcome) { public ContinuousEffectImpl(Duration duration, Outcome outcome) {
super(outcome); super(outcome);
@ -91,6 +92,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
this.startingControllerId = effect.startingControllerId; this.startingControllerId = effect.startingControllerId;
this.startingTurnWasActive = effect.startingTurnWasActive; this.startingTurnWasActive = effect.startingTurnWasActive;
this.effectStartingOnTurn = effect.effectStartingOnTurn; this.effectStartingOnTurn = effect.effectStartingOnTurn;
this.effectStartingEndStep = effect.effectStartingEndStep;
this.dependencyTypes = effect.dependencyTypes; this.dependencyTypes = effect.dependencyTypes;
this.dependendToTypes = effect.dependendToTypes; this.dependendToTypes = effect.dependendToTypes;
this.characterDefining = effect.characterDefining; this.characterDefining = effect.characterDefining;
@ -211,6 +213,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
this.startingTurnWasActive = activePlayerId != null this.startingTurnWasActive = activePlayerId != null
&& activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too && activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too
this.effectStartingOnTurn = game.getTurnNum(); this.effectStartingOnTurn = game.getTurnNum();
this.effectStartingEndStep = EndStepCountWatcher.getCount(startingController, game);
} }
@Override @Override
@ -219,6 +222,11 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
&& game.isActivePlayer(startingControllerId); && game.isActivePlayer(startingControllerId);
} }
@Override
public boolean isYourNextEndStep(Game game) {
return EndStepCountWatcher.getCount(startingControllerId, game) > effectStartingEndStep;
}
@Override @Override
public boolean isInactive(Ability source, Game game) { public boolean isInactive(Ability source, Game game) {
// YOUR turn checks // YOUR turn checks
@ -227,6 +235,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
switch (duration) { switch (duration) {
case UntilYourNextTurn: case UntilYourNextTurn:
case UntilEndOfYourNextTurn: case UntilEndOfYourNextTurn:
case UntilYourNextEndStep:
break; break;
default: default:
return false; return false;
@ -237,7 +246,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
return false; return false;
} }
boolean canDelete = false; boolean canDelete;
Player player = game.getPlayer(startingControllerId); Player player = game.getPlayer(startingControllerId);
// discard on start of turn for leaved player // discard on start of turn for leaved player
@ -247,18 +256,26 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
switch (duration) { switch (duration) {
case UntilYourNextTurn: case UntilYourNextTurn:
case UntilEndOfYourNextTurn: case UntilEndOfYourNextTurn:
canDelete = player == null canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving());
|| (!player.isInGame() break;
&& player.hasReachedNextTurnAfterLeaving()); default:
canDelete = false;
}
if (canDelete) {
return true;
} }
// discard on another conditions (start of your turn) // discard on another conditions (start of your turn)
switch (duration) { switch (duration) {
case UntilYourNextTurn: case UntilYourNextTurn:
if (player != null if (player != null && player.isInGame()) {
&& player.isInGame()) { return this.isYourNextTurn(game);
canDelete = canDelete }
|| this.isYourNextTurn(game); break;
case UntilYourNextEndStep:
if (player != null && player.isInGame()) {
return this.isYourNextEndStep(game);
} }
} }

View file

@ -50,7 +50,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
// rules 514.2 // rules 514.2
for (Iterator<T> i = this.iterator(); i.hasNext(); ) { for (Iterator<T> i = this.iterator(); i.hasNext(); ) {
T entry = i.next(); T entry = i.next();
boolean canRemove = false; boolean canRemove;
switch (entry.getDuration()) { switch (entry.getDuration()) {
case EndOfTurn: case EndOfTurn:
canRemove = true; canRemove = true;
@ -58,6 +58,11 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
case UntilEndOfYourNextTurn: case UntilEndOfYourNextTurn:
canRemove = entry.isYourNextTurn(game); canRemove = entry.isYourNextTurn(game);
break; break;
case UntilYourNextEndStep:
canRemove = entry.isYourNextEndStep(game);
break;
default:
canRemove = false;
} }
if (canRemove) { if (canRemove) {
i.remove(); i.remove();
@ -149,6 +154,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
case Custom: case Custom:
case UntilYourNextTurn: case UntilYourNextTurn:
case UntilEndOfYourNextTurn: case UntilEndOfYourNextTurn:
case UntilYourNextEndStep:
// until your turn effects continue until real turn reached, their used it's own inactive method // 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 // 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. // (including phased-out permanents) is removed and all "until end of turn" and "this turn" effects end.

View file

@ -12,6 +12,7 @@ public enum Duration {
WhileInGraveyard("", false, false), WhileInGraveyard("", false, false),
EndOfTurn("until end of turn", true, true), EndOfTurn("until end of turn", true, true),
UntilYourNextTurn("until your next turn", true, true), UntilYourNextTurn("until your next turn", true, true),
UntilYourNextEndStep("until your next end step", true, true),
UntilEndOfYourNextTurn("until the end of your next turn", 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 UntilSourceLeavesBattlefield("until {this} leaves the battlefield", true, false), // supported for continuous layered effects
EndOfCombat("until end of combat", true, true), EndOfCombat("until end of combat", true, true),

View file

@ -1299,6 +1299,7 @@ public abstract class GameImpl implements Game {
newWatchers.add(new ManaSpentToCastWatcher()); newWatchers.add(new ManaSpentToCastWatcher());
newWatchers.add(new ManaPaidSourceWatcher()); newWatchers.add(new ManaPaidSourceWatcher());
newWatchers.add(new BlockingOrBlockedWatcher()); newWatchers.add(new BlockingOrBlockedWatcher());
newWatchers.add(new EndStepCountWatcher());
newWatchers.add(new CommanderPlaysCountWatcher()); // commander plays count uses in non commander games by some cards newWatchers.add(new CommanderPlaysCountWatcher()); // commander plays count uses in non commander games by some cards
// runtime check - allows only GAME scope (one watcher per game) // runtime check - allows only GAME scope (one watcher per game)

View file

@ -0,0 +1,38 @@
package mage.watchers.common;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.util.CardUtil;
import mage.watchers.Watcher;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author TheElk801
*/
public class EndStepCountWatcher extends Watcher {
private final Map<UUID, Integer> playerMap = new HashMap<>();
public EndStepCountWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.END_TURN_STEP_PRE) {
playerMap.compute(game.getActivePlayerId(), CardUtil::setOrIncrementValue);
}
}
public static int getCount(UUID playerId, Game game) {
return game
.getState()
.getWatcher(EndStepCountWatcher.class)
.playerMap
.getOrDefault(playerId, 0);
}
}