From 73a9c83b05c6dad467c7e8ee5508836744ca2cec Mon Sep 17 00:00:00 2001 From: Northmoc <103371817+Northmoc@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:55:00 -0500 Subject: [PATCH] ONE: Elesh Norn, Mother of Machines and support (#2179) * elesh_norn_mother_of_machines.txt * StaticAbilityDisableTriggers.java --- .../src/main/java/forge/ai/AiController.java | 12 +- .../java/forge/game/GlobalRuleChange.java | 2 - .../java/forge/game/ability/AbilityKey.java | 1 + .../StaticAbilityDisableTriggers.java | 127 ++++++++++++++++++ .../StaticAbilityPanharmonicon.java | 8 +- .../game/trigger/TriggerAbilityTriggered.java | 2 +- .../forge/game/trigger/TriggerHandler.java | 75 +---------- .../ai/simulation/GameSimulationTest.java | 37 +++++ forge-gui/res/cardsfolder/h/hushbringer.txt | 5 +- .../res/cardsfolder/h/hushwing_gryff.txt | 2 +- .../res/cardsfolder/t/tocatli_honor_guard.txt | 3 +- forge-gui/res/cardsfolder/t/torpor_orb.txt | 2 +- .../elesh_norn_mother_of_machines.txt | 8 ++ 13 files changed, 198 insertions(+), 86 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/staticability/StaticAbilityDisableTriggers.java create mode 100644 forge-gui/res/cardsfolder/upcoming/elesh_norn_mother_of_machines.txt diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 2b97b75890d..0c862cd8d87 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -54,6 +54,7 @@ import forge.game.replacement.ReplacementLayer; import forge.game.replacement.ReplacementType; import forge.game.spellability.*; import forge.game.staticability.StaticAbility; +import forge.game.staticability.StaticAbilityDisableTriggers; import forge.game.staticability.StaticAbilityMustTarget; import forge.game.trigger.Trigger; import forge.game.trigger.TriggerType; @@ -262,11 +263,6 @@ public class AiController { } } - if (card.isCreature() - && game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noCreatureETBTriggers)) { - return api == null; - } - boolean rightapi = false; // Trigger play improvements @@ -281,6 +277,12 @@ public class AiController { continue; } + final Map runParams = AbilityKey.mapFromCard(tr.getHostCard()); + runParams.put(AbilityKey.Destination, ZoneType.Battlefield.name()); + if (StaticAbilityDisableTriggers.disabled(game, tr, runParams)) { + return api == null; + } + if (tr.hasParam("ValidCard")) { String validCard = tr.getParam("ValidCard"); if (!validCard.contains("Self")) { diff --git a/forge-game/src/main/java/forge/game/GlobalRuleChange.java b/forge-game/src/main/java/forge/game/GlobalRuleChange.java index ef364c1b354..26480436bba 100644 --- a/forge-game/src/main/java/forge/game/GlobalRuleChange.java +++ b/forge-game/src/main/java/forge/game/GlobalRuleChange.java @@ -25,8 +25,6 @@ public enum GlobalRuleChange { alwaysWither ("All damage is dealt as though its source had wither."), attackerChoosesBlockers ("The attacking player chooses how each creature blocks each combat."), manaBurn ("A player losing unspent mana causes that player to lose that much life."), - noCreatureETBTriggers ("Creatures entering the battlefield don't cause abilities to trigger."), - noCreatureDyingTriggers ("Creatures dying don't cause abilities to trigger."), noNight ("It can't become night."), /* onlyOneAttackerATurn ("No more than one creature can attack each turn."), */ onlyOneAttackerACombat ("No more than one creature can attack each combat."), diff --git a/forge-game/src/main/java/forge/game/ability/AbilityKey.java b/forge-game/src/main/java/forge/game/ability/AbilityKey.java index 72c85a515f5..c03300096ab 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityKey.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityKey.java @@ -31,6 +31,7 @@ public enum AbilityKey { CastSA("CastSA"), Card("Card"), Cards("Cards"), + CardsFiltered("CardsFiltered"), CardLKI("CardLKI"), Cause("Cause"), Causer("Causer"), diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityDisableTriggers.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityDisableTriggers.java new file mode 100644 index 00000000000..efeee10ec00 --- /dev/null +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityDisableTriggers.java @@ -0,0 +1,127 @@ +package forge.game.staticability; + +import com.google.common.base.Predicates; +import com.google.common.collect.Table.Cell; + +import forge.game.Game; +import forge.game.card.*; +import forge.game.ability.AbilityKey; +import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerType; +import forge.game.zone.ZoneType; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Map; + +public class StaticAbilityDisableTriggers { + + static String MODE = "DisableTriggers"; + + public static boolean disabled(final Game game, final Trigger regtrig, final Map runParams) { + CardCollectionView cardList = null; + // if LTB look back + if ((regtrig.getMode() == TriggerType.ChangesZone || regtrig.getMode() == TriggerType.ChangesZoneAll) && "Battlefield".equals(regtrig.getParam("Origin"))) { + if (runParams.containsKey(AbilityKey.LastStateBattlefield)) { + cardList = (CardCollectionView) runParams.get(AbilityKey.LastStateBattlefield); + } + if (cardList == null) { + cardList = game.getLastStateBattlefield(); + } + } else { + cardList = game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES); + } + + for (final Card ca : cardList) { + for (final StaticAbility stAb : ca.getStaticAbilities()) { + if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) { + continue; + } + + if (isDisabled(stAb, regtrig, runParams)) { + return true; + } + } + } + return false; + } + + public static boolean isDisabled(final StaticAbility stAb, final Trigger regtrig, final Map runParams) { + final TriggerType trigMode = regtrig.getMode(); + + // CR 603.2e + if (stAb.hasParam("ValidCard") && regtrig.getSpawningAbility() != null) { + return false; + } + + if (!stAb.matchesValidParam("ValidCard", regtrig.getHostCard())) { + return false; + } + + if (stAb.hasParam("ValidMode")) { + if (!ArrayUtils.contains(stAb.getParam("ValidMode").split(","), trigMode.toString())) { + return false; + } + } + + if (trigMode.equals(TriggerType.ChangesZone)) { + // Cause of the trigger – the card changing zones + if (!stAb.matchesValidParam("ValidCause", runParams.get(AbilityKey.Card))) { + return false; + } + if (!stAb.matchesValidParam("Destination", runParams.get(AbilityKey.Destination))) { + return false; + } + if (!stAb.matchesValidParam("Origin", runParams.get(AbilityKey.Origin))) { + return false; + } + if ("Graveyard".equals(runParams.get(AbilityKey.Destination)) + && "Battlefield".equals(runParams.get(AbilityKey.Origin))) { + // Allow triggered ability of a dying creature that triggers + // only when that creature is put into a graveyard from anywhere + if ("Card.Self".equals(regtrig.getParam("ValidCard")) + && (!regtrig.hasParam("Origin") || "Any".equals(regtrig.getParam("Origin")))) { + return false; + } + } + } else if (trigMode.equals(TriggerType.ChangesZoneAll)) { + final String origin = stAb.getParam("Origin"); + final String destination = stAb.getParam("Destination"); + // check if some causes were already ignored by a different ability, then the forbidden causes will be combined + CardZoneTable table = (CardZoneTable) runParams.get(AbilityKey.CardsFiltered); + if (table == null) { + table = (CardZoneTable) runParams.get(AbilityKey.Cards); + } + CardZoneTable filtered = new CardZoneTable(); + boolean possiblyDisabled = false; + + // purge all forbidden causes from table + for (Cell cell : table.cellSet()) { + CardCollection changers = cell.getValue(); + if ((origin == null || cell.getRowKey() == ZoneType.valueOf(origin)) && + (destination == null || cell.getColumnKey() == ZoneType.valueOf(destination))) { + changers = CardLists.filter(changers, Predicates.not(CardPredicates.restriction(stAb.getParam("ValidCause").split(","), stAb.getHostCard().getController(), stAb.getHostCard(), stAb))); + // static will match some of the causes + if (changers.size() < cell.getValue().size()) { + possiblyDisabled = true; + } + } + filtered.put(cell.getRowKey(), cell.getColumnKey(), changers); + } + + if (!possiblyDisabled) { + return false; + } + + // test if trigger would still fire when ignoring forbidden causes + final Map runParamsFiltered = AbilityKey.newMap(runParams); + runParamsFiltered.put(AbilityKey.Cards, filtered); + if (regtrig.performTest(runParamsFiltered)) { + // store the filtered Cards because Panharmonicon shouldn't see the others + runParams.put(AbilityKey.CardsFiltered, filtered); + + return false; + } + } + return true; + } +} diff --git a/forge-game/src/main/java/forge/game/staticability/StaticAbilityPanharmonicon.java b/forge-game/src/main/java/forge/game/staticability/StaticAbilityPanharmonicon.java index 00ee0ccc414..377a2ac608d 100644 --- a/forge-game/src/main/java/forge/game/staticability/StaticAbilityPanharmonicon.java +++ b/forge-game/src/main/java/forge/game/staticability/StaticAbilityPanharmonicon.java @@ -37,7 +37,7 @@ public class StaticAbilityPanharmonicon { CardCollectionView cardList = null; // if LTB look back - if (t.getMode() == TriggerType.ChangesZone && "Battlefield".equals(t.getParam("Origin"))) { + if ((t.getMode() == TriggerType.ChangesZone || t.getMode() == TriggerType.ChangesZoneAll) && "Battlefield".equals(t.getParam("Origin"))) { if (runParams.containsKey(AbilityKey.LastStateBattlefield)) { cardList = (CardCollectionView) runParams.get(AbilityKey.LastStateBattlefield); } @@ -102,7 +102,11 @@ public class StaticAbilityPanharmonicon { // Check if the cards have a trigger at all final String origin = stAb.getParam("Origin"); final String destination = stAb.getParam("Destination"); - final CardZoneTable table = (CardZoneTable) runParams.get(AbilityKey.Cards); + // check if some causes were ignored + CardZoneTable table = (CardZoneTable) runParams.get(AbilityKey.CardsFiltered); + if (table == null) { + table = (CardZoneTable) runParams.get(AbilityKey.Cards); + } if (table.filterCards(origin == null ? null : ImmutableList.of(ZoneType.smartValueOf(origin)), ZoneType.smartValueOf(destination), stAb.getParam("ValidCause"), card, stAb).isEmpty()) { return false; diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java b/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java index e3fa7bf0946..e17b405baf6 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java @@ -83,7 +83,7 @@ public class TriggerAbilityTriggered extends Trigger { if (!matchesValidParam("ValidCause", causes)) { return false; } - + if (hasParam("TriggeredOwnAbility") && "True".equals(getParam("TriggeredOwnAbility")) && !Iterables.contains(causes, source)) { return false; } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index 7f33bc8604a..be0b3aa8c1e 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -19,18 +19,15 @@ package forge.game.trigger; import java.util.*; -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.Multimaps; -import com.google.common.collect.Table.Cell; import forge.game.CardTraitBase; import forge.game.CardTraitPredicates; import forge.game.Game; -import forge.game.GlobalRuleChange; import forge.game.IHasSVars; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; @@ -39,6 +36,7 @@ import forge.game.card.*; import forge.game.player.Player; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; +import forge.game.staticability.StaticAbilityDisableTriggers; import forge.game.staticability.StaticAbilityPanharmonicon; import forge.game.zone.Zone; import forge.game.zone.ZoneType; @@ -445,74 +443,9 @@ public class TriggerHandler { } } - // Torpor Orb check - if (game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noCreatureETBTriggers) - && !regtrig.isStatic()) { - if (mode.equals(TriggerType.ChangesZone)) { - if (runParams.get(AbilityKey.Destination) instanceof String) { - final String dest = (String) runParams.get(AbilityKey.Destination); - if (dest.equals("Battlefield") && runParams.get(AbilityKey.Card) instanceof Card) { - final Card card = (Card) runParams.get(AbilityKey.Card); - if (card.isCreature()) { - return false; - } - } - } - } else if (mode.equals(TriggerType.ChangesZoneAll)) { - CardZoneTable table = (CardZoneTable) runParams.get(AbilityKey.Cards); - // find out if any other cards would still trigger it - boolean found = false; - for (Cell cell : table.cellSet()) { - // this currently assumes the table will not contain multiple destinations - // however with some effects (e.g. Goblin Welder) that should indeed be the case - // once Forge handles that correctly this section needs to account for that - // (by doing a closer check of the triggered ability first) - if (cell.getColumnKey() != ZoneType.Battlefield) { - found = true; - } else if (Iterables.any(cell.getValue(), Predicates.not(CardPredicates.isType("Creature")))) { - found = true; - } - if (found) break; - } - if (!found) { - return false; - } - } - } // Torpor Orb check - - if (game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noCreatureDyingTriggers) - && !regtrig.isStatic()) { - if (mode.equals(TriggerType.ChangesZone)) { - if (runParams.get(AbilityKey.Destination) instanceof String && runParams.get(AbilityKey.Origin) instanceof String) { - final String dest = (String) runParams.get(AbilityKey.Destination); - final String origin = (String) runParams.get(AbilityKey.Origin); - if (dest.equals("Graveyard") && origin.equals("Battlefield") && runParams.get(AbilityKey.Card) instanceof Card) { - // It will trigger if the ability is of a dying creature that triggers only when that creature is put into a graveyard from anywhere - if (!"Card.Self".equals(regtrig.getParam("ValidCard")) || (regtrig.hasParam("Origin") && !"Any".equals(regtrig.getParam("Origin")))) { - final Card card = (Card) runParams.get(AbilityKey.Card); - if (card.isCreature()) { - return false; - } - } - } - } - } else if (mode.equals(TriggerType.ChangesZoneAll)) { - CardZoneTable table = (CardZoneTable) runParams.get(AbilityKey.Cards); - boolean found = false; - for (Cell cell : table.cellSet()) { - if (cell.getRowKey() != ZoneType.Battlefield) { - found = true; - } else if (cell.getColumnKey() != ZoneType.Graveyard) { - found = true; - } else if (Iterables.any(cell.getValue(), Predicates.not(CardPredicates.isType("Creature")))) { - found = true; - } - if (found) break; - } - if (!found) { - return false; - } - } + // check if any static abilities are disabling the trigger (Torpor Orb and the like) + if (!regtrig.isStatic() && StaticAbilityDisableTriggers.disabled(game, regtrig, runParams)) { + return false; } return true; } diff --git a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java index 3355b6f10b7..066583e4366 100644 --- a/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java +++ b/forge-gui-desktop/src/test/java/forge/ai/simulation/GameSimulationTest.java @@ -2495,4 +2495,41 @@ public class GameSimulationTest extends SimulationTest { AssertJUnit.assertNotNull(simMentor); AssertJUnit.assertEquals(1, simMentor.getCounters(CounterEnumType.P1P1)); } + + @Test + public void testHushbringer() { + Game game = initAndCreateGame(); + Player p = game.getPlayers().get(0); + + addCard("Naban, Dean of Iteration", p); + addCard("Hushbringer", p); + addCard("Ingenious Artillerist", p); + + // both ETB together and Artillerist should still trigger from the non-creature + addCardToZone("Spellbook", p, ZoneType.Library); + // whereas Naban doesn't see Memnarch to double the trigger + addCardToZone("Memnarch", p, ZoneType.Library); + + addCard("Forest", p); + addCard("Forest", p); + addCard("Island", p); + addCard("Island", p); + addCard("Island", p); + addCard("Mountain", p); + addCard("Mountain", p); + + Card genesis = addCardToZone("Genesis Ultimatum", p, ZoneType.Hand); + + SpellAbility genesisSA = genesis.getFirstSpellAbility(); + + game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p); + game.getAction().checkStateEffects(true); + + GameSimulator sim = createSimulator(game, p); + sim.simulateSpellAbility(genesisSA); + Game simGame = sim.getSimulatedGameState(); + + // 2 damage dealt for 2 artifacts + AssertJUnit.assertEquals(18, simGame.getPlayers().get(1).getLife()); + } } diff --git a/forge-gui/res/cardsfolder/h/hushbringer.txt b/forge-gui/res/cardsfolder/h/hushbringer.txt index 3fc0204661c..3265bc793d8 100644 --- a/forge-gui/res/cardsfolder/h/hushbringer.txt +++ b/forge-gui/res/cardsfolder/h/hushbringer.txt @@ -4,7 +4,8 @@ Types:Creature Faerie PT:1/2 K:Flying K:Lifelink -S:Mode$ Continuous | GlobalRule$ Creatures entering the battlefield don't cause abilities to trigger. | Description$ Creatures entering the battlefield or dying don't cause abilities to trigger. -S:Mode$ Continuous | GlobalRule$ Creatures dying don't cause abilities to trigger. | Secondary$ True | Description$ Creatures entering the battlefield or dying don't cause abilities to trigger. +S:Mode$ DisableTriggers | ValidCause$ Creature | ValidMode$ ChangesZone,ChangesZoneAll | Destination$ Battlefield | Description$ Creatures entering the battlefield or dying don't cause abilities to trigger. +S:Mode$ DisableTriggers | ValidCause$ Creature | ValidMode$ ChangesZone,ChangesZoneAll | Origin$ Battlefield | Destination$ Graveyard | Secondary$ True | Description$ Creatures entering the battlefield or dying don't cause abilities to trigger. AI:RemoveDeck:Random +DeckHas:Ability$LifeGain Oracle:Flying, lifelink\nCreatures entering the battlefield or dying don't cause abilities to trigger. diff --git a/forge-gui/res/cardsfolder/h/hushwing_gryff.txt b/forge-gui/res/cardsfolder/h/hushwing_gryff.txt index ab0d71c5894..6aa9a77b09e 100644 --- a/forge-gui/res/cardsfolder/h/hushwing_gryff.txt +++ b/forge-gui/res/cardsfolder/h/hushwing_gryff.txt @@ -4,6 +4,6 @@ Types:Creature Hippogriff PT:2/1 K:Flash K:Flying -S:Mode$ Continuous | GlobalRule$ Creatures entering the battlefield don't cause abilities to trigger. | Description$ Creatures entering the battlefield don't cause abilities to trigger. +S:Mode$ DisableTriggers | ValidCause$ Creature | ValidMode$ ChangesZone,ChangesZoneAll | Destination$ Battlefield | Description$ Creatures entering the battlefield don't cause abilities to trigger. AI:RemoveDeck:Random Oracle:Flash\nFlying\nCreatures entering the battlefield don't cause abilities to trigger. diff --git a/forge-gui/res/cardsfolder/t/tocatli_honor_guard.txt b/forge-gui/res/cardsfolder/t/tocatli_honor_guard.txt index da597558dc2..3a490f3aa6a 100644 --- a/forge-gui/res/cardsfolder/t/tocatli_honor_guard.txt +++ b/forge-gui/res/cardsfolder/t/tocatli_honor_guard.txt @@ -2,5 +2,6 @@ Name:Tocatli Honor Guard ManaCost:1 W Types:Creature Human Soldier PT:1/3 -S:Mode$ Continuous | GlobalRule$ Creatures entering the battlefield don't cause abilities to trigger. | Description$ Creatures entering the battlefield don't cause abilities to trigger. +S:Mode$ DisableTriggers | ValidCause$ Creature | ValidMode$ ChangesZone,ChangesZoneAll | Destination$ Battlefield | Description$ Creatures entering the battlefield don't cause abilities to trigger. +AI:RemoveDeck:Random Oracle:Creatures entering the battlefield don't cause abilities to trigger. diff --git a/forge-gui/res/cardsfolder/t/torpor_orb.txt b/forge-gui/res/cardsfolder/t/torpor_orb.txt index a71f3ba25b7..6afd7bfab0f 100644 --- a/forge-gui/res/cardsfolder/t/torpor_orb.txt +++ b/forge-gui/res/cardsfolder/t/torpor_orb.txt @@ -1,7 +1,7 @@ Name:Torpor Orb ManaCost:2 Types:Artifact -S:Mode$ Continuous | GlobalRule$ Creatures entering the battlefield don't cause abilities to trigger. | Description$ Creatures entering the battlefield don't cause abilities to trigger. +S:Mode$ DisableTriggers | ValidCause$ Creature | ValidMode$ ChangesZone,ChangesZoneAll | Destination$ Battlefield | Description$ Creatures entering the battlefield don't cause abilities to trigger. SVar:NonStackingEffect:True AI:RemoveDeck:Random Oracle:Creatures entering the battlefield don't cause abilities to trigger. diff --git a/forge-gui/res/cardsfolder/upcoming/elesh_norn_mother_of_machines.txt b/forge-gui/res/cardsfolder/upcoming/elesh_norn_mother_of_machines.txt new file mode 100644 index 00000000000..9d3236ba682 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/elesh_norn_mother_of_machines.txt @@ -0,0 +1,8 @@ +Name:Elesh Norn, Mother of Machines +ManaCost:4 W +Types:Legendary Creature Phyrexian Praetor +PT:4/7 +K:Vigilance +S:Mode$ Panharmonicon | ValidMode$ ChangesZone,ChangesZoneAll | ValidCard$ Permanent.YouCtrl | ValidCause$ Permanent | Destination$ Battlefield | Description$ If a permanent entering the battlefield under your control causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time. +S:Mode$ DisableTriggers | ValidCause$ Permanent | ValidMode$ ChangesZone,ChangesZoneAll | Destination$ Battlefield | ValidCard$ Permanent.OppCtrl | Description$ Permanents entering the battlefield don't cause abilities of permanents your opponents control to trigger. +Oracle:Vigilance\nIf a permanent entering the battlefield causes a triggered ability of a permanent you control to trigger, that ability triggers an additional time.\nPermanents entering the battlefield don't cause abilities of permanents your opponents control to trigger.