Refactor CreateTokenEffect to allow multiple tokens at once. (#12704)

* Refactor CreateTokenEffect to allow multiple tokens at once.

Partial solution to #10811 - Token copy effects still need to be redone so that mass token copy effects (Ocelot Pride, Mirror Match, other similar effects) can be created in a single batch.
This commit is contained in:
Grath 2024-08-25 10:34:42 -04:00 committed by GitHub
parent da48821754
commit 543f9f074e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 132 additions and 154 deletions

View file

@ -59,11 +59,7 @@ public final class AKillerAmongUs extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{G}"); super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{G}");
// When A Killer Among Us enters the battlefield, create a 1/1 white Human creature token, a 1/1 blue Merfolk creature token, and a 1/1 red Goblin creature token. Then secretly choose Human, Merfolk, or Goblin. // When A Killer Among Us enters the battlefield, create a 1/1 white Human creature token, a 1/1 blue Merfolk creature token, and a 1/1 red Goblin creature token. Then secretly choose Human, Merfolk, or Goblin.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new HumanToken())); Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new HumanToken()).withAdditionalTokens(new MerfolkToken(), new GoblinToken()));
ability.addEffect(new CreateTokenEffect(new MerfolkToken())
.setText(", a 1/1 blue Merfolk creature token"));
ability.addEffect(new CreateTokenEffect(new GoblinToken())
.setText(", and a 1/1 red Goblin creature token."));
ability.addEffect(new ChooseHumanMerfolkOrGoblinEffect()); ability.addEffect(new ChooseHumanMerfolkOrGoblinEffect());
this.addAbility(ability); this.addAbility(ability);

View file

