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

@ -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);