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}");
// 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.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 ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new HumanToken()).withAdditionalTokens(new MerfolkToken(), new GoblinToken()));
ability.addEffect(new ChooseHumanMerfolkOrGoblinEffect());
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}");
// 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 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"));
this.getSpellAbility().addEffect(new CreateTokenEffect(new SnakeToken()).withAdditionalTokens(new WolfToken(), new ElephantToken()));
}
private BestialMenace(final BestialMenace card) {

View file

@ -64,9 +64,7 @@ public final class DevouringSugarmaw extends AdventureCard {
// Have for Dinner
// 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 FoodToken())
.setText("and a Food token"));
this.getSpellCard().getSpellAbility().addEffect(new CreateTokenEffect(new HumanToken()).withAdditionalTokens(new FoodToken()));
this.finalizeAdventure();
}

View file

@ -31,16 +31,14 @@ public final class FaeOffering extends CardImpl {
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.
Ability ability = new ConditionalInterveningIfTriggeredAbility(
this.addAbility(new ConditionalInterveningIfTriggeredAbility(
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, " +
"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.addEffect(new CreateTokenEffect(new FoodToken()));
ability.addEffect(new CreateTokenEffect(new TreasureToken()));
this.addAbility(ability.addHint(FaeOfferingHint.instance));
).addHint(FaeOfferingHint.instance));
}
private FaeOffering(final FaeOffering card) {

View file

@ -1,7 +1,6 @@
package mage.cards.f;
import mage.MageInt;
import mage.abilities.TriggeredAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.CreateTokenEffect;
@ -31,15 +30,8 @@ public final class FarmerCotton extends CardImpl {
this.toughness = new MageInt(1);
// When Farmer Cotton enters the battlefield, create X 1/1 white Halfling creature tokens and X Food tokens.
TriggeredAbility trigger = new EntersBattlefieldTriggeredAbility(
new CreateTokenEffect(new HalflingToken(), GetXValue.instance)
);
trigger.addEffect(
new CreateTokenEffect(new FoodToken(), GetXValue.instance)
.setText("and X Food tokens")
);
this.addAbility(trigger);
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new HalflingToken(),
GetXValue.instance).withAdditionalTokens(new FoodToken())));
}
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}");
// 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 HumanSoldierToken())
.setText("and a 1/1 white Human Soldier creature token"));
this.getSpellAbility().addEffect(new CreateTokenEffect(new DinosaurHasteToken()).withAdditionalTokens(new HumanSoldierToken()));
}
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.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new FishNoAbilityToken(), 2));
ability.addEffect(new CreateTokenEffect(new TreasureToken(), 2, true)
.setText("and two tapped Treasure tokens"));
.setText("and create two tapped Treasure tokens"));
this.addAbility(ability);
// Fish you control have haste and can't be blocked by Humans.

View file

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

View file