@ -19,9 +19,7 @@ public final class BestialMenace extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}{G}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}{G}");
// Create a 1/1 green Snake creature token, a 2/2 green Wolf creature token, and a 3/3 green Elephant creature token. // Create a 1/1 green Snake creature token, a 2/2 green Wolf creature token, and a 3/3 green Elephant creature token.
this.getSpellAbility().addEffect(new CreateTokenEffect(new SnakeToken())); this.getSpellAbility().addEffect(new CreateTokenEffect(new SnakeToken()).withAdditionalTokens(new WolfToken(), new ElephantToken()));
this.getSpellAbility().addEffect(new CreateTokenEffect(new WolfToken()).setText(", a 2/2 green Wolf creature token"));
this.getSpellAbility().addEffect(new CreateTokenEffect(new ElephantToken()).setText(", and a 3/3 green Elephant creature token"));
} }
private BestialMenace(final BestialMenace card) { private BestialMenace(final BestialMenace card) {

View file

@ -64,9 +64,7 @@ public final class DevouringSugarmaw extends AdventureCard {
// Have for Dinner // Have for Dinner
// Create a 1/1 white Human creature token and a Food token. // Create a 1/1 white Human creature token and a Food token.
this.getSpellCard().getSpellAbility().addEffect(new CreateTokenEffect(new HumanToken())); this.getSpellCard().getSpellAbility().addEffect(new CreateTokenEffect(new HumanToken()).withAdditionalTokens(new FoodToken()));
this.getSpellCard().getSpellAbility().addEffect(new CreateTokenEffect(new FoodToken())
.setText("and a Food token"));
this.finalizeAdventure(); this.finalizeAdventure();
} }

View file

@ -31,16 +31,14 @@ public final class FaeOffering extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}");
// At the beginning of each end step, if you've cast both a creature spell and a noncreature spell this turn, create a Clue token, a Food token, and a Treasure token. // At the beginning of each end step, if you've cast both a creature spell and a noncreature spell this turn, create a Clue token, a Food token, and a Treasure token.
Ability ability = new ConditionalInterveningIfTriggeredAbility( this.addAbility(new ConditionalInterveningIfTriggeredAbility(
new BeginningOfEndStepTriggeredAbility( new BeginningOfEndStepTriggeredAbility(
new CreateTokenEffect(new ClueArtifactToken()), TargetController.ANY, false new CreateTokenEffect(new ClueArtifactToken()).withAdditionalTokens(new FoodToken(), new TreasureToken()),
TargetController.ANY, false
), FaeOfferingCondition.instance, "At the beginning of each end step, " + ), FaeOfferingCondition.instance, "At the beginning of each end step, " +
"if you've cast both a creature spell and a noncreature spell this turn, " + "if you've cast both a creature spell and a noncreature spell this turn, " +
"create a Clue token, a Food token, and a Treasure token." "create a Clue token, a Food token, and a Treasure token."
); ).addHint(FaeOfferingHint.instance));
ability.addEffect(new CreateTokenEffect(new FoodToken()));
ability.addEffect(new CreateTokenEffect(new TreasureToken()));
this.addAbility(ability.addHint(FaeOfferingHint.instance));
} }
private FaeOffering(final FaeOffering card) { private FaeOffering(final FaeOffering card) {

View file

@ -1,7 +1,6 @@
package mage.cards.f; package mage.cards.f;
import mage.MageInt; import mage.MageInt;
import mage.abilities.TriggeredAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
@ -31,15 +30,8 @@ public final class FarmerCotton extends CardImpl {
this.toughness = new MageInt(1); this.toughness = new MageInt(1);
// When Farmer Cotton enters the battlefield, create X 1/1 white Halfling creature tokens and X Food tokens. // When Farmer Cotton enters the battlefield, create X 1/1 white Halfling creature tokens and X Food tokens.
TriggeredAbility trigger = new EntersBattlefieldTriggeredAbility( this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new HalflingToken(),
new CreateTokenEffect(new HalflingToken(), GetXValue.instance) GetXValue.instance).withAdditionalTokens(new FoodToken())));
);
trigger.addEffect(
new CreateTokenEffect(new FoodToken(), GetXValue.instance)
.setText("and X Food tokens")
);
this.addAbility(trigger);
} }
private FarmerCotton(final FarmerCotton card) { private FarmerCotton(final FarmerCotton card) {

View file

@ -18,9 +18,7 @@ public final class ForbiddenFriendship extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{R}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{R}");
// Create a 1/1 red Dinosaur creature token with haste and a 1/1 white Human Soldier creature token. // Create a 1/1 red Dinosaur creature token with haste and a 1/1 white Human Soldier creature token.
this.getSpellAbility().addEffect(new CreateTokenEffect(new DinosaurHasteToken())); this.getSpellAbility().addEffect(new CreateTokenEffect(new DinosaurHasteToken()).withAdditionalTokens(new HumanSoldierToken()));
this.getSpellAbility().addEffect(new CreateTokenEffect(new HumanSoldierToken())
.setText("and a 1/1 white Human Soldier creature token"));
} }
private ForbiddenFriendship(final ForbiddenFriendship card) { private ForbiddenFriendship(final ForbiddenFriendship card) {

View file

@ -42,7 +42,7 @@ public final class FrostFairLureFish extends CardImpl {
// When Frost Fair Lure Fish enters the battlefield, create two 1/1 blue Fish creature tokens and two tapped Treasure tokens. // When Frost Fair Lure Fish enters the battlefield, create two 1/1 blue Fish creature tokens and two tapped Treasure tokens.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new FishNoAbilityToken(), 2)); Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new FishNoAbilityToken(), 2));
ability.addEffect(new CreateTokenEffect(new TreasureToken(), 2, true) ability.addEffect(new CreateTokenEffect(new TreasureToken(), 2, true)
.setText("and two tapped Treasure tokens")); .setText("and create two tapped Treasure tokens"));
this.addAbility(ability); this.addAbility(ability);
// Fish you control have haste and can't be blocked by Humans. // Fish you control have haste and can't be blocked by Humans.

View file

@ -1,7 +1,6 @@
package mage.cards.l; package mage.cards.l;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -64,34 +63,34 @@ class LiberatedLivestockEffect extends OneShotEffect {
if (controller == null) { if (controller == null) {
return false; return false;
} }
List<Token> tokens = Arrays.asList(new CatToken2(), new BirdToken(), new OxToken()); Token firstToken = new CatToken2();
tokens.forEach(token -> token.putOntoBattlefield(1, game, source, source.getControllerId())); firstToken.putOntoBattlefield(1, game, source, source.getControllerId(),
false, false, null, null, true,
Arrays.asList(firstToken, new BirdToken(), new OxToken()));
game.processAction(); game.processAction();
for (Token token : tokens) { for (UUID tokenId : firstToken.getLastAddedTokenIds()) {
for (UUID tokenId : token.getLastAddedTokenIds()) { Permanent tokenPermanent = game.getPermanent(tokenId);
Permanent tokenPermanent = game.getPermanent(tokenId); if (tokenPermanent == null) {
if (tokenPermanent == null) { continue;
continue; }
} FilterCard filter = new FilterCard("Aura from your hand or graveyard that can attach to " + tokenPermanent.getName());
FilterCard filter = new FilterCard("Aura from your hand or graveyard that can attach to " + tokenPermanent.getName()); filter.add(SubType.AURA.getPredicate());
filter.add(SubType.AURA.getPredicate()); filter.add(new AuraCardCanAttachToPermanentId(tokenPermanent.getId()));
filter.add(new AuraCardCanAttachToPermanentId(tokenPermanent.getId())); Cards auraCards = new CardsImpl();
Cards auraCards = new CardsImpl(); auraCards.addAllCards(controller.getHand().getCards(filter, source.getControllerId(), source, game));
auraCards.addAllCards(controller.getHand().getCards(filter, source.getControllerId(), source, game)); auraCards.addAllCards(controller.getGraveyard().getCards(filter, source.getControllerId(), source, game));
auraCards.addAllCards(controller.getGraveyard().getCards(filter, source.getControllerId(), source, game)); if (auraCards.isEmpty()) {
if (auraCards.isEmpty()) { continue;
continue; }
} TargetCard target = new TargetCard(0, 1, Zone.ALL, filter);
TargetCard target = new TargetCard(0, 1, Zone.ALL, filter); target.withNotTarget(true);
target.withNotTarget(true); controller.chooseTarget(outcome, auraCards, target, source, game);
controller.chooseTarget(outcome, auraCards, target, source, game); Card auraCard = game.getCard(target.getFirstTarget());
Card auraCard = game.getCard(target.getFirstTarget()); if (auraCard != null && !tokenPermanent.cantBeAttachedBy(auraCard, source, game, true)) {
if (auraCard != null && !tokenPermanent.cantBeAttachedBy(auraCard, source, game, true)) { game.getState().setValue("attachTo:" + auraCard.getId(), tokenPermanent);
game.getState().setValue("attachTo:" + auraCard.getId(), tokenPermanent); controller.moveCards(auraCard, Zone.BATTLEFIELD, source, game);
controller.moveCards(auraCard, Zone.BATTLEFIELD, source, game); tokenPermanent.addAttachment(auraCard.getId(), source, game);
tokenPermanent.addAttachment(auraCard.getId(), source, game);
}
} }
} }
return true; return true;

View file

@ -1,7 +1,6 @@
package mage.cards.m; package mage.cards.m;
import mage.MageInt; import mage.MageInt;
import mage.abilities.TriggeredAbility;
import mage.abilities.common.DealtDamageAndDiedTriggeredAbility; import mage.abilities.common.DealtDamageAndDiedTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
@ -36,9 +35,7 @@ public final class MadameVastra extends CardImpl {
this.addAbility(new SimpleStaticAbility(new MustBeBlockedByAtLeastOneSourceEffect(Duration.WhileOnBattlefield))); this.addAbility(new SimpleStaticAbility(new MustBeBlockedByAtLeastOneSourceEffect(Duration.WhileOnBattlefield)));
// Whenever a creature dealt damage by Madame Vastra this turn dies, create a Clue token and a Food token. // Whenever a creature dealt damage by Madame Vastra this turn dies, create a Clue token and a Food token.
TriggeredAbility trigger = new DealtDamageAndDiedTriggeredAbility(new CreateTokenEffect(new ClueArtifactToken())); this.addAbility(new DealtDamageAndDiedTriggeredAbility(new CreateTokenEffect(new ClueArtifactToken()).withAdditionalTokens(new FoodToken())));
trigger.addEffect(new CreateTokenEffect(new FoodToken()).setText("and a Food token"));
this.addAbility(trigger);
} }
private MadameVastra(final MadameVastra card) { private MadameVastra(final MadameVastra card) {

View file

@ -22,11 +22,7 @@ public final class MascotExhibition extends CardImpl {
this.subtype.add(SubType.LESSON); this.subtype.add(SubType.LESSON);
// Create a 2/1 white and black Inkling creature token with flying, a 3/2 red and white Spirit creature token, and a 4/4 blue and red Elemental creature token. // Create a 2/1 white and black Inkling creature token with flying, a 3/2 red and white Spirit creature token, and a 4/4 blue and red Elemental creature token.
this.getSpellAbility().addEffect(new CreateTokenEffect(new InklingToken())); this.getSpellAbility().addEffect(new CreateTokenEffect(new InklingToken()).withAdditionalTokens(new Spirit32Token(), new Elemental44Token()));
this.getSpellAbility().addEffect(new CreateTokenEffect(new Spirit32Token())
.setText(", a 3/2 red and white Spirit creature token"));
this.getSpellAbility().addEffect(new CreateTokenEffect(new Elemental44Token())
.setText(", and a 4/4 blue and red Elemental creature token"));
} }
private MascotExhibition(final MascotExhibition card) { private MascotExhibition(final MascotExhibition card) {

View file

@ -1,7 +1,6 @@
package mage.cards.p; package mage.cards.p;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.DiesThisOrAnotherTriggeredAbility; import mage.abilities.common.DiesThisOrAnotherTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.keyword.LifelinkAbility; import mage.abilities.keyword.LifelinkAbility;
@ -50,12 +49,9 @@ public final class PolukranosEngineOfRuin extends CardImpl {
this.addAbility(LifelinkAbility.getInstance()); this.addAbility(LifelinkAbility.getInstance());
// Whenever Polukranos, Engine of Ruin or another nontoken Hydra you control dies, create a 3/3 green and white Phyrexian Hydra creature token with reach and a 3/3 green and white Phyrexian Hydra creature token with lifelink. // Whenever Polukranos, Engine of Ruin or another nontoken Hydra you control dies, create a 3/3 green and white Phyrexian Hydra creature token with reach and a 3/3 green and white Phyrexian Hydra creature token with lifelink.
Ability ability = new DiesThisOrAnotherTriggeredAbility( this.addAbility(new DiesThisOrAnotherTriggeredAbility(
new CreateTokenEffect(new PhyrexianHydraWithReachToken()), false, filter new CreateTokenEffect(new PhyrexianHydraWithReachToken()).withAdditionalTokens(new PhyrexianHydraWithLifelinkToken()), false, filter
); ));
ability.addEffect(new CreateTokenEffect(new PhyrexianHydraWithLifelinkToken())
.setText("and a 3/3 green and white Phyrexian Hydra creature token with lifelink"));
this.addAbility(ability);
} }
private PolukranosEngineOfRuin(final PolukranosEngineOfRuin card) { private PolukranosEngineOfRuin(final PolukranosEngineOfRuin card) {

View file

@ -48,12 +48,9 @@ public final class ReckonerBankbuster extends CardImpl {
new DrawCardSourceControllerEffect(1), new GenericManaCost(2) new DrawCardSourceControllerEffect(1), new GenericManaCost(2)
); );
ability.addEffect(new ConditionalOneShotEffect( ability.addEffect(new ConditionalOneShotEffect(
new CreateTokenEffect(new TreasureToken()), condition, new CreateTokenEffect(new TreasureToken()).withAdditionalTokens(new PilotToken()), condition,
"Then if there are no charge counters on {this}, create a Treasure token" "Then if there are no charge counters on {this}, create a Treasure token and a 1/1 colorless " +
)); "Pilot creature token with \"This creature crews Vehicles as though its power were 2 greater.\""
ability.addEffect(new ConditionalOneShotEffect(
new CreateTokenEffect(new PilotToken()), condition, "and a 1/1 colorless Pilot creature token " +
"with \"This creature crews Vehicles as though its power were 2 greater.\""
)); ));
ability.addCost(new TapSourceCost()); ability.addCost(new TapSourceCost());
ability.addCost(new RemoveCountersSourceCost(CounterType.CHARGE.createInstance())); ability.addCost(new RemoveCountersSourceCost(CounterType.CHARGE.createInstance()));

View file

@ -1,7 +1,6 @@
package mage.cards.s; package mage.cards.s;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
@ -33,10 +32,7 @@ public final class SomberwaldBeastmaster extends CardImpl {
this.toughness = new MageInt(1); this.toughness = new MageInt(1);
// When Somberwald Beastmaster enters the battlefield, create a 2/2 green Wolf creature token, a 3/3 green Beast creature token, and a 4/4 green Beast creature token. // When Somberwald Beastmaster enters the battlefield, create a 2/2 green Wolf creature token, a 3/3 green Beast creature token, and a 4/4 green Beast creature token.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new WolfToken())); this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new WolfToken()).withAdditionalTokens(new BeastToken(), new BeastToken2())));
ability.addEffect(new CreateTokenEffect(new BeastToken()).setText(", a 3/3 green Beast creature token"));
ability.addEffect(new CreateTokenEffect(new BeastToken2()).setText(", and a 4/4 green Beast creature token"));
this.addAbility(ability);
// Creature tokens you control have deathtouch. // Creature tokens you control have deathtouch.
this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect(

View file

@ -50,9 +50,7 @@ public final class SongOfEarendil extends CardImpl {
// II-- Create a Treasure token and a 2/2 blue Bird creature token with flying. // II-- Create a Treasure token and a 2/2 blue Bird creature token with flying.
sagaAbility.addChapterEffect( sagaAbility.addChapterEffect(
this, SagaChapter.CHAPTER_II, this, SagaChapter.CHAPTER_II,
new CreateTokenEffect(new TreasureToken()), new CreateTokenEffect(new TreasureToken()).withAdditionalTokens(new SwanSongBirdToken())
new CreateTokenEffect(new SwanSongBirdToken())
.setText("and a 2/2 blue Bird creature token with flying")
); );
// III-- Put a flying counter on each creature you control without flying. // III-- Put a flying counter on each creature you control without flying.

View file

@ -39,12 +39,10 @@ public final class SpecimenCollector extends CardImpl {
this.toughness = new MageInt(1); this.toughness = new MageInt(1);
// When Specimen Collector enters the battlefield, create a 1/1 green Squirrel creature token and a 0/3 blue Crab creature token. // When Specimen Collector enters the battlefield, create a 1/1 green Squirrel creature token and a 0/3 blue Crab creature token.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new SquirrelToken())); this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new SquirrelToken()).withAdditionalTokens(new CrabToken())));
ability.addEffect(new CreateTokenEffect(new CrabToken()).setText("and a 0/3 blue Crab creature token"));
this.addAbility(ability);
// When Specimen Collector dies, create a token that's a copy of target token you control. // When Specimen Collector dies, create a token that's a copy of target token you control.
ability = new DiesSourceTriggeredAbility(new CreateTokenCopyTargetEffect()); Ability ability = new DiesSourceTriggeredAbility(new CreateTokenCopyTargetEffect());
ability.addTarget(new TargetPermanent(filter)); ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -46,10 +46,7 @@ public final class TheEleventhHour extends CardImpl {
// II -- Create a Food token and a 1/1 white Human creature token with "Doctor spells you cast cost 1 less to cast." // II -- Create a Food token and a 1/1 white Human creature token with "Doctor spells you cast cost 1 less to cast."
sagaAbility.addChapterEffect( sagaAbility.addChapterEffect(
this, SagaChapter.CHAPTER_II, this, SagaChapter.CHAPTER_II,
new CreateTokenEffect(new FoodToken()), new CreateTokenEffect(new FoodToken()).withAdditionalTokens(new TheEleventhHourToken())
new CreateTokenEffect(new TheEleventhHourToken())
.setText("and a 1/1 white Human creature token with " +
"\"Doctor spells you cast cost {1} less to cast.\"")
); );
// III -- Create a token that's a copy of target creature, except it's a legendary Alien named Prisoner Zero. // III -- Create a token that's a copy of target creature, except it's a legendary Alien named Prisoner Zero.

View file

@ -1,7 +1,6 @@
package mage.cards.t; package mage.cards.t;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.common.DiesSourceTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
@ -39,12 +38,7 @@ public final class TriplicateTitan extends CardImpl {
this.addAbility(TrampleAbility.getInstance()); this.addAbility(TrampleAbility.getInstance());
// When Triplicate Titan dies, create a 3/3 colorless Golem artifact creature token with flying, a 3/3 colorless Golem artifact creature token with vigilance, and a 3/3 colorless Golem artifact creature token with trample. // When Triplicate Titan dies, create a 3/3 colorless Golem artifact creature token with flying, a 3/3 colorless Golem artifact creature token with vigilance, and a 3/3 colorless Golem artifact creature token with trample.
Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new GolemFlyingToken())); this.addAbility(new DiesSourceTriggeredAbility(new CreateTokenEffect(new GolemFlyingToken()).withAdditionalTokens(new GolemVigilanceToken(), new GolemTrampleToken())));
ability.addEffect(new CreateTokenEffect(new GolemVigilanceToken())
.setText(", a 3/3 colorless Golem artifact creature token with vigilance"));
ability.addEffect(new CreateTokenEffect(new GolemTrampleToken())
.setText(", and a 3/3 colorless Golem artifact creature token with trample"));
this.addAbility(ability);
} }
private TriplicateTitan(final TriplicateTitan card) { private TriplicateTitan(final TriplicateTitan card) {

View file

@ -3,7 +3,6 @@ package mage.cards.t;
import java.util.UUID; import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
@ -29,10 +28,7 @@ public final class TrostanisSummoner extends CardImpl {
this.toughness = new MageInt(1); this.toughness = new MageInt(1);
// When Trostani's Summoner enters the battlefield, create a 2/2 white Knight creature token with vigilance, a 3/3 green Centaur creature token, and a 4/4 green Rhino creature token with trample. // When Trostani's Summoner enters the battlefield, create a 2/2 white Knight creature token with vigilance, a 3/3 green Centaur creature token, and a 4/4 green Rhino creature token with trample.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new KnightToken())); this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new KnightToken()).withAdditionalTokens(new CentaurToken(), new RhinoToken())));
ability.addEffect(new CreateTokenEffect(new CentaurToken()).setText(", a 3/3 green Centaur creature token"));
ability.addEffect(new CreateTokenEffect(new RhinoToken()).setText(", and a 4/4 green Rhino creature token with trample"));
this.addAbility(ability);
} }

View file

@ -35,8 +35,7 @@ public final class Vault101BirthdayParty extends CardImpl {
// I -- Create a 1/1 white Human Soldier creature token and a Food token. // I -- Create a 1/1 white Human Soldier creature token and a Food token.
sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I, sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I,
new CreateTokenEffect(new HumanSoldierToken()), new CreateTokenEffect(new HumanSoldierToken()).withAdditionalTokens(new FoodToken()));
new CreateTokenEffect(new FoodToken()).setText("and a Food token"));
// II, III -- You may put an Aura or Equipment card from your hand or graveyard onto the battlefield. // II, III -- You may put an Aura or Equipment card from your hand or graveyard onto the battlefield.
// If an Equipment is put onto the battlefield this way, you may attach it to a creature you control. // If an Equipment is put onto the battlefield this way, you may attach it to a creature you control.

View file

@ -32,8 +32,7 @@ public final class WurmcoilEngine extends CardImpl {
this.addAbility(LifelinkAbility.getInstance()); this.addAbility(LifelinkAbility.getInstance());
// When Wurmcoil Engine dies, create a 3/3 colorless Wurm artifact creature token with deathtouch and a 3/3 colorless Wurm artifact creature token with lifelink. // When Wurmcoil Engine dies, create a 3/3 colorless Wurm artifact creature token with deathtouch and a 3/3 colorless Wurm artifact creature token with lifelink.
Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new WurmWithDeathtouchToken()), false); Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new WurmWithDeathtouchToken()).withAdditionalTokens(new WurmWithLifelinkToken()), false);
ability.addEffect(new CreateTokenEffect(new WurmWithLifelinkToken()).setText("and a 3/3 colorless Phyrexian Wurm artifact creature token with lifelink"));
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -35,9 +35,7 @@ public final class WurmcoilLarva extends CardImpl {
this.addAbility(LifelinkAbility.getInstance()); this.addAbility(LifelinkAbility.getInstance());
// When Wurmcoil Larva dies, create a 1/2 black Phyrexian Wurm artifact creature token with deathtouch and a 2/1 black Phyrexian Wurm artifact creature token with lifelink. // When Wurmcoil Larva dies, create a 1/2 black Phyrexian Wurm artifact creature token with deathtouch and a 2/1 black Phyrexian Wurm artifact creature token with lifelink.
Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new PhyrexianWurm12DeathtouchToken()), false); Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new PhyrexianWurm12DeathtouchToken()).withAdditionalTokens(new PhyrexianWurm21LifelinkToken()), false);
ability.addEffect(new CreateTokenEffect(new PhyrexianWurm21LifelinkToken())
.setText("and a 2/1 black Phyrexian Wurm artifact creature token with lifelink"));
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -59,8 +59,8 @@ public class KambalProfiteeringMayorTest extends CardTestPlayerBase {
assertPermanentCount(playerA, "Wolf Token", 1); assertPermanentCount(playerA, "Wolf Token", 1);
assertPermanentCount(playerA, "Elephant Token", 1); assertPermanentCount(playerA, "Elephant Token", 1);
assertPermanentCount(playerB, "Snake Token", 1); assertPermanentCount(playerB, "Snake Token", 1);
assertPermanentCount(playerB, "Wolf Token", 0); // TODO: this is a bug, should be 1, see #10811 assertPermanentCount(playerB, "Wolf Token", 1);
assertPermanentCount(playerB, "Elephant Token", 0); // TODO: this is a bug, should be 1, see #10811 assertPermanentCount(playerB, "Elephant Token", 1);
} }
@Test @Test

View file

@ -16,6 +16,7 @@ import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil; import mage.util.CardUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -24,7 +25,7 @@ import java.util.UUID;
*/ */
public class CreateTokenEffect extends OneShotEffect { public class CreateTokenEffect extends OneShotEffect {
private final Token token; private final List<Token> tokens = new ArrayList<>();
private final DynamicValue amount; private final DynamicValue amount;
private final boolean tapped; private final boolean tapped;
private final boolean attacking; private final boolean attacking;
@ -56,7 +57,10 @@ public class CreateTokenEffect extends OneShotEffect {
public CreateTokenEffect(Token token, DynamicValue amount, boolean tapped, boolean attacking) { public CreateTokenEffect(Token token, DynamicValue amount, boolean tapped, boolean attacking) {
super(Outcome.PutCreatureInPlay); super(Outcome.PutCreatureInPlay);
this.token = token; if (token == null) {
throw new IllegalArgumentException("Wrong code usage. Token provided to CreateTokenEffect must not be null.");
}
this.tokens.add(token);
this.amount = amount.copy(); this.amount = amount.copy();
this.tapped = tapped; this.tapped = tapped;
this.attacking = attacking; this.attacking = attacking;
@ -66,7 +70,9 @@ public class CreateTokenEffect extends OneShotEffect {
protected CreateTokenEffect(final CreateTokenEffect effect) { protected CreateTokenEffect(final CreateTokenEffect effect) {
super(effect); super(effect);
this.amount = effect.amount.copy(); this.amount = effect.amount.copy();
this.token = effect.token.copy(); for (Token token : effect.tokens) {
this.tokens.add(token.copy());
}
this.tapped = effect.tapped; this.tapped = effect.tapped;
this.attacking = effect.attacking; this.attacking = effect.attacking;
this.lastAddedTokenIds.addAll(effect.lastAddedTokenIds); this.lastAddedTokenIds.addAll(effect.lastAddedTokenIds);
@ -82,6 +88,11 @@ public class CreateTokenEffect extends OneShotEffect {
return this; return this;
} }
public CreateTokenEffect withAdditionalTokens(Token... tokens) {
this.tokens.addAll(Arrays.asList(tokens));
return this;
}
@Override @Override
public CreateTokenEffect copy() { public CreateTokenEffect copy() {
return new CreateTokenEffect(this); return new CreateTokenEffect(this);
@ -90,8 +101,8 @@ public class CreateTokenEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
int value = amount.calculate(game, source, this); int value = amount.calculate(game, source, this);
token.putOntoBattlefield(value, game, source, source.getControllerId(), tapped, attacking); tokens.get(0).putOntoBattlefield(value, game, source, source.getControllerId(), tapped, attacking, null, null, true, tokens);
this.lastAddedTokenIds = token.getLastAddedTokenIds(); this.lastAddedTokenIds = tokens.get(0).getLastAddedTokenIds();
// TODO: Workaround to add counters to all created tokens, necessary for correct interactions with cards like Chatterfang, Squirrel General and Ochre Jelly / Printlifter Ooze. See #10786 // TODO: Workaround to add counters to all created tokens, necessary for correct interactions with cards like Chatterfang, Squirrel General and Ochre Jelly / Printlifter Ooze. See #10786
if (counterType != null) { if (counterType != null) {
for (UUID tokenId : lastAddedTokenIds) { for (UUID tokenId : lastAddedTokenIds) {
@ -150,41 +161,53 @@ public class CreateTokenEffect extends OneShotEffect {
} }
private void setText() { private void setText() {
String tokenDescription = token.getDescription();
boolean singular = amount.toString().equals("1"); boolean singular = amount.toString().equals("1");
if (tokenDescription.contains(", a legendary")) {
staticText = "create " + tokenDescription;
return;
}
if (oldPhrasing) {
tokenDescription = tokenDescription.replace("token with \"",
singular ? "token. It has \"" : "tokens. They have \"");
}
StringBuilder sb = new StringBuilder("create "); StringBuilder sb = new StringBuilder("create ");
if (singular) { for (int i = 0; i < tokens.size(); i++) {
if (tapped && !attacking) { if (i > 0) {
sb.append("a tapped "); if (tokens.size() > 2) {
sb.append(", ");
} else {
sb.append(" ");
}
if (i+1 == tokens.size()) {
sb.append("and ");
}
}
String tokenDescription = tokens.get(i).getDescription();
if (tokenDescription.contains(", a legendary")) {
sb.append(tokenDescription); sb.append(tokenDescription);
continue;
}
if (oldPhrasing) {
tokenDescription = tokenDescription.replace("token with \"",
singular ? "token. It has \"" : "tokens. They have \"");
}
if (singular) {
if (tapped && !attacking) {
sb.append("a tapped ");
sb.append(tokenDescription);
} else {
sb.append(CardUtil.addArticle(tokenDescription));
}
} else { } else {
sb.append(CardUtil.addArticle(tokenDescription)); sb.append(CardUtil.numberToText(amount.toString())).append(' ');
} if (tapped && !attacking) {
} else { sb.append("tapped ");
sb.append(CardUtil.numberToText(amount.toString())).append(' '); }
if (tapped && !attacking) { sb.append(tokenDescription);
sb.append("tapped "); if (tokenDescription.endsWith("token")) {
} sb.append("s");
sb.append(tokenDescription); }
if (tokenDescription.endsWith("token")) { int tokenLocation = sb.indexOf("token ");
sb.append("s"); if (tokenLocation != -1) {
} sb.replace(tokenLocation, tokenLocation + 6, "tokens ");
int tokenLocation = sb.indexOf("token "); }
if (tokenLocation != -1) {
sb.replace(tokenLocation, tokenLocation + 6, "tokens ");
} }
} }
if (attacking) { if (attacking) {
if (singular) { if (singular && tokens.size() == 1) {
sb.append(" that's"); sb.append(" that's");
} else { } else {
sb.append(" that are"); sb.append(" that are");

View file

@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.game.permanent.token.Token; import mage.game.permanent.token.Token;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -17,11 +18,15 @@ public class CreateTokenEvent extends GameEvent {
* @param source * @param source
* @param controllerId * @param controllerId
* @param amount * @param amount
* @param token * @param tokensList
*/ */
public CreateTokenEvent(Ability source, UUID controllerId, int amount, Token token) { public CreateTokenEvent(Ability source, UUID controllerId, int amount, List<Token> tokensList) {
super(GameEvent.EventType.CREATE_TOKEN, null, source, controllerId, amount, false); super(GameEvent.EventType.CREATE_TOKEN, null, source, controllerId, amount, false);
tokens.put(token, amount); if (tokensList != null) {
for (Token token : tokensList) {
tokens.put(token, amount);
}
}
} }
public Map<Token, Integer> getTokens() { public Map<Token, Integer> getTokens() {

View file

@ -40,6 +40,8 @@ public interface Token extends MageObject {
boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo, boolean created); boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo, boolean created);
boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo, boolean created, List<Token> additionalTokens);
void setPower(int power); void setPower(int power);
void setToughness(int toughness); void setToughness(int toughness);

View file

@ -218,8 +218,12 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
return putOntoBattlefield(amount, game, source, controllerId, tapped, attacking, attackedPlayer, attachedTo, true); 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) { public boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo, boolean created) {
return putOntoBattlefield(amount, game, source, controllerId, tapped, attacking, attackedPlayer, attachedTo, created, Collections.singletonList(this));
}
@Override
public boolean putOntoBattlefield(int amount, Game game, Ability source, UUID controllerId, boolean tapped, boolean attacking, UUID attackedPlayer, UUID attachedTo, boolean created, List<Token> tokens) {
Player controller = game.getPlayer(controllerId); Player controller = game.getPlayer(controllerId);
if (controller == null) { if (controller == null) {
return false; return false;
@ -229,7 +233,11 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
} }
lastAddedTokenIds.clear(); lastAddedTokenIds.clear();
CreateTokenEvent event = new CreateTokenEvent(source, controllerId, amount, this); if (tokens == null || tokens.get(0) != this) {
throw new IllegalArgumentException("Wrong code usage. token.putOntoBattlefield parameter tokens must be initialized to a list of all tokens to be made, with the first element being the token you are calling putOntoBattlefield() on.");
}
CreateTokenEvent event = new CreateTokenEvent(source, controllerId, amount, tokens);
if (!created || !game.replaceEvent(event)) { if (!created || !game.replaceEvent(event)) {
int currentTokens = game.getBattlefield().countTokens(event.getPlayerId()); int currentTokens = game.getBattlefield().countTokens(event.getPlayerId());
int tokenSlots = Math.max(MAX_TOKENS_PER_GAME - currentTokens, 0); int tokenSlots = Math.max(MAX_TOKENS_PER_GAME - currentTokens, 0);