diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index a1a3168b5ed..c7f6a1a36ab 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -1,26 +1,8 @@ package forge.ai; -import java.util.ArrayList; -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.Set; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; - import com.google.common.base.Predicate; import com.google.common.base.Predicates; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Iterables; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; - +import com.google.common.collect.*; import forge.ai.ability.AnimateAi; import forge.card.ColorSet; import forge.card.MagicColor; @@ -34,20 +16,10 @@ import forge.game.GameActionUtil; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; -import forge.game.card.Card; -import forge.game.card.CardCollection; -import forge.game.card.CardCollectionView; -import forge.game.card.CardLists; -import forge.game.card.CardPredicates; -import forge.game.card.CardUtil; -import forge.game.card.CounterEnumType; +import forge.game.card.*; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; -import forge.game.cost.Cost; -import forge.game.cost.CostAdjustment; -import forge.game.cost.CostPartMana; -import forge.game.cost.CostPayEnergy; -import forge.game.cost.CostPayment; +import forge.game.cost.*; import forge.game.keyword.Keyword; import forge.game.mana.Mana; import forge.game.mana.ManaCostBeingPaid; @@ -67,6 +39,10 @@ import forge.game.trigger.TriggerType; import forge.game.zone.ZoneType; import forge.util.MyRandom; import forge.util.TextUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.*; public class ComputerUtilMana { private final static boolean DEBUG_MANA_PAYMENT = false; @@ -272,28 +248,87 @@ public class ComputerUtilMana { public static SpellAbility chooseManaAbility(ManaCostBeingPaid cost, SpellAbility sa, Player ai, ManaCostShard toPay, Collection saList, boolean checkCosts) { + Card saHost = sa.getHostCard(); + + // CastTotalManaSpent (AIPreference:ManaFrom$Type or AIManaPref$ Type) + String manaSourceType = ""; + if (saHost.hasSVar("AIPreference")) { + String condition = saHost.getSVar("AIPreference"); + if (condition.startsWith("ManaFrom")) { + manaSourceType = TextUtil.split(condition, '$')[1]; + } + } else if (sa.hasParam("AIManaPref")) { + manaSourceType = sa.getParam("AIManaPref"); + } + if (manaSourceType != "") { + List filteredList = Lists.newArrayList(saList); + switch (manaSourceType) { + case "Snow": + filteredList.sort(new Comparator() { + @Override + public int compare(SpellAbility ab1, SpellAbility ab2) { + return ab1.getHostCard() != null && ab1.getHostCard().isSnow() + && ab2.getHostCard() != null && !ab2.getHostCard().isSnow() ? -1 : 1; + } + }); + saList = filteredList; + break; + case "Treasure": + // Try to spend only one Treasure if possible + filteredList.sort(new Comparator() { + @Override + public int compare(SpellAbility ab1, SpellAbility ab2) { + return ab1.getHostCard() != null && ab1.getHostCard().getType().hasSubtype("Treasure") + && ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1; + } + }); + SpellAbility first = filteredList.get(0); + if (first.getHostCard() != null && first.getHostCard().getType().hasSubtype("Treasure")) { + saList.remove(first); + List updatedList = Lists.newArrayList(); + updatedList.add(first); + updatedList.addAll(saList); + saList = updatedList; + } + break; + case "TreasureMax": + // Ok to spend as many Treasures as possible + filteredList.sort(new Comparator() { + @Override + public int compare(SpellAbility ab1, SpellAbility ab2) { + return ab1.getHostCard() != null && ab1.getHostCard().getType().hasSubtype("Treasure") + && ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1; + } + }); + saList = filteredList; + break; + default: + break; + } + } + for (final SpellAbility ma : saList) { - if (ma.getHostCard() == sa.getHostCard()) { + if (ma.getHostCard() == saHost) { continue; } - if (sa.getHostCard() != null) { + if (saHost != null) { if (sa.getApi() == ApiType.Animate) { // For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana - if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined")) - && ma.getHostCard() == sa.getHostCard().getEnchantingCard() + if (saHost.isAura() && "Enchanted".equals(sa.getParam("Defined")) + && ma.getHostCard() == saHost.getEnchantingCard() && ma.getPayCosts().hasTapCost()) { continue; } // If a manland was previously animated this turn, do not tap it to animate another manland - if (sa.getHostCard().isLand() && ma.getHostCard().isLand() + if (saHost.isLand() && ma.getHostCard().isLand() && ai.getController().isAI() && AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) { continue; } } else if (sa.getApi() == ApiType.Pump) { - if ((sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) + if ((saHost.isInstant() || saHost.isSorcery()) && ma.getHostCard().isCreature() && ai.getController().isAI() && ma.getPayCosts().hasTapCost() @@ -303,7 +338,7 @@ public class ComputerUtilMana { continue; } } else if (sa.getApi() == ApiType.Attach - && "AvoidPayingWithAttachTarget".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) { + && "AvoidPayingWithAttachTarget".equals(saHost.getSVar("AIPaymentPreference"))) { // For cards like Genju of the Cedars, make sure we're not attaching to the same land that will // be tapped to pay its own cost if there's another untapped land like that available if (ma.getHostCard().equals(sa.getTargetCard())) { @@ -320,7 +355,7 @@ public class ComputerUtilMana { // Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability // to attempt to make the spell uncounterable when possible. if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls") - && sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) { + && saHost.getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) { if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) { // Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability continue; diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 7a5583272a1..3be56b8bdb2 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -1832,25 +1832,26 @@ public class AbilityUtils { return doXMath(list.size(), expr, c, ctb); } - if (sq[0].startsWith("CastTotalManaSpent")) { - return doXMath(c.getCastSA() != null ? c.getCastSA().getTotalManaSpent() : 0, expr, c, ctb); + if (sq[0].equals("ResolvedThisTurn")) { + return doXMath(sa.getResolvedThisTurn(), expr, c, ctb); } - if (sq[0].equals("CastTotalSnowManaSpent")) { + if (sq[0].startsWith("TotalManaSpent ")) { + final String[] k = sq[0].split(" "); int v = 0; - if (c.getCastSA() != null) { - for (Mana m : c.getCastSA().getPayingMana()) { - if (m.isSnow()) { - v += 1; + if (sa.getRootAbility().getPayingMana() != null) { + for (Mana m : sa.getRootAbility().getPayingMana()) { + Card source = m.getSourceCard(); + if (source != null) { + if (source.isValid(k[1].split(","), player, c, sa)) { + v += 1; + } } } } return doXMath(v, expr, c, ctb); } - if (sq[0].equals("ResolvedThisTurn")) { - return doXMath(sa.getResolvedThisTurn(), expr, c, ctb); - } } else { // fallback if ctb isn't a spellability if (sq[0].startsWith("LastStateBattlefield")) { @@ -1870,8 +1871,29 @@ public class AbilityUtils { if (sq[0].startsWith("xPaid")) { return doXMath(c.getXManaCostPaid(), expr, c, ctb); } + } // end SpellAbility + if (sq[0].equals("CastTotalManaSpent")) { + return doXMath(c.getCastSA() != null ? c.getCastSA().getTotalManaSpent() : 0, expr, c, ctb); + } + + if (sq[0].startsWith("CastTotalManaSpent ")) { + final String[] k = sq[0].split(" "); + int v = 0; + if (c.getCastSA() != null) { + for (Mana m : c.getCastSA().getPayingMana()) { + Card source = m.getSourceCard(); + if (source != null) { + if (source.isValid(k[1].split(","), player, c, ctb)) { + v += 1; + } + } + } + } + return doXMath(v, expr, c, ctb); + } + // Count$TargetedLifeTotal (targeted player's life total) // Not optimal but since xCount doesn't take SAs, we need to replicate while we have it // Probably would be best if xCount took an optional SA to use in these circumstances diff --git a/forge-gui/res/cardsfolder/b/berg_strider.txt b/forge-gui/res/cardsfolder/b/berg_strider.txt index 66be1984604..1bfd7e73cb6 100644 --- a/forge-gui/res/cardsfolder/b/berg_strider.txt +++ b/forge-gui/res/cardsfolder/b/berg_strider.txt @@ -5,6 +5,7 @@ PT:4/4 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigTap | TriggerDescription$ When CARDNAME enters the battlefield, tap target artifact or creature an opponent controls. If {S} was spent to cast this spell, that permanent doesn't untap during its controller's next untap step. ({S} is mana from a snow source.) SVar:TrigTap:DB$ Tap | ValidTgts$ Artifact.OppCtrl,Creature.OppCtrl | TgtPrompt$ Choose target artifact or creature an opponent controls | SubAbility$ DBPump SVar:DBPump:DB$ Pump | Defined$ Targeted | ConditionCheckSVar$ S | ConditionSVarCompare$ GE1 | KW$ HIDDEN This card doesn't untap during your next untap step. | Duration$ Permanent -SVar:S:Count$CastTotalSnowManaSpent +SVar:S:Count$CastTotalManaSpent Snow +SVar:AIPreference:ManaFrom$Snow DeckNeeds:Type$Snow Oracle:When Berg Strider enters the battlefield, tap target artifact or creature an opponent controls. If {S} was spent to cast this spell, that permanent doesn't untap during its controller's next untap step. ({S} is mana from a snow source.) diff --git a/forge-gui/res/cardsfolder/b/blessing_of_frost.txt b/forge-gui/res/cardsfolder/b/blessing_of_frost.txt index d437692d7f0..48a734344ec 100644 --- a/forge-gui/res/cardsfolder/b/blessing_of_frost.txt +++ b/forge-gui/res/cardsfolder/b/blessing_of_frost.txt @@ -2,9 +2,10 @@ Name:Blessing of Frost ManaCost:3 G Types:Snow Sorcery A:SP$ PutCounter | Cost$ 3 G | Choices$ Creature.YouCtrl | ChoiceTitle$ Choose any number of creatures you control to distribute counters to | CounterType$ P1P1 | CounterNum$ X | ChoiceAmount$ X | MinChoiceAmount$ 1 | DividedAsYouChoose$ X | SubAbility$ DBDraw | SpellDescription$ Distribute X +1/+1 counters among any number of creatures you control, where X is the amount of {S} spent to cast this spell. -SVar:X:Count$CastTotalSnowManaSpent +SVar:X:Count$CastTotalManaSpent Snow SVar:DBDraw:DB$ Draw | NumCards$ Y | SpellDescription$ Then draw a card for each creature you control with power 4 or greater. SVar:Y:Count$Valid Creature.powerGE4+YouCtrl +SVar:AIPreference:ManaFrom$Snow DeckHints:Type$Snow DeckHas:Ability$Counters Oracle:Distribute X +1/+1 counters among any number of creatures you control, where X is the amount of {S} spent to cast this spell. Then draw a card for each creature you control with power 4 or greater. diff --git a/forge-gui/res/cardsfolder/b/blood_on_the_snow.txt b/forge-gui/res/cardsfolder/b/blood_on_the_snow.txt index 20cb4296a07..6b023dd1ba2 100644 --- a/forge-gui/res/cardsfolder/b/blood_on_the_snow.txt +++ b/forge-gui/res/cardsfolder/b/blood_on_the_snow.txt @@ -5,7 +5,8 @@ A:SP$ Charm | Cost$ 4 B B | Choices$ DestroyCtrs,DestroyPWs | CharmNum$ 1 | Spel SVar:DestroyCtrs:DB$ DestroyAll | ValidCards$ Creature | SubAbility$ DBReturn | SpellDescription$ Destroy all creatures. SVar:DestroyPWs:DB$ DestroyAll | ValidCards$ Planeswalker | SubAbility$ DBReturn | SpellDescription$ Destroy all planeswalkers. SVar:DBReturn:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | Hidden$ True | Mandatory$ True | ChangeType$ Creature.YouOwn+cmcLEX,Planeswalker.YouOwn+cmcLEX | SpellDescription$ Then return a creature or planeswalker card with mana value X or less from your graveyard to the battlefield, where X is the amount of {S} spent to cast this spell. ({S} is mana from a snow source.) -SVar:X:Count$CastTotalSnowManaSpent +SVar:X:Count$CastTotalManaSpent Snow +SVar:AIPreference:ManaFrom$Snow AI:RemoveDeck:Random DeckHints:Type$Snow Oracle:Choose one —\n• Destroy all creatures.\n• Destroy all planeswalkers.\nThen return a creature or planeswalker card with mana value X or less from your graveyard to the battlefield, where X is the amount of {S} spent to cast this spell. ({S} is mana from a snow source.) diff --git a/forge-gui/res/cardsfolder/g/graven_lore.txt b/forge-gui/res/cardsfolder/g/graven_lore.txt index c99e6d282cb..d08140ff45f 100644 --- a/forge-gui/res/cardsfolder/g/graven_lore.txt +++ b/forge-gui/res/cardsfolder/g/graven_lore.txt @@ -3,6 +3,7 @@ ManaCost:3 U U Types:Snow Instant A:SP$ Scry | Cost$ 3 U U | ScryNum$ X | SubAbility$ DBDraw | SpellDescription$ Scry X, where X is the amount of {S} spent to cast this spell, then draw three cards. ({S} is mana from a snow source.) SVar:DBDraw:DB$ Draw | NumCards$ 3 -SVar:X:Count$CastTotalSnowManaSpent +SVar:X:Count$CastTotalManaSpent Snow +SVar:AIPreference:ManaFrom$Snow DeckNeeds:Type$Snow Oracle:Scry X, where X is the amount of {S} spent to cast this spell, then draw three cards. ({S} is mana from a snow source.) diff --git a/forge-gui/res/cardsfolder/s/search_for_glory.txt b/forge-gui/res/cardsfolder/s/search_for_glory.txt index f613937997f..f6717e21da9 100644 --- a/forge-gui/res/cardsfolder/s/search_for_glory.txt +++ b/forge-gui/res/cardsfolder/s/search_for_glory.txt @@ -3,7 +3,8 @@ ManaCost:2 W Types:Snow Sorcery A:SP$ ChangeZone | Cost$ 2 W | Origin$ Library | Destination$ Hand | ChangeType$ Permanent.Snow,Legendary,Saga | ChangeNum$ 1 | SubAbility$ DBGainLife | SpellDescription$ Search your library for a snow permanent card, a legendary card, or a Saga card, reveal it, put it into your hand, then shuffle. You gain 1 life for each {S} spent to cast this spell. SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ X -SVar:X:Count$CastTotalSnowManaSpent +SVar:X:Count$CastTotalManaSpent Snow +SVar:AIPreference:ManaFrom$Snow AI:RemoveDeck:Random DeckHints:Type$Legendary Oracle:Search your library for a snow permanent card, a legendary card, or a Saga card, reveal it, put it into your hand, then shuffle. You gain 1 life for each {S} spent to cast this spell. ({S} is mana from a snow source.) diff --git a/forge-gui/res/cardsfolder/t/tundra_fumarole.txt b/forge-gui/res/cardsfolder/t/tundra_fumarole.txt index 6096b3ccc46..05652d04d25 100644 --- a/forge-gui/res/cardsfolder/t/tundra_fumarole.txt +++ b/forge-gui/res/cardsfolder/t/tundra_fumarole.txt @@ -3,6 +3,7 @@ ManaCost:1 R R Types:Snow Sorcery A:SP$ DealDamage | Cost$ 1 R R | ValidTgts$ Creature,Planeswalker | TgtPrompt$ Select target creature or planeswalker. | NumDmg$ 4 | SubAbility$ DBColorlessMana | SpellDescription$ CARDNAME deals 4 damage to target creature or planeswalker. Add {C} for each {S} spent to cast this spell. Until end of turn, you don't lose this mana as steps and phases end. | StackDescription$ SpellDescription SVar:DBColorlessMana:DB$ Mana | Produced$ C | Amount$ X | PersistentMana$ True -SVar:X:Count$CastTotalSnowManaSpent +SVar:X:Count$CastTotalManaSpent Snow +SVar:AIPreference:ManaFrom$Snow DeckHints:Type$Snow Oracle:Tundra Fumarole deals 4 damage to target creature or planeswalker. Add {C} for each {S} spent to cast this spell. Until end of turn, you don't lose this mana as steps and phases end. ({S} is mana from a snow source.) diff --git a/forge-gui/res/cardsfolder/upcoming/forsworn_paladin.txt b/forge-gui/res/cardsfolder/upcoming/forsworn_paladin.txt new file mode 100644 index 00000000000..dea1392506f --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/forsworn_paladin.txt @@ -0,0 +1,10 @@ +Name:Forsworn Paladin +ManaCost:B +Types:Creature Human Knight +PT:1/1 +K:Menace +A:AB$ Token | Cost$ 1 B T PayLife<1> | TokenAmount$ 1 | TokenScript$ c_a_treasure_sac | TokenOwner$ You | SpellDescription$ Create a Treasure token. +A:AB$ Pump | Cost$ 2 B | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ +2 | SubAbility$ DBDeathtouch | AIManaPref$ Treasure | SpellDescription$ Target creature gets +2/+0 until end of turn. If mana from a Treasure was spent to activate this ability, that creature also gains deathtouch until end of turn. +SVar:DBDeathtouch:DB$ Pump | KW$ Deathtouch | Defined$ ParentTarget | ConditionCheckSVar$ TreasureCheck +SVar:TreasureCheck:Count$TotalManaSpent Treasure +Oracle:Menace\n{1}{B}, {T}, Pay 1 life: Create a Treasure token.\n{2}{B}: Target creature gets +2/+0 until end of turn. If mana from a Treasure was spent to activate this ability, that creature also gains deathtouch until end of turn. diff --git a/forge-gui/res/cardsfolder/upcoming/hired_hexblade.txt b/forge-gui/res/cardsfolder/upcoming/hired_hexblade.txt new file mode 100644 index 00000000000..679753d6c3b --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/hired_hexblade.txt @@ -0,0 +1,10 @@ +Name:Hired Hexblade +ManaCost:1 B +Types:Creature Elf Warlock +PT:2/2 +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | CheckSVar$ TreasureCheck | Execute$ TrigDraw | TriggerDescription$ When CARDNAME enters the battlefield, if mana from a treasure was spent to cast it, you draw a card and you lose 1 life. +SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ 1 | SubAbility$ DBLoseLife +SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 1 +SVar:TreasureCheck:Count$CastTotalManaSpent Treasure +SVar:AIPreference:ManaFrom$Treasure +Oracle:When Hired Hexblade enters the battlefield, if mana from a treasure was spent to cast it, you draw a card and you lose 1 life.