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;
}

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", "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
supportedSets.clear();
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);
card.setTokenClassName(tokenClassName);
list.add(card);
// logger.debug("Token: " + set + "/" + card.getName() + " type: " + type);
} else if (params[1].toLowerCase(Locale.ENGLISH).equals("generate") && params[2].startsWith("EMBLEM:")) {
String set = params[2].substring(7);
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);
card.setTokenClassName(tokenClassName);
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 {
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:STX|Lukka, Wayward Bonder||Emblem Lukka|LukkaWaywardBonderEmblem|
|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
|Generate|PLANE:PCA|Plane - Academy at Tolaria West|||AcademyAtTolariaWestPlane|
@ -177,6 +181,11 @@
|Generate|TOK:AER|Gremlin|||GremlinToken|
|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
|Generate|TOK:AKH|Beast|||BeastToken3|
|Generate|TOK:AKH|Cat|||CatToken2|
@ -1563,4 +1572,21 @@
|Generate|TOK:MH2|Treasure|1||TreasureToken|
|Generate|TOK:MH2|Treasure|2||TreasureToken|
|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 {
if (callbackClient.isConnected()) {
if (callbackClient != null && callbackClient.isConnected()) {
callbackClient.removeListener(callbackHandler);
callbackClient.disconnect();
}

View file

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

View file

@ -446,8 +446,14 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ {
logger.info("simulating - timed out");
task.cancel(true);
} catch (ExecutionException e) {
// exception error in simulated game
e.printStackTrace();
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) {
e.printStackTrace();
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) {
if (log.isDebugEnabled()) {
log.debug("chooseTarget: " + outcome.toString() + ':' + target.toString());
log.debug("choose: " + outcome.toString() + ':' + target.toString());
}
// controller hints:
@ -205,21 +205,30 @@ public class ComputerPlayer extends PlayerImpl implements Player {
}
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;
if (outcome.isCanTargetAll()) {
targets = threats(null, sourceId, origTarget.getFilter(), game, target.getTargets());
targets = threats(null, sourceId, filter, game, target.getTargets());
} else {
if (outcome.isGood()) {
targets = threats(abilityControllerId, sourceId, origTarget.getFilter(), game, target.getTargets());
targets = threats(abilityControllerId, sourceId, filter, game, target.getTargets());
} else {
targets = threats(randomOpponentId, sourceId, origTarget.getFilter(), game, target.getTargets());
targets = threats(randomOpponentId, sourceId, filter, game, target.getTargets());
}
if (targets.isEmpty() && target.isRequired()) {
if (!outcome.isGood()) {
targets = threats(abilityControllerId, sourceId, origTarget.getFilter(), game, target.getTargets());
targets = threats(abilityControllerId, sourceId, filter, game, target.getTargets());
} 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) {
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()) {
Card card = pickTarget(abilityControllerId, cards, outcome, target, null, game);
if (card != null && alreadyTargeted != null && !alreadyTargeted.contains(card.getId())) {
@ -461,10 +479,23 @@ public class ComputerPlayer extends PlayerImpl implements Player {
if (!required) {
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
@Override
@ -578,8 +609,17 @@ public class ComputerPlayer extends PlayerImpl implements Player {
// TODO: implemented findBestPlayerTargets
// TODO: add findBest*Targets for all target types
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);
// 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) {
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<>();
for (UUID uuid : target.possibleTargets(source.getSourceId(), source.getControllerId(), game)) {
Card card = game.getCard(uuid);
if (card != null) {
if (card != null && game.getState().getZone(card.getId()) == Zone.EXILED) {
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
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
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:
for (Mode mode : modes.getAvailableModes(source, game)) {
for (UUID selectedModeId : modes.getSelectedModes()) {

View file

@ -30,7 +30,6 @@ import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
*
* @author Colin Redman
*/
public class AminatouTheFateshifter extends CardImpl {
@ -53,9 +52,9 @@ public class AminatouTheFateshifter extends CardImpl {
Ability ability = new LoyaltyAbility(new AminatouPlusEffect(), +1);
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.addEffect(new ReturnToBattlefieldUnderYourControlTargetEffect(true));
ability.addEffect(new ReturnToBattlefieldUnderYourControlTargetEffect(false));
ability.addTarget(new TargetPermanent(filter));
this.addAbility(ability);

View file

@ -34,7 +34,7 @@ public final class ArlinnVoiceOfThePack extends CardImpl {
this.addAbility(new SimpleStaticAbility(new ArlinnVoiceOfThePackReplacementEffect()));
// -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) {

View file

@ -59,7 +59,7 @@ public final class BagOfDevouring extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{B}");
// 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.
Ability ability = new SimpleActivatedAbility(new DrawCardSourceControllerEffect(1), new GenericManaCost(2));

View file

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

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());
}
if (!opponents.isEmpty()) {
permanent.attachTo(RandomUtil.randomFromSet(opponents), source, game);
permanent.attachTo(RandomUtil.randomFromCollection(opponents), source, game);
}
return true;
}

View file

@ -11,10 +11,7 @@ import mage.abilities.effects.mana.BasicManaEffect;
import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
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)),
new RemoveCountersSourceCost(CounterType.CHARGE.createInstance(1)));
this.netMana.add(new Mana(0, 0, 0, 0, 0, 0, 0, 1));
this.setMayActivate(TargetController.ANY);
}
public ManaCacheManaAbility(final ManaCacheManaAbility ability) {
@ -102,17 +100,15 @@ class ManaCacheManaAbility extends ActivatedManaAbilityImpl {
@Override
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
Player player = game.getPlayer(playerId);
if (player == null
|| !playerId.equals(game.getActivePlayerId())
|| !game.getStep().getType().isBefore(PhaseStep.END_TURN)) {
return ActivationStatus.getFalse();
}
Player player = game.getPlayer(playerId);
if (player != null && playerId.equals(game.getActivePlayerId()) && game.getStep().getType().isBefore(PhaseStep.END_TURN)) {
if (costs.canPay(this, this, playerId, game)) {
this.setControllerId(playerId);
return ActivationStatus.getTrue(this, game);
}
}
return ActivationStatus.getFalse();
return super.canActivate(playerId, game);
}
@Override

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)) {
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);
// 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) {

View file

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

View file

@ -41,7 +41,7 @@ public final class WrensRunPackmaster extends CardImpl {
this.addAbility(new ChampionAbility(this, SubType.ELF, false));
// {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.
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("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("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("Hex", 101, Rarity.RARE, mage.cards.h.Hex.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("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("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("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("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("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));
@ -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("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("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("Obsessive Stitcher", 189, Rarity.UNCOMMON, mage.cards.o.ObsessiveStitcher.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()) {
if (player.getName().equals(target)) {
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) {
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();
}
if (selectedMode == null) {
throw new UnsupportedOperationException("Mode not available for " + ability.toString());
throw new UnsupportedOperationException("Mode not available for " + ability);
}
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()) {
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;
}
// 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
if (params[0].equals(CHECK_COMMAND_EXILE_COUNT) && params.length == 3) {
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);
}
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) {
int foundCount = 0;
for (Card card : game.getExile().getAllCards(game)) {
@ -1465,9 +1491,9 @@ public class TestPlayer implements Player {
}
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 {
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 conditional = player.getManaPool().getConditionalMana().stream().mapToInt(a -> a.get(manaType)).sum(); // calcs FULL conditional mana, not real conditions
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) {
@ -1952,7 +1978,18 @@ public class TestPlayer implements Player {
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);
}
@ -2298,7 +2335,8 @@ public class TestPlayer implements Player {
|| (target.getOriginalTarget() instanceof TargetPermanentOrPlayer)
|| (target.getOriginalTarget() instanceof TargetAnyTarget)
|| (target.getOriginalTarget() instanceof TargetCreatureOrPlayer)
|| (target.getOriginalTarget() instanceof TargetDefender)) {
|| (target.getOriginalTarget() instanceof TargetDefender)
|| (target.getOriginalTarget() instanceof TargetPermanentOrSuspendedCard)) {
for (String targetDefinition : targets) {
if (targetDefinition.startsWith("targetPlayer=")) {
continue;
@ -2330,6 +2368,9 @@ public class TestPlayer implements Player {
if (filter instanceof FilterPlaneswalkerOrPlayer) {
filter = ((FilterPlaneswalkerOrPlayer) filter).getFilterPermanent();
}
if (filter instanceof FilterPermanentOrSuspendedCard) {
filter = ((FilterPermanentOrSuspendedCard) filter).getPermanentFilter();
}
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 (target.canTarget(abilityControllerId, permanent.getId(), source, game) && !target.getTargets().contains(permanent.getId())) {
@ -2375,14 +2416,26 @@ public class TestPlayer implements Player {
}
// card in exile
if (target.getOriginalTarget() instanceof TargetCardInExile) {
TargetCardInExile targetFull = (TargetCardInExile) target.getOriginalTarget();
if (target.getOriginalTarget() instanceof TargetCardInExile
|| 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) {
checkTargetDefinitionMarksSupport(target, targetDefinition, "^");
String[] targetList = targetDefinition.split("\\^");
boolean targetFound = false;
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 (target.canTarget(abilityControllerId, card.getId(), source, game) && !target.getTargets().contains(card.getId())) {
target.addTarget(card.getId(), source, game);
@ -2508,16 +2561,17 @@ public class TestPlayer implements Player {
String message;
if (source != null) {
message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used in ["
+ "card " + source.getSourceObject(game)
+ " -> ability " + source.getClass().getSimpleName() + " (" + source.getRule().substring(0, Math.min(20, source.getRule().length()) - 1) + "..." + ")"
+ " -> target " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")"
+ "]";
message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used"
+ "\nCard: " + source.getSourceObject(game)
+ "\nAbility: " + source.getClass().getSimpleName() + " (" + source.getRule() + ")"
+ "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")"
+ "\nYou must implement target class support in TestPlayer or setup good targets";
} else {
message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used in ["
+ "card XXX"
+ " -> target " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")"
+ "]";
message = this.getName() + " - Targets list was setup by addTarget with " + targets + ", but not used"
+ "\nCard: unknown source"
+ "\nAbility: unknown source"
+ "\nTarget: " + target.getClass().getSimpleName() + " (" + target.getMessage() + ")"
+ "\nYou must implement target class support in TestPlayer or setup good targets";
}
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_TAPPED = "PERMANENT_TAPPED";
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_GRAVEYARD_COUNT = "GRAVEYARD_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());
}
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) {
//Assert.assertNotEquals("", permanentName);
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.SubType;
import mage.constants.SuperType;
import mage.game.command.Dungeon;
import mage.game.command.Plane;
import mage.game.draft.DraftCube;
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) {
MtgJsonCard ref = MtgJsonService.cardFromSet(card.getExpansionSetCode(), card.getName(), card.getCardNumber());
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
/**
* Who can activate an ability. By default, only you (the controller/owner).
*
* @param 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>
* 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
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);
if (!controlsAbility(playerId, game) && null == approvingObject) {
return ActivationStatus.getFalse();

View file

@ -87,6 +87,9 @@ public class SpellAbility extends ActivatedAbilityImpl {
@Override
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 (spellAbilityType == SpellAbilityType.SPLIT
|| 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 (getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) {
SplitCard splitCard = (SplitCard) game.getCard(getSourceId());

View file

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

View file

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

View file

@ -42,18 +42,6 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl
@Override
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
// activated mana abilities (e.g. because they started to use improvise or convoke)
if (!game.getStack().isEmpty()) {
@ -69,8 +57,7 @@ public abstract class ActivatedManaAbilityImpl extends ActivatedAbilityImpl impl
}
}
//20091005 - 605.3a
return new ActivationStatus(costs.canPay(this, this, controllerId, game), null);
return super.canActivate(playerId, game);
}
/**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,8 @@ import mage.constants.Zone;
import mage.game.Game;
import mage.players.Player;
import java.util.Arrays;
/**
*
* @author weirddan455
@ -27,10 +29,14 @@ public final class DogIllusionToken extends TokenImpl {
subtype.add(SubType.ILLUSION);
power = 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)
.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) {

View file

@ -32,7 +32,7 @@ public final class GoblinToken extends TokenImpl {
availableImageSetCodes = Arrays.asList("10E", "ALA", "SOM", "M10", "NPH", "M13", "RTR",
"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) {

View file

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

View file

@ -14,6 +14,8 @@ import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate;
import mage.target.TargetPermanent;
import java.util.Arrays;
/**
* @author TheElk801
*/
@ -35,11 +37,19 @@ public class IcingdeathFrostTongueToken extends TokenImpl {
cardType.add(CardType.ARTIFACT);
color.setWhite(true);
subtype.add(SubType.EQUIPMENT);
// Equipped creature gets +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.addTarget(new TargetPermanent(filter));
this.addAbility(ability);
// Equip {2}
this.addAbility(new EquipAbility(2));
availableImageSetCodes = Arrays.asList("AFR");
}
private IcingdeathFrostTongueToken(final IcingdeathFrostTongueToken token) {

View file

@ -6,6 +6,8 @@ import mage.abilities.keyword.ReachAbility;
import mage.constants.CardType;
import mage.constants.SubType;
import java.util.Arrays;
/**
* @author TheElk801
*/
@ -18,8 +20,14 @@ public final class LolthSpiderToken extends TokenImpl {
subtype.add(SubType.SPIDER);
power = new MageInt(2);
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) {

View file

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

View file

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

View file

@ -27,7 +27,7 @@ public final class TreasureToken extends TokenImpl {
ability.addCost(new SacrificeSourceCost());
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) {

View file

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

View file

@ -3,48 +3,35 @@ package mage.game.permanent.token;
import mage.MageInt;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.util.RandomUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author BetaSteward_at_googlemail.com
*/
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() {
this(null, 0);
}
public WolfToken(String setCode) {
this(setCode, 0);
}
public WolfToken(String setCode, int tokenType) {
super("Wolf", "2/2 green Wolf creature token");
availableImageSetCodes = tokenImageSets;
setOriginalExpansionSetCode(setCode);
cardType.add(CardType.CREATURE);
color.setGreen(true);
subtype.add(SubType.WOLF);
power = 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
public void setExpansionSetCodeForImage(String code) {
super.setExpansionSetCodeForImage(code);
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",
"SOI", "EMN", "EMA", "MM3", "AKH", "CMA", "E01",
"RNA", "WAR", "MH1", "M20", "C19", "THB", "M21",
"CMR", "C21", "MH2");
"CMR", "C21", "MH2", "AFR");
}
@Override

View file

@ -2716,7 +2716,7 @@ public abstract class PlayerImpl implements Player, Serializable {
.stream()
.filter(card -> filter.match(card, source.getSourceId(), getId(), game))
.collect(Collectors.toSet());
Card card = RandomUtil.randomFromSet(cards);
Card card = RandomUtil.randomFromCollection(cards);
if (card == null) {
return false;
}
@ -3706,6 +3706,9 @@ public abstract class PlayerImpl implements Player, Serializable {
}
// 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) {
for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {
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;
import mage.MageObject;
@ -154,7 +125,7 @@ public class TargetPermanentOrSuspendedCard extends TargetImpl {
@Override
public String getTargetedName(Game game) {
StringBuilder sb = new StringBuilder("");
StringBuilder sb = new StringBuilder();
for (UUID targetId : this.getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {

View file

@ -1,16 +1,15 @@
package mage.util;
import java.awt.*;
import java.io.Serializable;
import java.util.Collection;
import java.util.Random;
import java.util.Set;
/**
* Created by IGOUDT on 5-9-2016.
*/
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() {
}
@ -43,15 +42,15 @@ public final class RandomUtil {
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) {
return collection.stream().findFirst().orElse(null);
}
int rand = nextInt(collection.size());
int count = 0;
for (T currentId : collection) {
for (T current : collection) {
if (count == rand) {
return currentId;
return current;
}
count++;
}

View file

@ -1,6 +1,8 @@
package mage.watchers.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.filter.StaticFilters;
@ -68,4 +70,18 @@ public class SpellsCastWatcher extends Watcher {
public int getNumberOfNonCreatureSpells() {
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;
}
}