diff --git a/Mage.Sets/src/mage/cards/e/EstridTheMasked.java b/Mage.Sets/src/mage/cards/e/EstridTheMasked.java index 955e2794ab1..8b72f3e2495 100644 --- a/Mage.Sets/src/mage/cards/e/EstridTheMasked.java +++ b/Mage.Sets/src/mage/cards/e/EstridTheMasked.java @@ -1,20 +1,14 @@ package mage.cards.e; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.LoyaltyAbility; import mage.abilities.common.CanBeYourCommanderAbility; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.MillCardsControllerEffect; import mage.abilities.effects.common.UntapAllControllerEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.SubType; -import mage.constants.SuperType; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.common.FilterEnchantmentCard; import mage.filter.predicate.Predicates; @@ -26,8 +20,9 @@ import mage.game.permanent.token.MaskToken; import mage.players.Player; import mage.target.TargetPermanent; +import java.util.UUID; + /** - * * @author TheElk801 */ public final class EstridTheMasked extends CardImpl { @@ -98,17 +93,11 @@ class EstridTheMaskedTokenEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - CreateTokenEffect effect = new CreateTokenEffect(new MaskToken()); - effect.apply(game, source); - for (UUID tokenId : effect.getLastAddedTokenIds()) { - Permanent token = game.getPermanent(tokenId); - if (token == null) { - continue; - } - token.getAbilities().get(0).getTargets().get(0).add(source.getFirstTarget(), game); - token.getAbilities().get(0).getEffects().get(0).apply(game, token.getAbilities().get(0)); - } - return true; + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + return permanent != null && new MaskToken().putOntoBattlefield( + 1, game, source, source.getControllerId(), false, + false, null, permanent.getId() + ); } } diff --git a/Mage.Sets/src/mage/cards/s/SmokeSpiritsAid.java b/Mage.Sets/src/mage/cards/s/SmokeSpiritsAid.java index 8091e3c926e..2bf60604541 100644 --- a/Mage.Sets/src/mage/cards/s/SmokeSpiritsAid.java +++ b/Mage.Sets/src/mage/cards/s/SmokeSpiritsAid.java @@ -7,9 +7,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.game.Game; -import mage.game.permanent.Permanent; import mage.game.permanent.token.SmokeBlessingToken; -import mage.game.permanent.token.Token; import mage.target.common.TargetCreaturePermanent; import mage.target.targetadjustment.TargetAdjuster; @@ -70,21 +68,11 @@ class SmokeSpiritsAidEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Token token = new SmokeBlessingToken(); for (UUID targetId : getTargetPointer().getTargets(game, source)) { - Permanent permanent = game.getPermanent(targetId); - if (permanent == null) { - continue; - } - token.putOntoBattlefield(1, game, source); - for (UUID tokenId : token.getLastAddedTokenIds()) { - Permanent aura = game.getPermanent(tokenId); - if (aura == null) { - continue; - } - aura.getAbilities().get(0).getTargets().get(0).add(targetId, game); - aura.getAbilities().get(0).getEffects().get(0).apply(game, aura.getAbilities().get(0)); - } + new SmokeBlessingToken().putOntoBattlefield( + 1, game, source, source.getControllerId(), + false, false, null, targetId + ); } return true; } diff --git a/Mage.Sets/src/mage/cards/s/StanggEchoWarrior.java b/Mage.Sets/src/mage/cards/s/StanggEchoWarrior.java index 0e4215162e9..59f01980cc8 100644 --- a/Mage.Sets/src/mage/cards/s/StanggEchoWarrior.java +++ b/Mage.Sets/src/mage/cards/s/StanggEchoWarrior.java @@ -109,8 +109,8 @@ class StanggEchoWarriorEffect extends OneShotEffect { for (Permanent attachment : attachments) { CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect(); effect.setSavedPermanent(attachment); + effect.setAttachedTo(tokenId); effect.apply(game, source); - effect.getAddedPermanents().forEach(t -> permanent.addAttachment(t.getId(), source, game)); toSacrifice.addAll(effect.getAddedPermanents()); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/RoleTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/RoleTest.java index d5c6c8f5034..5123d699009 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/RoleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/RoleTest.java @@ -1,5 +1,6 @@ package org.mage.test.cards.enchantments; +import mage.abilities.keyword.TrampleAbility; import mage.constants.PhaseStep; import mage.constants.Zone; import org.junit.Test; @@ -11,41 +12,122 @@ import org.mage.test.serverside.base.CardTestPlayerBase; public class RoleTest extends CardTestPlayerBase { private static final String courtier = "Cursed Courtier"; - private static final String veteran = "Embereth Veteran"; + private static final String rage = "Monstrous Rage"; + private static final String wardens = "Nexus Wardens"; + private static final String murder = "Murder"; @Test public void testRegular() { addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.BATTLEFIELD, playerA, wardens); addCard(Zone.HAND, playerA, courtier); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courtier); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); assertPermanentCount(playerA, courtier, 1); assertPermanentCount(playerA, "Cursed", 1); assertPowerToughness(playerA, courtier, 1, 1); + assertLife(playerA, 20 + 2); } @Test public void testReplace() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); - addCard(Zone.BATTLEFIELD, playerA, veteran); + addCard(Zone.BATTLEFIELD, playerA, "Plateau", 4); + addCard(Zone.BATTLEFIELD, playerA, wardens); addCard(Zone.HAND, playerA, courtier); + addCard(Zone.HAND, playerA, rage); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courtier); - activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{1}, Sacrifice", courtier); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, rage, courtier); + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); assertPermanentCount(playerA, courtier, 1); - assertPermanentCount(playerA, veteran, 0); - assertGraveyardCount(playerA, veteran, 1); + assertGraveyardCount(playerA, rage, 1); assertPermanentCount(playerA, "Cursed", 0); - assertPermanentCount(playerA, "Young Hero", 1); - assertPowerToughness(playerA, courtier, 3, 3); + assertPermanentCount(playerA, "Monster", 1); + assertPowerToughness(playerA, courtier, 3 + 2 + 1, 3 + 1); + assertAbility(playerA, courtier, TrampleAbility.getInstance(), true); + assertLife(playerA, 20 + 2 + 2); + } + + @Test + public void testReplaceResponse() { + addCard(Zone.BATTLEFIELD, playerA, "Plateau", 4); + addCard(Zone.BATTLEFIELD, playerA, wardens); + addCard(Zone.HAND, playerA, courtier); + addCard(Zone.HAND, playerA, rage); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courtier); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rage, courtier); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, courtier, 1); + assertGraveyardCount(playerA, rage, 1); + assertPermanentCount(playerA, "Cursed", 1); + assertPermanentCount(playerA, "Monster", 0); + assertPowerToughness(playerA, courtier, 1 + 2, 1); + assertAbility(playerA, courtier, TrampleAbility.getInstance(), false); + assertLife(playerA, 20 + 2 + 2); + } + + @Test + public void testSeparatePlayer() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, wardens); + addCard(Zone.BATTLEFIELD, playerB, wardens); + addCard(Zone.HAND, playerA, courtier); + addCard(Zone.HAND, playerB, rage); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courtier); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, rage, courtier); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, courtier, 1); + assertGraveyardCount(playerB, rage, 1); + assertPermanentCount(playerA, "Cursed", 1); + assertPermanentCount(playerB, "Monster", 1); + assertPowerToughness(playerA, courtier, 1 + 2 + 1, 1 + 1); + assertAbility(playerA, courtier, TrampleAbility.getInstance(), true); + assertLife(playerA, 20 + 2); + assertLife(playerB, 20 + 2); + } + + @Test + public void testDoesntEnter() { + addCard(Zone.BATTLEFIELD, playerA, "Scrubland", 3 + 3); + addCard(Zone.BATTLEFIELD, playerA, wardens); + addCard(Zone.HAND, playerA, courtier); + addCard(Zone.HAND, playerA, murder); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, courtier); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, murder, courtier); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, courtier, 0); + assertPermanentCount(playerA, "Cursed", 0); + assertGraveyardCount(playerA, courtier, 1); + assertGraveyardCount(playerA, murder, 1); + assertLife(playerA, 20); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java index 6760da2a668..6435bab08da 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateTokenCopyTargetEffect.java @@ -42,6 +42,7 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { private final CardType additionalCardType; private SubType additionalSubType; private final UUID attackedPlayer; + private UUID attachedTo = null; private final boolean attacking; private boolean becomesArtifact; private ObjectColor color; @@ -134,6 +135,7 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { this.additionalCardType = effect.additionalCardType; this.additionalSubType = effect.additionalSubType; this.attackedPlayer = effect.attackedPlayer; + this.attachedTo = effect.attachedTo; this.attacking = effect.attacking; this.becomesArtifact = effect.becomesArtifact; this.color = effect.color; @@ -264,7 +266,7 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { } } - token.putOntoBattlefield(number, game, source, playerId == null ? source.getControllerId() : playerId, tapped, attacking, attackedPlayer); + token.putOntoBattlefield(number, game, source, playerId == null ? source.getControllerId() : playerId, tapped, attacking, attackedPlayer, attachedTo); for (UUID tokenId : token.getLastAddedTokenIds()) { // by cards like Doubling Season multiple tokens can be added to the battlefield Permanent tokenPermanent = game.getPermanent(tokenId); if (tokenPermanent != null) { @@ -386,6 +388,11 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect { return this; } + public CreateTokenCopyTargetEffect setAttachedTo(UUID attachedTo) { + this.attachedTo = attachedTo; + return this; + } + public void sacrificeTokensCreatedAtNextEndStep(Game game, Ability source) { this.removeTokensCreatedAtEndOf(game, source, PhaseStep.END_TURN, false); } diff --git a/Mage/src/main/java/mage/constants/RoleType.java b/Mage/src/main/java/mage/constants/RoleType.java index 702f25f3e19..a5d09bc4783 100644 --- a/Mage/src/main/java/mage/constants/RoleType.java +++ b/Mage/src/main/java/mage/constants/RoleType.java @@ -5,7 +5,6 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.permanent.token.*; -import java.util.UUID; import java.util.function.Supplier; /** @@ -39,15 +38,7 @@ public enum RoleType { public Token createToken(Permanent permanent, Game game, Ability source) { Token token = supplier.get(); - token.putOntoBattlefield(1, game, source); - for (UUID tokenId : token.getLastAddedTokenIds()) { - Permanent aura = game.getPermanent(tokenId); - if (aura == null || !aura.hasSubtype(SubType.AURA, game)) { - continue; - } - aura.getAbilities().get(0).getTargets().get(0).add(permanent.getId(), game); - aura.getAbilities().get(0).getEffects().get(0).apply(game, aura.getAbilities().get(0)); - } + token.putOntoBattlefield(1, game, source, source.getControllerId(), false, false, null, permanent.getId()); return token; } } diff --git a/Mage/src/main/java/mage/game/permanent/token/Token.java b/Mage/src/main/java/mage/game/permanent/token/Token.java index d9596762473..686f8ef4e5a 100644 --- a/Mage/src/main/java/mage/game/permanent/token/Token.java +++ b/Mage/src/main/java/mage/game/permanent/token/Token.java @@ -35,7 +35,9 @@ public interface Token extends MageObject { boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer); - boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, boolean created); + boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo); + + boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo, boolean created); void setPower(int power); diff --git a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java index 8efcc825311..ecfe8d28c4e 100644 --- a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java +++ b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java @@ -197,11 +197,16 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { @Override public boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer) { - return putOntoBattlefield(amount, game, source, controllerId, tapped, attacking, attackedPlayer, true); + return putOntoBattlefield(amount, game, source, controllerId, tapped, attacking, attackedPlayer, null); } @Override - public boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, boolean created) { + public boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo) { + return putOntoBattlefield(amount, game, source, controllerId, tapped, attacking, attackedPlayer, attachedTo, true); + } + + @Override + public boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo, boolean created) { Player controller = game.getPlayer(controllerId); if (controller == null) { return false; @@ -233,7 +238,7 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { it.remove(); } } - putOntoBattlefieldHelper(event, game, source, tapped, attacking, attackedPlayer, created); + putOntoBattlefieldHelper(event, game, source, tapped, attacking, attackedPlayer, attachedTo, created); event.getTokens() .keySet() .stream() @@ -247,7 +252,7 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { return false; } - private static void putOntoBattlefieldHelper(CreateTokenEvent event, Game game, Ability source, boolean tapped, boolean attacking, UUID attackedPlayer, boolean created) { + private static void putOntoBattlefieldHelper(CreateTokenEvent event, Game game, Ability source, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo, boolean created) { Player controller = game.getPlayer(event.getPlayerId()); if (controller == null) { return; @@ -258,6 +263,18 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { Token token = entry.getKey(); int amount = entry.getValue(); + // check if token needs to be attached to something + Permanent permanentAttachedTo; + if (attachedTo != null) { + permanentAttachedTo = game.getPermanent(attachedTo); + if (permanentAttachedTo == null || permanentAttachedTo.cantBeAttachedBy(token, source, game, true)) { + game.informPlayers(token.getName() + " will not be created as it cannot be attached to the chosen permanent"); + continue; + } + } else { + permanentAttachedTo = null; + } + // choose token's set code due source TokenInfo tokenInfo = TokenImpl.generateTokenInfo((TokenImpl) token, game, source == null ? null : source.getSourceId()); token.setExpansionSetCode(tokenInfo.getSetCode()); @@ -317,7 +334,7 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { // if token was created (not a spell copy) handle auras coming into the battlefield // code blindly copied from CopyPermanentEffect // TODO: clean this up -- half the comments make no sense in the context of creating a token - if (created && permanent.getSubtype().contains(SubType.AURA)) { + if (created && permanent.getSubtype().contains(SubType.AURA) && attachedTo == null) { Outcome auraOutcome = Outcome.BoostCreature; Target auraTarget = null; @@ -375,6 +392,15 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { } // end of aura code : just remove this line if everything works out well + if (permanentAttachedTo != null) { + if (permanent.hasSubtype(SubType.AURA, game)) { + permanent.getAbilities().get(0).getTargets().get(0).add(permanentAttachedTo.getId(), game); + permanent.getAbilities().get(0).getEffects().get(0).apply(game, permanent.getAbilities().get(0)); + } else { + permanentAttachedTo.addAttachment(permanent.getId(), source, game); + } + } + // must attack if (attacking && game.getCombat() != null && game.getActivePlayerId().equals(permanent.getControllerId())) { game.getCombat().addAttackingCreature(permanent.getId(), game, attackedPlayer); diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 86bb86f9766..f45ff9af105 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -314,7 +314,7 @@ public class Spell extends StackObjectImpl implements Card { if (isCopy()) { Token token = CopyTokenFunction.createTokenCopy(card, game, this); // The token that a resolving copy of a spell becomes isn’t said to have been “created.” (2020-09-25) - if (token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, false)) { + if (token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false)) { permId = token.getLastAddedTokenIds().stream().findFirst().orElse(null); flag = true; } else { @@ -381,7 +381,7 @@ public class Spell extends StackObjectImpl implements Card { } else if (isCopy()) { Token token = CopyTokenFunction.createTokenCopy(card, game, this); // The token that a resolving copy of a spell becomes isn’t said to have been “created.” (2020-09-25) - token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, false); + token.putOntoBattlefield(1, game, ability, getControllerId(), false, false, null, null, false); return true; } else { return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);