Tokens improved:

- added auto-generated token names for CreatureToken;
 - added verify tests for token names;
 - removed outdated ElementalCreatureToken;
 - fixed wrong indefinite article in some tokens;
 - fixed miss Token in some token names (related to #10139);
This commit is contained in:
Oleg Agafonov 2023-05-04 17:38:54 +04:00
parent e2d2d1f08e
commit 6ed702a7b3
13 changed files with 190 additions and 69 deletions

View file

@ -15,7 +15,7 @@ import mage.constants.SubType;
import mage.constants.Duration;
import mage.constants.Zone;
import mage.filter.common.FilterLandCard;
import mage.game.permanent.token.custom.ElementalCreatureToken;
import mage.game.permanent.token.custom.CreatureToken;
import mage.target.common.TargetCardInYourGraveyard;
/**
@ -33,7 +33,7 @@ public final class HostileDesert extends CardImpl {
addAbility(new ColorlessManaAbility());
// {2}, Exile a land card from your graveyard: Hostile Desert becomes a 3/4 Elemental creature until end of turn. It's still a land.
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new BecomesCreatureSourceEffect(
new ElementalCreatureToken(3, 4, "3/4 Elemental creature"),
new CreatureToken(3, 4, "3/4 Elemental creature", SubType.ELEMENTAL),
"land", Duration.EndOfTurn), new GenericManaCost(2));
ability.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(new FilterLandCard("land card from your graveyard"))));
addAbility(ability);

View file

@ -3,7 +3,6 @@ package mage.cards.i;
import java.util.UUID;
import mage.MageInt;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.common.SimpleActivatedAbility;
@ -23,7 +22,7 @@ import mage.counters.CounterType;
import mage.filter.common.FilterLandPermanent;
import mage.filter.predicate.permanent.TappedPredicate;
import mage.game.Game;
import mage.game.permanent.token.custom.ElementalCreatureToken;
import mage.game.permanent.token.custom.CreatureToken;
import mage.target.common.TargetLandPermanent;
/**
@ -47,7 +46,7 @@ public final class IgnitionTeam extends CardImpl {
// {2}{R}, Remove a +1/+1 counter from Ignition Team: Target land becomes a 4/4 red Elemental creature until end of turn. It's still a land.
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new BecomesCreatureTargetEffect(
new ElementalCreatureToken(4, 4, "4/4 red Elemental creature", new ObjectColor("R")),
new CreatureToken(4, 4, "4/4 red Elemental creature", SubType.ELEMENTAL).withColor("R"),
false, true, Duration.EndOfTurn), new ManaCostsImpl<>("{2}{R}"));
ability.addCost(new RemoveCountersSourceCost(CounterType.P1P1.createInstance(1)));
ability.addTarget(new TargetLandPermanent());

View file

@ -3,7 +3,7 @@
package mage.cards.r;
import java.util.UUID;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.common.EntersBattlefieldTappedAbility;
@ -19,9 +19,10 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.permanent.token.custom.ElementalCreatureToken;
import mage.game.permanent.token.custom.CreatureToken;
/**
*
@ -38,7 +39,7 @@ public final class RagingRavine extends CardImpl {
this.addAbility(new GreenManaAbility());
this.addAbility(new RedManaAbility());
Effect effect = new BecomesCreatureSourceEffect(
new ElementalCreatureToken(3, 3, "3/3 red and green Elemental creature", new ObjectColor("RG")),
new CreatureToken(3, 3, "3/3 red and green Elemental creature", SubType.ELEMENTAL).withColor("RG"),
"land", Duration.EndOfTurn);
effect.setText("Until end of turn, {this} becomes a 3/3 red and green Elemental creature");
// {2}{R}{G}: Until end of turn, Raging Ravine becomes a 3/3 red and green Elemental creature with "Whenever this creature attacks, put a +1/+1 counter on it." It's still a land.

View file

@ -17,7 +17,7 @@ import mage.constants.Duration;
import mage.constants.Zone;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.common.FilterControlledLandPermanent;
import mage.game.permanent.token.custom.ElementalCreatureToken;
import mage.game.permanent.token.custom.CreatureToken;
import mage.target.TargetPermanent;
/**
@ -40,7 +40,7 @@ public final class SkarrgGuildmage extends CardImpl {
new ManaCostsImpl<>("{R}{G}")));
// {1}{R}{G}: Target land you control becomes a 4/4 Elemental creature until end of turn. It's still a land.
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new BecomesCreatureTargetEffect(
new ElementalCreatureToken(4, 4, "4/4 Elemental creature"),
new CreatureToken(4, 4, "4/4 Elemental creature", SubType.ELEMENTAL),
false, true, Duration.EndOfTurn), new ManaCostsImpl<>("{1}{R}{G}") );
ability.addTarget(new TargetPermanent(new FilterControlledLandPermanent()));
this.addAbility(ability);

View file

@ -2,7 +2,7 @@
package mage.cards.v;
import java.util.UUID;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.common.DiesAttachedTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
@ -13,7 +13,7 @@ import mage.abilities.keyword.EnchantAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.permanent.token.custom.ElementalCreatureToken;
import mage.game.permanent.token.custom.CreatureToken;
import mage.target.TargetPermanent;
import mage.target.common.TargetLandPermanent;
@ -39,7 +39,7 @@ public final class VastwoodZendikon extends CardImpl {
this.addAbility(ability);
Ability ability2 = new SimpleStaticAbility(Zone.BATTLEFIELD, new BecomesCreatureAttachedEffect(
new ElementalCreatureToken(6, 4, "6/4 green Elemental creature", new ObjectColor("G")),
new CreatureToken(6, 4, "6/4 green Elemental creature", SubType.ELEMENTAL).withColor("G"),
"Enchanted land is a 6/4 green Elemental creature. It's still a land", Duration.WhileOnBattlefield, BecomesCreatureAttachedEffect.LoseType.COLOR));
this.addAbility(ability2);

View file

@ -0,0 +1,95 @@
package org.mage.test.serverside;
import mage.constants.PhaseStep;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.permanent.token.custom.CreatureToken;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* 111.4. A spell or ability that creates a token sets both its name and its subtype(s).
* If the spell or ability doesnt specify the name of the token, its name is the same as
* its subtype(s) plus the word Token. Once a token is on the battlefield, changing its
* name doesnt change its subtype(s), and vice versa.
* <p>
* 111.10. Some effects instruct a player to create a predefined token. These effects use the
* definition below to determine the characteristics the token is created with. The effect that
* creates a predefined token may also modify or add to the predefined characteristics.
*
* @author JayDi85
*/
public class TokenNamesTest extends CardTestPlayerBase {
@Test
public void test_Rules_111_4_Example1() {
// Example: Dwarven Reinforcements is a sorcery that says, in part, Create two 2/1 red Dwarf Berserker
// creature tokens. The tokens created as it resolves are each named Dwarf Berserker Token and each
// have the creature types Dwarf and Berserker.
addCard(Zone.HAND, playerA, "Dwarven Reinforcements", 1); // {3}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dwarven Reinforcements");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Dwarf Berserker Token", 2);
}
@Test
public void test_Rules_111_4_Example2() {
// Example: Minsc, Beloved Ranger says, in part, When Minsc, Beloved Ranger enters the battlefield,
// create Boo, a legendary 1/1 red Hamster creature token with trample and haste. That tokens
// subtype is Hamster, but because Minsc specifies that the tokens name is Boo, neither Hamster
// nor Token are part of its name.
addCard(Zone.HAND, playerA, "Minsc, Beloved Ranger", 1); // {R}{G}{W}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Minsc, Beloved Ranger");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Boo", 1);
}
@Test
public void test_Rules_111_4_Example3() {
// Example: Spitting Image is a sorcery that says, in part, Create a token thats a copy of target
// creature. All of that tokens characteristics will match the copiable characteristics of the
// creature targeted by that spell. If Spitting Image targets Doomed Dissenter, a Human creature,
// the name of the token the spell creates will be Doomed Dissenter, not Human Token or Doomed Dissenter Token.
// Create a token that's a copy of target creature.
addCard(Zone.HAND, playerA, "Spitting Image", 1); // {4}{G/U}{G/U}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
//
addCard(Zone.BATTLEFIELD, playerA, "Doomed Dissenter", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spitting Image");
addTarget(playerA, "Doomed Dissenter");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Doomed Dissenter", 2); // card + token
assertTokenCount(playerA, "Doomed Dissenter", 1);
}
@Test
public void test_Rules_111_4_AutoGeneratedName() {
Assert.assertEquals("Human Cleric Token", new CreatureToken(2, 2, "", SubType.HUMAN, SubType.CLERIC).getName());
Assert.assertEquals("Warrior Token", new CreatureToken(2, 2, "", SubType.WARRIOR).getName());
Assert.assertEquals("Custom Name", new CreatureToken(2, 2, "", SubType.WARRIOR).withName("Custom Name").getName());
}
}

View file

@ -27,6 +27,7 @@ import mage.game.draft.DraftCube;
import mage.game.draft.RateCard;
import mage.game.permanent.token.Token;
import mage.game.permanent.token.TokenImpl;
import mage.game.permanent.token.custom.CreatureToken;
import mage.server.util.SystemUtil;
import mage.sets.TherosBeyondDeath;
import mage.util.CardUtil;
@ -1285,21 +1286,66 @@ public class VerifyCardDataTest {
// CHECK: private class for inner tokens (no needs at all -- all private tokens must be replaced by CreatureToken)
for (Class<? extends TokenImpl> tokenClass : privateTokens) {
String className = extractShortClass(tokenClass);
errorsList.add("Error: no needs in private tokens, replace it with CreatureToken: " + className + " from " + tokenClass.getName());
errorsList.add("Warning: no needs in private tokens, replace it with CreatureToken: " + className + " from " + tokenClass.getName());
}
// CHECK: all public tokens must have tok-data (private tokens uses for innner abilities -- no need images for it)
for (Class<? extends TokenImpl> tokenClass : publicTokens) {
Token token = (Token) createNewObject(tokenClass);
if (token == null) {
// how-to fix:
// - create empty param
// - fix error in token's constructor
errorsList.add("Error: token must have default constructor with zero params: " + tokenClass.getName());
} else if (tokDataNamesIndex.getOrDefault(token.getName().replace(" Token", ""), "").isEmpty()) {
// how-to fix: public token must be downloadable, so tok-data must contain miss set
// (also don't forget to add new set to scryfall download)
if (token instanceof CreatureToken) {
// ignore custom token builders
continue;
}
// how-to fix:
// - public token must be downloadable, so tok-data must contain miss set
// (also don't forget to add new set to scryfall download)
errorsList.add("Error: can't find data in tokens-database.txt for token: " + tokenClass.getName() + " -> " + token.getName());
}
}
// 111.4. A spell or ability that creates a token sets both its name and its subtype(s).
// If the spell or ability doesnt specify the name of the token, its name is the same
// as its subtype(s) plus the word Token. Once a token is on the battlefield, changing
// its name doesnt change its subtype(s), and vice versa.
for (Class<? extends TokenImpl> tokenClass : publicTokens) {
Token token = (Token) createNewObject(tokenClass);
if (token == null) {
// error in constructor, see checks above
continue;
}
// CHECK: tokens must have Token word in the name
if (token.getDescription().startsWith(token.getName() + ", ")
|| token.getDescription().contains("named " + token.getName())
|| (token instanceof CreatureToken)) {
// ignore some names:
// - Boo, a legendary 1/1 red Hamster creature token with trample and haste
// - 1/1 green Insect creature token with flying named Butterfly
// - custom token builders
} else {
if (!token.getName().endsWith("Token")) {
errorsList.add("Error: token's name must ends with Token: "
+ tokenClass.getName() + " - " + token.getName());
}
}
// CHECK: named tokens must not have Token in the name
if (token.getDescription().contains("named") && token.getName().contains("Token")) {
if (token.getDescription().contains("card named")) {
// ignore ability text like Return a card named Deathpact Angel from
continue;
}
errorsList.add("Error: named token must not have Token in the name: "
+ tokenClass.getName() + " - " + token.getName() + " - " + token.getDescription());
}
}
// CHECK: wrong set codes in tok-data
tokDataTokensBySetIndex.forEach((setCode, setTokens) -> {
if (!allSetCodes.contains(setCode)) {

View file

@ -15,7 +15,7 @@ public final class NestingDragonToken extends TokenImpl {
public NestingDragonToken() {
super(
"Dragon Egg",
"Dragon Egg Token",
"0/2 red Dragon Egg creature token with defender and "
+ "\""
+ "When this creature dies, "

View file

@ -16,7 +16,7 @@ public final class OviyaPashiriSageLifecrafterToken extends TokenImpl {
}
public OviyaPashiriSageLifecrafterToken(int number) {
super("Construct Token", "an X/X colorless Construct artifact creature token, where X is the number of creatures you control");
super("Construct Token", "X/X colorless Construct artifact creature token, where X is the number of creatures you control");
cardType.add(CardType.ARTIFACT);
cardType.add(CardType.CREATURE);
subtype.add(SubType.CONSTRUCT);

View file

@ -11,7 +11,7 @@ import mage.constants.SubType;
public class RatRogueToken extends TokenImpl {
public RatRogueToken() {
super("Rat Rogue", "1/1 black Rat Rogue creature token");
super("Rat Rogue Token", "1/1 black Rat Rogue creature token");
cardType.add(CardType.CREATURE);
color.setBlack(true);
subtype.add(SubType.RAT);

View file

@ -12,7 +12,7 @@ import mage.MageInt;
public final class SubterraneanTremorsLizardToken extends TokenImpl {
public SubterraneanTremorsLizardToken() {
super("Lizard Token", "an 8/8 red Lizard creature token");
super("Lizard Token", "8/8 red Lizard creature token");
cardType.add(CardType.CREATURE);
color.setRed(true);
subtype.add(SubType.LIZARD);

View file

@ -5,13 +5,15 @@ import mage.ObjectColor;
import mage.abilities.Ability;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.game.permanent.token.Token;
import mage.game.permanent.token.TokenImpl;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* Token builder for token effects
*
* <p>
* Use it for custom tokens (tokens without public class and image)
*
* @author JayDi85
@ -27,7 +29,7 @@ public final class CreatureToken extends TokenImpl {
}
public CreatureToken(int power, int toughness, String description) {
this(power, toughness, description, null);
this(power, toughness, description, (SubType[]) null);
}
public CreatureToken(int power, int toughness, String description, SubType... extraSubTypes) {
@ -41,6 +43,11 @@ public final class CreatureToken extends TokenImpl {
}
}
public CreatureToken withName(String name) {
this.setName(name);
return this;
}
public CreatureToken withAbility(Ability ability) {
this.addAbility(ability);
return this;
@ -73,4 +80,24 @@ public final class CreatureToken extends TokenImpl {
public CreatureToken copy() {
return new CreatureToken(this);
}
private static String AutoGenerateTokenName(Token token) {
// 111.4. A spell or ability that creates a token sets both its name and its subtype(s).
// If the spell or ability doesnt specify the name of the token,
// its name is the same as its subtype(s) plus the word Token.
// Once a token is on the battlefield, changing its name doesnt change its subtype(s), and vice versa.
return token.getSubtype()
.stream()
.map(SubType::getDescription)
.collect(Collectors.joining(" ")) + " Token";
}
@Override
public String getName() {
String name = super.getName();
if (name.isEmpty()) {
name = AutoGenerateTokenName(this);
}
return name;
}
}

View file

@ -1,47 +0,0 @@
package mage.game.permanent.token.custom;
import mage.MageInt;
import mage.ObjectColor;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.game.permanent.token.TokenImpl;
/**
*
* @author JayDi85
*/
public final class ElementalCreatureToken extends TokenImpl {
public ElementalCreatureToken() {
this(0, 0);
}
public ElementalCreatureToken(int power, int toughness) {
this(power, toughness, String.format("%d/%d Elemental creature", power, toughness));
}
public ElementalCreatureToken(int power, int toughness, String description) {
this(power, toughness, description, (ObjectColor) null);
}
public ElementalCreatureToken(int power, int toughness, String description, ObjectColor color) {
super("", description);
this.cardType.add(CardType.CREATURE);
this.subtype.add(SubType.ELEMENTAL);
this.power = new MageInt(power);
this.toughness = new MageInt(toughness);
if (color != null) {
this.color.addColor(color);
}
}
public ElementalCreatureToken(final ElementalCreatureToken token) {
super(token);
}
public ElementalCreatureToken copy() {
return new ElementalCreatureToken(this);
}
}