diff --git a/res/card-pictures.txt b/res/card-pictures.txt index d26f58f5b02..c1b7ed289f0 100644 --- a/res/card-pictures.txt +++ b/res/card-pictures.txt @@ -38,6 +38,8 @@ snow_covered_mountain.jpg http://www.wizards.com/global/images/magic/gene snow_covered_mountain1.jpg http://www.wizards.com/global/images/magic/general/snow_covered_mountain.jpg snow_covered_mountain2.jpg http://www.magickartenmarkt.de/img/cards/Ice_Age/snow_covered_mountain.jpg snow_covered_mountain3.jpg http://www.magickartenmarkt.de/img/cards/Ice_Age/snow_covered_mountain.jpg +expedition_map.jpg http://www.wizards.com/global/images/magic/general/expedition_map.jpg +valakut_the_molten_pinnacle.jpg http://www.wizards.com/global/images/magic/general/valakut_the_molten_pinnacle.jpg power_matrix.jpg http://www.wizards.com/global/images/magic/general/power_matrix.jpg scornful_aether_lich.jpg http://www.wizards.com/global/images/magic/general/scornful_aether_lich.jpg recall.jpg http://www.wizards.com/global/images/magic/general/recall.jpg diff --git a/res/cards.txt b/res/cards.txt index 1529096cd97..5e9e61ffdca 100644 --- a/res/cards.txt +++ b/res/cards.txt @@ -1,3 +1,15 @@ +Expedition Map +1 +Artifact +no text + +Valakut, the Molten Pinnacle +no cost +Land +Whenever a Mountain enters the battlefield under your control, if you control at least five other Mountains, you may have Valakut, the Molten Pinnacle deal 3 damage to target creature or player. +Valakut, the Molten Pinnacle enters the battlefield tapped. +tap: add R + Serpent of the Endless Sea 4 U Creature Serpent diff --git a/src/forge/AllZoneUtil.java b/src/forge/AllZoneUtil.java index 9ae93aa447a..1560597960e 100644 --- a/src/forge/AllZoneUtil.java +++ b/src/forge/AllZoneUtil.java @@ -291,6 +291,18 @@ public class AllZoneUtil { return cards; } + /** + * gets a list of all cards of a certain type that a given player has in his library + * @param player the player to check for cards in play + * @param cardType the card type to check for + * @return a CardList with all cards of a certain type the player has in his library + */ + public static CardList getPlayerTypeInLibrary(final String player, final String cardType) { + CardList cards = getPlayerCardsInLibrary(player); + cards = cards.getType(cardType); + return cards; + } + ////////////// cardListFilter for different types public static CardListFilter artifacts = new CardListFilter() { public boolean addCard(Card c) { diff --git a/src/forge/CardFactory.java b/src/forge/CardFactory.java index 12e9f5a540a..80dc3921ab6 100644 --- a/src/forge/CardFactory.java +++ b/src/forge/CardFactory.java @@ -1,6 +1,6 @@ - package forge; + import java.io.File; import java.util.ArrayList; import java.util.Arrays; @@ -18121,8 +18121,6 @@ public class CardFactory implements NewConstants { @Override public void resolve() { - PlayerZone grave = AllZone.getZone(Constant.Zone.Graveyard, card.getController()); - PlayerZone removed = AllZone.getZone(Constant.Zone.Removed_From_Play, card.getController()); if(getTargetCard() != null) { if(AllZone.GameAction.isCardInPlay(getTargetCard()) @@ -18132,12 +18130,12 @@ public class CardFactory implements NewConstants { } } else AllZone.GameAction.getPlayerLife(getTargetPlayer()).subtractLife(damage); - grave.remove(card); - removed.add(card); - + card.setKicked(true); } };//flashback - kicker.setManaCost("4 R"); + kicker.setManaCost("R 4"); + kicker.setAdditionalManaCost("4"); + kicker.setKickerAbility(true); kicker.setBeforePayMana(CardFactoryUtil.input_targetCreaturePlayer(kicker, true, false)); kicker.setDescription("Kicker: 4"); @@ -19840,6 +19838,31 @@ public class CardFactory implements NewConstants { };//Input spell.setBeforePayMana(target); }//*************** END ************ END ************************** + //*************** START *********** START ************************** + else if(cardName.equals("Expedition Map")) { + final Ability_Tap ability = new Ability_Tap(card, "2") { + + private static final long serialVersionUID = -5796728507926918991L; + + @Override + public boolean canPlayAI() { + return AllZoneUtil.getPlayerTypeInLibrary(Constant.Player.Computer, + "Land").size() >= 1; + } + + @Override + public void resolve() { + AllZone.GameAction.searchLibraryLand("Land", + card.getController(), Constant.Zone.Hand, false); + AllZone.GameAction.sacrifice(card); + } + };//ability + + ability.setDescription("2, tap, sacrifice Expedition Map: Search your library for a land card, reveal it, and put it into your hand. Then shuffle your library."); + ability.setStackDescription("Sacrifice Expedition Map: search your library for a land and put it into your hand."); + ability.setManaCost("2"); + card.addSpellAbility(ability); + }//*************** END ************ END ************************** //*************** START *********** START ************************** else if(cardName.equals("Recall")) { diff --git a/src/forge/GameAction.java b/src/forge/GameAction.java index 3e5547cebb8..7baae6b86af 100644 --- a/src/forge/GameAction.java +++ b/src/forge/GameAction.java @@ -5,6 +5,7 @@ package forge; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.Random; @@ -1415,7 +1416,7 @@ public class GameAction { playSpellAbilityForFree(sa); } - public void playSpellAbilityForFree(SpellAbility sa) { + public void playSpellAbilityForFree(final SpellAbility sa) { if(sa.getBeforePayMana() == null) { @@ -1423,6 +1424,17 @@ public class GameAction { if (sa.getSourceCard().getManaCost().contains("X")) x = true; + if (sa.isKickerAbility()) { + Command paid1 = new Command() { + + private static final long serialVersionUID = 1L; + + public void execute() { + AllZone.Stack.add(sa); + } + }; + AllZone.InputControl.setInput(new Input_PayManaCost_Ability(sa.getAdditionalManaCost(),paid1)); + } AllZone.Stack.add(sa, x); /* @@ -1435,7 +1447,12 @@ public class GameAction { } */ } else { - sa.getBeforePayMana().setFree(true); + if (sa.isKickerAbility()) { + sa.getBeforePayMana().setFree(false); + sa.setManaCost(sa.getAdditionalManaCost()); + } else { + sa.getBeforePayMana().setFree(true); + } AllZone.InputControl.setInput(sa.getBeforePayMana()); } } @@ -1718,31 +1735,70 @@ public class GameAction { } - public void searchLibraryBasicLand(String player, String Zone1, boolean tapLand) { - searchLibraryTwoBasicLand(player, Zone1, tapLand, "", false); + public void searchLibraryLand(String type, String player, String Zone1, boolean tapLand) { + searchLibraryTwoLand(type, player, Zone1, tapLand, "", false); } - - public void searchLibraryTwoBasicLand(String player, - String Zone1, boolean tapFirstLand, - String Zone2, boolean tapSecondLand) { - + + public void searchLibraryBasicLand(String player, String Zone1, boolean tapLand) { + searchLibraryTwoLand("Basic", player, Zone1, tapLand, "", false); + } + + public void searchLibraryTwoLand(String type, String player, + String Zone1, boolean tapFirstLand, + String Zone2, boolean tapSecondLand) { if(player.equals(Constant.Player.Human)) { - humanSearchTwoBasicLand(Zone1, tapFirstLand, Zone2, tapSecondLand); + humanSearchTwoLand(type, Zone1, tapFirstLand, Zone2, tapSecondLand); } else { - aiSearchTwoBasicLand(Zone1, tapFirstLand, Zone2, tapSecondLand); + aiSearchTwoLand(type, Zone1, tapFirstLand, Zone2, tapSecondLand); } AllZone.GameAction.shuffle(player); + + } + public void searchLibraryTwoBasicLand(String player, + String Zone1, boolean tapFirstLand, + String Zone2, boolean tapSecondLand) { + searchLibraryTwoLand("Basic", player, Zone1, tapFirstLand, Zone2, tapSecondLand); } - - private void aiSearchTwoBasicLand(String Zone1, boolean tapFirstLand, + + private void aiSearchTwoLand(String type, String Zone1, boolean tapFirstLand, String Zone2, boolean tapSecondLand) { CardList land = new CardList(AllZone.Computer_Library.getCards()); - land = land.getType("Basic"); + land = land.getType(type); PlayerZone firstZone = AllZone.getZone(Zone1, Constant.Player.Computer); - //just to make the computer a little less predictable - land.shuffle(); + if (type.contains("Basic")) { + // No need for special sorting for basic land + // just shuffle to make the computer a little less predictable + land.shuffle(); + } else { + Comparator aiLandComparator = new Comparator() + { + private int scoreLand(Card a) { + String valakutName = "Valakut, the Molten Pinnacle"; + + int theScore = 0; + if (!a.isBasicLand()) { + // favor non-basic land + theScore++; + if (a.getName().contains(valakutName)) { + // TODO: Add names of other special lands + theScore++; + } + } + return theScore; + } + public int compare(Card a, Card b) + { + int aScore = scoreLand(a); + int bScore = scoreLand(b); + return bScore - aScore; + } // compare + };//Comparator + + // Prioritize the land somewhat + land.sort(aiLandComparator); + } //3 branches: 1-no land in deck, 2-one land in deck, 3-two or more land in deck if(land.size() != 0) { //branch 2 - at least 1 land in library @@ -1765,20 +1821,28 @@ public class GameAction { } } - private void humanSearchTwoBasicLand(String Zone1, boolean tapFirstLand, String Zone2, boolean tapSecondLand) { + private void humanSearchTwoLand(String type, String Zone1, boolean tapFirstLand, String Zone2, boolean tapSecondLand) { PlayerZone firstZone = AllZone.getZone(Zone1, Constant.Player.Human); PlayerZone library = AllZone.getZone(Constant.Zone.Library, Constant.Player.Human); CardList list = new CardList(library.getCards()); - list = list.getType("Basic"); + list = list.getType(type); //3 branches: 1-no land in deck, 2-one land in deck, 3-two or more land in deck //branch 1 if(list.size() == 0) return; + // Check whether we were only asked for one land, and adjust the prompt accordingly + boolean onlyOneLand = (Zone2.trim().length() == 0); + String firstPrompt; + if (onlyOneLand) + firstPrompt = new String("Choose a land"); + else + firstPrompt = new String("Choose first land"); + //branch 2 - Object o = AllZone.Display.getChoiceOptional("Choose first land", list.toArray()); + Object o = AllZone.Display.getChoiceOptional(firstPrompt, list.toArray()); if(o != null) { Card c = (Card) o; list.remove(c); @@ -1789,7 +1853,7 @@ public class GameAction { firstZone.add(c); }//if - if ((list.size() == 0) || Zone2.trim().length() == 0) return; + if ((list.size() == 0) || onlyOneLand) return; //branch 3 o = AllZone.Display.getChoiceOptional("Choose second land", list.toArray()); if(o != null) { diff --git a/src/forge/GameActionUtil.java b/src/forge/GameActionUtil.java index 81caf695738..06b6f932bc8 100644 --- a/src/forge/GameActionUtil.java +++ b/src/forge/GameActionUtil.java @@ -3742,6 +3742,118 @@ public class GameActionUtil { else if(c.getName().equals("Bloodghast")) landfall_Bloodghast(c); else if(c.getName().equals("Avenger of Zendikar")) landfall_Avenger_of_Zendikar(c); } + + private static boolean checkValakutCondition(Card valakutCard, Card mtn) { + // Get a list of all mountains + CardList mountainList = AllZoneUtil.getPlayerTypeInPlay(valakutCard.getController(), + "Mountain"); + // Don't count the one that just came into play + if (mountainList.contains(mtn)) + mountainList.remove(mtn); + + // Do not activate if at least 5 other mountains are not present. + if (mountainList.size() < 5) + return false; + else + return true; + + } + // Returns true if the routine found enough mountains to activate the effect + // Returns false otherwise + // This lets the calling routine break if a player has multiple Valakut in play + public static boolean executeValakutEffect(final Card valakutCard, final Card mtn) { + + if (!checkValakutCondition(valakutCard, mtn)) + return false; // Tell the calling routine there aren't enough mountains, don't call again + + SpellAbility DamageTgt = new Spell(valakutCard) { + + private static final long serialVersionUID = -7360567876931046530L; + + public boolean canPlayAI() { + return getCreature().size() != 0 || AllZone.Human_Life.getLife() < 10; + } + + public boolean canPlay() { + return true; + } + + CardList getCreature() { + //toughness of 3 + CardList list = CardFactoryUtil.AI_getHumanCreature(3, valakutCard, true); + list = list.filter(new CardListFilter() { + public boolean addCard(Card c) { + //only get 1/1 flyers or 2/1 or bigger creatures + return (2 <= c.getNetAttack()) || c.getKeyword().contains("Flying"); + } + }); + return list; + }//getCreature() + + @Override + public void chooseTargetAI() { + boolean targetHuman; + // Get a list of all creatures Valakut could destroy + CardList list = getCreature(); + + CardList listValakut = list.filter(new CardListFilter() { + public boolean addCard(Card c) { + return c.getName().contains("Valakut, the Molten Pinnacle"); + } + }); + + int lifeThreshold = Math.max( 3 * listValakut.size(), 6); + if ( (AllZone.Human_Life.getLife() < lifeThreshold) || list.isEmpty()) { + targetHuman = true; + } else { + // Remove any creatures that have been targeted by other Valakuts + for (int ix = 0; ix < AllZone.Stack.size(); ix++) { + SpellAbility sa = AllZone.Stack.peek(ix); + if (sa.getSourceCard().getName().contains("Valakut, the Molten Pinnacle")) { + Card target = sa.getTargetCard(); + if ((target != null) && list.contains(target)) { + list.remove(target); + } + } + } + if (list.isEmpty()) { + targetHuman = true; + } else { + targetHuman = false; + } + } + + + if(targetHuman) setTargetPlayer(Constant.Player.Human); + else { + list.shuffle(); + setTargetCard(list.get(0)); + } + }//chooseTargetAI() + + @Override + public void resolve() { + if (!checkValakutCondition(valakutCard, mtn)) + return; + if(getTargetCard() != null) { + if(AllZone.GameAction.isCardInPlay(getTargetCard()) + && CardFactoryUtil.canTarget(valakutCard, getTargetCard())) getTargetCard().addDamage(3, + valakutCard); + } else AllZone.GameAction.getPlayerLife(getTargetPlayer()).subtractLife(3); + }//resolve() + + }; + DamageTgt.setManaCost(new String("0")); + DamageTgt.setStackDescription("Valakut, the Molten Pinnacle deals 3 damage to target creature or player."); + if (valakutCard.getController() == Constant.Player.Human) { + AllZone.InputControl.setInput(CardFactoryUtil.input_targetCreaturePlayer(DamageTgt, true, true)); + } else { + DamageTgt.chooseTargetAI(); + AllZone.Stack.add(DamageTgt); + } + return true; // Tell the calling routine it's okay to call again if there are other Valakuts in play + } + private static boolean showLandfallDialog(Card c) { String[] choices = {"Yes", "No"}; diff --git a/src/forge/PlayerZone_ComesIntoPlay.java b/src/forge/PlayerZone_ComesIntoPlay.java index 68d9ed263cd..1318f9c89ab 100644 --- a/src/forge/PlayerZone_ComesIntoPlay.java +++ b/src/forge/PlayerZone_ComesIntoPlay.java @@ -75,6 +75,12 @@ public class PlayerZone_ComesIntoPlay extends DefaultPlayerZone { CardList list = new CardList(play.getCards()); CardList graveList = new CardList(grave.getCards()); + CardList listValakut = list.filter(new CardListFilter() { + public boolean addCard(Card c) { + return c.getName().contains("Valakut, the Molten Pinnacle"); + } + }); + list = list.filter(new CardListFilter() { public boolean addCard(Card c) { return c.getKeyword().contains("Landfall") || @@ -96,6 +102,16 @@ public class PlayerZone_ComesIntoPlay extends DefaultPlayerZone { GameActionUtil.executeLandfallEffects(list.get(i)); } + // Check for a mountain + if (!listValakut.isEmpty() && c.getType().contains("Mountain") ) { + for (int i = 0; i < listValakut.size(); i++) { + boolean b = GameActionUtil.executeValakutEffect(listValakut.get(i),c); + if (!b) { + // Not enough mountains to activate Valakut -- stop the loop + break; + } + } + } }//isLand() //hack to make tokens trigger ally effects: diff --git a/src/forge/SpellAbility.java b/src/forge/SpellAbility.java index 7080cb729f8..578fc3a7efe 100644 --- a/src/forge/SpellAbility.java +++ b/src/forge/SpellAbility.java @@ -31,6 +31,7 @@ public abstract class SpellAbility { private boolean flashBackAbility = false; private boolean multiKicker = false; private boolean xCost = false; + private boolean kickerAbility = false; private Input beforePayMana; private Input afterResolve; @@ -296,7 +297,12 @@ public abstract class SpellAbility { public boolean isFlashBackAbility() { return flashBackAbility; } - + public void setKickerAbility(boolean kab) { + this.kickerAbility=kab; + } + public boolean isKickerAbility() { + return kickerAbility; + } // Only used by Ability_Reflected_Mana, because the user has an option to cancel the input. // Most spell abilities and even most mana abilities do not need to use this. public boolean wasCancelled() {