This commit is contained in:
Jeff 2021-08-21 18:05:47 -05:00
commit 28d9d64112
55 changed files with 1136 additions and 217 deletions

View file

@ -318,6 +318,33 @@ public class MageBook extends JComponent {
} }
} }
// dungeons
List<CardDownloadData> allDungeons = getTokenCardUrls();
for (CardDownloadData dungeon : allDungeons) {
if (dungeon.getSet().equals(currentSet)) {
try {
String className = dungeon.getName();
if (dungeon.getTokenClassName() != null && dungeon.getTokenClassName().length() > 0) {
if (dungeon.getTokenClassName().toLowerCase(Locale.ENGLISH).matches(".*dungeon.*")) {
className = dungeon.getTokenClassName();
className = "mage.game.command.dungeons." + className;
}
} else {
continue;
}
Class<?> c = Class.forName(className);
Constructor<?> cons = c.getConstructor();
Object newDungeon = cons.newInstance();
if (newDungeon instanceof Dungeon) {
((Dungeon) newDungeon).setExpansionSetCodeForImage(currentSet);
res.add(newDungeon);
}
} catch (ClassNotFoundException | InvocationTargetException | IllegalArgumentException | IllegalAccessException | InstantiationException | SecurityException | NoSuchMethodException ex) {
// Swallow exception
}
}
}
return res; return res;
} }

View file

