Improve attachment to permanent logic; implement [PIP] Codsworth, Handy Helper (#12098)

* [PIP] Implement Codsworth, Handy Helper

* Fix Codsworth and Halvar

* Write tests for attachments

* Fix auras going to graveyard when attaching to illegal targets

* Fix Captured by the Consulate interaction

* Fix failing tests, add additional test

* Add source name to log message

* Implement requested changes

* Revert removed null check

* Remove filter check, clean up code

* Add additional test

* Fix failing roles test

* Account for all current attachment edge cases

* Implement rule 303.4g

* Apply requested changes
This commit is contained in:
PurpleCrowbar 2024-08-24 06:03:33 +01:00 committed by GitHub
parent 9fcbfdeac6
commit 8d249aa691
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 580 additions and 20 deletions

View file

@ -0,0 +1,168 @@
package mage.cards.c;
import mage.ConditionalMana;
import mage.MageInt;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.ActivateAsSorceryActivatedAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.continuous.GainAbilityControlledEffect;
import mage.abilities.keyword.WardAbility;
import mage.abilities.mana.ConditionalColoredManaAbility;
import mage.abilities.mana.builder.ConditionalManaBuilder;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.CommanderPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author PurpleCrowbar
*/
public final class CodsworthHandyHelper extends CardImpl {
private static final FilterPermanent filter = new FilterControlledPermanent("commanders");
private static final FilterControlledPermanent filter2 = new FilterControlledPermanent("Aura or Equipment you control");
static {
filter.add(CommanderPredicate.instance);
filter2.add(Predicates.or(SubType.AURA.getPredicate(), SubType.EQUIPMENT.getPredicate()));
}
public CodsworthHandyHelper(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{2}{W}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.ROBOT);
this.power = new MageInt(2);
this.toughness = new MageInt(3);
// Commanders you control have ward {2}.
this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect(
new WardAbility(new GenericManaCost(2)), Duration.WhileOnBattlefield, filter
)));
// {T}: Add {W}{W}. Spend this mana only to cast Aura and/or Equipment spells.
this.addAbility(new ConditionalColoredManaAbility(
new TapSourceCost(), Mana.WhiteMana(2),
new CodsworthHandyHelperManaBuilder()
));
// {T}: Attach target Aura or Equipment you control to target creature you control. Activate only as a sorcery.
Ability ability = new ActivateAsSorceryActivatedAbility(new CodsworthHandyHelperEffect(), new TapSourceCost());
ability.addTarget(new TargetPermanent(filter2));
ability.addTarget(new TargetControlledCreaturePermanent());
this.addAbility(ability);
}
private CodsworthHandyHelper(final CodsworthHandyHelper card) {
super(card);
}
@Override
public CodsworthHandyHelper copy() {
return new CodsworthHandyHelper(this);
}
}
class CodsworthHandyHelperManaBuilder extends ConditionalManaBuilder {
@Override
public ConditionalMana build(Object... options) {
return new CodsworthHandyHelperConditionalMana(this.mana);
}
@Override
public String getRule() {
return "Spend this mana only to cast Aura and/or Equipment spells";
}
}
class CodsworthHandyHelperConditionalMana extends ConditionalMana {
public CodsworthHandyHelperConditionalMana(Mana mana) {
super(mana);
addCondition(new CodsworthHandyHelperManaCondition());
}
private CodsworthHandyHelperConditionalMana(final CodsworthHandyHelperConditionalMana conditionalMana) {
super(conditionalMana);
}
@Override
public CodsworthHandyHelperConditionalMana copy() {
return new CodsworthHandyHelperConditionalMana(this);
}
}
class CodsworthHandyHelperManaCondition implements Condition {
@Override
public boolean apply(Game game, Ability source) {
if (source instanceof SpellAbility) {
Card card = game.getCard(source.getSourceId());
return card != null && (
card.getSubtype(game).contains(SubType.AURA) || card.getSubtype(game).contains(SubType.EQUIPMENT)
);
}
return false;
}
}
class CodsworthHandyHelperEffect extends OneShotEffect {
CodsworthHandyHelperEffect() {
super(Outcome.Benefit);
staticText = "Attach target Aura or Equipment you control to target creature you control";
}
private CodsworthHandyHelperEffect(final CodsworthHandyHelperEffect effect) {
super(effect);
}
@Override
public CodsworthHandyHelperEffect copy() {
return new CodsworthHandyHelperEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
Permanent attachment = game.getPermanent(source.getTargets().get(0).getFirstTarget());
Permanent creature = game.getPermanent(source.getTargets().get(1).getFirstTarget());
if (controller == null || attachment == null || creature == null) {
return false;
}
if (creature.cantBeAttachedBy(attachment, source, game, true)) {
game.informPlayers(attachment.getLogName() + " was not attached to " + creature.getLogName()
+ " because it's not a legal target" + CardUtil.getSourceLogName(game, source));
return false;
}
Permanent oldCreature = game.getPermanent(attachment.getAttachedTo());
if (oldCreature != null) {
oldCreature.removeAttachment(attachment.getId(), source, game);
}
creature.addAttachment(attachment.getId(), source, game);
game.informPlayers(attachment.getLogName() + " was "
+ (oldCreature != null ? "unattached from " + oldCreature.getLogName() + " and " : "")
+ "attached to " + creature.getLogName()
);
return true;
}
}

