mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
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:
parent
e2d2d1f08e
commit
6ed702a7b3
13 changed files with 190 additions and 69 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 doesn’t 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 doesn’t 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 token’s
|
||||
// subtype is Hamster, but because Minsc specifies that the token’s 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 that’s a copy of target
|
||||
// creature.” All of that token’s 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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 doesn’t 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 doesn’t 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)) {
|
||||
|
|
|
|||
|
|
@ -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, "
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 doesn’t 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 doesn’t 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue