From c75552f54b92e5a643a962bb960cfa9a9d3dbd94 Mon Sep 17 00:00:00 2001 From: Michael Kamensky Date: Sun, 24 Oct 2021 08:27:32 +0000 Subject: [PATCH] - Account for random destruction (Wild Swing) - Some minor reorganization/refactoring. --- .../main/java/forge/ai/ability/CounterAi.java | 3 + .../game/ability/effects/CounterEffect.java | 137 ++++++++++++++++-- forge-gui/res/cardsfolder/e/equinox.txt | 8 + 3 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 forge-gui/res/cardsfolder/e/equinox.txt diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java index f9bce402881..b93c01d40a3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.Iterator; +import forge.game.ability.effects.CounterEffect; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -69,6 +70,8 @@ public class CounterAi extends SpellAbilityAi { || ai.getAllies().contains(topSA.getActivatingPlayer())) { // might as well check for player's friendliness return false; + } else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) { + return false; } // check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided diff --git a/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java index 2e94b834a44..cd7993acd90 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CounterEffect.java @@ -1,18 +1,12 @@ package forge.game.ability.effects; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - import com.google.common.collect.Lists; - import forge.game.Game; import forge.game.GameLogEntryType; import forge.game.ability.AbilityKey; +import forge.game.ability.ApiType; import forge.game.ability.SpellAbilityEffect; -import forge.game.card.Card; -import forge.game.card.CardFactoryUtil; -import forge.game.card.CardZoneTable; +import forge.game.card.*; import forge.game.replacement.ReplacementResult; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; @@ -20,8 +14,13 @@ import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellPermanent; import forge.game.trigger.TriggerType; import forge.game.zone.Zone; +import forge.game.zone.ZoneType; import forge.util.Localizer; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + public class CounterEffect extends SpellAbilityEffect { @Override protected String getStackDescription(SpellAbility sa) { @@ -141,6 +140,10 @@ public class CounterEffect extends SpellAbilityEffect { continue; } + if (sa.hasParam("ConditionWouldDestroy") && !checkForConditionWouldDestroy(sa, tgtSA)) { + continue; + } + removeFromStack(tgtSA, sa, si, table); // Destroy Permanent may be able to be turned into a SubAbility @@ -163,11 +166,127 @@ public class CounterEffect extends SpellAbilityEffect { table.triggerChangesZoneAll(game, sa); } // end counterResolve + public static boolean checkForConditionWouldDestroy(SpellAbility sa, SpellAbility tgtSA) { + List testChain = Lists.newArrayList(); + + // TODO: add anything that may be important for the test chain here + SpellAbility currentTgtSA = tgtSA; + while (currentTgtSA != null) { + testChain.add(currentTgtSA); + currentTgtSA = currentTgtSA.getSubAbility(); + } + + for (SpellAbility viableTgtSA : testChain) { + if (checkSingleSAForConditionWouldDestroy(sa, viableTgtSA)) { + return true; + } + } + + return false; + } + + private static boolean checkSingleSAForConditionWouldDestroy(SpellAbility sa, SpellAbility tgtSA) { + Game game = sa.getHostCard().getGame(); + + if (tgtSA.getApi() != ApiType.Destroy && tgtSA.getApi() != ApiType.DestroyAll) { + return false; + } + + String wouldDestroy = sa.getParam("ConditionWouldDestroy"); + CardCollectionView cardsOTB = game.getCardsIn(ZoneType.Battlefield); + // Potential candidates that our condition (ConditionWouldDestroy) is checking for + CardCollection conditionCandidates = CardLists.getValidCards(cardsOTB, wouldDestroy, sa.getActivatingPlayer(), sa.getHostCard(), sa); + + // Determine which cards will be affected by the target SA + CardCollection affected = new CardCollection(); + if (tgtSA.hasParam("ValidTgts") || tgtSA.hasParam("Defined")) { + affected.addAll(getDefinedCardsOrTargeted(tgtSA)); + } else if (tgtSA.hasParam("ValidCards")) { + affected.addAll(CardLists.getValidCards(cardsOTB, tgtSA.getParam("ValidCards"), tgtSA.getActivatingPlayer(), tgtSA.getHostCard(), tgtSA)); + } + + // Determine which of the condition-specific candidates are potentially affected with the target SA + CardCollection validAffected = new CardCollection(); + for (Card cand : conditionCandidates) { + if (affected.contains(cand)) { + validAffected.add(cand); + } + } + + // Special case: Wild Swing random destruction - only counter if all targets are valid and each can be destroyed (100% chance + // to destroy one of the owned lands) + // TODO: this is hacky... make the detection of this ability more robust and generic? + boolean isRandomDestruction = false; + if (validAffected.isEmpty() && tgtSA.getRootAbility().getApi() == ApiType.Pump + && tgtSA.getRootAbility().hasParam("TargetMax") + && tgtSA.getRootAbility().getSubAbility() != null + && tgtSA.getRootAbility().getSubAbility().getApi() == ApiType.ChooseCard + && tgtSA.getRootAbility().getSubAbility().hasParam("AtRandom") + && "ChosenCard".equals(tgtSA.getParam("Defined"))) { + isRandomDestruction = true; + boolean allValid = true; + affected.addAll(getDefinedCardsOrTargeted(tgtSA.getRootAbility())); + for (Card cand : conditionCandidates) { + if (affected.contains(cand)) { + validAffected.add(cand); + } + } + CardCollectionView rootTgts = tgtSA.getRootAbility().getTargets().getTargetCards(); + for (Card rootTgt : rootTgts) { + if (!validAffected.contains(rootTgt)) { + allValid = false; + break; + } + } + if (!allValid) { + return false; + } + } + + if (validAffected.isEmpty()) { + return false; + } else if (tgtSA.hasParam("Sacrifice")) { + return false; // Sacrifice doesn't count as Destroy + } + + // Dry run Destroy on each validAffected to see if it can be destroyed at this moment + boolean willDestroyCondition = false; + final boolean noRegen = tgtSA.hasParam("NoRegen"); + CardZoneTable testTable = new CardZoneTable(); + Map testParams = AbilityKey.newMap(); + testParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield()); + + boolean willDestroyAll = true; + for (Card aff : validAffected) { + if (tgtSA.usesTargeting() && !aff.canBeTargetedBy(tgtSA)) { + willDestroyAll = false; + continue; // Should account for Protection/Hexproof/etc. + } + + Card toBeDestroyed = CardFactory.copyCard(aff, true); + + game.getTriggerHandler().setSuppressAllTriggers(true); + boolean destroyed = game.getAction().destroy(toBeDestroyed, tgtSA, !noRegen, testTable, testParams); + game.getTriggerHandler().setSuppressAllTriggers(false); + + if (destroyed) { + willDestroyCondition = true; // this should pick up replacement effects replacing Destroy + if (!isRandomDestruction) { + break; + } + } else { + willDestroyAll = false; + } + } + + return isRandomDestruction ? willDestroyAll : willDestroyCondition; + } + /** *

* removeFromStack. *

- * + * * @param tgtSA * a {@link forge.game.spellability.SpellAbility} object. * @param srcSA diff --git a/forge-gui/res/cardsfolder/e/equinox.txt b/forge-gui/res/cardsfolder/e/equinox.txt new file mode 100644 index 00000000000..91cc93b6234 --- /dev/null +++ b/forge-gui/res/cardsfolder/e/equinox.txt @@ -0,0 +1,8 @@ +Name:Equinox +ManaCost:W +Types:Enchantment Aura +K:Enchant land +A:SP$ Attach | Cost$ W | ValidTgts$ Land | TgtPrompt$ Select target land | AILogic$ Pump +S:Mode$ Continuous | Affected$ Card.EnchantedBy | AddAbility$ CounterDestroyLand | Description$ Enchanted land has "{T}: Counter target spell if it would destroy a land you control." +SVar:CounterDestroyLand:AB$ Counter | Cost$ T | TargetType$ Spell | ConditionWouldDestroy$ Land.YouCtrl | TgtPrompt$ Select target spell | ValidTgts$ Card | SpellDescription$ Counter target spell if it would destroy a land you control. +Oracle:Enchant land\nEnchanted land has "{T}: Counter target spell if it would destroy a land you control."