@ -613,6 +613,30 @@ public class ScryfallImageSupportTokens {
put("MH2/Zombie Army", "https://api.scryfall.com/cards/tmh2/7/en?format=image"); put("MH2/Zombie Army", "https://api.scryfall.com/cards/tmh2/7/en?format=image");
put("MH2/Zombie", "https://api.scryfall.com/cards/tmh2/6/en?format=image"); put("MH2/Zombie", "https://api.scryfall.com/cards/tmh2/6/en?format=image");
// AFR
put("AFR/Angel", "https://api.scryfall.com/cards/tafr/1/en?format=image");
put("AFR/Boo", "https://api.scryfall.com/cards/tafr/10/en?format=image");
put("AFR/Devil", "https://api.scryfall.com/cards/tafr/11/en?format=image");
put("AFR/Dog Illusion", "https://api.scryfall.com/cards/tafr/3/en?format=image");
put("AFR/Dungeon of the Mad Mage", "https://api.scryfall.com/cards/tafr/20/en?format=image");
put("AFR/Emblem Ellywick Tumblestrum", "https://api.scryfall.com/cards/tafr/16/en?format=image");
put("AFR/Faerie Dragon", "https://api.scryfall.com/cards/tafr/4/en?format=image");
put("AFR/Goblin", "https://api.scryfall.com/cards/tafr/12/en?format=image");
put("AFR/Guenhwyvar", "https://api.scryfall.com/cards/tafr/13/en?format=image");
put("AFR/Icingdeath, Frost Tongue", "https://api.scryfall.com/cards/tafr/2/en?format=image");
put("AFR/Emblem Lolth, Spider Queen", "https://api.scryfall.com/cards/tafr/17/en?format=image");
put("AFR/Lost Mine of Phandelver", "https://api.scryfall.com/cards/tafr/21/en?format=image");
put("AFR/Emblem Mordenkainen", "https://api.scryfall.com/cards/tafr/18/en?format=image");
put("AFR/Skeleton", "https://api.scryfall.com/cards/tafr/6/en?format=image");
put("AFR/Spider", "https://api.scryfall.com/cards/tafr/7/en?format=image");
put("AFR/The Atropal", "https://api.scryfall.com/cards/tafr/5/en?format=image");
put("AFR/Tomb of Annihilation", "https://api.scryfall.com/cards/tafr/22/en?format=image");
put("AFR/Treasure", "https://api.scryfall.com/cards/tafr/15/en?format=image");
put("AFR/Vecna", "https://api.scryfall.com/cards/tafr/8/en?format=image");
put("AFR/Wolf", "https://api.scryfall.com/cards/tafr/14/en?format=image");
put("AFR/Emblem Zariel, Archduke of Avernus", "https://api.scryfall.com/cards/tafr/19/en?format=image");
put("AFR/Zombie", "https://api.scryfall.com/cards/tafr/9/en?format=image");
// generate supported sets // generate supported sets
supportedSets.clear(); supportedSets.clear();
for (String cardName : this.keySet()) { for (String cardName : this.keySet()) {

View file

@ -550,7 +550,6 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
CardDownloadData card = new CardDownloadData(params[3], set, "0", false, type, "", "", true); CardDownloadData card = new CardDownloadData(params[3], set, "0", false, type, "", "", true);
card.setTokenClassName(tokenClassName); card.setTokenClassName(tokenClassName);
list.add(card); list.add(card);
// logger.debug("Token: " + set + "/" + card.getName() + " type: " + type);
} else if (params[1].toLowerCase(Locale.ENGLISH).equals("generate") && params[2].startsWith("EMBLEM:")) { } else if (params[1].toLowerCase(Locale.ENGLISH).equals("generate") && params[2].startsWith("EMBLEM:")) {
String set = params[2].substring(7); String set = params[2].substring(7);
CardDownloadData card = new CardDownloadData("Emblem " + params[3], set, "0", false, type, "", "", true, fileName); CardDownloadData card = new CardDownloadData("Emblem " + params[3], set, "0", false, type, "", "", true, fileName);
@ -571,6 +570,11 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
CardDownloadData card = new CardDownloadData(params[3], set, "0", false, type, "", "", true, fileName); CardDownloadData card = new CardDownloadData(params[3], set, "0", false, type, "", "", true, fileName);
card.setTokenClassName(tokenClassName); card.setTokenClassName(tokenClassName);
list.add(card); list.add(card);
} else if (params[1].toLowerCase(Locale.ENGLISH).equals("generate") && params[2].startsWith("DUNGEON:")) {
String set = params[2].substring(8);
CardDownloadData card = new CardDownloadData(params[3], set, "0", false, type, "", "", true, fileName);
card.setTokenClassName(tokenClassName);
list.add(card);
} else { } else {
logger.error("wrong line format in tokens file: " + line); logger.error("wrong line format in tokens file: " + line);
} }

View file

@ -108,6 +108,10 @@
|Generate|EMBLEM:KHM|Tyvar Kell||Emblem Tyvar|TyvarKellEmblem| |Generate|EMBLEM:KHM|Tyvar Kell||Emblem Tyvar|TyvarKellEmblem|
|Generate|EMBLEM:STX|Lukka, Wayward Bonder||Emblem Lukka|LukkaWaywardBonderEmblem| |Generate|EMBLEM:STX|Lukka, Wayward Bonder||Emblem Lukka|LukkaWaywardBonderEmblem|
|Generate|EMBLEM:STX|Rowan, Scholar of Sparks||Emblem Rowan|RowanScholarOfSparksEmblem| |Generate|EMBLEM:STX|Rowan, Scholar of Sparks||Emblem Rowan|RowanScholarOfSparksEmblem|
|Generate|EMBLEM:AFR|Ellywick Tumblestrum||Emblem Ellywick|EllywickTumblestrumEmblem|
|Generate|EMBLEM:AFR|Lolth, Spider Queen||Emblem Lolth|LolthSpiderQueenEmblem|
|Generate|EMBLEM:AFR|Mordenkainen||Emblem Mordenkainen|MordenkainenEmblem|
|Generate|EMBLEM:AFR|Zariel, Archduke of Avernus||Emblem Zariel|ZarielArchdukeOfAvernusEmblem|
# Planes # Planes
|Generate|PLANE:PCA|Plane - Academy at Tolaria West|||AcademyAtTolariaWestPlane| |Generate|PLANE:PCA|Plane - Academy at Tolaria West|||AcademyAtTolariaWestPlane|
@ -177,6 +181,11 @@
|Generate|TOK:AER|Gremlin|||GremlinToken| |Generate|TOK:AER|Gremlin|||GremlinToken|
|Generate|TOK:AER|Ragavan|||RagavanToken| |Generate|TOK:AER|Ragavan|||RagavanToken|
# Dungeons
|Generate|DUNGEON:AFR|Tomb of Annihilation|||TombOfAnnihilationDungeon|
|Generate|DUNGEON:AFR|Lost Mine of Phandelver|||LostMineOfPhandelverDungeon|
|Generate|DUNGEON:AFR|Dungeon of the Mad Mage|||DungeonOfTheMadMageDungeon|
# AKH # AKH
|Generate|TOK:AKH|Beast|||BeastToken3| |Generate|TOK:AKH|Beast|||BeastToken3|
|Generate|TOK:AKH|Cat|||CatToken2| |Generate|TOK:AKH|Cat|||CatToken2|
@ -1564,3 +1573,20 @@
|Generate|TOK:MH2|Treasure|2||TreasureToken| |Generate|TOK:MH2|Treasure|2||TreasureToken|
|Generate|TOK:MH2|Zombie|||ZombieToken| |Generate|TOK:MH2|Zombie|||ZombieToken|
|Generate|TOK:MH2|Zombie Army|||ZombieArmyToken| |Generate|TOK:MH2|Zombie Army|||ZombieArmyToken|
# AFR
|Generate|TOK:AFR|Angel|||Angel33Token|
|Generate|TOK:AFR|Boo|||BooToken|
|Generate|TOK:AFR|Devil|||DevilToken|
|Generate|TOK:AFR|Dog Illusion|||DogIllusionToken|
# |Generate|TOK:AFR|Faerie Dragon|||xxx| TODO: add after dice pr merge
|Generate|TOK:AFR|Goblin|||GoblinToken|
|Generate|TOK:AFR|Guenhwyvar|||GuenhwyvarToken|
|Generate|TOK:AFR|Icingdeath, Frost Tongue|||IcingdeathFrostTongueToken|
|Generate|TOK:AFR|Skeleton|||SkeletonToken|
|Generate|TOK:AFR|Spider|||LolthSpiderToken|
|Generate|TOK:AFR|The Atropal|||TheAtropalToken|
|Generate|TOK:AFR|Treasure|||TreasureToken|
|Generate|TOK:AFR|Vecna|||VecnaToken|
|Generate|TOK:AFR|Wolf|||WolfToken|
|Generate|TOK:AFR|Zombie|||ZombieToken|

View file

@ -518,7 +518,7 @@ public class SessionImpl implements Session {
} }
try { try {
if (callbackClient.isConnected()) { if (callbackClient != null && callbackClient.isConnected()) {
callbackClient.removeListener(callbackHandler); callbackClient.removeListener(callbackHandler);
callbackClient.disconnect(); callbackClient.disconnect();
} }

View file

@ -429,10 +429,12 @@ public class CardView extends SimpleCardView {
this.cardIcons.add(FaceDownCardIcon.instance); this.cardIcons.add(FaceDownCardIcon.instance);
} }
// commander // commander
if (game != null) {
Player owner = game.getPlayer(game.getOwnerId(permanent)); Player owner = game.getPlayer(game.getOwnerId(permanent));
if (owner != null && game.isCommanderObject(owner, permanent)) { if (owner != null && game.isCommanderObject(owner, permanent)) {
this.cardIcons.add(CommanderCardIcon.instance); this.cardIcons.add(CommanderCardIcon.instance);
} }
}
} else { } else {
if (card.isCopy()) { if (card.isCopy()) {
this.mageObjectType = MageObjectType.COPY_CARD; this.mageObjectType = MageObjectType.COPY_CARD;

View file

@ -446,8 +446,14 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
logger.info("simulating - timed out"); logger.info("simulating - timed out");
task.cancel(true); task.cancel(true);
} catch (ExecutionException e) { } catch (ExecutionException e) {
// exception error in simulated game
e.printStackTrace(); e.printStackTrace();
task.cancel(true); task.cancel(true);
// real games: must catch
// unit tests: must raise again for test fail
if (this.isTestsMode()) {
throw new IllegalStateException("One of the simulated games raise the error: " + e.getCause());
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
task.cancel(true); task.cancel(true);

View file

@ -130,7 +130,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map<String, Serializable> options) { public boolean choose(Outcome outcome, Target target, UUID sourceId, Game game, Map<String, Serializable> options) {
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString()); log.debug("choose: " + outcome.toString() + ':' + target.toString());
} }
// controller hints: // controller hints:
@ -205,21 +205,30 @@ public class ComputerPlayer extends PlayerImpl implements Player {
} }
if (target.getOriginalTarget() instanceof TargetPermanent) { if (target.getOriginalTarget() instanceof TargetPermanent) {
TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget();
FilterPermanent filter = null;
if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) {
filter = (FilterPermanent) target.getOriginalTarget().getFilter();
}
if (filter == null) {
throw new IllegalStateException("Unsupported permanent filter in computer's choose method: "
+ target.getOriginalTarget().getClass().getCanonicalName());
}
List<Permanent> targets; List<Permanent> targets;
if (outcome.isCanTargetAll()) { if (outcome.isCanTargetAll()) {
targets = threats(null, sourceId, origTarget.getFilter(), game, target.getTargets()); targets = threats(null, sourceId, filter, game, target.getTargets());
} else { } else {
if (outcome.isGood()) { if (outcome.isGood()) {
targets = threats(abilityControllerId, sourceId, origTarget.getFilter(), game, target.getTargets()); targets = threats(abilityControllerId, sourceId, filter, game, target.getTargets());
} else { } else {
targets = threats(randomOpponentId, sourceId, origTarget.getFilter(), game, target.getTargets()); targets = threats(randomOpponentId, sourceId, filter, game, target.getTargets());
} }
if (targets.isEmpty() && target.isRequired()) { if (targets.isEmpty() && target.isRequired()) {
if (!outcome.isGood()) { if (!outcome.isGood()) {
targets = threats(abilityControllerId, sourceId, origTarget.getFilter(), game, target.getTargets()); targets = threats(abilityControllerId, sourceId, filter, game, target.getTargets());
} else { } else {
targets = threats(randomOpponentId, sourceId, origTarget.getFilter(), game, target.getTargets()); targets = threats(randomOpponentId, sourceId, filter, game, target.getTargets());
} }
} }
} }
@ -429,8 +438,17 @@ public class ComputerPlayer extends PlayerImpl implements Player {
if (target.getOriginalTarget() instanceof TargetCardInExile) { if (target.getOriginalTarget() instanceof TargetCardInExile) {
List<UUID> alreadyTargeted = target.getTargets(); List<UUID> alreadyTargeted = target.getTargets();
TargetCard originalTarget = (TargetCard) target.getOriginalTarget();
List<Card> cards = game.getExile().getCards(originalTarget.getFilter(), game); FilterCard filter = null;
if (target.getOriginalTarget().getFilter() instanceof FilterCard) {
filter = (FilterCard) target.getOriginalTarget().getFilter();
}
if (filter == null) {
throw new IllegalStateException("Unsupported exile target filter in computer's choose method: "
+ target.getOriginalTarget().getClass().getCanonicalName());
}
List<Card> cards = game.getExile().getCards(filter, game);
while (!cards.isEmpty()) { while (!cards.isEmpty()) {
Card card = pickTarget(abilityControllerId, cards, outcome, target, null, game); Card card = pickTarget(abilityControllerId, cards, outcome, target, null, game);
if (card != null && alreadyTargeted != null && !alreadyTargeted.contains(card.getId())) { if (card != null && alreadyTargeted != null && !alreadyTargeted.contains(card.getId())) {
@ -461,10 +479,23 @@ public class ComputerPlayer extends PlayerImpl implements Player {
if (!required) { if (!required) {
return false; return false;
} }
throw new IllegalStateException("TargetSource wasn't handled. class: " + target.getClass().toString()); throw new IllegalStateException("TargetSource wasn't handled in computer's choose method: " + target.getClass().getCanonicalName());
} }
throw new IllegalStateException("Target wasn't handled. class: " + target.getClass().toString()); if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) {
Cards cards = new CardsImpl(possibleTargets);
List<Card> possibleCards = new ArrayList<>(cards.getCards(game));
while (!target.isChosen() && !possibleCards.isEmpty()) {
Card pick = pickTarget(abilityControllerId, possibleCards, outcome, target, null, game);
if (pick != null) {
target.addTarget(pick.getId(), null, game);
possibleCards.remove(pick);
}
}
return target.isChosen();
}
throw new IllegalStateException("Target wasn't handled in computer's choose method: " + target.getClass().getCanonicalName());
} //end of choose method } //end of choose method
@Override @Override
@ -578,8 +609,17 @@ public class ComputerPlayer extends PlayerImpl implements Player {
// TODO: implemented findBestPlayerTargets // TODO: implemented findBestPlayerTargets
// TODO: add findBest*Targets for all target types // TODO: add findBest*Targets for all target types
if (target.getOriginalTarget() instanceof TargetPermanent) { if (target.getOriginalTarget() instanceof TargetPermanent) {
TargetPermanent origTarget = (TargetPermanent) target.getOriginalTarget();
findBestPermanentTargets(outcome, abilityControllerId, sourceId, origTarget.getFilter(), FilterPermanent filter = null;
if (target.getOriginalTarget().getFilter() instanceof FilterPermanent) {
filter = (FilterPermanent) target.getOriginalTarget().getFilter();
}
if (filter == null) {
throw new IllegalStateException("Unsupported permanent filter in computer's chooseTarget method: "
+ target.getOriginalTarget().getClass().getCanonicalName());
}
findBestPermanentTargets(outcome, abilityControllerId, sourceId, filter,
game, target, goodList, badList, allList); game, target, goodList, badList, allList);
// use good list all the time and add maximum targets // use good list all the time and add maximum targets
@ -921,10 +961,20 @@ public class ComputerPlayer extends PlayerImpl implements Player {
} }
if (target.getOriginalTarget() instanceof TargetCardInExile) { if (target.getOriginalTarget() instanceof TargetCardInExile) {
FilterCard filter = null;
if (target.getOriginalTarget().getFilter() instanceof FilterCard) {
filter = (FilterCard) target.getOriginalTarget().getFilter();
}
if (filter == null) {
throw new IllegalStateException("Unsupported exile target filter in computer's chooseTarget method: "
+ target.getOriginalTarget().getClass().getCanonicalName());
}
List<Card> cards = new ArrayList<>(); List<Card> cards = new ArrayList<>();
for (UUID uuid : target.possibleTargets(source.getSourceId(), source.getControllerId(), game)) { for (UUID uuid : target.possibleTargets(source.getSourceId(), source.getControllerId(), game)) {
Card card = game.getCard(uuid); Card card = game.getCard(uuid);
if (card != null) { if (card != null && game.getState().getZone(card.getId()) == Zone.EXILED) {
cards.add(card); cards.add(card);
} }
} }
@ -968,7 +1018,20 @@ public class ComputerPlayer extends PlayerImpl implements Player {
} }
} }
throw new IllegalStateException("Target wasn't handled. class:" + target.getClass().toString()); if (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) {
Cards cards = new CardsImpl(possibleTargets);
List<Card> possibleCards = new ArrayList<>(cards.getCards(game));
while (!target.isChosen() && !possibleCards.isEmpty()) {
Card pick = pickTarget(abilityControllerId, possibleCards, outcome, target, source, game);
if (pick != null) {
target.addTarget(pick.getId(), source, game);
possibleCards.remove(pick);
}
}
return target.isChosen();
}
throw new IllegalStateException("Target wasn't handled in computer's chooseTarget method: " + target.getClass().getCanonicalName());
} //end of chooseTarget method } //end of chooseTarget method
protected Card pickTarget(UUID abilityControllerId, List<Card> cards, Outcome outcome, Target target, Ability source, Game game) { protected Card pickTarget(UUID abilityControllerId, List<Card> cards, Outcome outcome, Target target, Ability source, Game game) {
@ -1996,7 +2059,10 @@ public class ComputerPlayer extends PlayerImpl implements Player {
// mode was already set by the AI // mode was already set by the AI
return modes.getMode(); return modes.getMode();
} }
//TODO: improve this;
// spell modes simulated by AI, see addModeOptions
// trigger modes chooses here
// TODO: add AI support to select best modes, current code uses first valid mode
AvailableMode: AvailableMode:
for (Mode mode : modes.getAvailableModes(source, game)) { for (Mode mode : modes.getAvailableModes(source, game)) {
for (UUID selectedModeId : modes.getSelectedModes()) { for (UUID selectedModeId : modes.getSelectedModes()) {

View file

@ -30,7 +30,6 @@ import mage.target.targetpointer.FixedTarget;
import java.util.UUID; import java.util.UUID;
/** /**
*
* @author Colin Redman * @author Colin Redman
*/ */
public class AminatouTheFateshifter extends CardImpl { public class AminatouTheFateshifter extends CardImpl {
@ -53,9 +52,9 @@ public class AminatouTheFateshifter extends CardImpl {
Ability ability = new LoyaltyAbility(new AminatouPlusEffect(), +1); Ability ability = new LoyaltyAbility(new AminatouPlusEffect(), +1);
this.addAbility(ability); this.addAbility(ability);
// -1: Exile another target permanent you own, then return it to the battlefield under your control. // 1: Exile another target permanent you own, then return it to the battlefield under your control.
ability = new LoyaltyAbility(new ExileTargetForSourceEffect(), -1); ability = new LoyaltyAbility(new ExileTargetForSourceEffect(), -1);
ability.addEffect(new ReturnToBattlefieldUnderYourControlTargetEffect(true)); ability.addEffect(new ReturnToBattlefieldUnderYourControlTargetEffect(false));
ability.addTarget(new TargetPermanent(filter)); ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability); this.addAbility(ability);

View file

@ -34,7 +34,7 @@ public final class ArlinnVoiceOfThePack extends CardImpl {
this.addAbility(new SimpleStaticAbility(new ArlinnVoiceOfThePackReplacementEffect())); this.addAbility(new SimpleStaticAbility(new ArlinnVoiceOfThePackReplacementEffect()));
// -2: Create a 2/2 green Wolf creature token. // -2: Create a 2/2 green Wolf creature token.
this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new WolfToken("WAR")), -2)); this.addAbility(new LoyaltyAbility(new CreateTokenEffect(new WolfToken()), -2));
} }
private ArlinnVoiceOfThePack(final ArlinnVoiceOfThePack card) { private ArlinnVoiceOfThePack(final ArlinnVoiceOfThePack card) {

View file

@ -59,7 +59,7 @@ public final class BagOfDevouring extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{B}"); super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{B}");
// Whenever you sacrifice another nontoken artifact or creature, exile it. // Whenever you sacrifice another nontoken artifact or creature, exile it.
this.addAbility(new SacrificePermanentTriggeredAbility(new ExileTargetForSourceEffect(), filter, true)); this.addAbility(new SacrificePermanentTriggeredAbility(new ExileTargetForSourceEffect().setText("exile it"), filter, true));
// {2}, {T}, Sacrifice another artifact or creature: Draw a card. // {2}, {T}, Sacrifice another artifact or creature: Draw a card.
Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new GenericManaCost(2)); Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new GenericManaCost(2));

View file

@ -1,9 +1,5 @@
package mage.cards.c; package mage.cards.c;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility;
import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.common.EntersBattlefieldAbility;
@ -11,7 +7,6 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.effects.common.counter.RemoveCounterSourceEffect; import mage.abilities.effects.common.counter.RemoveCounterSourceEffect;
import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
@ -20,10 +15,12 @@ import mage.constants.TargetController;
import mage.constants.Zone; import mage.constants.Zone;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import java.util.*;
/** /**
*
* @author LevelX2 * @author LevelX2
*/ */
public final class CelestialConvergence extends CardImpl { public final class CelestialConvergence extends CardImpl {
@ -70,11 +67,12 @@ class CelestialConvergenceEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Card sourceObject = game.getCard(source.getSourceId()); Permanent sourcePermanent = source.getSourcePermanentOrLKI(game);
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (sourceObject != null if (sourcePermanent == null || controller == null
&& controller != null || sourcePermanent.getCounters(game).getCount(CounterType.OMEN) > 0) {
&& sourceObject.getCounters(game).getCount(CounterType.OMEN) == 0) { return false;
}
/** /**
* 801.14. If an effect states that a player wins the game, all of * 801.14. If an effect states that a player wins the game, all of
@ -87,32 +85,24 @@ class CelestialConvergenceEffect extends OneShotEffect {
* leave the game. All remaining players continue to play the game. * leave the game. All remaining players continue to play the game.
* *
*/ */
List<UUID> highestLifePlayers = new ArrayList<>(); Map<Integer, Set<UUID>> playerMap = new HashMap<>();
int highLife = Integer.MIN_VALUE; game.getState()
for (UUID playerId : game.getState().getPlayersInRange(controller.getId(), game)) { .getPlayersInRange(controller.getId(), game)
Player player = game.getPlayer(playerId); .stream()
if (player != null) { .map(game::getPlayer)
if (player.getLife() > highLife) { .filter(Objects::nonNull)
highestLifePlayers.clear(); .forEach(player -> playerMap.computeIfAbsent(
highestLifePlayers.add(player.getId()); player.getLife(), x -> new HashSet<>()
} else if (player.getLife() == highLife) { ).add(player.getId()));
highestLifePlayers.add(player.getId()); int highLife = playerMap.keySet().stream().mapToInt(x -> x).max().orElse(Integer.MIN_VALUE);
} if (playerMap.get(highLife).size() > 1) {
}
}
if (highestLifePlayers.isEmpty()) {
return false;
}
if (highestLifePlayers.size() > 1) {
game.setDraw(controller.getId()); game.setDraw(controller.getId());
} else { return true;
Player winner = game.getPlayer(highestLifePlayers.iterator().next()); }
Player winner = game.getPlayer(playerMap.get(highLife).iterator().next());
if (winner != null) { if (winner != null) {
winner.won(game); winner.won(game);
} }
}
return true; return true;
} }
return false;
}
} }

View file

@ -0,0 +1,123 @@
package mage.cards.h;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.LoseLifeSourceControllerEffect;
import mage.abilities.effects.common.SacrificeSourceEffect;
import mage.abilities.effects.common.continuous.GainAbilityAllEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.watchers.common.SpellsCastWatcher;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class HellishRebuke extends CardImpl {
public HellishRebuke(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{B}");
// Until end of turn, permanents your opponents control gain "When this permanent deals damage to the player who cast Hellish Rebuke, sacrifice this permanent. You lose 2 life."
this.getSpellAbility().addEffect(new HellishRebukeEffect());
this.getSpellAbility().addWatcher(new SpellsCastWatcher());
}
private HellishRebuke(final HellishRebuke card) {
super(card);
}
@Override
public HellishRebuke copy() {
return new HellishRebuke(this);
}
}
class HellishRebukeEffect extends OneShotEffect {
HellishRebukeEffect() {
super(Outcome.Benefit);
staticText = "until end of turn, permanents your opponents control gain " +
"\"When this permanent deals damage to the player who cast {this}, " +
"sacrifice this permanent. You lose 2 life.\"";
}
private HellishRebukeEffect(final HellishRebukeEffect effect) {
super(effect);
}
@Override
public HellishRebukeEffect copy() {
return new HellishRebukeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
MageObject mageObject = source.getSourceObject(game);
game.addEffect(new GainAbilityAllEffect(
new HellishRebukeTriggeredAbility(source, game),
Duration.EndOfTurn, StaticFilters.FILTER_OPPONENTS_PERMANENT
), source);
return true;
}
}
class HellishRebukeTriggeredAbility extends TriggeredAbilityImpl {
private final String sourceName;
private final UUID casterId;
HellishRebukeTriggeredAbility(Ability source, Game game) {
super(Zone.BATTLEFIELD, new SacrificeSourceEffect());
this.addEffect(new LoseLifeSourceControllerEffect(2));
this.sourceName = getSourceName(source, game);
this.casterId = getCasterId(source, game);
}
private HellishRebukeTriggeredAbility(final HellishRebukeTriggeredAbility ability) {
super(ability);
this.sourceName = ability.sourceName;
this.casterId = ability.casterId;
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return event.getSourceId().equals(getSourceId()) && event.getPlayerId().equals(casterId);
}
@Override
public HellishRebukeTriggeredAbility copy() {
return new HellishRebukeTriggeredAbility(this);
}
@Override
public String getRule() {
return "When this permanent deals damage to the player who cast "
+ sourceName + ", sacrifice this permanent. You lose 2 life.";
}
private static final String getSourceName(Ability source, Game game) {
MageObject object = source.getSourceObject(game);
return object != null ? object.getName() : "Hellish Rebuke";
}
private static final UUID getCasterId(Ability source, Game game) {
SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class);
return watcher.getCasterId(source, game);
}
}

View file

@ -0,0 +1,136 @@
package mage.cards.k;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.common.*;
import mage.abilities.effects.common.combat.GoadTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.targetpointer.FixedTarget;
import java.util.Set;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class KarazikarTheEyeTyrant extends CardImpl {
public KarazikarTheEyeTyrant(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}{R}");
this.addSuperType(SuperType.LEGENDARY);
this.subtype.add(SubType.BEHOLDER);
this.power = new MageInt(5);
this.toughness = new MageInt(5);
// Whenever you attack a player, tap target creature that player controls and goad it.
this.addAbility(new KarazikarTheEyeTyrantFirstTriggeredAbility());
// Whenever an opponent attacks another one of your opponents, you and the attacking player each draw a card and lose 1 life.
this.addAbility(new KarazikarTheEyeTyrantSecondTriggeredAbility());
}
private KarazikarTheEyeTyrant(final KarazikarTheEyeTyrant card) {
super(card);
}
@Override
public KarazikarTheEyeTyrant copy() {
return new KarazikarTheEyeTyrant(this);
}
}
class KarazikarTheEyeTyrantFirstTriggeredAbility extends TriggeredAbilityImpl {
KarazikarTheEyeTyrantFirstTriggeredAbility() {
super(Zone.BATTLEFIELD, new TapTargetEffect(), false);
this.addEffect(new GoadTargetEffect());
}
private KarazikarTheEyeTyrantFirstTriggeredAbility(final KarazikarTheEyeTyrantFirstTriggeredAbility ability) {
super(ability);
}
@Override
public KarazikarTheEyeTyrantFirstTriggeredAbility copy() {
return new KarazikarTheEyeTyrantFirstTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!isControlledBy(event.getPlayerId())) {
return false;
}
Player player = game.getPlayer(event.getTargetId());
if (player == null) {
return false;
}
FilterPermanent filter = new FilterCreaturePermanent("creature controlled by " + player.getName());
filter.add(new ControllerIdPredicate(player.getId()));
this.getTargets().clear();
this.addTarget(new TargetPermanent(filter));
return true;
}
@Override
public String getRule() {
return "Whenever you attack a player, tap target creature that player controls and goad it.";
}
}
class KarazikarTheEyeTyrantSecondTriggeredAbility extends TriggeredAbilityImpl {
KarazikarTheEyeTyrantSecondTriggeredAbility() {
super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false);
this.addEffect(new LoseLifeSourceControllerEffect(1));
this.addEffect(new DrawCardTargetEffect(1));
this.addEffect(new LoseLifeTargetEffect(1));
}
private KarazikarTheEyeTyrantSecondTriggeredAbility(final KarazikarTheEyeTyrantSecondTriggeredAbility ability) {
super(ability);
}
@Override
public KarazikarTheEyeTyrantSecondTriggeredAbility copy() {
return new KarazikarTheEyeTyrantSecondTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Set<UUID> opponents = game.getOpponents(getControllerId());
if (!opponents.contains(event.getPlayerId()) || !opponents.contains(event.getTargetId())) {
return false;
}
this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId()));
return true;
}
@Override
public String getRule() {
return "Whenever an opponent attacks another one of your opponents, " +
"you and the attacking player each draw a card and lose 1 life.";
}
}