View file

@ -28,7 +28,6 @@ import mage.filter.predicate.permanent.EquippedPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledCreaturePermanent;
@ -126,17 +125,12 @@ class HalvarGodOfBattleEffect extends OneShotEffect {
if (controller != null && attachment != null && creature != null && creature.isControlledBy(controller.getId())) {
Permanent oldCreature = game.getPermanent(attachment.getAttachedTo());
if (oldCreature != null && oldCreature.isControlledBy(controller.getId()) && !oldCreature.equals(creature)) {
boolean canAttach = true;
if (attachment.hasSubtype(SubType.AURA, game)) {
Target auraTarget = attachment.getSpellAbility().getTargets().get(0);
if (!auraTarget.canTarget(creature.getId(), game)) {
canAttach = false;
}
}
if (!canAttach) {
if (creature.cantBeAttachedBy(attachment, source, game, true)) {
game.informPlayers(attachment.getLogName() + " was not attached to " + creature.getLogName()
+ " because it's not a legal target for the aura");
} else if (controller.chooseUse(Outcome.BoostCreature, "Attach " + attachment.getLogName()
+ " because it's not a legal target");
return false;
}
if (controller.chooseUse(Outcome.BoostCreature, "Attach " + attachment.getLogName()
+ " to " + creature.getLogName() + "?", source, game)) {
oldCreature.removeAttachment(attachment.getId(), source, game);
creature.addAttachment(attachment.getId(), source, game);

View file

@ -85,6 +85,8 @@ public final class Fallout extends ExpansionSet {
cards.add(new SetCardInfo("Charisma Bobblehead", 130, Rarity.UNCOMMON, mage.cards.c.CharismaBobblehead.class));
cards.add(new SetCardInfo("Cinder Glade", 257, Rarity.RARE, mage.cards.c.CinderGlade.class));
cards.add(new SetCardInfo("Clifftop Retreat", 258, Rarity.RARE, mage.cards.c.ClifftopRetreat.class));
cards.add(new SetCardInfo("Codsworth, Handy Helper", 14, Rarity.RARE, mage.cards.c.CodsworthHandyHelper.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Codsworth, Handy Helper", 366, Rarity.RARE, mage.cards.c.CodsworthHandyHelper.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Colonel Autumn", 98, Rarity.RARE, mage.cards.c.ColonelAutumn.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Colonel Autumn", 411, Rarity.RARE, mage.cards.c.ColonelAutumn.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Colonel Autumn", 626, Rarity.RARE, mage.cards.c.ColonelAutumn.class, NON_FULL_USE_VARIOUS));

View file

@ -0,0 +1,374 @@
package org.mage.test.cards.rules;
import mage.constants.CardType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author PurpleCrowbar
*/
public class AttachmentTest extends CardTestPlayerBase {
// {T}: Attach target Aura or Equipment you control to target creature you control. Activate only as a sorcery.
private static final String codsworth = "Codsworth, Handy Helper";
/**
* Tests that a permanent that becomes non-attachable (i.e., non-aura, non-equipment, non-fortification)
* due to loss of card type before resolution of an ability is not still attached.
*/
@Test
public void testAttachNonAttachable() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, codsworth);
addCard(Zone.BATTLEFIELD, playerA, "Lion Sash"); // Reconfigurable equipment creature
addCard(Zone.BATTLEFIELD, playerA, "Vedalken Orrery"); // You may cast spells as though they had flash
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
addCard(Zone.HAND, playerA, "Darksteel Mutation"); // Enchanted creature loses equipment type
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Attach", "Lion Sash");
addTarget(playerA, codsworth);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Darksteel Mutation", "Lion Sash");
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertAttachedTo(playerA, "Lion Sash", codsworth, false);
assertGraveyardCount(playerA, "Darksteel Mutation", 0);
assertType("Lion Sash", CardType.CREATURE, true);
}
/**
* Tests that protection prevents attachment. Attachment should remain attached to whatever it was attached to.
*/
@Test
public void testProtectionPreventsAttachment() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, codsworth);
addCard(Zone.BATTLEFIELD, playerA, "Candlestick"); // Blue equipment
addCard(Zone.HAND, playerA, "Agoraphobia"); // Blue aura
addCard(Zone.BATTLEFIELD, playerA, "Bloated Toad"); // Protection from blue
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", codsworth);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Agoraphobia", codsworth);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Attach", "Candlestick");
addTarget(playerA, "Bloated Toad");
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Attach", "Agoraphobia");
addTarget(playerA, "Bloated Toad");
setStopAt(3, PhaseStep.END_TURN);
execute();
assertAttachedTo(playerA, "Candlestick", codsworth, true);
assertAttachedTo(playerA, "Agoraphobia", codsworth, true);
}
/**
* Tests that attachments fall off a permanent that gains the appropriate protection.
*/
@Test
public void testProtectionShedsAttachments() {
setStrictChooseMode(true);
// {R}: {this} gains protection from red until end of turn.
addCard(Zone.BATTLEFIELD, playerA, "Keeper of Kookus");
addCard(Zone.BATTLEFIELD, playerA, "Jackhammer"); // Red equipment
addCard(Zone.HAND, playerA, "Agility"); // Red aura
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", "Keeper of Kookus");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Agility", "Keeper of Kookus");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{R}:");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Agility", 1);
assertPermanentCount(playerA, "Jackhammer", 1);
assertAttachedTo(playerA, "Jackhammer", "Keeper of Kookus", false);
}
/**
* Tests that attachments can only attach to legal targets. Specifically,
* that an "Enchant land" aura cannot be attached to a nonland creature.
*/
@Test
public void testDeniedMoveIllegalAuraTarget() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, codsworth);
addCard(Zone.BATTLEFIELD, playerA, "Forest");
addCard(Zone.HAND, playerA, "Wild Growth"); // Enchant land aura
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wild Growth", "Forest");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Attach", "Wild Growth");
addTarget(playerA, codsworth);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Wild Growth", 0);
assertAttachedTo(playerA, "Wild Growth", "Forest", true);
}
/**
* Tests that an equipment cannot be moved to a noncreature permanent.
* Equipment should remain attached to whatever it was attached to.
*/
@Test
public void testDeniedMoveIllegalEquipmentTarget() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, codsworth);
addCard(Zone.BATTLEFIELD, playerA, "Bonesplitter"); // Equip {1}
addCard(Zone.BATTLEFIELD, playerA, "Bonebreaker Giant"); // Arbitrary creature
addCard(Zone.BATTLEFIELD, playerA, "Vedalken Orrery"); // You may cast spells as though they had flash
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.HAND, playerA, "One with the Stars"); // Enchanted permanent loses creature type
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {1}", codsworth);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Attach", "Bonesplitter");
addTarget(playerA, "Bonebreaker Giant");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "One with the Stars", "Bonebreaker Giant");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAttachedTo(playerA, "Bonesplitter", codsworth, true);
assertGraveyardCount(playerA, 0);
}
/**
* Tests that an enchant ability prohibition not pertaining to card type is respected when trying to move an aura.
* In this case, an "Enchant creature you don't control" aura shouldn't be movable to a controlled creature.
*/
@Test
public void testDeniedMoveIllegalAuraTargetNotCardType() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, codsworth);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
addCard(Zone.HAND, playerA, "Captured by the Consulate"); // Enchant creature you don't control
addCard(Zone.BATTLEFIELD, playerB, "Pia Nalaar"); // Arbitrary creature
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}.", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Captured by the Consulate", "Pia Nalaar");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Attach", "Captured by the Consulate");
addTarget(playerA, codsworth);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, 0);
assertAttachedTo(playerB, "Captured by the Consulate", "Pia Nalaar", true);
}
/**
* Tests that an aura that attaches to a player (specifically an opponent in this case) cannot attach to a permanent.
*/
@Test
public void testDeniedMoveIllegalPlayerAuraTarget() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, codsworth);
addCard(Zone.HAND, playerA, "Psychic Possession"); // Enchant opponent
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}.", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Psychic Possession", playerB);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Attach", "Psychic Possession");
addTarget(playerA, codsworth);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Psychic Possession", 0); // TODO: Currently, the aura goes to graveyard. Must fix
assertAttachedTo(playerA, "Psychic Possession", codsworth, false);
}
/**
* Tests that an aura that attaches to a card in the graveyard cannot attach to a permanent.
*/
@Test
public void testDeniedMoveIllegalCardAuraTarget() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, codsworth);
addCard(Zone.HAND, playerA, "Spellweaver Volute"); // Enchant instant card in graveyard
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.GRAVEYARD, playerA, "Counterspell"); // Arbitrary instant
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}.", 5);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spellweaver Volute", "Counterspell");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Attach", "Spellweaver Volute");
addTarget(playerA, codsworth);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Spellweaver Volute", 0); // TODO: Currently goes to graveyard. Must fix
assertAttachedTo(playerA, "Spellweaver Volute", codsworth, false);
}
/**
* Tests that when a player gains protection from an aura, it falls off (is moved to the graveyard).
*/
@Test
public void testCurseFallsOffFromGainedProtection() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, "Curse of Opulence"); // Arbitrary curse (enchant player)
addCard(Zone.HAND, playerB, "Runed Halo"); // You have protection from the chosen card name.
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Curse of Opulence", playerB);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Runed Halo");
setChoice(playerB, "Curse of Opulence");
setStopAt(2, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Curse of Opulence", 1);
}
/**
* Tests that when an aura is cast and the target becomes illegal (in this
* case, due to a change in card type), the aura goes to the graveyard.
*/
@Test
public void testCastAuraIllegalTarget() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, "Arcane Flight"); // Arbitrary "enchant creature" aura
addCard(Zone.HAND, playerA, "One with the Stars"); // Enchanted permanent loses creature type
addCard(Zone.BATTLEFIELD, playerA, "Runeclaw Bear"); // Arbitrary creature
addCard(Zone.BATTLEFIELD, playerA, "Vedalken Orrery"); // You may cast spells as though they had flash
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arcane Flight", "Runeclaw Bear");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "One with the Stars", "Runeclaw Bear");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Arcane Flight", 1);
}
/**
* Tests that when an aura tries to move from a player's hand without being cast, and
* it has no legal objects to attach to, it instead remains in the player's hand.
*/
@Test
public void testAuraMoveFromHandWithNoAttachableObject() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, "Show and Tell"); // Puts an aura from hand onto the battlefield without casting
addCard(Zone.HAND, playerA, "Aether Tunnel"); // Enchant creature
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Show and Tell");
setChoice(playerA, true);
setChoice(playerB, false);
addTarget(playerA, "Aether Tunnel");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Aether Tunnel", 0);
assertHandCount(playerA, "Aether Tunnel", 1);
}
/**
* Tests that when an aura tries to move from a player's graveyard without being cast, and
* it has no legal objects to attach to, it instead remains in the player's graveyard.
*/
@Test
public void testAuraMoveFromGraveyardWithNoAttachableObject() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, "Replenish"); // Put all enchantments from graveyard onto battlefield
addCard(Zone.GRAVEYARD, playerA, "Divine Favor"); // Enchant creature, gain 3 life on ETB
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Replenish");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Divine Favor", 1);
assertLife(playerA, 20);
}
/**
* Tests that when an aura tries to move from exile without being cast, and
* it has no legal objects to attach to, it instead remains in exile.
*/
@Test
public void testAuraMoveFromExileWithNoAttachableObject() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, "Sudden Disappearance"); // Exile nonland permanents, return at end of turn
addCard(Zone.HAND, playerA, "Spiritual Visit"); // Create a 1/1 token
addCard(Zone.HAND, playerA, "Divine Favor"); // Enchant creature, gain 3 life on ETB
addCard(Zone.BATTLEFIELD, playerA, "Plains", 9);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spiritual Visit");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Divine Favor", "Spirit Token");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sudden Disappearance", playerA);
waitStackResolved(1, PhaseStep.END_TURN, playerA);
setStopAt(2, PhaseStep.UNTAP);
execute();
assertGraveyardCount(playerA, "Divine Favor", 0);
assertExileCount(playerA, "Divine Favor", 1);
assertLife(playerA, 23);
}
/**
* Tests that Dream Leash can correctly attach to untapped permanents when it enters through non-casting means (e.g., via Show and Tell)
*/
@Ignore
@Test
public void testDreamLeashNoUntappedPermanents() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, "Omniscience");
// Enchant permanent
// You can't choose an untapped permanent as Dream Leash's target as you cast Dream Leash.
// You control enchanted permanent.
addCard(Zone.HAND, playerA, "Dream Leash");
addCard(Zone.HAND, playerA, "Show and Tell"); // Puts a permanent directly into play without casting
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Show and Tell");
setChoice(playerA, true, 2); // 1. Cast without paying cost? 2. Put permanent into play?
setChoice(playerB, false); // Put permanent into play?
addTarget(playerA, "Dream Leash"); // Show and Tell's target
setChoice(playerA, "Omniscience"); // Dream Leash's choice
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Dream Leash", 0);
assertAttachedTo(playerA, "Dream Leash", "Omniscience", true);
assertHandCount(playerA, "Dream Leash", 0);
}
}