@ -1,7 +1,6 @@
package mage.cards.m;
import mage.MageInt;
import mage.abilities.TriggeredAbility;
import mage.abilities.common.DealtDamageAndDiedTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.CreateTokenEffect;
@ -36,9 +35,7 @@ public final class MadameVastra extends CardImpl {
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.
TriggeredAbility trigger = new DealtDamageAndDiedTriggeredAbility(new CreateTokenEffect(new ClueArtifactToken()));
trigger.addEffect(new CreateTokenEffect(new FoodToken()).setText("and a Food token"));
this.addAbility(trigger);
this.addAbility(new DealtDamageAndDiedTriggeredAbility(new CreateTokenEffect(new ClueArtifactToken()).withAdditionalTokens(new FoodToken())));
}
private MadameVastra(final MadameVastra card) {

View file

@ -22,11 +22,7 @@ public final class MascotExhibition extends CardImpl {
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.
this.getSpellAbility().addEffect(new CreateTokenEffect(new InklingToken()));
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"));
this.getSpellAbility().addEffect(new CreateTokenEffect(new InklingToken()).withAdditionalTokens(new Spirit32Token(), new Elemental44Token()));
}
private MascotExhibition(final MascotExhibition card) {

View file

@ -1,7 +1,6 @@
package mage.cards.p;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.DiesThisOrAnotherTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.keyword.LifelinkAbility;
@ -50,12 +49,9 @@ public final class PolukranosEngineOfRuin extends CardImpl {
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.
Ability ability = new DiesThisOrAnotherTriggeredAbility(
new CreateTokenEffect(new PhyrexianHydraWithReachToken()), 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);
this.addAbility(new DiesThisOrAnotherTriggeredAbility(
new CreateTokenEffect(new PhyrexianHydraWithReachToken()).withAdditionalTokens(new PhyrexianHydraWithLifelinkToken()), false, filter
));
}
private PolukranosEngineOfRuin(final PolukranosEngineOfRuin card) {

View file

@ -48,12 +48,9 @@ public final class ReckonerBankbuster extends CardImpl {
new DrawCardSourceControllerEffect(1), new GenericManaCost(2)
);
ability.addEffect(new ConditionalOneShotEffect(
new CreateTokenEffect(new TreasureToken()), condition,
"Then if there are no charge counters on {this}, create a Treasure token"
));
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.\""
new CreateTokenEffect(new TreasureToken()).withAdditionalTokens(new PilotToken()), condition,
"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.addCost(new TapSourceCost());
ability.addCost(new RemoveCountersSourceCost(CounterType.CHARGE.createInstance()));

View file

@ -1,7 +1,6 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.CreateTokenEffect;
@ -33,10 +32,7 @@ public final class SomberwaldBeastmaster extends CardImpl {
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.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new WolfToken()));
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);
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new WolfToken()).withAdditionalTokens(new BeastToken(), new BeastToken2())));
// Creature tokens you control have deathtouch.
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.
sagaAbility.addChapterEffect(
this, SagaChapter.CHAPTER_II,
new CreateTokenEffect(new TreasureToken()),
new CreateTokenEffect(new SwanSongBirdToken())
.setText("and a 2/2 blue Bird creature token with flying")
new CreateTokenEffect(new TreasureToken()).withAdditionalTokens(new SwanSongBirdToken())
);
// 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);
// 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()));
ability.addEffect(new CreateTokenEffect(new CrabToken()).setText("and a 0/3 blue Crab creature token"));
this.addAbility(ability);
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new SquirrelToken()).withAdditionalTokens(new CrabToken())));
// 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));
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."
sagaAbility.addChapterEffect(
this, SagaChapter.CHAPTER_II,
new CreateTokenEffect(new FoodToken()),
new CreateTokenEffect(new TheEleventhHourToken())
.setText("and a 1/1 white Human creature token with " +
"\"Doctor spells you cast cost {1} less to cast.\"")
new CreateTokenEffect(new FoodToken()).withAdditionalTokens(new TheEleventhHourToken())
);
// 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;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.DiesSourceTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.keyword.FlyingAbility;
@ -39,12 +38,7 @@ public final class TriplicateTitan extends CardImpl {
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.
Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new GolemFlyingToken()));
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);
this.addAbility(new DiesSourceTriggeredAbility(new CreateTokenEffect(new GolemFlyingToken()).withAdditionalTokens(new GolemVigilanceToken(), new GolemTrampleToken())));
}
private TriplicateTitan(final TriplicateTitan card) {

View file

@ -3,7 +3,6 @@ package mage.cards.t;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.cards.CardImpl;
@ -29,10 +28,7 @@ public final class TrostanisSummoner extends CardImpl {
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.
Ability ability = new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new KnightToken()));
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);
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new KnightToken()).withAdditionalTokens(new CentaurToken(), new RhinoToken())));
}

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.
sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I,
new CreateTokenEffect(new HumanSoldierToken()),
new CreateTokenEffect(new FoodToken()).setText("and a Food token"));
new CreateTokenEffect(new HumanSoldierToken()).withAdditionalTokens(new FoodToken()));
// 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.

View file

@ -32,8 +32,7 @@ public final class WurmcoilEngine extends CardImpl {
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.
Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new WurmWithDeathtouchToken()), false);
ability.addEffect(new CreateTokenEffect(new WurmWithLifelinkToken()).setText("and a 3/3 colorless Phyrexian Wurm artifact creature token with lifelink"));
Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new WurmWithDeathtouchToken()).withAdditionalTokens(new WurmWithLifelinkToken()), false);
this.addAbility(ability);
}

View file

