diff --git a/.gitattributes b/.gitattributes index 976d7ae02ab..68ada3ff5e9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1764,6 +1764,7 @@ res/cardsfolder/c/chimeric_mass.txt svneol=native#text/plain res/cardsfolder/c/chimeric_sphere.txt -text res/cardsfolder/c/chimeric_staff.txt svneol=native#text/plain res/cardsfolder/c/chimney_imp.txt svneol=native#text/plain +res/cardsfolder/c/chisei_heart_of_oceans.txt -text res/cardsfolder/c/chittering_rats.txt svneol=native#text/plain res/cardsfolder/c/chlorophant.txt -text res/cardsfolder/c/cho_arrim_alchemist.txt -text @@ -2923,6 +2924,7 @@ res/cardsfolder/d/dream_coat.txt -text svneol=unset#text/plain res/cardsfolder/d/dream_fighter.txt -text res/cardsfolder/d/dream_fracture.txt svneol=native#text/plain res/cardsfolder/d/dream_halls.txt -text +res/cardsfolder/d/dream_leash.txt -text res/cardsfolder/d/dream_prowler.txt svneol=native#text/plain res/cardsfolder/d/dream_salvage.txt -text res/cardsfolder/d/dream_stalker.txt svneol=native#text/plain @@ -10399,6 +10401,7 @@ res/cardsfolder/s/spined_wurm.txt svneol=native#text/plain res/cardsfolder/s/spineless_thug.txt svneol=native#text/plain res/cardsfolder/s/spinerock_knoll.txt -text res/cardsfolder/s/spinneret_sliver.txt svneol=native#text/plain +res/cardsfolder/s/spinning_darkness.txt -text res/cardsfolder/s/spiny_starfish.txt -text res/cardsfolder/s/spiraling_duelist.txt svneol=native#text/plain res/cardsfolder/s/spiraling_embers.txt svneol=native#text/plain @@ -11233,6 +11236,7 @@ res/cardsfolder/t/thought_devourer.txt svneol=native#text/plain res/cardsfolder/t/thought_eater.txt svneol=native#text/plain res/cardsfolder/t/thought_gorger.txt -text res/cardsfolder/t/thought_hemorrhage.txt -text svneol=unset#text/plain +res/cardsfolder/t/thought_lash.txt -text res/cardsfolder/t/thought_nibbler.txt svneol=native#text/plain res/cardsfolder/t/thought_prison.txt -text svneol=unset#text/plain res/cardsfolder/t/thought_reflection.txt -text diff --git a/res/cardsfolder/c/chisei_heart_of_oceans.txt b/res/cardsfolder/c/chisei_heart_of_oceans.txt new file mode 100644 index 00000000000..c302e354165 --- /dev/null +++ b/res/cardsfolder/c/chisei_heart_of_oceans.txt @@ -0,0 +1,11 @@ +Name:Chisei, Heart of Oceans +ManaCost:2 U U +Types:Legendary Creature Spirit +PT:4/4 +K:Flying +T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigSac | TriggerDescription$ At the beginning of your upkeep, sacrifice CARDNAME unless you remove a counter from a permanent you control. +SVar:TrigSac:AB$ Sacrifice | Cost$ 0 | Defined$ Self | UnlessPayer$ You | UnlessCost$ RemoveAnyCounter<1/Permanent.YouCtrl/a permanent you control> +#AI only removes negative counters +SVar:RemAIDeck:True +SVar:Picture:http://www.wizards.com/global/images/magic/general/chisei_heart_of_oceans.jpg +Oracle:Flying\nAt the beginning of your upkeep, sacrifice Chisei, Heart of Oceans unless you remove a counter from a permanent you control. diff --git a/res/cardsfolder/d/dream_leash.txt b/res/cardsfolder/d/dream_leash.txt new file mode 100644 index 00000000000..2435a86bbc8 --- /dev/null +++ b/res/cardsfolder/d/dream_leash.txt @@ -0,0 +1,11 @@ +Name:Dream Leash +ManaCost:3 U U +Types:Enchantment Aura +Text:You can't choose an untapped permanent as CARDNAME's target as you cast CARDNAME. +K:Enchant permanent +K:SpellCantTarget:Permanent.untapped +A:SP$ Attach | Cost$ 3 U U | ValidTgts$ Permanent | AILogic$ GainControl +S:Mode$ Continuous | Affected$ Card.EnchantedBy | GainControl$ You | Description$ You control enchanted creature. +SVar:PlayMain1:TRUE +SVar:Picture:http://www.wizards.com/global/images/magic/general/dream_leash.jpg +Oracle:Enchant permanent\nYou can't choose an untapped permanent as Dream Leash's target as you cast Dream Leash.\nYou control enchanted permanent. diff --git a/res/cardsfolder/s/spinning_darkness.txt b/res/cardsfolder/s/spinning_darkness.txt new file mode 100644 index 00000000000..76fb9130f8a --- /dev/null +++ b/res/cardsfolder/s/spinning_darkness.txt @@ -0,0 +1,9 @@ +Name:Spinning Darkness +ManaCost:4 B B +Types:Instant +SVar:AltCost:Cost$ ExileFromGrave<3/Card.Black+FromTopGrave> | Description$ You may exile the top three black cards of your graveyard rather than pay CARDNAME's mana cost. +A:SP$ DealDamage | Cost$ 4 B B | ValidTgts$ Creature.nonBlack | TgtPrompt$ Select target nonblack creature | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 3 damage to target nonblack creature. You gain 3 life. +SVar:DBGainLife:DB$ GainLife | LifeAmount$ 3 +SVar:RemAIDeck:True +SVar:Picture:http://www.wizards.com/global/images/magic/general/spinning_darkness.jpg +Oracle:You may exile the top three black cards of your graveyard rather than pay Spinning Darkness's mana cost.\nSpinning Darkness deals 3 damage to target nonblack creature. You gain 3 life. diff --git a/res/cardsfolder/t/thought_lash.txt b/res/cardsfolder/t/thought_lash.txt new file mode 100644 index 00000000000..e443c8dfb85 --- /dev/null +++ b/res/cardsfolder/t/thought_lash.txt @@ -0,0 +1,11 @@ +Name:Thought Lash +ManaCost:2 U U +Types:Enchantment +K:Cumulative upkeep:ExileFromTop<1/Card>:Exile the top card of your library. +T:Mode$ PayCumulativeUpkeep | ValidCard$ Card.Self | Paid$ False | Execute$ TrigExileAll | TriggerDescription$ When a player doesn't pay CARDNAME's cumulative upkeep, that player exiles all cards from his or her library. +SVar:TrigExileAll:AB$ ChangeZoneAll | Cost$ 0 | Origin$ Library | Destination$ Exile | ChangeType$ Card.YouCtrl +A:AB$ PreventDamage | Cost$ ExileFromTop<1/Card> | Defined$ You | Amount$ 1 | SpellDescription$ Prevent the next 1 damage that would be dealt to you this turn. +SVar:RemAIDeck:True +SVar:RemRandomDeck:True +SVar:Picture:http://www.wizards.com/global/images/magic/general/thought_lash.jpg +Oracle:Cumulative upkeep-Exile the top card of your library. (At the beginning of your upkeep, put an age counter on this permanent, then sacrifice it unless you pay its upkeep cost for each age counter on it.)\nWhen a player doesn't pay Thought Lash's cumulative upkeep, that player exiles all cards from his or her library.\nExile the top card of your library: Prevent the next 1 damage that would be dealt to you this turn. diff --git a/src/main/java/forge/Card.java b/src/main/java/forge/Card.java index a69470959eb..c97197883bb 100644 --- a/src/main/java/forge/Card.java +++ b/src/main/java/forge/Card.java @@ -2013,20 +2013,11 @@ public class Card extends GameEntity implements Comparable { || keyword.startsWith("PreventAllDamageBy") || keyword.startsWith("CantBlock") || keyword.startsWith("CantBeBlockedBy") - || keyword.startsWith("CantEquip")) { + || keyword.startsWith("CantEquip") + || keyword.startsWith("SpellCantTarget")) { continue; } - if (keyword.startsWith("CostChange")) { - final String[] k = keyword.split(":"); - if (k.length > 8) { - sbLong.append(k[8]).append("\r\n"); - } - /*} else if (keyword.startsWith("AdjustLandPlays")) { - final String[] k = keyword.split(":"); - if (k.length > 3) { - sbLong.append(k[3]).append("\r\n"); - }*/ - } else if (keyword.startsWith("etbCounter")) { + if (keyword.startsWith("etbCounter")) { final String[] p = keyword.split(":"); final StringBuilder s = new StringBuilder(); if (p.length > 4) { @@ -2345,18 +2336,6 @@ public class Card extends GameEntity implements Comparable { // keyword descriptions for (int i = 0; i < kw.size(); i++) { final String keyword = kw.get(i); - if (keyword.startsWith("CostChange")) { - final String[] k = keyword.split(":"); - if (k.length > 8) { - sb.append(k[8]).append("\r\n"); - } - } - /*if (keyword.startsWith("AdjustLandPlays")) { - final String[] k = keyword.split(":"); - if (k.length > 3) { - sb.append(k[3]).append("\r\n"); - } - }*/ if ((keyword.startsWith("Ripple") && !sb.toString().contains("Ripple")) || (keyword.startsWith("Dredge") && !sb.toString().contains("Dredge")) || (keyword.startsWith("Madness") && !sb.toString().contains("Madness")) @@ -7881,10 +7860,10 @@ public class Card extends GameEntity implements Comparable { if (this.isPhasedOut()) { return false; } + + final Card source = sa.getSourceCard(); if (this.getKeyword() != null) { - final Card source = sa.getSourceCard(); - for (String kw : this.getKeyword()) { if (kw.equals("Shroud")) { return false; @@ -7941,6 +7920,15 @@ public class Card extends GameEntity implements Comparable { } } } + if (sa.isSpell() && source.hasStartOfKeyword("SpellCantTarget")) { + final int keywordPosition = source.getKeywordPosition("SpellCantTarget"); + final String parse = source.getKeyword().get(keywordPosition).toString(); + final String[] k = parse.split(":"); + final String[] restrictions = k[1].split(","); + if (this.isValid(restrictions, source.getController(), source)) { + return false; + } + } return true; } diff --git a/src/main/java/forge/card/cost/CostExile.java b/src/main/java/forge/card/cost/CostExile.java index 3849bb1d11c..fac07f9b675 100644 --- a/src/main/java/forge/card/cost/CostExile.java +++ b/src/main/java/forge/card/cost/CostExile.java @@ -18,6 +18,7 @@ package forge.card.cost; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; import forge.Card; @@ -155,10 +156,13 @@ public class CostExile extends CostPartWithList { final Player activator = ability.getActivatingPlayer(); final Card source = ability.getSourceCard(); final Game game = activator.getGame(); + String type = this.getType(); List typeList = new ArrayList(); - if (this.getType().equals("All")) { + if (type.equals("All")) { return true; // this will always work + } else if (type.contains("FromTopGrave")) { + type = type.replace("FromTopGrave", ""); } if (this.getFrom().equals(ZoneType.Stack)) { for (SpellAbilityStackInstance si : game.getStack()) { @@ -172,7 +176,7 @@ public class CostExile extends CostPartWithList { } } if (!this.payCostFromSource()) { - typeList = CardLists.getValidCards(typeList, this.getType().split(";"), activator, source); + typeList = CardLists.getValidCards(typeList, type.split(";"), activator, source); final Integer amount = this.convertAmount(); if ((amount != null) && (typeList.size() < amount)) { @@ -212,6 +216,13 @@ public class CostExile extends CostPartWithList { final Card source = ability.getSourceCard(); Integer c = this.convertAmount(); final Player activator = ability.getActivatingPlayer(); + String type = this.getType(); + boolean fromTopGrave = false; + if (type.contains("FromTopGrave")) { + type = type.replace("FromTopGrave", ""); + fromTopGrave = true; + } + List list; if (this.sameZone) { @@ -220,13 +231,13 @@ public class CostExile extends CostPartWithList { list = new ArrayList(activator.getCardsIn(this.getFrom())); } - if (this.getType().equals("All")) { + if (type.equals("All")) { for (final Card card : list) { executePayment(ability, card); } return true; } - list = CardLists.getValidCards(list, this.getType().split(";"), activator, source); + list = CardLists.getValidCards(list, type.split(";"), activator, source); if (c == null) { final String sVar = ability.getSVar(amount); // Generalize this @@ -236,12 +247,11 @@ public class CostExile extends CostPartWithList { c = AbilityUtils.calculateAmount(source, amount, ability); } } - - + if (this.payCostFromSource()) return activator.getZone(from).contains(source) && GuiDialog.confirm(source, source.getName() + " - Exile?") && executePayment(ability, source); - List validCards = CardLists.getValidCards(activator.getCardsIn(from), getType().split(";"), activator, source); + List validCards = CardLists.getValidCards(activator.getCardsIn(from), type.split(";"), activator, source); if (this.from == ZoneType.Battlefield || this.from == ZoneType.Hand) { InputSelectCards inp = new InputSelectCardsFromList(c, c, validCards); inp.setMessage("Exile %d card(s) from your" + from ); @@ -252,6 +262,7 @@ public class CostExile extends CostPartWithList { if (this.from == ZoneType.Stack) return exileFromStack(ability, c); if (this.from == ZoneType.Library) return exileFromTop(ability, c); + if (fromTopGrave) return exileFromTopGraveType(ability, c, validCards); if (!this.sameZone) return exileFromMiscZone(ability, c, validCards); @@ -404,11 +415,8 @@ public class CostExile extends CostPartWithList { } } - // Exile - // ExileFromHand + // ExileFromGrave - // ExileFromTop (of library) - // ExileSameGrave private boolean exileFromMiscZone(SpellAbility sa, int nNeeded, List typeList) { for (int i = 0; i < nNeeded; i++) { @@ -428,6 +436,18 @@ public class CostExile extends CostPartWithList { return true; } + private boolean exileFromTopGraveType(SpellAbility sa, int nNeeded, List typeList) { + Collections.reverse(typeList); + for (int i = 0; i < nNeeded; i++) { + if (typeList.isEmpty()) { + return false; + } + final Card c = typeList.get(0); + typeList.remove(c); + executePayment(sa, c); + } + return true; + } /* (non-Javadoc) * @see forge.card.cost.CostPartWithList#executePayment(forge.card.spellability.SpellAbility, forge.Card) */ @@ -456,9 +476,10 @@ public class CostExile extends CostPartWithList { if (this.getType().equals("All")) { return new PaymentDecision(new ArrayList(ai.getCardsIn(this.getFrom()))); + } else if (this.getType().contains("FromTopGrave")) { + return null; } - - + Integer c = this.convertAmount(); if (c == null) { final String sVar = ability.getSVar(this.getAmount()); diff --git a/src/main/java/forge/game/player/HumanPlay.java b/src/main/java/forge/game/player/HumanPlay.java index 3264d52f705..56ab45a126a 100644 --- a/src/main/java/forge/game/player/HumanPlay.java +++ b/src/main/java/forge/game/player/HumanPlay.java @@ -2,9 +2,12 @@ package forge.game.player; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; +import com.google.common.base.Predicate; + import forge.Card; import forge.CardLists; import forge.CardPredicates.Presets; @@ -30,6 +33,7 @@ import forge.card.cost.CostPayLife; import forge.card.cost.CostPayment; import forge.card.cost.CostPutCardToLib; import forge.card.cost.CostPutCounter; +import forge.card.cost.CostRemoveAnyCounter; import forge.card.cost.CostRemoveCounter; import forge.card.cost.CostReturn; import forge.card.cost.CostReveal; @@ -375,17 +379,23 @@ public class HumanPlay { else if (part instanceof CostPutCounter) { CounterType counterType = ((CostPutCounter) part).getCounter(); int amount = getAmountFromPartX(part, source, sourceAbility); - - if (false == 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 (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 (!GuiDialog.confirm(source, "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); + boolean hasPaid = payCostPart(sourceAbility, (CostPartWithList)part, amount, list, "add a counter." + orString); + if(!hasPaid) return false; } - - if (false == GuiDialog.confirm(source, "Do you want to put " + Lang.nounWithAmount(amount, counterType.getName() + " counter") + " on " + source + "?")) - return false; - - source.addCounter(counterType, amount, false); } else if (part instanceof CostRemoveCounter) { @@ -400,7 +410,57 @@ public class HumanPlay { 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 (!GuiDialog.confirm(source, "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; + InputSelectCards inp = new InputSelectCardsFromList(1, 1, list); + inp.setMessage("Select a card to remove a counter"); + inp.setCancelAllowed(true); + Singletons.getControl().getInputQueue().setInputAndWait(inp); + if (inp.hasCancelled()) + continue; + Card selected = inp.getSelected().get(0); + 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 prompt = "Select type counters to remove"; + counterType = GuiChoose.one(prompt, typeChoices); + } else { + counterType = typeChoices.get(0); + } + selected.subtractCounter(counterType, 1); + amount--; + } + } + else if (part instanceof CostExile) { if ("All".equals(part.getType())) { if (false == GuiDialog.confirm(source, "Do you want to exile all cards in your graveyard?")) @@ -417,7 +477,16 @@ public class HumanPlay { final int nNeeded = getAmountFromPart(costPart, source, sourceAbility); if (list.size() < nNeeded) return false; - + if (from == ZoneType.Library) { + if (!GuiDialog.confirm(source, "Do you want to exile card(s) from you 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);