refactor: fixed dies events support in single cards (part 6);

This commit is contained in:
Oleg Agafonov 2024-11-30 16:56:00 +04:00
parent d49ff89a81
commit b1024d23fc
10 changed files with 80 additions and 29 deletions

View file

@ -1,4 +1,3 @@
package mage.cards.c;
import java.util.UUID;
@ -39,6 +38,7 @@ public final class CallerOfTheClaw extends CardImpl {
// 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());

View file

@ -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 @@ class DiabolicServitudeCreatureDiesTriggeredAbility extends TriggeredAbilityImpl
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 {

View file

@ -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 @@ class EndlessEvilBounceAbility extends TriggeredAbilityImpl {
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);
}
}

View file

@ -3,6 +3,7 @@ package mage.cards.e;
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 @@ class EnigmaSphinxTriggeredAbility extends TriggeredAbilityImpl {
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 @@ class EnigmaSphinxTriggeredAbility extends TriggeredAbilityImpl {
}
return false;
}
@Override
public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
}
}
class EnigmaSphinxEffect extends OneShotEffect {

View file

@ -60,6 +60,7 @@ class KayasGhostformTriggeredAbility extends TriggeredAbilityImpl {
KayasGhostformTriggeredAbility() {
super(Zone.ALL, new ReturnToBattlefieldUnderYourControlAttachedEffect(), false);
setLeavesTheBattlefieldTrigger(true);
}
private KayasGhostformTriggeredAbility(final KayasGhostformTriggeredAbility ability) {

View file

@ -2,6 +2,7 @@ package mage.cards.m;
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 @@ class MagusOfTheBridgeTriggeredAbility extends TriggeredAbilityImpl {
}
return false;
}
@Override
public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
}
}

View file

@ -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) {

View file

@ -1985,33 +1985,52 @@ public class VerifyCardDataTest {
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<String> 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<String> 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

View file

@ -1290,7 +1290,7 @@ public abstract class AbilityImpl implements Ability {
}
@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());

View file

@ -47,6 +47,7 @@ public class HauntAbility extends TriggeredAbilityImpl {
setTriggerPhrase((creatureHaunt ? "When {this} enters or the creature it haunts dies, "
: "When the creature {this} haunts dies, ")
);
setLeavesTheBattlefieldTrigger(true);
}
private HauntAbility(final HauntAbility ability) {