View file

@ -774,7 +774,7 @@ public class ContinuousEffects implements Serializable {
}
/**
* Checks if an event won't happen because of an rule modifying effect
* Checks if an event won't happen because of a rule modifying effect
*
* @param event
* @param targetAbility ability the event is attached to. can be null.

View file

@ -21,7 +21,7 @@ import mage.constants.*;
import mage.counters.Counter;
import mage.counters.CounterType;
import mage.counters.Counters;
import mage.filter.FilterOpponent;
import mage.filter.*;
import mage.game.Game;
import mage.game.GameState;
import mage.game.ZoneChangeInfo;
@ -1355,7 +1355,20 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return !ability.getDoesntRemoveControlled() || isControlledBy(game.getControllerId(attachment.getId()));
}
}
return game.getContinuousEffects().preventedByRuleModification(new StayAttachedEvent(this.getId(), attachment.getId(), source), null, game, silentMode);
boolean canAttach = true;
Permanent attachmentPermanent = game.getPermanent(attachment.getId());
// If attachment is an aura, ensures this permanent can still be legally enchanted, according to the enchantment's Enchant ability
if (attachment.hasSubtype(SubType.AURA, game)
&& attachmentPermanent != null
&& attachmentPermanent.getSpellAbility() != null
&& !attachmentPermanent.getSpellAbility().getTargets().isEmpty()) {
// Line of code below functionally gets the target of the aura's Enchant ability, then compares to this permanent. Enchant improperly implemented in XMage, see #9583
// Note: stillLegalTarget used exclusively to account for Dream Leash. Can be made canTarget in the event that that card is rewritten (and "stillLegalTarget" removed from TargetImpl).
canAttach = attachmentPermanent.getSpellAbility().getTargets().get(0).copy().withNotTarget(true).stillLegalTarget(attachmentPermanent.getControllerId(), this.getId(), source, game);
}
return !canAttach || game.getContinuousEffects().preventedByRuleModification(new StayAttachedEvent(this.getId(), attachment.getId(), source), null, game, silentMode);
}
@Override

View file

@ -4836,6 +4836,15 @@ public abstract class PlayerImpl implements Player, Serializable {
if (enterTransformed != null && enterTransformed && !card.isTransformable()) {
continue;
}
// 303.4g. If an Aura is entering the battlefield and there is no legal object or player for it to enchant,
// the Aura remains in its current zone, unless that zone is the stack. In that case, the Aura is put into
// its owner's graveyard instead of entering the battlefield. If the Aura is a token, it isn't created.
if (card.hasSubtype(SubType.AURA, game)
&& card.getSpellAbility() != null
&& !card.getSpellAbility().getTargets().isEmpty()
&& !card.getSpellAbility().getTargets().get(0).copy().withNotTarget(true).canChoose(byOwner ? card.getOwnerId() : getId(), game)) {
continue;
}
ZoneChangeEvent event = new ZoneChangeEvent(card.getId(), source,
byOwner ? card.getOwnerId() : getId(), fromZone, Zone.BATTLEFIELD, appliedEffects);
infoList.add(new ZoneChangeInfo.Battlefield(event, faceDown, tapped, source));

View file

@ -73,7 +73,7 @@ public interface Target extends Serializable {
*/
boolean canTarget(UUID id, Ability source, Game game);
boolean stillLegalTarget(UUID id, Ability source, Game game);
boolean stillLegalTarget(UUID playerId, UUID id, Ability source, Game game);
boolean canTarget(UUID playerId, UUID id, Ability source, Game game);

