diff --git a/.gitattributes b/.gitattributes index b1b20e0b8e0..095103b6848 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16015,6 +16015,7 @@ forge-m-base/libs/gdx-freetype.jar -text forge-m-base/libs/gdx-sources.jar -text forge-m-base/libs/gdx.jar -text forge-m-base/pom.xml -text +forge-m-base/src/forge/FThreads.java -text forge-m-base/src/forge/Forge.java -text forge-m-base/src/forge/assets/FImage.java -text forge-m-base/src/forge/assets/FSkin.java -text @@ -16034,6 +16035,21 @@ forge-m-base/src/forge/model/CardCollections.java -text forge-m-base/src/forge/model/FModel.java -text forge-m-base/src/forge/model/MetaSet.java -text forge-m-base/src/forge/model/UnOpenedMeta.java -text +forge-m-base/src/forge/net/FServer.java -text +forge-m-base/src/forge/net/IClientSocket.java -text +forge-m-base/src/forge/net/IConnectionObserver.java -text +forge-m-base/src/forge/net/Lobby.java -text +forge-m-base/src/forge/net/LobbyPlayerRemote.java -text +forge-m-base/src/forge/net/client/INetClient.java -text +forge-m-base/src/forge/net/client/InvalidFieldInPacketException.java -text +forge-m-base/src/forge/net/client/NetClient.java -text +forge-m-base/src/forge/net/client/state/ConnectedClientState.java -text +forge-m-base/src/forge/net/client/state/IClientState.java -text +forge-m-base/src/forge/net/client/state/InLobbyClientState.java -text +forge-m-base/src/forge/net/client/state/UnauthorizedClientState.java -text +forge-m-base/src/forge/player/HumanCostDecision.java -text +forge-m-base/src/forge/player/HumanPlay.java -text +forge-m-base/src/forge/player/HumanPlaySpellAbility.java -text forge-m-base/src/forge/player/LobbyPlayerHuman.java -text forge-m-base/src/forge/player/PlayerControllerHuman.java -text forge-m-base/src/forge/screens/FScreen.java -text @@ -16044,11 +16060,41 @@ forge-m-base/src/forge/screens/draft/DraftScreen.java -text forge-m-base/src/forge/screens/guantlet/GuantletScreen.java -text forge-m-base/src/forge/screens/home/HomeScreen.java -text forge-m-base/src/forge/screens/match/FControl.java -text +forge-m-base/src/forge/screens/match/FControlGameEventHandler.java -text +forge-m-base/src/forge/screens/match/FControlGamePlayback.java -text forge-m-base/src/forge/screens/match/MatchScreen.java -text +forge-m-base/src/forge/screens/match/events/IUiEventVisitor.java -text +forge-m-base/src/forge/screens/match/events/UiEvent.java -text +forge-m-base/src/forge/screens/match/events/UiEventAttackerDeclared.java -text +forge-m-base/src/forge/screens/match/events/UiEventBlockerAssigned.java -text +forge-m-base/src/forge/screens/match/input/ButtonUtil.java -text +forge-m-base/src/forge/screens/match/input/Input.java -text +forge-m-base/src/forge/screens/match/input/InputAttack.java -text +forge-m-base/src/forge/screens/match/input/InputBase.java -text +forge-m-base/src/forge/screens/match/input/InputBlock.java -text +forge-m-base/src/forge/screens/match/input/InputConfirm.java -text +forge-m-base/src/forge/screens/match/input/InputConfirmMulligan.java -text +forge-m-base/src/forge/screens/match/input/InputLockUI.java -text +forge-m-base/src/forge/screens/match/input/InputPassPriority.java -text +forge-m-base/src/forge/screens/match/input/InputPayMana.java -text +forge-m-base/src/forge/screens/match/input/InputPayManaOfCostPayment.java -text +forge-m-base/src/forge/screens/match/input/InputPayManaSimple.java -text +forge-m-base/src/forge/screens/match/input/InputPayManaX.java -text +forge-m-base/src/forge/screens/match/input/InputPlaybackControl.java -text +forge-m-base/src/forge/screens/match/input/InputProliferate.java -text +forge-m-base/src/forge/screens/match/input/InputProxy.java -text +forge-m-base/src/forge/screens/match/input/InputQueue.java -text +forge-m-base/src/forge/screens/match/input/InputSelectCardsForConvoke.java -text +forge-m-base/src/forge/screens/match/input/InputSelectCardsFromList.java -text +forge-m-base/src/forge/screens/match/input/InputSelectEntitiesFromList.java -text +forge-m-base/src/forge/screens/match/input/InputSelectManyBase.java -text +forge-m-base/src/forge/screens/match/input/InputSelectTargets.java -text +forge-m-base/src/forge/screens/match/input/InputSynchronized.java -text +forge-m-base/src/forge/screens/match/input/InputSyncronizedBase.java -text forge-m-base/src/forge/screens/match/views/VAvatar.java -text forge-m-base/src/forge/screens/match/views/VField.java -text forge-m-base/src/forge/screens/match/views/VLog.java -text -forge-m-base/src/forge/screens/match/views/VPhases.java -text +forge-m-base/src/forge/screens/match/views/VPhaseIndicator.java -text forge-m-base/src/forge/screens/match/views/VPlayerPanel.java -text forge-m-base/src/forge/screens/match/views/VPrompt.java -text forge-m-base/src/forge/screens/match/views/VStack.java -text @@ -16068,7 +16114,9 @@ forge-m-base/src/forge/toolbox/FOverlay.java -text forge-m-base/src/forge/toolbox/FProgressBar.java -text forge-m-base/src/forge/toolbox/FScrollPane.java -text forge-m-base/src/forge/toolbox/GuiChoose.java -text +forge-m-base/src/forge/toolbox/GuiDialog.java -text forge-m-base/src/forge/utils/Constants.java -text +forge-m-base/src/forge/utils/Evaluator.java -text forge-m-base/src/forge/utils/ForgePreferences.java -text forge-m-base/src/forge/utils/ForgeProfileProperties.java -text forge-m-base/src/forge/utils/Preferences.java -text diff --git a/forge-m-base/src/forge/FThreads.java b/forge-m-base/src/forge/FThreads.java new file mode 100644 index 00000000000..5394723501d --- /dev/null +++ b/forge-m-base/src/forge/FThreads.java @@ -0,0 +1,101 @@ +package forge; + +import java.io.PrintStream; + +import forge.util.ThreadUtil; + +/** + * TODO: Write javadoc for this type. + * + */ +public class FThreads { + private FThreads() { } //don't allow creating instance + + public static void assertExecutedByEdt(final boolean mustBeEDT) { + //TODO + } + + public static void invokeInEdtLater(Runnable runnable) { + //SwingUtilities.invokeLater(runnable); + } + + public static void invokeInEdtNowOrLater(Runnable proc) { + if (isGuiThread()) { + proc.run(); + } + else { + invokeInEdtLater(proc); + } + } + + public static void invokeInEdtAndWait(final Runnable proc) { + proc.run(); + /*if (SwingUtilities.isEventDispatchThread()) { + // Just run in the current thread. + proc.run(); + } + else { + try { + SwingUtilities.invokeAndWait(proc); + } + catch (final InterruptedException exn) { + throw new RuntimeException(exn); + } + catch (final InvocationTargetException exn) { + throw new RuntimeException(exn); + } + }*/ + } + + public static boolean isGuiThread() { + return true; + //return SwingUtilities.isEventDispatchThread(); + } + + public static void delayInEDT(int milliseconds, final Runnable inputUpdater) { + Runnable runInEdt = new Runnable() { + @Override + public void run() { + FThreads.invokeInEdtNowOrLater(inputUpdater); + } + }; + ThreadUtil.delay(milliseconds, runInEdt); + } + + public static String debugGetCurrThreadId() { + return isGuiThread() ? "EDT" : Thread.currentThread().getName(); + } + + public static String prependThreadId(String message) { + return debugGetCurrThreadId() + " > " + message; + } + + public static void dumpStackTrace(PrintStream stream) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + stream.printf("%s > %s called from %s%n", debugGetCurrThreadId(), + trace[2].getClassName() + "." + trace[2].getMethodName(), trace[3].toString()); + int i = 0; + for (StackTraceElement se : trace) { + if (i<2) { i++; } + else { stream.println(se.toString()); } + } + } + + public static String debugGetStackTraceItem(int depth, boolean shorter) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + String lastItem = trace[depth].toString(); + if (shorter) { + int lastPeriod = lastItem.lastIndexOf('.'); + lastPeriod = lastItem.lastIndexOf('.', lastPeriod-1); + lastPeriod = lastItem.lastIndexOf('.', lastPeriod-1); + lastItem = lastItem.substring(lastPeriod+1); + return String.format("%s > from %s", debugGetCurrThreadId(), lastItem); + } + return String.format("%s > %s called from %s", debugGetCurrThreadId(), + trace[2].getClassName() + "." + trace[2].getMethodName(), lastItem); + } + + public static String debugGetStackTraceItem(int depth) { + return debugGetStackTraceItem(depth, false); + } +} diff --git a/forge-m-base/src/forge/model/FModel.java b/forge-m-base/src/forge/model/FModel.java index 077784fe8b8..c54afd31ef2 100644 --- a/forge-m-base/src/forge/model/FModel.java +++ b/forge-m-base/src/forge/model/FModel.java @@ -45,6 +45,8 @@ import com.badlogic.gdx.Gdx; * this class must be either private or public static final. */ public class FModel { + private FModel() { } //don't allow creating instance + private static StaticData magicDb; private static PrintStream oldSystemOut; diff --git a/forge-m-base/src/forge/net/FServer.java b/forge-m-base/src/forge/net/FServer.java new file mode 100644 index 00000000000..8221140f635 --- /dev/null +++ b/forge-m-base/src/forge/net/FServer.java @@ -0,0 +1,32 @@ +package forge.net; + +import com.google.common.base.Supplier; + +import forge.game.player.LobbyPlayer; +import forge.player.LobbyPlayerHuman; + +public class FServer { + private FServer() { } //don't allow creating instance + + private static Lobby lobby; + + public static Lobby getLobby() { + if (lobby == null) { + //not a very good solution still + lobby = new Lobby(new Supplier() { + @Override + public LobbyPlayer get() { + return new LobbyPlayerHuman("Human"); + } + }); + } + return lobby; + } + + /*private final NetServer server = new NetServer(); + + public NetServer getServer() { + // TODO Auto-generated method stub + return server; + }*/ +} diff --git a/forge-m-base/src/forge/net/IClientSocket.java b/forge-m-base/src/forge/net/IClientSocket.java new file mode 100644 index 00000000000..e99e1eae305 --- /dev/null +++ b/forge-m-base/src/forge/net/IClientSocket.java @@ -0,0 +1,7 @@ +package forge.net; + +public interface IClientSocket { + boolean isOpen(); + void send(String message); + void close(String farewell); +} \ No newline at end of file diff --git a/forge-m-base/src/forge/net/IConnectionObserver.java b/forge-m-base/src/forge/net/IConnectionObserver.java new file mode 100644 index 00000000000..e5e7dc83881 --- /dev/null +++ b/forge-m-base/src/forge/net/IConnectionObserver.java @@ -0,0 +1,9 @@ +package forge.net; + +public interface IConnectionObserver { + /** Notifies that the client is gone, it's too late to send anything */ + public void onConnectionClosed(); + + /** Notifies of an incoming message */ + public void onMessage(String data); +} \ No newline at end of file diff --git a/forge-m-base/src/forge/net/Lobby.java b/forge-m-base/src/forge/net/Lobby.java new file mode 100644 index 00000000000..7aa4e3f0f06 --- /dev/null +++ b/forge-m-base/src/forge/net/Lobby.java @@ -0,0 +1,87 @@ +package forge.net; + +import com.google.common.base.Supplier; + +import forge.ai.AiProfileUtil; +import forge.ai.LobbyPlayerAi; +import forge.assets.FSkin; +import forge.game.player.LobbyPlayer; +import forge.model.FModel; +import forge.net.client.INetClient; +import forge.util.MyRandom; +import forge.utils.ForgePreferences.FPref; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class Lobby { + private final LobbyPlayer guiPlayer; + + public Lobby(Supplier humanFactory){ + guiPlayer = humanFactory.get(); + } + + private Map remotePlayers = new ConcurrentHashMap(); + + //private final LobbyPlayerAi system = new LobbyPlayerAi("System"); + + public final LobbyPlayer getGuiPlayer() { + return guiPlayer; + } + + public final LobbyPlayer getAiPlayer() { return getAiPlayer(getRandomName()); } + public final LobbyPlayer getAiPlayer(String name) { + return getAiPlayer(name, FSkin.isLoaded() ? MyRandom.getRandom().nextInt(FSkin.getAvatars().size()) : 0); + } + public final LobbyPlayer getAiPlayer(String name, int avatarIndex) { + LobbyPlayerAi player = new LobbyPlayerAi(name); + + // TODO: implement specific AI profiles for quest mode. + String lastProfileChosen = FModel.getPreferences().getPref(FPref.UI_CURRENT_AI_PROFILE); + player.setRotateProfileEachGame(lastProfileChosen.equals(AiProfileUtil.AI_PROFILE_RANDOM_DUEL)); + if(lastProfileChosen.equals(AiProfileUtil.AI_PROFILE_RANDOM_MATCH)) { + lastProfileChosen = AiProfileUtil.getRandomProfile(); + System.out.println(String.format("AI profile %s was chosen for the lobby player %s.", lastProfileChosen, player.getName())); + } + player.setAiProfile(lastProfileChosen); + + if(FSkin.isLoaded()) + player.setAvatarIndex(avatarIndex); + return player; + } + + /** Returns a random name from the supplied list. */ + private String getRandomName() { + return "Bob"; + /*String playerName = GuiDisplayUtil.getPlayerName(); + String aiName = NameGenerator.getRandomName("Any", "Generic", playerName); + return aiName;*/ + } + + public LobbyPlayer getQuestPlayer() { + return guiPlayer; + } + + public synchronized LobbyPlayer findOrCreateRemotePlayer(String name, INetClient client) { + if (remotePlayers.containsKey(name)) + return remotePlayers.get(name); + + LobbyPlayerRemote res = new LobbyPlayerRemote(name, client); + //speak(ChatArea.Room, system, res.getName() + " has joined the server."); + // have to load avatar from remote user's preferences here + remotePlayers.put(name, res); + + return res; + } + + public void disconnectPlayer(LobbyPlayer player) { + // Should set up a timer here to discard player and all of his games after 20 minutes of being offline + } + + /*public void speak(ChatArea room, LobbyPlayer player, String message) { + getGuiPlayer().hear(player, message); + for(LobbyPlayer remote : remotePlayers.values()) { + remote.hear(player, message); + } + }*/ +} diff --git a/forge-m-base/src/forge/net/LobbyPlayerRemote.java b/forge-m-base/src/forge/net/LobbyPlayerRemote.java new file mode 100644 index 00000000000..2812e7906d1 --- /dev/null +++ b/forge-m-base/src/forge/net/LobbyPlayerRemote.java @@ -0,0 +1,46 @@ +package forge.net; + +import forge.game.Game; +import forge.game.player.LobbyPlayer; +import forge.game.player.Player; +import forge.game.player.PlayerController; +import forge.net.client.INetClient; +import forge.net.protocol.toclient.ChatPacketClt; + +public class LobbyPlayerRemote extends LobbyPlayer { + + private final INetClient connection; + + public LobbyPlayerRemote(String name, INetClient netClient) { // This is actually a doubtful idea - this means 1 window per player. + super(name); + connection = netClient; + } + + @Override + protected PlayerType getType() { + return PlayerType.REMOTE; + } + + /* (non-Javadoc) + * @see forge.game.player.LobbyPlayer#getPlayer(forge.game.GameState) + */ + @Override + public Player createIngamePlayer(Game gameState) { + // Cannot create remote players yet + throw new UnsupportedOperationException("method is not implemented"); + } + + @Override + public void hear(LobbyPlayer player, String message) { + connection.send(new ChatPacketClt(player.getName(), message)); + } + + /* (non-Javadoc) + * @see forge.game.player.LobbyPlayer#createControllerFor(forge.game.player.Player) + */ + @Override + public PlayerController createControllerFor(Player p) { + // Cannot create remote players yet + throw new UnsupportedOperationException("method is not implemented"); + } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/net/client/INetClient.java b/forge-m-base/src/forge/net/client/INetClient.java new file mode 100644 index 00000000000..4140fd3642e --- /dev/null +++ b/forge-m-base/src/forge/net/client/INetClient.java @@ -0,0 +1,25 @@ +package forge.net.client; + +import forge.game.player.LobbyPlayer; +import forge.net.client.state.IClientState; +import forge.net.protocol.toclient.IPacketClt; + +/** + * TODO: Write javadoc for this type. + * + */ +public interface INetClient { + + /** + * TODO: Write javadoc for this method. + * @param echoMessage + */ + void send(IPacketClt message); + + + void createPlayer(String playerName); + LobbyPlayer getPlayer(); + + + void replaceState(IClientState old, IClientState newState); +} diff --git a/forge-m-base/src/forge/net/client/InvalidFieldInPacketException.java b/forge-m-base/src/forge/net/client/InvalidFieldInPacketException.java new file mode 100644 index 00000000000..afd4bc534ac --- /dev/null +++ b/forge-m-base/src/forge/net/client/InvalidFieldInPacketException.java @@ -0,0 +1,29 @@ +package forge.net.client; + +/** + * Indicates incorrect field in an incoming packet + */ +public class InvalidFieldInPacketException extends RuntimeException { + + private static final long serialVersionUID = 4505312413627923468L; + + + /** + * TODO: Write javadoc for Constructor. + * @param message + */ + public InvalidFieldInPacketException(String message) { + super(message); + } + + + /** + * TODO: Write javadoc for Constructor. + * @param message + * @param cause + */ + public InvalidFieldInPacketException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/forge-m-base/src/forge/net/client/NetClient.java b/forge-m-base/src/forge/net/client/NetClient.java new file mode 100644 index 00000000000..5eff06e9f6e --- /dev/null +++ b/forge-m-base/src/forge/net/client/NetClient.java @@ -0,0 +1,98 @@ +package forge.net.client; + +import forge.game.player.LobbyPlayer; +import forge.net.FServer; +import forge.net.IClientSocket; +import forge.net.IConnectionObserver; +import forge.net.client.state.ConnectedClientState; +import forge.net.client.state.IClientState; +import forge.net.client.state.UnauthorizedClientState; +import forge.net.protocol.ClientProtocol; +import forge.net.protocol.ClientProtocolJson; +import forge.net.protocol.toclient.ErrorIncorrectPacketClt; +import forge.net.protocol.toclient.ErrorNoStateForPacketClt; +import forge.net.protocol.toclient.IPacketClt; +import forge.net.protocol.toclient.WelcomePacketClt; +import forge.net.protocol.toserver.IPacketSrv; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; + +public class NetClient implements IConnectionObserver, INetClient{ + + private final IClientSocket socket; + private final BlockingDeque state = new LinkedBlockingDeque(); + private LobbyPlayer player = null; + private final ClientProtocol protocol; + + + public NetClient(IClientSocket clientSocket) { + socket = clientSocket; + state.push(new ConnectedClientState(this)); + state.push(new UnauthorizedClientState(this)); + protocol = new ClientProtocolJson(); + send(new WelcomePacketClt("Welcome to server")); + } + + public void autorized() { + + } + + /* (non-Javadoc) + * @see forge.net.client.IConnectionObserver#onConnectionClosed() + */ + @Override + public void onConnectionClosed() { + // Tell the game, the client is gone. + if ( player != null ) { + FServer.getLobby().disconnectPlayer(player); + } + } + + + @Override + public final LobbyPlayer getPlayer() { + return player; + } + + /** Receives input from network client */ + @Override + public void onMessage(String data) { + IPacketSrv p = protocol.decodePacket(data); + boolean processed = false; + try{ + for(IClientState s : state) { + if ( s.processPacket(p) ) { processed = true; break; } + } + if (!processed) + send(new ErrorNoStateForPacketClt(p.getClass().getSimpleName())); + } catch ( InvalidFieldInPacketException ex ) { + send(new ErrorIncorrectPacketClt(ex.getMessage())); + } + } + + + @Override + public void send(IPacketClt message) { + String rawData = protocol.encodePacket(message); + socket.send(rawData); + } + + /* (non-Javadoc) + * @see forge.net.client.INetClient#setPlayer(forge.game.player.LobbyPlayer) + */ + @Override + public final void createPlayer(String name) { + player = FServer.getLobby().findOrCreateRemotePlayer(name, this); + } + + /* (non-Javadoc) + * @see forge.net.client.INetClient#replaceState(forge.net.client.state.IClientState, forge.net.client.state.IClientState) + */ + @Override + public synchronized void replaceState(IClientState old, IClientState newState) { + state.removeFirstOccurrence(old); + state.push(newState); + } + +} diff --git a/forge-m-base/src/forge/net/client/state/ConnectedClientState.java b/forge-m-base/src/forge/net/client/state/ConnectedClientState.java new file mode 100644 index 00000000000..eaa7c84710d --- /dev/null +++ b/forge-m-base/src/forge/net/client/state/ConnectedClientState.java @@ -0,0 +1,33 @@ +package forge.net.client.state; + +import forge.net.client.INetClient; +import forge.net.protocol.toclient.EchoPacketClt; +import forge.net.protocol.toclient.ErrorIncorrectPacketClt; +import forge.net.protocol.toserver.EchoPacketSrv; +import forge.net.protocol.toserver.IPacketSrv; +import forge.net.protocol.toserver.IncorrectPacketSrv; + + + +public class ConnectedClientState implements IClientState { + + private final INetClient client; + + public ConnectedClientState(INetClient client) { + this.client = client; + } + + @Override + public boolean processPacket(IPacketSrv packet ) { + if( packet instanceof EchoPacketSrv) { + client.send(new EchoPacketClt(((EchoPacketSrv)packet).getMessage())); + return true; + } + if( packet instanceof IncorrectPacketSrv) { + client.send(new ErrorIncorrectPacketClt(((IncorrectPacketSrv)packet).getMessage())); + return true; + } + return false; + } + +} diff --git a/forge-m-base/src/forge/net/client/state/IClientState.java b/forge-m-base/src/forge/net/client/state/IClientState.java new file mode 100644 index 00000000000..3a65cdbfeb1 --- /dev/null +++ b/forge-m-base/src/forge/net/client/state/IClientState.java @@ -0,0 +1,11 @@ +package forge.net.client.state; + +import forge.net.protocol.toserver.IPacketSrv; + +/** + * TODO: Write javadoc for this type. + * + */ +public interface IClientState { + boolean processPacket(IPacketSrv data); +} diff --git a/forge-m-base/src/forge/net/client/state/InLobbyClientState.java b/forge-m-base/src/forge/net/client/state/InLobbyClientState.java new file mode 100644 index 00000000000..0a89756162b --- /dev/null +++ b/forge-m-base/src/forge/net/client/state/InLobbyClientState.java @@ -0,0 +1,26 @@ +package forge.net.client.state; + +import forge.net.client.INetClient; +import forge.net.protocol.toserver.ChatPacketSrv; +import forge.net.protocol.toserver.IPacketSrv; + +public class InLobbyClientState implements IClientState { + + private final INetClient client; + protected InLobbyClientState(INetClient client) { + this.client = client; + } + + @Override + public boolean processPacket(IPacketSrv data) { + if( data instanceof ChatPacketSrv) { + //ChatPacketSrv cp = (ChatPacketSrv) data; + // if ( not muted ) + //FServer.instance.getLobby().speak(ChatArea.Room, client.getPlayer(), cp.getMessage()); + // else + // client.send("You are banned and cannot speak"); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/net/client/state/UnauthorizedClientState.java b/forge-m-base/src/forge/net/client/state/UnauthorizedClientState.java new file mode 100644 index 00000000000..2e5f8bed180 --- /dev/null +++ b/forge-m-base/src/forge/net/client/state/UnauthorizedClientState.java @@ -0,0 +1,47 @@ +package forge.net.client.state; + +import forge.net.client.INetClient; +import forge.net.client.InvalidFieldInPacketException; +import forge.net.protocol.toclient.AuthResultPacketClt; +import forge.net.protocol.toserver.AuthorizePacketSrv; +import forge.net.protocol.toserver.IPacketSrv; +import org.apache.commons.lang3.StringUtils; + +/** + * TODO: Write javadoc for this type. + * + */ +public class UnauthorizedClientState implements IClientState { + + /** + * TODO: Write javadoc for Constructor. + * @param client + */ + private final INetClient client; + public UnauthorizedClientState(INetClient client) { + this.client = client; + } + + + @Override + public boolean processPacket(IPacketSrv packet) { + if( packet instanceof AuthorizePacketSrv ) { + AuthorizePacketSrv p = (AuthorizePacketSrv)packet; + + if( StringUtils.isBlank(p.getUsername())) + throw new InvalidFieldInPacketException("username is blank"); + + // check credentials here! + + client.createPlayer(p.getUsername()); + + client.send(new AuthResultPacketClt(client.getPlayer().getName(), true)); + client.replaceState(this, new InLobbyClientState(client)); + + return true; + } + + return false; + } + +} diff --git a/forge-m-base/src/forge/player/HumanCostDecision.java b/forge-m-base/src/forge/player/HumanCostDecision.java new file mode 100644 index 00000000000..84856d485b0 --- /dev/null +++ b/forge-m-base/src/forge/player/HumanCostDecision.java @@ -0,0 +1,1125 @@ +package forge.player; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +import forge.card.CardType; +import forge.game.Game; +import forge.game.GameEntity; +import forge.game.ability.AbilityUtils; +import forge.game.card.Card; +import forge.game.card.CardLists; +import forge.game.card.CardPredicates; +import forge.game.card.CardPredicates.Presets; +import forge.game.card.CounterType; +import forge.game.cost.*; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.SpellAbilityStackInstance; +import forge.game.zone.ZoneType; +import forge.screens.match.input.InputSelectCardsFromList; +import forge.screens.match.input.InputSelectManyBase; +import forge.toolbox.GuiChoose; +import forge.util.Aggregates; +import forge.util.Lang; + +import java.util.*; +import java.util.Map.Entry; + +public class HumanCostDecision extends CostDecisionMakerBase { + + private final SpellAbility ability; + private final Card source; + + public HumanCostDecision(Player p, SpellAbility sa, Card source) { + super(p); + ability = sa; + this.source = source; + } + + protected int chooseXValue(final int maxValue) { + /*final String chosen = sa.getSVar("ChosenX"); + if (chosen.length() > 0) { + return AbilityFactory.calculateAmount(card, "ChosenX", null); + }*/ + + int chosenX = player.getController().chooseNumber(ability, source.toString() + " - Choose a Value for X", 0, maxValue); + ability.setSVar("ChosenX", Integer.toString(chosenX)); + source.setSVar("ChosenX", Integer.toString(chosenX)); + return chosenX; + } + + @Override + public PaymentDecision visit(CostAddMana cost) { + Integer c = cost.convertAmount(); + if (c == null) { + c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); + } + return PaymentDecision.number(c); + } + + @Override + public PaymentDecision visit(CostChooseCreatureType cost) { + String choice = player.getController().chooseSomeType("Creature", ability, new ArrayList(CardType.getCreatureTypes()), new ArrayList(), true); + if( null == choice ) + return null; + return PaymentDecision.type(choice); + } + + @Override + public PaymentDecision visit(CostDiscard cost) { + List handList = new ArrayList(player.getCardsIn(ZoneType.Hand)); + String discardType = cost.getType(); + final String amount = cost.getAmount(); + + if (cost.payCostFromSource()) { + return handList.contains(source) ? PaymentDecision.card(source) : null; + } + + if (discardType.equals("Hand")) { + return PaymentDecision.card(handList); + } + + if (discardType.equals("LastDrawn")) { + final Card lastDrawn = player.getLastDrawnCard(); + return handList.contains(lastDrawn) ? PaymentDecision.card(lastDrawn) : null; + } + + Integer c = cost.convertAmount(); + + if (discardType.equals("Random")) { + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(handList.size()); + } + else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + return PaymentDecision.card(Aggregates.random(handList, c)); + } + if (discardType.contains("+WithSameName")) { + String type = discardType.replace("+WithSameName", ""); + handList = CardLists.getValidCards(handList, type.split(";"), player, source); + final List landList2 = handList; + handList = CardLists.filter(handList, new Predicate() { + @Override + public boolean apply(final Card c) { + for (Card card : landList2) { + if (!card.equals(c) && card.getName().equals(c.getName())) { + return true; + } + } + return false; + } + }); + if (c == 0) { return PaymentDecision.card(Lists.newArrayList()); } + List discarded = new ArrayList(); + while (c > 0) { + InputSelectCardsFromList inp = new InputSelectCardsFromList(1, 1, handList); + inp.setMessage("Select one of the cards with the same name to discard. Already chosen: " + discarded); + inp.setCancelAllowed(true); + inp.showAndWait(); + if (inp.hasCancelled()) { + return null; + } + final Card first = inp.getFirstSelected(); + discarded.add(first); + handList = CardLists.filter(handList, CardPredicates.nameEquals(first.getName())); + handList.remove(first); + c--; + } + return PaymentDecision.card(discarded); + } + + String type = new String(discardType); + final String[] validType = type.split(";"); + handList = CardLists.getValidCards(handList, validType, player, source); + + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(handList.size()); + } + else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + InputSelectCardsFromList inp = new InputSelectCardsFromList(c, c, handList); + inp.setMessage("Select %d more " + cost.getDescriptiveType() + " to discard."); + inp.setCancelAllowed(true); + inp.showAndWait(); + if (inp.hasCancelled() || inp.getSelected().size() != c) { + return null; + } + + return PaymentDecision.card(inp.getSelected()); + } + + @Override + public PaymentDecision visit(CostDamage cost) { + final String amount = cost.getAmount(); + final int life = player.getLife(); + + Integer c = cost.convertAmount(); + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(life); + } + else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + if (player.canPayLife(c) && player.getController().confirmPayment(cost, "Pay " + c + " Life?")) { + return PaymentDecision.number(c); + } + return null; + } + + @Override + public PaymentDecision visit(CostDraw cost) { + final String amount = cost.getAmount(); + + Integer c = cost.convertAmount(); + if (c == null) { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + + if (!player.getController().confirmPayment(cost, "Draw " + c + " Card" + (c == 1 ? "" : "s"))) { + return null; + } + + return PaymentDecision.number(c); + } + + @Override + public PaymentDecision visit(CostExile cost) { + final String amount = cost.getAmount(); + final Game game = player.getGame(); + + Integer c = cost.convertAmount(); + String type = cost.getType(); + boolean fromTopGrave = false; + if (type.contains("FromTopGrave")) { + type = type.replace("FromTopGrave", ""); + fromTopGrave = true; + } + + List list; + if (cost.getFrom().equals(ZoneType.Stack)) { + list = new ArrayList(); + for (SpellAbilityStackInstance si : game.getStack()) { + list.add(si.getSourceCard()); + } + } + else if (cost.sameZone) { + list = new ArrayList(game.getCardsIn(cost.from)); + } + else { + list = new ArrayList(player.getCardsIn(cost.from)); + } + + if (cost.payCostFromSource()) { + return source.getZone() == player.getZone(cost.from) && player.getController().confirmPayment(cost, "Exile " + source.getName() + "?") ? PaymentDecision.card(source) : null; + + } + + if (type.equals("All")) { + return PaymentDecision.card(list); + } + list = CardLists.getValidCards(list, type.split(";"), player, source); + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(list.size()); + } + else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + if (cost.from == ZoneType.Battlefield || cost.from == ZoneType.Hand) { + InputSelectCardsFromList inp = new InputSelectCardsFromList(c, c, list); + inp.setMessage("Exile %d card(s) from your" + cost.from); + inp.setCancelAllowed(true); + inp.showAndWait(); + return inp.hasCancelled() ? null : PaymentDecision.card(inp.getSelected()); + } + + if (cost.from == ZoneType.Stack) { return exileFromStack(cost, ability, c); } + if (cost.from == ZoneType.Library) { return exileFromTop(cost, ability, player, c); } + if (fromTopGrave) { return exileFromTopGraveType(ability, c, list); } + if (!cost.sameZone) { return exileFromMiscZone(cost, ability, c, list); } + + List players = game.getPlayers(); + List payableZone = new ArrayList(); + for (Player p : players) { + List enoughType = CardLists.filter(list, CardPredicates.isOwner(p)); + if (enoughType.size() < c) { + list.removeAll(enoughType); + } + else { + payableZone.add(p); + } + } + return exileFromSame(cost, list, c, payableZone); + } + + + + // Inputs + + // Exile + // ExileFromHand + // ExileFromGrave + // ExileFromTop (of library) + // ExileSameGrave + + private PaymentDecision exileFromSame(CostExile cost, List list, int nNeeded, List payableZone) { + if (nNeeded == 0) { + return PaymentDecision.number(0); + } + + final Player p = GuiChoose.oneOrNone(String.format("Exile from whose %s?", cost.getFrom()), payableZone); + if (p == null) { + return null; + } + + List typeList = CardLists.filter(list, CardPredicates.isOwner(p)); + if(typeList.size() < nNeeded) + return null; + + List toExile = GuiChoose.many("Exile from " + cost.getFrom(), "To be exiled", nNeeded, typeList, null); + return PaymentDecision.card(toExile); + } + + private PaymentDecision exileFromStack(CostExile cost, SpellAbility sa, int nNeeded) { + if (nNeeded == 0) { + return PaymentDecision.number(0); + } + + final Game game = sa.getActivatingPlayer().getGame(); + ArrayList saList = new ArrayList(); + ArrayList descList = new ArrayList(); + + for (SpellAbilityStackInstance si : game.getStack()) { + final Card stC = si.getSourceCard(); + final SpellAbility stSA = si.getSpellAbility().getRootAbility(); + if (stC.isValid(cost.getType().split(";"), sa.getActivatingPlayer(), sa.getHostCard()) && stSA.isSpell()) { + saList.add(stSA); + if (stC.isCopiedSpell()) { + descList.add(stSA.getStackDescription() + " (Copied Spell)"); + } else { + descList.add(stSA.getStackDescription()); + } + } + } + + if (saList.size() < nNeeded) { + return null; + } + + List exiled = new ArrayList(); + for (int i = 0; i < nNeeded; i++) { + //Have to use the stack descriptions here because some copied spells have no description otherwise + final String o = GuiChoose.oneOrNone("Exile from " + cost.getFrom(), descList); + + if (o != null) { + final SpellAbility toExile = saList.get(descList.indexOf(o)); + final Card c = toExile.getHostCard(); + + saList.remove(toExile); + descList.remove(o); + + exiled.add(c); + } + else { + return null; + } + } + return PaymentDecision.card(exiled); + } + + private PaymentDecision exileFromTop(final CostExile cost, final SpellAbility sa, final Player player, final int nNeeded) { + final StringBuilder sb = new StringBuilder(); + sb.append("Exile ").append(nNeeded).append(" cards from the top of your library?"); + final List list = player.getCardsIn(ZoneType.Library, nNeeded); + + if (list.size() > nNeeded || !player.getController().confirmPayment(cost, "Exile " + Lang.nounWithAmount(nNeeded, "card") + " from the top of your library?")) { + return null; + } + + return PaymentDecision.card(list); + } + + private PaymentDecision exileFromMiscZone(CostExile cost, SpellAbility sa, int nNeeded, List typeList) { + if (typeList.size() < nNeeded) + return null; + + List exiled = new ArrayList(); + for (int i = 0; i < nNeeded; i++) { + final Card c = GuiChoose.oneOrNone("Exile from " + cost.getFrom(), typeList); + + if (c != null) { + typeList.remove(c); + exiled.add(c); + } else { + return null; + } + } + return PaymentDecision.card(exiled); + } + + private PaymentDecision exileFromTopGraveType(SpellAbility sa, int nNeeded, List typeList) { + if (typeList.size() < nNeeded) + return null; + + Collections.reverse(typeList); + return PaymentDecision.card(Lists.newArrayList(Iterables.limit(typeList, nNeeded))); + } + + @Override + public PaymentDecision visit(CostExiledMoveToGrave cost) { + Integer c = cost.convertAmount(); + if (c == null) { + c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); + } + + final Player activator = ability.getActivatingPlayer(); + List list = activator.getGame().getCardsIn(ZoneType.Exile); + list = CardLists.getValidCards(list, cost.getType().split(";"), activator, source); + + if (list.size() < c) + return null; + + return PaymentDecision.card(GuiChoose.many("Choose an exiled card to put into graveyard", "To graveyard", c, list, source)); + } + + @Override + public PaymentDecision visit(CostFlipCoin cost) { + final String amount = cost.getAmount(); + Integer c = cost.convertAmount(); + + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(cost.getList().size()); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + return PaymentDecision.number(c); + } + + @Override + public PaymentDecision visit(CostGainControl cost) { + final String amount = cost.getAmount(); + + Integer c = cost.convertAmount(); + if (c == null) { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + final List list = player.getCardsIn(ZoneType.Battlefield); + List validCards = CardLists.getValidCards(list, cost.getType().split(";"), player, source); + + InputSelectCardsFromList inp = new InputSelectCardsFromList(c, validCards); + final String desc = cost.getTypeDescription() == null ? cost.getType() : cost.getTypeDescription(); + inp.setMessage("Gain control of %d " + desc); + inp.showAndWait(); + if (inp.hasCancelled()) { + return null; + } + return PaymentDecision.card(inp.getSelected()); + } + + @Override + public PaymentDecision visit(CostGainLife cost) { + final String amount = cost.getAmount(); + + final int life = player.getLife(); + + Integer c = cost.convertAmount(); + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(life); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + final List oppsThatCanGainLife = new ArrayList(); + for (final Player opp : cost.getPotentialTargets(player, source)) { + if (opp.canGainLife()) { + oppsThatCanGainLife.add(opp); + } + } + + if (cost.getCntPlayers() == Integer.MAX_VALUE) // applied to all players who can gain + return PaymentDecision.players(oppsThatCanGainLife); + + final StringBuilder sb = new StringBuilder(); + sb.append(source.getName()).append(" - Choose an opponent to gain ").append(c).append(" life:"); + + final Player chosenToGain = GuiChoose.oneOrNone(sb.toString(), oppsThatCanGainLife); + if (null == chosenToGain) + return null; + else + return PaymentDecision.players(Lists.newArrayList(chosenToGain)); + } + + @Override + public PaymentDecision visit(CostMill cost) { + final String amount = cost.getAmount(); + Integer c = cost.convertAmount(); + + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(cost.getList().size()); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + if (!player.getController().confirmPayment(cost, "Mill " + c + " card" + (c == 1 ? "" : "s") + " from your library?")) { + return null; + } + return PaymentDecision.card(player.getCardsIn(ZoneType.Library, c)); + } + + @Override + public PaymentDecision visit(CostPayLife cost) { + final String amount = cost.getAmount(); + final int life = player.getLife(); + + Integer c = cost.convertAmount(); + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.startsWith("XChoice")) { + int limit = life; + if (sVar.contains("LimitMax")) { + limit = AbilityUtils.calculateAmount(source, sVar.split("LimitMax.")[1], ability); + } + int maxLifePayment = limit < life ? limit : life; + c = chooseXValue(maxLifePayment); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + if (player.canPayLife(c) && player.getController().confirmPayment(cost, "Pay " + c + " Life?")) { + return PaymentDecision.number(c); + } + return null; + } + + @Override + public PaymentDecision visit(CostPartMana cost) { + // only interactive payment possible for now =( + return new PaymentDecision(0); + } + + @Override + public PaymentDecision visit(CostPutCardToLib cost) { + final String amount = cost.getAmount(); + Integer c = cost.convertAmount(); + + List list = cost.sameZone ? player.getGame().getCardsIn(cost.getFrom()) : player.getCardsIn(cost.getFrom()); + + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(cost.getList().size()); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + list = CardLists.getValidCards(list, cost.getType().split(";"), player, source); + + if (cost.from == ZoneType.Hand) { + InputSelectCardsFromList inp = new InputSelectCardsFromList(c, c, list); + inp.setMessage("Put %d card(s) from your " + cost.from ); + inp.setCancelAllowed(true); + inp.showAndWait(); + return inp.hasCancelled() ? null : PaymentDecision.card(inp.getSelected()); + } + + if (cost.sameZone){ + List players = player.getGame().getPlayers(); + List payableZone = new ArrayList(); + for (Player p : players) { + List enoughType = CardLists.filter(list, CardPredicates.isOwner(p)); + if (enoughType.size() < c) { + list.removeAll(enoughType); + } else { + payableZone.add(p); + } + } + return putFromSame(list, c.intValue(), payableZone, cost.from); + } else {//Graveyard + return putFromMiscZone(ability, c.intValue(), list, cost.from); + } + } + + + private PaymentDecision putFromMiscZone(SpellAbility sa, int nNeeded, List typeList, ZoneType fromZone) { + if(typeList.size() < nNeeded) + return null; + + List chosen = new ArrayList<>(); + for (int i = 0; i < nNeeded; i++) { + final Card c = GuiChoose.oneOrNone("Put from " + fromZone + " to library", typeList); + + if (c == null) + return null; + + typeList.remove(c); + chosen.add(c); + } + return PaymentDecision.card(chosen); + } + + private PaymentDecision putFromSame(List list, int nNeeded, List payableZone, ZoneType fromZone) { + if (nNeeded == 0) { + return PaymentDecision.number(0); + } + + final Player p = GuiChoose.oneOrNone(String.format("Put cards from whose %s?", fromZone), payableZone); + if (p == null) { + return null; + } + + List typeList = CardLists.filter(list, CardPredicates.isOwner(p)); + if(typeList.size() < nNeeded) + return null; + + List chosen = new ArrayList<>(); + for (int i = 0; i < nNeeded; i++) { + final Card c = GuiChoose.oneOrNone("Put cards from " + fromZone + " to Library", typeList); + if (c == null) + return null; + typeList.remove(c); + chosen.add(c); + } + return PaymentDecision.card(chosen); + } + + @Override + public PaymentDecision visit(CostPutCounter cost) { + Integer c = cost.getNumberOfCounters(ability); + + if (cost.payCostFromSource()) { + cost.setLastPaidAmount(c); + return PaymentDecision.number(c); + } + + // Cards to use this branch: Scarscale Ritual, Wandering Mage - each adds only one counter + List typeList = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), player, ability.getHostCard()); + + InputSelectCardsFromList inp = new InputSelectCardsFromList(1, 1, typeList); + inp.setMessage("Put " + Lang.nounWithAmount(c, cost.getCounter().getName() + " counter") + " on " +cost.getDescriptiveType()); + inp.setCancelAllowed(true); + inp.showAndWait(); + + if(inp.hasCancelled()) + return null; + + return PaymentDecision.card(inp.getSelected()); + } + + @Override + public PaymentDecision visit(CostReturn cost) { + final String amount = cost.getAmount(); + Integer c = cost.convertAmount(); + + final List list = player.getCardsIn(ZoneType.Battlefield); + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(list.size()); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + if (cost.payCostFromSource()) { + final Card card = ability.getHostCard(); + if (card.getController() == player && card.isInPlay()) { + return player.getController().confirmPayment(cost, "Return " + card.getName() + " to hand?") ? PaymentDecision.card(card) : null; + } + } + else { + List validCards = CardLists.getValidCards(ability.getActivatingPlayer().getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), ability.getActivatingPlayer(), ability.getHostCard()); + + InputSelectCardsFromList inp = new InputSelectCardsFromList(c, c, validCards); + inp.setMessage("Return %d " + cost.getType() + " " + cost.getType() + " card(s) to hand"); + inp.showAndWait(); + if (inp.hasCancelled()) + return null; + + return PaymentDecision.card(inp.getSelected()); + } + return null; + } + + @Override + public PaymentDecision visit(CostReveal cost) { + final String amount = cost.getAmount(); + + if (cost.payCostFromSource()) + return PaymentDecision.card(source); + + if (cost.getType().equals("Hand")) + return PaymentDecision.card(player.getCardsIn(ZoneType.Hand)); + + InputSelectCardsFromList inp = null; + if (cost.getType().equals("SameColor")) { + Integer num = cost.convertAmount(); + List handList = player.getCardsIn(ZoneType.Hand); + final List handList2 = handList; + handList = CardLists.filter(handList, new Predicate() { + @Override + public boolean apply(final Card c) { + for (Card card : handList2) { + if (!card.equals(c) && card.sharesColorWith(c)) { + return true; + } + } + return false; + } + }); + if (num == 0) + return PaymentDecision.number(0); + + inp = new InputSelectCardsFromList(num, handList) { + private static final long serialVersionUID = 8338626212893374798L; + + @Override + protected void onCardSelected(Card c) { + Card firstCard = Iterables.getFirst(this.selected, null); + if (firstCard != null && !CardPredicates.sharesColorWith(firstCard).apply(c)) { + return; + } + super.onCardSelected(c); + } + }; + inp.setMessage("Select " + Lang.nounWithAmount(num, "card" ) + " of same color to reveal."); + + } else { + Integer num = cost.convertAmount(); + + List handList = player.getCardsIn(ZoneType.Hand); + handList = CardLists.getValidCards(handList, cost.getType().split(";"), player, ability.getHostCard()); + + if (num == null) { + final String sVar = ability.getSVar(amount); + if (sVar.equals("XChoice")) { + num = chooseXValue(handList.size()); + } else { + num = AbilityUtils.calculateAmount(source, amount, ability); + } + } + if ( num == 0 ) + return PaymentDecision.number(0);; + + inp = new InputSelectCardsFromList(num, num, handList); + inp.setMessage("Select %d more " + cost.getDescriptiveType() + " card(s) to reveal."); + } + inp.setCancelAllowed(true); + inp.showAndWait(); + if (inp.hasCancelled()) + return null; + + return PaymentDecision.card(inp.getSelected()); + } + + @Override + public PaymentDecision visit(CostRemoveAnyCounter cost) { + Integer c = cost.convertAmount(); + final String type = cost.getType(); + + if (c == null) { + c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); + } + + List list = new ArrayList(player.getCardsIn(ZoneType.Battlefield)); + list = CardLists.getValidCards(list, type.split(";"), player, source); + + + list = CardLists.filter(list, new Predicate() { + @Override + public boolean apply(final Card card) { + return card.hasCounters(); + } + }); + InputSelectCardsFromList inp = new InputSelectCardsFromList(1, 1, list); + inp.setMessage("Select " + cost.getDescriptiveType() + " to remove a counter"); + inp.setCancelAllowed(false); + inp.showAndWait(); + Card selected = inp.getFirstSelected(); + final Map tgtCounters = selected.getCounters(); + final ArrayList typeChoices = new ArrayList(); + for (CounterType key : tgtCounters.keySet()) { + if (tgtCounters.get(key) > 0) { + typeChoices.add(key); + } + } + + String prompt = "Select type counters to remove"; + cost.setCounterType(GuiChoose.one(prompt, typeChoices)); + + return PaymentDecision.card(selected, cost.getCounter()); + } + + public static final class InputSelectCardToRemoveCounter extends InputSelectManyBase { + private static final long serialVersionUID = 2685832214519141903L; + + private final Map cardsChosen; + private final CounterType counterType; + private final List validChoices; + + public InputSelectCardToRemoveCounter(int cntCounters, CounterType cType, List validCards) { + super(cntCounters, cntCounters); + this.validChoices = validCards; + counterType = cType; + cardsChosen = cntCounters > 0 ? new HashMap() : null; + } + + @Override + protected void onCardSelected(Card c) { + if (!isValidChoice(c) || c.getCounters(counterType) <= getTimesSelected(c)) { + return; + } + + int tc = getTimesSelected(c); + cardsChosen.put(c, tc+1); + + onSelectStateChanged(c, true); + refresh(); + }; + + @Override + protected boolean hasEnoughTargets() { + return hasAllTargets(); + } + + @Override + protected boolean hasAllTargets() { + int sum = getDistibutedCounters(); + return sum >= max; + } + + protected String getMessage() { + return max == Integer.MAX_VALUE + ? String.format(message, getDistibutedCounters()) + : String.format(message, max - getDistibutedCounters()); + } + + private int getDistibutedCounters() { + int sum = 0; + for(Entry kv : cardsChosen.entrySet()) { + sum += kv.getValue().intValue(); + } + return sum; + } + + protected final boolean isValidChoice(GameEntity choice) { + return validChoices.contains(choice); + } + + public int getTimesSelected(Card c) { + return cardsChosen.containsKey(c) ? cardsChosen.get(c).intValue() : 0; + } + + @Override + public Collection getSelected() { + return cardsChosen.keySet(); + } + } + + @Override + public PaymentDecision visit(CostRemoveCounter cost) { + + final String amount = cost.getAmount(); + Integer c = cost.convertAmount(); + final String type = cost.getType(); + + String sVarAmount = ability.getSVar(amount); + int cntRemoved = 1; + if (c != null) + cntRemoved = c.intValue(); + else if (!"XChoice".equals(sVarAmount)) { + cntRemoved = AbilityUtils.calculateAmount(source, amount, ability); + } + + if (cost.payCostFromSource()) { + int maxCounters = source.getCounters(cost.counter); + if (amount.equals("All")) + cntRemoved = maxCounters; + else if ( c == null && "XChoice".equals(sVarAmount)) { + cntRemoved = chooseXValue(maxCounters); + } + + if (maxCounters < cntRemoved) + return null; + return PaymentDecision.card(source, cntRemoved >= 0 ? cntRemoved : maxCounters); + + } else if (type.equals("OriginalHost")) { + int maxCounters = ability.getOriginalHost().getCounters(cost.counter); + if (amount.equals("All")) { + cntRemoved = maxCounters; + } + if (maxCounters < cntRemoved) + return null; + + return PaymentDecision.card(ability.getOriginalHost(), cntRemoved >= 0 ? cntRemoved : maxCounters); + } + + List validCards = CardLists.getValidCards(player.getCardsIn(cost.zone), type.split(";"), player, source); + if (cost.zone.equals(ZoneType.Battlefield)) { + final InputSelectCardToRemoveCounter inp = new InputSelectCardToRemoveCounter(cntRemoved, cost.counter, validCards); + inp.setMessage("Remove %d " + cost.counter.getName() + " counters from " + cost.getDescriptiveType()); + inp.setCancelAllowed(true); + inp.showAndWait(); + if (inp.hasCancelled()) { + return null; + } + + // Have to hack here: remove all counters minus one, without firing any triggers, + // triggers will fire when last is removed by executePayment. + // They don't care how many were removed anyway + // int sum = 0; + for (Card crd : inp.getSelected()) { + int removed = inp.getTimesSelected(crd); + // sum += removed; + if(removed < 2) continue; + int oldVal = crd.getCounters().get(cost.counter).intValue(); + crd.getCounters().put(cost.counter, Integer.valueOf(oldVal - removed + 1)); + } + return PaymentDecision.card(inp.getSelected(), 1); + } + + // Rift Elemental only - always removes 1 counter, so there will be no code for N counters. + List suspended = new ArrayList(); + for(Card crd : validCards) + if(crd.getCounters( cost.counter) > 0 ) + suspended.add(crd); + + final Card card = GuiChoose.oneOrNone("Remove counter(s) from a card in " + cost.zone, suspended); + return null == card ? null : PaymentDecision.card(card, c); + } + + @Override + public PaymentDecision visit(CostSacrifice cost) { + final String amount = cost.getAmount(); + final String type = cost.getType(); + + List list = new ArrayList(player.getCardsIn(ZoneType.Battlefield)); + list = CardLists.getValidCards(list, type.split(";"), player, source); + if (player.hasKeyword("You can't sacrifice creatures to cast spells or activate abilities.")) { + list = CardLists.getNotType(list, "Creature"); + } + + if (cost.payCostFromSource()) { + if (source.getController() == ability.getActivatingPlayer() && source.isInPlay()) { + return player.getController().confirmPayment(cost, "Sacrifice " + source.getName() + "?") ? PaymentDecision.card(source) : null; + } else + return null; + } + + if (amount.equals("All")) + return PaymentDecision.card(list); + + + Integer c = cost.convertAmount(); + if (c == null) { + // Generalize this + if (ability.getSVar(amount).equals("XChoice")) { + c = chooseXValue(list.size()); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + if (0 == c.intValue()) { + return PaymentDecision.number(0); + } + if (list.size() < c) { + return null; + } + InputSelectCardsFromList inp = new InputSelectCardsFromList(c, c, list); + inp.setMessage("Select a " + cost.getDescriptiveType() + " to sacrifice (%d left)"); + inp.setCancelAllowed(true); + inp.showAndWait(); + if ( inp.hasCancelled() ) + return null; + + return PaymentDecision.card(inp.getSelected()); + + } + + @Override + public PaymentDecision visit(CostTap cost) { + // if (!canPay(ability, source, ability.getActivatingPlayer(), + // payment.getCost())) + // return false; + return PaymentDecision.number(1); + } + + @Override + public PaymentDecision visit(CostTapType cost) { + List typeList = new ArrayList(player.getCardsIn(ZoneType.Battlefield)); + String type = cost.getType(); + final String amount = cost.getAmount(); + Integer c = cost.convertAmount(); + + boolean sameType = false; + if (type.contains("sharesCreatureTypeWith")) { + sameType = true; + type = type.replace("sharesCreatureTypeWith", ""); + } + + boolean totalPower = false; + String totalP = ""; + if (type.contains("+withTotalPowerGE")) { + totalPower = true; + totalP = type.split("withTotalPowerGE")[1]; + type = type.replace("+withTotalPowerGE" + totalP, ""); + } + + typeList = CardLists.getValidCards(typeList, type.split(";"), player, ability.getHostCard()); + typeList = CardLists.filter(typeList, Presets.UNTAPPED); + if (c == null && !amount.equals("Any")) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(typeList.size()); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + + if (sameType) { + final List List2 = typeList; + typeList = CardLists.filter(typeList, new Predicate() { + @Override + public boolean apply(final Card c) { + for (Card card : List2) { + if (!card.equals(c) && card.sharesCreatureTypeWith(c)) { + return true; + } + } + return false; + } + }); + if (c == 0) return PaymentDecision.number(0); + List tapped = new ArrayList(); + while (c > 0) { + InputSelectCardsFromList inp = new InputSelectCardsFromList(1, 1, typeList); + inp.setMessage("Select one of the cards to tap. Already chosen: " + tapped); + inp.setCancelAllowed(true); + inp.showAndWait(); + if (inp.hasCancelled()) + return null; + final Card first = inp.getFirstSelected(); + tapped.add(first); + typeList = CardLists.filter(typeList, new Predicate() { + @Override + public boolean apply(final Card c) { + return c.sharesCreatureTypeWith(first); + } + }); + typeList.remove(first); + c--; + } + return PaymentDecision.card(tapped); + } + + if (totalPower) { + int i = Integer.parseInt(totalP); + InputSelectCardsFromList inp = new InputSelectCardsFromList(0, typeList.size(), typeList); + inp.setMessage("Select a card to tap."); + inp.setUnselectAllowed(true); + inp.setCancelAllowed(true); + inp.showAndWait(); + + if (inp.hasCancelled() || CardLists.getTotalPower(inp.getSelected()) < i) { + return null; + } else { + return PaymentDecision.card(inp.getSelected()); + } + } + + InputSelectCardsFromList inp = new InputSelectCardsFromList(c, c, typeList); + inp.setMessage("Select a " + cost.getDescriptiveType() + " to tap (%d left)"); + inp.showAndWait(); + if ( inp.hasCancelled() ) + return null; + + return PaymentDecision.card(inp.getSelected()); + } + + @Override + public PaymentDecision visit(CostUntapType cost) { + List typeList = CardLists.getValidCards(player.getGame().getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), + player, ability.getHostCard()); + typeList = CardLists.filter(typeList, Presets.TAPPED); + if (!cost.canUntapSource) { + typeList.remove(source); + } + final String amount = cost.getAmount(); + Integer c = cost.convertAmount(); + if (c == null) { + final String sVar = ability.getSVar(amount); + // Generalize this + if (sVar.equals("XChoice")) { + c = chooseXValue(typeList.size()); + } else { + c = AbilityUtils.calculateAmount(source, amount, ability); + } + } + InputSelectCardsFromList inp = new InputSelectCardsFromList(c, c, typeList); + inp.setMessage("Select a " + cost.getDescriptiveType() + " to untap (%d left)"); + inp.showAndWait(); + if( inp.hasCancelled() || inp.getSelected().size() != c ) + return null; + return PaymentDecision.card(inp.getSelected()); + } + + @Override + public PaymentDecision visit(CostUntap cost) { + return PaymentDecision.number(1); + } + + @Override + public PaymentDecision visit(CostUnattach cost) { + final Card source = ability.getHostCard(); + + Card cardToUnattach = cost.findCardToUnattach(source, player, ability); + if (cardToUnattach != null && player.getController().confirmPayment(cost, "Unattach " + cardToUnattach.getName() + "?")) { + return PaymentDecision.card(cardToUnattach); + } + return null; + } + + @Override + public boolean paysRightAfterDecision() { + return true; + } +} diff --git a/forge-m-base/src/forge/player/HumanPlay.java b/forge-m-base/src/forge/player/HumanPlay.java new file mode 100644 index 00000000000..c72491576cb --- /dev/null +++ b/forge-m-base/src/forge/player/HumanPlay.java @@ -0,0 +1,754 @@ +package forge.player; + +import com.google.common.base.Predicate; + +import forge.FThreads; +import forge.card.MagicColor; +import forge.card.mana.ManaCost; +import forge.card.mana.ManaCostShard; +import forge.game.Game; +import forge.game.GameActionUtil; +import forge.game.GameLogEntryType; +import forge.game.ability.AbilityUtils; +import forge.game.ability.ApiType; +import forge.game.ability.effects.CharmEffect; +import forge.game.ability.effects.FlipCoinEffect; +import forge.game.card.*; +import forge.game.card.CardPredicates.Presets; +import forge.game.cost.*; +import forge.game.mana.ManaCostAdjustment; +import forge.game.mana.ManaCostBeingPaid; +import forge.game.player.Player; +import forge.game.spellability.Ability; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.TargetRestrictions; +import forge.game.zone.ZoneType; +import forge.screens.match.input.InputPayMana; +import forge.screens.match.input.InputPayManaOfCostPayment; +import forge.screens.match.input.InputPayManaSimple; +import forge.screens.match.input.InputPayManaX; +import forge.screens.match.input.InputSelectCardsFromList; +import forge.toolbox.GuiChoose; +import forge.util.Lang; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +public class HumanPlay { + public HumanPlay() { + // TODO Auto-generated constructor stub + } + + /** + *

+ * playSpellAbility. + *

+ * + * @param sa + * a {@link forge.game.spellability.SpellAbility} object. + */ + public final static void playSpellAbility(Player p, SpellAbility sa) { + FThreads.assertExecutedByEdt(false); + + if (sa == Ability.PLAY_LAND_SURROGATE) { + p.playLand(sa.getHostCard(), false); + return; + } + + sa.setActivatingPlayer(p); + final Card source = sa.getHostCard(); + source.setSplitStateToPlayAbility(sa); + + sa = chooseOptionalAdditionalCosts(p, sa); + if (sa == null) { + return; + } + + if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { + CharmEffect.makeChoices(sa); + } + + if (sa.hasParam("Bestow")) { + source.animateBestow(); + } + + // Need to check PayCosts, and Ability + All SubAbilities for Target + boolean newAbility = sa.getPayCosts() != null; + SpellAbility ability = sa; + while ((ability != null) && !newAbility) { + final TargetRestrictions tgt = ability.getTargetRestrictions(); + + newAbility |= tgt != null; + ability = ability.getSubAbility(); + } + + // System.out.println("Playing:" + sa.getDescription() + " of " + sa.getHostCard() + " new = " + newAbility); + if (newAbility) { + Cost abCost = sa.getPayCosts() == null ? new Cost("0", sa.isAbility()) : sa.getPayCosts(); + CostPayment payment = new CostPayment(abCost, sa); + + final HumanPlaySpellAbility req = new HumanPlaySpellAbility(sa, payment); + req.playAbility(true, false, false); + } + else if (payManaCostIfNeeded(p, sa)) { + if (sa.isSpell() && !source.isCopiedSpell()) { + sa.setHostCard(p.getGame().getAction().moveToStack(source)); + } + p.getGame().getStack().add(sa); + } + } + + /** + * choose optional additional costs. For HUMAN only + * @param activator + * + * @param original + * the original sa + * @return an ArrayList. + */ + static SpellAbility chooseOptionalAdditionalCosts(Player p, final SpellAbility original) { + if (!original.isSpell()) { + return original; + } + final List abilities = GameActionUtil.getOptionalCosts(original); + return p.getController().getAbilityToPlay(abilities); + } + + private static boolean payManaCostIfNeeded(final Player p, final SpellAbility sa) { + final ManaCostBeingPaid manaCost; + if (sa.getHostCard().isCopiedSpell() && sa.isSpell()) { + manaCost = new ManaCostBeingPaid(ManaCost.ZERO); + } + else { + manaCost = new ManaCostBeingPaid(sa.getPayCosts().getTotalMana()); + ManaCostAdjustment.adjust(manaCost, sa, false); + } + + boolean isPaid = manaCost.isPaid(); + + if (!isPaid) { + InputPayMana inputPay = new InputPayManaSimple(p.getGame(), sa, manaCost); + inputPay.showAndWait(); + isPaid = inputPay.isPaid(); + } + return isPaid; + } + + /** + *

+ * playSpellAbilityForFree. + *

+ * + * @param sa + * a {@link forge.game.spellability.SpellAbility} object. + */ + public static final void playSaWithoutPayingManaCost(final Game game, final SpellAbility sa, boolean mayChooseNewTargets) { + FThreads.assertExecutedByEdt(false); + final Card source = sa.getHostCard(); + + source.setSplitStateToPlayAbility(sa); + + if (sa.getPayCosts() != null) { + if (sa.getApi() == ApiType.Charm && !sa.isWrapper() && !sa.isCopied()) { + CharmEffect.makeChoices(sa); + } + final CostPayment payment = new CostPayment(sa.getPayCosts(), sa); + + final HumanPlaySpellAbility req = new HumanPlaySpellAbility(sa, payment); + req.playAbility(mayChooseNewTargets, true, false); + } + else { + if (sa.isSpell()) { + final Card c = sa.getHostCard(); + if (!c.isCopiedSpell()) { + sa.setHostCard(game.getAction().moveToStack(c)); + } + } + game.getStack().add(sa); + } + } + + /** + *

+ * playSpellAbility_NoStack. + *

+ * + * @param sa + * a {@link forge.game.spellability.SpellAbility} object. + * @param skipTargeting + * a boolean. + */ + public final static void playSpellAbilityNoStack(final Player player, final SpellAbility sa) { + playSpellAbilityNoStack(player, sa, false); + } + + public final static void playSpellAbilityNoStack(final Player player, final SpellAbility sa, boolean useOldTargets) { + sa.setActivatingPlayer(player); + + if (sa.getPayCosts() != null) { + final HumanPlaySpellAbility req = new HumanPlaySpellAbility(sa, new CostPayment(sa.getPayCosts(), sa)); + + req.playAbility(!useOldTargets, false, true); + } + else if (payManaCostIfNeeded(player, sa)) { + AbilityUtils.resolve(sa); + } + } + + // ------------------------------------------------------------------------ + + private static int getAmountFromPart(CostPart part, Card source, SpellAbility sourceAbility) { + String amountString = part.getAmount(); + return StringUtils.isNumeric(amountString) ? Integer.parseInt(amountString) : AbilityUtils.calculateAmount(source, amountString, sourceAbility); + } + + /** + * TODO: Write javadoc for this method. + * @param part + * @param source + * @param sourceAbility + * @return + */ + private static int getAmountFromPartX(CostPart part, Card source, SpellAbility sourceAbility) { + String amountString = part.getAmount(); + return StringUtils.isNumeric(amountString) ? Integer.parseInt(amountString) : CardFactoryUtil.xCount(source, source.getSVar(amountString)); + } + + /** + *

+ * payCostDuringAbilityResolve. + *

+ * + * @param ability + * a {@link forge.game.spellability.SpellAbility} object. + * @param cost + * a {@link forge.game.cost.Cost} object. + * @param paid + * a {@link forge.UiCommand} object. + * @param unpaid + * a {@link forge.UiCommand} object. + * @param sourceAbility TODO + */ + public static boolean payCostDuringAbilityResolve(final Player p, final Card source, final Cost cost, SpellAbility sourceAbility, String prompt) { + // Only human player pays this way + Card current = null; // Used in spells with RepeatEach effect to distinguish cards, Cut the Tethers + if (!source.getRemembered().isEmpty()) { + if (source.getRemembered().get(0) instanceof Card) { + current = (Card) source.getRemembered().get(0); + } + } + if (!source.getImprinted().isEmpty()) { + current = source.getImprinted().get(0); + } + + final List parts = cost.getCostParts(); + ArrayList remainingParts = new ArrayList(parts); + CostPart costPart = null; + if (!parts.isEmpty()) { + costPart = parts.get(0); + } + String orString = prompt == null ? sourceAbility.getStackDescription().trim() : ""; + if (!orString.isEmpty()) { + orString = " (or: " + orString + ")"; + } + + if (parts.isEmpty() || (costPart.getAmount().equals("0") && parts.size() < 2)) { + return p.getController().confirmPayment(costPart, "Do you want to pay {0}?" + orString); + } + // 0 mana costs were slipping through because CostPart.getAmount returns 1 + else if (costPart instanceof CostPartMana && parts.size() < 2) { + if (((CostPartMana) costPart).getManaToPay().isZero()) { + return p.getController().confirmPayment(costPart, "Do you want to pay {0}?" + orString); + } + } + + HumanCostDecision hcd = new HumanCostDecision(p, sourceAbility, source); + + //the following costs do not need inputs + for (CostPart part : parts) { + boolean mayRemovePart = true; + + if (part instanceof CostPayLife) { + final int amount = getAmountFromPart(part, source, sourceAbility); + if (!p.canPayLife(amount)) { + return false; + } + + if (!p.getController().confirmPayment(part, "Do you want to pay " + amount + " life?" + orString)) { + return false; + } + + p.payLife(amount, null); + } + else if (part instanceof CostDraw) { + final int amount = getAmountFromPart(part, source, sourceAbility); + List res = new ArrayList(); + String type = part.getType(); + for (Player player : p.getGame().getPlayers()) { + if (player.isValid(type, p, source) && player.canDraw()) { + res.add(player); + } + } + + if (res.isEmpty()) { + return false; + } + + StringBuilder sb = new StringBuilder("Do you want to "); + sb.append(res.contains(p) ? "" : "let that player "); + sb.append("draw " + Lang.nounWithAmount(amount, " card") + "?" + orString); + + if (!p.getController().confirmPayment(part, sb.toString())) { + return false; + } + + for (Player player : res) { + player.drawCards(amount); + } + } + else if (part instanceof CostGainLife) { + PaymentDecision pd = part.accept(hcd); + + if (pd == null) + return false; + else + part.payAsDecided(p, pd, sourceAbility); + } + else if (part instanceof CostAddMana) { + if (!p.getController().confirmPayment(part, "Do you want to add " + ((CostAddMana) part).toString() + " to your mana pool?" + orString)) { + return false; + } + PaymentDecision pd = part.accept(hcd); + + if (pd == null) + return false; + else + part.payAsDecided(p, pd, sourceAbility); + } + else if (part instanceof CostMill) { + final int amount = getAmountFromPart(part, source, sourceAbility); + final List list = p.getCardsIn(ZoneType.Library); + if (list.size() < amount) { return false; } + if (!p.getController().confirmPayment(part, "Do you want to mill " + amount + " card" + (amount == 1 ? "" : "s") + "?" + orString)) { + return false; + } + List listmill = p.getCardsIn(ZoneType.Library, amount); + ((CostMill) part).executePayment(sourceAbility, listmill); + } + else if (part instanceof CostFlipCoin) { + final int amount = getAmountFromPart(part, source, sourceAbility); + if (!p.getController().confirmPayment(part, "Do you want to flip " + amount + " coin" + (amount == 1 ? "" : "s") + "?" + orString)) { + return false; + } + final int n = FlipCoinEffect.getFilpMultiplier(p); + for (int i = 0; i < amount; i++) { + FlipCoinEffect.flipCoinCall(p, sourceAbility, n); + } + } + else if (part instanceof CostDamage) { + int amount = getAmountFromPartX(part, source, sourceAbility); + if (!p.canPayLife(amount)) { + return false; + } + + if (!p.getController().confirmPayment(part, "Do you want " + source + " to deal " + amount + " damage to you?")) { + return false; + } + + p.addDamage(amount, source); + } + else if (part instanceof CostPutCounter) { + CounterType counterType = ((CostPutCounter) part).getCounter(); + int amount = getAmountFromPartX(part, source, sourceAbility); + if (part.payCostFromSource()) { + if (!source.canReceiveCounters(counterType)) { + String message = String.format("Won't be able to pay upkeep for %s but it can't have %s counters put on it.", source, counterType.getName()); + p.getGame().getGameLog().add(GameLogEntryType.STACK_RESOLVE, message); + return false; + } + + if (!p.getController().confirmPayment(part, "Do you want to put " + Lang.nounWithAmount(amount, counterType.getName() + " counter") + " on " + source + "?")) { + return false; + } + + source.addCounter(counterType, amount, false); + } + else { + List list = p.getGame().getCardsIn(ZoneType.Battlefield); + list = CardLists.getValidCards(list, part.getType().split(";"), p, source); + if (list.isEmpty()) { return false; } + if (!p.getController().confirmPayment(part, "Do you want to put " + Lang.nounWithAmount(amount, counterType.getName() + " counter") + " on " + part.getTypeDescription() + "?")) { + return false; + } + while (amount > 0) { + InputSelectCardsFromList inp = new InputSelectCardsFromList(1, 1, list); + inp.setMessage("Select a card to add a counter to"); + inp.setCancelAllowed(true); + inp.showAndWait(); + if (inp.hasCancelled()) { + continue; + } + Card selected = inp.getFirstSelected(); + selected.addCounter(counterType, 1, false); + amount--; + } + } + } + else if (part instanceof CostRemoveCounter) { + CounterType counterType = ((CostRemoveCounter) part).counter; + int amount = getAmountFromPartX(part, source, sourceAbility); + + if (!part.canPay(sourceAbility)) { + return false; + } + + if (!p.getController().confirmPayment(part, "Do you want to remove " + Lang.nounWithAmount(amount, counterType.getName() + " counter") + " from " + source + "?")) { + return false; + } + + source.subtractCounter(counterType, amount); + } + else if (part instanceof CostRemoveAnyCounter) { + int amount = getAmountFromPartX(part, source, sourceAbility); + List list = new ArrayList(p.getCardsIn(ZoneType.Battlefield)); + int allCounters = 0; + for (Card c : list) { + final Map tgtCounters = c.getCounters(); + for (Integer value : tgtCounters.values()) { + allCounters += value; + } + } + if (allCounters < amount) { return false; } + if (!p.getController().confirmPayment(part, "Do you want to remove counters from " + part.getDescriptiveType() + " ?")) { + return false; + } + + list = CardLists.getValidCards(list, ((CostRemoveAnyCounter) part).getType().split(";"), p, source); + while (amount > 0) { + final CounterType counterType; + list = CardLists.filter(list, new Predicate() { + @Override + public boolean apply(final Card card) { + return card.hasCounters(); + } + }); + if (list.isEmpty()) { return false; } + InputSelectCardsFromList inp = new InputSelectCardsFromList(1, 1, list); + inp.setMessage("Select a card to remove a counter"); + inp.setCancelAllowed(true); + inp.showAndWait(); + if (inp.hasCancelled()) { + continue; + } + Card selected = inp.getFirstSelected(); + final Map tgtCounters = selected.getCounters(); + final ArrayList typeChoices = new ArrayList(); + for (CounterType key : tgtCounters.keySet()) { + if (tgtCounters.get(key) > 0) { + typeChoices.add(key); + } + } + if (typeChoices.size() > 1) { + String cprompt = "Select type counters to remove"; + counterType = GuiChoose.one(cprompt, typeChoices); + } + else { + counterType = typeChoices.get(0); + } + selected.subtractCounter(counterType, 1); + amount--; + } + } + else if (part instanceof CostExile) { + if ("All".equals(part.getType())) { + if (!p.getController().confirmPayment(part, "Do you want to exile all cards in your graveyard?")) { + return false; + } + + List cards = new ArrayList(p.getCardsIn(ZoneType.Graveyard)); + for (final Card card : cards) { + p.getGame().getAction().exile(card); + } + } + else { + CostExile costExile = (CostExile) part; + ZoneType from = costExile.getFrom(); + List list = CardLists.getValidCards(p.getCardsIn(from), part.getType().split(";"), p, source); + final int nNeeded = getAmountFromPart(costPart, source, sourceAbility); + if (list.size() < nNeeded) { + return false; + } + if (from == ZoneType.Library) { + if (!p.getController().confirmPayment(part, "Do you want to exile " + nNeeded + + " card" + (nNeeded == 1 ? "" : "s") + " from your library?")) { + return false; + } + list = list.subList(0, nNeeded); + for (Card c : list) { + p.getGame().getAction().exile(c); + } + return true; + } + // replace this with input + for (int i = 0; i < nNeeded; i++) { + final Card c = GuiChoose.oneOrNone("Exile from " + from, list); + if (c == null) { + return false; + } + + list.remove(c); + p.getGame().getAction().exile(c); + } + } + } + else if (part instanceof CostPutCardToLib) { + int amount = Integer.parseInt(((CostPutCardToLib) part).getAmount()); + final ZoneType from = ((CostPutCardToLib) part).getFrom(); + final boolean sameZone = ((CostPutCardToLib) part).isSameZone(); + List list; + if (sameZone) { + list = p.getGame().getCardsIn(from); + } + else { + list = p.getCardsIn(from); + } + list = CardLists.getValidCards(list, part.getType().split(";"), p, source); + + if (sameZone) { // Jotun Grunt + List players = p.getGame().getPlayers(); + List payableZone = new ArrayList(); + for (Player player : players) { + List enoughType = CardLists.filter(list, CardPredicates.isOwner(player)); + if (enoughType.size() < amount) { + list.removeAll(enoughType); + } else { + payableZone.add(player); + } + } + Player chosen = GuiChoose.oneOrNone(String.format("Put cards from whose %s?", from), payableZone); + if (chosen == null) { + return false; + } + + List typeList = CardLists.filter(list, CardPredicates.isOwner(chosen)); + + for (int i = 0; i < amount; i++) { + if (typeList.isEmpty()) { + return false; + } + + final Card c = GuiChoose.oneOrNone("Put cards to Library", typeList); + + if (c != null) { + typeList.remove(c); + p.getGame().getAction().moveToLibrary(c, Integer.parseInt(((CostPutCardToLib) part).getLibPos())); + } + else { + return false; + } + } + } + else if (from == ZoneType.Hand) { // Tainted Specter + boolean hasPaid = payCostPart(sourceAbility, (CostPartWithList)part, amount, list, "put into library." + orString); + if (!hasPaid) { + return false; + } + } + return true; + } + else if (part instanceof CostSacrifice) { + int amount = Integer.parseInt(((CostSacrifice)part).getAmount()); + List list = CardLists.getValidCards(p.getCardsIn(ZoneType.Battlefield), part.getType(), p, source); + boolean hasPaid = payCostPart(sourceAbility, (CostPartWithList)part, amount, list, "sacrifice." + orString); + if (!hasPaid) { return false; } + } + else if (part instanceof CostGainControl) { + int amount = Integer.parseInt(((CostGainControl)part).getAmount()); + List list = CardLists.getValidCards(p.getGame().getCardsIn(ZoneType.Battlefield), part.getType(), p, source); + boolean hasPaid = payCostPart(sourceAbility, (CostPartWithList)part, amount, list, "gain control." + orString); + if (!hasPaid) { return false; } + } + else if (part instanceof CostReturn) { + List list = CardLists.getValidCards(p.getCardsIn(ZoneType.Battlefield), part.getType(), p, source); + int amount = getAmountFromPartX(part, source, sourceAbility); + boolean hasPaid = payCostPart(sourceAbility, (CostPartWithList)part, amount, list, "return to hand." + orString); + if (!hasPaid) { return false; } + } + else if (part instanceof CostDiscard) { + List list = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), part.getType(), p, source); + int amount = getAmountFromPartX(part, source, sourceAbility); + boolean hasPaid = payCostPart(sourceAbility, (CostPartWithList)part, amount, list, "discard." + orString); + if (!hasPaid) { return false; } + } + else if (part instanceof CostReveal) { + List list = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), part.getType(), p, source); + int amount = getAmountFromPartX(part, source, sourceAbility); + boolean hasPaid = payCostPart(sourceAbility, (CostPartWithList)part, amount, list, "reveal." + orString); + if (!hasPaid) { return false; } + } + else if (part instanceof CostTapType) { + List list = CardLists.getValidCards(p.getCardsIn(ZoneType.Battlefield), part.getType(), p, source); + list = CardLists.filter(list, Presets.UNTAPPED); + int amount = getAmountFromPartX(part, source, sourceAbility); + boolean hasPaid = payCostPart(sourceAbility, (CostPartWithList)part, amount, list, "tap." + orString); + if (!hasPaid) { return false; } + } + else if (part instanceof CostPartMana) { + if (!((CostPartMana) part).getManaToPay().isZero()) { // non-zero costs require input + mayRemovePart = false; + } + } + else { + throw new RuntimeException("GameActionUtil.payCostDuringAbilityResolve - An unhandled type of cost was met: " + part.getClass()); + } + + if (mayRemovePart) { + remainingParts.remove(part); + } + } + + if (remainingParts.isEmpty()) { + return true; + } + if (remainingParts.size() > 1) { + throw new RuntimeException("GameActionUtil.payCostDuringAbilityResolve - Too many payment types - " + source); + } + costPart = remainingParts.get(0); + // check this is a mana cost + if (!(costPart instanceof CostPartMana)) { + throw new RuntimeException("GameActionUtil.payCostDuringAbilityResolve - The remaining payment type is not Mana."); + } + + if (prompt == null) { + String promptCurrent = current == null ? "" : "Current Card: " + current; + prompt = source + "\n" + promptCurrent; + } + + if( sourceAbility != null ) + sourceAbility.clearManaPaid(); + boolean paid = p.getController().payManaCost(cost.getCostMana(), sourceAbility, prompt, false); + if (!paid) { + p.getManaPool().refundManaPaid(sourceAbility); + } + return paid; + } + + private static boolean payCostPart(SpellAbility sourceAbility, CostPartWithList cpl, int amount, List list, String actionName) { + if (list.size() < amount) { return false; } // unable to pay (not enough cards) + + InputSelectCardsFromList inp = new InputSelectCardsFromList(amount, amount, list); + inp.setMessage("Select %d " + cpl.getDescriptiveType() + " card(s) to " + actionName); + inp.setCancelAllowed(true); + + inp.showAndWait(); + if (inp.hasCancelled() || inp.getSelected().size() != amount) { + return false; + } + + for (Card c : inp.getSelected()) { + cpl.executePayment(sourceAbility, c); + } + if (sourceAbility != null) { + cpl.reportPaidCardsTo(sourceAbility); + } + return true; + } + + + private static boolean handleOfferingAndConvoke(final SpellAbility ability, boolean manaInputCancelled, boolean isPaid) { + boolean done = !manaInputCancelled && isPaid; + if (ability.isOffering() && ability.getSacrificedAsOffering() != null) { + final Card offering = ability.getSacrificedAsOffering(); + offering.setUsedToPay(false); + if (done) { + ability.getHostCard().getGame().getAction().sacrifice(offering, ability); + } + ability.resetSacrificedAsOffering(); + } + if (ability.getTappedForConvoke() != null) { + for (final Card c : ability.getTappedForConvoke()) { + c.setTapped(false); + if (done) { + c.tap(); + } + } + ability.clearTappedForConvoke(); + } + return done; + } + + public static boolean payManaCost(final ManaCost realCost, final CostPartMana mc, final SpellAbility ability, final Player activator, String prompt, boolean isActivatedSa) { + final Card source = ability.getHostCard(); + ManaCostBeingPaid toPay = new ManaCostBeingPaid(realCost, mc.getRestiction()); + + boolean xWasBilled = false; + String xInCard = source.getSVar("X"); + if (mc.getAmountOfX() > 0 && !"Count$xPaid".equals(xInCard)) { // announce X will overwrite whatever was in card script + // this currently only works for things about Targeted object + int xCost = AbilityUtils.calculateAmount(source, "X", ability) * mc.getAmountOfX(); + byte xColor = MagicColor.fromName(ability.hasParam("XColor") ? ability.getParam("XColor") : "1"); + toPay.increaseShard(ManaCostShard.valueOf(xColor), xCost); + xWasBilled = true; + } + int timesMultikicked = ability.getHostCard().getKickerMagnitude(); + if ( timesMultikicked > 0 && ability.isAnnouncing("Multikicker")) { + ManaCost mkCost = ability.getMultiKickerManaCost(); + for(int i = 0; i < timesMultikicked; i++) + toPay.addManaCost(mkCost); + } + + if( isActivatedSa ) + ManaCostAdjustment.adjust(toPay, ability, false); + + InputPayMana inpPayment; + if (ability.isOffering() && ability.getSacrificedAsOffering() == null) { + System.out.println("Sacrifice input for Offering cancelled"); + return false; + } + if (!toPay.isPaid()) { + inpPayment = new InputPayManaOfCostPayment(toPay, ability, activator); + inpPayment.setMessagePrefix(prompt); + inpPayment.showAndWait(); + if (!inpPayment.isPaid()) { + return handleOfferingAndConvoke(ability, true, false); + } + + source.setColorsPaid(toPay.getColorsPaid()); + source.setSunburstValue(toPay.getSunburst()); + } + if (mc.getAmountOfX() > 0) { + if (!ability.isAnnouncing("X") && !xWasBilled) { + source.setXManaCostPaid(0); + inpPayment = new InputPayManaX(ability, mc.getAmountOfX(), mc.canXbe0()); + inpPayment.showAndWait(); + if (!inpPayment.isPaid()) { + return false; + } + } else { + int x = AbilityUtils.calculateAmount(source, "X", ability); + source.setXManaCostPaid(x); + } + } + + // Handle convoke and offerings + if (ability.isOffering() && ability.getSacrificedAsOffering() != null) { + System.out.println("Finishing up Offering"); + final Card offering = ability.getSacrificedAsOffering(); + offering.setUsedToPay(false); + activator.getGame().getAction().sacrifice(offering, ability); + ability.resetSacrificedAsOffering(); + } + if (ability.getTappedForConvoke() != null) { + for (final Card c : ability.getTappedForConvoke()) { + c.setTapped(false); + c.tap(); + } + ability.clearTappedForConvoke(); + } + return handleOfferingAndConvoke(ability, false, true); + } +} diff --git a/forge-m-base/src/forge/player/HumanPlaySpellAbility.java b/forge-m-base/src/forge/player/HumanPlaySpellAbility.java new file mode 100644 index 00000000000..4b3e75d5975 --- /dev/null +++ b/forge-m-base/src/forge/player/HumanPlaySpellAbility.java @@ -0,0 +1,262 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.player; + +import com.google.common.collect.Iterables; + +import forge.card.CardType; +import forge.game.Game; +import forge.game.GameObject; +import forge.game.ability.AbilityFactory; +import forge.game.ability.AbilityUtils; +import forge.game.card.Card; +import forge.game.cost.CostPartMana; +import forge.game.cost.CostPayment; +import forge.game.mana.ManaPool; +import forge.game.player.Player; +import forge.game.player.PlayerController; +import forge.game.spellability.AbilitySub; +import forge.game.spellability.Spell; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.TargetRestrictions; +import forge.game.zone.Zone; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + *

+ * SpellAbility_Requirements class. + *

+ * + * @author Forge + * @version $Id: HumanPlaySpellAbility.java 24317 2014-01-17 08:32:39Z Max mtg $ + */ +public class HumanPlaySpellAbility { + private final SpellAbility ability; + private final CostPayment payment; + + public HumanPlaySpellAbility(final SpellAbility sa, final CostPayment cp) { + this.ability = sa; + this.payment = cp; + } + + public final void playAbility(boolean mayChooseTargets, boolean isFree, boolean skipStack) { + final Player human = ability.getActivatingPlayer(); + final Game game = ability.getActivatingPlayer().getGame(); + + // used to rollback + Zone fromZone = null; + int zonePosition = 0; + final ManaPool manapool = human.getManaPool(); + + final Card c = this.ability.getHostCard(); + boolean manaConversion = (ability.isSpell() && c.hasKeyword("May spend mana as though it were mana of any color to cast CARDNAME")); + + Map params = AbilityFactory.getMapParams(c.getSVar("ManaConversionMatrix")); + + if (this.ability instanceof Spell && !c.isCopiedSpell()) { + fromZone = game.getZoneOf(c); + if (fromZone != null) { + zonePosition = fromZone.getCards().indexOf(c); + } + this.ability.setHostCard(game.getAction().moveToStack(c)); + } + + // freeze Stack. No abilities should go onto the stack while I'm filling requirements. + game.getStack().freezeStack(); + + if (manaConversion) { + AbilityUtils.applyManaColorConversion(human, params); + } + // This line makes use of short-circuit evaluation of boolean values, that is each subsequent argument + // is only executed or evaluated if the first argument does not suffice to determine the value of the expression + boolean prerequisitesMet = this.announceValuesLikeX() + && this.announceType() + && (!mayChooseTargets || setupTargets()) // if you can choose targets, then do choose them. + && (isFree || this.payment.payCost(new HumanCostDecision(human, ability, ability.getHostCard()))); + + if (!prerequisitesMet) { + if (!ability.isTrigger()) { + rollbackAbility(fromZone, zonePosition); + if (ability.isMadness()) { + // if a player failed to play madness cost, move the card to graveyard + game.getAction().moveToGraveyard(c); + ability.setMadness(false); + } else if (ability.getHostCard().isBestowed()) { + ability.getHostCard().unanimateBestow(); + } + } + if (manaConversion) { + manapool.restoreColorReplacements(); + } + return; + } + + if (isFree || this.payment.isFullyPaid()) { + if (skipStack) { + AbilityUtils.resolve(this.ability); + } + else { + this.enusureAbilityHasDescription(this.ability); + game.getStack().addAndUnfreeze(this.ability); + } + + // no worries here. The same thread must resolve, and by this moment ability will have been resolved already + // Triggers haven't resolved yet ?? + if (mayChooseTargets) { + clearTargets(ability); + } + if (manaConversion) { + manapool.restoreColorReplacements(); + } + } + } + + private final boolean setupTargets() { + // Skip to paying if parent ability doesn't target and has no subAbilities. + // (or trigger case where its already targeted) + SpellAbility currentAbility = ability; + final Card source = ability.getHostCard(); + do { + TargetRestrictions tgt = currentAbility.getTargetRestrictions(); + if (tgt != null && tgt.doesTarget()) { + clearTargets(currentAbility); + Player targetingPlayer; + if (currentAbility.hasParam("TargetingPlayer")) { + List candidates = AbilityUtils.getDefinedPlayers(source, currentAbility.getParam("TargetingPlayer"), currentAbility); + // activator chooses targeting player + targetingPlayer = ability.getActivatingPlayer().getController().chooseSingleEntityForEffect( + candidates, currentAbility, "Choose the targeting player"); + } else { + targetingPlayer = ability.getActivatingPlayer(); + } + + if (!targetingPlayer.getController().chooseTargetsFor(currentAbility)) + return false; + } + final SpellAbility subAbility = currentAbility.getSubAbility(); + if (subAbility != null) { + // This is necessary for "TargetsWithDefinedController$ ParentTarget" + ((AbilitySub) subAbility).setParent(currentAbility); + } + currentAbility = subAbility; + } while (currentAbility != null); + return true; + } + + public final void clearTargets(SpellAbility ability) { + TargetRestrictions tg = ability.getTargetRestrictions(); + if (tg != null) { + ability.resetTargets(); + tg.calculateStillToDivide(ability.getParam("DividedAsYouChoose"), ability.getHostCard(), ability); + } + } + + private void rollbackAbility(Zone fromZone, int zonePosition) { + // cancel ability during target choosing + final Game game = ability.getActivatingPlayer().getGame(); + + if (fromZone != null) { // and not a copy + // add back to where it came from + game.getAction().moveTo(fromZone, ability.getHostCard(), zonePosition >= 0 ? Integer.valueOf(zonePosition) : null); + } + + clearTargets(ability); + + this.ability.resetOnceResolved(); + this.payment.refundPayment(); + game.getStack().clearFrozen(); + } + + private boolean announceValuesLikeX() { + // Announcing Requirements like Choosing X or Multikicker + // SA Params as comma delimited list + String announce = ability.getParam("Announce"); + if (announce != null) { + for(String aVar : announce.split(",")) { + String varName = aVar.trim(); + + boolean isX = "X".equalsIgnoreCase(varName); + CostPartMana manaCost = ability.getPayCosts().getCostMana(); + boolean allowZero = !ability.hasParam("XCantBe0") && (!isX || manaCost == null || manaCost.canXbe0()); + + Integer value = ability.getActivatingPlayer().getController().announceRequirements(ability, varName, allowZero); + if (value == null) { + return false; + } + + ability.setSVar(varName, value.toString()); + if ("Multikicker".equals(varName)) { + ability.getHostCard().setKickerMagnitude(value); + } + else { + ability.getHostCard().setSVar(varName, value.toString()); + } + } + } + return true; + } + + private boolean announceType() { + // Announcing Requirements like choosing creature type or number + String announce = ability.getParam("AnnounceType"); + PlayerController pc = ability.getActivatingPlayer().getController(); + if (announce != null) { + for(String aVar : announce.split(",")) { + String varName = aVar.trim(); + if ("CreatureType".equals(varName)) { + String choice = pc.chooseSomeType("Creature", ability, CardType.getCreatureTypes(), new ArrayList()); + ability.getHostCard().setChosenType(choice); + } + if ("ChooseNumber".equals(varName)) { + int min = Integer.parseInt(ability.getParam("Min")); + int max = Integer.parseInt(ability.getParam("Max")); + int i = ability.getActivatingPlayer().getController().chooseNumber(ability, + "Choose a number", min, max); + ability.getHostCard().setChosenNumber(i); + } + } + } + return true; + } + + private void enusureAbilityHasDescription(SpellAbility ability) { + if (!StringUtils.isBlank(ability.getStackDescription())) { + return; + } + + // For older abilities that don't setStackDescription set it here + final StringBuilder sb = new StringBuilder(); + sb.append(ability.getHostCard().getName()); + if (ability.getTargetRestrictions() != null) { + final Iterable targets = ability.getTargets().getTargets(); + if (!Iterables.isEmpty(targets)) { + sb.append(" - Targeting "); + for (final GameObject o : targets) { + sb.append(o.toString()).append(" "); + } + } + } + + ability.setStackDescription(sb.toString()); + } +} diff --git a/forge-m-base/src/forge/screens/match/FControl.java b/forge-m-base/src/forge/screens/match/FControl.java index 64c2c2a8efa..2bca03812fd 100644 --- a/forge-m-base/src/forge/screens/match/FControl.java +++ b/forge-m-base/src/forge/screens/match/FControl.java @@ -1,24 +1,54 @@ package forge.screens.match; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +import forge.FThreads; import forge.Forge; import forge.game.Game; import forge.game.Match; import forge.game.card.Card; +import forge.game.combat.Combat; +import forge.game.phase.PhaseType; import forge.game.player.LobbyPlayer; import forge.game.player.Player; +import forge.game.zone.Zone; +import forge.game.zone.ZoneType; import forge.model.FModel; +import forge.screens.match.events.IUiEventVisitor; +import forge.screens.match.events.UiEvent; +import forge.screens.match.events.UiEventAttackerDeclared; +import forge.screens.match.events.UiEventBlockerAssigned; +import forge.screens.match.input.InputQueue; +import forge.screens.match.views.VPhaseIndicator.PhaseLabel; +import forge.screens.match.views.VPlayerPanel; import forge.utils.ForgePreferences.FPref; public class FControl { + private FControl() { } //don't allow creating instance + private static Game game; private static MatchScreen view; + private static InputQueue inputQueue; private static List sortedPlayers; + private static final EventBus uiEvents; + private static MatchUiEventVisitor visitor = new MatchUiEventVisitor(); - public static void startGame(final Match match0, final MatchScreen view0) { - game = match0.createGame(); + static { + uiEvents = new EventBus("ui events"); + //uiEvents.register(Singletons.getControl().getSoundSystem()); + uiEvents.register(visitor); + } + + public static void startGame(final Match match, final MatchScreen view0) { + game = match.createGame(); view = view0; /*if (game.getRules().getGameType() == GameType.Quest) { @@ -30,14 +60,14 @@ public class FControl { game.subscribeToEvents(qc); // this one listens to player's mulligans ATM }*/ - //inputQueue = new InputQueue(); + inputQueue = new InputQueue(); //game.subscribeToEvents(Singletons.getControl().getSoundSystem()); LobbyPlayer humanLobbyPlayer = game.getRegisteredPlayers().get(0).getLobbyPlayer(); //FServer.instance.getLobby().getGuiPlayer(); // The UI controls should use these game data as models initMatch(game.getRegisteredPlayers(), humanLobbyPlayer); - + // It's important to run match in a different thread to allow GUI inputs to be invoked from inside game. // Game is set on pause while gui player takes decisions /*game.getAction().invoke(new Runnable() { @@ -48,10 +78,36 @@ public class FControl { });*/ } + public static Game getGame() { + return game; + } + public static MatchScreen getView() { return view; } + public static InputQueue getInputQueue() { + return inputQueue; + } + + public static boolean stopAtPhase(final Player turn, final PhaseType phase) { + PhaseLabel label = getPlayerPanel(turn).getPhaseIndicator().getLabel(phase); + return label == null || label.getStopAtPhase(); + } + + public static void setCard(final Card c) { + FThreads.assertExecutedByEdt(true); + setCard(c, false); + } + + public static void setCard(final Card c, final boolean showFlipped) { + //TODO + } + + private static int getPlayerIndex(Player player) { + return sortedPlayers.indexOf(player); + } + public static void endCurrentGame() { if (game == null) { return; } @@ -132,7 +188,152 @@ public class FControl { return sortedPlayers; } + public static void resetAllPhaseButtons() { + for (final VPlayerPanel panel : view.getPlayerPanels()) { + panel.getPhaseIndicator().resetPhaseButtons(); + } + } + + public static void showMessage(final String s0) { + view.getPrompt().setMessage(s0); + } + + public static VPlayerPanel getPlayerPanel(Player p) { + int idx = getPlayerIndex(p); + return idx < 0 ? null : view.getPlayerPanels().get(idx); + } + public static boolean mayShowCard(Card c) { return true;// game == null || !gameHasHumanPlayer || c.canBeShownTo(getCurrentPlayer()); } + + public static void showCombat(Combat combat) { + /*if (combat != null && combat.getAttackers().size() > 0 && combat.getAttackingPlayer().getGame().getStack().isEmpty()) { + if (selectedDocBeforeCombat == null) { + IVDoc combatDoc = EDocID.REPORT_COMBAT.getDoc(); + if (combatDoc.getParentCell() != null) { + selectedDocBeforeCombat = combatDoc.getParentCell().getSelected(); + if (selectedDocBeforeCombat != combatDoc) { + SDisplayUtil.showTab(combatDoc); + } + else { + selectedDocBeforeCombat = null; //don't need to cache combat doc this way + } + } + } + } + else if (selectedDocBeforeCombat != null) { //re-select doc that was selected before once combat finished + SDisplayUtil.showTab(selectedDocBeforeCombat); + selectedDocBeforeCombat = null; + } + CCombat.SINGLETON_INSTANCE.setModel(combat); + CCombat.SINGLETON_INSTANCE.update();*/ + } // showBlockers() + + private static Set highlightedPlayers = new HashSet(); + public static void setHighlighted(Player ge, boolean b) { + if (b) highlightedPlayers.add(ge); + else highlightedPlayers.remove(ge); + } + + public static boolean isHighlighted(Player player) { + return highlightedPlayers.contains(player); + } + + private static Set highlightedCards = new HashSet(); + // used to highlight cards in UI + public static void setUsedToPay(Card card, boolean value) { + FThreads.assertExecutedByEdt(true); + + boolean hasChanged = value ? highlightedCards.add(card) : highlightedCards.remove(card); + if (hasChanged) { // since we are in UI thread, may redraw the card right now + updateSingleCard(card); + } + } + + public static boolean isUsedToPay(Card card) { + return highlightedCards.contains(card); + } + + public static void updateZones(List> zonesToUpdate) { + //System.out.println("updateZones " + zonesToUpdate); + /*for (Pair kv : zonesToUpdate) { + Player owner = kv.getKey(); + ZoneType zt = kv.getValue(); + + if (zt == ZoneType.Command) + getCommandFor(owner).getTabletop().setupPlayZone(); + else if (zt == ZoneType.Hand) { + VHand vHand = getHandFor(owner); + if (null != vHand) + vHand.getLayoutControl().updateHand(); + getFieldViewFor(owner).getDetailsPanel().updateZones(); + } + else if (zt == ZoneType.Battlefield) { + getFieldViewFor(owner).getTabletop().setupPlayZone(); + } else if (zt == ZoneType.Ante) { + CAntes.SINGLETON_INSTANCE.update(); + } + else { + getFieldViewFor(owner).getDetailsPanel().updateZones(); + } + }*/ + } + + // Player's mana pool changes + public static void updateManaPool(List manaPoolUpdate) { + /*for (Player p : manaPoolUpdate) { + getFieldViewFor(p).getDetailsPanel().updateManaPool(); + }*/ + } + + // Player's lives and poison counters + public static void updateLives(List livesUpdate) { + /*for (Player p : livesUpdate) { + getFieldViewFor(p).updateDetails(); + }*/ + } + + public static void updateCards(Set cardsToUpdate) { + for (Card c : cardsToUpdate) { + updateSingleCard(c); + } + } + + public static void updateSingleCard(Card c) { + Zone zone = c.getZone(); + if (zone != null && zone.getZoneType() == ZoneType.Battlefield) { + /*PlayArea pa = getFieldViewFor(zone.getPlayer()).getTabletop(); + pa.updateSingleCard(c);*/ + } + } + + private final static boolean LOG_UIEVENTS = false; + + // UI-related events should arrive here + public static void fireEvent(UiEvent uiEvent) { + if (LOG_UIEVENTS) { + //System.out.println("UI: " + uiEvent.toString() + " \t\t " + FThreads.debugGetStackTraceItem(4, true)); + } + uiEvents.post(uiEvent); + } + + private static class MatchUiEventVisitor implements IUiEventVisitor { + @Override + public Void visit(UiEventBlockerAssigned event) { + updateSingleCard(event.blocker); + return null; + } + + @Override + public Void visit(UiEventAttackerDeclared event) { + updateSingleCard(event.attacker); + return null; + } + + @Subscribe + public void receiveEvent(UiEvent evt) { + evt.visit(this); + } + } } diff --git a/forge-m-base/src/forge/screens/match/FControlGameEventHandler.java b/forge-m-base/src/forge/screens/match/FControlGameEventHandler.java new file mode 100644 index 00000000000..ada0e7f8c41 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/FControlGameEventHandler.java @@ -0,0 +1,385 @@ +package forge.screens.match; + +import com.google.common.eventbus.Subscribe; + +import forge.FThreads; +import forge.game.Game; +import forge.game.card.Card; +import forge.game.event.*; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.zone.PlayerZone; +import forge.game.zone.Zone; +import forge.game.zone.ZoneType; +import forge.net.FServer; +import forge.screens.match.views.VPhaseIndicator.PhaseLabel; +import forge.toolbox.GuiChoose; +import forge.util.Lang; +import forge.util.maps.MapOfLists; + +import org.apache.commons.lang3.tuple.Pair; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicBoolean; + +public class FControlGameEventHandler extends IGameEventVisitor.Base { + public FControlGameEventHandler() { + } + + @Subscribe + public void receiveGameEvent(final GameEvent ev) { + ev.visit(this); + } + + private final AtomicBoolean phaseUpdPlanned = new AtomicBoolean(false); + @Override + public Void visit(final GameEventTurnPhase ev) { + if (phaseUpdPlanned.getAndSet(true)) return null; + + FThreads.invokeInEdtNowOrLater(new Runnable() { @Override public void run() { + PhaseHandler pH = FControl.getGame().getPhaseHandler(); + Player p = pH.getPlayerTurn(); + PhaseType ph = pH.getPhase(); + + phaseUpdPlanned.set(false); + + PhaseLabel lbl = FControl.getPlayerPanel(p).getPhaseIndicator().getLabel(ph); + + FControl.resetAllPhaseButtons(); + if (lbl != null) { lbl.setActive(true); } + } }); + return null; + } + + /* (non-Javadoc) + * @see forge.game.event.IGameEventVisitor.Base#visit(forge.game.event.GameEventPlayerPriority) + */ + private final AtomicBoolean combatUpdPlanned = new AtomicBoolean(false); + @Override + public Void visit(GameEventPlayerPriority event) { + if (combatUpdPlanned.getAndSet(true)) { return null; } + FThreads.invokeInEdtNowOrLater(new Runnable() { + @Override + public void run() { + combatUpdPlanned.set(false); + FControl.showCombat(FControl.getGame().getCombat()); + } + }); + return null; + } + + private final AtomicBoolean turnUpdPlanned = new AtomicBoolean(false); + @Override + public Void visit(final GameEventTurnBegan event) { + if (turnUpdPlanned.getAndSet(true)) { return null; } + + final Game game = FControl.getGame(); // to make sure control gets a correct game instance + FThreads.invokeInEdtNowOrLater(new Runnable() { + @Override + public void run() { + /*VField nextField = FControl.getFieldViewFor(event.turnOwner); + SDisplayUtil.showTab(nextField);*/ + + turnUpdPlanned.set(false); + FControl.getView().getPrompt().updateText(game); + } + }); + return null; + } + + @Override + public Void visit(GameEventAnteCardsSelected ev) { + // Require EDT here? + List options = new ArrayList(); + for (final Entry kv : ((GameEventAnteCardsSelected) ev).cards.entries()) { + options.add(" -- From " + Lang.getPossesive(kv.getKey().getName()) + " deck --"); + options.add(kv.getValue()); + } + GuiChoose.one("These cards were chosen to ante", options); + return null; + } + + @Override + public Void visit(GameEventPlayerControl ev) { + if (FControl.getGame().isGameOver()) { + return null; + } + + FThreads.invokeInEdtNowOrLater(new Runnable() { @Override public void run() { + FControl.initHandViews(FServer.getLobby().getGuiPlayer()); + /*SLayoutIO.loadLayout(null); + VMatchUI.SINGLETON_INSTANCE.populate(); + for (VHand h : VMatchUI.SINGLETON_INSTANCE.getHands()) { + h.getLayoutControl().updateHand(); + }*/ + } }); + return null; + } + + private final Runnable unlockGameThreadOnGameOver = new Runnable() { @Override public void run() { + FControl.getInputQueue().onGameOver(true); // this will unlock any game threads waiting for inputs to complete + } }; + + @Override + public Void visit(GameEventGameOutcome ev) { + FThreads.invokeInEdtNowOrLater(unlockGameThreadOnGameOver); + return null; + } + + @Override + public Void visit(GameEventGameFinished ev) { + FThreads.invokeInEdtNowOrLater(new Runnable() { + @Override + public void run() { + /*new ViewWinLose(FControl.getGame()); + SOverlayUtils.showOverlay();*/ + } + }); + return null; + } + + private final AtomicBoolean stackUpdPlanned = new AtomicBoolean(false); + private final Runnable updStack = new Runnable() { + @Override + public void run() { + stackUpdPlanned.set(false); + FControl.getView().getStack().update(); + } + }; + + @Override + public Void visit(GameEventSpellAbilityCast event) { + if (!stackUpdPlanned.getAndSet(true)) { + FThreads.invokeInEdtNowOrLater(updStack); + } + return null; + } + @Override + public Void visit(GameEventSpellResolved event) { + if (!stackUpdPlanned.getAndSet(true)) { + FThreads.invokeInEdtNowOrLater(updStack); + } + return null; + } + @Override + public Void visit(GameEventSpellRemovedFromStack event) { + if (!stackUpdPlanned.getAndSet(true)) { + FThreads.invokeInEdtNowOrLater(updStack); + } + return null; + } + + private final List> zonesToUpdate = new Vector>(); + private final Runnable updZones = new Runnable() { + @Override public void run() { + synchronized (zonesToUpdate) { + FControl.updateZones(zonesToUpdate); + zonesToUpdate.clear(); + } + } + }; + + @Override + public Void visit(GameEventZone event) { + if (event.player != null) { + // anything except stack will get here + updateZone(Pair.of(event.player, event.zoneType)); + } + return null; + } + + @Override + public Void visit(GameEventCardAttachment event) { + // TODO Auto-generated method stub + Game game = event.equipment.getGame(); + PlayerZone zEq = (PlayerZone)game.getZoneOf(event.equipment); + if (event.oldEntiy instanceof Card) { + updateZone(game.getZoneOf((Card)event.oldEntiy)); + } + if (event.newTarget instanceof Card) { + updateZone(game.getZoneOf((Card)event.newTarget)); + } + return updateZone(zEq); + } + + private Void updateZone(Zone z) { + return updateZone(Pair.of(z.getPlayer(), z.getZoneType())); + } + + private Void updateZone(Pair kv) { + boolean needUpdate = false; + synchronized (zonesToUpdate) { + needUpdate = zonesToUpdate.isEmpty(); + if (!zonesToUpdate.contains(kv)) { + zonesToUpdate.add(kv); + } + } + if (needUpdate) { + FThreads.invokeInEdtNowOrLater(updZones); + } + return null; + } + + private final Set cardsToUpdate = new HashSet(); + private final Runnable updCards = new Runnable() { + @Override + public void run() { + synchronized (cardsToUpdate) { + FControl.updateCards(cardsToUpdate); + cardsToUpdate.clear(); + } + } + }; + + @Override + public Void visit(GameEventCardTapped event) { + return updateSingleCard(event.card); + } + + @Override + public Void visit(GameEventCardPhased event) { + return updateSingleCard(event.card); + } + + @Override + public Void visit(GameEventCardDamaged event) { + return updateSingleCard(event.card); + } + + @Override + public Void visit(GameEventCardCounters event) { + return updateSingleCard(event.card); + } + + @Override + public Void visit(GameEventBlockersDeclared event) { // This is to draw icons on blockers declared by AI + for (MapOfLists kv : event.blockers.values()) { + for (Collection blockers : kv.values()) { + updateManyCards(blockers); + } + } + return super.visit(event); + } + + @Override + public Void visit(GameEventAttackersDeclared event) { + // Skip redraw for GUI player? + if (event.player.getLobbyPlayer() == FServer.getLobby().getGuiPlayer()) { + return null; + } + + // Update all attackers. + // Although they might have been updated when they were apped, there could be someone with vigilance, not redrawn yet. + updateManyCards(event.attackersMap.values()); + + return super.visit(event); + } + + @Override + public Void visit(GameEventCombatEnded event) { + // This should remove sword/shield icons from combatants by the time game moves to M2 + updateManyCards(event.attackers); + updateManyCards(event.blockers); + return null; + } + + private Void updateSingleCard(Card c) { + boolean needUpdate = false; + synchronized (cardsToUpdate) { + needUpdate = cardsToUpdate.isEmpty(); + if (!cardsToUpdate.contains(c)) { + cardsToUpdate.add(c); + } + } + if (needUpdate) { + FThreads.invokeInEdtNowOrLater(updCards); + } + return null; + } + + private Void updateManyCards(Collection cc) { + boolean needUpdate = false; + synchronized (cardsToUpdate) { + needUpdate = cardsToUpdate.isEmpty(); + cardsToUpdate.addAll(cc); + } + if (needUpdate) { + FThreads.invokeInEdtNowOrLater(updCards); + } + return null; + } + + /* (non-Javadoc) + * @see forge.game.event.IGameEventVisitor.Base#visit(forge.game.event.GameEventCardStatsChanged) + */ + @Override + public Void visit(GameEventCardStatsChanged event) { + // TODO Smart partial updates + return updateManyCards(event.cards); + } + + // Update manapool + private final List manaPoolUpdate = new Vector(); + private final Runnable updManaPool = new Runnable() { + @Override public void run() { + synchronized (manaPoolUpdate) { + FControl.updateManaPool(manaPoolUpdate); + manaPoolUpdate.clear(); + } + } + }; + + @Override + public Void visit(GameEventManaPool event) { + boolean invokeUpdate = false; + synchronized (manaPoolUpdate) { + if (!manaPoolUpdate.contains(event.player)) { + invokeUpdate = manaPoolUpdate.isEmpty(); + manaPoolUpdate.add(event.player); + } + } + if (invokeUpdate) + FThreads.invokeInEdtNowOrLater(updManaPool); + return null; + } + + // Update lives counters + private final List livesUpdate = new Vector(); + private final Runnable updLives = new Runnable() { + @Override public void run() { + synchronized (livesUpdate) { + FControl.updateLives(livesUpdate); + livesUpdate.clear(); + } + } + }; + @Override + public Void visit(GameEventPlayerLivesChanged event) { + boolean invokeUpdate = false; + synchronized (livesUpdate) { + if (!livesUpdate.contains(event.player)) { + invokeUpdate = livesUpdate.isEmpty(); + livesUpdate.add(event.player); + } + } + if (invokeUpdate) + FThreads.invokeInEdtNowOrLater(updLives); + return null; + } + + @Override + public Void visit(GameEventPlayerPoisoned event) { + boolean invokeUpdate = false; + synchronized (livesUpdate) { + if (!livesUpdate.contains(event.receiver)) { + invokeUpdate = livesUpdate.isEmpty(); + livesUpdate.add(event.receiver); + } + } + if (invokeUpdate) + FThreads.invokeInEdtNowOrLater(updLives); + return null; + } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/FControlGamePlayback.java b/forge-m-base/src/forge/screens/match/FControlGamePlayback.java new file mode 100644 index 00000000000..6da9d9079af --- /dev/null +++ b/forge-m-base/src/forge/screens/match/FControlGamePlayback.java @@ -0,0 +1,176 @@ +package forge.screens.match; + +import com.google.common.eventbus.Subscribe; + +import forge.FThreads; +import forge.game.event.*; +import forge.screens.match.input.InputPlaybackControl; + +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicBoolean; + +public class FControlGamePlayback extends IGameEventVisitor.Base { + private final InputPlaybackControl inputPlayback = new InputPlaybackControl(this); + private final AtomicBoolean paused = new AtomicBoolean(false); + + private final CyclicBarrier gameThreadPauser = new CyclicBarrier(2); + + public FControlGamePlayback() { + } + + @Subscribe + public void receiveGameEvent(final GameEvent ev) { ev.visit(this); } + + private int phasesDelay = 200; + private int combatDelay = 400; + private int castDelay = 400; + private int resolveDelay = 400; + + private boolean fasterPlayback = false; + + private void pauseForEvent(int delay) { + try { + Thread.sleep(fasterPlayback ? delay / 10 : delay); + } catch (InterruptedException e) { + // TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log. + e.printStackTrace(); + } + } + + @Override + public Void visit(GameEventBlockersDeclared event) { + pauseForEvent(combatDelay); + return super.visit(event); + } + + /* (non-Javadoc) + * @see forge.game.event.IGameEventVisitor.Base#visit(forge.game.event.GameEventTurnPhase) + */ + @Override + public Void visit(GameEventTurnPhase ev) { + boolean isUiToStop = FControl.stopAtPhase(ev.playerTurn, ev.phase); + + switch(ev.phase) { + case COMBAT_END: + case COMBAT_DECLARE_ATTACKERS: + case COMBAT_DECLARE_BLOCKERS: + if (FControl.getGame().getPhaseHandler().inCombat()) { + pauseForEvent(combatDelay); + } + break; + default: + if (isUiToStop) { + pauseForEvent(phasesDelay); + } + break; + } + + return null; + } + + /* (non-Javadoc) + * @see forge.game.event.IGameEventVisitor.Base#visit(forge.game.event.GameEventDuelFinished) + */ + @Override + public Void visit(GameEventGameFinished event) { + FControl.getInputQueue().removeInput(inputPlayback); + return null; + } + + @Override + public Void visit(GameEventGameStarted event) { + FControl.getInputQueue().setInput(inputPlayback); + return null; + } + + @Override + public Void visit(GameEventLandPlayed event) { + pauseForEvent(resolveDelay); + return super.visit(event); + } + + @Override + public Void visit(final GameEventSpellResolved event) { + FThreads.invokeInEdtNowOrLater(new Runnable() { @Override public void run() { FControl.setCard(event.spell.getHostCard()); } }); + pauseForEvent(resolveDelay); + return null; + } + + /* (non-Javadoc) + * @see forge.game.event.IGameEventVisitor.Base#visit(forge.game.event.GameEventSpellAbilityCast) + */ + @Override + public Void visit(final GameEventSpellAbilityCast event) { + FThreads.invokeInEdtNowOrLater(new Runnable() { @Override public void run() { FControl.setCard(event.sa.getHostCard()); } }); + pauseForEvent(castDelay); + return null; + } + + /* (non-Javadoc) + * @see forge.game.event.IGameEventVisitor.Base#visit(forge.game.event.GameEventPlayerPriority) + */ + @Override + public Void visit(GameEventPlayerPriority event) { + if (paused.get()) { + try { + inputPlayback.onGamePaused(); + gameThreadPauser.await(); + gameThreadPauser.reset(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (BrokenBarrierException e) { + e.printStackTrace(); + } + } + return null; + } + + public void onGameStopRequested() { + paused.set(false); + if (gameThreadPauser.getNumberWaiting() != 0) { + releaseGameThread(); + } + } + + private void releaseGameThread() { + // just need to run another thread through the barrier... not edt preferrably :) + + FControl.getGame().getAction().invoke(new Runnable() { + @Override + public void run() { + try { + gameThreadPauser.await(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log. + e.printStackTrace(); + } catch (BrokenBarrierException e) { + // TODO Auto-generated catch block ignores the exception, but sends it to System.err and probably forge.log. + e.printStackTrace(); + } + } + }); + } + + public void resume() { + paused.set(false); + releaseGameThread(); + } + + public void pause() { + paused.set(true); + } + + public void singleStep() { + releaseGameThread(); + } + + /** + * TODO: Write javadoc for this method. + * @param isFast + */ + public void setSpeed(boolean isFast) { + fasterPlayback = isFast; + } + +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/MatchScreen.java b/forge-m-base/src/forge/screens/match/MatchScreen.java index 4b5185a41ce..11552c3fe0d 100644 --- a/forge-m-base/src/forge/screens/match/MatchScreen.java +++ b/forge-m-base/src/forge/screens/match/MatchScreen.java @@ -1,8 +1,7 @@ package forge.screens.match; -import java.util.HashMap; -import java.util.Map; - +import java.util.ArrayList; +import java.util.List; import forge.screens.FScreen; import forge.screens.match.views.VAvatar; import forge.screens.match.views.VPlayerPanel; @@ -20,7 +19,7 @@ public class MatchScreen extends FScreen { public static FSkinColor BORDER_COLOR = FSkinColor.get(Colors.CLR_BORDERS); private final Match match; - private final Map playerPanels; + private final List playerPanels; //private final VLog log; private final VStack stack; private final VPrompt prompt; @@ -31,12 +30,12 @@ public class MatchScreen extends FScreen { super(true, "Game", true); match = match0; - playerPanels = new HashMap(); + playerPanels = new ArrayList(); for (RegisteredPlayer player : match.getPlayers()) { - playerPanels.put(player, add(new VPlayerPanel(player))); + playerPanels.add(add(new VPlayerPanel(player))); } - bottomPlayerPanel = playerPanels.get(match.getPlayers().get(0)); - topPlayerPanel = playerPanels.get(match.getPlayers().get(1)); + bottomPlayerPanel = playerPanels.get(0); + topPlayerPanel = playerPanels.get(1); topPlayerPanel.setFlipped(true); bottomPlayerPanel.setSelectedZone(ZoneType.Hand); @@ -47,6 +46,18 @@ public class MatchScreen extends FScreen { FControl.startGame(match0, this); } + public VPrompt getPrompt() { + return prompt; + } + + public VStack getStack() { + return stack; + } + + public List getPlayerPanels() { + return playerPanels; + } + @Override public void drawBackground(Graphics g) { super.drawBackground(g); diff --git a/forge-m-base/src/forge/screens/match/events/IUiEventVisitor.java b/forge-m-base/src/forge/screens/match/events/IUiEventVisitor.java new file mode 100644 index 00000000000..099b57092b7 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/events/IUiEventVisitor.java @@ -0,0 +1,6 @@ +package forge.screens.match.events; + +public interface IUiEventVisitor { + T visit(UiEventBlockerAssigned event); + T visit(UiEventAttackerDeclared event); +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/events/UiEvent.java b/forge-m-base/src/forge/screens/match/events/UiEvent.java new file mode 100644 index 00000000000..4f9fe1571bf --- /dev/null +++ b/forge-m-base/src/forge/screens/match/events/UiEvent.java @@ -0,0 +1,7 @@ +package forge.screens.match.events; + + +public abstract class UiEvent { + + public abstract T visit(IUiEventVisitor visitor); +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/events/UiEventAttackerDeclared.java b/forge-m-base/src/forge/screens/match/events/UiEventAttackerDeclared.java new file mode 100644 index 00000000000..1c5f755d332 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/events/UiEventAttackerDeclared.java @@ -0,0 +1,32 @@ +package forge.screens.match.events; + +import forge.game.GameEntity; +import forge.game.card.Card; + +/** + * TODO: Write javadoc for this type. + * + */ +public class UiEventAttackerDeclared extends UiEvent { + + public final Card attacker; + public final GameEntity defender; + + public UiEventAttackerDeclared(Card card, GameEntity currentDefender) { + attacker = card; + defender = currentDefender; + } + + @Override + public T visit(IUiEventVisitor visitor) { + return visitor.visit(this); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return attacker.toString() + ( defender == null ? " removed from combat" : " declared to attack " + defender.getName() ); + } +} diff --git a/forge-m-base/src/forge/screens/match/events/UiEventBlockerAssigned.java b/forge-m-base/src/forge/screens/match/events/UiEventBlockerAssigned.java new file mode 100644 index 00000000000..fdd68954614 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/events/UiEventBlockerAssigned.java @@ -0,0 +1,21 @@ +package forge.screens.match.events; + +import forge.game.card.Card; + +public class UiEventBlockerAssigned extends UiEvent { + + public final Card blocker; + public final Card attackerBeingBlocked; + + public UiEventBlockerAssigned(Card card, Card currentAttacker) { + blocker = card; + attackerBeingBlocked = currentAttacker; + } + + @Override + public T visit(IUiEventVisitor visitor) { + return visitor.visit(this); + } + + +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/input/ButtonUtil.java b/forge-m-base/src/forge/screens/match/input/ButtonUtil.java new file mode 100644 index 00000000000..d8a4164530a --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/ButtonUtil.java @@ -0,0 +1,65 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import forge.screens.match.FControl; +import forge.toolbox.FButton; + +/** + * Manages match UI OK/Cancel button enabling and focus + */ +public class ButtonUtil { + public static void setButtonText(String okLabel, String cancelLabel) { + getOk().setText(okLabel); + getCancel().setText(cancelLabel); + } + + public static void reset() { + disableAll(); + getOk().setText("OK"); + getCancel().setText("Cancel"); + } + + public static void enableAll() { + getOk().setEnabled(true); + getCancel().setEnabled(true); + } + + public static void disableAll() { + getOk().setEnabled(false); + getCancel().setEnabled(false); + } + + public static void enableOnlyOk() { + getOk().setEnabled(true); + getCancel().setEnabled(false); + } + + public static void enableOnlyCancel() { + getOk().setEnabled(false); + getCancel().setEnabled(true); + } + + private static FButton getOk() { + return FControl.getView().getPrompt().getBtnOk(); + } + + private static FButton getCancel() { + return FControl.getView().getPrompt().getBtnCancel(); + } +} diff --git a/forge-m-base/src/forge/screens/match/input/Input.java b/forge-m-base/src/forge/screens/match/input/Input.java new file mode 100644 index 00000000000..94526ab76af --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/Input.java @@ -0,0 +1,22 @@ +package forge.screens.match.input; + +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; + +public interface Input { + + // showMessage() is always the first method called + void showMessageInitial(); + + void selectCard(Card c); + + void selectAbility(SpellAbility ab); + + void selectPlayer(Player player); + + void selectButtonOK(); + + void selectButtonCancel(); + +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/input/InputAttack.java b/forge-m-base/src/forge/screens/match/input/InputAttack.java new file mode 100644 index 00000000000..a21ab93e213 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputAttack.java @@ -0,0 +1,242 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import com.google.common.collect.Iterables; + +import forge.game.GameEntity; +import forge.game.ability.AbilityUtils; +import forge.game.card.Card; +import forge.game.card.CardPredicates; +import forge.game.combat.AttackingBand; +import forge.game.combat.Combat; +import forge.game.combat.CombatUtil; +import forge.game.player.Player; +import forge.game.zone.ZoneType; +import forge.screens.match.FControl; +import forge.screens.match.events.UiEventAttackerDeclared; + +import java.util.List; + +/** + *

+ * InputAttack class. + *

+ * + * @author Forge + * @version $Id: InputAttack.java 24769 2014-02-09 13:56:04Z Hellfish $ + */ +public class InputAttack extends InputSyncronizedBase { + /** Constant serialVersionUID=7849903731842214245L. */ + private static final long serialVersionUID = 7849903731842214245L; + + private final Combat combat; + private final List defenders; + private GameEntity currentDefender; + private final Player playerAttacks; + private final Player playerDeclares; + private AttackingBand activeBand = null; + + public InputAttack(Player attacks, Player declares, Combat combat) { + this.playerAttacks = attacks; + this.playerDeclares = declares; + this.combat = combat; + this.defenders = combat.getDefenders(); + } + + /** {@inheritDoc} */ + @Override + public final void showMessage() { + // TODO still seems to have some issues with multiple planeswalkers + + ButtonUtil.enableOnlyOk(); + + setCurrentDefender(defenders.isEmpty() ? null : defenders.get(0)); + + if ( null == currentDefender ) { + System.err.println("InputAttack has no potential defenders!"); + return; // should even throw here! + } + + List possibleAttackers = playerAttacks.getCardsIn(ZoneType.Battlefield); + for (Card c : Iterables.filter(possibleAttackers, CardPredicates.Presets.CREATURES)) { + if (c.hasKeyword("CARDNAME attacks each turn if able.")) { + for(GameEntity def : defenders ) { + if( CombatUtil.canAttack(c, def, combat) ) { + combat.addAttacker(c, currentDefender); + FControl.fireEvent(new UiEventAttackerDeclared(c, currentDefender)); + break; + } + } + } else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) { + final int i = c.getKeywordPosition("CARDNAME attacks specific player each combat if able"); + final String defined = c.getKeyword().get(i).split(":")[1]; + final Player player = AbilityUtils.getDefinedPlayers(c, defined, null).get(0); + if (player != null && CombatUtil.canAttack(c, player, combat)) { + combat.addAttacker(c, player); + FControl.fireEvent(new UiEventAttackerDeclared(c, player)); + } + } + } + } + + private void showCombat() { + // redraw sword icons + FControl.showCombat(combat); + } + + /** {@inheritDoc} */ + @Override + protected final void onOk() { + // TODO Add check to see if each must attack creature is attacking + // Propaganda costs could have been paid here. + setCurrentDefender(null); // remove highlights + activateBand(null); + stop(); + } + + @Override + protected final void onPlayerSelected(Player selected) { + if (defenders.contains(selected)) { + setCurrentDefender(selected); + } + else { + flashIncorrectAction(); // cannot attack that player + } + } + + /** {@inheritDoc} */ + @Override + protected final void onCardSelected(final Card card) { + final List att = combat.getAttackers(); + if (/*triggerEvent.getButton() == 3 && */att.contains(card) && !card.hasKeyword("CARDNAME attacks each turn if able.") + && !card.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) { + // TODO Is there no way to attacks each turn cards to attack Planeswalkers? + combat.removeFromCombat(card); + FControl.setUsedToPay(card, false); + showCombat(); + // When removing an attacker clear the attacking band + this.activateBand(null); + + FControl.fireEvent(new UiEventAttackerDeclared(card, null)); + return; + } + + if (combat.isAttacking(card, currentDefender)) { + // Activate band by selecting/deselecting a band member + if (this.activeBand == null) { + this.activateBand(combat.getBandOfAttacker(card)); + } else if (this.activeBand.getAttackers().contains(card)) { + this.activateBand(null); + } else { // Join a band by selecting a non-active band member after activating a band + if (this.activeBand.canJoinBand(card)) { + combat.removeFromCombat(card); + declareAttacker(card); + } else { + flashIncorrectAction(); + } + } + + updateMessage(); + return; + } + + if ( card.getController().isOpponentOf(playerAttacks) ) { + if ( defenders.contains(card) ) { // planeswalker? + setCurrentDefender(card); + return; + } + } + + if (playerAttacks.getZone(ZoneType.Battlefield).contains(card) && CombatUtil.canAttack(card, currentDefender, combat)) { + if (this.activeBand != null && !this.activeBand.canJoinBand(card)) { + this.activateBand(null); + updateMessage(); + flashIncorrectAction(); + return; + } + + if(combat.isAttacking(card)) { + combat.removeFromCombat(card); + } + + declareAttacker(card); + showCombat(); + } + else { + flashIncorrectAction(); + } + } // selectCard() + + + /** + * TODO: Write javadoc for this method. + * @param card + */ + private void declareAttacker(final Card card) { + combat.addAttacker(card, currentDefender, this.activeBand); + this.activateBand(this.activeBand); + updateMessage(); + + FControl.fireEvent(new UiEventAttackerDeclared(card, currentDefender)); + } + + private final void setCurrentDefender(GameEntity def) { + currentDefender = def; + for( GameEntity ge: defenders ) { + if ( ge instanceof Card) { + FControl.setUsedToPay((Card)ge, ge == def); + } + else if (ge instanceof Player) { + FControl.setHighlighted((Player) ge, ge == def); + } + } + + updateMessage(); + + // update UI + } + + private final void activateBand(AttackingBand band) { + if (this.activeBand != null) { + for(Card card : this.activeBand.getAttackers()) { + FControl.setUsedToPay(card, false); + } + } + this.activeBand = band; + + if (this.activeBand != null) { + for(Card card : this.activeBand.getAttackers()) { + FControl.setUsedToPay(card, true); + } + } + + // update UI + } + + private void updateMessage() { + StringBuilder sb = new StringBuilder(); + sb.append(playerDeclares.getName()).append(", "); + sb.append(playerAttacks == playerDeclares ? "declare attackers." : "declare attackers for " + playerAttacks.getName()).append("\n"); + sb.append("Selecting Creatures to Attack ").append(currentDefender).append("\n\n"); + sb.append("To change the current defender, click on the player or planeswalker you wish to attack.\n"); + sb.append("To attack as a band, click an attacking creature to activate its 'band', select another to join the band."); + + showMessage(sb.toString()); + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputBase.java b/forge-m-base/src/forge/screens/match/input/InputBase.java new file mode 100644 index 00000000000..d7c3fa2a1e6 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputBase.java @@ -0,0 +1,108 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import forge.game.Game; +import forge.game.card.Card; +import forge.game.phase.PhaseHandler; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.screens.match.FControl; + +/** + *

+ * Abstract Input class. + *

+ * + * @author Forge + * @version $Id: InputBase.java 24769 2014-02-09 13:56:04Z Hellfish $ + */ +public abstract class InputBase implements java.io.Serializable, Input { + /** Constant serialVersionUID=-6539552513871194081L. */ + private static final long serialVersionUID = -6539552513871194081L; + private boolean finished = false; + protected final boolean isFinished() { return finished; } + protected final void setFinished() { finished = true; } + + // showMessage() is always the first method called + @Override + public final void showMessageInitial() { + finished = false; + showMessage(); + } + + protected abstract void showMessage(); + + @Override + public final void selectPlayer(final Player player) { + if (isFinished()) { return; } + onPlayerSelected(player); + } + + @Override + public void selectAbility(SpellAbility ab) { } + + @Override + public final void selectButtonCancel() { + if (isFinished()) { return; } + onCancel(); + } + + @Override + public final void selectButtonOK() { + if (isFinished()) { return; } + onOk(); + } + + @Override + public final void selectCard(final Card c) { + if (isFinished()) { return; } + onCardSelected(c); + } + + protected void onCardSelected(final Card c) {} + protected void onPlayerSelected(final Player p) {} + protected void onCancel() {} + protected void onOk() {} + + // to remove need for CMatchUI dependence + protected final void showMessage(String message) { + FControl.showMessage(message); + } + + protected final void flashIncorrectAction() { + FControl.getView().getPrompt().remind(); + } + + protected String getTurnPhasePriorityMessage(Game game) { + final PhaseHandler ph = game.getPhaseHandler(); + final StringBuilder sb = new StringBuilder(); + + sb.append("Priority: ").append(ph.getPriorityPlayer()).append("\n"); + sb.append("Turn ").append(ph.getTurn()).append(" (").append(ph.getPlayerTurn()).append(")\n"); + sb.append("Phase: ").append(ph.getPhase().nameForUi).append("\n"); + sb.append("Stack: "); + if (!game.getStack().isEmpty()) { + sb.append(game.getStack().size()).append(" to Resolve."); + } else { + sb.append("Empty"); + } + sb.append("\n"); + return sb.toString(); + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputBlock.java b/forge-m-base/src/forge/screens/match/input/InputBlock.java new file mode 100644 index 00000000000..ff6e731e2ec --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputBlock.java @@ -0,0 +1,136 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import forge.game.card.Card; +import forge.game.combat.Combat; +import forge.game.combat.CombatUtil; +import forge.game.player.Player; +import forge.game.zone.ZoneType; +import forge.screens.match.FControl; +import forge.screens.match.events.UiEventBlockerAssigned; +import forge.toolbox.FOptionPane; + +/** + *

+ * Input_Block class. + *

+ * + * @author Forge + * @version $Id: InputBlock.java 24769 2014-02-09 13:56:04Z Hellfish $ + */ +public class InputBlock extends InputSyncronizedBase { + /** Constant serialVersionUID=6120743598368928128L. */ + private static final long serialVersionUID = 6120743598368928128L; + + private Card currentAttacker = null; + // some cards may block several creatures at a time. (ex: Two-Headed Dragon, Vanguard's Shield) + private final Combat combat; + private final Player defender; + private final Player declarer; + + /** + * TODO: Write javadoc for Constructor. + * @param priority + */ + public InputBlock(Player whoDeclares, Player whoDefends, Combat combat) { + defender = whoDefends; + declarer = whoDeclares; + this.combat = combat; + } + + /** {@inheritDoc} */ + @Override + protected final void showMessage() { + // could add "Reset Blockers" button + ButtonUtil.enableOnlyOk(); + + String prompt = declarer == defender ? "declare blockers." : "declare blockers for " + defender.getName(); + + final StringBuilder sb = new StringBuilder(declarer.getName()); + sb.append(", ").append(prompt).append("\n\n"); + + if (this.currentAttacker == null) { + sb.append("To Block, click on your opponent's attacker first, then your blocker(s).\n"); + sb.append("To cancel a block right-click on your blocker"); + } + else { + final String attackerName = this.currentAttacker.isFaceDown() ? "Morph" : this.currentAttacker.getName(); + sb.append("Select a creature to block ").append(attackerName).append(" ("); + sb.append(this.currentAttacker.getUniqueNumber()).append("). "); + sb.append("To cancel a block right-click on your blocker"); + } + + showMessage(sb.toString()); + FControl.showCombat(combat); + } + + /** {@inheritDoc} */ + @Override + public final void onOk() { + String blockErrors = CombatUtil.validateBlocks(combat, defender); + if( null == blockErrors ) { + // Done blocking + ButtonUtil.reset(); + setCurrentAttacker(null); + stop(); + } + else { + FOptionPane.showMessageDialog(blockErrors); + } + } + + /** {@inheritDoc} */ + @Override + public final void onCardSelected(final Card card) { + if (/*triggerEvent.getButton() == 3 &&*/ card.getController() == defender) { + combat.removeFromCombat(card); + FControl.fireEvent(new UiEventBlockerAssigned(card, (Card)null)); + } else { + // is attacking? + boolean isCorrectAction = false; + + if (combat.isAttacking(card)) { + setCurrentAttacker(card); + isCorrectAction = true; + } else { + // Make sure this card is valid to even be a blocker + if (this.currentAttacker != null && card.isCreature() && defender.getZone(ZoneType.Battlefield).contains(card)) { + isCorrectAction = CombatUtil.canBlock(this.currentAttacker, card, combat); + if ( isCorrectAction ) { + combat.addBlocker(this.currentAttacker, card); + FControl.fireEvent(new UiEventBlockerAssigned(card, currentAttacker)); + } + } + } + + if (!isCorrectAction) { + flashIncorrectAction(); + } + } + this.showMessage(); + } // selectCard() + + + private void setCurrentAttacker(Card card) { + currentAttacker = card; + for(Card c : combat.getAttackers()) { + FControl.setUsedToPay(c, card == c); + } + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputConfirm.java b/forge-m-base/src/forge/screens/match/input/InputConfirm.java new file mode 100644 index 00000000000..b08f8c60403 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputConfirm.java @@ -0,0 +1,81 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + + /** + *

+ * InputConfirm class. + *

+ * + * @author Forge + * @version $Id: InputConfirm.java 21647 2013-05-24 22:31:11Z Max mtg $ + */ +public class InputConfirm extends InputSyncronizedBase { + private static final long serialVersionUID = -3591794991788531626L; + + private final String message; + private final String yesButtonText; + private final String noButtonText; + private boolean result; + + public InputConfirm(String message0) { + this(message0, "Yes", "No", true); + } + + public InputConfirm(String message0, String yesButtonText0, String noButtonText0) { + this(message0, yesButtonText0, noButtonText0, true); + } + + public InputConfirm(String message0, String yesButtonText0, String noButtonText0, boolean defaultYes0) { + this.message = message0; + this.yesButtonText = yesButtonText0; + this.noButtonText = noButtonText0; + result = defaultYes0; + } + + /** {@inheritDoc} */ + @Override + protected final void showMessage() { + ButtonUtil.setButtonText(this.yesButtonText, this.noButtonText); + ButtonUtil.enableAll(); + showMessage(this.message); + } + + /** {@inheritDoc} */ + @Override + protected final void onOk() { + this.result = true; + done(); + } + + /** {@inheritDoc} */ + @Override + protected final void onCancel() { + this.result = false; + done(); + } + + private void done() { + ButtonUtil.reset(); + stop(); + } + + public final boolean getResult() { + return this.result; + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputConfirmMulligan.java b/forge-m-base/src/forge/screens/match/input/InputConfirmMulligan.java new file mode 100644 index 00000000000..46d009f7ce4 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputConfirmMulligan.java @@ -0,0 +1,162 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import forge.game.Game; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.zone.ZoneType; +import forge.screens.match.FControl; +import forge.toolbox.GuiDialog; +import forge.util.Lang; +import forge.util.ThreadUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * InputConfirmMulligan class. + *

+ * + * @author Forge + * @version $Id: InputConfirmMulligan.java 24769 2014-02-09 13:56:04Z Hellfish $ + */ +public class InputConfirmMulligan extends InputSyncronizedBase { + /** Constant serialVersionUID=-8112954303001155622L. */ + private static final long serialVersionUID = -8112954303001155622L; + + boolean keepHand = false; + final boolean isCommander; + + private final List selected = new ArrayList(); + private final Player player; + private final Player startingPlayer; + + public InputConfirmMulligan(Player humanPlayer, Player startsGame, boolean commander) { + player = humanPlayer; + isCommander = commander; + startingPlayer = startsGame; + } + + /** {@inheritDoc} */ + @Override + public final void showMessage() { + Game game = player.getGame(); + + StringBuilder sb = new StringBuilder(); + if (startingPlayer == player) { + sb.append(player).append(", you are going first!\n\n"); + } + else { + sb.append(startingPlayer.getName()).append(" is going first.\n"); + sb.append(player).append(", you are going ").append(Lang.getOrdinal(game.getPosition(player, startingPlayer))).append(".\n\n"); + } + + if (isCommander) { + ButtonUtil.setButtonText("Keep", "Exile"); + ButtonUtil.enableOnlyOk(); + sb.append("Will you keep your hand or choose some cards to exile those and draw one less card?"); + } + else { + ButtonUtil.setButtonText("Keep", "Mulligan"); + ButtonUtil.enableAll(); + sb.append("Do you want to keep your hand?"); + } + + showMessage(sb.toString()); + } + + /** {@inheritDoc} */ + @Override + protected final void onOk() { + keepHand = true; + done(); + } + + /** {@inheritDoc} */ + @Override + protected final void onCancel() { + keepHand = false; + done(); + } + + private void done() { + ButtonUtil.reset(); + if (isCommander) { + // Clear the "selected" icon after clicking the done button + for (Card c : this.selected) { + FControl.setUsedToPay(c, false); + } + } + stop(); + } + + volatile boolean cardSelectLocked = false; + + @Override + protected void onCardSelected(final Card c0) { // the only place that would cause troubles - input is supposed only to confirm, not to fire abilities + boolean fromHand = player.getZone(ZoneType.Hand).contains(c0); + boolean isSerumPowder = c0.getName().equals("Serum Powder"); + boolean isLegalChoice = fromHand && (isCommander || isSerumPowder); + if (!isLegalChoice || cardSelectLocked) { + flashIncorrectAction(); + return; + } + + if (isSerumPowder && GuiDialog.confirm(c0, "Use " + c0.getName() + "'s ability?")) { + cardSelectLocked = true; + ThreadUtil.invokeInGameThread(new Runnable() { + public void run() { + List hand = new ArrayList(c0.getController().getCardsIn(ZoneType.Hand)); + for (Card c : hand) { + player.getGame().getAction().exile(c); + } + c0.getController().drawCards(hand.size()); + cardSelectLocked = false; + } + }); + return; + } + + if (isCommander) { // allow to choose cards for partial paris + if (selected.contains(c0)) { + FControl.setUsedToPay(c0, false); + selected.remove(c0); + } + else { + FControl.setUsedToPay(c0, true); + selected.add(c0); + } + if (selected.isEmpty()) { + ButtonUtil.enableOnlyOk(); + } + else { + ButtonUtil.enableAll(); + } + } + } + + public final boolean isKeepHand() { + return keepHand; + } + + public List getSelectedCards() { + return selected; + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputLockUI.java b/forge-m-base/src/forge/screens/match/input/InputLockUI.java new file mode 100644 index 00000000000..b0deca6dab4 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputLockUI.java @@ -0,0 +1,65 @@ +package forge.screens.match.input; + +import forge.FThreads; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.screens.match.FControl; +import forge.util.ThreadUtil; + +import java.util.concurrent.atomic.AtomicInteger; + +public class InputLockUI implements Input { + private final AtomicInteger iCall = new AtomicInteger(); + + public InputLockUI(InputQueue inputQueue) { + } + + public void showMessageInitial() { + int ixCall = 1 + iCall.getAndIncrement(); + ThreadUtil.delay(500, new InputUpdater(ixCall)); + } + + @Override + public String toString() { + return "lockUI"; + } + + private class InputUpdater implements Runnable { + final int ixCall; + + public InputUpdater(final int idxCall) { + ixCall = idxCall; + } + + @Override + public void run() { + if ( ixCall != iCall.get() || !isActive()) // cancel the message if it's not from latest call or input is gone already + return; + FThreads.invokeInEdtLater(showMessageFromEdt); + } + }; + + private final Runnable showMessageFromEdt = new Runnable() { + + @Override + public void run() { + ButtonUtil.disableAll(); + showMessage("Waiting for actions..."); + } + }; + + protected final boolean isActive() { + return FControl.getInputQueue().getInput() == this; + } + + protected void showMessage(String message) { + FControl.showMessage(message); + } + + @Override public void selectCard(Card c) {} + @Override public void selectAbility(SpellAbility ab) {} + @Override public void selectPlayer(Player player) {} + @Override public void selectButtonOK() {} + @Override public void selectButtonCancel() {} +} diff --git a/forge-m-base/src/forge/screens/match/input/InputPassPriority.java b/forge-m-base/src/forge/screens/match/input/InputPassPriority.java new file mode 100644 index 00000000000..92158940ccd --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputPassPriority.java @@ -0,0 +1,79 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import java.util.List; + +/** + *

+ * Input_PassPriority class. + *

+ * + * @author Forge + * @version $Id: InputPassPriority.java 24769 2014-02-09 13:56:04Z Hellfish $ + */ +public class InputPassPriority extends InputSyncronizedBase { + /** Constant serialVersionUID=-581477682214137181L. */ + private static final long serialVersionUID = -581477682214137181L; + private final Player player; + + private SpellAbility chosenSa; + + public InputPassPriority(Player human) { + player = human; + } + + /** {@inheritDoc} */ + @Override + public final void showMessage() { + showMessage(getTurnPhasePriorityMessage(player.getGame())); + chosenSa = null; + ButtonUtil.enableOnlyOk(); + } + + /** {@inheritDoc} */ + @Override + protected final void onOk() { + stop(); + } + + public SpellAbility getChosenSa() { return chosenSa; } + + + @Override + protected void onCardSelected(final Card card) { + List abilities = card.getAllPossibleAbilities(player, false); + if (abilities.isEmpty()) { + flashIncorrectAction(); + return; + } + + selectAbility(player.getController().getAbilityToPlay(abilities)); + } + + @Override + public void selectAbility(final SpellAbility ab) { + if (ab != null) { + chosenSa = ab; + stop(); + } + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputPayMana.java b/forge-m-base/src/forge/screens/match/input/InputPayMana.java new file mode 100644 index 00000000000..d39d2875c48 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputPayMana.java @@ -0,0 +1,375 @@ +package forge.screens.match.input; + +import forge.FThreads; +import forge.ai.ComputerUtilMana; +import forge.ai.PlayerControllerAi; +import forge.card.ColorSet; +import forge.card.MagicColor; +import forge.card.mana.ManaAtom; +import forge.game.Game; +import forge.game.ability.ApiType; +import forge.game.card.Card; +import forge.game.card.CardUtil; +import forge.game.mana.ManaCostBeingPaid; +import forge.game.player.Player; +import forge.game.replacement.ReplacementEffect; +import forge.game.spellability.AbilityManaPart; +import forge.game.spellability.SpellAbility; +import forge.player.HumanPlay; +import forge.toolbox.GuiChoose; +import forge.utils.Evaluator; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public abstract class InputPayMana extends InputSyncronizedBase { + private static final long serialVersionUID = -9133423708688480255L; + + protected int phyLifeToLose = 0; + + protected final Player player; + protected final Game game; + protected ManaCostBeingPaid manaCost; + protected final SpellAbility saPaidFor; + + private boolean bPaid = false; + private Boolean canPayManaCost = null; + + private boolean locked = false; + + protected InputPayMana(SpellAbility saToPayFor, Player payer) { + this.player = payer; + this.game = player.getGame(); + this.saPaidFor = saToPayFor; + } + + @Override + protected void onCardSelected(final Card card) { + if (card.getManaAbility().isEmpty()) { + flashIncorrectAction(); + return; + } + // only tap card if the mana is needed + activateManaAbility(card, this.manaCost); + } + + public void useManaFromPool(byte colorCode) { + // find the matching mana in pool. + player.getManaPool().tryPayCostWithColor(colorCode, saPaidFor, manaCost); + onManaAbilityPaid(); + showMessage(); + } + + /** + *

+ * activateManaAbility. + *

+ * + * @param sa + * a {@link forge.game.spellability.SpellAbility} object. + * @param card + * a {@link forge.game.card.Card} object. + * @param manaCost + * a {@link forge.game.mana.ManaCostBeingPaid} object. + * @return a {@link forge.game.mana.ManaCostBeingPaid} object. + */ + protected void activateManaAbility(final Card card, ManaCostBeingPaid manaCost) { + if ( locked ) { + System.err.print("Should wait till previous call to playAbility finishes."); + return; + } + + // make sure computer's lands aren't selected + if (card.getController() != player) { + return; + } + + byte colorCanUse = 0; + byte colorNeeded = 0; + + for (final byte color : MagicColor.WUBRG) { + if (manaCost.isAnyPartPayableWith(color, player.getManaPool())) { colorCanUse |= color; } + if (manaCost.needsColor(color, player.getManaPool())) { colorNeeded |= color; } + } + if (manaCost.isAnyPartPayableWith((byte) ManaAtom.COLORLESS, player.getManaPool())) + colorCanUse |= ManaAtom.COLORLESS; + + if ( 0 == colorCanUse ) // no mana cost or something + return; + + List abilities = new ArrayList(); + // you can't remove unneeded abilities inside a for (am:abilities) loop :( + + final String typeRes = manaCost.getSourceRestriction(); + if (StringUtils.isNotBlank(typeRes) && !card.isType(typeRes)) { + return; + } + + boolean guessAbilityWithRequiredColors = true; + for (SpellAbility ma : card.getManaAbility()) { + ma.setActivatingPlayer(player); + + AbilityManaPart m = ma.getManaPartRecursive(); + if (m == null || !ma.canPlay()) { continue; } + if (!abilityProducesManaColor(ma, m, colorCanUse)) { continue; } + if (ma.isAbility() && ma.getRestrictions().isInstantSpeed()) { continue; } + if (!m.meetsManaRestrictions(saPaidFor)) { continue; } + + abilities.add(ma); + + // skip express mana if the ability is not undoable or reusable + if (!ma.isUndoable() || !ma.getPayCosts().isRenewableResource() || ma.getSubAbility() != null) { + guessAbilityWithRequiredColors = false; + } + } + + if (abilities.isEmpty()) { + return; + } + + // Store some information about color costs to help with any mana choices + if (colorNeeded == 0) { // only colorless left + if (saPaidFor.getHostCard() != null && saPaidFor.getHostCard().hasSVar("ManaNeededToAvoidNegativeEffect")) { + String[] negEffects = saPaidFor.getHostCard().getSVar("ManaNeededToAvoidNegativeEffect").split(","); + for (String negColor : negEffects) { + byte col = MagicColor.fromName(negColor); + colorCanUse |= col; + } + } + } + + // If the card has any ability that tracks mana spent, skip express Mana choice + if (saPaidFor.tracksManaSpent()) { + colorCanUse = MagicColor.ALL_COLORS; + guessAbilityWithRequiredColors = false; + } + + boolean choice = true; + if (guessAbilityWithRequiredColors) { + // express Mana Choice + if (colorNeeded == 0) { + choice = false; + //avoid unnecessary prompt by pretending we need White + //for the sake of "Add one mana of any color" effects + colorNeeded = MagicColor.WHITE; + } + else { + final ArrayList colorMatches = new ArrayList(); + for (SpellAbility sa : abilities) { + if (abilityProducesManaColor(sa, sa.getManaPartRecursive(), colorNeeded)) { + colorMatches.add(sa); + } + } + + if (colorMatches.isEmpty()) { + // can only match colorless just grab the first and move on. + // This is wrong. Sometimes all abilities aren't created equal + choice = false; + } + else if (colorMatches.size() < abilities.size()) { + // leave behind only color matches + abilities = colorMatches; + } + } + } + + final SpellAbility chosen = abilities.size() > 1 && choice ? GuiChoose.one("Choose mana ability", abilities) : abilities.get(0); + ColorSet colors = ColorSet.fromMask(0 == colorNeeded ? colorCanUse : colorNeeded); + chosen.getManaPartRecursive().setExpressChoice(colors); + + // System.out.println("Chosen sa=" + chosen + " of " + chosen.getHostCard() + " to pay mana"); + Runnable proc = new Runnable() { + @Override + public void run() { + HumanPlay.playSpellAbility(chosen.getActivatingPlayer(), chosen); + player.getManaPool().payManaFromAbility(saPaidFor, InputPayMana.this.manaCost, chosen); + + onManaAbilityPaid(); + onStateChanged(); + } + }; + locked = true; + game.getAction().invoke(proc); + } + + /** + *

+ * canMake. color is like "G", returns "Green". + *

+ * + * @param am + * a {@link forge.card.spellability.AbilityMana} object. + * @param mana + * a {@link java.lang.String} object. + * @return a boolean. + */ + private static boolean abilityProducesManaColor(final SpellAbility am, AbilityManaPart m, final byte neededColor) { + if (0 != (neededColor & MagicColor.COLORLESS)) { + return true; + } + + if (m.isAnyMana()) { + return true; + } + + // check for produce mana replacement effects - they mess this up, so just use the mana ability + final Card source = am.getHostCard(); + final Player activator = am.getActivatingPlayer(); + final Game g = source.getGame(); + final HashMap repParams = new HashMap(); + repParams.put("Event", "ProduceMana"); + repParams.put("Mana", m.getOrigProduced()); + repParams.put("Affected", source); + repParams.put("Player", activator); + repParams.put("AbilityMana", am); + + for (final Player p : g.getPlayers()) { + for (final Card crd : p.getAllCards()) { + for (final ReplacementEffect replacementEffect : crd.getReplacementEffects()) { + if (replacementEffect.requirementsCheck(g) + && replacementEffect.canReplace(repParams) + && replacementEffect.getMapParams().containsKey("ManaReplacement") + && replacementEffect.zonesCheck(g.getZoneOf(crd))) { + return true; + } + } + } + } + + if (am.getApi() == ApiType.ManaReflected) { + final Iterable reflectableColors = CardUtil.getReflectableManaColors(am); + for (final String color : reflectableColors) { + if (0 != (neededColor & MagicColor.fromName(color))) { + return true; + } + } + } + else { + String colorsProduced = m.isComboMana() ? m.getComboColors() : m.getOrigProduced(); + for (final String color : colorsProduced.split(" ")) { + if (0 != (neededColor & MagicColor.fromName(color))) { + return true; + } + if( (neededColor & ManaAtom.COLORLESS) != 0) + return true; + } + } + return false; + } + + protected boolean isAlreadyPaid() { + if (manaCost.isPaid()) { + bPaid = true; + } + return bPaid; + } + + protected boolean supportAutoPay() { + return true; + } + + private void runAsAi(Runnable proc) { + this.player.runWithController(proc, new PlayerControllerAi(this.game, this.player, this.player.getOriginalLobbyPlayer())); + } + + /** {@inheritDoc} */ + @Override + protected void onOk() { + if (supportAutoPay()) { + //use AI utility to automatically pay mana cost if possible + final Runnable proc = new Runnable() { + @Override + public void run() { + ComputerUtilMana.payManaCost(manaCost, saPaidFor, player); + } + }; + //must run in game thread as certain payment actions can only be automated there + game.getAction().invoke(new Runnable() { + @Override + public void run() { + runAsAi(proc); + onStateChanged(); + } + }); + } + } + + protected void updateButtons() { + if (supportAutoPay()) { + ButtonUtil.setButtonText("Auto", "Cancel"); + } + ButtonUtil.enableOnlyCancel(); + } + + protected final void updateMessage() { + locked = false; + if (supportAutoPay()) { + if (canPayManaCost == null) { + //use AI utility to determine if mana cost can be paid if that hasn't been determined yet + Evaluator proc = new Evaluator() { + @Override + public Boolean evaluate() { + return ComputerUtilMana.canPayManaCost(manaCost, saPaidFor, player); + } + }; + runAsAi(proc); + canPayManaCost = proc.getResult(); + } + if (canPayManaCost) { + ButtonUtil.enableAll(); //enabled Auto button if mana cost can be paid + } + } + showMessage(getMessage()); + } + + /** {@inheritDoc} */ + @Override + protected final void onStop() { + if (supportAutoPay()) { + ButtonUtil.reset(); + } + } + + /** {@inheritDoc} */ + @Override + public void showMessage() { + if (isFinished()) { return; } + updateButtons(); + onStateChanged(); + } + + protected void onStateChanged() { + if (isAlreadyPaid()) { + done(); + stop(); + } + else { + FThreads.invokeInEdtNowOrLater(new Runnable() { + @Override + public void run() { + updateMessage(); + } + }); + } + } + + protected void onManaAbilityPaid() {} // some inputs overload it + protected abstract void done(); + protected abstract String getMessage(); + + @Override + public String toString() { + return String.format("PayManaBase %s left", manaCost.toString()); + } + + public boolean isPaid() { return bPaid; } + + protected String messagePrefix; + public void setMessagePrefix(String prompt) { + // TODO Auto-generated method stub + messagePrefix = prompt; + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputPayManaOfCostPayment.java b/forge-m-base/src/forge/screens/match/input/InputPayManaOfCostPayment.java new file mode 100644 index 00000000000..afc9e8059ee --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputPayManaOfCostPayment.java @@ -0,0 +1,61 @@ +package forge.screens.match.input; + +import forge.game.card.Card; +import forge.game.mana.ManaCostBeingPaid; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; + +public class InputPayManaOfCostPayment extends InputPayMana { + public InputPayManaOfCostPayment(ManaCostBeingPaid cost, SpellAbility spellAbility, Player payer) { + super(spellAbility, payer); + manaCost = cost; + } + + private static final long serialVersionUID = 3467312982164195091L; + private int phyLifeToLose = 0; + + @Override + protected final void onPlayerSelected(Player selected) { + if (player == selected) { + if (player.canPayLife(this.phyLifeToLose + 2) && manaCost.payPhyrexian()) { + this.phyLifeToLose += 2; + } + + this.showMessage(); + } + } + + @Override + protected void done() { + final Card source = saPaidFor.getHostCard(); + if (this.phyLifeToLose > 0) { + player.payLife(this.phyLifeToLose, source); + } + } + + @Override + protected void onCancel() { + stop(); + } + + @Override + protected String getMessage() { + final String displayMana = manaCost.toString(false); + + final StringBuilder msg = new StringBuilder(); + if( messagePrefix != null ) + msg.append(messagePrefix).append("\n"); + msg.append("Pay Mana Cost: ").append(displayMana); + if (this.phyLifeToLose > 0) { + msg.append(" ("); + msg.append(this.phyLifeToLose); + msg.append(" life paid for phyrexian mana)"); + } + + if (manaCost.containsPhyrexianMana()) { + msg.append("\n(Click on your life total to pay life for phyrexian mana.)"); + } + + return msg.toString(); + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputPayManaSimple.java b/forge-m-base/src/forge/screens/match/input/InputPayManaSimple.java new file mode 100644 index 00000000000..06f26946702 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputPayManaSimple.java @@ -0,0 +1,133 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import forge.card.mana.ManaCost; +import forge.game.Game; +import forge.game.card.Card; +import forge.game.mana.ManaCostBeingPaid; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; + +//pays the cost of a card played from the player's hand +//the card is removed from the players hand if the cost is paid +//CANNOT be used for ABILITIES +public class InputPayManaSimple extends InputPayMana { + // anything that uses this should be converted to Ability_Cost + /** Constant serialVersionUID=3467312982164195091L. */ + private static final long serialVersionUID = 3467312982164195091L; + + private final Card originalCard; + private final ManaCost originalManaCost; + + public InputPayManaSimple(final Game game, final SpellAbility sa, final ManaCostBeingPaid manaCostToPay) { + super(sa, sa.getActivatingPlayer()); + this.originalManaCost = manaCostToPay.toManaCost(); + this.originalCard = sa.getHostCard(); + + if (sa.getHostCard().isCopiedSpell() && sa.isSpell()) { + this.manaCost = new ManaCostBeingPaid(ManaCost.ZERO); + game.getStack().add(this.saPaidFor); + } + else { + this.manaCost = manaCostToPay; + } + } + + protected void onManaAbilityPaid() { + if (this.manaCost.isPaid()) { + this.originalCard.setSunburstValue(this.manaCost.getSunburst()); + } + } + + /** {@inheritDoc} */ + @Override + protected final void onPlayerSelected(Player selected) { + if (player == selected) { + if (player.canPayLife(this.phyLifeToLose + 2) && manaCost.payPhyrexian()) { + this.phyLifeToLose += 2; + } + + this.showMessage(); + } + } + + /** + *

+ * done. + *

+ */ + @Override + protected void done() { + this.originalCard.setSunburstValue(this.manaCost.getSunburst()); + + if (this.phyLifeToLose > 0) { + player.payLife(this.phyLifeToLose, this.originalCard); + } + if (!this.saPaidFor.getHostCard().isCopiedSpell()) { + if (this.saPaidFor.isSpell()) { + this.saPaidFor.setHostCard(game.getAction().moveToStack(this.originalCard)); + } + } + } + + /** {@inheritDoc} */ + @Override + protected final void onCancel() { + player.getManaPool().refundManaPaid(this.saPaidFor); + // Update UI + + this.stop(); + } + + /** {@inheritDoc} */ + @Override + public final void showMessage() { + if (isFinished()) { return; } + + updateButtons(); + + if (this.manaCost.isPaid() && !new ManaCostBeingPaid(this.originalManaCost).isPaid()) { + this.done(); + this.stop(); + } + else { + updateMessage(); + } + } + + /* (non-Javadoc) + * @see forge.control.input.InputPayManaBase#updateMessage() + */ + @Override + protected String getMessage() { + final StringBuilder msg = new StringBuilder("Pay Mana Cost: " + this.manaCost.toString()); + if (this.phyLifeToLose > 0) { + msg.append(" ("); + msg.append(this.phyLifeToLose); + msg.append(" life paid for phyrexian mana)"); + } + + if (this.manaCost.containsPhyrexianMana()) { + msg.append("\n(Click on your life total to pay life for phyrexian mana.)"); + } + + // has its own variant of checkIfPaid + return msg.toString(); + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputPayManaX.java b/forge-m-base/src/forge/screens/match/input/InputPayManaX.java new file mode 100644 index 00000000000..b37658f88e4 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputPayManaX.java @@ -0,0 +1,129 @@ +package forge.screens.match.input; + +import forge.card.ColorSet; +import forge.card.mana.ManaCost; +import forge.card.mana.ManaCostParser; +import forge.game.card.Card; +import forge.game.mana.Mana; +import forge.game.mana.ManaCostBeingPaid; +import forge.game.spellability.SpellAbility; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +public class InputPayManaX extends InputPayMana { + private static final long serialVersionUID = -6900234444347364050L; + private int xPaid = 0; + private ArrayList xPaidByColor = new ArrayList<>(); + private byte colorsPaid; + private final ManaCost manaCostPerX; + private final boolean xCanBe0; + private boolean canceled = false; + + public InputPayManaX(final SpellAbility sa0, final int amountX, final boolean xCanBe0) { + super(sa0, sa0.getActivatingPlayer()); + xPaid = 0; + + if (saPaidFor.hasParam("XColor")) { + String xColor = saPaidFor.getParam("XColor"); + if (amountX == 1) { + manaCostPerX = new ManaCost(new ManaCostParser(xColor)); + } + else { + List list = new ArrayList(amountX); + for (int i = 0; i < amountX; i++) { + list.add(xColor); + } + manaCostPerX = new ManaCost(new ManaCostParser(StringUtils.join(list, ' '))); + } + } + else { + manaCostPerX = ManaCost.get(amountX); + } + manaCost = new ManaCostBeingPaid(manaCostPerX); + + this.xCanBe0 = xCanBe0; + colorsPaid = saPaidFor.getHostCard().getColorsPaid(); // for effects like sunburst + } + + /* (non-Javadoc) + * @see forge.control.input.InputPayManaBase#isPaid() + */ + @Override + public boolean isPaid() { + //return !( xPaid == 0 && !costMana.canXbe0() || this.colorX.equals("") && !this.manaCost.toString().equals(strX) ); + // return !( xPaid == 0 && !costMana.canXbe0()) && !(this.colorX.equals("") && !this.manaCost.toString().equals(strX)); + return !canceled && (xPaid > 0 || xCanBe0); + } + + @Override + protected boolean supportAutoPay() { + return false; + } + + @Override + public void showMessage() { + if (isFinished()) { return; } + + updateMessage(); + } + + @Override + protected String getMessage() { + StringBuilder msg = new StringBuilder("Pay X Mana Cost for "); + msg.append(saPaidFor.getHostCard().getName()).append("\n").append(this.xPaid); + msg.append(" Paid so far."); + if (!xCanBe0) { + msg.append(" X Can't be 0."); + } + // Enable just cancel is full X value hasn't been paid for multiple X values + // or X is 0, and x can't be 0 + if (!isPaid()) { + ButtonUtil.enableOnlyCancel(); + } + else { + ButtonUtil.enableAll(); + } + + return msg.toString(); + } + + @Override + protected void onCardSelected(final Card card) { + // don't allow here the cards that produce only wrong colors + activateManaAbility(card, this.manaCost); + } + + @Override + protected void onManaAbilityPaid() { + if (this.manaCost.isPaid()) { + this.colorsPaid |= manaCost.getColorsPaid(); + this.manaCost = new ManaCostBeingPaid(manaCostPerX); + this.xPaid++; + this.xPaidByColor.add(saPaidFor.getPayingMana().get(0)); + } + } + + @Override + protected final void onCancel() { + // If you hit cancel, isPaid needs to return false + this.canceled = true; + this.stop(); + } + + @Override + protected final void onOk() { + done(); + this.stop(); + } + + @Override + protected void done() { + final Card card = saPaidFor.getHostCard(); + card.setXManaCostPaid(this.xPaid); + card.setXManaCostPaidByColor(this.xPaidByColor); + card.setColorsPaid(this.colorsPaid); + card.setSunburstValue(ColorSet.fromMask(this.colorsPaid).countColors()); + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputPlaybackControl.java b/forge-m-base/src/forge/screens/match/input/InputPlaybackControl.java new file mode 100644 index 00000000000..8ddd1d652d7 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputPlaybackControl.java @@ -0,0 +1,72 @@ +package forge.screens.match.input; + +import forge.screens.match.FControl; +import forge.screens.match.FControlGamePlayback; + +/** + * TODO: Write javadoc for this type. + * + */ +public class InputPlaybackControl extends InputSyncronizedBase implements InputSynchronized { + private static final long serialVersionUID = 7979208993306642072L; + + FControlGamePlayback control; + + private boolean isPaused = false; + private boolean isFast = false; + + /** + * TODO: Write javadoc for Constructor. + * @param fControlGamePlayback + */ + public InputPlaybackControl(FControlGamePlayback fControlGamePlayback) { + control = fControlGamePlayback; + } + + /* (non-Javadoc) + * @see forge.gui.input.InputBase#showMessage() + */ + @Override + protected void showMessage() { + setPause(false); + ButtonUtil.enableAll(); + } + + private void setPause(boolean pause) { + isPaused = pause; + if ( isPaused ) + ButtonUtil.setButtonText("Resume", "Step"); + else { + ButtonUtil.setButtonText("Pause", isFast ? "1x Speed" : "10x Faster"); + showMessage("Press pause to pause game."); + } + } + + public void onGamePaused() { + showMessage(getTurnPhasePriorityMessage(FControl.getGame())); + } + + @Override + protected void onOk() { + if ( isPaused ) { + control.resume(); + setPause(false); + } else { + control.pause(); + setPause(true); + } + } + + @Override + protected void onCancel() { + if ( isPaused ) { + control.singleStep(); + } else { + isFast = !isFast; + control.setSpeed(isFast); + setPause(isPaused); // update message + } + + } + +} diff --git a/forge-m-base/src/forge/screens/match/input/InputProliferate.java b/forge-m-base/src/forge/screens/match/input/InputProliferate.java new file mode 100644 index 00000000000..4718fdbda01 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputProliferate.java @@ -0,0 +1,99 @@ +package forge.screens.match.input; + +import forge.game.GameEntity; +import forge.game.card.Card; +import forge.game.card.CounterType; +import forge.game.player.Player; +import forge.toolbox.GuiChoose; + +import java.util.*; +import java.util.Map.Entry; + +public final class InputProliferate extends InputSelectManyBase { + private static final long serialVersionUID = -1779224307654698954L; + private Map chosenCounters = new HashMap(); + + public InputProliferate() { + super(1, Integer.MAX_VALUE); + allowUnselect = true; + } + + + protected String getMessage() { + StringBuilder sb = new StringBuilder("Choose permanents and/or players with counters on them to add one more counter of that type."); + sb.append("\n\nYou've selected so far:\n"); + if (chosenCounters.isEmpty()) { + sb.append("(none)"); + } + else { + for (Entry ge : chosenCounters.entrySet()) { + if (ge.getKey() instanceof Player) { + sb.append("* A poison counter to player ").append(ge.getKey()).append("\n"); + } + else { + sb.append("* ").append(ge.getKey()).append(" -> ").append(ge.getValue()).append("counter\n"); + } + } + } + + return sb.toString(); + } + + @Override + protected void onCardSelected(final Card card) { + if (!card.hasCounters()) { + return; + } + + boolean entityWasSelected = chosenCounters.containsKey(card); + if (entityWasSelected) { + this.chosenCounters.remove(card); + } + else { + final List choices = new ArrayList(); + for (final CounterType ct : CounterType.values()) { + if (card.getCounters(ct) > 0) { + choices.add(ct); + } + } + + CounterType toAdd = choices.size() == 1 ? choices.get(0) : GuiChoose.one("Select counter type", choices); + chosenCounters.put(card, toAdd); + } + + refresh(); + } + + @Override + protected final void onPlayerSelected(Player player) { + if (player.getPoisonCounters() == 0 || player.hasKeyword("You can't get poison counters")) { + return; + } + + boolean entityWasSelected = chosenCounters.containsKey(player); + if (entityWasSelected) { + this.chosenCounters.remove(player); + } else + this.chosenCounters.put(player, null /* POISON counter is meant */); + + refresh(); + } + + public Map getProliferationMap() { + return chosenCounters; + } + + + @Override + protected boolean hasEnoughTargets() { return true; } + + @Override + protected boolean hasAllTargets() { return false; } + + + @Override + public Collection getSelected() { + // TODO Auto-generated method stub + return chosenCounters.keySet(); + } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/input/InputProxy.java b/forge-m-base/src/forge/screens/match/input/InputProxy.java new file mode 100644 index 00000000000..57391bbbf21 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputProxy.java @@ -0,0 +1,163 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import forge.FThreads; +import forge.game.Game; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.screens.match.FControl; +import forge.toolbox.FOptionPane; + +import java.util.Observable; +import java.util.Observer; +import java.util.concurrent.atomic.AtomicReference; + +/** + *

+ * GuiInput class. + *

+ * + * @author Forge + * @version $Id: InputProxy.java 24769 2014-02-09 13:56:04Z Hellfish $ + */ +public class InputProxy implements Observer { + + /** The input. */ + private AtomicReference input = new AtomicReference(); + private Game game = null; + +// private static final boolean DEBUG_INPUT = true; // false; + + public void setGame(Game game0) { + game = game0; + FControl.getInputQueue().addObserver(this); + } + + public boolean passPriority() { + Input inp = getInput(); + if (inp != null && inp instanceof InputPassPriority) { + inp.selectButtonOK(); + return true; + } + + FThreads.invokeInEdtNowOrLater(new Runnable() { + @Override + public void run() { + FOptionPane.showMessageDialog("Cannot pass priority at this time."); + } + }); + return false; + } + + @Override + public final void update(final Observable observable, final Object obj) { + final Input nextInput = FControl.getInputQueue().getActualInput(game); + +/* if(DEBUG_INPUT) + System.out.printf("%s ... \t%s on %s, \tstack = %s%n", + FThreads.debugGetStackTraceItem(6, true), nextInput == null ? "null" : nextInput.getClass().getSimpleName(), + game.getPhaseHandler().debugPrintState(), Singletons.getControl().getInputQueue().printInputStack()); +*/ + this.input.set(nextInput); + Runnable showMessage = new Runnable() { + @Override public void run() { + Input current = getInput(); + FControl.getInputQueue().syncPoint(); + //System.out.printf("\t%s > showMessage @ %s/%s during %s%n", FThreads.debugGetCurrThreadId(), nextInput.getClass().getSimpleName(), current.getClass().getSimpleName(), game.getPhaseHandler().debugPrintState()); + current.showMessageInitial(); + } + }; + + FThreads.invokeInEdtLater(showMessage); + } + /** + *

+ * selectButtonOK. + *

+ */ + public final void selectButtonOK() { + Input inp = getInput(); + if (inp != null) { + inp.selectButtonOK(); + } + } + + /** + *

+ * selectButtonCancel. + *

+ */ + public final void selectButtonCancel() { + Input inp = getInput(); + if (inp != null) { + inp.selectButtonCancel(); + } + } + + /** + *

+ * selectPlayer. + *

+ * + * @param player + * a {@link forge.game.player.Player} object. + */ + public final void selectPlayer(final Player player) { + Input inp = getInput(); + if (inp != null) { + inp.selectPlayer(player); + } + } + + /** + *

+ * selectCard. + *

+ * + * @param card + * a {@link forge.game.card.Card} object. + * @param triggerEvent + */ + public final void selectCard(final Card card) { + Input inp = getInput(); + if (inp != null) { + inp.selectCard(card); + } + } + + public final void selectAbility(SpellAbility ab) { + Input inp = getInput(); + if (inp != null) { + inp.selectAbility(ab); + } + } + + /** {@inheritDoc} */ + @Override + public final String toString() { + Input inp = getInput(); + return null == inp ? "(null)" : inp.toString(); + } + + /** @return {@link forge.gui.InputProxy.InputBase} */ + private Input getInput() { + return this.input.get(); + } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputQueue.java b/forge-m-base/src/forge/screens/match/input/InputQueue.java new file mode 100644 index 00000000000..6880d42aae9 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputQueue.java @@ -0,0 +1,102 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.screens.match.input; + +import forge.game.Game; +import java.util.Observable; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; + +/** + *

+ * InputControl class. + *

+ * + * @author Forge + * @version $Id: InputQueue.java 24769 2014-02-09 13:56:04Z Hellfish $ + */ +public class InputQueue extends Observable { + private final BlockingDeque inputStack = new LinkedBlockingDeque(); + private final InputLockUI inputLock; + + public InputQueue() { + inputLock = new InputLockUI(this); + } + + public final void updateObservers() { + this.setChanged(); + this.notifyObservers(); + } + + public final Input getInput() { + return inputStack.isEmpty() ? null : this.inputStack.peek(); + } + + public final void removeInput(Input inp) { + Input topMostInput = inputStack.isEmpty() ? null : inputStack.pop(); + + if (topMostInput != inp) { + throw new RuntimeException("Cannot remove input " + inp.getClass().getSimpleName() + " because it's not on top of stack. Stack = " + inputStack ); + } + updateObservers(); + } + + /** + *

+ * updateInput. + *

+ * + * @return a {@link forge.gui.input.InputBase} object. + */ + public final Input getActualInput(Game game) { + Input topMost = inputStack.peek(); // incoming input to Control + if (topMost != null && !game.isGameOver()) { + return topMost; + } + return inputLock; + } // getInput() + + // only for debug purposes + public String printInputStack() { + return inputStack.toString(); + } + + public void setInput(InputSynchronized input) { + this.inputStack.push(input); + syncPoint(); + this.updateObservers(); + } + + public void syncPoint() { + synchronized (inputLock) { + // acquire and release lock, so that actions from Game thread happen before EDT reads their results + } + } + + /** + * TODO: Write javadoc for this method. + */ + public void onGameOver(boolean releaseAllInputs) { + for (InputSynchronized inp : inputStack) { + inp.relaseLatchWhenGameIsOver(); + if (!releaseAllInputs) { + break; + } + } + } +} // InputControl diff --git a/forge-m-base/src/forge/screens/match/input/InputSelectCardsForConvoke.java b/forge-m-base/src/forge/screens/match/input/InputSelectCardsForConvoke.java new file mode 100644 index 00000000000..3717e4670d7 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputSelectCardsForConvoke.java @@ -0,0 +1,98 @@ +package forge.screens.match.input; + +import forge.card.mana.ManaCost; +import forge.card.mana.ManaCostShard; +import forge.game.card.Card; +import forge.game.card.CardUtil; +import forge.game.mana.ManaCostBeingPaid; +import forge.game.player.Player; +import org.apache.commons.lang3.tuple.ImmutablePair; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public final class InputSelectCardsForConvoke extends InputSelectManyBase { + private static final long serialVersionUID = -1779224307654698954L; + private final Map> chosenCards = new HashMap>(); + private final ManaCostBeingPaid remainingCost; + private final Player player; + + public InputSelectCardsForConvoke(Player p, ManaCost cost, List untapped) { + super(1, Math.min(cost.getCMC(), untapped.size())); + remainingCost = new ManaCostBeingPaid(cost); + player = p; + allowUnselect = true; + + } + + + protected String getMessage() { + return "Choose creatures to tap for convoke.\nRemaining mana cost is " + remainingCost.toString(); + } + + @Override + protected void onCardSelected(final Card card) { + + boolean entityWasSelected = chosenCards.containsKey(card); + if (entityWasSelected) { + ImmutablePair color = this.chosenCards.remove(card); + remainingCost.increaseShard(color.right, 1); + onSelectStateChanged(card, false); + } + else { + + byte chosenColor = player.getController().chooseColorAllowColorless("Convoke " + card.toString() + " for which color?", card, CardUtil.getColors(card)); + + if (remainingCost.getColorlessManaAmount() > 0 && (chosenColor == 0 || !remainingCost.needsColor(chosenColor, player.getManaPool()))) { + registerConvoked(card, ManaCostShard.COLORLESS, chosenColor); + } else { + for (ManaCostShard shard : remainingCost.getDistinctShards()) { + if (shard.canBePaidWithManaOfColor(chosenColor)) { + registerConvoked(card, shard, chosenColor); + return; + } + } + showMessage("The colors provided by " + card.toString() + " you've chosen cannot be used to decrease the manacost of " + remainingCost.toString()); + flashIncorrectAction(); + } + } + + refresh(); + } + + private void registerConvoked(Card card, ManaCostShard shard, byte chosenColor) { + remainingCost.decreaseShard(shard, 1); + chosenCards.put(card, ImmutablePair.of(chosenColor, shard)); + onSelectStateChanged(card, true); + } + + + @Override + protected final void onPlayerSelected(Player player) { + } + + public Map getConvokeMap() { + Map result = new HashMap(); + if( !hasCancelled() ) + for(Entry> c : chosenCards.entrySet()) + result.put(c.getKey(), c.getValue().right); + return result; + } + + + @Override + protected boolean hasEnoughTargets() { return true; } + + @Override + protected boolean hasAllTargets() { return false; } + + + @Override + public Collection getSelected() { + // TODO Auto-generated method stub + return chosenCards.keySet(); + } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/input/InputSelectCardsFromList.java b/forge-m-base/src/forge/screens/match/input/InputSelectCardsFromList.java new file mode 100644 index 00000000000..a19e4719bb2 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputSelectCardsFromList.java @@ -0,0 +1,21 @@ +package forge.screens.match.input; + +import forge.game.card.Card; + +import java.util.Collection; + +public class InputSelectCardsFromList extends InputSelectEntitiesFromList { + private static final long serialVersionUID = 6230360322294805986L; + + public InputSelectCardsFromList(int cnt, Collection validCards) { + super(cnt, cnt, validCards); // to avoid hangs + } + + public InputSelectCardsFromList(int min, int max, Collection validCards) { + super(min, max, validCards); // to avoid hangs + } + + public InputSelectCardsFromList(Collection validCards) { + super(1, 1, validCards); // to avoid hangs + } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/input/InputSelectEntitiesFromList.java b/forge-m-base/src/forge/screens/match/input/InputSelectEntitiesFromList.java new file mode 100644 index 00000000000..4759ba10a35 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputSelectEntitiesFromList.java @@ -0,0 +1,75 @@ +package forge.screens.match.input; + +import forge.game.GameEntity; +import forge.game.card.Card; +import forge.game.player.Player; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class InputSelectEntitiesFromList extends InputSelectManyBase { + private static final long serialVersionUID = -6609493252672573139L; + + private final Collection validChoices; + protected final List selected = new ArrayList(); + + public InputSelectEntitiesFromList(int min, int max, Collection validChoices) { + super(Math.min(min, validChoices.size()), Math.min(max, validChoices.size())); + this.validChoices = validChoices; + + if ( min > validChoices.size() ) + System.out.println(String.format("Trying to choose at least %d cards from a list with only %d cards!", min, validChoices.size())); + + } + + @Override + protected void onCardSelected(final Card c) { + if (!selectEntity(c)) { + return; + } + refresh(); + } + + @Override + protected void onPlayerSelected(final Player p) { + if (!selectEntity(p)) { + return; + } + refresh(); + } + + public final Collection getSelected() { + return selected; + } + + @SuppressWarnings("unchecked") + protected boolean selectEntity(GameEntity c) { + if (!validChoices.contains(c)) { + return false; + } + + boolean entityWasSelected = selected.contains(c); + if (entityWasSelected) { + if (!allowUnselect) + return false; + this.selected.remove(c); + } + else { + this.selected.add((T)c); + } + onSelectStateChanged(c, !entityWasSelected); + + return true; + } + + // might re-define later + protected boolean hasEnoughTargets() { return selected.size() >= min; } + protected boolean hasAllTargets() { return selected.size() >= max; } + + protected String getMessage() { + return max == Integer.MAX_VALUE + ? String.format(message, selected.size()) + : String.format(message, max - selected.size()); + } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/input/InputSelectManyBase.java b/forge-m-base/src/forge/screens/match/input/InputSelectManyBase.java new file mode 100644 index 00000000000..1ba006d4883 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputSelectManyBase.java @@ -0,0 +1,101 @@ +package forge.screens.match.input; + +import com.google.common.collect.Iterables; + +import forge.game.GameEntity; +import forge.game.card.Card; +import forge.screens.match.FControl; + +import java.util.Collection; + +public abstract class InputSelectManyBase extends InputSyncronizedBase { + private static final long serialVersionUID = -2305549394512889450L; + + protected boolean bCancelled = false; + protected final int min; + protected final int max; + protected boolean allowUnselect = false; + protected boolean allowCancel = false; + + protected String message = "Source-Card-Name - Select %d more card(s)"; + + protected InputSelectManyBase(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("Min must not be greater than Max"); + } + this.min = min; + this.max = max; + } + + protected void refresh() { + if (hasAllTargets()) { + selectButtonOK(); + } + else { + this.showMessage(); + } + } + + protected abstract boolean hasEnoughTargets(); + protected abstract boolean hasAllTargets(); + + protected abstract String getMessage(); + + @Override + public final void showMessage() { + showMessage(getMessage()); + + boolean canCancel = allowCancel; + boolean canOk = hasEnoughTargets(); + + if (canOk && canCancel) { ButtonUtil.enableAll(); } + if (!canOk && canCancel) { ButtonUtil.enableOnlyCancel(); } + if (canOk && !canCancel) { ButtonUtil.enableOnlyOk(); } + if (!canOk && !canCancel) { ButtonUtil.disableAll(); } + } + + + @Override + protected final void onCancel() { + bCancelled = true; + this.getSelected().clear(); + this.stop(); + afterStop(); + } + + public final boolean hasCancelled() { + return bCancelled; + } + + public abstract Collection getSelected(); + public T getFirstSelected() { return Iterables.getFirst(getSelected(), null); } + + @Override + protected final void onOk() { + this.stop(); + afterStop(); + } + + public void setMessage(String message0) { + this.message = message0; + } + + protected void onSelectStateChanged(GameEntity c, boolean newState) { + if (c instanceof Card) { + FControl.setUsedToPay((Card)c, newState); // UI supports card highlighting though this abstraction-breaking mechanism + } + } + + protected void afterStop() { + for (GameEntity c : getSelected()) { + if (c instanceof Card) { + FControl.setUsedToPay((Card)c, false); + } + } + } + + public final boolean isUnselectAllowed() { return allowUnselect; } + public final void setUnselectAllowed(boolean allow) { this.allowUnselect = allow; } + + public final void setCancelAllowed(boolean allow) { this.allowCancel = allow ; } +} diff --git a/forge-m-base/src/forge/screens/match/input/InputSelectTargets.java b/forge-m-base/src/forge/screens/match/input/InputSelectTargets.java new file mode 100644 index 00000000000..775737e9464 --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputSelectTargets.java @@ -0,0 +1,229 @@ +package forge.screens.match.input; + +import forge.game.GameEntity; +import forge.game.ability.ApiType; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.TargetRestrictions; +import forge.screens.match.FControl; +import forge.toolbox.GuiChoose; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public final class InputSelectTargets extends InputSyncronizedBase { + private final List choices; + // some cards can be targeted several times (eg: distribute damage as you choose) + private final Map targetDepth = new HashMap(); + private final TargetRestrictions tgt; + private final SpellAbility sa; + private boolean bCancel = false; + private boolean bOk = false; + private final boolean mandatory; + private static final long serialVersionUID = -1091595663541356356L; + + public final boolean hasCancelled() { return bCancel; } + public final boolean hasPressedOk() { return bOk; } + /** + * TODO: Write javadoc for Constructor. + * @param select + * @param choices + * @param req + * @param alreadyTargeted + * @param targeted + * @param tgt + * @param sa + * @param mandatory + */ + public InputSelectTargets(List choices, SpellAbility sa, boolean mandatory) { + this.choices = choices; + this.tgt = sa.getTargetRestrictions(); + this.sa = sa; + this.mandatory = mandatory; + } + + @Override + public void showMessage() { + final StringBuilder sb = new StringBuilder(); + sb.append("Targeted:\n"); + for (final Entry o : targetDepth.entrySet()) { + sb.append(o.getKey()); + if( o.getValue() > 1 ) + sb.append(" (").append(o.getValue()).append(" times)"); + sb.append("\n"); + } + if (!sa.getUniqueTargets().isEmpty()) { + sb.append("Parent Targeted:"); + sb.append(sa.getUniqueTargets()).append("\n"); + } + sb.append(sa.getHostCard() + " - " + tgt.getVTSelection()); + + int maxTargets = tgt.getMaxTargets(sa.getHostCard(), sa); + int targeted = sa.getTargets().getNumTargeted(); + if(maxTargets > 1) + sb.append("\n(").append(maxTargets - targeted).append(" more can be targeted)"); + + showMessage(sb.toString()); + + // If reached Minimum targets, enable OK button + if (!tgt.isMinTargetsChosen(sa.getHostCard(), sa) || tgt.isDividedAsYouChoose()) { + if (mandatory && tgt.hasCandidates(sa, true)) { + // Player has to click on a target + ButtonUtil.disableAll(); + } else { + ButtonUtil.enableOnlyCancel(); + } + } else { + if (mandatory && tgt.hasCandidates(sa, true)) { + // Player has to click on a target or ok + ButtonUtil.enableOnlyOk(); + } else { + ButtonUtil.enableAll(); + } + } + } + + @Override + protected final void onCancel() { + bCancel = true; + this.done(); + } + + @Override + protected final void onOk() { + bOk = true; + this.done(); + } + + @Override + protected final void onCardSelected(final Card card) { + if (!tgt.isUniqueTargets() && targetDepth.containsKey(card)) { + return; + } + + // leave this in temporarily, there some seriously wrong things going on here + // Can be targeted doesn't check if the target is a valid type, only if a card is generally "targetable" + if (!card.canBeTargetedBy(sa)) { + showMessage(sa.getHostCard() + " - Cannot target this card (Shroud? Protection? Restrictions)."); + return; + } + if (!choices.contains(card)) { + if (card.isPlaneswalker() && sa.getApi() == ApiType.DealDamage) { + showMessage(sa.getHostCard() + " - To deal an opposing Planeswalker direct damage, target its controller and then redirect the damage on resolution."); + } else { + showMessage(sa.getHostCard() + " - The selected card is not a valid choice to be targeted."); + } + return; + } + + if (tgt.isDividedAsYouChoose()) { + final int stillToDivide = tgt.getStillToDivide(); + int allocatedPortion = 0; + // allow allocation only if the max targets isn't reached and there are more candidates + if ((sa.getTargets().getNumTargeted() + 1 < tgt.getMaxTargets(sa.getHostCard(), sa)) + && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { + final Integer[] choices = new Integer[stillToDivide]; + for (int i = 1; i <= stillToDivide; i++) { + choices[i - 1] = i; + } + String apiBasedMessage = "Distribute how much to "; + if (sa.getApi() == ApiType.DealDamage) { + apiBasedMessage = "Select how much damage to deal to "; + } else if (sa.getApi() == ApiType.PreventDamage) { + apiBasedMessage = "Select how much damage to prevent to "; + } else if (sa.getApi() == ApiType.PutCounter) { + apiBasedMessage = "Select how many counters to distribute to "; + } + final StringBuilder sb = new StringBuilder(); + sb.append(apiBasedMessage); + sb.append(card.toString()); + Integer chosen = GuiChoose.oneOrNone(sb.toString(), choices); + if (null == chosen) { + return; + } + allocatedPortion = chosen; + } else { // otherwise assign the rest of the damage/protection + allocatedPortion = stillToDivide; + } + tgt.setStillToDivide(stillToDivide - allocatedPortion); + tgt.addDividedAllocation(card, allocatedPortion); + } + addTarget(card); + } // selectCard() + + @Override + protected final void onPlayerSelected(Player player) { + if (!tgt.isUniqueTargets() && targetDepth.containsKey(player)) { + return; + } + + if (!sa.canTarget(player)) { + showMessage(sa.getHostCard() + " - Cannot target this player (Hexproof? Protection? Restrictions?)."); + return; + } + + if (tgt.isDividedAsYouChoose()) { + final int stillToDivide = tgt.getStillToDivide(); + int allocatedPortion = 0; + // allow allocation only if the max targets isn't reached and there are more candidates + if ((sa.getTargets().getNumTargeted() + 1 < tgt.getMaxTargets(sa.getHostCard(), sa)) && (tgt.getNumCandidates(sa, true) - 1 > 0) && stillToDivide > 1) { + final Integer[] choices = new Integer[stillToDivide]; + for (int i = 1; i <= stillToDivide; i++) { + choices[i - 1] = i; + } + String apiBasedMessage = "Distribute how much to "; + if (sa.getApi() == ApiType.DealDamage) { + apiBasedMessage = "Select how much damage to deal to "; + } else if (sa.getApi() == ApiType.PreventDamage) { + apiBasedMessage = "Select how much damage to prevent to "; + } + final StringBuilder sb = new StringBuilder(); + sb.append(apiBasedMessage); + sb.append(player.getName()); + Integer chosen = GuiChoose.oneOrNone(sb.toString(), choices); + if (null == chosen) { + return; + } + allocatedPortion = chosen; + } else { // otherwise assign the rest of the damage/protection + allocatedPortion = stillToDivide; + } + tgt.setStillToDivide(stillToDivide - allocatedPortion); + tgt.addDividedAllocation(player, allocatedPortion); + } + addTarget(player); + } + + private void addTarget(GameEntity ge) { + sa.getTargets().add(ge); + if (ge instanceof Card) { + FControl.setUsedToPay((Card) ge, true); + } + Integer val = targetDepth.get(ge); + targetDepth.put(ge, val == null ? Integer.valueOf(1) : Integer.valueOf(val.intValue() + 1) ); + + if (hasAllTargets()) { + bOk = true; + this.done(); + } + else { + this.showMessage(); + } + } + + private void done() { + for (GameEntity c : targetDepth.keySet()) { + if (c instanceof Card) { + FControl.setUsedToPay((Card)c, false); + } + } + this.stop(); + } + + private boolean hasAllTargets() { + return tgt.isMaxTargetsChosen(sa.getHostCard(), sa) || ( tgt.getStillToDivide() == 0 && tgt.isDividedAsYouChoose()); + } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/input/InputSynchronized.java b/forge-m-base/src/forge/screens/match/input/InputSynchronized.java new file mode 100644 index 00000000000..63889af7e3e --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputSynchronized.java @@ -0,0 +1,6 @@ +package forge.screens.match.input; + +public interface InputSynchronized extends Input { + void awaitLatchRelease(); + void relaseLatchWhenGameIsOver(); +} diff --git a/forge-m-base/src/forge/screens/match/input/InputSyncronizedBase.java b/forge-m-base/src/forge/screens/match/input/InputSyncronizedBase.java new file mode 100644 index 00000000000..42c9c8720ab --- /dev/null +++ b/forge-m-base/src/forge/screens/match/input/InputSyncronizedBase.java @@ -0,0 +1,52 @@ +package forge.screens.match.input; + +import forge.FThreads; +import forge.error.BugReporter; +import forge.screens.match.FControl; + +import java.util.concurrent.CountDownLatch; + +public abstract class InputSyncronizedBase extends InputBase implements InputSynchronized { + private static final long serialVersionUID = 8756177361251703052L; + private final CountDownLatch cdlDone; + + public InputSyncronizedBase() { + cdlDone = new CountDownLatch(1); + } + + public void awaitLatchRelease() { + FThreads.assertExecutedByEdt(false); + try{ + cdlDone.await(); + } catch (InterruptedException e) { + BugReporter.reportException(e); + } + } + + public final void relaseLatchWhenGameIsOver() { + cdlDone.countDown(); + } + + public void showAndWait() { + FControl.getInputQueue().setInput(this); + awaitLatchRelease(); + } + + protected final void stop() { + onStop(); + + // ensure input won't accept any user actions. + FThreads.invokeInEdtNowOrLater(new Runnable() { + @Override + public void run() { + setFinished(); + } + }); + + // thread irrelevant + FControl.getInputQueue().removeInput(InputSyncronizedBase.this); + cdlDone.countDown(); + } + + protected void onStop() { } +} \ No newline at end of file diff --git a/forge-m-base/src/forge/screens/match/views/VLog.java b/forge-m-base/src/forge/screens/match/views/VLog.java index 31f17c14ffe..adb4771d928 100644 --- a/forge-m-base/src/forge/screens/match/views/VLog.java +++ b/forge-m-base/src/forge/screens/match/views/VLog.java @@ -1,6 +1,5 @@ package forge.screens.match.views; -import forge.screens.FScreen; import forge.toolbox.FContainer; public class VLog extends FContainer { diff --git a/forge-m-base/src/forge/screens/match/views/VPhases.java b/forge-m-base/src/forge/screens/match/views/VPhaseIndicator.java similarity index 89% rename from forge-m-base/src/forge/screens/match/views/VPhases.java rename to forge-m-base/src/forge/screens/match/views/VPhaseIndicator.java index 6c9e05d3ae3..d1d5eb8cf72 100644 --- a/forge-m-base/src/forge/screens/match/views/VPhases.java +++ b/forge-m-base/src/forge/screens/match/views/VPhaseIndicator.java @@ -15,13 +15,13 @@ import forge.toolbox.FContainer; import forge.toolbox.FDisplayObject; import forge.utils.Utils; -public class VPhases extends FContainer { +public class VPhaseIndicator extends FContainer { public static final float HEIGHT = Utils.AVG_FINGER_HEIGHT * 0.3f; private static final FSkinFont labelFont = FSkinFont.get(11); private final Map phaseLabels = new HashMap(); - public VPhases() { + public VPhaseIndicator() { addPhaseLabel("UP", PhaseType.UPKEEP); addPhaseLabel("DR", PhaseType.DRAW); addPhaseLabel("M1", PhaseType.MAIN1); @@ -40,6 +40,10 @@ public class VPhases extends FContainer { phaseLabels.put(phaseType, add(new PhaseLabel(caption, phaseType))); } + public PhaseLabel getLabel(PhaseType phaseType) { + return phaseLabels.get(phaseType); + } + public void resetPhaseButtons() { for (PhaseLabel lbl : phaseLabels.values()) { lbl.setActive(false); @@ -60,7 +64,7 @@ public class VPhases extends FContainer { } } - private class PhaseLabel extends FDisplayObject { + public class PhaseLabel extends FDisplayObject { private final String caption; private final PhaseType phaseType; private boolean stopAtPhase = false; @@ -78,6 +82,10 @@ public class VPhases extends FContainer { active = active0; } + public PhaseType getPhaseType() { + return phaseType; + } + public boolean getStopAtPhase() { return stopAtPhase; } diff --git a/forge-m-base/src/forge/screens/match/views/VPlayerPanel.java b/forge-m-base/src/forge/screens/match/views/VPlayerPanel.java index 186c7958a29..dc602d8d620 100644 --- a/forge-m-base/src/forge/screens/match/views/VPlayerPanel.java +++ b/forge-m-base/src/forge/screens/match/views/VPlayerPanel.java @@ -24,7 +24,7 @@ public class VPlayerPanel extends FContainer { private static final FSkinColor ZONE_BACK_COLOR = FSkinColor.get(Colors.CLR_INACTIVE).alphaColor(0.5f); private final RegisteredPlayer player; - private final VPhases phases; + private final VPhaseIndicator phaseIndicator; private final VField field; private final VAvatar avatar; private final List infoLabels = new ArrayList(); @@ -33,7 +33,7 @@ public class VPlayerPanel extends FContainer { public VPlayerPanel(RegisteredPlayer player0) { player = player0; - phases = add(new VPhases()); + phaseIndicator = add(new VPhaseIndicator()); field = add(new VField()); avatar = add(new VAvatar(player.getPlayer().getAvatarIndex())); infoLabels.add(add(new LifeLabel())); @@ -96,11 +96,19 @@ public class VPlayerPanel extends FContainer { return field; } + public VPhaseIndicator getPhaseIndicator() { + return phaseIndicator; + } + + public VAvatar getAvatar() { + return avatar; + } + @Override protected void doLayout(float width, float height) { //layout for bottom panel by default float x = VAvatar.WIDTH; - phases.setBounds(x, height - VPhases.HEIGHT, width - VAvatar.WIDTH, VPhases.HEIGHT); + phaseIndicator.setBounds(x, height - VPhaseIndicator.HEIGHT, width - VAvatar.WIDTH, VPhaseIndicator.HEIGHT); float y, zoneHeight; if (selectedZone != null) { @@ -118,7 +126,7 @@ public class VPlayerPanel extends FContainer { y = height - VAvatar.HEIGHT; avatar.setPosition(0, y); float infoLabelWidth; - float infoLabelHeight = VAvatar.HEIGHT - VPhases.HEIGHT; + float infoLabelHeight = VAvatar.HEIGHT - VPhaseIndicator.HEIGHT; for (InfoLabel infoLabel : infoLabels) { infoLabelWidth = infoLabel.getPreferredWidth(); infoLabel.setBounds(x, y, infoLabelWidth, infoLabelHeight); diff --git a/forge-m-base/src/forge/screens/match/views/VPrompt.java b/forge-m-base/src/forge/screens/match/views/VPrompt.java index 582dc32b68d..2941012872e 100644 --- a/forge-m-base/src/forge/screens/match/views/VPrompt.java +++ b/forge-m-base/src/forge/screens/match/views/VPrompt.java @@ -1,11 +1,18 @@ package forge.screens.match.views; +import org.apache.commons.lang3.StringUtils; + import com.badlogic.gdx.graphics.g2d.BitmapFont.HAlignment; +import forge.FThreads; import forge.Forge.Graphics; import forge.assets.FSkinColor; import forge.assets.FSkinFont; import forge.assets.FSkinColor.Colors; +import forge.game.Game; +import forge.game.GameRules; +import forge.game.Match; +import forge.screens.match.input.InputProxy; import forge.toolbox.FButton; import forge.toolbox.FContainer; import forge.utils.Utils; @@ -19,15 +26,59 @@ public class VPrompt extends FContainer { private static final FSkinFont font = FSkinFont.get(11); private final FButton btnOk, btnCancel; - private String message = "This is where the prompt would be.\nLine 2 of the prompt.\nLine 3 of the prompt."; + private final InputProxy inputProxy = new InputProxy(); + private String message; public VPrompt() { - btnOk = add(new FButton("Yes")); - btnCancel = add(new FButton("No")); + btnOk = add(new FButton("Yes", new Runnable() { + @Override + public void run() { + inputProxy.selectButtonOK(); + } + })); + btnCancel = add(new FButton("No", new Runnable() { + @Override + public void run() { + inputProxy.selectButtonCancel(); + } + })); btnOk.setSize(BTN_WIDTH, HEIGHT); btnCancel.setSize(BTN_WIDTH, HEIGHT); } + public FButton getBtnOk() { + return btnOk; + } + + public FButton getBtnCancel() { + return btnCancel; + } + + public String getMessage() { + return message; + } + public void setMessage(String message0) { + message = message0; + } + + public InputProxy getInputProxy() { + return inputProxy; + } + + /** Flashes animation on input panel if play is currently waiting on input. */ + public void remind() { + //SDisplayUtil.remind(view); + } + + public void updateText(Game game) { + //FThreads.assertExecutedByEdt(true); + //final Match match = game.getMatch(); + //final GameRules rules = game.getRules(); + //final String text = String.format("T:%d G:%d/%d [%s]", game.getPhaseHandler().getTurn(), match.getPlayedGames().size() + 1, rules.getGamesPerMatch(), rules.getGameType()); + //view.getLblGames().setText(text); + //view.getLblGames().setToolTipText(String.format("%s: Game #%d of %d, turn %d", rules.getGameType(), match.getPlayedGames().size() + 1, rules.getGamesPerMatch(), game.getPhaseHandler().getTurn())); + } + @Override protected void doLayout(float width, float height) { btnCancel.setLeft(width - BTN_WIDTH); @@ -39,7 +90,9 @@ public class VPrompt extends FContainer { float h = getHeight(); g.fillRect(backColor, 0, 0, w, h); - g.drawText(message, font, foreColor, BTN_WIDTH, 0, w - 2 * BTN_WIDTH, h, - true, HAlignment.CENTER, true); + if (!StringUtils.isEmpty(message)) { + g.drawText(message, font, foreColor, BTN_WIDTH, 0, w - 2 * BTN_WIDTH, h, + true, HAlignment.CENTER, true); + } } } diff --git a/forge-m-base/src/forge/screens/match/views/VStack.java b/forge-m-base/src/forge/screens/match/views/VStack.java index 45de9fbf67f..82cad252bee 100644 --- a/forge-m-base/src/forge/screens/match/views/VStack.java +++ b/forge-m-base/src/forge/screens/match/views/VStack.java @@ -13,6 +13,10 @@ public class VStack extends FContainer { setSize(WIDTH, HEIGHT); } + public void update() { + //TODO + } + @Override protected void doLayout(float width, float height) { // TODO Auto-generated method stub diff --git a/forge-m-base/src/forge/toolbox/FButton.java b/forge-m-base/src/forge/toolbox/FButton.java index e7ae9437479..12411efc06c 100644 --- a/forge-m-base/src/forge/toolbox/FButton.java +++ b/forge-m-base/src/forge/toolbox/FButton.java @@ -1,5 +1,7 @@ package forge.toolbox; +import org.apache.commons.lang3.StringUtils; + import com.badlogic.gdx.graphics.g2d.BitmapFont.HAlignment; import forge.Forge.Graphics; @@ -13,7 +15,7 @@ public class FButton extends FDisplayObject { private static final FSkinColor foreColor = FSkinColor.get(Colors.CLR_TEXT); private FSkinImage imgL, imgM, imgR; - private String caption; + private String text; private FSkinFont font; private boolean toggled = false; private Runnable command; @@ -25,12 +27,12 @@ public class FButton extends FDisplayObject { this("", null); } - public FButton(final String caption0) { - this(caption0, null); + public FButton(final String text0) { + this(text0, null); } - public FButton(final String caption0, Runnable command0) { - caption = caption0; + public FButton(final String text0, Runnable command0) { + text = text0; command = command0; font = FSkinFont.get(14); resetImg(); @@ -42,6 +44,13 @@ public class FButton extends FDisplayObject { imgR = FSkinImage.BTN_UP_RIGHT; } + public String getText() { + return text; + } + public void setText(String text0) { + text = text0; + } + @Override public void setEnabled(boolean b0) { if (isEnabled() == b0) { return; } @@ -127,8 +136,8 @@ public class FButton extends FDisplayObject { g.drawImage(imgL, 0, 0, buttonWidth, h); g.drawImage(imgR, buttonWidth, 0, w - buttonWidth, h); } - if (!caption.isEmpty()) { - g.drawText(caption, font, foreColor, insetX, 0, w - 2 * insetX, h, false, HAlignment.CENTER, true); + if (!StringUtils.isEmpty(text)) { + g.drawText(text, font, foreColor, insetX, 0, w - 2 * insetX, h, false, HAlignment.CENTER, true); } } } diff --git a/forge-m-base/src/forge/toolbox/GuiDialog.java b/forge-m-base/src/forge/toolbox/GuiDialog.java new file mode 100644 index 00000000000..feed8459b2b --- /dev/null +++ b/forge-m-base/src/forge/toolbox/GuiDialog.java @@ -0,0 +1,71 @@ +package forge.toolbox; + +import forge.FThreads; +import forge.game.card.Card; +import org.apache.commons.lang3.StringUtils; + +import javax.swing.*; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +/** + * Holds player interactions using standard windows + * + */ +public class GuiDialog { + private static final String[] defaultConfirmOptions = { "Yes", "No" }; + + public static boolean confirm(final Card c, final String question) { + return GuiDialog.confirm(c, question, true, null); + } + public static boolean confirm(final Card c, final String question, final boolean defaultChoice) { + return GuiDialog.confirm(c, question, defaultChoice, null); + } + public static boolean confirm(final Card c, final String question, String[] options) { + return GuiDialog.confirm(c, question, true, options); + } + + public static boolean confirm(final Card c, final String question, final boolean defaultIsYes, final String[] options) { + Callable confirmTask = new Callable() { + @Override + public Boolean call() throws Exception { + final String title = c == null ? "Question" : c.getName() + " - Ability"; + String questionToUse = StringUtils.isBlank(question) ? "Activate card's ability?" : question; + String[] opts = options == null ? defaultConfirmOptions : options; + int answer = FOptionPane.showOptionDialog(questionToUse, title, FOptionPane.QUESTION_ICON, opts, defaultIsYes ? 0 : 1); + return answer == 0; + }}; + + FutureTask future = new FutureTask(confirmTask); + FThreads.invokeInEdtAndWait(future); + try { + return future.get().booleanValue(); + } + catch (Exception e) { // should be no exception here + e.printStackTrace(); + } + return false; + } + + /** + *

+ * showInfoDialg. + *

+ * + * @param message + * a {@link java.lang.String} object. + */ + public static void message(final String message) { + message(message, UIManager.getString("OptionPane.messageDialogTitle")); + } + + public static void message(final String message, final String title) { + FThreads.invokeInEdtAndWait(new Runnable() { + @Override + public void run() { + FOptionPane.showMessageDialog(message, title, null); + } + }); + } +} diff --git a/forge-m-base/src/forge/utils/Evaluator.java b/forge-m-base/src/forge/utils/Evaluator.java new file mode 100644 index 00000000000..d850bcf90fb --- /dev/null +++ b/forge-m-base/src/forge/utils/Evaluator.java @@ -0,0 +1,16 @@ +package forge.utils; + +public abstract class Evaluator implements Runnable { + private T result; + + @Override + public final void run() { + result = evaluate(); + } + + public abstract T evaluate(); + + public T getResult() { + return result; + } +}