1
0
Fork 0
mirror of https://github.com/correl/mage.git synced 2025-04-06 09:13:45 -09:00

Implemented "Until your next end step" duration ()

* 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
Mage.Sets/src/mage/cards/r
Mage.Tests/src/test/java/org/mage/test/cards/continuous
Mage/src/main/java/mage

View file

@ -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);
}
}

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 isYourNextEndStep(Game game);
@Override
void newId();

View file

@ -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);
}
}

View file

@ -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.

View file

@ -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),

View file

@ -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)

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);
}
}