View file

@ -0,0 +1,78 @@
package mage.cards.k;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.condition.common.ControlACommanderCondition;
import mage.abilities.dynamicvalue.common.ManacostVariableValue;
import mage.abilities.effects.common.DamageAllEffect;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.game.Game;
import mage.target.TargetPermanent;
import mage.target.targetadjustment.TargetAdjuster;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class KlauthsWill extends CardImpl {
private static final FilterPermanent filter = new FilterCreaturePermanent("creature without flying");
static {
filter.add(Predicates.not(new AbilityPredicate(FlyingAbility.class)));
}
public KlauthsWill(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{X}{R}{R}{G}");
// Choose one. If you control a commander as you cast this spell, you may choose both.
this.getSpellAbility().getModes().setChooseText(
"Choose one. If you control a commander as you cast this spell, you may choose both."
);
this.getSpellAbility().getModes().setMoreCondition(ControlACommanderCondition.instance);
// Breathe Flame Klauth's Will deals X damage to each creature without flying.
this.getSpellAbility().addEffect(new DamageAllEffect(ManacostVariableValue.REGULAR, filter));
this.getSpellAbility().withFirstModeFlavorWord("Breathe Flame");
// Smash Relics Destroy up to X target artifacts and/or enchantments.
this.getSpellAbility().addMode(new Mode(
new DestroyTargetEffect().setText("destroy up to X target artifacts and/or enchantments")
).withFlavorWord("Smash Relics"));
this.getSpellAbility().setTargetAdjuster(KlauthsWillAdjuster.instance);
}
private KlauthsWill(final KlauthsWill card) {
super(card);
}
@Override
public KlauthsWill copy() {
return new KlauthsWill(this);
}
}
enum KlauthsWillAdjuster implements TargetAdjuster {
instance;
@Override
public void adjustTargets(Ability ability, Game game) {
if (ability.getEffects().stream().anyMatch(DestroyTargetEffect.class::isInstance)) {
ability.getTargets().clear();
ability.addTarget(new TargetPermanent(
0, ability.getManaCostsToPay().getX(),
StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT
));
}
}
}

View file

@ -132,7 +132,7 @@ class MaddeningHexEffect extends OneShotEffect {
opponents.remove(player.getId()); opponents.remove(player.getId());
} }
if (!opponents.isEmpty()) { if (!opponents.isEmpty()) {
permanent.attachTo(RandomUtil.randomFromSet(opponents), source, game); permanent.attachTo(RandomUtil.randomFromCollection(opponents), source, game);
} }
return true; return true;
} }

View file