@ -35,9 +35,7 @@ public final class WurmcoilLarva extends CardImpl {
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.
Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new PhyrexianWurm12DeathtouchToken()), false);
ability.addEffect(new CreateTokenEffect(new PhyrexianWurm21LifelinkToken())
.setText("and a 2/1 black Phyrexian Wurm artifact creature token with lifelink"));
Ability ability = new DiesSourceTriggeredAbility(new CreateTokenEffect(new PhyrexianWurm12DeathtouchToken()).withAdditionalTokens(new PhyrexianWurm21LifelinkToken()), false);
this.addAbility(ability);
}

View file

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

View file

@ -16,6 +16,7 @@ import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@ -24,7 +25,7 @@ import java.util.UUID;
*/
public class CreateTokenEffect extends OneShotEffect {
private final Token token;
private final List<Token> tokens = new ArrayList<>();
private final DynamicValue amount;
private final boolean tapped;
private final boolean attacking;
@ -56,7 +57,10 @@ public class CreateTokenEffect extends OneShotEffect {
public CreateTokenEffect(Token token, DynamicValue amount, boolean tapped, boolean attacking) {
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.tapped = tapped;
this.attacking = attacking;
@ -66,7 +70,9 @@ public class CreateTokenEffect extends OneShotEffect {
protected CreateTokenEffect(final CreateTokenEffect effect) {
super(effect);
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.attacking = effect.attacking;
this.lastAddedTokenIds.addAll(effect.lastAddedTokenIds);
@ -82,6 +88,11 @@ public class CreateTokenEffect extends OneShotEffect {
return this;
}
public CreateTokenEffect withAdditionalTokens(Token... tokens) {
this.tokens.addAll(Arrays.asList(tokens));
return this;
}
@Override
public CreateTokenEffect copy() {
return new CreateTokenEffect(this);
@ -90,8 +101,8 @@ public class CreateTokenEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
int value = amount.calculate(game, source, this);
token.putOntoBattlefield(value, game, source, source.getControllerId(), tapped, attacking);
this.lastAddedTokenIds = token.getLastAddedTokenIds();
tokens.get(0).putOntoBattlefield(value, game, source, source.getControllerId(), tapped, attacking, null, null, true, tokens);
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
if (counterType != null) {
for (UUID tokenId : lastAddedTokenIds) {
@ -150,41 +161,53 @@ public class CreateTokenEffect extends OneShotEffect {
}
private void setText() {
String tokenDescription = token.getDescription();
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 ");
if (singular) {
if (tapped && !attacking) {
sb.append("a tapped ");
for (int i = 0; i < tokens.size(); i++) {
if (i > 0) {
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);
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 {
sb.append(CardUtil.addArticle(tokenDescription));
}
} else {
sb.append(CardUtil.numberToText(amount.toString())).append(' ');
if (tapped && !attacking) {
sb.append("tapped ");
}
sb.append(tokenDescription);
if (tokenDescription.endsWith("token")) {
sb.append("s");
}
int tokenLocation = sb.indexOf("token ");
if (tokenLocation != -1) {
sb.replace(tokenLocation, tokenLocation + 6, "tokens ");
sb.append(CardUtil.numberToText(amount.toString())).append(' ');
if (tapped && !attacking) {
sb.append("tapped ");
}
sb.append(tokenDescription);
if (tokenDescription.endsWith("token")) {
sb.append("s");
}
int tokenLocation = sb.indexOf("token ");
if (tokenLocation != -1) {
sb.replace(tokenLocation, tokenLocation + 6, "tokens ");
}
}
}
if (attacking) {
if (singular) {
if (singular && tokens.size() == 1) {
sb.append(" that's");
} else {
sb.append(" that are");

View file

@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.game.permanent.token.Token;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -17,11 +18,15 @@ public class CreateTokenEvent extends GameEvent {
* @param source
* @param controllerId
* @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);
tokens.put(token, amount);
if (tokensList != null) {
for (Token token : tokensList) {
tokens.put(token, amount);
}
}
}
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, List<Token> additionalTokens);
void setPower(int power);
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);
}
@Override
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);
if (controller == null) {
return false;
@ -229,7 +233,11 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
}
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)) {
int currentTokens = game.getBattlefield().countTokens(event.getPlayerId());
int tokenSlots = Math.max(MAX_TOKENS_PER_GAME - currentTokens, 0);