diff --git a/Mage.Sets/src/mage/cards/c/CallerOfTheClaw.java b/Mage.Sets/src/mage/cards/c/CallerOfTheClaw.java index 6c9e7cf4a97d..2a514af68ca6 100644 --- a/Mage.Sets/src/mage/cards/c/CallerOfTheClaw.java +++ b/Mage.Sets/src/mage/cards/c/CallerOfTheClaw.java @@ -1,4 +1,3 @@ - package mage.cards.c; import java.util.UUID; @@ -39,6 +38,7 @@ public CallerOfTheClaw(UUID ownerId, CardSetInfo setInfo) { // Flash this.addAbility(FlashAbility.getInstance()); + // When Caller of the Claw enters the battlefield, create a 2/2 green Bear creature token for each nontoken creature put into your graveyard from the battlefield this turn. this.getSpellAbility().addWatcher(new CallerOfTheClawWatcher()); Effect effect = new CreateTokenEffect(new BearToken(), new CallerOfTheClawDynamicValue()); diff --git a/Mage.Sets/src/mage/cards/d/DiabolicServitude.java b/Mage.Sets/src/mage/cards/d/DiabolicServitude.java index bc8f05ca3873..8e9323877928 100644 --- a/Mage.Sets/src/mage/cards/d/DiabolicServitude.java +++ b/Mage.Sets/src/mage/cards/d/DiabolicServitude.java @@ -1,6 +1,8 @@ package mage.cards.d; import java.util.UUID; + +import mage.MageObject; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; @@ -91,6 +93,7 @@ class DiabolicServitudeCreatureDiesTriggeredAbility extends TriggeredAbilityImpl public DiabolicServitudeCreatureDiesTriggeredAbility() { super(Zone.BATTLEFIELD, new DiabolicServitudeExileCreatureEffect(), false); + setLeavesTheBattlefieldTrigger(true); } private DiabolicServitudeCreatureDiesTriggeredAbility(final DiabolicServitudeCreatureDiesTriggeredAbility ability) { @@ -123,6 +126,11 @@ public boolean checkTrigger(GameEvent event, Game game) { public String getRule() { return "When the creature put onto the battlefield with {this} dies, exile it and return {this} to its owner's hand."; } + + @Override + public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) { + return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game); + } } class DiabolicServitudeExileCreatureEffect extends OneShotEffect { diff --git a/Mage.Sets/src/mage/cards/e/EndlessEvil.java b/Mage.Sets/src/mage/cards/e/EndlessEvil.java index e259defedc25..d1cf80be8c08 100644 --- a/Mage.Sets/src/mage/cards/e/EndlessEvil.java +++ b/Mage.Sets/src/mage/cards/e/EndlessEvil.java @@ -1,5 +1,6 @@ package mage.cards.e; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbilityImpl; @@ -97,6 +98,7 @@ class EndlessEvilBounceAbility extends TriggeredAbilityImpl { public EndlessEvilBounceAbility() { super(Zone.BATTLEFIELD, new ReturnToHandSourceEffect(false, true)); + setLeavesTheBattlefieldTrigger(true); } private EndlessEvilBounceAbility(final EndlessEvilBounceAbility effect) { @@ -126,4 +128,9 @@ public boolean checkTrigger(GameEvent event, Game game) { public String getRule() { return "When enchanted creature dies, if that creature was a Horror, return {this} to its owner's hand."; } + + @Override + public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) { + return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game); + } } diff --git a/Mage.Sets/src/mage/cards/e/EnigmaSphinx.java b/Mage.Sets/src/mage/cards/e/EnigmaSphinx.java index 797e6e6f5676..a1f0488b0cc9 100644 --- a/Mage.Sets/src/mage/cards/e/EnigmaSphinx.java +++ b/Mage.Sets/src/mage/cards/e/EnigmaSphinx.java @@ -3,6 +3,7 @@ import java.util.UUID; import mage.MageInt; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; @@ -65,6 +66,7 @@ public EnigmaSphinxTriggeredAbility(Effect effect) { public EnigmaSphinxTriggeredAbility(Effect effect, boolean optional) { super(Zone.ALL, effect, optional); setTriggerPhrase("When {this} is put into your graveyard from the battlefield, "); + setLeavesTheBattlefieldTrigger(true); } private EnigmaSphinxTriggeredAbility(final EnigmaSphinxTriggeredAbility ability) { @@ -94,6 +96,11 @@ public boolean checkTrigger(GameEvent event, Game game) { } return false; } + + @Override + public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) { + return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game); + } } class EnigmaSphinxEffect extends OneShotEffect { diff --git a/Mage.Sets/src/mage/cards/k/KayasGhostform.java b/Mage.Sets/src/mage/cards/k/KayasGhostform.java index 0cd641eb58ca..de7433f48047 100644 --- a/Mage.Sets/src/mage/cards/k/KayasGhostform.java +++ b/Mage.Sets/src/mage/cards/k/KayasGhostform.java @@ -60,6 +60,7 @@ class KayasGhostformTriggeredAbility extends TriggeredAbilityImpl { KayasGhostformTriggeredAbility() { super(Zone.ALL, new ReturnToBattlefieldUnderYourControlAttachedEffect(), false); + setLeavesTheBattlefieldTrigger(true); } private KayasGhostformTriggeredAbility(final KayasGhostformTriggeredAbility ability) { diff --git a/Mage.Sets/src/mage/cards/m/MagusOfTheBridge.java b/Mage.Sets/src/mage/cards/m/MagusOfTheBridge.java index 9f28c451fd0a..81fc10fb6841 100644 --- a/Mage.Sets/src/mage/cards/m/MagusOfTheBridge.java +++ b/Mage.Sets/src/mage/cards/m/MagusOfTheBridge.java @@ -2,6 +2,7 @@ import java.util.UUID; import mage.MageInt; +import mage.MageObject; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.PutIntoGraveFromBattlefieldAllTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; @@ -59,6 +60,7 @@ class MagusOfTheBridgeTriggeredAbility extends TriggeredAbilityImpl { public MagusOfTheBridgeTriggeredAbility() { super(Zone.BATTLEFIELD, new ExileSourceEffect()); setTriggerPhrase("When a creature is put into an opponent's graveyard from the battlefield, "); + setLeavesTheBattlefieldTrigger(true); } private MagusOfTheBridgeTriggeredAbility(final MagusOfTheBridgeTriggeredAbility ability) { @@ -86,4 +88,9 @@ public boolean checkTrigger(GameEvent event, Game game) { } return false; } + + @Override + public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) { + return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game); + } } diff --git a/Mage.Sets/src/mage/cards/t/TaekoThePatientAvalanche.java b/Mage.Sets/src/mage/cards/t/TaekoThePatientAvalanche.java index 9637aaff982b..7412aeb49487 100644 --- a/Mage.Sets/src/mage/cards/t/TaekoThePatientAvalanche.java +++ b/Mage.Sets/src/mage/cards/t/TaekoThePatientAvalanche.java @@ -67,6 +67,7 @@ class TaekoThePatientAvalancheTriggeredAbility extends TriggeredAbilityImpl { super(Zone.BATTLEFIELD, new ScryEffect(1, false)); this.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance()).concatBy("and")); this.setTriggerPhrase("Whenever another creature you control leaves the battlefield, if it didn't die, "); + setLeavesTheBattlefieldTrigger(true); } private TaekoThePatientAvalancheTriggeredAbility(final TaekoThePatientAvalancheTriggeredAbility ability) { diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index 797e66bb201f..e018ec86f553 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -1985,33 +1985,52 @@ private void checkMissingAbilities(Card card, MtgJsonCard ref) { fail(card, "abilities", "mutate cards aren't implemented and shouldn't be available"); } - // special check: wrong dies triggers - card.getAbilities().stream() - .filter(a -> a instanceof TriggeredAbility) - .map(a -> (TriggeredAbility) a) - .filter(a -> !a.isLeavesTheBattlefieldTrigger()) - //.filter(a -> a.getRule().contains("whenever") || a.getRule().contains("Whenever")) // TODO: research failed cards - .filter(a -> a.getRule().contains("die ") - || a.getRule().contains("dies ") - || a.getRule().contains("die,") - || a.getRule().contains("dies,") - || (a.getRule().contains("put into") - && a.getRule().contains("graveyard") - && a.getRule().contains("from the battlefield")) - ) - .filter(a -> !a.getRule().contains("roll")) // ignore roll die effects - .filter(a -> !a.getRule().contains("with \"When")) // ignore token creating effects - .filter(a -> !a.getRule().contains("gains \"When")) // ignore token creating effects - .filter(a -> !a.getRule().contains("and \"When")) // ignore token creating effects - .filter(a -> !a.getRule().contains("dies while {this} is in your graveyard")) // ignore Boneyard Scourge - .filter(a -> !a.getRule().contains("all creature cards that were put into your")) // ignore Fell Shepherd - .filter(a -> !card.getName().equals("Massacre Girl") // delayed trigger fixed, but verify check can't find it - && !card.getName().equals("Infested Thrinax") - && !card.getName().equals("Xira, the Golden Sting") - ) - .forEach(a -> { - fail(card, "abilities", "dies trigger must use setLeavesTheBattlefieldTrigger(true) and override isInUseableZone - " + a.getClass().getSimpleName()); - }); + // special check: wrong dies triggers (there are also a runtime check on wrong usage, see isInUseableZoneDiesTrigger) + Set ignoredCards = new HashSet<>(); + ignoredCards.add("Caller of the Claw"); + ignoredCards.add("Boneyard Scourge"); + ignoredCards.add("Fell Shepherd"); + ignoredCards.add("Massacre Girl"); + ignoredCards.add("Infested Thrinax"); + ignoredCards.add("Xira, the Golden Sting"); + ignoredCards.add("Mawloc"); + List ignoredAbilities = new ArrayList<>(); + ignoredAbilities.add("roll"); // roll die effects + ignoredAbilities.add("with \"When"); // token creating effects + ignoredAbilities.add("gains \"When"); // token creating effects + ignoredAbilities.add("and \"When"); // token creating effects + ignoredAbilities.add("it has \"When"); // token creating effects + ignoredAbilities.add("beginning of your end step"); // step triggers + ignoredAbilities.add("beginning of each end step"); // step triggers + ignoredAbilities.add("beginning of combat"); // step triggers + if (!ignoredCards.contains(card.getName())) { + for (Ability ability : card.getAbilities()) { + TriggeredAbility triggeredAbility = ability instanceof TriggeredAbility ? (TriggeredAbility) ability : null; + if (triggeredAbility == null) { + continue; + } + // search and check dies related abilities + String rules = triggeredAbility.getRule(); + if (ignoredAbilities.stream().anyMatch(rules::contains)) { + continue; + } + boolean isDiesAbility = rules.contains("die ") + || rules.contains("dies ") + || rules.contains("die,") + || rules.contains("dies,"); + boolean isPutToGraveAbility = rules.contains("put into") + && rules.contains("graveyard") + && rules.contains("from the battlefield"); + if (triggeredAbility.isLeavesTheBattlefieldTrigger()) { + // TODO: add check for wrongly enabled settings too? + } else { + if (isDiesAbility || isPutToGraveAbility) { + fail(card, "abilities", "dies related trigger must use setLeavesTheBattlefieldTrigger(true) and possibly override isInUseableZone - " + + triggeredAbility.getClass().getSimpleName()); + } + } + } + } // special check: duplicated words in ability text (wrong target/filter usage) // example: You may exile __two two__ blue cards diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index ea7ea5d55eff..214b7b015a9c 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1290,7 +1290,7 @@ protected static UUID getRealSourceObjectId(Ability sourceAbility, MageObject so } @Override - public boolean hasSourceObjectAbility(Game game, MageObject sourceObject, GameEvent event) { + public final boolean hasSourceObjectAbility(Game game, MageObject sourceObject, GameEvent event) { MageObject object = sourceObject; if (object == null) { object = game.getPermanentEntering(getSourceId()); diff --git a/Mage/src/main/java/mage/abilities/keyword/HauntAbility.java b/Mage/src/main/java/mage/abilities/keyword/HauntAbility.java index c35e7592b214..f4d0a650c5b6 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HauntAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HauntAbility.java @@ -47,6 +47,7 @@ public HauntAbility(Card card, Effect effect) { setTriggerPhrase((creatureHaunt ? "When {this} enters or the creature it haunts dies, " : "When the creature {this} haunts dies, ") ); + setLeavesTheBattlefieldTrigger(true); } private HauntAbility(final HauntAbility ability) {