diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index cb839fcf745..39c1c6c8d80 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -782,15 +782,9 @@ public class AiController { return AiPlayDecision.CantAfford; } } - if (wardCost.hasSpecificCostType(CostPayLife.class)) { - int lifeToPay = wardCost.getCostPartByType(CostPayLife.class).convertAmount(); - if (lifeToPay > player.getLife() || (lifeToPay == player.getLife() && !player.cantLoseForZeroOrLessLife())) { - return AiPlayDecision.CantAfford; - } - } - if (wardCost.hasSpecificCostType(CostDiscard.class) - && wardCost.getCostPartByType(CostDiscard.class).convertAmount() > player.getCardsIn(ZoneType.Hand).size()) { - return AiPlayDecision.CantAfford; + SpellAbilityAi topAI = new SpellAbilityAi() {}; + if (!topAI.willPayCosts(player, sa , wardCost, host)) { + return AiPlayDecision.CostNotAcceptable; } } } diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index 1054469a22d..7ad1fd3632a 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -620,6 +620,9 @@ public abstract class GameState { game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated + // prevent interactions with objects from old state + game.copyLastState(); + // Set negative or zero life after state effects if need be, important for some puzzles that rely on // pre-setting negative life (e.g. PS_NEO4). for (int i = 0; i < playerStates.size(); i++) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java index a4ec4a79a96..5ac39d70207 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/SacrificeEffect.java @@ -64,16 +64,13 @@ public class SacrificeEffect extends SpellAbilityEffect { table.replaceCounterEffect(game, sa, true); - Cost cumCost = new Cost(sa.getParam("CumulativeUpkeep"), true); Cost payCost = new Cost(ManaCost.ZERO, true); int n = card.getCounters(CounterEnumType.AGE); - - // multiply cost - for (int i = 0; i < n; ++i) { - payCost.add(cumCost); + if (n > 0) { + Cost cumCost = new Cost(sa.getParam("CumulativeUpkeep"), true); + payCost.mergeTo(cumCost, n); } - sa.setCumulativeupkeep(true); game.updateLastStateForCard(card); StringBuilder sb = new StringBuilder(); diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index f2ecaae3e46..d9a2ad21d0f 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -883,7 +883,24 @@ public class Cost implements Serializable { return sb.toString(); } + public void mergeTo(Cost source, int amt) { + // multiply to create the full cost + if (amt > 1) { + // to double itself we need to work on a copy + Cost sourceCpy = source.copy(); + for (int i = 1; i < amt; ++i) { + // in theory setAmount could be used instead but it depends on the cost complexity (probably not worth trying to determine that first) + source.add(sourceCpy); + } + } + // combine costs (these shouldn't mix together) + this.add(source, false); + } + public Cost add(Cost cost1) { + return add(cost1, true); + } + public Cost add(Cost cost1, boolean mergeAdditional) { CostPartMana costPart2 = this.getCostMana(); List toRemove = Lists.newArrayList(); for (final CostPart part : cost1.getCostParts()) { @@ -906,10 +923,10 @@ public class Cost implements Serializable { } else { costParts.add(0, new CostPartMana(oldManaCost.toManaCost(), r)); } - } else if (part instanceof CostDiscard || part instanceof CostDraw - || part instanceof CostAddMana || part instanceof CostPayLife - || part instanceof CostPutCounter || part instanceof CostTapType - || part instanceof CostExile) { + } else if (part instanceof CostPutCounter || (mergeAdditional && // below usually not desired because they're from different causes + (part instanceof CostDiscard || part instanceof CostDraw || + part instanceof CostAddMana || part instanceof CostPayLife || + part instanceof CostSacrifice || part instanceof CostTapType))) { boolean alreadyAdded = false; for (final CostPart other : costParts) { if ((other.getClass().equals(part.getClass()) || (part instanceof CostPutCounter && ((CostPutCounter)part).getCounter().is(CounterEnumType.LOYALTY))) && @@ -917,7 +934,7 @@ public class Cost implements Serializable { StringUtils.isNumeric(part.getAmount()) && StringUtils.isNumeric(other.getAmount())) { String amount = String.valueOf(part.convertAmount() + other.convertAmount()); - if (part instanceof CostPutCounter) { // path for Carth & Cumulative Upkeep + if (part instanceof CostPutCounter) { // CR 606.5 path for Carth if (other instanceof CostPutCounter && ((CostPutCounter)other).getCounter().equals(((CostPutCounter) part).getCounter())) { costParts.add(new CostPutCounter(amount, ((CostPutCounter) part).getCounter(), part.getType(), part.getTypeDescription())); } else if (other instanceof CostRemoveCounter && ((CostRemoveCounter)other).counter.is(CounterEnumType.LOYALTY)) { @@ -931,6 +948,8 @@ public class Cost implements Serializable { } else { continue; } + } else if (part instanceof CostSacrifice) { + costParts.add(new CostSacrifice(amount, part.getType(), part.getTypeDescription())); } else if (part instanceof CostDiscard) { costParts.add(new CostDiscard(amount, part.getType(), part.getTypeDescription())); } else if (part instanceof CostDraw) { @@ -942,12 +961,6 @@ public class Cost implements Serializable { costParts.add(new CostAddMana(amount, part.getType(), part.getTypeDescription())); } else if (part instanceof CostPayLife) { costParts.add(new CostPayLife(amount, part.getTypeDescription())); - } else if (part instanceof CostExile) { - ZoneType z = ((CostExile) part).getFrom(); - if (((CostExile) other).getFrom() != z) { - continue; - } - costParts.add(new CostExile(amount, part.getType(), part.getTypeDescription(), z)); } toRemove.add(other); alreadyAdded = true; diff --git a/forge-game/src/main/java/forge/game/cost/CostAdjustment.java b/forge-game/src/main/java/forge/game/cost/CostAdjustment.java index 8578adb2f4d..766d8f021a5 100644 --- a/forge-game/src/main/java/forge/game/cost/CostAdjustment.java +++ b/forge-game/src/main/java/forge/game/cost/CostAdjustment.java @@ -112,7 +112,6 @@ public class CostAdjustment { } final String scost = st.getParamOrDefault("Cost", "1"); - Cost part = new Cost(scost, sa.isAbility()); int count = 0; if (st.hasParam("ForEachShard")) { @@ -155,8 +154,9 @@ public class CostAdjustment { // Amount 1 as default count = 1; } - for (int i = 0; i < count; ++i) { - cost.add(part); + if (count > 0) { + Cost part = new Cost(scost, sa.isAbility()); + cost.mergeTo(part, count); } } @@ -177,7 +177,7 @@ public class CostAdjustment { originalCard.turnFaceDownNoUpdate(); isStateChangeToFaceDown = true; } - } // isSpell + } CardCollection cardsOnBattlefield = new CardCollection(game.getCardsIn(ZoneType.Battlefield)); cardsOnBattlefield.addAll(game.getCardsIn(ZoneType.Stack)); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 190f93bdb8a..ee015b41390 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -127,7 +127,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private boolean aftermath = false; - private boolean cumulativeupkeep = false; private boolean blessing = false; private Integer chapter = null; private boolean lastChapter = false; @@ -516,6 +515,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return this.hasParam("Ninjutsu"); } + public boolean isCumulativeupkeep() { + return hasParam("CumulativeUpkeep"); + } + public boolean isEpic() { AbilitySub sub = this.getSubAbility(); while (sub != null && !sub.hasParam("Epic")) { @@ -2140,13 +2143,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return ForgeScript.spellAbilityHasProperty(this, property, sourceController, source, spellAbility); } - public boolean isCumulativeupkeep() { - return cumulativeupkeep; - } - public void setCumulativeupkeep(boolean cumulativeupkeep0) { - cumulativeupkeep = cumulativeupkeep0; - } - // Return whether this spell tracks what color mana is spent to cast it for the sake of the effect public boolean tracksManaSpent() { if (hostCard == null || hostCard.getRules() == null) { return false; } diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantGainLosePayLife.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantGainLosePayLife.java index d84da37fb74..9d54f41cd6a 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantGainLosePayLife.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityCantGainLosePayLife.java @@ -72,7 +72,7 @@ public class StaticAbilityCantGainLosePayLife { } if (!stAb.matchesValidParam("ValidCause", cause)) { - return false; + continue; } if (applyCommonAbility(stAb, player)) { diff --git a/forge-gui/res/cardsfolder/l/land_grant.txt b/forge-gui/res/cardsfolder/l/land_grant.txt index 61c23084f58..666f6c8ebb8 100644 --- a/forge-gui/res/cardsfolder/l/land_grant.txt +++ b/forge-gui/res/cardsfolder/l/land_grant.txt @@ -1,7 +1,7 @@ Name:Land Grant ManaCost:1 G Types:Sorcery -S:Mode$ Continuous | CharacteristicDefining$ True | AddKeyword$ Alternative Cost:Reveal<1/Hand> | CheckSVar$ X | SVarCompare$ EQ0 | Description$ If you have no land cards in hand, you may reveal your hand rather than pay Land Grant's mana cost. +S:Mode$ Continuous | CharacteristicDefining$ True | AddKeyword$ Alternative Cost:Reveal<1/Hand> | CheckSVar$ X | SVarCompare$ EQ0 | Description$ If you have no land cards in hand, you may reveal your hand rather than pay this spell's mana cost. SVar:X:Count$TypeInYourHand.Land A:SP$ ChangeZone | Cost$ 1 G | Origin$ Library | Destination$ Hand | ChangeType$ Forest | ChangeNum$ 1 | SpellDescription$ Search your library for a Forest card, reveal that card, put it into your hand, then shuffle. Oracle:If you have no land cards in hand, you may reveal your hand rather than pay this spell's mana cost.\nSearch your library for a Forest card, reveal that card, put it into your hand, then shuffle. diff --git a/forge-gui/res/cardsfolder/t/the_lady_of_otaria.txt b/forge-gui/res/cardsfolder/t/the_lady_of_otaria.txt index 05524142e5c..a6ee929ff0d 100644 --- a/forge-gui/res/cardsfolder/t/the_lady_of_otaria.txt +++ b/forge-gui/res/cardsfolder/t/the_lady_of_otaria.txt @@ -2,7 +2,7 @@ Name:The Lady of Otaria ManaCost:3 R G Types:Legendary Creature Avatar PT:5/5 -SVar:AltCost:Cost$ tapXType<3/Creature.Dwarf> | Description$ You may tap three untapped Dwarves you control rather than pay this spell's mana cost. +SVar:AltCost:Cost$ tapXType<3/Dwarf> | Description$ You may tap three untapped Dwarves you control rather than pay this spell's mana cost. T:Mode$ Phase | Phase$ End of Turn | TriggerZones$ Battlefield | Execute$ TrigDig | CheckSVar$ X | TriggerDescription$ At the beginning of each end step, if a land you controlled was put into your graveyard from the battlefield this turn, reveal the top four cards of your library. Put any number of Dwarf cards from among them into your hand and the rest on the bottom of your library in a random order. SVar:TrigDig:DB$ Dig | DigNum$ 4 | ChangeValid$ Dwarf | DestinationZone$ Hand | RestRandomOrder$ True | AnyNumber$ True SVar:X:Count$ThisTurnEntered_Graveyard_from_Battlefield_Land.YouCtrl+YouOwn diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index a6fcfe96810..0c171457e2d 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -452,7 +452,6 @@ public class HumanPlay { return false; } } - return true; } else if (part instanceof CostGainControl) { int amount = Integer.parseInt(part.getAmount());