@ -11,10 +11,7 @@ import mage.abilities.effects.mana.BasicManaEffect;
import mage.abilities.mana.ActivatedManaAbilityImpl; import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.*;
import mage.constants.Outcome;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.filter.FilterPermanent; import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledLandPermanent; import mage.filter.common.FilterControlledLandPermanent;
@ -94,6 +91,7 @@ class ManaCacheManaAbility extends ActivatedManaAbilityImpl {
super(Zone.BATTLEFIELD, new BasicManaEffect(Mana.ColorlessMana(1), new CountersSourceCount(CounterType.CHARGE)), super(Zone.BATTLEFIELD, new BasicManaEffect(Mana.ColorlessMana(1), new CountersSourceCount(CounterType.CHARGE)),
new RemoveCountersSourceCost(CounterType.CHARGE.createInstance(1))); new RemoveCountersSourceCost(CounterType.CHARGE.createInstance(1)));
this.netMana.add(new Mana(0, 0, 0, 0, 0, 0, 0, 1)); this.netMana.add(new Mana(0, 0, 0, 0, 0, 0, 0, 1));
this.setMayActivate(TargetController.ANY);
} }
public ManaCacheManaAbility(final ManaCacheManaAbility ability) { public ManaCacheManaAbility(final ManaCacheManaAbility ability) {
@ -102,19 +100,17 @@ class ManaCacheManaAbility extends ActivatedManaAbilityImpl {
@Override @Override
public ActivationStatus canActivate(UUID playerId, Game game) { public ActivationStatus canActivate(UUID playerId, Game game) {
if (!super.hasMoreActivationsThisTurn(game) || !(condition == null || condition.apply(game, this))) { // any player, but only during their turn before the end step
return ActivationStatus.getFalse();
}
Player player = game.getPlayer(playerId); Player player = game.getPlayer(playerId);
if (player != null && playerId.equals(game.getActivePlayerId()) && game.getStep().getType().isBefore(PhaseStep.END_TURN)) { if (player == null
if (costs.canPay(this, this, playerId, game)) { || !playerId.equals(game.getActivePlayerId())
this.setControllerId(playerId); || !game.getStep().getType().isBefore(PhaseStep.END_TURN)) {
return ActivationStatus.getTrue(this, game);
}
}
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();
} }
return super.canActivate(playerId, game);
}
@Override @Override
public ManaCacheManaAbility copy() { public ManaCacheManaAbility copy() {
return new ManaCacheManaAbility(this); return new ManaCacheManaAbility(this);

View file

@ -0,0 +1,165 @@
package mage.cards.n;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.AttacksAllTriggeredAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.delayed.ReflexiveTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.GainLifeEffect;
import mage.abilities.effects.common.continuous.GainControlTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.permanent.TappedPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class Nihiloor extends CardImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent();
static {
filter.add(TargetController.OPPONENT.getOwnerPredicate());
}
public Nihiloor(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{U}{B}");
this.addSuperType(SuperType.LEGENDARY);
this.subtype.add(SubType.HORROR);
this.power = new MageInt(3);
this.toughness = new MageInt(5);
// When Nihiloor enters the battlefield, for each opponent, tap up to one untapped creature you control. When you do, gain control of target creature that player controls with power less than or equal to the tapped creature's power for as long as you control Nihiloor.
this.addAbility(new EntersBattlefieldTriggeredAbility(new NihiloorControlEffect()));
// Whenever you attack with a creature an opponent owns, you gain 2 life and that player loses 2 life.
Ability ability = new AttacksAllTriggeredAbility(
new GainLifeEffect(1), false, filter,
SetTargetPointer.PERMANENT, false
).setTriggerPhrase("Whenever you attack with a creature an opponent owns, ");
ability.addEffect(new NihiloorLoseLifeEffect());
this.addAbility(ability);
}
private Nihiloor(final Nihiloor card) {
super(card);
}
@Override
public Nihiloor copy() {
return new Nihiloor(this);
}
}
class NihiloorControlEffect extends OneShotEffect {
private static final FilterPermanent filter
= new FilterControlledCreaturePermanent("untapped creatured you control");
static {
filter.add(TappedPredicate.UNTAPPED);
}
private static final class NihiloorPredicate implements Predicate<Permanent> {
private final Permanent permanent;
private final UUID playerId;
private NihiloorPredicate(Permanent permanent, UUID playerId) {
this.permanent = permanent;
this.playerId = playerId;
}
@Override
public boolean apply(Permanent input, Game game) {
return input.isControlledBy(playerId)
&& input.getPower().getValue() <= permanent.getPower().getValue();
}
}
NihiloorControlEffect() {
super(Outcome.Benefit);
staticText = "for each opponent, tap up to one untapped creature you control. When you do, " +
"gain control of target creature that player controls with power less than " +
"or equal to the tapped creature's power for as long as you control {this}";
}
private NihiloorControlEffect(final NihiloorControlEffect effect) {
super(effect);
}
@Override
public NihiloorControlEffect copy() {
return new NihiloorControlEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
for (UUID playerId : game.getOpponents(source.getControllerId())) {
Player opponent = game.getPlayer(playerId);
if (opponent == null) {
continue;
}
TargetPermanent target = new TargetPermanent(0, 1, filter, true);
target.withChooseHint("tapping a creature controlled by " + opponent.getName());
controller.choose(outcome, target, source.getControllerId(), game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent == null || !permanent.tap(source, game)) {
continue;
}
FilterPermanent filter2 = new FilterPermanent(
"creature controlled by " + opponent.getName()
+ " with power " + permanent.getPower().getValue() + " or less"
);
filter2.add(new NihiloorPredicate(permanent, playerId));
ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(
new GainControlTargetEffect(Duration.Custom, true),
false, "gain control of target creature that player controls with " +
"power less than or equal to the tapped creature's power for as long as you control {this}"
);
ability.addTarget(new TargetPermanent(filter2));
game.fireReflexiveTriggeredAbility(ability, source);
}
return true;
}
}
class NihiloorLoseLifeEffect extends OneShotEffect {
NihiloorLoseLifeEffect() {
super(Outcome.Benefit);
staticText = "you gain 2 life and that player loses 2 life";
}
private NihiloorLoseLifeEffect(final NihiloorLoseLifeEffect effect) {
super(effect);
}
@Override
public NihiloorLoseLifeEffect copy() {
return new NihiloorLoseLifeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(game.getControllerId(getTargetPointer().getFirst(game, source)));
return player != null && player.loseLife(2, game, source, false) > 0;
}
}

View file

@ -187,7 +187,10 @@ class TheBookOfVileDarknessEffect extends OneShotEffect {
} }
for (Ability ability : card.getAbilities(game)) { for (Ability ability : card.getAbilities(game)) {
if (ability instanceof TriggeredAbility) { if (ability instanceof TriggeredAbility) {
token.addAbility(ability.copy()); Ability copyAbility = ability.copy();
copyAbility.newId();
copyAbility.setControllerId(source.getControllerId());
token.addAbility(copyAbility);
} }
} }
} }

View file

@ -26,7 +26,7 @@ public final class WolfSkullShaman extends CardImpl {
this.toughness = new MageInt(2); this.toughness = new MageInt(2);
// Kinship - At the beginning of your upkeep, you may look at the top card of your library. If it shares a creature type with Wolf-Skull Shaman, you may reveal it. If you do, create a 2/2 green Wolf creature token. // Kinship - At the beginning of your upkeep, you may look at the top card of your library. If it shares a creature type with Wolf-Skull Shaman, you may reveal it. If you do, create a 2/2 green Wolf creature token.
this.addAbility(new KinshipAbility(new CreateTokenEffect(new WolfToken("LRW")))); this.addAbility(new KinshipAbility(new CreateTokenEffect(new WolfToken())));
} }
private WolfSkullShaman(final WolfSkullShaman card) { private WolfSkullShaman(final WolfSkullShaman card) {

View file

@ -72,7 +72,7 @@ class WolfcallersHowlEffect extends OneShotEffect {
} }
} }
if (count > 0) { if (count > 0) {
return new CreateTokenEffect(new WolfToken("C14"), count).apply(game, source); return new CreateTokenEffect(new WolfToken(), count).apply(game, source);
} }
return true; return true;
} }

View file

@ -41,7 +41,7 @@ public final class WrensRunPackmaster extends CardImpl {
this.addAbility(new ChampionAbility(this, SubType.ELF, false)); this.addAbility(new ChampionAbility(this, SubType.ELF, false));
// {2}{G}: Create a 2/2 green Wolf creature token. // {2}{G}: Create a 2/2 green Wolf creature token.
this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new CreateTokenEffect(new WolfToken("LRW")), new ManaCostsImpl<>("{2}{G}"))); this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new CreateTokenEffect(new WolfToken()), new ManaCostsImpl<>("{2}{G}")));
// Each Wolf you control has deathtouch. // Each Wolf you control has deathtouch.
Effect effect = new GainAbilityAllEffect(DeathtouchAbility.getInstance(), Duration.WhileOnBattlefield, filter); Effect effect = new GainAbilityAllEffect(DeathtouchAbility.getInstance(), Duration.WhileOnBattlefield, filter);

View file

@ -125,6 +125,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet {
cards.add(new SetCardInfo("Halimar Depths", 244, Rarity.COMMON, mage.cards.h.HalimarDepths.class)); cards.add(new SetCardInfo("Halimar Depths", 244, Rarity.COMMON, mage.cards.h.HalimarDepths.class));
cards.add(new SetCardInfo("Haven of the Spirit Dragon", 245, Rarity.RARE, mage.cards.h.HavenOfTheSpiritDragon.class)); cards.add(new SetCardInfo("Haven of the Spirit Dragon", 245, Rarity.RARE, mage.cards.h.HavenOfTheSpiritDragon.class));
cards.add(new SetCardInfo("Heirloom Blade", 208, Rarity.UNCOMMON, mage.cards.h.HeirloomBlade.class)); cards.add(new SetCardInfo("Heirloom Blade", 208, Rarity.UNCOMMON, mage.cards.h.HeirloomBlade.class));
cards.add(new SetCardInfo("Hellish Rebuke", 26, Rarity.RARE, mage.cards.h.HellishRebuke.class));
cards.add(new SetCardInfo("Heroic Intervention", 161, Rarity.RARE, mage.cards.h.HeroicIntervention.class)); cards.add(new SetCardInfo("Heroic Intervention", 161, Rarity.RARE, mage.cards.h.HeroicIntervention.class));
cards.add(new SetCardInfo("Hex", 101, Rarity.RARE, mage.cards.h.Hex.class)); cards.add(new SetCardInfo("Hex", 101, Rarity.RARE, mage.cards.h.Hex.class));
cards.add(new SetCardInfo("High Market", 246, Rarity.RARE, mage.cards.h.HighMarket.class)); cards.add(new SetCardInfo("High Market", 246, Rarity.RARE, mage.cards.h.HighMarket.class));
@ -136,9 +137,11 @@ public final class ForgottenRealmsCommander extends ExpansionSet {
cards.add(new SetCardInfo("Imprisoned in the Moon", 85, Rarity.RARE, mage.cards.i.ImprisonedInTheMoon.class)); cards.add(new SetCardInfo("Imprisoned in the Moon", 85, Rarity.RARE, mage.cards.i.ImprisonedInTheMoon.class));
cards.add(new SetCardInfo("Indomitable Might", 40, Rarity.RARE, mage.cards.i.IndomitableMight.class)); cards.add(new SetCardInfo("Indomitable Might", 40, Rarity.RARE, mage.cards.i.IndomitableMight.class));
cards.add(new SetCardInfo("Izzet Chemister", 130, Rarity.RARE, mage.cards.i.IzzetChemister.class)); cards.add(new SetCardInfo("Izzet Chemister", 130, Rarity.RARE, mage.cards.i.IzzetChemister.class));
cards.add(new SetCardInfo("Karazikar, the Eye Tyrant", 49, Rarity.MYTHIC, mage.cards.k.KarazikarTheEyeTyrant.class));
cards.add(new SetCardInfo("Karmic Guide", 68, Rarity.RARE, mage.cards.k.KarmicGuide.class)); cards.add(new SetCardInfo("Karmic Guide", 68, Rarity.RARE, mage.cards.k.KarmicGuide.class));
cards.add(new SetCardInfo("Kenrith's Transformation", 162, Rarity.UNCOMMON, mage.cards.k.KenrithsTransformation.class)); cards.add(new SetCardInfo("Kenrith's Transformation", 162, Rarity.UNCOMMON, mage.cards.k.KenrithsTransformation.class));
cards.add(new SetCardInfo("Kindred Summons", 163, Rarity.RARE, mage.cards.k.KindredSummons.class)); cards.add(new SetCardInfo("Kindred Summons", 163, Rarity.RARE, mage.cards.k.KindredSummons.class));
cards.add(new SetCardInfo("Klauth's Will", 51, Rarity.RARE, mage.cards.k.KlauthsWill.class));
cards.add(new SetCardInfo("Klauth, Unrivaled Ancient", 50, Rarity.MYTHIC, mage.cards.k.KlauthUnrivaledAncient.class)); cards.add(new SetCardInfo("Klauth, Unrivaled Ancient", 50, Rarity.MYTHIC, mage.cards.k.KlauthUnrivaledAncient.class));
cards.add(new SetCardInfo("Knight of Autumn", 187, Rarity.RARE, mage.cards.k.KnightOfAutumn.class)); cards.add(new SetCardInfo("Knight of Autumn", 187, Rarity.RARE, mage.cards.k.KnightOfAutumn.class));
cards.add(new SetCardInfo("Light Up the Stage", 131, Rarity.UNCOMMON, mage.cards.l.LightUpTheStage.class)); cards.add(new SetCardInfo("Light Up the Stage", 131, Rarity.UNCOMMON, mage.cards.l.LightUpTheStage.class));
@ -165,6 +168,7 @@ public final class ForgottenRealmsCommander extends ExpansionSet {
cards.add(new SetCardInfo("Nature's Lore", 164, Rarity.COMMON, mage.cards.n.NaturesLore.class)); cards.add(new SetCardInfo("Nature's Lore", 164, Rarity.COMMON, mage.cards.n.NaturesLore.class));
cards.add(new SetCardInfo("Necromantic Selection", 103, Rarity.RARE, mage.cards.n.NecromanticSelection.class)); cards.add(new SetCardInfo("Necromantic Selection", 103, Rarity.RARE, mage.cards.n.NecromanticSelection.class));
cards.add(new SetCardInfo("Necrotic Sliver", 188, Rarity.UNCOMMON, mage.cards.n.NecroticSliver.class)); cards.add(new SetCardInfo("Necrotic Sliver", 188, Rarity.UNCOMMON, mage.cards.n.NecroticSliver.class));
cards.add(new SetCardInfo("Nihiloor", 53, Rarity.MYTHIC, mage.cards.n.Nihiloor.class));
cards.add(new SetCardInfo("Nimbus Maze", 252, Rarity.RARE, mage.cards.n.NimbusMaze.class)); cards.add(new SetCardInfo("Nimbus Maze", 252, Rarity.RARE, mage.cards.n.NimbusMaze.class));
cards.add(new SetCardInfo("Obsessive Stitcher", 189, Rarity.UNCOMMON, mage.cards.o.ObsessiveStitcher.class)); cards.add(new SetCardInfo("Obsessive Stitcher", 189, Rarity.UNCOMMON, mage.cards.o.ObsessiveStitcher.class));
cards.add(new SetCardInfo("Ogre Slumlord", 104, Rarity.RARE, mage.cards.o.OgreSlumlord.class)); cards.add(new SetCardInfo("Ogre Slumlord", 104, Rarity.RARE, mage.cards.o.OgreSlumlord.class));

View file

@ -0,0 +1,121 @@
package org.mage.test.cards.single.tsr;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
/**
* @author JayDi85
*/
public class ShivanSandMageTest extends CardTestPlayerBaseWithAIHelps {
@Test
public void test_ModeOne_Manual() {
// When Shivan Sand-Mage enters the battlefield, choose one
// Remove two time counters from target permanent or suspended card.
// Put two time counters on target permanent with a time counter on it or suspended card.
addCard(Zone.HAND, playerA, "Shivan Sand-Mage", 1); // {2}{R}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shivan Sand-Mage");
setModeChoice(playerA, "1");
addTarget(playerA, "Grizzly Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Shivan Sand-Mage", 1);
}
@Test
public void test_ModeOne_AI() {
// When Shivan Sand-Mage enters the battlefield, choose one
// Remove two time counters from target permanent or suspended card.
// Put two time counters on target permanent with a time counter on it or suspended card.
addCard(Zone.HAND, playerA, "Shivan Sand-Mage", 1); // {2}{R}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
// AI must play card
aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Shivan Sand-Mage", 1);
}
@Test
public void test_ModeTwo_Manual() {
// When Shivan Sand-Mage enters the battlefield, choose one
// Remove two time counters from target permanent or suspended card.
// Put two time counters on target permanent with a time counter on it or suspended card.
addCard(Zone.HAND, playerA, "Shivan Sand-Mage", 1); // {2}{R}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
//
// Suspend 3{1}{W}
addCard(Zone.HAND, playerA, "Duskrider Peregrine", 1); // {5}{W}
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
// prepare suspended card
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}", 2);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Suspend 3");
checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", 1);
checkCardCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", CounterType.TIME, 3);
// add +2 time counters
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shivan Sand-Mage");
setModeChoice(playerA, "2");
addTarget(playerA, "Duskrider Peregrine");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkCardCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", CounterType.TIME, 3 + 2);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Shivan Sand-Mage", 1);
}
@Test
public void test_ModeTwo_AI() {
// When Shivan Sand-Mage enters the battlefield, choose one
// Remove two time counters from target permanent or suspended card.
// Put two time counters on target permanent with a time counter on it or suspended card.
addCard(Zone.HAND, playerA, "Shivan Sand-Mage", 1); // {2}{R}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
//
// Suspend 3{1}{W}
addCard(Zone.HAND, playerA, "Duskrider Peregrine", 1); // {5}{W}
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
// prepare suspended card
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}", 2);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Suspend 3");
checkExileCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", 1);
checkCardCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", CounterType.TIME, 3);
// must add +2 time counters
// due to AI limitation with ETB's modes - it checks only target support
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shivan Sand-Mage");
setModeChoice(playerA, "2");
aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkCardCounters("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Duskrider Peregrine", CounterType.TIME, 3 + 2);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Shivan Sand-Mage", 1);
}
}

