mirror of
https://github.com/magefree/mage.git
synced 2025-12-22 11:32:00 -08:00
* Until end of your turn - fixed that effects discarded too early in multiplayer games (#5759, #5676);
Tests: added dozen tests for end of turn effects and related cards.
This commit is contained in:
parent
4288e45c23
commit
534037e095
22 changed files with 758 additions and 137 deletions
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
package mage.abilities;
|
||||
|
||||
import mage.constants.Duration;
|
||||
|
|
@ -48,8 +46,8 @@ public class DelayedTriggeredAbilities extends AbilitiesImpl<DelayedTriggeredAbi
|
|||
}
|
||||
}
|
||||
|
||||
public void removeEndOfTurnAbilities() {
|
||||
this.removeIf(ability -> ability.getDuration() == Duration.EndOfTurn);
|
||||
public void removeEndOfTurnAbilities(Game game) {
|
||||
this.removeIf(ability -> ability.getDuration() == Duration.EndOfTurn); // TODO: add Duration.EndOfYourTurn like effects
|
||||
}
|
||||
|
||||
public void removeEndOfCombatAbilities() {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ public interface ContinuousEffect extends Effect {
|
|||
|
||||
void init(Ability source, Game game);
|
||||
|
||||
void init(Ability source, Game game, UUID activePlayerId);
|
||||
|
||||
Layer getLayer();
|
||||
|
||||
SubLayer getSublayer();
|
||||
|
|
@ -58,14 +60,14 @@ public interface ContinuousEffect extends Effect {
|
|||
|
||||
void addDependedToType(DependencyType dependencyType);
|
||||
|
||||
void setStartingTurnNum(Game game, UUID startingController);
|
||||
|
||||
int getStartingTurnNum();
|
||||
|
||||
int getNextStartingControllerTurnNum();
|
||||
void setStartingControllerAndTurnNum(Game game, UUID startingController, UUID activePlayerId);
|
||||
|
||||
UUID getStartingController();
|
||||
|
||||
void incYourTurnNumPlayed();
|
||||
|
||||
boolean isYourNextTurn(Game game);
|
||||
|
||||
@Override
|
||||
void newId();
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import mage.players.Player;
|
|||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||
*/
|
||||
public abstract class ContinuousEffectImpl extends EffectImpl implements ContinuousEffect {
|
||||
|
||||
|
|
@ -38,10 +38,10 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
*/
|
||||
protected boolean characterDefining = false;
|
||||
|
||||
// until your next turn
|
||||
private int startingTurnNum;
|
||||
private int yourNextTurnNum;
|
||||
private UUID startingControllerId;
|
||||
// until your next turn or until end of your next turn
|
||||
private UUID startingControllerId; // player to checkss turns (can't different with real controller ability)
|
||||
private boolean startingTurnWasActive;
|
||||
private int yourTurnNumPlayed = 0; // turnes played after effect was created
|
||||
|
||||
public ContinuousEffectImpl(Duration duration, Outcome outcome) {
|
||||
super(outcome);
|
||||
|
|
@ -69,9 +69,9 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
this.affectedObjectsSet = effect.affectedObjectsSet;
|
||||
this.affectedObjectList.addAll(effect.affectedObjectList);
|
||||
this.temporary = effect.temporary;
|
||||
this.startingTurnNum = effect.startingTurnNum;
|
||||
this.yourNextTurnNum = effect.yourNextTurnNum;
|
||||
this.startingControllerId = effect.startingControllerId;
|
||||
this.startingTurnWasActive = effect.startingTurnWasActive;
|
||||
this.yourTurnNumPlayed = effect.yourTurnNumPlayed;
|
||||
this.dependencyTypes = effect.dependencyTypes;
|
||||
this.dependendToTypes = effect.dependendToTypes;
|
||||
this.characterDefining = effect.characterDefining;
|
||||
|
|
@ -139,6 +139,11 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
|
||||
@Override
|
||||
public void init(Ability source, Game game) {
|
||||
init(source, game, game.getActivePlayerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Ability source, Game game, UUID activePlayerId) {
|
||||
targetPointer.init(game, source);
|
||||
//20100716 - 611.2c
|
||||
if (AbilityType.ACTIVATED == source.getAbilityType()
|
||||
|
|
@ -161,50 +166,75 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
|
|||
this.affectedObjectsSet = true;
|
||||
}
|
||||
}
|
||||
setStartingTurnNum(game, source.getControllerId());
|
||||
setStartingControllerAndTurnNum(game, source.getControllerId(), activePlayerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStartingTurnNum(Game game, UUID startingController) {
|
||||
this.startingControllerId = startingController;
|
||||
this.startingTurnNum = game.getTurnNum();
|
||||
this.yourNextTurnNum = game.isActivePlayer(startingControllerId) ? startingTurnNum + 2 : startingTurnNum + 1;
|
||||
}
|
||||
|
||||
public int getStartingTurnNum() {
|
||||
return this.startingTurnNum;
|
||||
}
|
||||
|
||||
public int getNextStartingControllerTurnNum() {
|
||||
return this.yourNextTurnNum;
|
||||
}
|
||||
|
||||
public UUID getStartingController() {
|
||||
return this.startingControllerId;
|
||||
return startingControllerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStartingControllerAndTurnNum(Game game, UUID startingController, UUID activePlayerId) {
|
||||
this.startingControllerId = startingController;
|
||||
this.startingTurnWasActive = activePlayerId != null && activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too
|
||||
this.yourTurnNumPlayed = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incYourTurnNumPlayed() {
|
||||
yourTurnNumPlayed++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isYourNextTurn(Game game) {
|
||||
if (this.startingTurnWasActive) {
|
||||
return yourTurnNumPlayed == 1 && game.isActivePlayer(startingControllerId);
|
||||
} else {
|
||||
return yourTurnNumPlayed == 0 && game.isActivePlayer(startingControllerId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInactive(Ability source, Game game) {
|
||||
if (duration == Duration.UntilYourNextTurn || duration == Duration.UntilEndOfYourNextTurn) {
|
||||
Player player = game.getPlayer(startingControllerId);
|
||||
if (player != null) {
|
||||
if (player.isInGame()) {
|
||||
boolean canDelete = false;
|
||||
switch (duration) {
|
||||
case UntilYourNextTurn:
|
||||
canDelete = game.getTurnNum() >= yourNextTurnNum;
|
||||
break;
|
||||
case UntilEndOfYourNextTurn:
|
||||
canDelete = (game.getTurnNum() > yourNextTurnNum)
|
||||
|| (game.getTurnNum() == yourNextTurnNum && game.getStep().getType().isAfter(PhaseStep.END_TURN));
|
||||
}
|
||||
return canDelete;
|
||||
}
|
||||
return player.hasReachedNextTurnAfterLeaving();
|
||||
}
|
||||
return true;
|
||||
// YOUR turn checks
|
||||
// until end of turn - must be checked on cleanup step, see rules 514.2
|
||||
// other must checked here (active and leave players), see rules 800.4
|
||||
switch (duration) {
|
||||
case UntilYourNextTurn:
|
||||
case UntilEndOfYourNextTurn:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
// cheat engine put cards without play and calls direct applyEffects with clean -- need to ignore it
|
||||
if (game.getActivePlayerId() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean canDelete = false;
|
||||
Player player = game.getPlayer(startingControllerId);
|
||||
|
||||
// discard on start of turn for leave player
|
||||
// 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn
|
||||
// or until a specific point in that turn will last until that turn would have begun.
|
||||
// They neither expire immediately nor last indefinitely.
|
||||
switch (duration) {
|
||||
case UntilYourNextTurn:
|
||||
case UntilEndOfYourNextTurn:
|
||||
canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving());
|
||||
}
|
||||
|
||||
// discard on another conditions (start of your turn)
|
||||
switch (duration) {
|
||||
case UntilYourNextTurn:
|
||||
if (player != null && player.isInGame()) {
|
||||
canDelete = canDelete || this.isYourNextTurn(game);
|
||||
}
|
||||
}
|
||||
|
||||
return canDelete;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
package mage.abilities.effects;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
import mage.MageObject;
|
||||
import mage.MageObjectReference;
|
||||
import mage.abilities.*;
|
||||
|
|
@ -31,6 +27,11 @@ import mage.players.Player;
|
|||
import mage.target.common.TargetCardInHand;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
|
|
@ -54,7 +55,7 @@ public class ContinuousEffects implements Serializable {
|
|||
private final Map<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> asThoughEffectsMap = new EnumMap<>(AsThoughEffectType.class);
|
||||
public final List<ContinuousEffectsList<?>> allEffectsLists = new ArrayList<>();
|
||||
private final ApplyCountersEffect applyCounters;
|
||||
// private final PlaneswalkerRedirectionEffect planeswalkerRedirectionEffect;
|
||||
// private final PlaneswalkerRedirectionEffect planeswalkerRedirectionEffect;
|
||||
private final AuraReplacementEffect auraReplacementEffect;
|
||||
|
||||
private final List<ContinuousEffect> previous = new ArrayList<>();
|
||||
|
|
@ -134,18 +135,18 @@ public class ContinuousEffects implements Serializable {
|
|||
spliceCardEffects.removeEndOfCombatEffects();
|
||||
}
|
||||
|
||||
public synchronized void removeEndOfTurnEffects() {
|
||||
layeredEffects.removeEndOfTurnEffects();
|
||||
continuousRuleModifyingEffects.removeEndOfTurnEffects();
|
||||
replacementEffects.removeEndOfTurnEffects();
|
||||
preventionEffects.removeEndOfTurnEffects();
|
||||
requirementEffects.removeEndOfTurnEffects();
|
||||
restrictionEffects.removeEndOfTurnEffects();
|
||||
public synchronized void removeEndOfTurnEffects(Game game) {
|
||||
layeredEffects.removeEndOfTurnEffects(game);
|
||||
continuousRuleModifyingEffects.removeEndOfTurnEffects(game);
|
||||
replacementEffects.removeEndOfTurnEffects(game);
|
||||
preventionEffects.removeEndOfTurnEffects(game);
|
||||
requirementEffects.removeEndOfTurnEffects(game);
|
||||
restrictionEffects.removeEndOfTurnEffects(game);
|
||||
for (ContinuousEffectsList asThoughtlist : asThoughEffectsMap.values()) {
|
||||
asThoughtlist.removeEndOfTurnEffects();
|
||||
asThoughtlist.removeEndOfTurnEffects(game);
|
||||
}
|
||||
costModificationEffects.removeEndOfTurnEffects();
|
||||
spliceCardEffects.removeEndOfTurnEffects();
|
||||
costModificationEffects.removeEndOfTurnEffects(game);
|
||||
spliceCardEffects.removeEndOfTurnEffects(game);
|
||||
}
|
||||
|
||||
public synchronized void removeInactiveEffects(Game game) {
|
||||
|
|
@ -163,6 +164,20 @@ public class ContinuousEffects implements Serializable {
|
|||
spliceCardEffects.removeInactiveEffects(game);
|
||||
}
|
||||
|
||||
public synchronized void incYourTurnNumPlayed(Game game) {
|
||||
layeredEffects.incYourTurnNumPlayed(game);
|
||||
continuousRuleModifyingEffects.incYourTurnNumPlayed(game);
|
||||
replacementEffects.incYourTurnNumPlayed(game);
|
||||
preventionEffects.incYourTurnNumPlayed(game);
|
||||
requirementEffects.incYourTurnNumPlayed(game);
|
||||
restrictionEffects.incYourTurnNumPlayed(game);
|
||||
for (ContinuousEffectsList asThoughtlist : asThoughEffectsMap.values()) {
|
||||
asThoughtlist.incYourTurnNumPlayed(game);
|
||||
}
|
||||
costModificationEffects.incYourTurnNumPlayed(game);
|
||||
spliceCardEffects.incYourTurnNumPlayed(game);
|
||||
}
|
||||
|
||||
public synchronized List<ContinuousEffect> getLayeredEffects(Game game) {
|
||||
List<ContinuousEffect> layerEffects = new ArrayList<>();
|
||||
for (ContinuousEffect effect : layeredEffects) {
|
||||
|
|
@ -322,7 +337,7 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
// boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT);
|
||||
//get all applicable transient Replacement effects
|
||||
for (Iterator<ReplacementEffect> iterator = replacementEffects.iterator(); iterator.hasNext();) {
|
||||
for (Iterator<ReplacementEffect> iterator = replacementEffects.iterator(); iterator.hasNext(); ) {
|
||||
ReplacementEffect effect = iterator.next();
|
||||
if (!effect.checksEventType(event, game)) {
|
||||
continue;
|
||||
|
|
@ -354,7 +369,7 @@ public class ContinuousEffects implements Serializable {
|
|||
replaceEffects.put(effect, applicableAbilities);
|
||||
}
|
||||
}
|
||||
for (Iterator<PreventionEffect> iterator = preventionEffects.iterator(); iterator.hasNext();) {
|
||||
for (Iterator<PreventionEffect> iterator = preventionEffects.iterator(); iterator.hasNext(); ) {
|
||||
PreventionEffect effect = iterator.next();
|
||||
if (!effect.checksEventType(event, game)) {
|
||||
continue;
|
||||
|
|
@ -376,7 +391,7 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
}
|
||||
if (!applicableAbilities.isEmpty()) {
|
||||
replaceEffects.put((ReplacementEffect) effect, applicableAbilities);
|
||||
replaceEffects.put(effect, applicableAbilities);
|
||||
}
|
||||
}
|
||||
return replaceEffects;
|
||||
|
|
@ -478,7 +493,6 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param objectId
|
||||
* @param type
|
||||
* @param affectedAbility
|
||||
|
|
@ -697,10 +711,10 @@ public class ContinuousEffects implements Serializable {
|
|||
* Checks if an event won't happen because of an rule modifying effect
|
||||
*
|
||||
* @param event
|
||||
* @param targetAbility ability the event is attached to. can be null.
|
||||
* @param targetAbility ability the event is attached to. can be null.
|
||||
* @param game
|
||||
* @param checkPlayableMode true if the event does not really happen but
|
||||
* it's checked if the event would be replaced
|
||||
* it's checked if the event would be replaced
|
||||
* @return
|
||||
*/
|
||||
public boolean preventedByRuleModification(GameEvent event, Ability targetAbility, Game game, boolean checkPlayableMode) {
|
||||
|
|
@ -747,7 +761,7 @@ public class ContinuousEffects implements Serializable {
|
|||
do {
|
||||
Map<ReplacementEffect, Set<Ability>> rEffects = getApplicableReplacementEffects(event, game);
|
||||
// Remove all consumed effects (ability dependant)
|
||||
for (Iterator<ReplacementEffect> it1 = rEffects.keySet().iterator(); it1.hasNext();) {
|
||||
for (Iterator<ReplacementEffect> it1 = rEffects.keySet().iterator(); it1.hasNext(); ) {
|
||||
ReplacementEffect entry = it1.next();
|
||||
if (consumed.containsKey(entry.getId()) /*&& !(entry instanceof CommanderReplacementEffect) */) { // 903.9.
|
||||
Set<UUID> consumedAbilitiesIds = consumed.get(entry.getId());
|
||||
|
|
@ -938,7 +952,7 @@ public class ContinuousEffects implements Serializable {
|
|||
|
||||
if (!waitingEffects.isEmpty()) {
|
||||
// check if waiting effects can be applied now
|
||||
for (Iterator<Map.Entry<ContinuousEffect, Set<UUID>>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext();) {
|
||||
for (Iterator<Map.Entry<ContinuousEffect, Set<UUID>>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext(); ) {
|
||||
Map.Entry<ContinuousEffect, Set<UUID>> entry = iterator.next();
|
||||
if (appliedEffects.containsAll(entry.getValue())) { // all dependent to effects are applied now so apply the effect itself
|
||||
appliedAbilities = appliedEffectAbilities.get(entry.getKey());
|
||||
|
|
@ -1059,9 +1073,7 @@ public class ContinuousEffects implements Serializable {
|
|||
final Card card = game.getPermanentOrLKIBattlefield(ability.getSourceId());
|
||||
if (!(effect instanceof BecomesFaceDownCreatureEffect)) {
|
||||
if (card != null) {
|
||||
if (!card.getAbilities(game).contains(ability)) {
|
||||
return false;
|
||||
}
|
||||
return card.getAbilities(game).contains(ability);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -41,10 +41,21 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
|
|||
return new ContinuousEffectsList<>(this);
|
||||
}
|
||||
|
||||
public void removeEndOfTurnEffects() {
|
||||
public void removeEndOfTurnEffects(Game game) {
|
||||
// calls every turn on cleanup step (only end of turn duration)
|
||||
// rules 514.2
|
||||
for (Iterator<T> i = this.iterator(); i.hasNext(); ) {
|
||||
T entry = i.next();
|
||||
if (entry.getDuration() == Duration.EndOfTurn) {
|
||||
boolean canRemove = false;
|
||||
switch (entry.getDuration()) {
|
||||
case EndOfTurn:
|
||||
canRemove = true;
|
||||
break;
|
||||
case UntilEndOfYourNextTurn:
|
||||
canRemove = entry.isYourNextTurn(game);
|
||||
break;
|
||||
}
|
||||
if (canRemove) {
|
||||
i.remove();
|
||||
effectAbilityMap.remove(entry.getId());
|
||||
}
|
||||
|
|
@ -72,6 +83,15 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
|
|||
}
|
||||
}
|
||||
|
||||
public void incYourTurnNumPlayed(Game game) {
|
||||
for (Iterator<T> i = this.iterator(); i.hasNext(); ) {
|
||||
T entry = i.next();
|
||||
if (game.isActivePlayer(entry.getStartingController())) {
|
||||
entry.incYourTurnNumPlayed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInactive(T effect, Game game) {
|
||||
Set<Ability> set = effectAbilityMap.get(effect.getId());
|
||||
if (set == null) {
|
||||
|
|
|
|||
|
|
@ -569,18 +569,20 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
combat.checkForRemoveFromCombat(game);
|
||||
}
|
||||
|
||||
// Remove End of Combat effects
|
||||
// remove end of combat effects
|
||||
public void removeEocEffects(Game game) {
|
||||
effects.removeEndOfCombatEffects();
|
||||
delayed.removeEndOfCombatAbilities();
|
||||
game.applyEffects();
|
||||
}
|
||||
|
||||
// remove end of turn effects
|
||||
public void removeEotEffects(Game game) {
|
||||
effects.removeEndOfTurnEffects();
|
||||
delayed.removeEndOfTurnAbilities();
|
||||
effects.removeEndOfTurnEffects(game);
|
||||
delayed.removeEndOfTurnAbilities(game);
|
||||
exile.cleanupEndOfTurnZones(game);
|
||||
game.applyEffects();
|
||||
effects.incYourTurnNumPlayed(game);
|
||||
}
|
||||
|
||||
public void addEffect(ContinuousEffect effect, Ability source) {
|
||||
|
|
@ -788,7 +790,7 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
public void addCard(Card card) {
|
||||
setZone(card.getId(), Zone.OUTSIDE);
|
||||
for (Ability ability : card.getAbilities()) {
|
||||
addAbility(ability, card);
|
||||
addAbility(ability, null, card);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
|
||||
package mage.game.turn;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.TurnPhase;
|
||||
|
|
@ -18,8 +12,13 @@ import mage.game.stack.StackObject;
|
|||
import mage.players.Player;
|
||||
import mage.util.ThreadLocalStringBuilder;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class Turn implements Serializable {
|
||||
|
|
@ -93,7 +92,6 @@ public class Turn implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param game
|
||||
* @param activePlayer
|
||||
* @return true if turn is skipped
|
||||
|
|
@ -105,6 +103,7 @@ public class Turn implements Serializable {
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (game.getState().getTurnMods().skipTurn(activePlayer.getId())) {
|
||||
game.informPlayers(activePlayer.getLogName() + " skips their turn.");
|
||||
return true;
|
||||
|
|
@ -239,6 +238,7 @@ public class Turn implements Serializable {
|
|||
this.play(game, activePlayerId);
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Used for some spells with end turn effect (e.g. Time Stop).
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue