diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 7ee3f1ebccb..56d4e666592 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -30,8 +30,10 @@ import mage.constants.*; import mage.game.events.PlayerQueryEvent; import mage.players.PlayableObjectStats; import mage.players.PlayableObjectsList; +import mage.util.CardUtil; import mage.util.DebugUtil; import mage.util.MultiAmountMessage; +import mage.util.StreamUtils; import mage.view.*; import org.apache.log4j.Logger; import org.mage.plugins.card.utils.impl.ImageManagerImpl; @@ -53,6 +55,7 @@ import java.util.*; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static mage.client.dialog.PreferencesDialog.*; import static mage.constants.PlayerAction.*; @@ -1810,6 +1813,7 @@ public final class GamePanel extends javax.swing.JPanel { // hand if (needZone == Zone.HAND || needZone == Zone.ALL) { + // my hand for (CardView card : lastGameData.game.getMyHand().values()) { if (needSelectable.contains(card.getId())) { card.setChoosable(true); @@ -1821,6 +1825,34 @@ public final class GamePanel extends javax.swing.JPanel { card.setPlayableStats(needPlayable.getStats(card.getId())); } } + + // opponent hands (switching by GUI's button with my hand) + List list = lastGameData.game.getOpponentHands().values().stream().flatMap(s -> s.values().stream()).collect(Collectors.toList()); + for (SimpleCardView card : list) { + if (needSelectable.contains(card.getId())) { + card.setChoosable(true); + } + if (needChosen.contains(card.getId())) { + card.setSelected(true); + } + if (needPlayable.containsObject(card.getId())) { + card.setPlayableStats(needPlayable.getStats(card.getId())); + } + } + + // watched hands (switching by GUI's button with my hand) + list = lastGameData.game.getWatchedHands().values().stream().flatMap(s -> s.values().stream()).collect(Collectors.toList()); + for (SimpleCardView card : list) { + if (needSelectable.contains(card.getId())) { + card.setChoosable(true); + } + if (needChosen.contains(card.getId())) { + card.setSelected(true); + } + if (needPlayable.containsObject(card.getId())) { + card.setPlayableStats(needPlayable.getStats(card.getId())); + } + } } // stack diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 81547206754..f55dcf7b402 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -37,6 +37,7 @@ import mage.game.tournament.Tournament; import mage.players.Player; import mage.players.PlayerImpl; import mage.players.PlayerList; +import mage.players.net.UserData; import mage.target.Target; import mage.target.TargetAmount; import mage.target.TargetCard; @@ -93,7 +94,7 @@ public class HumanPlayer extends PlayerImpl { // * - GAME thread: open response for income command and wait (go to sleep by response.wait) // * - CALL thread: on closed response - waiting open status of player's response object (if it's too long then cancel the answer) // * - CALL thread: on opened response - save answer to player's response object and notify GAME thread about it by response.notifyAll - // * - GAME thread: on nofify from response - check new answer value and process it (if it bad then repeat and wait the next one); + // * - GAME thread: on notify from response - check new answer value and process it (if it bad then repeat and wait the next one); private transient Boolean responseOpenedForAnswer = false; // GAME thread waiting new answer private transient long responseLastWaitingThreadId = 0; private final transient PlayerResponse response = new PlayerResponse(); @@ -1159,25 +1160,27 @@ public class HumanPlayer extends PlayerImpl { // TODO: change pass and other states like passedUntilStackResolved for controlling player, not for "this" // TODO: check and change all "this" to controling player calls, many bugs with hand, mana, skips - https://github.com/magefree/mage/issues/2088 // TODO: use controlling player in all choose dialogs (and canRespond too, what's with take control of player AI?!) + UserData controllingUserData = this.userData; if (canRespond()) { - HumanPlayer controllingPlayer = this; - if (isGameUnderControl()) { // TODO: must be ! to get real controlling player + if (!isGameUnderControl()) { Player player = game.getPlayer(getTurnControlledBy()); if (player instanceof HumanPlayer) { - controllingPlayer = (HumanPlayer) player; + controllingUserData = player.getUserData(); + } else { + // TODO: add computer opponent here?! } } // TODO: check that all skips and stops used from real controlling player // like holdingPriority (is it a bug here?) if (getJustActivatedType() != null && !holdingPriority) { - if (controllingPlayer.getUserData().isPassPriorityCast() + if (controllingUserData.isPassPriorityCast() && getJustActivatedType() == AbilityType.SPELL) { setJustActivatedType(null); pass(game); return false; } - if (controllingPlayer.getUserData().isPassPriorityActivation() + if (controllingUserData.isPassPriorityActivation() && getJustActivatedType().isNonManaActivatedAbility()) { setJustActivatedType(null); pass(game); @@ -1252,7 +1255,7 @@ public class HumanPlayer extends PlayerImpl { // it's main step if (!skippedAtLeastOnce || (!playerId.equals(game.getActivePlayerId()) - && !controllingPlayer.getUserData().getUserSkipPrioritySteps().isStopOnAllMainPhases())) { + && !controllingUserData.getUserSkipPrioritySteps().isStopOnAllMainPhases())) { skippedAtLeastOnce = true; if (passWithManaPoolCheck(game)) { return false; @@ -1274,8 +1277,7 @@ public class HumanPlayer extends PlayerImpl { // it's end of turn step if (!skippedAtLeastOnce || (playerId.equals(game.getActivePlayerId()) - && !controllingPlayer - .getUserData() + && !controllingUserData .getUserSkipPrioritySteps() .isStopOnAllEndPhases())) { skippedAtLeastOnce = true; @@ -1295,7 +1297,7 @@ public class HumanPlayer extends PlayerImpl { } if (!dontCheckPassStep - && checkPassStep(game, controllingPlayer)) { + && checkPassStep(game, controllingUserData)) { if (passWithManaPoolCheck(game)) { return false; } @@ -1308,8 +1310,7 @@ public class HumanPlayer extends PlayerImpl { if (passedUntilStackResolved) { if (haveNewObjectsOnStack && (playerId.equals(game.getActivePlayerId()) - && controllingPlayer - .getUserData() + && controllingUserData .getUserSkipPrioritySteps() .isStopOnStackNewObjects())) { // new objects on stack -- disable "pass until stack resolved" @@ -1433,17 +1434,17 @@ public class HumanPlayer extends PlayerImpl { return response.getUUID(); } - private boolean checkPassStep(Game game, HumanPlayer controllingPlayer) { + private boolean checkPassStep(Game game, UserData controllingUserData) { try { if (playerId.equals(game.getActivePlayerId())) { - return !controllingPlayer.getUserData().getUserSkipPrioritySteps().getYourTurn().isPhaseStepSet(game.getTurnStepType()); + return !controllingUserData.getUserSkipPrioritySteps().getYourTurn().isPhaseStepSet(game.getTurnStepType()); } else { - return !controllingPlayer.getUserData().getUserSkipPrioritySteps().getOpponentTurn().isPhaseStepSet(game.getTurnStepType()); + return !controllingUserData.getUserSkipPrioritySteps().getOpponentTurn().isPhaseStepSet(game.getTurnStepType()); } } catch (NullPointerException ex) { - if (controllingPlayer.getUserData() != null) { - if (controllingPlayer.getUserData().getUserSkipPrioritySteps() != null) { + if (controllingUserData != null) { + if (controllingUserData.getUserSkipPrioritySteps() != null) { if (game.getStep() != null) { if (game.getTurnStepType() == null) { logger.error("game.getTurnStepType() == null"); @@ -2929,19 +2930,8 @@ public class HumanPlayer extends PlayerImpl { protected boolean passWithManaPoolCheck(Game game) { if (userData.confirmEmptyManaPool() && game.getStack().isEmpty() && getManaPool().count() > 0 && getManaPool().canLostManaOnEmpty()) { - String activePlayerText; - if (game.isActivePlayer(playerId)) { - activePlayerText = "Your turn"; - } else { - activePlayerText = game.getPlayer(game.getActivePlayerId()).getName() + "'s turn"; - } - String priorityPlayerText = ""; - if (!isGameUnderControl()) { - priorityPlayerText = " / priority " + game.getPlayer(game.getPriorityPlayerId()).getName(); - } - // TODO: chooseUse and other dialogs must be under controlling player - if (!chooseUse(Outcome.Detriment, GameLog.getPlayerConfirmColoredText("You still have mana in your mana pool and it will be lose. Pass anyway?") - + GameLog.getSmallSecondLineText(activePlayerText + " / " + game.getTurnStepType().toString() + priorityPlayerText), null, game)) { + String message = GameLog.getPlayerConfirmColoredText("You still have mana in your mana pool and it will be lose. Pass anyway?"); + if (!chooseUse(Outcome.Detriment, message, null, game)) { sendPlayerAction(PlayerAction.PASS_PRIORITY_CANCEL_ALL_ACTIONS, game, null); return false; } diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java index 2a768f4ebd9..868c161ae34 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -906,14 +906,14 @@ public class GameController implements GameCallback { perform(playerId, playerId1 -> getGameSession(playerId1).getMultiAmount(messages, min, max, options)); } - private void informOthers(UUID playerId) { + private void informOthers(UUID waitingPlayerId) { StringBuilder message = new StringBuilder(); if (game.getStep() != null) { message.append(game.getTurnStepType().toString()).append(" - "); } - message.append("Waiting for ").append(game.getPlayer(playerId).getLogName()); + message.append("Waiting for ").append(game.getPlayer(waitingPlayerId).getLogName()); for (final Entry entry : getGameSessionsMap().entrySet()) { - if (!entry.getKey().equals(playerId)) { + if (!entry.getKey().equals(waitingPlayerId)) { entry.getValue().inform(message.toString()); } } @@ -1030,7 +1030,7 @@ public class GameController implements GameCallback { // TODO: if watcher disconnects then game freezes with active timer, must be fix for such use case // same for another player (can be fixed by super-duper connection) if (informOthers) { - informOthers(playerId); + informOthers(realPlayerController.getId()); } } diff --git a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java index 790d85d31db..b4b7bd6c7c3 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java +++ b/Mage.Server/src/main/java/mage/server/game/GameSessionPlayer.java @@ -212,13 +212,14 @@ public class GameSessionPlayer extends GameSessionWatcher { // game view calculation can take some time and can be called from non-game thread, // so use copy for thread save (protection from ConcurrentModificationException) Game sourceGame = game.copy(); - - Player player = sourceGame.getPlayer(playerId); // null for watcher GameView gameView = new GameView(sourceGame.getState(), sourceGame, playerId, null); - if (player != null) { - if (gameView.getPriorityPlayerName().equals(player.getName())) { - gameView.setCanPlayObjects(player.getPlayableObjects(sourceGame, Zone.ALL)); - } + + // playable info (if opponent under control then show opponent's playable) + Player player = sourceGame.getPlayer(playerId); // null for watcher + Player priorityPlayer = sourceGame.getPlayer(sourceGame.getPriorityPlayerId()); + Player controllingPlayer = priorityPlayer == null ? null : sourceGame.getPlayer(priorityPlayer.getTurnControlledBy()); + if (controllingPlayer != null && player == controllingPlayer) { + gameView.setCanPlayObjects(priorityPlayer.getPlayableObjects(sourceGame, Zone.ALL)); } processControlledPlayers(sourceGame, player, gameView); diff --git a/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java b/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java index f37ff6f5aac..e69ee664e21 100644 --- a/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java +++ b/Mage.Tests/src/test/java/org/mage/test/load/LoadCallbackClient.java @@ -104,18 +104,17 @@ public class LoadCallbackClient implements CallbackClient { GameClientMessage message = (GameClientMessage) callback.getData(); this.gameView = message.getGameView(); log.info(getLogStartInfo() + " target: " + message.getMessage()); - switch (message.getMessage()) { - case "Select a starting player": - session.sendPlayerUUID(gameId, playerId); - return; - case "Select a card to discard": - log.info(getLogStartInfo() + "hand size: " + gameView.getMyHand().size()); - SimpleCardView card = gameView.getMyHand().values().iterator().next(); - session.sendPlayerUUID(gameId, card.getId()); - return; - default: - log.error(getLogStartInfo() + "unknown GAME_TARGET message: " + message.toString()); - return; + if (message.getMessage().startsWith("Select a starting player")) { + session.sendPlayerUUID(gameId, playerId); + return; + } else if (message.getMessage().startsWith("Select a card to discard")) { + log.info(getLogStartInfo() + "hand size: " + gameView.getMyHand().size()); + SimpleCardView card = gameView.getMyHand().values().iterator().next(); + session.sendPlayerUUID(gameId, card.getId()); + return; + } else { + log.error(getLogStartInfo() + "unknown GAME_TARGET message: " + message.toString()); + return; } } diff --git a/Mage/src/main/java/mage/abilities/common/delayed/AtTheEndOfTurnStepPostDelayedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/delayed/AtTheEndOfTurnStepPostDelayedTriggeredAbility.java deleted file mode 100644 index d0078125fa3..00000000000 --- a/Mage/src/main/java/mage/abilities/common/delayed/AtTheEndOfTurnStepPostDelayedTriggeredAbility.java +++ /dev/null @@ -1,42 +0,0 @@ - -package mage.abilities.common.delayed; - -import mage.abilities.DelayedTriggeredAbility; -import mage.abilities.effects.Effect; -import mage.game.Game; -import mage.game.events.GameEvent; - -/** - * @author nantuko - */ -public class AtTheEndOfTurnStepPostDelayedTriggeredAbility extends DelayedTriggeredAbility { - - public AtTheEndOfTurnStepPostDelayedTriggeredAbility(Effect effect) { - this(effect, false); - } - - public AtTheEndOfTurnStepPostDelayedTriggeredAbility(Effect effect, boolean usesStack) { - super(effect); - this.usesStack = usesStack; - setTriggerPhrase("At end of turn "); - } - - public AtTheEndOfTurnStepPostDelayedTriggeredAbility(AtTheEndOfTurnStepPostDelayedTriggeredAbility ability) { - super(ability); - } - - @Override - public AtTheEndOfTurnStepPostDelayedTriggeredAbility copy() { - return new AtTheEndOfTurnStepPostDelayedTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.END_TURN_STEP_POST; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - return true; - } -} diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index c6d8c2b0aa0..0dd53c256e2 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -550,6 +550,12 @@ public interface Game extends MageItem, Serializable, Copyable { @Deprecated // TODO: must research usage and remove it from all non engine code (example: Bestow ability, ProcessActions must be used instead) boolean checkStateAndTriggered(); + /** + * Play priority by all players + * + * @param activePlayerId starting priority player + * @param resuming false to reset passed priority and ask it again + */ void playPriority(UUID activePlayerId, boolean resuming); void resetControlAfterSpellResolve(UUID topId); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 66ea48734b6..dc2cb22fbaa 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2987,21 +2987,35 @@ public abstract class GameImpl implements Game { } String message; if (this.canPlaySorcery(playerId)) { - message = "Play spells and abilities."; + message = "Play spells and abilities"; } else { - message = "Play instants and activated abilities."; + message = "Play instants and activated abilities"; } - playerQueryEventSource.select(playerId, message); + + message += getControllingPlayerHint(playerId); + + Player player = this.getPlayer(playerId); + playerQueryEventSource.select(player.getTurnControlledBy(), message); getState().clearLookedAt(); getState().clearRevealed(); } + private String getControllingPlayerHint(UUID playerId) { + Player player = this.getPlayer(playerId); + Player controllingPlayer = this.getPlayer(player.getTurnControlledBy()); + if (player != controllingPlayer) { + return " (as " + player.getLogName() + ")"; + } else { + return ""; + } + } + @Override public synchronized void fireSelectEvent(UUID playerId, String message) { if (simulation) { return; } - playerQueryEventSource.select(playerId, message); + playerQueryEventSource.select(playerId, message + getControllingPlayerHint(playerId)); } @Override @@ -3009,7 +3023,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.select(playerId, message, options); + playerQueryEventSource.select(playerId, message + getControllingPlayerHint(playerId), options); } @Override @@ -3017,7 +3031,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.playMana(playerId, message, options); + playerQueryEventSource.playMana(playerId, message + getControllingPlayerHint(playerId), options); } @Override @@ -3025,7 +3039,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.playXMana(playerId, message); + playerQueryEventSource.playXMana(playerId, message + getControllingPlayerHint(playerId)); } @Override @@ -3038,7 +3052,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.ask(playerId, message.getMessage(), source, addMessageToOptions(message, options)); + playerQueryEventSource.ask(playerId, message.getMessage() + getControllingPlayerHint(playerId), source, addMessageToOptions(message, options)); } @Override @@ -3050,7 +3064,7 @@ public abstract class GameImpl implements Game { if (object != null) { objectName = object.getName(); } - playerQueryEventSource.chooseAbility(playerId, message, objectName, choices); + playerQueryEventSource.chooseAbility(playerId, message + getControllingPlayerHint(playerId), objectName, choices); } @Override @@ -3058,7 +3072,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.chooseMode(playerId, message, modes); + playerQueryEventSource.chooseMode(playerId, message + getControllingPlayerHint(playerId), modes); } @Override @@ -3066,7 +3080,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.target(playerId, message.getMessage(), targets, required, addMessageToOptions(message, options)); + playerQueryEventSource.target(playerId, message.getMessage() + getControllingPlayerHint(playerId), targets, required, addMessageToOptions(message, options)); } @Override @@ -3074,7 +3088,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.target(playerId, message.getMessage(), cards, required, addMessageToOptions(message, options)); + playerQueryEventSource.target(playerId, message.getMessage() + getControllingPlayerHint(playerId), cards, required, addMessageToOptions(message, options)); } /** @@ -3087,7 +3101,7 @@ public abstract class GameImpl implements Game { */ @Override public void fireSelectTargetTriggeredAbilityEvent(UUID playerId, String message, List abilities) { - playerQueryEventSource.target(playerId, message, abilities); + playerQueryEventSource.target(playerId, message + getControllingPlayerHint(playerId), abilities); } @Override @@ -3095,7 +3109,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.target(playerId, message, perms, required); + playerQueryEventSource.target(playerId, message + getControllingPlayerHint(playerId), perms, required); } @Override @@ -3103,7 +3117,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.amount(playerId, message, min, max); + playerQueryEventSource.amount(playerId, message + getControllingPlayerHint(playerId), min, max); } @Override @@ -3128,7 +3142,7 @@ public abstract class GameImpl implements Game { if (simulation) { return; } - playerQueryEventSource.choosePile(playerId, message, pile1, pile2); + playerQueryEventSource.choosePile(playerId, message + getControllingPlayerHint(playerId), pile1, pile2); } @Override diff --git a/Mage/src/main/java/mage/game/turn/BeginningPhase.java b/Mage/src/main/java/mage/game/turn/BeginningPhase.java index c62795ccc74..5a7da443928 100644 --- a/Mage/src/main/java/mage/game/turn/BeginningPhase.java +++ b/Mage/src/main/java/mage/game/turn/BeginningPhase.java @@ -1,5 +1,3 @@ - - package mage.game.turn; import mage.constants.TurnPhase; diff --git a/Mage/src/main/java/mage/game/turn/CleanupStep.java b/Mage/src/main/java/mage/game/turn/CleanupStep.java index 5bfe000c562..be102a7816c 100644 --- a/Mage/src/main/java/mage/game/turn/CleanupStep.java +++ b/Mage/src/main/java/mage/game/turn/CleanupStep.java @@ -1,14 +1,12 @@ - - package mage.game.turn; -import java.util.UUID; - import mage.constants.PhaseStep; import mage.game.Game; import mage.game.events.GameEvent.EventType; import mage.players.Player; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -30,19 +28,31 @@ public class CleanupStep extends Step { super.beginStep(game, activePlayerId); Player activePlayer = game.getPlayer(activePlayerId); game.getState().setPriorityPlayerId(activePlayer.getId()); - //20091005 - 514.1 + + // 514.1 + // First, if the active player’s hand contains more cards than his or her maximum hand size + // (normally seven), he or she discards enough cards to reduce his or her hand size to that number. + // This turn-based action doesn’t use the stack. if (activePlayer.isInGame()) { activePlayer.discardToMax(game); } - //20100423 - 514.2 + + // 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. This turn-based action doesn’t use the stack. game.getBattlefield().endOfTurn(game); game.getState().removeEotEffects(game); + + // 514.3 + // Normally, no player receives priority during the cleanup step, so no spells can be cast + // and no abilities can be activated. However, this rule is subject to the following exception: 514.3a + // + // Look at EndPhase code to process 514.3 } @Override public void endStep(Game game, UUID activePlayerId) { - Player activePlayer = game.getPlayer(activePlayerId); - activePlayer.setGameUnderYourControl(true); super.endStep(game, activePlayerId); } diff --git a/Mage/src/main/java/mage/game/turn/EndPhase.java b/Mage/src/main/java/mage/game/turn/EndPhase.java index 03e69c4e8bc..d9dc0431cb5 100644 --- a/Mage/src/main/java/mage/game/turn/EndPhase.java +++ b/Mage/src/main/java/mage/game/turn/EndPhase.java @@ -31,16 +31,21 @@ public class EndPhase extends Phase { game.getState().increaseStepNum(); game.getTurn().setEndTurnRequested(false); // so triggers trigger again prePriority(game, activePlayerId); - // 514.3a At this point, the game checks to see if any state-based actions would be performed + + // 514.3. + // Normally, no player receives priority during the cleanup step, so no spells can be cast and + // no abilities can be activated. However, this rule is subject to the following exception: + // 514.3a + // At this point, the game checks to see if any state-based actions would be performed // and/or any triggered abilities are waiting to be put onto the stack (including those that // trigger "at the beginning of the next cleanup step"). If so, those state-based actions are // performed, then those triggered abilities are put on the stack, then the active player gets // priority. Players may cast spells and activate abilities. Once the stack is empty and all players // pass in succession, another cleanup step begins if (game.checkStateAndTriggered()) { - // Queues a new cleanup step + game.informPlayers("State-based actions or triggers happened on cleanup step, so players get priority due 514.3a"); + // queues a new cleanup step and request new priorities game.getState().getTurnMods().add(new TurnMod(activePlayerId).withExtraStep(new CleanupStep())); - // resume priority if (!game.isPaused() && !game.checkIfGameIsOver() && !game.executingRollback()) { currentStep.priority(game, activePlayerId, false); if (game.executingRollback()) { diff --git a/Mage/src/main/java/mage/game/turn/Step.java b/Mage/src/main/java/mage/game/turn/Step.java index 24e6e7debf6..56c18402355 100644 --- a/Mage/src/main/java/mage/game/turn/Step.java +++ b/Mage/src/main/java/mage/game/turn/Step.java @@ -60,6 +60,12 @@ public abstract class Step implements Serializable, Copyable { stepPart = StepPart.PRE; } + /** + * Play priority by all players + * + * @param activePlayerId starting priority player + * @param resuming false to reset passed priority and ask it again + */ public void priority(Game game, UUID activePlayerId, boolean resuming) { if (hasPriority) { stepPart = StepPart.PRIORITY; diff --git a/Mage/src/main/java/mage/game/turn/Turn.java b/Mage/src/main/java/mage/game/turn/Turn.java index 4c1d76fac3e..1ada5011092 100644 --- a/Mage/src/main/java/mage/game/turn/Turn.java +++ b/Mage/src/main/java/mage/game/turn/Turn.java @@ -107,12 +107,16 @@ public class Turn implements Serializable { )); return true; } - logStartOfTurn(game, activePlayer); - checkTurnIsControlledByOtherPlayer(game, activePlayer.getId()); + logStartOfTurn(game, activePlayer); + resetCounts(); this.activePlayerId = activePlayer.getId(); - resetCounts(); + this.currentPhase = null; + + // turn control must be called after potential turn skip due 720.1. + checkTurnIsControlledByOtherPlayer(game, activePlayer.getId()); + game.getPlayer(activePlayer.getId()).beginTurn(game); for (Phase phase : phases) { if (game.isPaused() || game.checkIfGameIsOver()) { @@ -220,8 +224,31 @@ public class Turn implements Serializable { } private void checkTurnIsControlledByOtherPlayer(Game game, UUID activePlayerId) { + // 720.1. + // Some cards allow a player to control another player during that player’s next turn. + // This effect applies to the next turn that the affected player actually takes. + // The affected player is controlled during the entire turn; the effect doesn’t end until + // the beginning of the next turn. + // + // 720.1b + // If a turn is skipped, any pending player-controlling effects wait until the player who would be + // affected actually takes a turn. + + // remove old under control + game.getPlayers().values().forEach(player -> { + if (player.isInGame() && !player.isGameUnderControl()) { + Player controllingPlayer = game.getPlayer(player.getTurnControlledBy()); + if (player != controllingPlayer && controllingPlayer != null) { + game.informPlayers(controllingPlayer.getLogName() + " lost control over " + player.getLogName()); + } + player.setGameUnderYourControl(true); + } + }); + + // add new under control TurnMod newControllerMod = game.getState().getTurnMods().useNextNewController(activePlayerId); if (newControllerMod != null && !newControllerMod.getNewControllerId().equals(activePlayerId)) { + // set player under new control // game logs added in child's call (controlPlayersTurn) game.getPlayer(newControllerMod.getNewControllerId()).controlPlayersTurn(game, activePlayerId, newControllerMod.getInfo()); } diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 257f6443dc7..3f3073986d2 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -366,7 +366,7 @@ public interface Player extends MageItem, Copyable { boolean isGameUnderControl(); /** - * Returns false in case you don't control the game. + * False in case you don't control the game. *

* Note: For effects like "You control target player during that player's * next turn". diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 4492b47d95a..87e0bac8875 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -7,7 +7,6 @@ import mage.abilities.ActivatedAbility.ActivationStatus; import mage.abilities.common.PassAbility; import mage.abilities.common.PlayLandAsCommanderAbility; import mage.abilities.common.WhileSearchingPlayFromLibraryAbility; -import mage.abilities.common.delayed.AtTheEndOfTurnStepPostDelayedTriggeredAbility; import mage.abilities.costs.*; import mage.abilities.costs.mana.AlternateManaPaymentAbility; import mage.abilities.costs.mana.ManaCost; @@ -15,7 +14,6 @@ import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.RestrictionEffect; import mage.abilities.effects.RestrictionUntapNotMoreThanEffect; -import mage.abilities.effects.common.LoseControlOnOtherPlayersControllerEffect; import mage.abilities.keyword.*; import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.abilities.mana.ManaOptions; @@ -609,11 +607,7 @@ public abstract class PlayerImpl implements Player, Serializable { if (!playerUnderControl.hasLeft() && !playerUnderControl.hasLost()) { playerUnderControl.setGameUnderYourControl(false); } - DelayedTriggeredAbility ability = new AtTheEndOfTurnStepPostDelayedTriggeredAbility( - new LoseControlOnOtherPlayersControllerEffect(this.getLogName(), playerUnderControl.getLogName())); - ability.setSourceId(getId()); - ability.setControllerId(getId()); - game.addDelayedTriggeredAbility(ability, null); + // control will reset on start of the turn } } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 609861d1f91..d77bdca4a6a 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -2127,9 +2127,10 @@ public final class CardUtil { return null; } - // not started game + // T0 - for not started game + // T2 - for starting of the turn if (gameState.getTurn().getStep() == null) { - return "T0"; + return "T" + gameState.getTurnNum(); } // normal game diff --git a/Mage/src/main/java/mage/util/GameLog.java b/Mage/src/main/java/mage/util/GameLog.java index 4e12e9fe55d..b2a6bae3b27 100644 --- a/Mage/src/main/java/mage/util/GameLog.java +++ b/Mage/src/main/java/mage/util/GameLog.java @@ -160,10 +160,6 @@ public final class GameLog { return "" + name + ""; } - public static String getSmallSecondLineText(String text) { - return "

" + text + "
"; - } - private static String getColorName(ObjectColor objectColor) { if (objectColor.isMulticolored()) { return LOG_COLOR_MULTI;