View file

@ -342,7 +342,7 @@ public class TestPlayer implements Player {
for (Player player : game.getPlayers().values()) { for (Player player : game.getPlayers().values()) {
if (player.getName().equals(target)) { if (player.getName().equals(target)) {
if (ability.getTargets().isEmpty()) { if (ability.getTargets().isEmpty()) {
throw new UnsupportedOperationException("Ability has no targets, but there is a player target set - " + ability.toString()); throw new UnsupportedOperationException("Ability has no targets, but there is a player target set - " + ability);
} }
if (ability.getTargets().get(0) instanceof TargetAmount) { if (ability.getTargets().get(0) instanceof TargetAmount) {
return true; // targetAmount have to be set by setTargetAmount in the test script return true; // targetAmount have to be set by setTargetAmount in the test script
@ -461,10 +461,10 @@ public class TestPlayer implements Player {
selectedMode = ability.getModes().getMode(); selectedMode = ability.getModes().getMode();
} }
if (selectedMode == null) { if (selectedMode == null) {
throw new UnsupportedOperationException("Mode not available for " + ability.toString()); throw new UnsupportedOperationException("Mode not available for " + ability);
} }
if (selectedMode.getTargets().isEmpty()) { if (selectedMode.getTargets().isEmpty()) {
throw new AssertionError("Ability has no targets. " + ability.toString()); throw new AssertionError("Ability has no targets. " + ability);
} }
if (index >= selectedMode.getTargets().size()) { if (index >= selectedMode.getTargets().size()) {
break; // this can happen if targets should be set but can't be used because of hexproof e.g. break; // this can happen if targets should be set but can't be used because of hexproof e.g.
@ -821,6 +821,13 @@ public class TestPlayer implements Player {
wasProccessed = true; wasProccessed = true;
} }
// check card counters: card name, counter type, count
if (params[0].equals(CHECK_COMMAND_CARD_COUNTERS) && params.length == 4) {
assertCardCounters(action, game, computerPlayer, params[1], CounterType.findByName(params[2]), Integer.parseInt(params[3]));
actions.remove(action);
wasProccessed = true;
}
// check exile count: card name, count // check exile count: card name, count
if (params[0].equals(CHECK_COMMAND_EXILE_COUNT) && params.length == 3) { if (params[0].equals(CHECK_COMMAND_EXILE_COUNT) && params.length == 3) {
assertExileCount(action, game, params[1], Integer.parseInt(params[2])); assertExileCount(action, game, params[1], Integer.parseInt(params[2]));
@ -1365,6 +1372,25 @@ public class TestPlayer implements Player {
Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must have " + count + " " + counterType.toString(), count, foundCount); Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must have " + count + " " + counterType.toString(), count, foundCount);
} }
private void assertCardCounters(PlayerAction action, Game game, Player player, String cardName, CounterType counterType, int count) {
int foundCount = 0;
Set<Card> allCards = new HashSet<>();
// collect possible cards from visible zones except library
allCards.addAll(player.getHand().getCards(game));
allCards.addAll(player.getGraveyard().getCards(game));
allCards.addAll(game.getExile().getAllCards(game));
for (Card card : allCards) {
if (hasObjectTargetNameOrAlias(card, cardName) && card.getOwnerId().equals(player.getId())) {
foundCount = card.getCounters(game).getCount(counterType);
}
}
Assert.assertEquals(action.getActionName() + " - card " + cardName + " must have " + count + " " + counterType.toString(), count, foundCount);
}
private void assertExileCount(PlayerAction action, Game game, String permanentName, int count) { private void assertExileCount(PlayerAction action, Game game, String permanentName, int count) {
int foundCount = 0; int foundCount = 0;
for (Card card : game.getExile().getAllCards(game)) { for (Card card : game.getExile().getAllCards(game)) {
@ -1465,9 +1491,9 @@ public class TestPlayer implements Player {
} }
if (mustHave) { if (mustHave) {
Assert.assertEquals(action.getActionName() + " - must contain colors [" + searchColors.toString() + "] but found only [" + cardColor.toString() + "]", 0, colorsDontHave.size()); Assert.assertEquals(action.getActionName() + " - must contain colors [" + searchColors + "] but found only [" + cardColor.toString() + "]", 0, colorsDontHave.size());
} else { } else {
Assert.assertEquals(action.getActionName() + " - must not contain colors [" + searchColors.toString() + "] but found [" + cardColor.toString() + "]", 0, colorsHave.size()); Assert.assertEquals(action.getActionName() + " - must not contain colors [" + searchColors + "] but found [" + cardColor.toString() + "]", 0, colorsHave.size());
} }
} }
@ -1558,7 +1584,7 @@ public class TestPlayer implements Player {
Integer normal = player.getManaPool().getMana().get(manaType); Integer normal = player.getManaPool().getMana().get(manaType);
Integer conditional = player.getManaPool().getConditionalMana().stream().mapToInt(a -> a.get(manaType)).sum(); // calcs FULL conditional mana, not real conditions Integer conditional = player.getManaPool().getConditionalMana().stream().mapToInt(a -> a.get(manaType)).sum(); // calcs FULL conditional mana, not real conditions
Integer current = normal + conditional; Integer current = normal + conditional;
Assert.assertEquals(action.getActionName() + " - mana pool must contain [" + amount.toString() + " " + manaType.toString() + "], but found [" + current.toString() + "]", amount, current); Assert.assertEquals(action.getActionName() + " - mana pool must contain [" + amount.toString() + " " + manaType + "], but found [" + current + "]", amount, current);
} }
private void assertManaPool(PlayerAction action, Game game, Player player, String colors, Integer amount) { private void assertManaPool(PlayerAction action, Game game, Player player, String colors, Integer amount) {
@ -1952,7 +1978,18 @@ public class TestPlayer implements Player {
return null; return null;
} }
this.chooseStrictModeFailed("mode", game, getInfo(source, game)); StringBuilder modesInfo = new StringBuilder();
modesInfo.append("\nAvailable modes:");
int i = 1;
for (Mode mode : modes.getAvailableModes(source, game)) {
if (modesInfo.length() > 0) {
modesInfo.append("\n");
}
modesInfo.append(String.format("%d: %s", i, mode.getEffects().getText(mode)));
i++;
}
this.chooseStrictModeFailed("mode", game, getInfo(source, game) + modesInfo);
return computerPlayer.chooseMode(modes, source, game); return computerPlayer.chooseMode(modes, source, game);
} }
@ -2298,7 +2335,8 @@ public class TestPlayer implements Player {
|| (target.getOriginalTarget() instanceof TargetPermanentOrPlayer) || (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)
|| (target.getOriginalTarget() instanceof TargetAnyTarget) || (target.getOriginalTarget() instanceof TargetAnyTarget)
|| (target.getOriginalTarget() instanceof TargetCreatureOrPlayer) || (target.getOriginalTarget() instanceof TargetCreatureOrPlayer)
|| (target.getOriginalTarget() instanceof TargetDefender)) { || (target.getOriginalTarget() instanceof TargetDefender)
|| (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard)) {
for (String targetDefinition : targets) { for (String targetDefinition : targets) {
if (targetDefinition.startsWith("targetPlayer=")) { if (targetDefinition.startsWith("targetPlayer=")) {
continue; continue;
@ -2330,6 +2368,9 @@ public class TestPlayer implements Player {
if (filter instanceof FilterPlaneswalkerOrPlayer) { if (filter instanceof FilterPlaneswalkerOrPlayer) {
filter = ((FilterPlaneswalkerOrPlayer) filter).getFilterPermanent(); filter = ((FilterPlaneswalkerOrPlayer) filter).getFilterPermanent();
} }
if (filter instanceof FilterPermanentOrSuspendedCard) {
filter = ((FilterPermanentOrSuspendedCard) filter).getPermanentFilter();
}
for (Permanent permanent : game.getBattlefield().getActivePermanents((FilterPermanent) filter, abilityControllerId, sourceId, game)) { for (Permanent permanent : game.getBattlefield().getActivePermanents((FilterPermanent) filter, abilityControllerId, sourceId, game)) {
if (hasObjectTargetNameOrAlias(permanent, targetName) || (permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove exp code search? if (hasObjectTargetNameOrAlias(permanent, targetName) || (permanent.getName() + '-' + permanent.getExpansionSetCode()).equals(targetName)) { // TODO: remove exp code search?
if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) { if (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) {
@ -2375,14 +2416,26 @@ public class TestPlayer implements Player {
} }
// card in exile // card in exile
if (target.getOriginalTarget() instanceof TargetCardInExile) { if (target.getOriginalTarget() instanceof TargetCardInExile
TargetCardInExile targetFull = (TargetCardInExile) target.getOriginalTarget(); || target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard) {
FilterCard filter = null;
if (target.getOriginalTarget().getFilter() instanceof FilterCard) {
filter = (FilterCard) target.getOriginalTarget().getFilter();
} else if (target.getOriginalTarget().getFilter() instanceof FilterPermanentOrSuspendedCard) {
filter = ((FilterPermanentOrSuspendedCard) target.getOriginalTarget().getFilter()).getCardFilter();
}
if (filter == null) {
Assert.fail("Unsupported exile target filter in TestPlayer: "
+ target.getOriginalTarget().getClass().getCanonicalName());
}
for (String targetDefinition : targets) { for (String targetDefinition : targets) {
checkTargetDefinitionMarksSupport(target, targetDefinition, "^"); checkTargetDefinitionMarksSupport(target, targetDefinition, "^");
String[] targetList = targetDefinition.split("\\^"); String[] targetList = targetDefinition.split("\\^");
boolean targetFound = false; boolean targetFound = false;
for (String targetName : targetList) { for (String targetName : targetList) {
for (Card card : game.getExile().getCards(targetFull.getFilter(), game)) { for (Card card : game.getExile().getCards(filter, game)) {
if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search? if (hasObjectTargetNameOrAlias(card, targetName) || (card.getName() + '-' + card.getExpansionSetCode()).equals(targetName)) { // TODO: remove set code search?
if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) { if (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) {
target.addTarget(card.getId(), source, game); target.addTarget(card.getId(), source, game);
@ -2508,16 +2561,17 @@ public class TestPlayer implements Player {
String message; String message;
if (source != null) { if (source != null) {
message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used in [" message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used"
+ "card " + source.getSourceObject(game) + "\nCard: " + source.getSourceObject(game)
+ " -> ability " + source.getClass().getSimpleName() + " (" + source.getRule().substring(0, Math.min(20, source.getRule().length()) - 1) + "..." + ")" + "\nAbility: " + source.getClass().getSimpleName() + " (" + source.getRule() + ")"
+ " -> target " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")" + "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")"
+ "]"; + "\nYou must implement target class support in TestPlayer or setup good targets";
} else { } else {
message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used in [" message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used"
+ "card XXX" + "\nCard: unknown source"
+ " -> target " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")" + "\nAbility: unknown source"
+ "]"; + "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")"
+ "\nYou must implement target class support in TestPlayer or setup good targets";
} }
Assert.fail(message); Assert.fail(message);
} }

View file

@ -97,6 +97,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
public static final String CHECK_COMMAND_PERMANENT_COUNT = "PERMANENT_COUNT"; public static final String CHECK_COMMAND_PERMANENT_COUNT = "PERMANENT_COUNT";
public static final String CHECK_COMMAND_PERMANENT_TAPPED = "PERMANENT_TAPPED"; public static final String CHECK_COMMAND_PERMANENT_TAPPED = "PERMANENT_TAPPED";
public static final String CHECK_COMMAND_PERMANENT_COUNTERS = "PERMANENT_COUNTERS"; public static final String CHECK_COMMAND_PERMANENT_COUNTERS = "PERMANENT_COUNTERS";
public static final String CHECK_COMMAND_CARD_COUNTERS = "CARD_COUNTERS";
public static final String CHECK_COMMAND_EXILE_COUNT = "EXILE_COUNT"; public static final String CHECK_COMMAND_EXILE_COUNT = "EXILE_COUNT";
public static final String CHECK_COMMAND_GRAVEYARD_COUNT = "GRAVEYARD_COUNT"; public static final String CHECK_COMMAND_GRAVEYARD_COUNT = "GRAVEYARD_COUNT";
public static final String CHECK_COMMAND_LIBRARY_COUNT = "LIBRARY_COUNT"; public static final String CHECK_COMMAND_LIBRARY_COUNT = "LIBRARY_COUNT";
@ -457,6 +458,10 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNTERS, permanentName, counterType.toString(), count.toString()); check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNTERS, permanentName, counterType.toString(), count.toString());
} }
public void checkCardCounters(String checkName, int turnNum, PhaseStep step, TestPlayer player, String cardName, CounterType counterType, Integer count) {
check(checkName, turnNum, step, player, CHECK_COMMAND_CARD_COUNTERS, cardName, counterType.toString(), count.toString());
}
public void checkExileCount(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer count) { public void checkExileCount(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer count) {
//Assert.assertNotEquals("", permanentName); //Assert.assertNotEquals("", permanentName);
check(checkName, turnNum, step, player, CHECK_COMMAND_EXILE_COUNT, permanentName, count.toString()); check(checkName, turnNum, step, player, CHECK_COMMAND_EXILE_COUNT, permanentName, count.toString());

View file

@ -19,6 +19,7 @@ import mage.constants.CardType;
import mage.constants.Rarity; import mage.constants.Rarity;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.game.command.Dungeon;
import mage.game.command.Plane; import mage.game.command.Plane;
import mage.game.draft.DraftCube; import mage.game.draft.DraftCube;
import mage.game.draft.RateCard; import mage.game.draft.RateCard;
@ -1188,6 +1189,47 @@ public class VerifyCardDataTest {
} }
} }
@Test
public void test_checkMissingDungeonsData() {
Collection<String> errorsList = new ArrayList<>();
Reflections reflections = new Reflections("mage.");
Set<Class<? extends Dungeon>> dungeonClassesList = reflections.getSubTypesOf(Dungeon.class);
// 1. correct class name
for (Class<? extends Dungeon> dungeonClass : dungeonClassesList) {
if (!dungeonClass.getName().endsWith("Dungeon")) {
String className = extractShortClass(dungeonClass);
errorsList.add("Error: dungeon class must ends with Dungeon: " + className + " from " + dungeonClass.getName());
}
}
// 2. correct package
for (Class<? extends Dungeon> dungeonClass : dungeonClassesList) {
String fullClass = dungeonClass.getName();
if (!fullClass.startsWith("mage.game.command.dungeons.")) {
String className = extractShortClass(dungeonClass);
errorsList.add("Error: dungeon must be stored in mage.game.command.dungeons package: " + className + " from " + dungeonClass.getName());
}
}
// 3. correct constructor
for (Class<? extends Dungeon> dungeonClass : dungeonClassesList) {
String className = extractShortClass(dungeonClass);
Dungeon dungeon;
try {
dungeon = (Dungeon) createNewObject(dungeonClass);
} catch (Throwable e) {
errorsList.add("Error: can't create dungeon with default constructor: " + className + " from " + dungeonClass.getName());
}
}
printMessages(errorsList);
if (errorsList.size() > 0) {
Assert.fail("Found dungeon errors: " + errorsList.size());
}
}
private void check(Card card, int cardIndex, boolean skipWarning) { private void check(Card card, int cardIndex, boolean skipWarning) {
MtgJsonCard ref = MtgJsonService.cardFromSet(card.getExpansionSetCode(), card.getName(), card.getCardNumber()); MtgJsonCard ref = MtgJsonService.cardFromSet(card.getExpansionSetCode(), card.getName(), card.getCardNumber());
if (ref != null) { if (ref != null) {

View file

@ -53,6 +53,11 @@ public interface ActivatedAbility extends Ability {
*/ */
ActivationStatus canActivate(UUID playerId, Game game); // has to return a reference to the permitting ability/source ActivationStatus canActivate(UUID playerId, Game game); // has to return a reference to the permitting ability/source
/**
* Who can activate an ability. By default, only you (the controller/owner).
*
* @param mayActivate
*/
void setMayActivate(TargetController mayActivate); void setMayActivate(TargetController mayActivate);
/** /**

View file

@ -173,7 +173,7 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
} }
/** /**
* Basic activation check. It contains costs and targets legality too. * Activated ability check, not spells. It contains costs and targets legality too.
* <p> * <p>
* WARNING, don't forget to call super.canActivate on override in card's code in most cases. * WARNING, don't forget to call super.canActivate on override in card's code in most cases.
* *

View file

@ -25,6 +25,14 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
@Override @Override
public ActivationStatus canActivate(UUID playerId, Game game) { public ActivationStatus canActivate(UUID playerId, Game game) {
// 20210723 - 116.2a
// Playing a land is a special action. To play a land, a player puts that land onto the battlefield
// from the zone it was in (usually that players hand). By default, a player can take this action
// only once during each of their turns. A player can take this action any time they have priority
// and the stack is empty during a main phase of their turn. See rule 305, Lands.
// no super.canActivate() call
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game); ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (!controlsAbility(playerId, game) && null == approvingObject) { if (!controlsAbility(playerId, game) && null == approvingObject) {
return ActivationStatus.getFalse(); return ActivationStatus.getFalse();

View file

@ -87,6 +87,9 @@ public class SpellAbility extends ActivatedAbilityImpl {
@Override @Override
public ActivationStatus canActivate(UUID playerId, Game game) { public ActivationStatus canActivate(UUID playerId, Game game) {
// spells can be cast from non hand zones, so must use custom check
// no super.canActivate() call
if (this.spellCanBeActivatedRegularlyNow(playerId, game)) { if (this.spellCanBeActivatedRegularlyNow(playerId, game)) {
if (spellAbilityType == SpellAbilityType.SPLIT if (spellAbilityType == SpellAbilityType.SPLIT
|| spellAbilityType == SpellAbilityType.SPLIT_AFTERMATH) { || spellAbilityType == SpellAbilityType.SPLIT_AFTERMATH) {
@ -121,7 +124,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
} }
} }
// can pay all costs // can pay all costs and choose targets
if (costs.canPay(this, this, playerId, game)) { if (costs.canPay(this, this, playerId, game)) {
if (getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) { if (getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) {
SplitCard splitCard = (SplitCard) game.getCard(getSourceId()); SplitCard splitCard = (SplitCard) game.getCard(getSourceId());

View file

@ -57,4 +57,6 @@ public interface TriggeredAbility extends Ability {
GameEvent getTriggerEvent(); GameEvent getTriggerEvent();
String getTriggerPhrase(); String getTriggerPhrase();
TriggeredAbility setTriggerPhrase(String triggerPhrase);
} }

View file

@ -25,6 +25,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
protected boolean leavesTheBattlefieldTrigger; protected boolean leavesTheBattlefieldTrigger;
private boolean triggersOnce = false; private boolean triggersOnce = false;
private GameEvent triggerEvent = null; private GameEvent triggerEvent = null;
private String triggerPhrase = null;
public TriggeredAbilityImpl(Zone zone, Effect effect) { public TriggeredAbilityImpl(Zone zone, Effect effect) {
this(zone, effect, false); this(zone, effect, false);
@ -71,6 +72,12 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
), game.getTurnNum()); ), game.getTurnNum());
} }
@Override
public TriggeredAbilityImpl setTriggerPhrase(String triggerPhrase) {
this.triggerPhrase = triggerPhrase;
return this;
}
@Override @Override
public void setTriggerEvent(GameEvent triggerEvent) { public void setTriggerEvent(GameEvent triggerEvent) {
this.triggerEvent = triggerEvent; this.triggerEvent = triggerEvent;
@ -182,7 +189,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
prefix = ""; prefix = "";
} }
return prefix + getTriggerPhrase() + sb; return prefix + (triggerPhrase == null ? getTriggerPhrase() : triggerPhrase) + sb;
} }
@Override @Override

View file

@ -42,18 +42,6 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl
@Override @Override
public ActivationStatus canActivate(UUID playerId, Game game) { public ActivationStatus canActivate(UUID playerId, Game game) {
if (!super.hasMoreActivationsThisTurn(game) || !(condition == null || condition.apply(game, this))) {
return ActivationStatus.getFalse();
}
if (!controlsAbility(playerId, game)) {
return ActivationStatus.getFalse();
}
if (timing == TimingRule.SORCERY
&& !game.canPlaySorcery(playerId)
&& null == game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.ACTIVATE_AS_INSTANT, this, controllerId, game)) {
return ActivationStatus.getFalse();
}
// check if player is in the process of playing spell costs and they are no longer allowed to use // check if player is in the process of playing spell costs and they are no longer allowed to use
// activated mana abilities (e.g. because they started to use improvise or convoke) // activated mana abilities (e.g. because they started to use improvise or convoke)
if (!game.getStack().isEmpty()) { if (!game.getStack().isEmpty()) {
@ -69,8 +57,7 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl
} }
} }
//20091005 - 605.3a return super.canActivate(playerId, game);
return new ActivationStatus(costs.canPay(this, this, controllerId, game), null);
} }
/** /**

View file

@ -21,9 +21,9 @@ import mage.constants.Outcome;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.game.Game; import mage.game.Game;
import mage.game.command.dungeons.DungeonOfTheMadMage; import mage.game.command.dungeons.DungeonOfTheMadMageDungeon;
import mage.game.command.dungeons.LostMineOfPhandelver; import mage.game.command.dungeons.LostMineOfPhandelverDungeon;
import mage.game.command.dungeons.TombOfAnnihilation; import mage.game.command.dungeons.TombOfAnnihilationDungeon;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.players.Player; import mage.players.Player;
@ -56,7 +56,7 @@ public class Dungeon implements CommandObject {
private MageObject copyFrom; // copied card INFO (used to call original adjusters) private MageObject copyFrom; // copied card INFO (used to call original adjusters)
private FrameStyle frameStyle; private FrameStyle frameStyle;
private final Abilities<Ability> abilites = new AbilitiesImpl<>(); private final Abilities<Ability> abilites = new AbilitiesImpl<>();
private final String expansionSetCodeForImage; private String expansionSetCodeForImage;
private final List<DungeonRoom> dungeonRooms = new ArrayList<>(); private final List<DungeonRoom> dungeonRooms = new ArrayList<>();
private DungeonRoom currentRoom = null; private DungeonRoom currentRoom = null;
@ -141,11 +141,11 @@ public class Dungeon implements CommandObject {
public static Dungeon createDungeon(String name) { public static Dungeon createDungeon(String name) {
switch (name) { switch (name) {
case "Tomb of Annihilation": case "Tomb of Annihilation":
return new TombOfAnnihilation(); return new TombOfAnnihilationDungeon();
case "Lost Mine of Phandelver": case "Lost Mine of Phandelver":
return new LostMineOfPhandelver(); return new LostMineOfPhandelverDungeon();
case "Dungeon of the Mad Mage": case "Dungeon of the Mad Mage":
return new DungeonOfTheMadMage(); return new DungeonOfTheMadMageDungeon();
default: default:
throw new UnsupportedOperationException("A dungeon should have been chosen"); throw new UnsupportedOperationException("A dungeon should have been chosen");
} }
@ -322,6 +322,10 @@ public class Dungeon implements CommandObject {
return expansionSetCodeForImage; return expansionSetCodeForImage;
} }
public void setExpansionSetCodeForImage(String expansionSetCodeForImage) {
this.expansionSetCodeForImage = expansionSetCodeForImage;
}
@Override @Override
public int getZoneChangeCounter(Game game) { public int getZoneChangeCounter(Game game) {
return 1; return 1;

View file

@ -26,9 +26,9 @@ import mage.target.common.TargetCreaturePermanent;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
public class DungeonOfTheMadMage extends Dungeon { public class DungeonOfTheMadMageDungeon extends Dungeon {
public DungeonOfTheMadMage() { public DungeonOfTheMadMageDungeon() {
super("Dungeon of the Mad Mage", "AFR"); super("Dungeon of the Mad Mage", "AFR");
// (1) Yawning Portal You gain 1 life. ( 2) // (1) Yawning Portal You gain 1 life. ( 2)
DungeonRoom yawningPortal = new DungeonRoom("Yawning Portal", new GainLifeEffect(1)); DungeonRoom yawningPortal = new DungeonRoom("Yawning Portal", new GainLifeEffect(1));
@ -86,12 +86,12 @@ public class DungeonOfTheMadMage extends Dungeon {
this.addRoom(madWizardsLair); this.addRoom(madWizardsLair);
} }
private DungeonOfTheMadMage(final DungeonOfTheMadMage dungeon) { private DungeonOfTheMadMageDungeon(final DungeonOfTheMadMageDungeon dungeon) {
super(dungeon); super(dungeon);
} }
public DungeonOfTheMadMage copy() { public DungeonOfTheMadMageDungeon copy() {
return new DungeonOfTheMadMage(this); return new DungeonOfTheMadMageDungeon(this);
} }
} }

View file

@ -18,9 +18,9 @@ import mage.target.common.TargetCreaturePermanent;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
public class LostMineOfPhandelver extends Dungeon { public class LostMineOfPhandelverDungeon extends Dungeon {
public LostMineOfPhandelver() { public LostMineOfPhandelverDungeon() {
super("Lost Mine of Phandelver", "AFR"); super("Lost Mine of Phandelver", "AFR");
// (1) Cave Entrance Scry 1. ( 2a or 2b) // (1) Cave Entrance Scry 1. ( 2a or 2b)
DungeonRoom caveEntrance = new DungeonRoom( DungeonRoom caveEntrance = new DungeonRoom(
@ -75,12 +75,12 @@ public class LostMineOfPhandelver extends Dungeon {
this.addRoom(templeOfDumathoin); this.addRoom(templeOfDumathoin);
} }
private LostMineOfPhandelver(final LostMineOfPhandelver dungeon) { private LostMineOfPhandelverDungeon(final LostMineOfPhandelverDungeon dungeon) {
super(dungeon); super(dungeon);
} }
@Override @Override
public LostMineOfPhandelver copy() { public LostMineOfPhandelverDungeon copy() {
return new LostMineOfPhandelver(this); return new LostMineOfPhandelverDungeon(this);
} }
} }

View file

@ -28,7 +28,7 @@ import java.util.*;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
public final class TombOfAnnihilation extends Dungeon { public final class TombOfAnnihilationDungeon extends Dungeon {
static final FilterControlledPermanent filter static final FilterControlledPermanent filter
= new FilterControlledPermanent("an artifact, a creature, or a land"); = new FilterControlledPermanent("an artifact, a creature, or a land");
@ -41,7 +41,7 @@ public final class TombOfAnnihilation extends Dungeon {
)); ));
} }
public TombOfAnnihilation() { public TombOfAnnihilationDungeon() {
super("Tomb of Annihilation", "AFR"); super("Tomb of Annihilation", "AFR");
// (1) Trapped Entry Each player loses 1 life. ( 2a or 2b) // (1) Trapped Entry Each player loses 1 life. ( 2a or 2b)
DungeonRoom trappedEntry = new DungeonRoom("Trapped Entry", new LoseLifeAllPlayersEffect(1)); DungeonRoom trappedEntry = new DungeonRoom("Trapped Entry", new LoseLifeAllPlayersEffect(1));
@ -71,12 +71,12 @@ public final class TombOfAnnihilation extends Dungeon {
this.addRoom(cradleOfTheDeathGod); this.addRoom(cradleOfTheDeathGod);
} }
private TombOfAnnihilation(final TombOfAnnihilation dungeon) { private TombOfAnnihilationDungeon(final TombOfAnnihilationDungeon dungeon) {
super(dungeon); super(dungeon);
} }
public TombOfAnnihilation copy() { public TombOfAnnihilationDungeon copy() {
return new TombOfAnnihilation(this); return new TombOfAnnihilationDungeon(this);
} }
} }
@ -169,7 +169,7 @@ class OublietteTarget extends TargetControlledPermanent {
CardType.CREATURE, CardType.CREATURE,
CardType.LAND CardType.LAND
); );
private static final FilterControlledPermanent filter = TombOfAnnihilation.filter.copy(); private static final FilterControlledPermanent filter = TombOfAnnihilationDungeon.filter.copy();
static { static {
filter.setMessage("an artifact, a creature, and a land"); filter.setMessage("an artifact, a creature, and a land");
@ -245,7 +245,7 @@ class SandfallCellEffect extends OneShotEffect {
if (player == null) { if (player == null) {
continue; continue;
} }
TargetPermanent target = new TargetPermanent(0, 1, TombOfAnnihilation.filter, true); TargetPermanent target = new TargetPermanent(0, 1, TombOfAnnihilationDungeon.filter, true);
player.choose(Outcome.PreventDamage, target, source.getSourceId(), game); player.choose(Outcome.PreventDamage, target, source.getSourceId(), game);
map.put(playerId, game.getPermanent(target.getFirstTarget())); map.put(playerId, game.getPermanent(target.getFirstTarget()));
} }

View file

@ -7,6 +7,8 @@ import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import java.util.Arrays;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -20,8 +22,11 @@ public final class BooToken extends TokenImpl {
subtype.add(SubType.HAMSTER); subtype.add(SubType.HAMSTER);
power = new MageInt(1); power = new MageInt(1);
toughness = new MageInt(1); toughness = new MageInt(1);
addAbility(TrampleAbility.getInstance()); addAbility(TrampleAbility.getInstance());
addAbility(HasteAbility.getInstance()); addAbility(HasteAbility.getInstance());
availableImageSetCodes = Arrays.asList("AFR");
} }
private BooToken(final BooToken token) { private BooToken(final BooToken token) {

View file

@ -1,6 +1,7 @@
package mage.game.permanent.token; package mage.game.permanent.token;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import mage.MageInt; import mage.MageInt;
@ -27,11 +28,15 @@ public final class DevilToken extends TokenImpl {
color.setRed(true); color.setRed(true);
power = new MageInt(1); power = new MageInt(1);
toughness = new MageInt(1); toughness = new MageInt(1);
// When this creature dies, it deals 1 damage to any target.
Effect effect = new DamageTargetEffect(1); Effect effect = new DamageTargetEffect(1);
effect.setText("it deals 1 damage to any target"); effect.setText("it deals 1 damage to any target");
Ability ability = new DiesSourceTriggeredAbility(effect); Ability ability = new DiesSourceTriggeredAbility(effect);
ability.addTarget(new TargetAnyTarget()); ability.addTarget(new TargetAnyTarget());
this.addAbility(ability); this.addAbility(ability);
availableImageSetCodes = Arrays.asList("SOI", "WAR", "AFR");
} }
public DevilToken(final DevilToken token) { public DevilToken(final DevilToken token) {

View file

@ -13,6 +13,8 @@ import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import java.util.Arrays;
/** /**
* *
* @author weirddan455 * @author weirddan455
@ -27,10 +29,14 @@ public final class DogIllusionToken extends TokenImpl {
subtype.add(SubType.ILLUSION); subtype.add(SubType.ILLUSION);
power = new MageInt(0); power = new MageInt(0);
toughness = new MageInt(0); toughness = new MageInt(0);
addAbility(new SimpleStaticAbility(Zone.ALL, new SetPowerToughnessSourceEffect(
// This creature's power and toughness are each equal to twice the number of cards in your hand.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetPowerToughnessSourceEffect(
DogIllusionValue.instance, Duration.EndOfGame) DogIllusionValue.instance, Duration.EndOfGame)
.setText("this creature's power and toughness are each equal to twice the number of cards in your hand") .setText("this creature's power and toughness are each equal to twice the number of cards in your hand")
)); ));
availableImageSetCodes = Arrays.asList("AFR");
} }
private DogIllusionToken(final DogIllusionToken token) { private DogIllusionToken(final DogIllusionToken token) {

View file

@ -32,7 +32,7 @@ public final class GoblinToken extends TokenImpl {
availableImageSetCodes = Arrays.asList("10E", "ALA", "SOM", "M10", "NPH", "M13", "RTR", availableImageSetCodes = Arrays.asList("10E", "ALA", "SOM", "M10", "NPH", "M13", "RTR",
"MMA", "M15", "C14", "KTK", "EVG", "DTK", "ORI", "DDG", "DDN", "EVG", "MM2", "MMA", "M15", "C14", "KTK", "EVG", "DTK", "ORI", "DDG", "DDN", "EVG", "MM2",
"MM3", "EMA", "C16", "DOM", "ANA", "RNA", "WAR", "MH1", "TSR", "MH2"); "MM3", "EMA", "C16", "DOM", "ANA", "RNA", "WAR", "MH1", "TSR", "MH2", "AFR");
} }
public GoblinToken(final GoblinToken token) { public GoblinToken(final GoblinToken token) {

View file

@ -6,6 +6,8 @@ import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import java.util.Arrays;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -19,7 +21,11 @@ public final class GuenhwyvarToken extends TokenImpl {
subtype.add(SubType.CAT); subtype.add(SubType.CAT);
power = new MageInt(4); power = new MageInt(4);
toughness = new MageInt(1); toughness = new MageInt(1);
addAbility(TrampleAbility.getInstance());
// Trample
this.addAbility(TrampleAbility.getInstance());
availableImageSetCodes = Arrays.asList("AFR");
} }
private GuenhwyvarToken(final GuenhwyvarToken token) { private GuenhwyvarToken(final GuenhwyvarToken token) {

View file

@ -14,6 +14,8 @@ import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate; import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import java.util.Arrays;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -35,11 +37,19 @@ public class IcingdeathFrostTongueToken extends TokenImpl {
cardType.add(CardType.ARTIFACT); cardType.add(CardType.ARTIFACT);
color.setWhite(true); color.setWhite(true);
subtype.add(SubType.EQUIPMENT); subtype.add(SubType.EQUIPMENT);
// Equipped creature gets +2/+0
this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(2, 0))); this.addAbility(new SimpleStaticAbility(new BoostEquippedEffect(2, 0)));
// Whenever equipped creature attacks, tap target creature defending player controls
Ability ability = new AttacksAttachedTriggeredAbility(new TapTargetEffect(), false); Ability ability = new AttacksAttachedTriggeredAbility(new TapTargetEffect(), false);
ability.addTarget(new TargetPermanent(filter)); ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability); this.addAbility(ability);
// Equip {2}
this.addAbility(new EquipAbility(2)); this.addAbility(new EquipAbility(2));
availableImageSetCodes = Arrays.asList("AFR");
} }
private IcingdeathFrostTongueToken(final IcingdeathFrostTongueToken token) { private IcingdeathFrostTongueToken(final IcingdeathFrostTongueToken token) {

View file

@ -6,6 +6,8 @@ import mage.abilities.keyword.ReachAbility;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import java.util.Arrays;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -18,8 +20,14 @@ public final class LolthSpiderToken extends TokenImpl {
subtype.add(SubType.SPIDER); subtype.add(SubType.SPIDER);
power = new MageInt(2); power = new MageInt(2);
toughness = new MageInt(1); toughness = new MageInt(1);
addAbility(new MenaceAbility());
addAbility(ReachAbility.getInstance()); // Menace
this.addAbility(new MenaceAbility());
// Reach
this.addAbility(ReachAbility.getInstance());
availableImageSetCodes = Arrays.asList("AFR");
} }
public LolthSpiderToken(final LolthSpiderToken token) { public LolthSpiderToken(final LolthSpiderToken token) {

View file

@ -4,6 +4,8 @@ import mage.MageInt;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import java.util.Arrays;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -16,6 +18,8 @@ public final class SkeletonToken extends TokenImpl {
color.setBlack(true); color.setBlack(true);
power = new MageInt(1); power = new MageInt(1);
toughness = new MageInt(1); toughness = new MageInt(1);
availableImageSetCodes = Arrays.asList("AFR");
} }
public SkeletonToken(final SkeletonToken token) { public SkeletonToken(final SkeletonToken token) {

View file

@ -6,6 +6,8 @@ import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import java.util.Arrays;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -20,7 +22,11 @@ public final class TheAtropalToken extends TokenImpl {
subtype.add(SubType.HORROR); subtype.add(SubType.HORROR);
power = new MageInt(4); power = new MageInt(4);
toughness = new MageInt(4); toughness = new MageInt(4);
// Deathtouch
addAbility(DeathtouchAbility.getInstance()); addAbility(DeathtouchAbility.getInstance());
availableImageSetCodes = Arrays.asList("AFR");
} }
public TheAtropalToken(final TheAtropalToken token) { public TheAtropalToken(final TheAtropalToken token) {

View file

@ -27,7 +27,7 @@ public final class TreasureToken extends TokenImpl {
ability.addCost(new SacrificeSourceCost()); ability.addCost(new SacrificeSourceCost());
this.addAbility(ability); this.addAbility(ability);
availableImageSetCodes = Arrays.asList("XLN", "RNA", "M20", "C19", "C20", "M21", "CMR", "KHM", "STX", "MH2"); availableImageSetCodes = Arrays.asList("XLN", "RNA", "M20", "C19", "C20", "M21", "CMR", "KHM", "STX", "MH2", "AFR");
} }
public TreasureToken(final TreasureToken token) { public TreasureToken(final TreasureToken token) {

View file

@ -6,6 +6,8 @@ import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import java.util.Arrays;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
@ -20,7 +22,11 @@ public final class VecnaToken extends TokenImpl {
subtype.add(SubType.GOD); subtype.add(SubType.GOD);
power = new MageInt(8); power = new MageInt(8);
toughness = new MageInt(8); toughness = new MageInt(8);
addAbility(IndestructibleAbility.getInstance());
// Indestructible
this.addAbility(IndestructibleAbility.getInstance());
availableImageSetCodes = Arrays.asList("AFR");
} }
private VecnaToken(final VecnaToken token) { private VecnaToken(final VecnaToken token) {

View file

@ -3,48 +3,35 @@ package mage.game.permanent.token;
import mage.MageInt; import mage.MageInt;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.util.RandomUtil;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public final class WolfToken extends TokenImpl { public final class WolfToken extends TokenImpl {
static final private List<String> tokenImageSets = new ArrayList<>();
static {
tokenImageSets.addAll(Arrays.asList("BNG", "C14", "CNS", "FNMP", "ISD", "LRW", "M10", "M14", "MM2", "SHM", "SOM",
"ZEN", "SOI", "C15", "M15", "WAR", "M20", "THB"));
}
public WolfToken() { public WolfToken() {
this(null, 0);
}
public WolfToken(String setCode) {
this(setCode, 0);
}
public WolfToken(String setCode, int tokenType) {
super("Wolf", "2/2 green Wolf creature token"); super("Wolf", "2/2 green Wolf creature token");
availableImageSetCodes = tokenImageSets;
setOriginalExpansionSetCode(setCode);
cardType.add(CardType.CREATURE); cardType.add(CardType.CREATURE);
color.setGreen(true); color.setGreen(true);
subtype.add(SubType.WOLF); subtype.add(SubType.WOLF);
power = new MageInt(2); power = new MageInt(2);
toughness = new MageInt(2); toughness = new MageInt(2);
availableImageSetCodes = Arrays.asList("BNG", "C14", "C15", "CMA", "CMD", "CNS", "DKA", "EVE", "ISD",
"LRW", "M10", "M14", "MM2", "MOR", "SHM", "SOI", "SOM", "V10", "WWK", "ZEN", "WAR", "M20",
"THB", "AFR");
} }
@Override @Override
public void setExpansionSetCodeForImage(String code) { public void setExpansionSetCodeForImage(String code) {
super.setExpansionSetCodeForImage(code); super.setExpansionSetCodeForImage(code);
if (getOriginalExpansionSetCode() != null && getOriginalExpansionSetCode().equals("ISD")) { if (getOriginalExpansionSetCode() != null && getOriginalExpansionSetCode().equals("ISD")) {
this.setTokenType(2); this.setTokenType(RandomUtil.nextInt(2) + 1); // 2 images
} }
} }

View file

@ -25,7 +25,7 @@ public final class ZombieToken extends TokenImpl {
"CNS", "MMA", "BNG", "KTK", "DTK", "ORI", "OGW", "CNS", "MMA", "BNG", "KTK", "DTK", "ORI", "OGW",
"SOI", "EMN", "EMA", "MM3", "AKH", "CMA", "E01", "SOI", "EMN", "EMA", "MM3", "AKH", "CMA", "E01",
"RNA", "WAR", "MH1", "M20", "C19", "THB", "M21", "RNA", "WAR", "MH1", "M20", "C19", "THB", "M21",
"CMR", "C21", "MH2"); "CMR", "C21", "MH2", "AFR");
} }
@Override @Override

View file

@ -2716,7 +2716,7 @@ public abstract class PlayerImpl implements Player, Serializable {
.stream() .stream()
.filter(card -> filter.match(card, source.getSourceId(), getId(), game)) .filter(card -> filter.match(card, source.getSourceId(), getId(), game))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Card card = RandomUtil.randomFromSet(cards); Card card = RandomUtil.randomFromCollection(cards);
if (card == null) { if (card == null) {
return false; return false;
} }
@ -3706,6 +3706,9 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
// check the hand zone (Sen Triplets) // check the hand zone (Sen Triplets)
// TODO: remove direct hand check (reveal fix in Sen Triplets)?
// human games: cards from opponent's hand must be revealed before play
// AI games: computer can see and play cards from opponent's hand without reveal
if (fromAll || fromZone == Zone.HAND) { if (fromAll || fromZone == Zone.HAND) {
for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {
Player player = game.getPlayer(playerInRangeId); Player player = game.getPlayer(playerInRangeId);

View file

@ -1,32 +1,3 @@
/*
*
* Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of BetaSteward_at_googlemail.com.
*
*/
package mage.target.common; package mage.target.common;
import mage.MageObject; import mage.MageObject;
@ -154,7 +125,7 @@ public class TargetPermanentOrSuspendedCard extends TargetImpl {
@Override @Override
public String getTargetedName(Game game) { public String getTargetedName(Game game) {
StringBuilder sb = new StringBuilder(""); StringBuilder sb = new StringBuilder();
for (UUID targetId : this.getTargets()) { for (UUID targetId : this.getTargets()) {
Permanent permanent = game.getPermanent(targetId); Permanent permanent = game.getPermanent(targetId);
if (permanent != null) { if (permanent != null) {

View file

@ -1,16 +1,15 @@
package mage.util; package mage.util;
import java.awt.*; import java.awt.*;
import java.io.Serializable; import java.util.Collection;
import java.util.Random; import java.util.Random;
import java.util.Set;
/** /**
* Created by IGOUDT on 5-9-2016. * Created by IGOUDT on 5-9-2016.
*/ */
public final class RandomUtil { public final class RandomUtil {
private static Random random = new Random(); // thread safe with seed support private static final Random random = new Random(); // thread safe with seed support
private RandomUtil() { private RandomUtil() {
} }
@ -43,15 +42,15 @@ public final class RandomUtil {
random.setSeed(newSeed); random.setSeed(newSeed);
} }
public static <T extends Serializable> T randomFromSet(Set<T> collection) { public static <T> T randomFromCollection(Collection<T> collection) {
if (collection.size() < 2) { if (collection.size() < 2) {
return collection.stream().findFirst().orElse(null); return collection.stream().findFirst().orElse(null);
} }
int rand = nextInt(collection.size()); int rand = nextInt(collection.size());
int count = 0; int count = 0;
for (T currentId : collection) { for (T current : collection) {
if (count == rand) { if (count == rand) {
return currentId; return current;
} }
count++; count++;
} }

View file

@ -1,6 +1,8 @@
package mage.watchers.common; package mage.watchers.common;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.constants.WatcherScope; import mage.constants.WatcherScope;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
@ -68,4 +70,18 @@ public class SpellsCastWatcher extends Watcher {
public int getNumberOfNonCreatureSpells() { public int getNumberOfNonCreatureSpells() {
return nonCreatureSpells; return nonCreatureSpells;
} }
public UUID getCasterId(Ability source, Game game) {
for (Map.Entry<UUID, List<Spell>> entry : spellsCast.entrySet()) {
if (entry.getValue()
.stream()
.map(Spell::getCard)
.map(Card::getMainCard)
.anyMatch(card -> card.getId().equals(source.getSourceId())
&& card.getZoneChangeCounter(game) == source.getSourceObjectZoneChangeCounter())) {
return entry.getKey();
}
}
return null;
}
} }