mirror of
https://github.com/correl/mage.git
synced 2025-04-06 09:13:45 -09:00
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:
parent
1807565ef0
commit
6e65db284c
8 changed files with 160 additions and 90 deletions
Mage.Sets/src/mage/cards/r
Mage.Tests/src/test/java/org/mage/test/cards/continuous
Mage/src/main/java/mage
abilities/effects
constants
game
watchers/common
|
@ -1,26 +1,19 @@
|
|||
package mage.cards.r;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
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.ExileTopXMayPlayUntilEndOfTurnEffect;
|
||||
import mage.abilities.effects.common.SacrificeEffect;
|
||||
import mage.cards.*;
|
||||
import mage.constants.*;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Duration;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent;
|
||||
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.common.TargetOpponent;
|
||||
import mage.util.CardUtil;
|
||||
import mage.watchers.Watcher;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
|
@ -46,8 +39,9 @@ public final class RiveteersCharm extends CardImpl {
|
|||
this.getSpellAbility().addTarget(new TargetOpponent());
|
||||
|
||||
// • 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().addWatcher(new RiveteersCharmWatcher());
|
||||
this.getSpellAbility().addMode(new Mode(new ExileTopXMayPlayUntilEndOfTurnEffect(
|
||||
3, false, Duration.UntilYourNextEndStep
|
||||
)));
|
||||
|
||||
// • Exile target player's graveyard.
|
||||
this.getSpellAbility().addMode(new Mode(new ExileGraveyardAllTargetPlayerEffect()).addTarget(new TargetPlayer()));
|
||||
|
@ -62,69 +56,3 @@ public final class RiveteersCharm extends CardImpl {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -67,6 +67,8 @@ public interface ContinuousEffect extends Effect {
|
|||
|
||||
boolean isYourNextTurn(Game game);
|
||||
|
||||
boolean isYourNextEndStep(Game game);
|
||||
|
||||
@Override
|
||||
void newId();
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import mage.MageObjectReference;
|
|||
import mage.abilities.Ability;
|
||||
import mage.abilities.CompoundAbility;
|
||||
import mage.abilities.MageSingleton;
|
||||
import mage.abilities.costs.mana.ActivationManaAbilityStep;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.dynamicvalue.common.DomainValue;
|
||||
import mage.abilities.dynamicvalue.common.SignInversionDynamicValue;
|
||||
|
@ -19,6 +18,7 @@ import mage.game.stack.Spell;
|
|||
import mage.game.stack.StackObject;
|
||||
import mage.players.Player;
|
||||
import mage.target.targetpointer.TargetPointer;
|
||||
import mage.watchers.common.EndStepCountWatcher;
|
||||
|
||||
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 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;
|
||||
|
||||
public ContinuousEffectImpl(Duration duration, Outcome outcome) {
|
||||
super(outcome);
|
||||
|
@ -91,6 +92,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
this.startingControllerId = effect.startingControllerId;
|
||||
this.startingTurnWasActive = effect.startingTurnWasActive;
|
||||
this.effectStartingOnTurn = effect.effectStartingOnTurn;
|
||||
this.effectStartingEndStep = effect.effectStartingEndStep;
|
||||
this.dependencyTypes = effect.dependencyTypes;
|
||||
this.dependendToTypes = effect.dependendToTypes;
|
||||
this.characterDefining = effect.characterDefining;
|
||||
|
@ -211,6 +213,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
this.startingTurnWasActive = activePlayerId != null
|
||||
&& 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -219,6 +222,11 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
&& game.isActivePlayer(startingControllerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isYourNextEndStep(Game game) {
|
||||
return EndStepCountWatcher.getCount(startingControllerId, game) > effectStartingEndStep;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInactive(Ability source, Game game) {
|
||||
// YOUR turn checks
|
||||
|
@ -227,6 +235,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
switch (duration) {
|
||||
case UntilYourNextTurn:
|
||||
case UntilEndOfYourNextTurn:
|
||||
case UntilYourNextEndStep:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
|
@ -237,7 +246,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
return false;
|
||||
}
|
||||
|
||||
boolean canDelete = false;
|
||||
boolean canDelete;
|
||||
Player player = game.getPlayer(startingControllerId);
|
||||
|
||||
// discard on start of turn for leaved player
|
||||
|
@ -247,18 +256,26 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
switch (duration) {
|
||||
case UntilYourNextTurn:
|
||||
case UntilEndOfYourNextTurn:
|
||||
canDelete = player == null
|
||||
|| (!player.isInGame()
|
||||
&& player.hasReachedNextTurnAfterLeaving());
|
||||
canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving());
|
||||
break;
|
||||
default:
|
||||
canDelete = false;
|
||||
}
|
||||
|
||||
if (canDelete) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// discard on another conditions (start of your turn)
|
||||
switch (duration) {
|
||||
case UntilYourNextTurn:
|
||||
if (player != null
|
||||
&& player.isInGame()) {
|
||||
canDelete = canDelete
|
||||
|| this.isYourNextTurn(game);
|
||||
if (player != null && player.isInGame()) {
|
||||
return this.isYourNextTurn(game);
|
||||
}
|
||||
break;
|
||||
case UntilYourNextEndStep:
|
||||
if (player != null && player.isInGame()) {
|
||||
return this.isYourNextEndStep(game);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
|
|||
// rules 514.2
|
||||
for (Iterator<T> i = this.iterator(); i.hasNext(); ) {
|
||||
T entry = i.next();
|
||||
boolean canRemove = false;
|
||||
boolean canRemove;
|
||||
switch (entry.getDuration()) {
|
||||
case EndOfTurn:
|
||||
canRemove = true;
|
||||
|
@ -58,6 +58,11 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
|
|||
case UntilEndOfYourNextTurn:
|
||||
canRemove = entry.isYourNextTurn(game);
|
||||
break;
|
||||
case UntilYourNextEndStep:
|
||||
canRemove = entry.isYourNextEndStep(game);
|
||||
break;
|
||||
default:
|
||||
canRemove = false;
|
||||
}
|
||||
if (canRemove) {
|
||||
i.remove();
|
||||
|
@ -149,6 +154,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
|
|||
case Custom:
|
||||
case UntilYourNextTurn:
|
||||
case UntilEndOfYourNextTurn:
|
||||
case UntilYourNextEndStep:
|
||||
// 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.
|
||||
|
|
|
@ -12,6 +12,7 @@ public enum Duration {
|
|||
WhileInGraveyard("", false, false),
|
||||
EndOfTurn("until end of 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),
|
||||
UntilSourceLeavesBattlefield("until {this} leaves the battlefield", true, false), // supported for continuous layered effects
|
||||
EndOfCombat("until end of combat", true, true),
|
||||
|
|
|
@ -1299,6 +1299,7 @@ public abstract class GameImpl implements Game {
|
|||
newWatchers.add(new ManaSpentToCastWatcher());
|
||||
newWatchers.add(new ManaPaidSourceWatcher());
|
||||
newWatchers.add(new BlockingOrBlockedWatcher());
|
||||
newWatchers.add(new EndStepCountWatcher());
|
||||
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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue