mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-15 18:28:00 +00:00
ONE: Elesh Norn, Mother of Machines and support (#2179)
* elesh_norn_mother_of_machines.txt * StaticAbilityDisableTriggers.java
This commit is contained in:
@@ -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")) {
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -31,6 +31,7 @@ public enum AbilityKey {
|
||||
CastSA("CastSA"),
|
||||
Card("Card"),
|
||||
Cards("Cards"),
|
||||
CardsFiltered("CardsFiltered"),
|
||||
CardLKI("CardLKI"),
|
||||
Cause("Cause"),
|
||||
Causer("Causer"),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,75 +443,10 @@ 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()) {
|
||||
// check if any static abilities are disabling the trigger (Torpor Orb and the like)
|
||||
if (!regtrig.isStatic() && StaticAbilityDisableTriggers.disabled(game, regtrig, runParams)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user