Bestow rework/fixes (#13973)

* New Bestow test, minor improvements

* Partially rework Bestow to not rely on perpetual card modifications

* Add Bestow subtype tests, improve PrototypeTest

* Fix Subtype existing without required card type

* Improve docs, improve aura spell copy target copying check, improve subtype handling

* Add additional test

* Review improvements

* Remove subtype/type check

* Consolidate temporary becomeAura into function

* Add Enchant Creature ability
This commit is contained in:
ssk97 2025-09-27 23:57:31 -07:00 committed by GitHub
parent 95f53fc671
commit 8cd1bec19d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 248 additions and 166 deletions

View file

@ -6,14 +6,13 @@ import mage.abilities.SpellAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.Costs;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.common.AttachEffect;
import mage.cards.Card;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
import mage.util.CardUtil;
@ -90,9 +89,9 @@ public class BestowAbility extends SpellAbility {
TargetPermanent auraTarget = new TargetCreaturePermanent();
this.addTarget(auraTarget);
this.addEffect(new AttachEffect(Outcome.BoostCreature));
Ability ability = new SimpleStaticAbility(new BestowEntersBattlefieldEffect());
Ability ability = new SimpleStaticAbility(new BestowTypeEffect());
ability.setRuleVisible(false);
addSubAbility(ability);
this.addSubAbility(ability);
}
protected BestowAbility(final BestowAbility ability) {
@ -122,70 +121,65 @@ public class BestowAbility extends SpellAbility {
sb.append(" <i>(If you cast this card for its bestow cost, it's an Aura spell with enchant creature. It becomes a creature again if it's not attached to a creature.)</i>");
return sb.toString();
}
public static void becomeCreature(Permanent permanent, Game game) {
// permanently changes to the object
if (permanent != null) {
MageObject basicObject = permanent.getBasicMageObject();
if (basicObject != null) {
game.checkStateAndTriggered(); // Bug #8157
basicObject.getSubtype().remove(SubType.AURA);
basicObject.addCardType(CardType.CREATURE);
}
permanent.getSubtype().remove(SubType.AURA);
permanent.addCardType(CardType.CREATURE);
}
}
public static void becomeAura(Card card) {
// permanently changes to the object
// permanently changes to the object, only use on copies
if (card != null) {
if (!card.getCardType().contains(CardType.ENCHANTMENT)) {
throw new IllegalStateException("Bestow perpetual becomeAura called on non-enchantment card");
}
card.addSubType(SubType.AURA);
card.removeCardType(CardType.CREATURE);
card.addCardType(CardType.ENCHANTMENT);
card.removeAllCreatureTypes();
if (card instanceof Spell) {
((Spell) card).addAbilityForCopy(new EnchantAbility(new TargetCreaturePermanent()));
} else {
card.addAbility(new EnchantAbility(new TargetCreaturePermanent()));
}
}
}
public static void becomeAura(Game game, MageObject object) {
// temporary changes only
if (object != null && object.getCardType(game).contains(CardType.ENCHANTMENT)) {
object.addSubType(game, SubType.AURA);
object.removeCardType(game, CardType.CREATURE);
object.removeAllCreatureTypes(game);
if (object instanceof Permanent) {
((Permanent) object).addAbility(new EnchantAbility(new TargetCreaturePermanent()), object.getId(), game);
} else if (object instanceof Spell) {
game.getState().addOtherAbility(((Spell) object).getCard(), new EnchantAbility(new TargetCreaturePermanent()));
} else if (object instanceof Card) {
game.getState().addOtherAbility((Card) object, new EnchantAbility(new TargetCreaturePermanent()));
} else {
throw new IllegalArgumentException("Bestow temporary becomeAura called on non-Permanent non-Spell object: " + object.getClass().getName());
}
}
}
}
class BestowEntersBattlefieldEffect extends ReplacementEffectImpl {
class BestowTypeEffect extends ContinuousEffectImpl {
public BestowEntersBattlefieldEffect() {
super(Duration.WhileOnBattlefield, Outcome.Neutral);
BestowTypeEffect() {
super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Benefit);
}
protected BestowEntersBattlefieldEffect(final BestowEntersBattlefieldEffect effect) {
private BestowTypeEffect(final BestowTypeEffect effect) {
super(effect);
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return EventType.ENTERS_THE_BATTLEFIELD_SELF == event.getType();
public BestowTypeEffect copy() {
return new BestowTypeEffect(this);
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
return event.getTargetId().equals(source.getSourceId());
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Permanent bestowPermanent = game.getPermanentEntering(source.getSourceId());
if (bestowPermanent == null || !bestowPermanent.hasSubtype(SubType.AURA, game)) {
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return false;
}
// change types permanently
MageObject basicObject = bestowPermanent.getBasicMageObject();
if (basicObject != null && !basicObject.getSubtype().contains(SubType.AURA)) {
basicObject.addSubType(SubType.AURA);
basicObject.removeCardType(CardType.CREATURE);
if (game.getPermanent(permanent.getAttachedTo()) != null){
BestowAbility.becomeAura(game, permanent);
}
return false;
return true;
}
@Override
public BestowEntersBattlefieldEffect copy() {
return new BestowEntersBattlefieldEffect(this);
}
}

View file

@ -135,6 +135,7 @@ class ReconfigureTypeEffect extends ContinuousEffectImpl {
return false;
}
permanent.removeCardType(game, CardType.CREATURE);
permanent.removeAllCreatureTypes(game);
return true;
}
}

View file

@ -2670,10 +2670,9 @@ public abstract class GameImpl implements Game {
Permanent attachedTo = getPermanent(perm.getAttachedTo());
if (attachedTo == null || !attachedTo.getAttachments().contains(perm.getId())) {
// handle bestow unattachment
Card card = this.getCard(perm.getId());
if (card != null && card.isCreature(this)) {
if (perm.getAbilities().stream().anyMatch(x -> x instanceof BestowAbility)) {
UUID wasAttachedTo = perm.getAttachedTo();
perm.attachTo(null, null, this);
perm.unattach(this);
fireEvent(new UnattachedEvent(wasAttachedTo, perm.getId(), perm, null));
} else if (movePermanentToGraveyardWithInfo(perm)) {
somethingHappened = true;
@ -2683,11 +2682,9 @@ public abstract class GameImpl implements Game {
if (auraFilter instanceof FilterPermanent) {
if (!((FilterPermanent) auraFilter).match(attachedTo, perm.getControllerId(), perm.getSpellAbility(), this)
|| attachedTo.cantBeAttachedBy(perm, null, this, true)) {
Card card = this.getCard(perm.getId());
if (card != null && card.isCreature(this)) {
if (perm.getAbilities().stream().anyMatch(x -> x instanceof BestowAbility)) {
UUID wasAttachedTo = perm.getAttachedTo();
perm.attachTo(null, null, this);
BestowAbility.becomeCreature(perm, this);
perm.unattach(this);
fireEvent(new UnattachedEvent(wasAttachedTo, perm.getId(), perm, null));
} else if (movePermanentToGraveyardWithInfo(perm)) {
somethingHappened = true;
@ -2695,11 +2692,9 @@ public abstract class GameImpl implements Game {
}
} else if (!auraFilter.match(attachedTo, this) || attachedTo.cantBeAttachedBy(perm, null, this, true)) {
// handle bestow unattachment
Card card = this.getCard(perm.getId());
if (card != null && card.isCreature(this)) {
if (perm.getAbilities().stream().anyMatch(x -> x instanceof BestowAbility)) {
UUID wasAttachedTo = perm.getAttachedTo();
perm.attachTo(null, null, this);
BestowAbility.becomeCreature(perm, this);
perm.unattach(this);
fireEvent(new UnattachedEvent(wasAttachedTo, perm.getId(), perm, null));
} else if (movePermanentToGraveyardWithInfo(perm)) {
somethingHappened = true;

View file

@ -2061,24 +2061,6 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return color;
}
//20180810 - 701.3d
//If an object leaves the zone it's in, all attached permanents become unattached
//note that this code doesn't actually detach anything, and is a bit of a bandaid
public void detachAllAttachments(Game game) {
for (UUID attachmentId : getAttachments()) {
Permanent attachment = game.getPermanent(attachmentId);
Card attachmentCard = game.getCard(attachmentId);
if (attachment != null && attachmentCard != null) {
//make bestow cards and licids into creatures
//aura test to stop bludgeon brawl shenanigans from using this code
//consider adding code to handle that case?
if (attachment.hasSubtype(SubType.AURA, game) && attachmentCard.isCreature(game)) {
BestowAbility.becomeCreature(attachment, game);
}
}
}
}
@Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
Zone fromZone = game.getState().getZone(objectId);
@ -2091,12 +2073,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
} else {
zoneChangeInfo = new ZoneChangeInfo(event);
}
boolean successfullyMoved = ZonesHandler.moveCard(zoneChangeInfo, game, source);
//20180810 - 701.3d
if (successfullyMoved) {
detachAllAttachments(game);
}
return successfullyMoved;
return ZonesHandler.moveCard(zoneChangeInfo, game, source);
}
return false;
}
@ -2107,11 +2084,6 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
ZoneChangeEvent event = new ZoneChangeEvent(this, source, ownerId, fromZone, Zone.EXILED, appliedEffects);
ZoneChangeInfo.Exile zcInfo = new ZoneChangeInfo.Exile(event, exileId, name);
boolean successfullyMoved = ZonesHandler.moveCard(zcInfo, game, source);
//20180810 - 701.3d
if (successfullyMoved) {
detachAllAttachments(game);
}
return successfullyMoved;
return ZonesHandler.moveCard(zcInfo, game, source);
}
}

View file

@ -20,7 +20,6 @@ import mage.game.MageObjectAttribute;
import mage.game.events.CopiedStackObjectEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.token.Token;
import mage.players.Player;
import mage.util.CardUtil;
@ -335,45 +334,39 @@ public class Spell extends StackObjectImpl implements Card {
counter(null, /*this.getSpellAbility()*/ game);
return false;
} else if (this.isEnchantment(game) && this.hasSubtype(SubType.AURA, game)) {
boolean bestow = SpellAbilityCastMode.BESTOW.equals(ability.getSpellAbilityCastMode());
if (ability.getTargets().stillLegal(ability, game)) {
boolean bestow = SpellAbilityCastMode.BESTOW.equals(ability.getSpellAbilityCastMode());
if (bestow) {
// before put to play:
// Must be removed first time, after that will be removed by continous effect
// Otherwise effects like evolve trigger from creature comes into play event
card.removeCardType(CardType.CREATURE);
card.addSubType(game, SubType.AURA);
BestowAbility.becomeAura(game, card);
}
UUID permId;
boolean flag;
boolean permanentCreated;
if (isCopy()) {
Token token = CopyTokenFunction.createTokenCopy(card, game, this);
// The token that a resolving copy of a spell becomes isnt said to have been created. (2020-09-25)
if (token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false)) {
permId = token.getLastAddedTokenIds().stream().findFirst().orElse(null);
flag = true;
permanentCreated = true;
} else {
permId = null;
flag = false;
permanentCreated = false;
}
} else {
permId = card.getId();
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
flag = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
permanentCreated = controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
}
if (flag) {
if (permanentCreated) {
if (bestow) {
// card will be copied during putOntoBattlefield, so the card of CardPermanent has to be changed
// TODO: Find a better way to prevent bestow creatures from being effected by creature affecting abilities
Permanent permanent = game.getPermanent(permId);
if (permanent instanceof PermanentCard) {
// after put to play:
// restore removed stats (see "before put to play" above)
permanent.setSpellAbility(ability); // otherwise spell ability without bestow will be set
card.addCardType(CardType.CREATURE);
card.getSubtype().remove(SubType.AURA);
}
permanent.setSpellAbility(ability); // otherwise spell ability without bestow will be set
// The continuous effect that makes the permanent an aura doesn't apply until after the permanent has already entered,
// so it must be modified manually here first. Same root cause as the Blood Moon problem https://github.com/magefree/mage/issues/4202
BestowAbility.becomeAura(game, permanent);
}
if (isCopy()) {
Permanent token = game.getPermanent(permId);
@ -381,7 +374,8 @@ public class Spell extends StackObjectImpl implements Card {
return false;
}
for (Ability ability2 : token.getAbilities()) {
if (!bestow || ability2 instanceof BestowAbility) {
if (ability2 instanceof SpellAbility && ability2.getTargets().size() == 1) {
// Copy aura SpellAbility's targets into the new token's SpellAbility
ability2.getTargets().get(0).add(ability.getFirstTarget(), game);
ability2.getEffects().get(0).apply(game, ability2);
return ability2.resolve(game);
@ -391,24 +385,13 @@ public class Spell extends StackObjectImpl implements Card {
}
return ability.resolve(game);
}
if (bestow) {
card.addCardType(game, CardType.CREATURE);
}
return false;
}
// Aura has no legal target and its a bestow enchantment -> Add it to battlefield as creature
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) {
if (bestow) {
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
if (controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null)) {
Permanent permanent = game.getPermanent(card.getId());
if (permanent instanceof PermanentCard) {
((PermanentCard) permanent).getCard().addCardType(game, CardType.CREATURE);
((PermanentCard) permanent).getCard().removeSubType(game, SubType.AURA);
return true;
}
}
return false;
return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
} else {
//20091005 - 608.2b
if (!game.isSimulation()) {
@ -585,10 +568,8 @@ public class Spell extends StackObjectImpl implements Card {
return cardTypes;
}
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) {
List<CardType> cardTypes = new ArrayList<>();
cardTypes.addAll(card.getCardType(game));
cardTypes.remove(CardType.CREATURE);
return cardTypes;
Card modifiedCard = this.getSpellAbility().getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(card, this.getSpellAbility(), game);
return modifiedCard.getCardType(game);
}
return card.getCardType(game);
}
@ -600,26 +581,19 @@ public class Spell extends StackObjectImpl implements Card {
@Override
public SubTypes getSubtype(Game game) {
// Bestow's changes are non-copiable, and must be reapplied
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) {
SubTypes subtypes = card.getSubtype(game);
if (!subtypes.contains(SubType.AURA)) { // do it only once
subtypes.add(SubType.AURA);
}
return subtypes;
Card modifiedCard = this.getSpellAbility().getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(card, this.getSpellAbility(), game);
return modifiedCard.getSubtype();
}
return card.getSubtype(game);
}
@Override
public boolean hasSubtype(SubType subtype, Game game) {
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) { // workaround for Bestow (don't like it)
SubTypes subtypes = card.getSubtype(game);
if (!subtypes.contains(SubType.AURA)) { // do it only once
subtypes.add(SubType.AURA);
}
if (subtypes.contains(subtype)) {
return true;
}
if (SpellAbilityCastMode.BESTOW.equals(this.getSpellAbility().getSpellAbilityCastMode())) {
Card modifiedCard = this.getSpellAbility().getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(card, this.getSpellAbility(), game);
return modifiedCard.hasSubtype(subtype, game);
}
return card.hasSubtype(subtype, game);
}