mirror of
https://github.com/magefree/mage.git
synced 2025-12-21 19:11:59 -08:00
Implement Impending mechanic (#12865)
* implement Impending mechanic * add initial test * add more tests * small fix
This commit is contained in:
parent
50f892b935
commit
d923f2941d
9 changed files with 348 additions and 20 deletions
|
|
@ -36,7 +36,7 @@ public final class OverlordOfTheBalemurk extends CardImpl {
|
|||
this.toughness = new MageInt(5);
|
||||
|
||||
// Impending 5--{1}{B}
|
||||
this.addAbility(new ImpendingAbility("{1}{B}", 5));
|
||||
this.addAbility(new ImpendingAbility(5, "{1}{B}"));
|
||||
|
||||
// Whenever Overlord of the Balemurk enters or attacks, mill four cards, then you may return a non-Avatar creature card or a planeswalker card from your graveyard to your hand.
|
||||
Ability ability = new EntersBattlefieldOrAttacksSourceTriggeredAbility(new MillCardsControllerEffect(4));
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public final class OverlordOfTheBoilerbilges extends CardImpl {
|
|||
this.toughness = new MageInt(5);
|
||||
|
||||
// Impending 4--{2}{R}{R}
|
||||
this.addAbility(new ImpendingAbility("{2}{R}{R}"));
|
||||
this.addAbility(new ImpendingAbility(4, "{2}{R}{R}"));
|
||||
|
||||
// Whenever Overlord of the Boilerbilges enters or attacks, it deals 4 damage to any target.
|
||||
Ability ability = new EntersBattlefieldOrAttacksSourceTriggeredAbility(new DamageTargetEffect(4));
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public final class OverlordOfTheFloodpits extends CardImpl {
|
|||
this.toughness = new MageInt(3);
|
||||
|
||||
// Impending 4--{1}{U}{U}
|
||||
this.addAbility(new ImpendingAbility("{1}{U}{U}"));
|
||||
this.addAbility(new ImpendingAbility(4, "{1}{U}{U}"));
|
||||
|
||||
// Flying
|
||||
this.addAbility(FlyingAbility.getInstance());
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public final class OverlordOfTheHauntwoods extends CardImpl {
|
|||
this.toughness = new MageInt(5);
|
||||
|
||||
// Impending 4--{1}{G}{G}
|
||||
this.addAbility(new ImpendingAbility("{1}{G}{G}"));
|
||||
this.addAbility(new ImpendingAbility(4, "{1}{G}{G}"));
|
||||
|
||||
// Whenever Overlord of the Hauntwoods enters or attacks, create a tapped colorless land token named Everywhere that is every basic land type.
|
||||
this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public final class OverlordOfTheMistmoors extends CardImpl {
|
|||
this.toughness = new MageInt(6);
|
||||
|
||||
// Impending 4--{2}{W}{W}
|
||||
this.addAbility(new ImpendingAbility("{2}{W}{W}"));
|
||||
this.addAbility(new ImpendingAbility(4, "{2}{W}{W}"));
|
||||
|
||||
// Whenever Overlord of the Mistmoors enters or attacks, create two 2/1 white Insect creature tokens with flying.
|
||||
this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new CreateTokenEffect(new InsectWhiteToken(), 2)));
|
||||
|
|
|
|||
|
|
@ -224,7 +224,5 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
|
|||
cards.add(new SetCardInfo("Winter, Misanthropic Guide", 240, Rarity.RARE, mage.cards.w.WinterMisanthropicGuide.class));
|
||||
cards.add(new SetCardInfo("Withering Torment", 124, Rarity.UNCOMMON, mage.cards.w.WitheringTorment.class));
|
||||
cards.add(new SetCardInfo("Zimone, All-Questioning", 241, Rarity.RARE, mage.cards.z.ZimoneAllQuestioning.class));
|
||||
|
||||
cards.removeIf(setCardInfo -> setCardInfo.getName().startsWith("Overlord"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,226 @@
|
|||
package org.mage.test.cards.abilities.keywords;
|
||||
|
||||
import mage.abilities.keyword.ImpendingAbility;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.permanent.Permanent;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class ImpendingTest extends CardTestPlayerBase {
|
||||
|
||||
private static final String hauntwoods = "Overlord of the Hauntwoods";
|
||||
|
||||
public void assertHasImpending(String name, boolean hasAbility) {
|
||||
Permanent permanent = getPermanent(name);
|
||||
Assert.assertEquals(
|
||||
"Should" + (hasAbility ? "" : "n't") + " have Impending ability",
|
||||
hasAbility, permanent.getAbilities(currentGame).containsClass(ImpendingAbility.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCastRegular() {
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
|
||||
addCard(Zone.HAND, playerA, hauntwoods);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hauntwoods);
|
||||
setChoice(playerA, "Cast with no");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, hauntwoods, 1);
|
||||
assertType(hauntwoods, CardType.ENCHANTMENT, true);
|
||||
assertType(hauntwoods, CardType.CREATURE, true);
|
||||
assertSubtype(hauntwoods, SubType.AVATAR);
|
||||
assertSubtype(hauntwoods, SubType.HORROR);
|
||||
assertCounterCount(playerA, hauntwoods, CounterType.TIME, 0);
|
||||
assertPowerToughness(playerA, hauntwoods, 6, 5);
|
||||
assertHasImpending(hauntwoods, true);
|
||||
|
||||
assertPermanentCount(playerA, "Everywhere", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCastImpending() {
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
addCard(Zone.HAND, playerA, hauntwoods);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hauntwoods);
|
||||
setChoice(playerA, "Cast with Impending");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, hauntwoods, 1);
|
||||
assertType(hauntwoods, CardType.ENCHANTMENT, true);
|
||||
assertType(hauntwoods, CardType.CREATURE, false);
|
||||
assertCounterCount(playerA, hauntwoods, CounterType.TIME, 4);
|
||||
assertHasImpending(hauntwoods, true);
|
||||
|
||||
assertPermanentCount(playerA, "Everywhere", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImpendingRemoveCounter() {
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
addCard(Zone.HAND, playerA, hauntwoods);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hauntwoods);
|
||||
setChoice(playerA, "Cast with Impending");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(2, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, hauntwoods, 1);
|
||||
assertType(hauntwoods, CardType.ENCHANTMENT, true);
|
||||
assertType(hauntwoods, CardType.CREATURE, false);
|
||||
assertCounterCount(playerA, hauntwoods, CounterType.TIME, 4 - 1);
|
||||
assertHasImpending(hauntwoods, true);
|
||||
|
||||
assertPermanentCount(playerA, "Everywhere", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCastImpendingRemoveAllCounters() {
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
addCard(Zone.HAND, playerA, hauntwoods);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hauntwoods);
|
||||
setChoice(playerA, "Cast with Impending");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(8, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, hauntwoods, 1);
|
||||
assertType(hauntwoods, CardType.ENCHANTMENT, true);
|
||||
assertType(hauntwoods, CardType.CREATURE, true);
|
||||
assertSubtype(hauntwoods, SubType.AVATAR);
|
||||
assertSubtype(hauntwoods, SubType.HORROR);
|
||||
assertCounterCount(playerA, hauntwoods, CounterType.TIME, 0);
|
||||
assertPowerToughness(playerA, hauntwoods, 6, 5);
|
||||
assertHasImpending(hauntwoods, false);
|
||||
|
||||
assertPermanentCount(playerA, "Everywhere", 1);
|
||||
}
|
||||
|
||||
private static final String hexmage = "Vampire Hexmage";
|
||||
|
||||
@Test
|
||||
public void testCastImpendingHexmage() {
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
addCard(Zone.BATTLEFIELD, playerA, hexmage);
|
||||
addCard(Zone.HAND, playerA, hauntwoods);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hauntwoods);
|
||||
setChoice(playerA, "Cast with Impending");
|
||||
|
||||
activateAbility(1, PhaseStep.BEGIN_COMBAT, playerA, "Sacrifice", hauntwoods);
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, hauntwoods, 1);
|
||||
assertType(hauntwoods, CardType.ENCHANTMENT, true);
|
||||
assertType(hauntwoods, CardType.CREATURE, true);
|
||||
assertSubtype(hauntwoods, SubType.AVATAR);
|
||||
assertSubtype(hauntwoods, SubType.HORROR);
|
||||
assertCounterCount(playerA, hauntwoods, CounterType.TIME, 0);
|
||||
assertPowerToughness(playerA, hauntwoods, 6, 5);
|
||||
assertHasImpending(hauntwoods, true);
|
||||
|
||||
assertPermanentCount(playerA, "Everywhere", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCastImpendingHexmageNextTurn() {
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
addCard(Zone.BATTLEFIELD, playerA, hexmage);
|
||||
addCard(Zone.HAND, playerA, hauntwoods);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hauntwoods);
|
||||
setChoice(playerA, "Cast with Impending");
|
||||
|
||||
activateAbility(1, PhaseStep.BEGIN_COMBAT, playerA, "Sacrifice", hauntwoods);
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(2, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, hauntwoods, 1);
|
||||
assertType(hauntwoods, CardType.ENCHANTMENT, true);
|
||||
assertType(hauntwoods, CardType.CREATURE, true);
|
||||
assertSubtype(hauntwoods, SubType.AVATAR);
|
||||
assertSubtype(hauntwoods, SubType.HORROR);
|
||||
assertCounterCount(playerA, hauntwoods, CounterType.TIME, 0);
|
||||
assertPowerToughness(playerA, hauntwoods, 6, 5);
|
||||
assertHasImpending(hauntwoods, false);
|
||||
|
||||
assertPermanentCount(playerA, "Everywhere", 1);
|
||||
}
|
||||
|
||||
private static final String solemnity = "Solemnity";
|
||||
|
||||
@Test
|
||||
public void testCastImpendingSolemnity() {
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
addCard(Zone.BATTLEFIELD, playerA, solemnity);
|
||||
addCard(Zone.HAND, playerA, hauntwoods);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hauntwoods);
|
||||
setChoice(playerA, "Cast with Impending");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, hauntwoods, 1);
|
||||
assertType(hauntwoods, CardType.ENCHANTMENT, true);
|
||||
assertType(hauntwoods, CardType.CREATURE, true);
|
||||
assertSubtype(hauntwoods, SubType.AVATAR);
|
||||
assertSubtype(hauntwoods, SubType.HORROR);
|
||||
assertCounterCount(playerA, hauntwoods, CounterType.TIME, 0);
|
||||
assertPowerToughness(playerA, hauntwoods, 6, 5);
|
||||
assertHasImpending(hauntwoods, true);
|
||||
|
||||
assertPermanentCount(playerA, "Everywhere", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCastImpendingSolemnityNextTurn() {
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
addCard(Zone.BATTLEFIELD, playerA, solemnity);
|
||||
addCard(Zone.HAND, playerA, hauntwoods);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, hauntwoods);
|
||||
setChoice(playerA, "Cast with Impending");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(2, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, hauntwoods, 1);
|
||||
assertType(hauntwoods, CardType.ENCHANTMENT, true);
|
||||
assertType(hauntwoods, CardType.CREATURE, true);
|
||||
assertSubtype(hauntwoods, SubType.AVATAR);
|
||||
assertSubtype(hauntwoods, SubType.HORROR);
|
||||
assertCounterCount(playerA, hauntwoods, CounterType.TIME, 0);
|
||||
assertPowerToughness(playerA, hauntwoods, 6, 5);
|
||||
assertHasImpending(hauntwoods, false);
|
||||
|
||||
assertPermanentCount(playerA, "Everywhere", 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,11 +30,15 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
|
|||
}
|
||||
|
||||
protected AlternativeSourceCostsImpl(String name, String reminderText, Cost cost) {
|
||||
this(name, reminderText, cost, name);
|
||||
}
|
||||
|
||||
protected AlternativeSourceCostsImpl(String name, String reminderText, Cost cost, String activationKey) {
|
||||
super(Zone.ALL, null);
|
||||
this.name = name;
|
||||
this.reminderText = reminderText;
|
||||
this.alternativeCost = new AlternativeCostImpl<>(name, reminderText, cost);
|
||||
this.activationKey = getActivationKey(name);
|
||||
this.activationKey = getActivationKey(activationKey);
|
||||
}
|
||||
|
||||
protected AlternativeSourceCostsImpl(final AlternativeSourceCostsImpl ability) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,33 @@
|
|||
package mage.abilities.keyword;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.BeginningOfEndStepTriggeredAbility;
|
||||
import mage.abilities.common.EntersBattlefieldAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.condition.common.SourceHasCounterCondition;
|
||||
import mage.abilities.costs.AlternativeSourceCostsImpl;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.decorator.ConditionalOneShotEffect;
|
||||
import mage.abilities.effects.ContinuousEffectImpl;
|
||||
import mage.abilities.effects.common.AddContinuousEffectToGame;
|
||||
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
|
||||
import mage.abilities.effects.common.counter.RemoveCounterSourceEffect;
|
||||
import mage.constants.*;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* TODO: Implement this
|
||||
* "Impending N–[cost]" is a keyword that represents multiple abilities.
|
||||
* The official rules are as follows:
|
||||
* (a) You may choose to pay [cost] rather than pay this spell's mana cost.
|
||||
* (b) If you chose to pay this spell's impending cost, it enters the battlefield with N time counters on it.
|
||||
* (c) As long as this permanent has a time counter on it, if it was cast for its impending cost, it's not a creature.
|
||||
* (d) At the beginning of your end step, if this permanent was cast for its impending cost, remove a time counter from it. Then if it has no time counters on it, it loses impending.
|
||||
*
|
||||
* @author TheElk801
|
||||
*/
|
||||
|
|
@ -16,14 +37,24 @@ public class ImpendingAbility extends AlternativeSourceCostsImpl {
|
|||
private static final String IMPENDING_REMINDER = "If you cast this spell for its impending cost, " +
|
||||
"it enters with %s time counters and isn't a creature until the last is removed. " +
|
||||
"At the beginning of your end step, remove a time counter from it.";
|
||||
private static final Condition counterCondition = new SourceHasCounterCondition(CounterType.TIME, 0, 0);
|
||||
|
||||
public ImpendingAbility(String manaString) {
|
||||
this(manaString, 4);
|
||||
}
|
||||
|
||||
public ImpendingAbility(String manaString, int amount) {
|
||||
super(IMPENDING_KEYWORD + ' ' + amount, String.format(IMPENDING_REMINDER, CardUtil.numberToText(amount)), manaString);
|
||||
public ImpendingAbility(int amount, String manaString) {
|
||||
super(IMPENDING_KEYWORD + ' ' + amount, String.format(IMPENDING_REMINDER, CardUtil.numberToText(amount)), new ManaCostsImpl<>(manaString), IMPENDING_KEYWORD);
|
||||
this.setRuleAtTheTop(true);
|
||||
this.addSubAbility(new EntersBattlefieldAbility(new ConditionalOneShotEffect(
|
||||
new AddCountersSourceEffect(CounterType.TIME.createInstance(amount)), ImpendingCondition.instance, ""
|
||||
), "").setRuleVisible(false));
|
||||
this.addSubAbility(new SimpleStaticAbility(new ImpendingAbilityTypeEffect()).setRuleVisible(false));
|
||||
Ability ability = new BeginningOfEndStepTriggeredAbility(
|
||||
new RemoveCounterSourceEffect(CounterType.TIME.createInstance()),
|
||||
TargetController.YOU, ImpendingCondition.instance, false
|
||||
);
|
||||
ability.addEffect(new ConditionalOneShotEffect(
|
||||
new AddContinuousEffectToGame(new ImpendingAbilityRemoveEffect()),
|
||||
counterCondition, "Then if it has no time counters on it, it loses impending"
|
||||
));
|
||||
this.addSubAbility(ability.setRuleVisible(false));
|
||||
}
|
||||
|
||||
private ImpendingAbility(final ImpendingAbility ability) {
|
||||
|
|
@ -35,12 +66,81 @@ public class ImpendingAbility extends AlternativeSourceCostsImpl {
|
|||
return new ImpendingAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(Ability source, Game game) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String getActivationKey() {
|
||||
return getActivationKey(IMPENDING_KEYWORD);
|
||||
}
|
||||
}
|
||||
|
||||
enum ImpendingCondition implements Condition {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
return CardUtil.checkSourceCostsTagExists(game, source, ImpendingAbility.getActivationKey());
|
||||
}
|
||||
}
|
||||
|
||||
class ImpendingAbilityTypeEffect extends ContinuousEffectImpl {
|
||||
|
||||
ImpendingAbilityTypeEffect() {
|
||||
super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Detriment);
|
||||
staticText = "As long as this permanent has a time counter on it, if it was cast for its impending cost, it's not a creature.";
|
||||
}
|
||||
|
||||
private ImpendingAbilityTypeEffect(final ImpendingAbilityTypeEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImpendingAbilityTypeEffect copy() {
|
||||
return new ImpendingAbilityTypeEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
if (!ImpendingCondition.instance.apply(game, source)) {
|
||||
return false;
|
||||
}
|
||||
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
|
||||
if (permanent.getCounters(game).getCount(CounterType.TIME) < 1) {
|
||||
return false;
|
||||
}
|
||||
permanent.removeCardType(game, CardType.CREATURE);
|
||||
permanent.removeAllCreatureTypes(game);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ImpendingAbilityRemoveEffect extends ContinuousEffectImpl {
|
||||
|
||||
ImpendingAbilityRemoveEffect() {
|
||||
super(Duration.Custom, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.LoseAbility);
|
||||
}
|
||||
|
||||
private ImpendingAbilityRemoveEffect(final ImpendingAbilityRemoveEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImpendingAbilityRemoveEffect copy() {
|
||||
return new ImpendingAbilityRemoveEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
|
||||
if (permanent == null) {
|
||||
discard();
|
||||
return false;
|
||||
}
|
||||
permanent.removeAbilities(
|
||||
permanent
|
||||
.getAbilities(game)
|
||||
.stream()
|
||||
.filter(ImpendingAbility.class::isInstance)
|
||||
.collect(Collectors.toList()),
|
||||
source.getSourceId(), game
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue