From 86c814af96761a92d057edaadb02d912b55d5bcb Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Tue, 17 Jan 2023 00:12:38 +0100 Subject: [PATCH 1/6] Cleanup check for Mishra, Tamer of Mak Fawa --- forge-ai/src/main/java/forge/ai/AiController.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) 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; } } } From 444a5629a874c54708a8bd121024707a587c95b1 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Tue, 17 Jan 2023 10:51:55 +0100 Subject: [PATCH 2/6] Fix reloading interacting with objects from old state --- forge-ai/src/main/java/forge/ai/GameState.java | 3 +++ 1 file changed, 3 insertions(+) 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++) { From 654d80d39c67611dd82982ebd9a07fc0f2abc04e Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Tue, 17 Jan 2023 11:04:46 +0100 Subject: [PATCH 3/6] Improve cost merging --- .../game/ability/effects/SacrificeEffect.java | 9 ++--- .../src/main/java/forge/game/cost/Cost.java | 33 ++++++++++++------- .../java/forge/game/cost/CostAdjustment.java | 8 ++--- .../forge/game/spellability/SpellAbility.java | 12 +++---- .../StaticAbilityCantGainLosePayLife.java | 2 +- 5 files changed, 34 insertions(+), 30 deletions(-) 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..e4b69430a89 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,22 @@ public class Cost implements Serializable { return sb.toString(); } + public void mergeTo(Cost source, int amt) { + if (source.isOnlyManaCost()) { + // multiply cost + for (int i = 0; i < amt; ++i) { + this.add(source); + } + } else { + source.getCostParts().get(0).setAmount(Integer.toString(amt)); + 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 +921,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 +932,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 +946,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 +959,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)) { From 8a608e30d4ad71ba559b19e14e91e905c73f8690 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Tue, 17 Jan 2023 13:09:51 +0100 Subject: [PATCH 4/6] Fix merging logic to correctly avoid cost mixups --- .../src/main/java/forge/game/cost/Cost.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 e4b69430a89..d9a2ad21d0f 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -884,15 +884,17 @@ public class Cost implements Serializable { } public void mergeTo(Cost source, int amt) { - if (source.isOnlyManaCost()) { - // multiply cost - for (int i = 0; i < amt; ++i) { - this.add(source); + // 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); } - } else { - source.getCostParts().get(0).setAmount(Integer.toString(amt)); - this.add(source, false); } + // combine costs (these shouldn't mix together) + this.add(source, false); } public Cost add(Cost cost1) { From 353c0d82cd5550e4a62825922bc651e05ea049e5 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Tue, 17 Jan 2023 13:11:11 +0100 Subject: [PATCH 5/6] Jotun fix --- forge-gui/src/main/java/forge/player/HumanPlay.java | 1 - 1 file changed, 1 deletion(-) 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()); From 6a0cf24e1e770327226aa68db80dbacd3ac05160 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Tue, 17 Jan 2023 16:37:54 +0100 Subject: [PATCH 6/6] Clean up --- forge-gui/res/cardsfolder/l/land_grant.txt | 2 +- forge-gui/res/cardsfolder/t/the_lady_of_otaria.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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