diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java index 6ac4c06c038..e7e89fb8b95 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -186,6 +186,11 @@ public class DestroyAi extends SpellAbilityAi { if (hasXCost) { // TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement. maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa)); + // X can't be more than the lands we have in our hand for "discard X lands"! + if ("ScorchedEarth".equals(logic)) { + int lands = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size(); + maxTargets = Math.min(maxTargets, lands); + } } if (sa.hasParam("AIMaxTgtsCount")) { // Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 66fc64cfcbd..012b6effe00 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -295,9 +295,10 @@ public class CardFactory { sa.setRightSplit(); } } + CardFactoryUtil.setupKeywordedAbilities(card); final CardState original = card.getState(CardStateName.Original); original.addNonManaAbilities(card.getCurrentState().getNonManaAbilities()); - original.addIntrinsicKeywords(card.getCurrentState().getIntrinsicKeywordStrings(), false); // Copy 'Fuse' to original side + original.addIntrinsicKeywords(card.getCurrentState().getIntrinsicKeywords()); // Copy 'Fuse' to original side original.getSVars().putAll(card.getCurrentState().getSVars()); // Unfortunately need to copy these to (Effect looks for sVars on execute) } else if (state != CardStateName.Original){ CardFactoryUtil.setupKeywordedAbilities(card); @@ -305,6 +306,10 @@ public class CardFactory { } card.setState(CardStateName.Original, false); + // need to update keyword cache for original spell + if (card.isSplitCard()) { + card.updateKeywordsCache(card.getCurrentState()); + } // ****************************************************************** // ************** Link to different CardFactories ******************* diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 022c5e0be99..a090562c9a2 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -2999,7 +2999,7 @@ public class CardFactoryUtil { public static void addReplacementEffect(final KeywordInterface inst, final Card card, final boolean intrinsic) { String keyword = inst.getOriginal(); - if (keyword.equals("Aftermath")) { + if (keyword.equals("Aftermath") && card.getCurrentStateName().equals(CardStateName.RightSplit)) { StringBuilder sb = new StringBuilder(); sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack | ExcludeDestination$ Exile "); sb.append("| ValidStackSa$ Spell.Aftermath | Description$ Aftermath"); @@ -3021,8 +3021,6 @@ public class CardFactoryUtil { re.setOverridingAbility(saExile); - // Aftermath only on Rightsplit - // doesn't make a copy with it inst.addReplacement(re); } else if (keyword.startsWith("Amplify")) { final String[] ampString = keyword.split(":"); @@ -3493,11 +3491,11 @@ public class CardFactoryUtil { inst.addSpellAbility(newSA); } - } else if (keyword.equals("Aftermath")) { + } else if (keyword.equals("Aftermath") && card.getCurrentStateName().equals(CardStateName.RightSplit)) { // Aftermath does modify existing SA, and does not add new one // only target RightSplit of it - final SpellAbility origSA = card.getState(CardStateName.RightSplit).getFirstAbility(); + final SpellAbility origSA = card.getFirstSpellAbility(); origSA.setAftermath(true); origSA.getRestrictions().setZone(ZoneType.Graveyard); // The Exile part is done by the System itself @@ -3742,9 +3740,9 @@ public class CardFactoryUtil { inst.addSpellAbility(sa); - } else if (keyword.startsWith("Fuse")) { + } else if (keyword.startsWith("Fuse") && card.getCurrentStateName().equals(CardStateName.Original)) { final SpellAbility sa = AbilityFactory.buildFusedAbility(card); - card.getState(CardStateName.Original).addNonManaAbility(sa); + card.addSpellAbility(sa); sa.setTemporary(!intrinsic); inst.addSpellAbility(sa); diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index c481ad2c234..d3f7d38698f 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -164,9 +164,6 @@ public class CardState extends GameObject { public final Collection getIntrinsicKeywords() { return intrinsicKeywords.getValues(); } - public final Iterable getIntrinsicKeywordStrings() { - return intrinsicKeywords; - } public final boolean hasIntrinsicKeyword(String k) { return intrinsicKeywords.contains(k); } diff --git a/forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt b/forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt index d510eff60af..b1b6cbabc98 100644 --- a/forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt +++ b/forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt @@ -3,5 +3,6 @@ ManaCost:W Types:Enchantment A:AB$ PumpAll | Cost$ 2 W | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | NumDef$ +1 | SpellDescription$ Creatures you control get +1/+1 until end of turn. SVar:NonStackingEffect:True +SVar:PlayMain1:TRUE SVar:Picture:http://www.wizards.com/global/images/magic/general/gerrards_battle_cry.jpg Oracle:{2}{W}: Creatures you control get +1/+1 until end of turn. diff --git a/forge-gui/res/cardsfolder/s/scorched_earth.txt b/forge-gui/res/cardsfolder/s/scorched_earth.txt index 2f2744c16a1..d67b59385df 100644 --- a/forge-gui/res/cardsfolder/s/scorched_earth.txt +++ b/forge-gui/res/cardsfolder/s/scorched_earth.txt @@ -1,10 +1,11 @@ Name:Scorched Earth ManaCost:X R Types:Sorcery -A:SP$ Destroy | Cost$ X R Discard | TargetMin$ 0 | TargetMax$ MaxTgts | ValidTgts$ Land | TgtPrompt$ Select target land | References$ X | SpellDescription$ Destroy X target lands. +A:SP$ Destroy | Cost$ X R Discard | TargetMin$ 0 | TargetMax$ MaxTgts | ValidTgts$ Land | TgtPrompt$ Select target land | References$ X | SpellDescription$ Destroy X target lands. | AILogic$ ScorchedEarth # It may seem wrong to not use X in the target, but since the Targets are what defines X, it's redundant (and not supported by the code) SVar:X:Targeted$Amount SVar:MaxTgts:Count$Valid Land -SVar:RemAIDeck:True +SVar:RemRandomDeck:True +SVar:PlayBeforeLandDrop:true SVar:Picture:http://www.wizards.com/global/images/magic/general/scorched_earth.jpg Oracle:As an additional cost to cast Scorched Earth, discard X land cards.\nDestroy X target lands. diff --git a/forge-gui/src/main/java/forge/match/GameLobby.java b/forge-gui/src/main/java/forge/match/GameLobby.java index 168a331c60b..097705ed545 100644 --- a/forge-gui/src/main/java/forge/match/GameLobby.java +++ b/forge-gui/src/main/java/forge/match/GameLobby.java @@ -4,10 +4,12 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; + import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -49,7 +51,7 @@ public abstract class GameLobby implements IHasGameType { private final boolean allowNetworking; private HostedMatch hostedMatch; - private final Map gameControllers = Maps.newHashMap(); + private final HashMap gameControllers = Maps.newHashMap(); protected GameLobby(final boolean allowNetworking) { this.allowNetworking = allowNetworking; } @@ -481,6 +483,8 @@ public abstract class GameLobby implements IHasGameType { } } + hostedMatch.gameControllers = gameControllers; + onGameStarted(); } }; diff --git a/forge-gui/src/main/java/forge/match/HostedMatch.java b/forge-gui/src/main/java/forge/match/HostedMatch.java index 3e3f757a962..d42aaa68072 100644 --- a/forge-gui/src/main/java/forge/match/HostedMatch.java +++ b/forge-gui/src/main/java/forge/match/HostedMatch.java @@ -3,11 +3,14 @@ package forge.match; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import forge.LobbyPlayer; +import forge.interfaces.IGameController; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -55,6 +58,7 @@ public class HostedMatch { private Match match; private Game game; private String title; + public HashMap gameControllers = null; private Runnable startGameHook = null; private final List humanControllers = Lists.newArrayList(); private Map guis; @@ -180,6 +184,12 @@ public class HostedMatch { game.subscribeToEvents(new FControlGameEventHandler(humanController)); playersPerGui.add(gui, p.getView()); + + if (gameControllers != null ) { + LobbySlot lobbySlot = getLobbySlot(p.getLobbyPlayer()); + gameControllers.put(lobbySlot, humanController); + } + humanControllers.add(humanController); humanCount++; } @@ -238,6 +248,18 @@ public class HostedMatch { }); } + private LobbySlot getLobbySlot(LobbyPlayer lobbyPlayer) { + for (LobbySlot key: gameControllers.keySet()) { + IGameController value = gameControllers.get(key); + if (value instanceof PlayerControllerHuman) { + if (lobbyPlayer == ((PlayerControllerHuman) value).getLobbyPlayer()) { + return key; + } + } + } + return null; + } + public void registerSpectator(final IGuiGame gui) { final PlayerControllerHuman humanController = new WatchLocalGame(game, null, gui); gui.setSpectator(humanController); diff --git a/forge-gui/src/main/java/forge/net/client/GameClientHandler.java b/forge-gui/src/main/java/forge/net/client/GameClientHandler.java index f15a4576bc5..08955a4d2c1 100644 --- a/forge-gui/src/main/java/forge/net/client/GameClientHandler.java +++ b/forge-gui/src/main/java/forge/net/client/GameClientHandler.java @@ -27,6 +27,8 @@ final class GameClientHandler extends GameProtocolHandler { private final FGameClient client; private final IGuiGame gui; private Tracker tracker; + private Match match; + private Game game; /** * Creates a client-side game handler. @@ -36,6 +38,8 @@ final class GameClientHandler extends GameProtocolHandler { this.client = client; this.gui = client.getGui(); this.tracker = null; + this.match = null; + this.game = null; } @Override @@ -58,32 +62,26 @@ final class GameClientHandler extends GameProtocolHandler { protected void beforeCall(final ProtocolMethod protocolMethod, final Object[] args) { switch (protocolMethod) { case openView: - if (this.tracker == null) { - int maxAttempts = 5; - for (int numAttempts = 0; numAttempts < maxAttempts; numAttempts++) { - try { + // only need one **match** + if (this.match == null) { + this.match = createMatch(); + } - this.tracker = createTracker(); + // openView is called **once** per game, for now create a new Game instance each time + this.game = createGame(); - for (PlayerView myPlayer : (TrackableCollection) args[0]) { - if (myPlayer.getTracker() == null) { - myPlayer.setTracker(this.tracker); - } - } + // get a tracker + this.tracker = createTracker(); - final TrackableCollection myPlayers = (TrackableCollection) args[0]; - client.setGameControllers(myPlayers); - - } catch (Exception e) { - System.err.println("Failed: attempt number: " + numAttempts + " - " + e.toString()); - try { - Thread.sleep(100); - } catch (InterruptedException e1) { - e1.printStackTrace(); - } - } + for (PlayerView myPlayer : (TrackableCollection) args[0]) { + if (myPlayer.getTracker() == null) { + myPlayer.setTracker(this.tracker); } } + + final TrackableCollection myPlayers = (TrackableCollection) args[0]; + client.setGameControllers(myPlayers); + break; default: break; @@ -108,19 +106,17 @@ final class GameClientHandler extends GameProtocolHandler { } /** - * This method creates the necessary objects and state to retrieve a Tracker object. - * - * Near as I can tell, that means that we need to create a Match. + * This method retrieves enough of the existing (incomplete) game state to + * recreate a new viable Match object * * Creating a Match requires that we have: * * GameRules * * RegisteredPlayers * * Title * - * @return Tracker + * @return Match */ - private Tracker createTracker() { - + private Match createMatch() { // retrieve what we can from the existing (but incomplete) state final IGuiGame gui = client.getGui(); GameView gameView = gui.getGameView(); @@ -134,12 +130,24 @@ final class GameClientHandler extends GameProtocolHandler { // create a valid match object and game Match match = new Match(gameRules, registeredPlayers, title); - Game game = match.createGame(); + return match; + } + + private Game createGame() { + this.tracker = null; + return this.match.createGame(); + } + + /** + * Ensure the stored GameView is correct and retrieve a Tracker object. + * + * @return Tracker + */ + private Tracker createTracker() { // replace the existing incomplete GameView with the newly created one gui.setGameView(null); gui.setGameView(game.getView()); - return gui.getGameView().getTracker(); } diff --git a/forge-gui/src/main/java/forge/net/server/FServerManager.java b/forge-gui/src/main/java/forge/net/server/FServerManager.java index 8b8416c8a04..2d9118764e5 100644 --- a/forge-gui/src/main/java/forge/net/server/FServerManager.java +++ b/forge-gui/src/main/java/forge/net/server/FServerManager.java @@ -55,6 +55,7 @@ import com.google.common.collect.Maps; public final class FServerManager { private static FServerManager instance = null; + private byte[] externalAddress = new byte[]{8,8,8,8}; private boolean isHosting = false; private final EventLoopGroup bossGroup = new NioEventLoopGroup(1); private final EventLoopGroup workerGroup = new NioEventLoopGroup(); @@ -210,7 +211,7 @@ public final class FServerManager { // https://stackoverflow.com/a/901943 private String getRoutableAddress(boolean preferIpv4, boolean preferIPv6) throws SocketException, UnknownHostException { DatagramSocket s = new DatagramSocket(); - s.connect(InetAddress.getByAddress(new byte[]{8,8,8,8}), 0); + s.connect(InetAddress.getByAddress(this.externalAddress), 0); NetworkInterface n = NetworkInterface.getByInetAddress(s.getLocalAddress()); Enumeration en = n.getInetAddresses(); while (en.hasMoreElements()) {