ONE: Elesh Norn, Mother of Machines and support (#2179)

* elesh_norn_mother_of_machines.txt

* StaticAbilityDisableTriggers.java
This commit is contained in:
Northmoc
2023-01-10 09:55:00 -05:00
committed by GitHub
parent 8250082c22
commit 73a9c83b05
13 changed files with 198 additions and 86 deletions

View File

@@ -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<AbilityKey, Object> 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")) {

View File

@@ -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."),

View File

@@ -31,6 +31,7 @@ public enum AbilityKey {
CastSA("CastSA"),
Card("Card"),
Cards("Cards"),
CardsFiltered("CardsFiltered"),
CardLKI("CardLKI"),
Cause("Cause"),
Causer("Causer"),

View File

@@ -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<AbilityKey, Object> 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<AbilityKey, Object> 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<ZoneType, ZoneType, CardCollection> 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<AbilityKey, Object> 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;
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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<ZoneType, ZoneType, CardCollection> 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<ZoneType, ZoneType, CardCollection> 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;
}

View File

@@ -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());
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.