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; package mage.cards.c;
import java.util.UUID; import java.util.UUID;
@ -39,6 +38,7 @@ public final class CallerOfTheClaw extends CardImpl {
// Flash // Flash
this.addAbility(FlashAbility.getInstance()); 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. // 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()); this.getSpellAbility().addWatcher(new CallerOfTheClawWatcher());
Effect effect = new CreateTokenEffect(new BearToken(), new CallerOfTheClawDynamicValue()); Effect effect = new CreateTokenEffect(new BearToken(), new CallerOfTheClawDynamicValue());

View file

@ -1,6 +1,8 @@
package mage.cards.d; package mage.cards.d;
import java.util.UUID; import java.util.UUID;
import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
@ -91,6 +93,7 @@ class DiabolicServitudeCreatureDiesTriggeredAbility extends TriggeredAbilityImpl
public DiabolicServitudeCreatureDiesTriggeredAbility() { public DiabolicServitudeCreatureDiesTriggeredAbility() {
super(Zone.BATTLEFIELD, new DiabolicServitudeExileCreatureEffect(), false); super(Zone.BATTLEFIELD, new DiabolicServitudeExileCreatureEffect(), false);
setLeavesTheBattlefieldTrigger(true);
} }
private DiabolicServitudeCreatureDiesTriggeredAbility(final DiabolicServitudeCreatureDiesTriggeredAbility ability) { private DiabolicServitudeCreatureDiesTriggeredAbility(final DiabolicServitudeCreatureDiesTriggeredAbility ability) {
@ -123,6 +126,11 @@ class DiabolicServitudeCreatureDiesTriggeredAbility extends TriggeredAbilityImpl
public String getRule() { public String getRule() {
return "When the creature put onto the battlefield with {this} dies, exile it and return {this} to its owner's hand."; 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 { class DiabolicServitudeExileCreatureEffect extends OneShotEffect {

View file

@ -1,5 +1,6 @@
package mage.cards.e; package mage.cards.e;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbility;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
@ -97,6 +98,7 @@ class EndlessEvilBounceAbility extends TriggeredAbilityImpl {
public EndlessEvilBounceAbility() { public EndlessEvilBounceAbility() {
super(Zone.BATTLEFIELD, new ReturnToHandSourceEffect(false, true)); super(Zone.BATTLEFIELD, new ReturnToHandSourceEffect(false, true));
setLeavesTheBattlefieldTrigger(true);
} }
private EndlessEvilBounceAbility(final EndlessEvilBounceAbility effect) { private EndlessEvilBounceAbility(final EndlessEvilBounceAbility effect) {
@ -126,4 +128,9 @@ class EndlessEvilBounceAbility extends TriggeredAbilityImpl {
public String getRule() { public String getRule() {
return "When enchanted creature dies, if that creature was a Horror, return {this} to its owner's hand."; 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 java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
@ -65,6 +66,7 @@ class EnigmaSphinxTriggeredAbility extends TriggeredAbilityImpl {
public EnigmaSphinxTriggeredAbility(Effect effect, boolean optional) { public EnigmaSphinxTriggeredAbility(Effect effect, boolean optional) {
super(Zone.ALL, effect, optional); super(Zone.ALL, effect, optional);
setTriggerPhrase("When {this} is put into your graveyard from the battlefield, "); setTriggerPhrase("When {this} is put into your graveyard from the battlefield, ");
setLeavesTheBattlefieldTrigger(true);
} }
private EnigmaSphinxTriggeredAbility(final EnigmaSphinxTriggeredAbility ability) { private EnigmaSphinxTriggeredAbility(final EnigmaSphinxTriggeredAbility ability) {
@ -94,6 +96,11 @@ class EnigmaSphinxTriggeredAbility extends TriggeredAbilityImpl {
} }
return false; return false;
} }
@Override
public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game);
}
} }
class EnigmaSphinxEffect extends OneShotEffect { class EnigmaSphinxEffect extends OneShotEffect {

View file

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

View file

@ -2,6 +2,7 @@ package mage.cards.m;
import java.util.UUID; import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.MageObject;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.PutIntoGraveFromBattlefieldAllTriggeredAbility; import mage.abilities.common.PutIntoGraveFromBattlefieldAllTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
@ -59,6 +60,7 @@ class MagusOfTheBridgeTriggeredAbility extends TriggeredAbilityImpl {
public MagusOfTheBridgeTriggeredAbility() { public MagusOfTheBridgeTriggeredAbility() {
super(Zone.BATTLEFIELD, new ExileSourceEffect()); super(Zone.BATTLEFIELD, new ExileSourceEffect());
setTriggerPhrase("When a creature is put into an opponent's graveyard from the battlefield, "); setTriggerPhrase("When a creature is put into an opponent's graveyard from the battlefield, ");
setLeavesTheBattlefieldTrigger(true);
} }
private MagusOfTheBridgeTriggeredAbility(final MagusOfTheBridgeTriggeredAbility ability) { private MagusOfTheBridgeTriggeredAbility(final MagusOfTheBridgeTriggeredAbility ability) {
@ -86,4 +88,9 @@ class MagusOfTheBridgeTriggeredAbility extends TriggeredAbilityImpl {
} }
return false; 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)); super(Zone.BATTLEFIELD, new ScryEffect(1, false));
this.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance()).concatBy("and")); this.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance()).concatBy("and"));
this.setTriggerPhrase("Whenever another creature you control leaves the battlefield, if it didn't die, "); this.setTriggerPhrase("Whenever another creature you control leaves the battlefield, if it didn't die, ");
setLeavesTheBattlefieldTrigger(true);
} }
private TaekoThePatientAvalancheTriggeredAbility(final TaekoThePatientAvalancheTriggeredAbility ability) { 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"); fail(card, "abilities", "mutate cards aren't implemented and shouldn't be available");
} }
// special check: wrong dies triggers // special check: wrong dies triggers (there are also a runtime check on wrong usage, see isInUseableZoneDiesTrigger)
card.getAbilities().stream() Set<String> ignoredCards = new HashSet<>();
.filter(a -> a instanceof TriggeredAbility) ignoredCards.add("Caller of the Claw");
.map(a -> (TriggeredAbility) a) ignoredCards.add("Boneyard Scourge");
.filter(a -> !a.isLeavesTheBattlefieldTrigger()) ignoredCards.add("Fell Shepherd");
//.filter(a -> a.getRule().contains("whenever") || a.getRule().contains("Whenever")) // TODO: research failed cards ignoredCards.add("Massacre Girl");
.filter(a -> a.getRule().contains("die ") ignoredCards.add("Infested Thrinax");
|| a.getRule().contains("dies ") ignoredCards.add("Xira, the Golden Sting");
|| a.getRule().contains("die,") ignoredCards.add("Mawloc");
|| a.getRule().contains("dies,") List<String> ignoredAbilities = new ArrayList<>();
|| (a.getRule().contains("put into") ignoredAbilities.add("roll"); // roll die effects
&& a.getRule().contains("graveyard") ignoredAbilities.add("with \"When"); // token creating effects
&& a.getRule().contains("from the battlefield")) ignoredAbilities.add("gains \"When"); // token creating effects
) ignoredAbilities.add("and \"When"); // token creating effects
.filter(a -> !a.getRule().contains("roll")) // ignore roll die effects ignoredAbilities.add("it has \"When"); // token creating effects
.filter(a -> !a.getRule().contains("with \"When")) // ignore token creating effects ignoredAbilities.add("beginning of your end step"); // step triggers
.filter(a -> !a.getRule().contains("gains \"When")) // ignore token creating effects ignoredAbilities.add("beginning of each end step"); // step triggers
.filter(a -> !a.getRule().contains("and \"When")) // ignore token creating effects ignoredAbilities.add("beginning of combat"); // step triggers
.filter(a -> !a.getRule().contains("dies while {this} is in your graveyard")) // ignore Boneyard Scourge if (!ignoredCards.contains(card.getName())) {
.filter(a -> !a.getRule().contains("all creature cards that were put into your")) // ignore Fell Shepherd for (Ability ability : card.getAbilities()) {
.filter(a -> !card.getName().equals("Massacre Girl") // delayed trigger fixed, but verify check can't find it TriggeredAbility triggeredAbility = ability instanceof TriggeredAbility ? (TriggeredAbility) ability : null;
&& !card.getName().equals("Infested Thrinax") if (triggeredAbility == null) {
&& !card.getName().equals("Xira, the Golden Sting") continue;
) }
.forEach(a -> { // search and check dies related abilities
fail(card, "abilities", "dies trigger must use setLeavesTheBattlefieldTrigger(true) and override isInUseableZone - " + a.getClass().getSimpleName()); 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) // special check: duplicated words in ability text (wrong target/filter usage)
// example: You may exile __two two__ blue cards // example: You may exile __two two__ blue cards

View file

@ -1290,7 +1290,7 @@ public abstract class AbilityImpl implements Ability {
} }
@Override @Override
public boolean hasSourceObjectAbility(Game game, MageObject sourceObject, GameEvent event) { public final boolean hasSourceObjectAbility(Game game, MageObject sourceObject, GameEvent event) {
MageObject object = sourceObject; MageObject object = sourceObject;
if (object == null) { if (object == null) {
object = game.getPermanentEntering(getSourceId()); 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, " setTriggerPhrase((creatureHaunt ? "When {this} enters or the creature it haunts dies, "
: "When the creature {this} haunts dies, ") : "When the creature {this} haunts dies, ")
); );
setLeavesTheBattlefieldTrigger(true);
} }
private HauntAbility(final HauntAbility ability) { private HauntAbility(final HauntAbility ability) {