View file

@ -409,7 +409,7 @@ public abstract class TargetImpl implements Target {
illegalTargets.add(targetId);
continue;
}
if (!stillLegalTarget(targetId, source, game)) {
if (!stillLegalTarget(source.getControllerId(), targetId, source, game)) {
illegalTargets.add(targetId);
}
}
@ -546,8 +546,8 @@ public abstract class TargetImpl implements Target {
}
@Override
public boolean stillLegalTarget(UUID id, Ability source, Game game) {
return canTarget(id, source, game);
public boolean stillLegalTarget(UUID controllerId, UUID id, Ability source, Game game) {
return canTarget(controllerId, id, source, game);
}
@Override

View file

@ -53,10 +53,10 @@ public class TargetTappedPermanentAsYouCast extends TargetPermanent {
// See ruling: https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/253345-dream-leash
@Override
public boolean stillLegalTarget(UUID id, Ability source, Game game) {
public boolean stillLegalTarget(UUID controllerId, UUID id, Ability source, Game game) {
Permanent permanent = game.getPermanent(id);
return permanent != null
&& getFilter().match(permanent, game)
&& super.canTarget(id, game); // check everything but leave out the tapped requirement
&& super.canTarget(controllerId, id, source, game); // check everything but leave out the tapped requirement
}
}