Merge pull request 'master' (#45) from External/mage:master into season3

Reviewed-on: #45
This commit is contained in:
Failure 2025-10-24 23:40:15 -07:00
commit ca9f3f3e1f
109 changed files with 4417 additions and 391 deletions

View file

@ -39,10 +39,9 @@ public class BracketLegalityLabel extends LegalityLabel {
private static final Logger logger = Logger.getLogger(BracketLegalityLabel.class);
private static final String GROUP_GAME_CHANGES = "Game Changers";
private static final String GROUP_INFINITE_COMBOS = "Infinite Combos";
private static final String GROUP_INFINITE_COMBOS = "Early-game 2-Card Combos";
private static final String GROUP_MASS_LAND_DESTRUCTION = "Mass Land Destruction";
private static final String GROUP_EXTRA_TURN = "Extra Turns";
private static final String GROUP_TUTORS = "Tutors";
private static final Map<String, List<Integer>> MAX_GROUP_LIMITS = new LinkedHashMap<>();
@ -78,8 +77,6 @@ public class BracketLegalityLabel extends LegalityLabel {
Arrays.asList(0, 0, 0, 0, 99, 99));
MAX_GROUP_LIMITS.put(GROUP_EXTRA_TURN,
Arrays.asList(0, 0, 0, 3, 99, 99));
MAX_GROUP_LIMITS.put(GROUP_TUTORS,
Arrays.asList(0, 3, 3, 99, 99, 99));
}
private static final String RESOURCE_INFINITE_COMBOS = "brackets/infinite-combos.txt";
@ -92,7 +89,6 @@ public class BracketLegalityLabel extends LegalityLabel {
private final List<String> foundInfiniteCombos = new ArrayList<>();
private final List<String> foundMassLandDestruction = new ArrayList<>();
private final List<String> foundExtraTurn = new ArrayList<>();
private final List<String> foundTutors = new ArrayList<>();
private final List<String> badCards = new ArrayList<>();
private final List<String> fullGameChanges = new ArrayList<>();
@ -126,9 +122,6 @@ public class BracketLegalityLabel extends LegalityLabel {
if (this.foundExtraTurn.size() > getMaxCardsLimit(GROUP_EXTRA_TURN)) {
this.badCards.addAll(this.foundExtraTurn);
}
if (this.foundTutors.size() > getMaxCardsLimit(GROUP_TUTORS)) {
this.badCards.addAll(this.foundTutors);
}
}
private Integer getMaxCardsLimit(String groupName) {
@ -165,7 +158,6 @@ public class BracketLegalityLabel extends LegalityLabel {
groups.put(GROUP_INFINITE_COMBOS + getStats(GROUP_INFINITE_COMBOS), this.foundInfiniteCombos);
groups.put(GROUP_MASS_LAND_DESTRUCTION + getStats(GROUP_MASS_LAND_DESTRUCTION), this.foundMassLandDestruction);
groups.put(GROUP_EXTRA_TURN + getStats(GROUP_EXTRA_TURN), this.foundExtraTurn);
groups.put(GROUP_TUTORS + getStats(GROUP_TUTORS), this.foundTutors);
groups.forEach((group, cards) -> {
showInfo.add("<br>");
showInfo.add("<span style='font-weight:bold;font-size: " + infoFontTextSize + "px;'>" + group + "</span>");
@ -199,9 +191,6 @@ public class BracketLegalityLabel extends LegalityLabel {
case GROUP_EXTRA_TURN:
currentAmount = this.foundExtraTurn.size();
break;
case GROUP_TUTORS:
currentAmount = this.foundTutors.size();
break;
default:
throw new IllegalArgumentException("Unknown group " + groupName);
}
@ -222,7 +211,6 @@ public class BracketLegalityLabel extends LegalityLabel {
collectInfiniteCombos(deck);
collectMassLandDestruction(deck);
collectExtraTurn(deck);
collectTutors(deck);
}
private void collectGameChangers(Deck deck) {
@ -244,12 +232,9 @@ public class BracketLegalityLabel extends LegalityLabel {
"Consecrated Sphinx",
"Crop Rotation",
"Cyclonic Rift",
"Deflecting Swat",
"Enlightened Tutor",
"Expropriate",
"Field of the Dead",
"Fierce Guardianship",
"Food Chain",
"Force of Will",
"Gaea's Cradle",
"Gamble",
@ -261,8 +246,6 @@ public class BracketLegalityLabel extends LegalityLabel {
"Imperial Seal",
"Intuition",
"Jeska's Will",
"Jin-Gitaxias, Core Augur",
"Kinnan, Bonder Prodigy",
"Lion's Eye Diamond",
"Mana Vault",
"Mishra's Workshop",
@ -280,18 +263,13 @@ public class BracketLegalityLabel extends LegalityLabel {
"Serra's Sanctum",
"Smothering Tithe",
"Survival of the Fittest",
"Sway of the Stars",
"Teferi's Protection",
"Tergrid, God of Fright",
"Thassa's Oracle",
"The One Ring",
"The Tabernacle at Pendrell Vale",
"Underworld Breach",
"Urza, Lord High Artificer",
"Vampiric Tutor",
"Vorinclex, Voice of Hunger",
"Yuriko, the Tiger's Shadow",
"Winota, Joiner of Forces",
"Worldly Tutor"
));
}
@ -393,19 +371,6 @@ public class BracketLegalityLabel extends LegalityLabel {
.forEach(this.foundExtraTurn::add);
}
private void collectTutors(Deck deck) {
// edh power level uses search for land and non-land card, but bracket need only non-land cards searching
this.foundTutors.clear();
Stream.concat(deck.getCards().stream(), deck.getSideboard().stream())
.filter(card -> card.getRules().stream()
.map(s -> s.toLowerCase(Locale.ENGLISH))
.anyMatch(s -> s.contains("search your library") && !isTextContainsLandCard(s))
)
.map(Card::getName)
.sorted()
.forEach(this.foundTutors::add);
}
private boolean isTextContainsLandCard(String lowerText) {
// TODO: share code with AbstractCommander and edh power level
// TODO: add tests

View file

@ -622,6 +622,8 @@ public class ScryfallImageSupportCards {
add("TLA"); // Avatar: The Last Airbender
add("TLE"); // Avatar: The Last Airbender Eternal
add("ECL"); // Lorwyn Eclipsed
add("TMT"); // Teenage Mutant Ninja Turtles
add("TMC"); // Teenage Mutant Ninja Turtles Eternal
// Custom sets using Scryfall images - must provide a direct link for each card in directDownloadLinks
add("CALC"); // Custom Alchemized versions of existing cards

View file

@ -2546,6 +2546,7 @@ public class ScryfallImageSupportTokens {
// DSK
put("DSK/Beast", "https://api.scryfall.com/cards/tdsk/3?format=image");
put("DSK/Demon", "https://api.scryfall.com/cards/tdsk/9?format=image");
put("DSK/Emblem Kaito", "https://api.scryfall.com/cards/tdsk/17/en?format=image");
put("DSK/Everywhere", "https://api.scryfall.com/cards/tdsk/16?format=image");
put("DSK/Glimmer", "https://api.scryfall.com/cards/tdsk/4?format=image");
@ -2558,6 +2559,7 @@ public class ScryfallImageSupportTokens {
put("DSK/Spider", "https://api.scryfall.com/cards/tdsk/12?format=image");
put("DSK/Spirit/1", "https://api.scryfall.com/cards/tdsk/6?format=image");
put("DSK/Spirit/2", "https://api.scryfall.com/cards/tdsk/8?format=image");
put("DSK/Toy", "https://api.scryfall.com/cards/tdsk/7?format=image");
put("DSK/Treasure", "https://api.scryfall.com/cards/tdsk/15?format=image");
// DSC

View file

@ -27,8 +27,7 @@ public abstract class AbstractCommander extends Constructed {
private static List<CommanderValidator> validators = Arrays.asList(
PartnerValidator.instance,
PartnerSurvivorsValidator.instance,
PartnerFatherAndSonValidator.instance,
PartnerVariantValidator.instance,
FriendsForeverValidator.instance,
PartnerWithValidator.instance,
ChooseABackgroundValidator.instance,

View file

@ -6,10 +6,10 @@ import mage.abilities.dynamicvalue.common.ManaSpentToCastCount;
import mage.abilities.effects.common.CastSourceTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.EntersBattlefieldUnderControlOfOpponentOfChoiceEffect;
import mage.abilities.keyword.PartnerSurvivorsAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.PartnerVariantType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.game.permanent.token.CordycepsInfectedToken;
@ -41,7 +41,7 @@ public final class AbbyMercilessSoldier extends CardImpl {
this.addAbility(new EntersBattlefieldAbility(new EntersBattlefieldUnderControlOfOpponentOfChoiceEffect()));
// Partner--Survivors
this.addAbility(PartnerSurvivorsAbility.getInstance());
this.addAbility(PartnerVariantType.SURVIVORS.makeAbility());
}
private AbbyMercilessSoldier(final AbbyMercilessSoldier card) {

View file

@ -1,17 +1,28 @@
package mage.cards.a;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.TapTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.filter.predicate.permanent.PermanentReferenceInCollectionPredicate;
import mage.game.Game;
import mage.game.permanent.token.HeroToken;
import mage.target.Target;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author TheElk801
@ -28,8 +39,7 @@ public final class AerithRescueMission extends CardImpl {
// * Take 59 Flights of Stairs -- Tap up to three target creatures. Put a stun counter on one of them.
this.getSpellAbility().addMode(new Mode(new TapTargetEffect())
.addEffect(new AddCountersTargetEffect(CounterType.STUN.createInstance())
.setText("Put a stun counter on one of them"))
.addEffect(new AerithRescueMissionStunEffect())
.addTarget(new TargetCreaturePermanent(0, 3))
.withFlavorWord("Take 59 Flights of Stairs"));
}
@ -43,3 +53,34 @@ public final class AerithRescueMission extends CardImpl {
return new AerithRescueMission(this);
}
}
class AerithRescueMissionStunEffect extends OneShotEffect {
AerithRescueMissionStunEffect() {
super(Outcome.Detriment);
staticText = "Put a stun counter on one of them";
}
private AerithRescueMissionStunEffect(final AerithRescueMissionStunEffect effect) {
super(effect);
}
@Override
public AerithRescueMissionStunEffect copy() {
return new AerithRescueMissionStunEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
FilterPermanent filter = new FilterPermanent("creature to put a stun counter on");
filter.add(new PermanentReferenceInCollectionPredicate(this.getTargetPointer().getTargets(game, source).stream()
.map(game::getPermanent).collect(Collectors.toList()), game));
Target target = new TargetPermanent(filter).withNotTarget(true);
if (target.choose(Outcome.UnboostCreature, source.getControllerId(), source, game)) {
Effect eff = new AddCountersTargetEffect(CounterType.STUN.createInstance());
eff.setTargetPointer(new FixedTarget(target.getFirstTarget(), game));
return eff.apply(game, source);
}
return false;
}
}

View file

@ -45,7 +45,7 @@ public final class AgentVenom extends CardImpl {
this.addAbility(FlashAbility.getInstance());
// Menace
this.addAbility(new MenaceAbility());
this.addAbility(new MenaceAbility(false));
// Whenever another nontoken creature you control dies, you draw a card and lose 1 life.
Ability ability = new DiesCreatureTriggeredAbility(

View file

@ -0,0 +1,132 @@
package mage.cards.a;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.hint.Hint;
import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.stack.Spell;
import mage.watchers.Watcher;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public final class AprilONeilHacktivist extends CardImpl {
public AprilONeilHacktivist(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.SCIENTIST);
this.power = new MageInt(1);
this.toughness = new MageInt(5);
// At the beginning of your end step, draw a card for each card type among spells you've cast this turn.
this.addAbility(new BeginningOfEndStepTriggeredAbility(
new DrawCardSourceControllerEffect(AprilONeilHacktivistValue.instance)
).addHint(AprilONeilHacktivistHint.instance), new AprilONeilHacktivistWatcher());
}
private AprilONeilHacktivist(final AprilONeilHacktivist card) {
super(card);
}
@Override
public AprilONeilHacktivist copy() {
return new AprilONeilHacktivist(this);
}
}
enum AprilONeilHacktivistValue implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return AprilONeilHacktivistWatcher.getCardTypesCast(sourceAbility.getControllerId(), game).size();
}
@Override
public AprilONeilHacktivistValue copy() {
return this;
}
@Override
public String getMessage() {
return "card type among spells you've cast this turn";
}
@Override
public String toString() {
return "1";
}
}
enum AprilONeilHacktivistHint implements Hint {
instance;
@Override
public String getText(Game game, Ability ability) {
List<String> types = AprilONeilHacktivistWatcher
.getCardTypesCast(ability.getControllerId(), game)
.stream()
.map(CardType::toString)
.sorted()
.collect(Collectors.toList());
return "Card types among spells you've cast this turn: " + types.size()
+ (types.size() > 0 ? " (" + String.join(", ", types) + ')' : "");
}
@Override
public Hint copy() {
return this;
}
}
class AprilONeilHacktivistWatcher extends Watcher {
private final Map<UUID, Set<CardType>> map = new HashMap<>();
AprilONeilHacktivistWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() != GameEvent.EventType.SPELL_CAST) {
return;
}
Spell spell = game.getSpell(event.getTargetId());
if (spell != null) {
map.computeIfAbsent(spell.getControllerId(), x -> new HashSet<>()).addAll(spell.getCardType(game));
}
}
@Override
public void reset() {
map.clear();
super.reset();
}
static Set<CardType> getCardTypesCast(UUID playerId, Game game) {
return game
.getState()
.getWatcher(AprilONeilHacktivistWatcher.class)
.map
.getOrDefault(playerId, Collections.emptySet());
}
}

View file

@ -10,14 +10,10 @@ import mage.abilities.dynamicvalue.common.CountersControllerCount;
import mage.abilities.effects.common.DamagePlayersEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.discard.DiscardControllerEffect;
import mage.abilities.keyword.PartnerFatherAndSonAbility;
import mage.abilities.keyword.ReachAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.TargetController;
import mage.constants.*;
import mage.counters.CounterType;
import java.util.UUID;
@ -50,7 +46,7 @@ public final class AtreusImpulsiveSon extends CardImpl {
this.addAbility(ability);
// Partner--Father & son
this.addAbility(PartnerFatherAndSonAbility.getInstance());
this.addAbility(PartnerVariantType.FATHER_AND_SON.makeAbility());
}
private AtreusImpulsiveSon(final AtreusImpulsiveSon card) {

View file

@ -0,0 +1,47 @@
package mage.cards.b;
import mage.MageInt;
import mage.abilities.common.AttacksOrBlocksTriggeredAbility;
import mage.abilities.costs.common.DiscardCardCost;
import mage.abilities.effects.common.DoIfCostPaid;
import mage.abilities.effects.common.SacrificeControllerEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class BebopAndRocksteady extends CardImpl {
public BebopAndRocksteady(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B/G}{B/G}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.BOAR);
this.subtype.add(SubType.RHINO);
this.subtype.add(SubType.MUTANT);
this.power = new MageInt(7);
this.toughness = new MageInt(5);
// Whenever Bebop & Rocksteady attacks or blocks, sacrifice a permanent unless you discard a card.
this.addAbility(new AttacksOrBlocksTriggeredAbility(new DoIfCostPaid(
null, new SacrificeControllerEffect(StaticFilters.FILTER_PERMANENT, 1, null),
new DiscardCardCost(), false), false
));
}
private BebopAndRocksteady(final BebopAndRocksteady card) {
super(card);
}
@Override
public BebopAndRocksteady copy() {
return new BebopAndRocksteady(this);
}
}

View file

@ -1,7 +1,5 @@
package mage.cards.b;
import java.util.UUID;
import mage.MageInt;
import mage.ObjectColor;
import mage.abilities.common.SimpleStaticAbility;
@ -12,17 +10,19 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.ColorPredicate;
import java.util.UUID;
/**
*
* @author North
*/
public final class BloodmarkMentor extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent("Red creatures");
private static final FilterPermanent filter = new FilterCreaturePermanent("Red creatures");
static {
filter.add(new ColorPredicate(ObjectColor.RED));

View file

@ -0,0 +1,59 @@
package mage.cards.b;
import java.util.UUID;
import mage.abilities.common.DealsDamageToAPlayerAllTriggeredAbility;
import mage.abilities.common.UnlockThisDoorTriggeredAbility;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.ReturnToHandTargetEffect;
import mage.cards.CardSetInfo;
import mage.cards.RoomCard;
import mage.constants.CardType;
import mage.constants.SetTargetPointer;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.constants.TargetController;
import mage.filter.StaticFilters;
import mage.target.common.TargetCreaturePermanent;
/**
* @author oscscull
*/
public final class BottomlessPoolLockerRoom extends RoomCard {
public BottomlessPoolLockerRoom(UUID ownerId, CardSetInfo setInfo) {
// Bottomless Pool
// {U}
// When you unlock this door, return up to one target creature to its owners hand.
// Locker Room
// {4}{U}
// Enchantment -- Room
// Whenever one or more creatures you control deal combat damage to a player, draw a card.
super(ownerId, setInfo,
new CardType[] { CardType.ENCHANTMENT },
"{U}", "{4}{U}", SpellAbilityType.SPLIT);
this.subtype.add(SubType.ROOM);
// Left half ability - "When you unlock this door, return up to one target creature to its owners hand."
UnlockThisDoorTriggeredAbility left = new UnlockThisDoorTriggeredAbility(
new ReturnToHandTargetEffect(), false, true);
left.addTarget(new TargetCreaturePermanent(0, 1));
// Right half ability - "Whenever one or more creatures you control deal combat damage to a player, draw a card."
DealsDamageToAPlayerAllTriggeredAbility right = new DealsDamageToAPlayerAllTriggeredAbility(
new DrawCardSourceControllerEffect(1),
StaticFilters.FILTER_CONTROLLED_A_CREATURE,
false, SetTargetPointer.PLAYER, true, true, TargetController.OPPONENT);
this.addRoomAbilities(left, right);
}
private BottomlessPoolLockerRoom(final BottomlessPoolLockerRoom card) {
super(card);
}
@Override
public BottomlessPoolLockerRoom copy() {
return new BottomlessPoolLockerRoom(this);
}
}

View file

@ -0,0 +1,48 @@
package mage.cards.c;
import mage.MageInt;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.LookLibraryAndPickControllerEffect;
import mage.abilities.keyword.HasteAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.PutCards;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class CaseyJonesJuryRigJusticiar extends CardImpl {
public CaseyJonesJuryRigJusticiar(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.BERSERKER);
this.power = new MageInt(2);
this.toughness = new MageInt(1);
// Haste
this.addAbility(HasteAbility.getInstance());
// When Casey Jones enters, look at the top four cards of your library. You may reveal an artifact card from among them and put it into your hand. Put the rest on the bottom of your library in a random order.
this.addAbility(new EntersBattlefieldTriggeredAbility(new LookLibraryAndPickControllerEffect(
4, 1, StaticFilters.FILTER_CARD_ARTIFACT_AN, PutCards.HAND, PutCards.BOTTOM_RANDOM
)));
}
private CaseyJonesJuryRigJusticiar(final CaseyJonesJuryRigJusticiar card) {
super(card);
}
@Override
public CaseyJonesJuryRigJusticiar copy() {
return new CaseyJonesJuryRigJusticiar(this);
}
}

View file

@ -0,0 +1,53 @@
package mage.cards.d;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.continuous.GainAbilityControlledSpellsEffect;
import mage.abilities.effects.common.continuous.UntapAllDuringEachOtherPlayersUntapStepEffect;
import mage.abilities.keyword.ConvokeAbility;
import mage.cards.CardSetInfo;
import mage.cards.RoomCard;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import mage.filter.common.FilterNonlandCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.AbilityPredicate;
import java.util.UUID;
/**
* @author PurpleCrowbar
*/
public final class DazzlingTheaterPropRoom extends RoomCard {
private static final FilterNonlandCard filter = new FilterNonlandCard("creature spells you cast");
static {
filter.add(CardType.CREATURE.getPredicate());
filter.add(Predicates.not(new AbilityPredicate(ConvokeAbility.class)));
}
public DazzlingTheaterPropRoom(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{W}", "{2}{W}", SpellAbilityType.SPLIT);
this.subtype.add(SubType.ROOM);
// Dazzling Theater: Creature spells you cast have convoke.
Ability left = new SimpleStaticAbility(new GainAbilityControlledSpellsEffect(new ConvokeAbility(), filter));
// Prop Room: Untap each creature you control during each other player's untap step.
Ability right = new SimpleStaticAbility(new UntapAllDuringEachOtherPlayersUntapStepEffect(StaticFilters.FILTER_CONTROLLED_CREATURES));
this.addRoomAbilities(left, right);
}
private DazzlingTheaterPropRoom(final DazzlingTheaterPropRoom card) {
super(card);
}
@Override
public DazzlingTheaterPropRoom copy() {
return new DazzlingTheaterPropRoom(this);
}
}

View file

@ -0,0 +1,54 @@
package mage.cards.d;
import mage.abilities.Ability;
import mage.abilities.common.AttacksPlayerWithCreaturesTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.common.CreaturesYouControlCount;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.continuous.SetBasePowerToughnessAllEffect;
import mage.abilities.hint.ValueHint;
import mage.cards.CardSetInfo;
import mage.cards.RoomCard;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.Predicates;
import mage.game.permanent.token.ToyToken;
import java.util.UUID;
/**
* @author PurpleCrowbar
*/
public final class DollmakersShopPorcelainGallery extends RoomCard {
private static final FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent("non-Toy creatures you control");
static {
filter.add(Predicates.not(SubType.TOY.getPredicate()));
}
public DollmakersShopPorcelainGallery(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{W}", "{4}{W}{W}", SpellAbilityType.SPLIT);
this.subtype.add(SubType.ROOM);
// Dollmaker's Shop: Whenever one or more non-Toy creatures you control attack a player, create a 1/1 white Toy artifact creature token.
Ability left = new AttacksPlayerWithCreaturesTriggeredAbility(new CreateTokenEffect(new ToyToken()), filter, SetTargetPointer.NONE);
// Porcelain Gallery: Creatures you control have base power and toughness each equal to the number of creatures you control.
Ability right = new SimpleStaticAbility(new SetBasePowerToughnessAllEffect(
CreaturesYouControlCount.PLURAL, Duration.WhileOnBattlefield, StaticFilters.FILTER_CONTROLLED_CREATURES
).setText("Creatures you control have base power and toughness each equal to the number of creatures you control"));
this.addRoomAbilities(left, right.addHint(new ValueHint("Creatures you control", CreaturesYouControlCount.PLURAL)));
}
private DollmakersShopPorcelainGallery (final DollmakersShopPorcelainGallery card) {
super(card);
}
@Override
public DollmakersShopPorcelainGallery copy() {
return new DollmakersShopPorcelainGallery(this);
}
}

View file

@ -0,0 +1,52 @@
package mage.cards.d;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.TapTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.keyword.VigilanceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.counters.CounterType;
import mage.target.common.TargetOpponentsCreaturePermanent;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class DonatelloRadScientist extends CardImpl {
public DonatelloRadScientist(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.TURTLE);
this.power = new MageInt(5);
this.toughness = new MageInt(6);
// Vigilance
this.addAbility(VigilanceAbility.getInstance());
// When Donatello enters, tap up to three target creatures your opponents control. Put a stun counter on each of them.
Ability ability = new EntersBattlefieldTriggeredAbility(new TapTargetEffect());
ability.addEffect(new AddCountersTargetEffect(CounterType.STUN.createInstance()).setText("Put a stun counter on each of them"));
ability.addTarget(new TargetOpponentsCreaturePermanent(0, 3));
this.addAbility(ability);
}
private DonatelloRadScientist(final DonatelloRadScientist card) {
super(card);
}
@Override
public DonatelloRadScientist copy() {
return new DonatelloRadScientist(this);
}
}

View file

@ -0,0 +1,89 @@
package mage.cards.d;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.CreateTokenEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.token.MutagenToken;
import mage.game.permanent.token.Token;
import mage.util.CardUtil;
import java.util.Map;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class DonatelloTheBrains extends CardImpl {
public DonatelloTheBrains(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.TURTLE);
this.power = new MageInt(2);
this.toughness = new MageInt(4);
// If one or more tokens would be created under your control, those tokens plus a Mutagen token are created instead.
this.addAbility(new SimpleStaticAbility(new DonatelloTheBrainsReplacementEffect()));
// Partner--Character select
this.addAbility(PartnerVariantType.CHARACTER_SELECT.makeAbility());
}
private DonatelloTheBrains(final DonatelloTheBrains card) {
super(card);
}
@Override
public DonatelloTheBrains copy() {
return new DonatelloTheBrains(this);
}
}
class DonatelloTheBrainsReplacementEffect extends ReplacementEffectImpl {
DonatelloTheBrainsReplacementEffect() {
super(Duration.WhileOnBattlefield, Outcome.Benefit);
this.staticText = "if one or more tokens would be created under your control, " +
"those tokens plus a Mutagen token are created instead";
}
private DonatelloTheBrainsReplacementEffect(final DonatelloTheBrainsReplacementEffect effect) {
super(effect);
}
@Override
public DonatelloTheBrainsReplacementEffect copy() {
return new DonatelloTheBrainsReplacementEffect(this);
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.CREATE_TOKEN;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
return source.isControlledBy(event.getPlayerId());
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
Map<Token, Integer> tokens = ((CreateTokenEvent) event).getTokens();
Token token = CardUtil
.castStream(tokens.values(), MutagenToken.class)
.findAny()
.orElseGet(MutagenToken::new);
tokens.compute(token, CardUtil::setOrIncrementValue);
return false;
}
}

View file

@ -0,0 +1,112 @@
package mage.cards.d;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.DrawCardTargetEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.FilterPlayer;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.other.AnotherTargetPredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.TargetPlayer;
import mage.target.common.TargetCardInYourGraveyard;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class DonnieAndAprilAdorkableDuo extends CardImpl {
private static final FilterPlayer filter0 = new FilterPlayer("a different player");
private static final FilterPlayer filter1 = new FilterPlayer();
private static final FilterPlayer filter2 = new FilterPlayer();
static {
filter1.add(new AnotherTargetPredicate(1, true));
filter2.add(new AnotherTargetPredicate(2, true));
}
public DonnieAndAprilAdorkableDuo(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.TURTLE);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// When Donnie & April enter, choose one or both. Each mode must target a different player.
// * Target player draws two cards.
Ability ability = new EntersBattlefieldTriggeredAbility(new DrawCardTargetEffect(2));
ability.addTarget(new TargetPlayer(filter1).withChooseHint("to draw a card"));
ability.getModes().setMinModes(1);
ability.getModes().setMaxModes(2);
ability.getModes().setLimitUsageByOnce(false);
ability.getModes().setMaxModesFilter(filter0);
// * Target player returns an artifact, instant, or sorcery card from their graveyard to their hand.
ability.addMode(new Mode(new DonnieAndAprilAdorkableDuoEffect())
.addTarget(new TargetPlayer(filter2).withChooseHint("to return a card from their graveyard to their hand")));
this.addAbility(ability);
}
private DonnieAndAprilAdorkableDuo(final DonnieAndAprilAdorkableDuo card) {
super(card);
}
@Override
public DonnieAndAprilAdorkableDuo copy() {
return new DonnieAndAprilAdorkableDuo(this);
}
}
class DonnieAndAprilAdorkableDuoEffect extends OneShotEffect {
private static final FilterCard filter = new FilterCard("artifact, instant, or sorcery card");
static {
filter.add(Predicates.or(
CardType.ARTIFACT.getPredicate(),
CardType.INSTANT.getPredicate(),
CardType.SORCERY.getPredicate()
));
}
DonnieAndAprilAdorkableDuoEffect() {
super(Outcome.Benefit);
staticText = "target player returns an artifact, instant, or sorcery card from their graveyard to their hand";
}
private DonnieAndAprilAdorkableDuoEffect(final DonnieAndAprilAdorkableDuoEffect effect) {
super(effect);
}
@Override
public DonnieAndAprilAdorkableDuoEffect copy() {
return new DonnieAndAprilAdorkableDuoEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(getTargetPointer().getFirst(game, source));
if (player == null || player.getGraveyard().count(filter, game) < 1) {
return false;
}
TargetCard target = new TargetCardInYourGraveyard(filter);
player.choose(Outcome.ReturnToHand, player.getGraveyard(), target, source, game);
Card card = game.getCard(target.getFirstTarget());
return card != null && player.moveCards(card, Zone.HAND, source, game);
}
}

View file

@ -13,6 +13,7 @@ import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetPlayer;
@ -42,7 +43,8 @@ public final class ElectroAssaultingBattery extends CardImpl {
this.addAbility(new SimpleStaticAbility(new YouDontLoseManaEffect(ManaType.RED)));
// Whenever you cast an instant or sorcery spell, add {R}.
this.addAbility(new SpellCastControllerTriggeredAbility(new AddManaToManaPoolSourceControllerEffect(Mana.RedMana(1)), false));
this.addAbility(new SpellCastControllerTriggeredAbility(new AddManaToManaPoolSourceControllerEffect(Mana.RedMana(1)),
StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false));
// When Electro leaves the battlefield, you may pay x. When you do, he deals X damage to target player.
this.addAbility(new LeavesBattlefieldTriggeredAbility(new ElectroAssaultingBatteryEffect()));

View file

@ -4,7 +4,6 @@ import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.PartnerSurvivorsAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
@ -34,7 +33,7 @@ public final class EllieBrickMaster extends CardImpl {
this.addAbility(new EllieBrickMasterTriggeredAbility());
// Partner--Survivors
this.addAbility(PartnerSurvivorsAbility.getInstance());
this.addAbility(PartnerVariantType.SURVIVORS.makeAbility());
}
private EllieBrickMaster(final EllieBrickMaster card) {

View file

@ -8,13 +8,9 @@ import mage.abilities.costs.common.SacrificeTargetCost;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilitySourceEffect;
import mage.abilities.keyword.IndestructibleAbility;
import mage.abilities.keyword.PartnerSurvivorsAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.target.TargetPlayer;
@ -44,7 +40,7 @@ public final class EllieVengefulHunter extends CardImpl {
this.addAbility(ability);
// Partner--Survivors
this.addAbility(PartnerSurvivorsAbility.getInstance());
this.addAbility(PartnerVariantType.SURVIVORS.makeAbility());
}
private EllieVengefulHunter(final EllieVengefulHunter card) {

View file

@ -10,12 +10,11 @@ import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import java.util.Optional;
import java.util.UUID;
/**
@ -70,14 +69,14 @@ class FallersFaithfulEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
boolean flag = permanent.getDealtDamageByThisTurn().isEmpty();
boolean notDamagedThisTurn = permanent.getDealtDamageByThisTurn().isEmpty();
permanent.destroy(source, game);
game.processAction();
if (!flag) {
Optional.ofNullable(permanent)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.ifPresent(player -> player.drawCards(2, source, game));
if (notDamagedThisTurn) {
game.processAction();
Player player = game.getPlayer(permanent.getControllerId());
if (player != null) {
player.drawCards(2, source, game);
}
}
return true;
}

View file

@ -0,0 +1,86 @@
package mage.cards.f;
import mage.abilities.Ability;
import mage.abilities.common.DealsDamageToAPlayerAttachedTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.EquipAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.token.FishNoAbilityToken;
import mage.players.Player;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class FishingGear extends CardImpl {
public FishingGear(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}");
this.subtype.add(SubType.EQUIPMENT);
// Whenever equipped creature deals combat damage to a player, exile the top card of that player's library. If it's a permanent card, you may put it onto the battlefield under your control. If you don't, create a 1/1 blue Fish creature token.
this.addAbility(new DealsDamageToAPlayerAttachedTriggeredAbility(
new FishingGearEffect(), "equipped", false, true, true
));
// Equip {2}
this.addAbility(new EquipAbility(2));
}
private FishingGear(final FishingGear card) {
super(card);
}
@Override
public FishingGear copy() {
return new FishingGear(this);
}
}
class FishingGearEffect extends OneShotEffect {
FishingGearEffect() {
super(Outcome.Benefit);
staticText = "exile the top card of that player's library. " +
"If it's a permanent card, you may put it onto the battlefield under your control. " +
"If you don't, create a 1/1 blue Fish creature token";
}
private FishingGearEffect(final FishingGearEffect effect) {
super(effect);
}
@Override
public FishingGearEffect copy() {
return new FishingGearEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
Player player = game.getPlayer(getTargetPointer().getFirst(game, source));
if (controller == null || player == null) {
return false;
}
Card card = player.getLibrary().getFromTop(game);
if (card == null) {
return false;
}
controller.moveCards(card, Zone.EXILED, source, game);
if (card.isPermanent(game) && controller.chooseUse(outcome, "Put it onto the battlefield?", source, game)) {
controller.moveCards(card, Zone.BATTLEFIELD, source, game);
} else {
new FishNoAbilityToken().putOntoBattlefield(1, game, source);
}
return true;
}
}

View file

@ -0,0 +1,50 @@
package mage.cards.f;
import mage.abilities.Ability;
import mage.abilities.common.DiesCreatureTriggeredAbility;
import mage.abilities.common.UnlockThisDoorTriggeredAbility;
import mage.abilities.effects.common.GainLifeEffect;
import mage.abilities.effects.common.LoseLifeOpponentsEffect;
import mage.abilities.effects.common.ReturnFromYourGraveyardToBattlefieldAllEffect;
import mage.cards.CardSetInfo;
import mage.cards.RoomCard;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author PurpleCrowbar
*/
public final class FuneralRoomAwakeningHall extends RoomCard {
public FuneralRoomAwakeningHall(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}", "{6}{B}{B}", SpellAbilityType.SPLIT);
this.subtype.add(SubType.ROOM);
// Funeral Room: Whenever a creature you control dies, each opponent loses 1 life and you gain 1 life.
Ability left = new DiesCreatureTriggeredAbility(
new LoseLifeOpponentsEffect(1), false,
StaticFilters.FILTER_CONTROLLED_A_CREATURE
);
left.addEffect(new GainLifeEffect(1).concatBy("and"));
// Awakening Hall: When you unlock this door, return all creature cards from your graveyard to the battlefield.
Ability right = new UnlockThisDoorTriggeredAbility(
new ReturnFromYourGraveyardToBattlefieldAllEffect(StaticFilters.FILTER_CARD_CREATURES), false, false
);
this.addRoomAbilities(left, right);
}
private FuneralRoomAwakeningHall(final FuneralRoomAwakeningHall card) {
super(card);
}
@Override
public FuneralRoomAwakeningHall copy() {
return new FuneralRoomAwakeningHall(this);
}
}

View file

@ -2,8 +2,7 @@ package mage.cards.g;
import mage.abilities.Ability;
import mage.abilities.common.SagaAbility;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.dynamicvalue.common.CountersSourceCount;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DestroyAllEffect;
@ -19,7 +18,6 @@ import mage.filter.predicate.Predicates;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.DalekToken;
import mage.players.Player;
import mage.target.common.TargetOpponent;
@ -44,7 +42,7 @@ public final class GenesisOfTheDaleks extends CardImpl {
// I, II, III -- Create a 3/3 black Dalek artifact creature token with menace for each lore counter on Genesis of the Daleks.
sagaAbility.addChapterEffect(
this, SagaChapter.CHAPTER_I, SagaChapter.CHAPTER_III,
new CreateTokenEffect(new DalekToken(), GenesisOfTheDaleksValue.instance)
new CreateTokenEffect(new DalekToken(), new CountersSourceCount(CounterType.LORE))
);
// IV -- Target opponent faces a villainous choice -- Destroy all Dalek creatures and each of your opponents loses life equal to the total power of Daleks that died this turn, or destroy all non-Dalek creatures.
@ -65,41 +63,6 @@ public final class GenesisOfTheDaleks extends CardImpl {
}
}
enum GenesisOfTheDaleksValue implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
Permanent permanent = sourceAbility.getSourcePermanentOrLKI(game);
if (permanent != null) {
return permanent
.getCounters(game)
.getCount(CounterType.LORE);
}
return Optional
.ofNullable(sourceAbility)
.map(Ability::getSourceId)
.map(game::getPermanentOrLKIBattlefield)
.map(p -> p.getCounters(game).getCount(CounterType.LORE))
.orElse(0);
}
@Override
public GenesisOfTheDaleksValue copy() {
return this;
}
@Override
public String getMessage() {
return "lore counter on {this}";
}
@Override
public String toString() {
return "1";
}
}
class GenesisOfTheDaleksEffect extends OneShotEffect {
private static final FaceVillainousChoice choice = new FaceVillainousChoice(

View file

@ -21,6 +21,7 @@ import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
@ -81,11 +82,11 @@ class GlamerSpinnersEffect extends OneShotEffect {
Permanent targetPermanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (targetPermanent == null
|| targetPermanent
.getAttachments()
.stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.noneMatch(p -> p.hasSubtype(SubType.AURA, game))) {
.getAttachments()
.stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.noneMatch(p -> p.hasSubtype(SubType.AURA, game))) {
return false;
}
FilterPermanent filter = new FilterPermanent(
@ -94,7 +95,7 @@ class GlamerSpinnersEffect extends OneShotEffect {
.map(Controllable::getControllerId)
.map(game::getPlayer)
.map(Player::getName)
.map(s -> " controlled by" + s)
.map(s -> " controlled by " + s)
.orElse("")
);
filter.add(new ControllerIdPredicate(targetPermanent.getControllerId()));
@ -102,18 +103,23 @@ class GlamerSpinnersEffect extends OneShotEffect {
if (!game.getBattlefield().contains(filter, source.getControllerId(), source, game, 1)) {
return false;
}
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
TargetPermanent target = new TargetPermanent(filter);
target.withNotTarget(true);
Optional.ofNullable(source)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.ifPresent(player -> player.choose(outcome, target, source, game));
player.choose(Outcome.AIDontUseIt, target, source, game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent == null) {
return false;
}
for (UUID attachmentId : targetPermanent.getAttachments()) {
permanent.addAttachment(attachmentId, source, game);
// new list to avoid concurrent modification
for (UUID attachmentId : new LinkedList<>(targetPermanent.getAttachments())) {
Permanent attachment = game.getPermanent(attachmentId);
if (attachment != null && attachment.hasSubtype(SubType.AURA, game)) {
permanent.addAttachment(attachmentId, source, game);
}
}
return true;
}

View file

@ -0,0 +1,80 @@
package mage.cards.h;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.OneOrMoreCombatDamagePlayerTriggeredAbility;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.MenaceAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.abilities.keyword.VigilanceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SetTargetPointer;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.Predicates;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class HeroesInAHalfShell extends CardImpl {
private static final FilterPermanent filter = new FilterControlledPermanent("Mutants, Ninjas, and/or Turtles you control");
static {
filter.add(Predicates.or(
SubType.MUTANT.getPredicate(),
SubType.NINJA.getPredicate(),
SubType.TURTLE.getPredicate()
));
}
public HeroesInAHalfShell(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}{U}{B}{R}{G}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.TURTLE);
this.power = new MageInt(5);
this.toughness = new MageInt(5);
// Vigilance
this.addAbility(VigilanceAbility.getInstance());
// Menace
this.addAbility(new MenaceAbility());
// Trample
this.addAbility(TrampleAbility.getInstance());
// Haste
this.addAbility(HasteAbility.getInstance());
// Whenever one or more Mutants, Ninjas, and/or Turtles you control deal combat damage to a player, put a +1/+1 counter on each of those creatures and draw a card.
Ability ability = new OneOrMoreCombatDamagePlayerTriggeredAbility(
new AddCountersTargetEffect(CounterType.P1P1.createInstance())
.setText("put a +1/+1 counter on each of those creatures"),
SetTargetPointer.PERMANENT, filter, false
);
ability.addEffect(new DrawCardSourceControllerEffect(1).concatBy("and"));
this.addAbility(ability);
}
private HeroesInAHalfShell(final HeroesInAHalfShell card) {
super(card);
}
@Override
public HeroesInAHalfShell copy() {
return new HeroesInAHalfShell(this);
}
}

View file

@ -6,10 +6,10 @@ import mage.abilities.common.DiesCreatureTriggeredAbility;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.MenaceAbility;
import mage.abilities.keyword.PartnerSurvivorsAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.PartnerVariantType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.counters.CounterType;
@ -43,7 +43,7 @@ public final class JoelResoluteSurvivor extends CardImpl {
this.addAbility(ability);
// Partner--Survivors
this.addAbility(PartnerSurvivorsAbility.getInstance());
this.addAbility(PartnerVariantType.SURVIVORS.makeAbility());
}
private JoelResoluteSurvivor(final JoelResoluteSurvivor card) {

View file

@ -0,0 +1,67 @@
package mage.cards.k;
import mage.MageInt;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.CardsInHandCondition;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.common.DrawCardsEqualToDifferenceEffect;
import mage.abilities.effects.common.continuous.BoostSourceEffect;
import mage.abilities.keyword.AffinityForArtifactsAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledArtifactPermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class KrangMasterMind extends CardImpl {
private static final Condition condition = new CardsInHandCondition(ComparisonType.FEWER_THAN, 4);
private static final FilterPermanent filter = new FilterControlledArtifactPermanent("other artifact you control");
static {
filter.add(AnotherPredicate.instance);
}
private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter);
public KrangMasterMind(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{6}{U}{U}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.UTROM);
this.subtype.add(SubType.WARRIOR);
this.power = new MageInt(1);
this.toughness = new MageInt(4);
// Affinity for artifacts
this.addAbility(new AffinityForArtifactsAbility());
// When Krang enters, if you have fewer than four cards in hand, draw cards equal to the difference.
this.addAbility(new EntersBattlefieldTriggeredAbility(new DrawCardsEqualToDifferenceEffect(4))
.withInterveningIf(condition));
// Krang gets +1/+0 for each other artifact you control.
this.addAbility(new SimpleStaticAbility(new BoostSourceEffect(
xValue, StaticValue.get(0), Duration.WhileOnBattlefield
)));
}
private KrangMasterMind(final KrangMasterMind card) {
super(card);
}
@Override
public KrangMasterMind copy() {
return new KrangMasterMind(this);
}
}

View file

@ -39,10 +39,13 @@ public final class KratosGodOfWar extends CardImpl {
this.addAbility(DoubleStrikeAbility.getInstance());
// All creatures have haste.
this.addAbility(new SimpleStaticAbility(new GainAbilityAllEffect(
HasteAbility.getInstance(), Duration.WhileControlled,
StaticFilters.FILTER_PERMANENT_CREATURE
).setText("all creatures have haste")));
this.addAbility(
new SimpleStaticAbility(
new GainAbilityAllEffect(
HasteAbility.getInstance(), Duration.WhileOnBattlefield,
StaticFilters.FILTER_PERMANENT_CREATURES
).setText("all creatures have haste")
));
// At the beginning of each player's end step, Kratos deals damage to that player equal to the number of creatures that player controls that didn't attack this turn.
this.addAbility(new BeginningOfEndStepTriggeredAbility(
@ -89,7 +92,7 @@ class KratosGodOfWarEffect extends OneShotEffect {
if (player == null) {
return false;
}
int count = game.getBattlefield().count(filter, source.getControllerId(), source, game);
int count = game.getBattlefield().count(filter, game.getActivePlayerId(), source, game);
return count > 0 && player.damage(count, source, game) > 0;
}
}

View file

@ -0,0 +1,73 @@
package mage.cards.l;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.continuous.GainAbilityAllEffect;
import mage.abilities.effects.common.counter.AddCountersAllEffect;
import mage.abilities.keyword.LifelinkAbility;
import mage.abilities.keyword.MenaceAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class LeonardoTheBalance extends CardImpl {
public LeonardoTheBalance(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.TURTLE);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// Whenever a token you control enters, you may put a +1/+1 counter on each creature you control. Do this only once each turn.
this.addAbility(new EntersBattlefieldControlledTriggeredAbility(
new AddCountersAllEffect(
CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE
), StaticFilters.FILTER_PERMANENT_TOKEN
).setDoOnlyOnceEachTurn(true));
// {W}{U}{B}{R}{G}: Creatures you control gain menace, trample, and lifelink until end of turn.
Ability ability = new SimpleActivatedAbility(
new GainAbilityAllEffect(
new MenaceAbility(false), Duration.EndOfTurn,
StaticFilters.FILTER_CONTROLLED_CREATURE
).setText("creatures you control gain menace"),
new ManaCostsImpl<>("{W}{U}{B}{R}{G}")
);
ability.addEffect(new GainAbilityAllEffect(
TrampleAbility.getInstance(), Duration.EndOfTurn,
StaticFilters.FILTER_CONTROLLED_CREATURE
).setText(", trample"));
ability.addEffect(new GainAbilityAllEffect(
LifelinkAbility.getInstance(), Duration.EndOfTurn,
StaticFilters.FILTER_CONTROLLED_CREATURE
).setText(", and lifelink until end of turn"));
this.addAbility(ability);
// Partner--Character select
this.addAbility(PartnerVariantType.CHARACTER_SELECT.makeAbility());
}
private LeonardoTheBalance(final LeonardoTheBalance card) {
super(card);
}
@Override
public LeonardoTheBalance copy() {
return new LeonardoTheBalance(this);
}
}

View file

@ -0,0 +1,50 @@
package mage.cards.l;
import mage.MageInt;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.common.CreaturesYouControlCount;
import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect;
import mage.abilities.hint.common.CreaturesYouControlHint;
import mage.abilities.keyword.DoubleStrikeAbility;
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 java.util.UUID;
/**
* @author TheElk801
*/
public final class LeonardoWorldlyWarrior extends CardImpl {
public LeonardoWorldlyWarrior(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{7}{W}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.TURTLE);
this.power = new MageInt(5);
this.toughness = new MageInt(5);
// This spell costs {1} less to cast for each creature you control.
this.addAbility(new SimpleStaticAbility(
Zone.ALL, new SpellCostReductionForEachSourceEffect(1, CreaturesYouControlCount.SINGULAR)
).addHint(CreaturesYouControlHint.instance));
// Double strike
this.addAbility(DoubleStrikeAbility.getInstance());
}
private LeonardoWorldlyWarrior(final LeonardoWorldlyWarrior card) {
super(card);
}
@Override
public LeonardoWorldlyWarrior copy() {
return new LeonardoWorldlyWarrior(this);
}
}

View file

@ -53,8 +53,8 @@ public final class LongListOfTheEnts extends CardImpl {
return new LongListOfTheEnts(this);
}
static String getKey(Game game, Ability source, int offset) {
return "EntList_" + source.getSourceId() + "_" + (offset + CardUtil.getActualSourceObjectZoneChangeCounter(game, source));
static String getKey(Game game, Ability source) {
return "EntList_" + source.getSourceId() + "_" + CardUtil.getActualSourceObjectZoneChangeCounter(game, source);
}
}
@ -67,7 +67,7 @@ enum LongListOfTheEntsHint implements Hint {
if (ability.getSourcePermanentIfItStillExists(game) == null) {
return null;
}
Set<SubType> subTypes = (Set<SubType>) game.getState().getValue(LongListOfTheEnts.getKey(game, ability, 0));
Set<SubType> subTypes = (Set<SubType>) game.getState().getValue(LongListOfTheEnts.getKey(game, ability));
if (subTypes == null || subTypes.isEmpty()) {
return "No creature types have been noted yet.";
}
@ -109,14 +109,11 @@ class LongListOfTheEntsEffect extends OneShotEffect {
return false;
}
Object existingEntList = game.getState().getValue(LongListOfTheEnts.getKey(game, source, 0));
int offset;
Object existingEntList = game.getState().getValue(LongListOfTheEnts.getKey(game, source));
Set<SubType> newEntList;
if (existingEntList == null) {
offset = 1; // zcc is off-by-one due to still entering battlefield
newEntList = new LinkedHashSet<>();
} else {
offset = 0;
newEntList = new LinkedHashSet<>((Set<SubType>) existingEntList);
}
Set<String> chosenTypes = newEntList
@ -132,7 +129,7 @@ class LongListOfTheEntsEffect extends OneShotEffect {
SubType subType = SubType.byDescription(choice.getChoiceKey());
game.informPlayers(player.getLogName() + " notes the creature type " + subType);
newEntList.add(subType);
game.getState().setValue(LongListOfTheEnts.getKey(game, source, offset), newEntList);
game.getState().setValue(LongListOfTheEnts.getKey(game, source), newEntList);
FilterSpell filter = new FilterCreatureSpell("a creature spell of that type");
filter.add(subType.getPredicate());
game.addDelayedTriggeredAbility(new AddCounterNextSpellDelayedTriggeredAbility(filter), source);

View file

@ -0,0 +1,53 @@
package mage.cards.m;
import mage.abilities.condition.common.KickedCondition;
import mage.abilities.decorator.ConditionalOneShotEffect;
import mage.abilities.dynamicvalue.common.GreatestAmongPermanentsValue;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.keyword.KickerAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.game.permanent.token.UtvaraHellkiteDragonToken;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetadjustment.ForEachPlayerTargetsAdjuster;
import mage.target.targetpointer.EachTargetPointer;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class MegaFlare extends CardImpl {
public MegaFlare(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{R}");
// Kicker {3}{R}{R}
this.addAbility(new KickerAbility("{3}{R}{R}"));
// If this spell was kicked, create a 6/6 red Dragon creature token with flying.
this.getSpellAbility().addEffect(new ConditionalOneShotEffect(
new CreateTokenEffect(new UtvaraHellkiteDragonToken()), KickedCondition.ONCE,
"if this spell was kicked, create a 6/6 red Dragon creature token with flying"
));
// For each opponent, choose up to one target creature that player controls. Mega Flare deals damage equal to the greatest power among creatures you control to each of the chosen creatures.
this.getSpellAbility().addEffect(new DamageTargetEffect(GreatestAmongPermanentsValue.POWER_CONTROLLED_CREATURES)
.setText("<br>For each opponent, choose up to one target creature that player controls. " +
"{this} deals damage equal to the greatest power among creatures you control " +
"to each of the chosen creatures").setTargetPointer(new EachTargetPointer()));
this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 1));
this.getSpellAbility().setTargetAdjuster(new ForEachPlayerTargetsAdjuster(false, true));
}
private MegaFlare(final MegaFlare card) {
super(card);
}
@Override
public MegaFlare copy() {
return new MegaFlare(this);
}
}

View file

@ -0,0 +1,62 @@
package mage.cards.m;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.condition.common.RaidCondition;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.hint.common.RaidHint;
import mage.abilities.keyword.TrampleAbility;
import mage.abilities.triggers.BeginningOfSecondMainTriggeredAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.PartnerVariantType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.counters.CounterType;
import mage.game.permanent.token.FoodToken;
import mage.target.common.TargetCreaturePermanent;
import mage.watchers.common.PlayerAttackedWatcher;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class MichelangeloTheHeart extends CardImpl {
public MichelangeloTheHeart(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.TURTLE);
this.power = new MageInt(2);
this.toughness = new MageInt(1);
// Trample
this.addAbility(TrampleAbility.getInstance());
// Raid (the Fridge) -- At the beginning of your second main phase, if you attacked this turn, put a +1/+1 counter on target creature and create a Food token.
Ability ability = new BeginningOfSecondMainTriggeredAbility(
new AddCountersTargetEffect(CounterType.P1P1.createInstance()), false
).withInterveningIf(RaidCondition.instance);
ability.addEffect(new CreateTokenEffect(new FoodToken()).concatBy("and"));
ability.addTarget(new TargetCreaturePermanent());
this.addAbility(ability.addHint(RaidHint.instance).withFlavorWord("Raid (the Fridge)"), new PlayerAttackedWatcher());
// Partner--Character select
this.addAbility(PartnerVariantType.CHARACTER_SELECT.makeAbility());
}
private MichelangeloTheHeart(final MichelangeloTheHeart card) {
super(card);
}
@Override
public MichelangeloTheHeart copy() {
return new MichelangeloTheHeart(this);
}
}

View file

@ -38,7 +38,7 @@ public final class MuYanlingCelestialWind extends CardImpl {
Ability ability = new LoyaltyAbility(new BoostTargetEffect(
-5, 0, Duration.UntilYourNextTurn
).setText("Until your next turn, up to one target creature gets -5/-0."), 1);
ability.addTarget(new TargetCreaturePermanent());
ability.addTarget(new TargetCreaturePermanent(0, 1));
this.addAbility(ability);
// 3: Return up to two target creatures to their owners' hands.

View file

@ -67,7 +67,7 @@ class RampagingGrowthEffect extends OneShotEffect {
}
TargetCardInLibrary target = new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND_A);
player.searchLibrary(target, source, game);
Card card = player.getLibrary().getCard(target.getTargetController(), game);
Card card = player.getLibrary().getCard(target.getFirstTarget(), game);
if (card == null) {
player.shuffleLibrary(source, game);
return true;

View file

@ -0,0 +1,96 @@
package mage.cards.r;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.MutagenToken;
import mage.util.CardUtil;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class RaphaelTheMuscle extends CardImpl {
public RaphaelTheMuscle(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.TURTLE);
this.power = new MageInt(4);
this.toughness = new MageInt(4);
// Double all damage that creatures you control with counters on them would deal.
this.addAbility(new SimpleStaticAbility(new RaphaelTheMuscleReplacementEffect()));
// When Raphael enters, create a Mutagen token.
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new MutagenToken())));
// Partner--Character select
this.addAbility(PartnerVariantType.CHARACTER_SELECT.makeAbility());
}
private RaphaelTheMuscle(final RaphaelTheMuscle card) {
super(card);
}
@Override
public RaphaelTheMuscle copy() {
return new RaphaelTheMuscle(this);
}
}
class RaphaelTheMuscleReplacementEffect extends ReplacementEffectImpl {
RaphaelTheMuscleReplacementEffect() {
super(Duration.WhileOnBattlefield, Outcome.Damage);
staticText = "double all damage that creatures you control with counters on them would deal";
}
private RaphaelTheMuscleReplacementEffect(final RaphaelTheMuscleReplacementEffect effect) {
super(effect);
}
@Override
public RaphaelTheMuscleReplacementEffect copy() {
return new RaphaelTheMuscleReplacementEffect(this);
}
@Override
public boolean checksEventType(GameEvent event, Game game) {
switch (event.getType()) {
case DAMAGE_PLAYER:
case DAMAGE_PERMANENT:
return true;
default:
return false;
}
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId());
return permanent != null
&& permanent.isCreature(game)
&& permanent.isControlledBy(source.getControllerId())
&& permanent.getCounters(game).getTotalCount() > 0;
}
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
event.setAmount(CardUtil.overflowMultiply(event.getAmount(), 2));
return false;
}
}

View file

@ -0,0 +1,76 @@
package mage.cards.r;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.SneakAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class RaphaelsTechnique extends CardImpl {
public RaphaelsTechnique(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{4}{R}{R}");
// Sneak {2}{R}
this.addAbility(new SneakAbility(this, "{2}{R}"));
// Each player may discard their hand and draw seven cards.
this.getSpellAbility().addEffect(new RaphaelsTechniqueEffect());
}
private RaphaelsTechnique(final RaphaelsTechnique card) {
super(card);
}
@Override
public RaphaelsTechnique copy() {
return new RaphaelsTechnique(this);
}
}
class RaphaelsTechniqueEffect extends OneShotEffect {
RaphaelsTechniqueEffect() {
super(Outcome.Benefit);
staticText = "each player may discard their hand and draw seven cards";
}
private RaphaelsTechniqueEffect(final RaphaelsTechniqueEffect effect) {
super(effect);
}
@Override
public RaphaelsTechniqueEffect copy() {
return new RaphaelsTechniqueEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
List<Player> wheelers = new ArrayList<>();
for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) {
Player player = game.getPlayer(playerId);
if (player != null && player.chooseUse(
Outcome.DrawCard, "Discard your hand and draw seven?", source, game
)) {
game.informPlayers(player.getName() + " chooses to discard their hand and draw seven");
wheelers.add(player);
}
}
for (Player player : wheelers) {
player.discard(player.getHand(), false, source, game);
player.drawCards(7, source, game);
}
return true;
}
}

View file

@ -1,7 +1,5 @@
package mage.cards.r;
import java.util.UUID;
import mage.MageInt;
import mage.ObjectColor;
import mage.abilities.common.SimpleStaticAbility;
@ -10,19 +8,21 @@ import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration;
import mage.constants.Zone;
import mage.constants.SubType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.ColorPredicate;
import java.util.UUID;
/**
*
* @author North
*/
public final class RoughshodMentor extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent("Green creatures");
private static final FilterPermanent filter = new FilterCreaturePermanent("Green creatures");
static {
filter.add(new ColorPredicate(ObjectColor.GREEN));

View file

@ -0,0 +1,102 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.AttacksPlayerWithCreaturesTriggeredAbility;
import mage.abilities.effects.common.combat.GoadTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.keyword.DeathtouchAbility;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetadjustment.ThatPlayerControlsTargetAdjuster;
import mage.target.targetpointer.FixedTarget;
import java.util.Objects;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SeiferBalambRival extends CardImpl {
public SeiferBalambRival(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.MERCENARY);
this.power = new MageInt(4);
this.toughness = new MageInt(3);
// First strike
this.addAbility(FirstStrikeAbility.getInstance());
// Whenever you attack a player, goad target creature that player controls.
Ability ability = new AttacksPlayerWithCreaturesTriggeredAbility(
new GoadTargetEffect().setText("goad target creature that player controls"), SetTargetPointer.NONE
);
ability.addTarget(new TargetCreaturePermanent());
ability.setTargetAdjuster(new ThatPlayerControlsTargetAdjuster());
this.addAbility(ability);
// Whenever a creature attacking one of your opponents becomes blocked by two or more creatures, that attacking creature gains deathtouch until end of turn.
this.addAbility(new SeiferBalambRivalTriggeredAbility());
}
private SeiferBalambRival(final SeiferBalambRival card) {
super(card);
}
@Override
public SeiferBalambRival copy() {
return new SeiferBalambRival(this);
}
}
class SeiferBalambRivalTriggeredAbility extends TriggeredAbilityImpl {
SeiferBalambRivalTriggeredAbility() {
super(Zone.BATTLEFIELD, new GainAbilityTargetEffect(DeathtouchAbility.getInstance()).setText("that attacking creature gains deathtouch until end of turn"));
this.setTriggerPhrase("Whenever a creature attacking one of your opponents becomes blocked by two or more creatures, ");
}
private SeiferBalambRivalTriggeredAbility(final SeiferBalambRivalTriggeredAbility ability) {
super(ability);
}
@Override
public SeiferBalambRivalTriggeredAbility copy() {
return new SeiferBalambRivalTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.CREATURE_BLOCKED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!game
.getOpponents(game.getCombat().getDefenderId(event.getTargetId()))
.contains(this.getControllerId())
|| game
.getCombat()
.findGroup(event.getTargetId())
.getBlockers()
.stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.filter(permanent -> permanent.isCreature(game))
.count() < 2) {
return false;
}
this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game));
return true;
}
}

View file

@ -30,12 +30,12 @@ public final class SpiderSense extends CardImpl {
public SpiderSense(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{U}");
// Web-slinging {U}
this.addAbility(new WebSlingingAbility(this, "{U}"));
// Counter target instant spell, sorcery spell, or triggered ability.
this.getSpellAbility().addEffect(new CounterTargetEffect());
this.getSpellAbility().addTarget(new TargetStackObject(filter));
// Web-slinging {U}
this.addAbility(new WebSlingingAbility(this, "{U}"));
}
private SpiderSense(final SpiderSense card) {

View file

@ -0,0 +1,57 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.common.LeavesBattlefieldAllTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.keyword.MenaceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.PartnerVariantType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.filter.FilterPermanent;
import mage.filter.FilterPermanentThisOrAnother;
import mage.filter.StaticFilters;
import mage.game.permanent.token.MutagenToken;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SplinterTheMentor extends CardImpl {
private static final FilterPermanent filter = new FilterPermanentThisOrAnother(
StaticFilters.FILTER_CONTROLLED_CREATURE_NON_TOKEN, false
);
public SplinterTheMentor(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.RAT);
this.power = new MageInt(2);
this.toughness = new MageInt(2);
// Menace
this.addAbility(new MenaceAbility());
// Whenever Splinter or another nontoken creature you control leaves the battlefield, create a Mutagen token.
this.addAbility(new LeavesBattlefieldAllTriggeredAbility(new CreateTokenEffect(new MutagenToken()), filter));
// Partner--Character select
this.addAbility(PartnerVariantType.CHARACTER_SELECT.makeAbility());
}
private SplinterTheMentor(final SplinterTheMentor card) {
super(card);
}
@Override
public SplinterTheMentor copy() {
return new SplinterTheMentor(this);
}
}

View file

@ -30,7 +30,7 @@ public class StrongTheBrutishThespian extends CardImpl {
this.toughness = new MageInt(7);
// Ward {2}
this.addAbility(new WardAbility(new ManaCostsImpl<>("{2}")));
this.addAbility(new WardAbility(new ManaCostsImpl<>("{2}"), false));
// Enrage - Whenever Strong is dealt damage, you get three rad counters and put three +1/+1 counters on Strong.
Ability enrageAbility = new DealtDamageToSourceTriggeredAbility(new AddCountersPlayersEffect(CounterType.RAD.createInstance(3), TargetController.YOU), false, true);

View file

@ -107,7 +107,7 @@ class SummonEsperValigarmandaExileEffect extends OneShotEffect {
return !cards.isEmpty()
&& controller.moveCardsToExile(
cards.getCards(game), source, game, true,
CardUtil.getExileZoneId(game, source, 1),
CardUtil.getExileZoneId(game, source),
CardUtil.getSourceName(game, source)
);
}

View file

@ -0,0 +1,56 @@
package mage.cards.s;
import mage.MageInt;
import mage.abilities.common.LeavesBattlefieldAllTriggeredAbility;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.MenaceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class SuperShredder extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent("another permanent");
static {
filter.add(AnotherPredicate.instance);
}
public SuperShredder(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.MUTANT);
this.subtype.add(SubType.NINJA);
this.subtype.add(SubType.HUMAN);
this.power = new MageInt(1);
this.toughness = new MageInt(1);
// Menace
this.addAbility(new MenaceAbility());
// Whenever another permanent leaves the battlefield, put a +1/+1 counter on Super Shredder.
this.addAbility(new LeavesBattlefieldAllTriggeredAbility(
new AddCountersSourceEffect(CounterType.P1P1.createInstance()), filter
));
}
private SuperShredder(final SuperShredder card) {
super(card);
}
@Override
public SuperShredder copy() {
return new SuperShredder(this);
}
}

View file

@ -0,0 +1,71 @@
package mage.cards.s;
import java.util.UUID;
import mage.abilities.common.AttacksWithCreaturesTriggeredAbility;
import mage.abilities.common.UnlockThisDoorTriggeredAbility;
import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.cards.CardSetInfo;
import mage.cards.RoomCard;
import mage.constants.CardType;
import mage.constants.ComparisonType;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.counters.CounterType;
import mage.filter.FilterCard;
import mage.filter.common.FilterCreatureCard;
import mage.filter.predicate.mageobject.ManaValuePredicate;
import mage.target.common.TargetAttackingCreature;
import mage.target.common.TargetCardInYourGraveyard;
/**
*
* @author oscscull
*/
public final class SurgicalSuiteHospitalRoom extends RoomCard {
private static final FilterCard filter = new FilterCreatureCard(
"creature card with mana value 3 or less from your graveyard");
static {
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 4));
}
public SurgicalSuiteHospitalRoom(UUID ownerId, CardSetInfo setInfo) {
// Surgical Suite
// {1}{W}
// When you unlock this door, return target creature card with mana value 3 or
// less from your graveyard to the battlefield.
// Hospital Room
// {3}{W}
// Enchantment -- Room
// Whenever you attack, put a +1/+1 counter on target attacking creature.
super(ownerId, setInfo,
new CardType[] { CardType.ENCHANTMENT },
"{1}{W}", "{3}{W}", SpellAbilityType.SPLIT);
this.subtype.add(SubType.ROOM);
// Left half ability - "When you unlock this door, return target creature card with mana value 3 or
// less from your graveyard to the battlefield."
UnlockThisDoorTriggeredAbility left = new UnlockThisDoorTriggeredAbility(
new ReturnFromGraveyardToBattlefieldTargetEffect(), false, true);
left.addTarget(new TargetCardInYourGraveyard(filter));
// Right half ability - "Whenever you attack, put a +1/+1 counter on target attacking creature."
AttacksWithCreaturesTriggeredAbility right = new AttacksWithCreaturesTriggeredAbility(
new AddCountersTargetEffect(CounterType.P1P1.createInstance()), 1
);
right.addTarget(new TargetAttackingCreature());
this.addRoomAbilities(left, right);
}
private SurgicalSuiteHospitalRoom(final SurgicalSuiteHospitalRoom card) {
super(card);
}
@Override
public SurgicalSuiteHospitalRoom copy() {
return new SurgicalSuiteHospitalRoom(this);
}
}

View file

@ -83,7 +83,7 @@ class TheAesirEscapeValhallaOneEffect extends OneShotEffect {
controller.choose(outcome, target, source, game);
Card card = game.getCard(target.getFirstTarget());
if (card != null) {
UUID exileId = CardUtil.getExileZoneId(game, source, 1);
UUID exileId = CardUtil.getExileZoneId(game, source);
MageObject sourceObject = source.getSourceObject(game);
String exileName = sourceObject != null ? sourceObject.getName() : "";
controller.moveCardsToExile(card, source, game, false, exileId, exileName);

View file

@ -81,7 +81,7 @@ class TheCreationOfAvacynOneEffect extends OneShotEffect {
if (card != null) {
// exile it face down
card.setFaceDown(true, game);
UUID exileId = CardUtil.getExileZoneId(game, source, 1);
UUID exileId = CardUtil.getExileZoneId(game, source);
MageObject sourceObject = source.getSourceObject(game);
String exileName = sourceObject != null ? sourceObject.getName() : "";
controller.moveCardsToExile(card, source, game, false, exileId, exileName);

View file

@ -30,7 +30,7 @@ public final class TheFinalDays extends CardImpl {
// Create two tapped 2/2 black Horror creature tokens. If this spell was cast from a graveyard, instead create X of those tokens, where X is the number of creature cards in your graveyard.
this.getSpellAbility().addEffect(new ConditionalOneShotEffect(
new CreateTokenEffect(new Horror3Token(), xValue), new CreateTokenEffect(new Horror3Token(), 2),
new CreateTokenEffect(new Horror3Token(), xValue, true, false), new CreateTokenEffect(new Horror3Token(), 2, true),
CastFromGraveyardSourceCondition.instance, "create two tapped 2/2 black Horror creature tokens. " +
"If this spell was cast from a graveyard, instead create X of those tokens, " +
"where X is the number of creature cards in your graveyard"

View file

@ -0,0 +1,92 @@
package mage.cards.u;
import mage.abilities.Ability;
import mage.abilities.common.UnlockThisDoorTriggeredAbility;
import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition;
import mage.abilities.decorator.ConditionalOneShotEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.LoseLifeSourceControllerEffect;
import mage.abilities.hint.ConditionHint;
import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility;
import mage.cards.CardSetInfo;
import mage.cards.RoomCard;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.filter.common.FilterControlledPermanent;
import mage.game.Game;
import mage.game.permanent.token.Demon66Token;
import mage.players.Player;
import java.util.UUID;
/**
* @author PurpleCrowbar
*/
public final class UnholyAnnexRitualChamber extends RoomCard {
private static final FilterControlledPermanent filter = new FilterControlledPermanent("Demon");
static {
filter.add(SubType.DEMON.getPredicate());
}
public UnholyAnnexRitualChamber(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}", "{3}{B}{B}", SpellAbilityType.SPLIT);
this.subtype.add(SubType.ROOM);
// Unholy Annex: At the beginning of your end step, draw a card. If you control a Demon, each opponent loses 2 life and you gain 2 life. Otherwise, you lose 2 life.
Ability left = new BeginningOfEndStepTriggeredAbility(new DrawCardSourceControllerEffect(1));
left.addEffect(new ConditionalOneShotEffect(
new UnholyAnnexDrainEffect(), new LoseLifeSourceControllerEffect(2),
new PermanentsOnTheBattlefieldCondition(filter), "If you control a Demon, each opponent loses 2 life and you gain 2 life. Otherwise, you lose 2 life"
));
left.addHint(new ConditionHint(new PermanentsOnTheBattlefieldCondition(filter), "You control a Demon"));
// Ritual Chamber: When you unlock this door, create a 6/6 black Demon creature token with flying.
Ability right = new UnlockThisDoorTriggeredAbility(new CreateTokenEffect(new Demon66Token()), false, false);
this.addRoomAbilities(left, right);
}
private UnholyAnnexRitualChamber(final UnholyAnnexRitualChamber card) {
super(card);
}
@Override
public UnholyAnnexRitualChamber copy() {
return new UnholyAnnexRitualChamber(this);
}
}
class UnholyAnnexDrainEffect extends OneShotEffect {
UnholyAnnexDrainEffect() {
super(Outcome.GainLife);
this.staticText = "each opponent loses 2 life and you gain 2 life";
}
private UnholyAnnexDrainEffect(final UnholyAnnexDrainEffect effect) {
super(effect);
}
@Override
public UnholyAnnexDrainEffect copy() {
return new UnholyAnnexDrainEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
for (UUID opponentId : game.getOpponents(source.getControllerId())) {
Player player = game.getPlayer(opponentId);
if (player != null) {
player.loseLife(2, game, source, false);
}
}
game.getPlayer(source.getControllerId()).gainLife(2, game, source);
return true;
}
}

View file

@ -0,0 +1,53 @@
package mage.cards.v;
import mage.abilities.common.EntersBattlefieldOrAttacksAllTriggeredAbility;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DoIfCostPaid;
import mage.abilities.effects.common.ReturnSourceFromGraveyardToHandEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.filter.predicate.mageobject.CommanderPredicate;
import mage.game.permanent.token.BlackWizardToken;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class VivisPersistence extends CardImpl {
private static final FilterPermanent filter = new FilterPermanent("you commander");
static {
filter.add(TargetController.YOU.getOwnerPredicate());
filter.add(CommanderPredicate.instance);
}
public VivisPersistence(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{R}");
// Create a 0/1 black Wizard creature token with "Whenever you cast a noncreature spell, this token deals 1 damage to each opponent."
this.getSpellAbility().addEffect(new CreateTokenEffect(new BlackWizardToken()));
// Whenever your commander enters or attacks, you may pay {2}. If you do, return this card from your graveyard to your hand.
this.addAbility(new EntersBattlefieldOrAttacksAllTriggeredAbility(
Zone.GRAVEYARD,
new DoIfCostPaid(new ReturnSourceFromGraveyardToHandEffect(), new GenericManaCost(2)),
filter, false
));
}
private VivisPersistence(final VivisPersistence card) {
super(card);
}
@Override
public VivisPersistence copy() {
return new VivisPersistence(this);
}
}

View file

@ -0,0 +1,49 @@
package mage.cards.w;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.common.UnlockThisDoorTriggeredAbility;
import mage.abilities.effects.common.replacement.GraveyardFromAnywhereExileReplacementEffect;
import mage.abilities.effects.common.ruleModifying.PlayFromGraveyardControllerEffect;
import mage.cards.CardSetInfo;
import mage.cards.RoomCard;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import java.util.UUID;
/**
* @author PurpleCrowbar
*/
public final class WalkInClosetForgottenCellar extends RoomCard {
public WalkInClosetForgottenCellar(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}", "{3}{G}{G}", SpellAbilityType.SPLIT);
this.subtype.add(SubType.ROOM);
// Walk-In Closet: You may play lands from your graveyard.
SimpleStaticAbility left = new SimpleStaticAbility(PlayFromGraveyardControllerEffect.playLands());
// Forgotten Cellar: When you unlock this door, you may cast spells from your graveyard this turn, and if a card would be put into your graveyard from anywhere this turn, exile it instead.
UnlockThisDoorTriggeredAbility right = new UnlockThisDoorTriggeredAbility(
new PlayFromGraveyardControllerEffect(StaticFilters.FILTER_CARD_NON_LAND, Duration.EndOfTurn)
.setText("you may cast spells from your graveyard this turn"), false, false
);
right.addEffect(new GraveyardFromAnywhereExileReplacementEffect(Duration.EndOfTurn).concatBy(", and")
.setText("if a card would be put into your graveyard from anywhere this turn, exile it instead")
);
this.addRoomAbilities(left, right);
}
private WalkInClosetForgottenCellar(final WalkInClosetForgottenCellar card) {
super(card);
}
@Override
public WalkInClosetForgottenCellar copy() {
return new WalkInClosetForgottenCellar(this);
}
}

View file

@ -43,6 +43,7 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Blazemire Verge", 329, Rarity.RARE, mage.cards.b.BlazemireVerge.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Bleeding Woods", 257, Rarity.COMMON, mage.cards.b.BleedingWoods.class));
cards.add(new SetCardInfo("Boilerbilges Ripper", 127, Rarity.COMMON, mage.cards.b.BoilerbilgesRipper.class));
cards.add(new SetCardInfo("Bottomless Pool // Locker Room", 43, Rarity.UNCOMMON, mage.cards.b.BottomlessPoolLockerRoom.class));
cards.add(new SetCardInfo("Break Down the Door", 170, Rarity.UNCOMMON, mage.cards.b.BreakDownTheDoor.class));
cards.add(new SetCardInfo("Broodspinner", 211, Rarity.UNCOMMON, mage.cards.b.Broodspinner.class));
cards.add(new SetCardInfo("Cackling Slasher", 85, Rarity.COMMON, mage.cards.c.CacklingSlasher.class));
@ -69,6 +70,8 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Cynical Loner", 89, Rarity.UNCOMMON, mage.cards.c.CynicalLoner.class));
cards.add(new SetCardInfo("Daggermaw Megalodon", 48, Rarity.COMMON, mage.cards.d.DaggermawMegalodon.class));
cards.add(new SetCardInfo("Dashing Bloodsucker", 90, Rarity.UNCOMMON, mage.cards.d.DashingBloodsucker.class));
cards.add(new SetCardInfo("Dazzling Theater // Prop Room", 3, Rarity.RARE, mage.cards.d.DazzlingTheaterPropRoom.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Dazzling Theater // Prop Room", 334, Rarity.RARE, mage.cards.d.DazzlingTheaterPropRoom.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Defiant Survivor", 175, Rarity.UNCOMMON, mage.cards.d.DefiantSurvivor.class));
cards.add(new SetCardInfo("Demonic Counsel", 310, Rarity.RARE, mage.cards.d.DemonicCounsel.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Demonic Counsel", 92, Rarity.RARE, mage.cards.d.DemonicCounsel.class, NON_FULL_USE_VARIOUS));
@ -76,6 +79,8 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Dissection Tools", 385, Rarity.RARE, mage.cards.d.DissectionTools.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Disturbing Mirth", 212, Rarity.UNCOMMON, mage.cards.d.DisturbingMirth.class));
cards.add(new SetCardInfo("Diversion Specialist", 132, Rarity.UNCOMMON, mage.cards.d.DiversionSpecialist.class));
cards.add(new SetCardInfo("Dollmaker's Shop // Porcelain Gallery", 4, Rarity.MYTHIC, mage.cards.d.DollmakersShopPorcelainGallery.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Dollmaker's Shop // Porcelain Gallery", 335, Rarity.MYTHIC, mage.cards.d.DollmakersShopPorcelainGallery.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Don't Make a Sound", 49, Rarity.COMMON, mage.cards.d.DontMakeASound.class));
cards.add(new SetCardInfo("Doomsday Excruciator", 346, Rarity.RARE, mage.cards.d.DoomsdayExcruciator.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Doomsday Excruciator", 94, Rarity.RARE, mage.cards.d.DoomsdayExcruciator.class, NON_FULL_USE_VARIOUS));
@ -139,6 +144,8 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Frantic Strength", 179, Rarity.COMMON, mage.cards.f.FranticStrength.class));
cards.add(new SetCardInfo("Friendly Ghost", 12, Rarity.COMMON, mage.cards.f.FriendlyGhost.class));
cards.add(new SetCardInfo("Friendly Teddy", 247, Rarity.COMMON, mage.cards.f.FriendlyTeddy.class));
cards.add(new SetCardInfo("Funeral Room // Awakening Hall", 100, Rarity.MYTHIC, mage.cards.f.FuneralRoomAwakeningHall.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Funeral Room // Awakening Hall", 338, Rarity.MYTHIC, mage.cards.f.FuneralRoomAwakeningHall.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Get Out", 60, Rarity.UNCOMMON, mage.cards.g.GetOut.class));
cards.add(new SetCardInfo("Ghost Vacuum", 248, Rarity.RARE, mage.cards.g.GhostVacuum.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Ghost Vacuum", 326, Rarity.RARE, mage.cards.g.GhostVacuum.class, NON_FULL_USE_VARIOUS));
@ -318,6 +325,7 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Stay Hidden, Stay Silent", 291, Rarity.UNCOMMON, mage.cards.s.StayHiddenStaySilent.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Stay Hidden, Stay Silent", 74, Rarity.UNCOMMON, mage.cards.s.StayHiddenStaySilent.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Strangled Cemetery", 268, Rarity.COMMON, mage.cards.s.StrangledCemetery.class));
cards.add(new SetCardInfo("Surgical Suite // Hospital Room", 34, Rarity.UNCOMMON, mage.cards.s.SurgicalSuiteHospitalRoom.class));
cards.add(new SetCardInfo("Swamp", 274, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Swamp", 281, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Swamp", 282, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS));
@ -357,6 +365,8 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Undead Sprinter", 350, Rarity.RARE, mage.cards.u.UndeadSprinter.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Under the Skin", 203, Rarity.UNCOMMON, mage.cards.u.UnderTheSkin.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Under the Skin", 323, Rarity.UNCOMMON, mage.cards.u.UnderTheSkin.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Unholy Annex // Ritual Chamber", 118, Rarity.RARE, mage.cards.u.UnholyAnnexRitualChamber.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Unholy Annex // Ritual Chamber", 339, Rarity.RARE, mage.cards.u.UnholyAnnexRitualChamber.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Unidentified Hovership", 305, Rarity.RARE, mage.cards.u.UnidentifiedHovership.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Unidentified Hovership", 37, Rarity.RARE, mage.cards.u.UnidentifiedHovership.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Unnerving Grasp", 80, Rarity.UNCOMMON, mage.cards.u.UnnervingGrasp.class));
@ -385,6 +395,8 @@ public final class DuskmournHouseOfHorror extends ExpansionSet {
cards.add(new SetCardInfo("Victor, Valgavoth's Seneschal", 364, Rarity.RARE, mage.cards.v.VictorValgavothsSeneschal.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Vile Mutilator", 122, Rarity.UNCOMMON, mage.cards.v.VileMutilator.class));
cards.add(new SetCardInfo("Violent Urge", 164, Rarity.UNCOMMON, mage.cards.v.ViolentUrge.class));
cards.add(new SetCardInfo("Walk-In Closet // Forgotten Cellar", 205, Rarity.MYTHIC, mage.cards.w.WalkInClosetForgottenCellar.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Walk-In Closet // Forgotten Cellar", 341, Rarity.MYTHIC, mage.cards.w.WalkInClosetForgottenCellar.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Waltz of Rage", 165, Rarity.RARE, mage.cards.w.WaltzOfRage.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Waltz of Rage", 318, Rarity.RARE, mage.cards.w.WaltzOfRage.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Wary Watchdog", 206, Rarity.COMMON, mage.cards.w.WaryWatchdog.class));

View file

@ -170,6 +170,7 @@ public final class FinalFantasyCommander extends ExpansionSet {
cards.add(new SetCardInfo("Fight Rigging", 303, Rarity.RARE, mage.cards.f.FightRigging.class));
cards.add(new SetCardInfo("Final Judgment", 243, Rarity.MYTHIC, mage.cards.f.FinalJudgment.class));
cards.add(new SetCardInfo("Fire-Lit Thicket", 392, Rarity.RARE, mage.cards.f.FireLitThicket.class));
cards.add(new SetCardInfo("Fishing Gear", 461, Rarity.RARE, mage.cards.f.FishingGear.class));
cards.add(new SetCardInfo("Flash Photography", 463, Rarity.RARE, mage.cards.f.FlashPhotography.class));
cards.add(new SetCardInfo("Flayer of the Hatebound", 293, Rarity.RARE, mage.cards.f.FlayerOfTheHatebound.class));
cards.add(new SetCardInfo("Flooded Grove", 393, Rarity.RARE, mage.cards.f.FloodedGrove.class));
@ -257,6 +258,7 @@ public final class FinalFantasyCommander extends ExpansionSet {
cards.add(new SetCardInfo("Maester Seymour", 160, Rarity.RARE, mage.cards.m.MaesterSeymour.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Maester Seymour", 68, Rarity.RARE, mage.cards.m.MaesterSeymour.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Mask of Memory", 350, Rarity.UNCOMMON, mage.cards.m.MaskOfMemory.class));
cards.add(new SetCardInfo("Mega Flare", 456, Rarity.RARE, mage.cards.m.MegaFlare.class));
cards.add(new SetCardInfo("Meteor Golem", 351, Rarity.UNCOMMON, mage.cards.m.MeteorGolem.class));
cards.add(new SetCardInfo("Millikin", 352, Rarity.UNCOMMON, mage.cards.m.Millikin.class));
cards.add(new SetCardInfo("Mind Stone", 353, Rarity.UNCOMMON, mage.cards.m.MindStone.class));
@ -332,6 +334,7 @@ public final class FinalFantasyCommander extends ExpansionSet {
cards.add(new SetCardInfo("Secret Rendezvous", 218, Rarity.UNCOMMON, mage.cards.s.SecretRendezvous.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Secret Rendezvous", 219, Rarity.UNCOMMON, mage.cards.s.SecretRendezvous.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Secret Rendezvous", 253, Rarity.UNCOMMON, mage.cards.s.SecretRendezvous.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Seifer, Balamb Rival", 451, Rarity.RARE, mage.cards.s.SeiferBalambRival.class));
cards.add(new SetCardInfo("Sephiroth, Fallen Hero", 182, Rarity.RARE, mage.cards.s.SephirothFallenHero.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Sephiroth, Fallen Hero", 92, Rarity.RARE, mage.cards.s.SephirothFallenHero.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Sepulchral Primordial", 284, Rarity.RARE, mage.cards.s.SepulchralPrimordial.class));
@ -466,6 +469,7 @@ public final class FinalFantasyCommander extends ExpansionSet {
cards.add(new SetCardInfo("Vincent, Vengeful Atoner", 64, Rarity.RARE, mage.cards.v.VincentVengefulAtoner.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Vindicate", 330, Rarity.RARE, mage.cards.v.Vindicate.class));
cards.add(new SetCardInfo("Vineglimmer Snarl", 440, Rarity.RARE, mage.cards.v.VineglimmerSnarl.class));
cards.add(new SetCardInfo("Vivi's Persistence", 458, Rarity.RARE, mage.cards.v.VivisPersistence.class));
cards.add(new SetCardInfo("Void Rend", 331, Rarity.RARE, mage.cards.v.VoidRend.class));
cards.add(new SetCardInfo("Wakka, Devoted Guardian", 190, Rarity.RARE, mage.cards.w.WakkaDevotedGuardian.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Wakka, Devoted Guardian", 477, Rarity.RARE, mage.cards.w.WakkaDevotedGuardian.class, NON_FULL_USE_VARIOUS));

View file

@ -0,0 +1,40 @@
package mage.sets;
import mage.cards.ExpansionSet;
import mage.constants.Rarity;
import mage.constants.SetType;
/**
* @author TheElk801
*/
public final class TeenageMutantNinjaTurtles extends ExpansionSet {
private static final TeenageMutantNinjaTurtles instance = new TeenageMutantNinjaTurtles();
public static TeenageMutantNinjaTurtles getInstance() {
return instance;
}
private TeenageMutantNinjaTurtles() {
super("Teenage Mutant Ninja Turtles", "TMT", ExpansionSet.buildDate(2026, 3, 6), SetType.EXPANSION);
this.blockName = "Teenage Mutant Ninja Turtles"; // for sorting in GUI
this.hasBasicLands = true;
cards.add(new SetCardInfo("April O'Neil, Hacktivist", 29, Rarity.RARE, mage.cards.a.AprilONeilHacktivist.class));
cards.add(new SetCardInfo("Bebop & Rocksteady", 140, Rarity.RARE, mage.cards.b.BebopAndRocksteady.class));
cards.add(new SetCardInfo("Casey Jones, Jury-Rig Justiciar", 87, Rarity.UNCOMMON, mage.cards.c.CaseyJonesJuryRigJusticiar.class));
cards.add(new SetCardInfo("Forest", 257, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Forest", 314, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Island", 254, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Island", 311, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Krang, Master Mind", 43, Rarity.RARE, mage.cards.k.KrangMasterMind.class));
cards.add(new SetCardInfo("Mountain", 256, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Mountain", 313, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Plains", 253, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Plains", 310, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Raphael's Technique", 105, Rarity.RARE, mage.cards.r.RaphaelsTechnique.class));
cards.add(new SetCardInfo("Super Shredder", 83, Rarity.MYTHIC, mage.cards.s.SuperShredder.class));
cards.add(new SetCardInfo("Swamp", 255, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Swamp", 312, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS));
}
}

View file

@ -0,0 +1,33 @@
package mage.sets;
import mage.cards.ExpansionSet;
import mage.constants.Rarity;
import mage.constants.SetType;
/**
* @author TheElk801
*/
public final class TeenageMutantNinjaTurtlesEternal extends ExpansionSet {
private static final TeenageMutantNinjaTurtlesEternal instance = new TeenageMutantNinjaTurtlesEternal();
public static TeenageMutantNinjaTurtlesEternal getInstance() {
return instance;
}
private TeenageMutantNinjaTurtlesEternal() {
super("Teenage Mutant Ninja Turtles Eternal", "TMC", ExpansionSet.buildDate(2026, 3, 6), SetType.EXPANSION);
this.hasBasicLands = false;
cards.add(new SetCardInfo("Dark Ritual", 131, Rarity.MYTHIC, mage.cards.d.DarkRitual.class));
cards.add(new SetCardInfo("Donatello, Rad Scientist", 109, Rarity.MYTHIC, mage.cards.d.DonatelloRadScientist.class));
cards.add(new SetCardInfo("Donatello, the Brains", 2, Rarity.MYTHIC, mage.cards.d.DonatelloTheBrains.class));
cards.add(new SetCardInfo("Donnie & April, Adorkable Duo", 111, Rarity.RARE, mage.cards.d.DonnieAndAprilAdorkableDuo.class));
cards.add(new SetCardInfo("Heroes in a Half Shell", 6, Rarity.MYTHIC, mage.cards.h.HeroesInAHalfShell.class));
cards.add(new SetCardInfo("Leonardo, Worldly Warrior", 101, Rarity.MYTHIC, mage.cards.l.LeonardoWorldlyWarrior.class));
cards.add(new SetCardInfo("Leonardo, the Balance", 1, Rarity.MYTHIC, mage.cards.l.LeonardoTheBalance.class));
cards.add(new SetCardInfo("Michelangelo, the Heart", 5, Rarity.MYTHIC, mage.cards.m.MichelangeloTheHeart.class));
cards.add(new SetCardInfo("Raphael, the Muscle", 4, Rarity.MYTHIC, mage.cards.r.RaphaelTheMuscle.class));
cards.add(new SetCardInfo("Splinter, the Mentor", 3, Rarity.MYTHIC, mage.cards.s.SplinterTheMentor.class));
}
}

View file

@ -50,21 +50,29 @@ public class SimulationTriggersAITest extends CardTestPlayerBaseWithAIHelps {
@Test
public void test_DeepglowSkate_PerformanceOnTooManyChoices() {
// bug: game freeze with 100% CPU usage
// https://github.com/magefree/mage/issues/9438
int cardsCount = 2; // 2+ cards will generate too much target options for simulations
int boostMultiplier = (int) Math.pow(2, cardsCount);
int quantity = 1;
String[] cardNames = {
"Island", "Plains", "Swamp", "Mountain",
"Runeclaw Bear", "Absolute Law", "Gilded Lotus", "Alpha Myr"
};
// When Deepglow Skate enters the battlefield, double the number of each kind of counter on any number
// of target permanents.
addCard(Zone.HAND, playerA, "Deepglow Skate", cardsCount); // {4}{U}
addCard(Zone.BATTLEFIELD, playerA, "Island", 5 * cardsCount);
//
addCard(Zone.HAND, playerA, "Deepglow Skate", 1); // {4}{U}
// Bloat the battlefield with permanents (possible targets)
for (String card : cardNames) {
addCard(Zone.BATTLEFIELD, playerA, card, quantity);
addCard(Zone.BATTLEFIELD, playerB, card, quantity);
addCard(Zone.BATTLEFIELD, playerC, card, quantity);
addCard(Zone.BATTLEFIELD, playerD, card, quantity);
}
addCard(Zone.BATTLEFIELD, playerA, "Ajani, Adversary of Tyrants", 1); // x4 loyalty
addCard(Zone.BATTLEFIELD, playerA, "Ajani, Caller of the Pride", 1); // x4 loyalty
addCard(Zone.BATTLEFIELD, playerB, "Ajani Goldmane", 1); // x4 loyalty
addCard(Zone.BATTLEFIELD, playerB, "Ajani, Inspiring Leader", 1); // x5 loyalty
//
// Players can't activate planeswalkers' loyalty abilities.
addCard(Zone.BATTLEFIELD, playerA, "The Immortal Sun", 1); // disable planeswalkers usage by AI
@ -75,9 +83,9 @@ public class SimulationTriggersAITest extends CardTestPlayerBaseWithAIHelps {
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, "Deepglow Skate", cardsCount);
assertCounterCount(playerA, "Ajani, Adversary of Tyrants", CounterType.LOYALTY, 4 * boostMultiplier);
assertCounterCount(playerA, "Ajani, Caller of the Pride", CounterType.LOYALTY, 4 * boostMultiplier);
assertPermanentCount(playerA, "Deepglow Skate", 1);
assertCounterCount(playerA, "Ajani, Adversary of Tyrants", CounterType.LOYALTY, 4 * 2);
assertCounterCount(playerA, "Ajani, Caller of the Pride", CounterType.LOYALTY, 4 * 2);
assertCounterCount(playerB, "Ajani Goldmane", CounterType.LOYALTY, 4);
assertCounterCount(playerB, "Ajani, Inspiring Leader", CounterType.LOYALTY, 5);
}

View file

@ -0,0 +1,904 @@
package org.mage.test.cards.cost.splitcards;
import mage.constants.CardType;
import mage.constants.EmptyNames;
import mage.constants.PhaseStep;
import mage.constants.SubType;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author oscscull
*/
public class RoomCardTest extends CardTestPlayerBase {
// Bottomless pool is cast. It unlocks, and the trigger to return a creature
// should bounce one of two grizzly bears.
@Test
public void testBottomlessPoolETB() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owners hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 2);
checkPlayableAbility("playerA can cast Bottomless Pool", 1, PhaseStep.PRECOMBAT_MAIN, playerA,
"Cast Bottomless Pool", true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
// Target one of playerB's "Grizzly Bears" with the return effect.
addTarget(playerA, "Grizzly Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
// Assertions:
// Verify that one "Grizzly Bears" is still on playerB's battlefield.
assertPermanentCount(playerB, "Grizzly Bears", 1);
// Verify that one "Grizzly Bears" has been returned to playerB's hand.
assertHandCount(playerB, "Grizzly Bears", 1);
// Verify that "Bottomless Pool" is on playerA's battlefield.
assertPermanentCount(playerA, "Bottomless Pool", 1);
// Verify that "Bottomless Pool" is an Enchantment.
assertType("Bottomless Pool", CardType.ENCHANTMENT, true);
// Verify that "Bottomless Pool" has the Room subtype.
assertSubtype("Bottomless Pool", SubType.ROOM);
}
// Locker room is cast. It enters, and gives a coastal piracy effect that
// triggers on damage.
@Test
public void testLockerRoomCombatDamageTrigger() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owners hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
// Cards to be drawn
addCard(Zone.LIBRARY, playerA, "Plains", 2); // Expected cards to be drawn
// 2 attackers
addCard(Zone.BATTLEFIELD, playerA, "Memnite", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Locker Room");
attack(1, playerA, "Memnite");
attack(1, playerA, "Memnite");
// After combat damage, Memnites dealt combat damage to playerB (1 damage * 2).
// 2 Locker Room triggers should go on the stack.
checkStackSize("Locker Room trigger must be on the stack", 1, PhaseStep.COMBAT_DAMAGE, playerA, 2);
checkStackObject("Locker Room trigger must be correct", 1, PhaseStep.COMBAT_DAMAGE, playerA,
"Whenever a creature you control deals combat damage to an opponent, draw a card.", 2);
// Stop at the end of the combat phase to check triggers.
setStopAt(1, PhaseStep.END_COMBAT);
execute();
// Assertions after the first execute() (Locker Room and creatures are on
// battlefield, combat resolved):
assertPermanentCount(playerA, "Locker Room", 1);
assertType("Locker Room", CardType.ENCHANTMENT, true);
assertSubtype("Locker Room", SubType.ROOM);
assertPermanentCount(playerA, "Memnite", 2);
setStrictChooseMode(true);
execute(); // Resolve the Locker Room trigger.
// PlayerA should have drawn two plains cards
assertHandCount(playerA, "Plains", 2);
}
@Test
public void testBottomlessPoolUnlock() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owners hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.BATTLEFIELD, playerA, "Island", 6);
// 2 creatures owned by player A
addCard(Zone.BATTLEFIELD, playerA, "Memnite", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPlayableAbility("playerA can unlock Bottomless Pool", 1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{U}: Unlock the left half.", true);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{U}: Unlock the left half.");
addTarget(playerA, "Memnite");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// Verify that one "Memnite" is still on playerA's battlefield.
assertPermanentCount(playerA, "Memnite", 1);
// Verify that one "Memnite" has been returned to playerA's hand.
assertHandCount(playerA, "Memnite", 1);
// Verify that "Bottomless Pool // Locker Room" is on playerA's battlefield.
assertPermanentCount(playerA, "Bottomless Pool // Locker Room", 1);
// Verify that "Bottomless Pool // Locker Room" is an Enchantment.
assertType("Bottomless Pool // Locker Room", CardType.ENCHANTMENT, true);
// Verify that "Bottomless Pool // Locker Room" has the Room subtype.
assertSubtype("Bottomless Pool // Locker Room", SubType.ROOM);
}
@Test
public void testFlickerNameAndManaCost() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owners hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.HAND, playerA, "Felidar Guardian");
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
// creatures owned by player A
addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
// resolve spell cast
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// unlock and trigger bounce on Memnite
addTarget(playerA, "Memnite");
// resolve bounce
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Felidar Guardian");
// resolve spell cast
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// etb and flicker on Bottomless Pool
setChoice(playerA, "Yes");
addTarget(playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// Verify that one "Memnite" has been returned to playerA's hand.
assertHandCount(playerA, "Memnite", 1);
// Verify that a room with no name is on playerA's battlefield.
assertPermanentCount(playerA, EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), 1);
// Verify that "Felidar Guardian" is on playerA's battlefield.
assertPermanentCount(playerA, "Felidar Guardian", 1);
// Verify that a room with no name is an Enchantment.
assertType(EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), CardType.ENCHANTMENT, true);
// Verify that a room with no name has the Room subtype.
assertSubtype(EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), SubType.ROOM);
}
@Test
public void testFlickerCanBeUnlockedAgain() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owners hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.HAND, playerA, "Felidar Guardian");
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 1);
// creatures owned by player A
addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1);
addCard(Zone.BATTLEFIELD, playerA, "Black Knight", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
// resolve spell cast
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// unlock and trigger bounce on Memnite
addTarget(playerA, "Memnite");
// resolve bounce
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Felidar Guardian");
// resolve spell cast
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// etb and flicker on Bottomless Pool
setChoice(playerA, "Yes");
addTarget(playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// can unlock again
checkPlayableAbility("playerA can unlock Bottomless Pool", 1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{U}: Unlock the left half.", true);
// unlock again
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{U}: Unlock the left half.");
addTarget(playerA, "Black Knight");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// Verify that one "Memnite" has been returned to playerA's hand.
assertHandCount(playerA, "Memnite", 1);
// Verify that one "Black Knight" has been returned to playerA's hand.
assertHandCount(playerA, "Black Knight", 1);
// Verify that "Bottomless Pool" is on playerA's battlefield.
assertPermanentCount(playerA, "Bottomless Pool", 1);
// Verify that "Felidar Guardian" is on playerA's battlefield.
assertPermanentCount(playerA, "Felidar Guardian", 1);
}
@Test
public void testEerie() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owners hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.BATTLEFIELD, playerA, "Island", 6);
addCard(Zone.BATTLEFIELD, playerA, "Erratic Apparition", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
// resolve spell cast
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setChoice(playerA, "When you unlock"); // x2 triggers
// don't bounce anything
addTarget(playerA, TestPlayer.TARGET_SKIP);
// resolve ability
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// unlock other side
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{4}{U}: Unlock the right half.");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// Verify that "Bottomless Pool // Locker Room" is on playerA's battlefield.
assertPermanentCount(playerA, "Bottomless Pool // Locker Room", 1);
// Verify that "Erratic Apparition" is on playerA's battlefield.
assertPermanentCount(playerA, "Erratic Apparition", 1);
// Verify that "Erratic Apparition" has been pumped twice (etb + fully unlock)
assertPowerToughness(playerA, "Erratic Apparition", 3, 5);
}
@Test
public void testCopyOnStack() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owners hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.HAND, playerA, "See Double");
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1);
addCard(Zone.BATTLEFIELD, playerA, "Ornithopter", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
// Copy spell on the stack
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "See Double");
setModeChoice(playerA, "1");
addTarget(playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 3);
addTarget(playerA, "Memnite");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, "Ornithopter");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// Verify that one "Memnite" has been returned to playerA's hand.
assertHandCount(playerA, "Memnite", 1);
// Verify that one "Ornithopter" has been returned to playerA's hand.
assertHandCount(playerA, "Ornithopter", 1);
// Verify that 2 "Bottomless Pool" are on playerA's battlefield.
assertPermanentCount(playerA, "Bottomless Pool", 2);
}
@Test
public void testCopyOnBattlefield() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.HAND, playerA, "Clever Impersonator");
addCard(Zone.BATTLEFIELD, playerA, "Island", 6);
addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1);
addCard(Zone.BATTLEFIELD, playerA, "Ornithopter", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, "Memnite");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Copy spell on the battlefield
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Clever Impersonator");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setChoice(playerA, "Yes");
setChoice(playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{U}: Unlock the left half.");
addTarget(playerA, "Ornithopter");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// Verify that one "Memnite" has been returned to playerA's hand (from original
// unlock).
assertHandCount(playerA, "Memnite", 1);
// Verify that "Ornithopter" has been returned to playerA's hand (from clone
// unlock).
assertHandCount(playerA, "Ornithopter", 1);
// Verify that the original "Bottomless Pool" is on playerA's battlefield, and a
// clone.
assertPermanentCount(playerA, "Bottomless Pool", 2);
}
@Test
public void testNameMatchOnStack() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
// Mindreaver
// {U}{U}
// Creature Human Wizard
// Heroic Whenever you cast a spell that targets this creature, exile the top
// three cards of target players library.
// {U}{U}, Sacrifice this creature: Counter target spell with the same name as a
// card exiled with this creature.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.HAND, playerA, "Twiddle");
addCard(Zone.BATTLEFIELD, playerA, "Mindreaver", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 6);
addCard(Zone.LIBRARY, playerA, "Bottomless Pool // Locker Room", 1);
addCard(Zone.LIBRARY, playerA, "Plains", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Twiddle");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// tap or untap target permanent
addTarget(playerA, "Mindreaver");
// tap that permanent?
setChoice(playerA, "No");
// Whenever you cast a spell that targets this creature, exile the top
// three cards of target players library.
addTarget(playerA, playerA);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{U}{U}, Sacrifice {this}:");
addTarget(playerA, "Bottomless Pool");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
}
@Test
public void testNameMatchOnFieldFromLocked() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
//
// Opalescence
// {2}{W}{W}
// Enchantment
// Each other non-Aura enchantment is a creature in addition to its other types
// and has base power and base toughness each equal to its mana value.
//
// Glorious Anthem
// {1}{W}{W}
// Enchantment
// Creatures you control get +1/+1.
//
// Cackling Counterpart
// {1}{U}{U}
// Instant
// Create a token that's a copy of target creature you control.
//
// Bile Blight
// {B}{B}
// Instant
// Target creature and all other creatures with the same name as that creature
// get -3/-3 until end of turn.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room", 4);
addCard(Zone.HAND, playerA, "Cackling Counterpart");
addCard(Zone.HAND, playerA, "Bile Blight");
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 17);
addCard(Zone.BATTLEFIELD, playerA, "Glorious Anthem");
addCard(Zone.BATTLEFIELD, playerA, "Opalescence");
// Cast Bottomless Pool (unlocked left half)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Locker Room (unlocked right half)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Bottomless Pool then unlock Locker Room (both halves unlocked)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{U}: Unlock the right half.");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Create a fully locked room using Cackling Counterpart
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cackling Counterpart");
addTarget(playerA, "Bottomless Pool // Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Bile Blight targeting the fully locked room
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bile Blight");
addTarget(playerA, EmptyNames.FULLY_LOCKED_ROOM.getTestCommand());
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// The fully locked room should be affected by Bile Blight (-3/-3)
// Since it's a 0/0 creature (mana value 0) +1/+1 from anthem, it becomes 1/1,
// then -2/-2 after Bile Blight (dies)
assertPermanentCount(playerA, EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), 0);
// Token, so nothing should be in grave
assertGraveyardCount(playerA, "Bottomless Pool // Locker Room", 0);
// Other rooms should NOT be affected by Bile Blight since they have different
// names
// Bottomless Pool: 1/1 base + 1/1 from anthem = 2/2
assertPowerToughness(playerA, "Bottomless Pool", 2, 2);
// Locker Room: 5/5 base + 1/1 from anthem = 6/6
assertPowerToughness(playerA, "Locker Room", 6, 6);
// Bottomless Pool // Locker Room: 6/6 base + 1/1 from anthem = 7/7
assertPowerToughness(playerA, "Bottomless Pool // Locker Room", 7, 7);
// Verify remaining rooms are still on battlefield
assertPermanentCount(playerA, "Bottomless Pool", 1);
assertPermanentCount(playerA, "Locker Room", 1);
assertPermanentCount(playerA, "Bottomless Pool // Locker Room", 1);
}
@Test
public void testNameMatchOnFieldFromHalf() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
//
// Opalescence
// {2}{W}{W}
// Enchantment
// Each other non-Aura enchantment is a creature in addition to its other types
// and has base power and base toughness each equal to its mana value.
//
// Glorious Anthem
// {1}{W}{W}
// Enchantment
// Creatures you control get +1/+1.
//
// Cackling Counterpart
// {1}{U}{U}
// Instant
// Create a token that's a copy of target creature you control.
//
// Bile Blight
// {B}{B}
// Instant
// Target creature and all other creatures with the same name as that creature
// get -3/-3 until end of turn.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room", 4);
addCard(Zone.HAND, playerA, "Cackling Counterpart");
addCard(Zone.HAND, playerA, "Bile Blight");
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 17);
addCard(Zone.BATTLEFIELD, playerA, "Glorious Anthem");
addCard(Zone.BATTLEFIELD, playerA, "Opalescence");
// Cast Bottomless Pool (unlocked left half)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Locker Room (unlocked right half)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Bottomless Pool then unlock Locker Room (both halves unlocked)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{U}: Unlock the right half.");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Create a fully locked room using Cackling Counterpart
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cackling Counterpart");
addTarget(playerA, "Bottomless Pool // Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Bile Blight targeting the half locked room
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bile Blight");
addTarget(playerA, "Locker Room");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// Locker Room and Bottomless Pool // Locker Room should both be affected by
// Bile Blight
// since they share the "Locker Room" name component
// Locker Room: 5/5 base + 1/1 from anthem - 3/3 from Bile Blight = 3/3
assertPowerToughness(playerA, "Locker Room", 3, 3);
// Bottomless Pool // Locker Room: 6/6 base + 1/1 from anthem - 3/3 from Bile
// Blight = 4/4
assertPowerToughness(playerA, "Bottomless Pool // Locker Room", 4, 4);
// Other rooms should NOT be affected
// Bottomless Pool: 1/1 base + 1/1 from anthem = 2/2 (unaffected)
assertPowerToughness(playerA, "Bottomless Pool", 2, 2);
// Fully locked room: 0/0 base + 1/1 from anthem = 1/1 (unaffected)
assertPowerToughness(playerA, EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), 1, 1);
// Verify all rooms are still on battlefield
assertPermanentCount(playerA, "Bottomless Pool", 1);
assertPermanentCount(playerA, "Locker Room", 1);
assertPermanentCount(playerA, "Bottomless Pool // Locker Room", 1);
assertPermanentCount(playerA, EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), 1);
}
@Test
public void testNameMatchOnFieldFromUnlocked() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
//
// Opalescence
// {2}{W}{W}
// Enchantment
// Each other non-Aura enchantment is a creature in addition to its other types
// and has base power and base toughness each equal to its mana value.
//
// Glorious Anthem
// {1}{W}{W}
// Enchantment
// Creatures you control get +1/+1.
//
// Cackling Counterpart
// {1}{U}{U}
// Instant
// Create a token that's a copy of target creature you control.
//
// Bile Blight
// {B}{B}
// Instant
// Target creature and all other creatures with the same name as that creature
// get -3/-3 until end of turn.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room", 4);
addCard(Zone.HAND, playerA, "Cackling Counterpart");
addCard(Zone.HAND, playerA, "Bile Blight");
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 17);
addCard(Zone.BATTLEFIELD, playerA, "Glorious Anthem");
addCard(Zone.BATTLEFIELD, playerA, "Opalescence");
// Cast Bottomless Pool (unlocked left half)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Locker Room (unlocked right half)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Bottomless Pool then unlock Locker Room (both halves unlocked)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{U}: Unlock the right half.");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Create a fully locked room using Cackling Counterpart
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cackling Counterpart");
addTarget(playerA, "Bottomless Pool // Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Bile Blight targeting the fully locked room
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bile Blight");
addTarget(playerA, "Bottomless Pool // Locker Room");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// All rooms except the fully locked room should be affected by Bile Blight
// since they all share name components with "Bottomless Pool // Locker Room"
// Bottomless Pool: 1/1 base + 1/1 from anthem - 3/3 from Bile Blight = -1/-1
// (dies)
assertPermanentCount(playerA, "Bottomless Pool", 0);
assertGraveyardCount(playerA, "Bottomless Pool // Locker Room", 1);
// Locker Room: 5/5 base + 1/1 from anthem - 3/3 from Bile Blight = 3/3
assertPowerToughness(playerA, "Locker Room", 3, 3);
// Bottomless Pool // Locker Room: 6/6 base + 1/1 from anthem - 3/3 from Bile
// Blight = 4/4
assertPowerToughness(playerA, "Bottomless Pool // Locker Room", 4, 4);
// Fully locked room should NOT be affected (different name)
// Fully locked room: 0/0 base + 1/1 from anthem = 1/1 (unaffected)
assertPowerToughness(playerA, EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), 1, 1);
// Verify remaining rooms are still on battlefield
assertPermanentCount(playerA, "Locker Room", 1);
assertPermanentCount(playerA, "Bottomless Pool // Locker Room", 1);
assertPermanentCount(playerA, EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), 1);
}
@Test
public void testCounterspellThenReanimate() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.HAND, playerA, "Counterspell");
addCard(Zone.HAND, playerA, "Campus Renovation");
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 3);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
// Target creature for potential bounce (should not be bounced)
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1);
// Cast Bottomless Pool
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
// Counter it while on stack
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Counterspell");
addTarget(playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Use Campus Renovation to return it from graveyard
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Campus Renovation");
addTarget(playerA, "Bottomless Pool // Locker Room");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Assertions:
// Verify that "Grizzly Bears" is still on playerB's battlefield (not bounced)
assertPermanentCount(playerB, "Grizzly Bears", 1);
// Verify that "Grizzly Bears" is not in playerB's hand
assertHandCount(playerB, "Grizzly Bears", 0);
// Verify that a room with no name is on playerA's battlefield
assertPermanentCount(playerA, EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), 1);
// Verify that the nameless room is an Enchantment
assertType(EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), CardType.ENCHANTMENT, true);
// Verify that the nameless room has the Room subtype
assertSubtype(EmptyNames.FULLY_LOCKED_ROOM.getTestCommand(), SubType.ROOM);
// Verify that Campus Renovation is in graveyard
assertGraveyardCount(playerA, "Campus Renovation", 1);
// Verify that Counterspell is in graveyard
assertGraveyardCount(playerA, "Counterspell", 1);
}
@Test
public void testPithingNeedleActivatedAbility() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
//
// Opalescence
// {2}{W}{W}
// Enchantment
// Each other non-Aura enchantment is a creature in addition to its other types
// and has base power and base toughness each equal to its mana value.
//
// Diviner's Wand
// {3}
// Kindred Artifact Wizard Equipment
// Equipped creature has "Whenever you draw a card, this creature gets +1/+1
// and gains flying until end of turn" and "{4}: Draw a card."
// Whenever a Wizard creature enters, you may attach this Equipment to it.
// Equip {3}
//
// Pithing Needle
// {1}
// Artifact
// As Pithing Needle enters, choose a card name.
// Activated abilities of sources with the chosen name can't be activated.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.HAND, playerA, "Pithing Needle");
addCard(Zone.BATTLEFIELD, playerA, "Opalescence");
addCard(Zone.BATTLEFIELD, playerA, "Diviner's Wand");
addCard(Zone.BATTLEFIELD, playerA, "Island", 20);
// Cast Bottomless Pool (unlocked left half only)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Equip Diviner's Wand
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip {3}");
addTarget(playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Pithing Needle naming the locked side
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pithing Needle");
setChoice(playerA, "Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Validate that the room can activate the gained ability
checkPlayableAbility("Room can use Diviner's Wand ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA,
"{4}: Draw a card.", true);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Unlock the other side
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{U}: Unlock the right half.");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Validate that you can no longer activate the ability
checkPlayableAbility("Room cannot use Diviner's Wand ability after unlock", 1, PhaseStep.PRECOMBAT_MAIN,
playerA, "{4}: Draw a card.", false);
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Verify the room is now fully unlocked
assertPermanentCount(playerA, "Bottomless Pool // Locker Room", 1);
}
// Test converting one permanent into one room, then another (the room halves
// should STAY UNLOCKED on the appropriate side!)
@Test
public void testUnlockingPermanentMakeCopyOfOtherRoom() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
//
// Surgical Suite {1}{W}
// When you unlock this door, return target creature card with mana value 3 or
// less from your graveyard to the battlefield.
// Hospital Room {3}{W}
// Whenever you attack, put a +1/+1 counter on target attacking creature.
//
// Mirage Mirror {3}
// {3}: Mirage Mirror becomes a copy of target artifact, creature, enchantment,
// or
// land until end of turn.
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.HAND, playerA, "Surgical Suite // Hospital Room");
addCard(Zone.BATTLEFIELD, playerA, "Mirage Mirror");
addCard(Zone.BATTLEFIELD, playerA, "Tundra", 20);
addCard(Zone.BATTLEFIELD, playerA, "Memnite", 1);
// Cast Bottomless Pool (unlocked left half only)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{U}: Unlock the right half.");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Surgical Suite (unlocked left half only)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Surgical Suite");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: {this} becomes a copy");
addTarget(playerA, "Bottomless Pool // Locker Room");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{U}: Unlock the right half.");
setStopAt(3, PhaseStep.PRECOMBAT_MAIN);
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: {this} becomes a copy");
addTarget(playerA, "Surgical Suite");
attack(3, playerA, "Memnite");
addTarget(playerA, "Memnite");
setStopAt(3, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Verify unlocked Bottomless pool
assertPermanentCount(playerA, "Bottomless Pool // Locker Room", 1);
// Verify unlocked Surgical Suite
assertPermanentCount(playerA, "Surgical Suite", 1);
// Verify mirage mirror is Hospital Room
assertPermanentCount(playerA, "Hospital Room", 1);
// Memnite got a buff
assertPowerToughness(playerA, "Memnite", 2, 2);
}
@Test
public void testSakashimaCopiesRoomCard() {
skipInitShuffling();
// Bottomless Pool {U} When you unlock this door, return up to one target
// creature to its owner's hand.
// Locker Room {4}{U} Whenever one or more creatures you control deal combat
// damage to a player, draw a card.
// Sakashima the Impostor {2}{U}{U}
// Legendary Creature Human Rogue
// You may have Sakashima the Impostor enter the battlefield as a copy of any
// creature on the battlefield,
// except its name is Sakashima the Impostor, it's legendary in addition to its
// other types,
// and it has "{2}{U}{U}: Return Sakashima the Impostor to its owner's hand at
// the beginning of the next end step."
addCard(Zone.HAND, playerA, "Bottomless Pool // Locker Room");
addCard(Zone.BATTLEFIELD, playerA, "Island", 10);
addCard(Zone.HAND, playerB, "Sakashima the Impostor");
addCard(Zone.BATTLEFIELD, playerB, "Island", 10);
// Cast Bottomless Pool (unlocked left half only)
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bottomless Pool");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
addTarget(playerA, TestPlayer.TARGET_SKIP);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// Cast Sakashima copying the room
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Sakashima the Impostor");
setChoice(playerB, "Yes"); // Choose to copy
waitStackResolved(2, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{U}: Unlock the right half.");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setStopAt(2, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
// Verify Sakashima entered and is copying the room
assertPermanentCount(playerB, "Sakashima the Impostor", 1);
}
}

View file

@ -2,7 +2,6 @@ package org.mage.test.cards.single.cmm;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
@ -19,7 +18,6 @@ public class BattleAtTheHelvaultTest extends CardTestPlayerBase {
*/
private static final String battle = "Battle at the Helvault";
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay() {
addCard(Zone.HAND, playerA, battle, 1);

View file

@ -3,7 +3,6 @@ package org.mage.test.cards.single.fic;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
@ -23,7 +22,6 @@ public class SummonIxionTest extends CardTestPlayerBase {
*/
private static final String ixion = "Summon: Ixion";
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay() {
addCard(Zone.HAND, playerA, ixion, 1);

View file

@ -2,7 +2,6 @@ package org.mage.test.cards.single.pip;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
@ -21,7 +20,6 @@ public class Vault13DwellersJourneyTest extends CardTestPlayerBase {
*/
private static final String vault = "Vault 13: Dweller's Journey";
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay_ReturnOne() {
addCard(Zone.HAND, playerA, vault, 1);
@ -49,7 +47,6 @@ public class Vault13DwellersJourneyTest extends CardTestPlayerBase {
assertPermanentCount(playerA, "Memnite", 1);
assertLife(playerA, 20 + 2);
}
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay_Return() {
addCard(Zone.HAND, playerA, vault, 1);
@ -81,7 +78,6 @@ public class Vault13DwellersJourneyTest extends CardTestPlayerBase {
assertLife(playerA, 20 + 2);
}
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay_NoReturn() {
addCard(Zone.HAND, playerA, vault, 1);

View file

@ -0,0 +1,74 @@
package org.mage.test.cards.single.shm;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author xenohedron
*/
public class GlamerSpinnersTest extends CardTestPlayerBase {
/*
Glamer Spinners
{4}{WU}
Creature - Faerie Wizard
Flash
Flying
When Glamer Spinners enters the battlefield, attach all Auras enchanting target permanent to another permanent with the same controller.
2/4
*/
private static final String glamerSpinners = "Glamer Spinners";
/*
Feral Invocation
{2}{G}
Enchantment - Aura
Flash <i>(You may cast this spell any time you could cast an instant.)</i>
Enchant creature
Enchanted creature gets +2/+2.
*/
private static final String feralInvocation = "Feral Invocation";
/*
Memnite
{0}
Artifact Creature - Construct
1/1
*/
private static final String memnite = "Memnite";
/*
Kraken Hatchling
{U}
Creature - Kraken
0/4
*/
private static final String krakenHatchling = "Kraken Hatchling";
@Test
public void testGlamerSpinners() {
addCard(Zone.HAND, playerA, glamerSpinners);
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.HAND, playerB, feralInvocation);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 3);
addCard(Zone.BATTLEFIELD, playerB, memnite);
addCard(Zone.BATTLEFIELD, playerB, krakenHatchling);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, feralInvocation, memnite);
checkPT("enchanted", 1, PhaseStep.BEGIN_COMBAT, playerB, memnite, 3, 3);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, glamerSpinners);
addTarget(playerA, memnite);
setChoice(playerA, krakenHatchling);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, glamerSpinners, 1);
assertPowerToughness(playerB, memnite, 1, 1);
assertPowerToughness(playerB, krakenHatchling, 2, 6);
}
}

View file

@ -1,8 +1,10 @@
package org.mage.test.cards.single.spm;
import mage.constants.ManaType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
@ -39,6 +41,16 @@ public class ElectroAssaultingBatteryTest extends CardTestPlayerBase {
*/
private static final String lightningBolt = "Lightning Bolt";
/*
Final Showdown
{W}
Instant
Spree
+ {1} -- All creatures lose all abilities until end of turn.
+ {1} -- Choose a creature you control. It gains indestructible until end of turn.
+ {3}{W}{W} -- Destroy all creatures.
*/
private static final String finalShowdown = "Final Showdown";
@Test
public void testElectroAssaultingBattery() {
@ -62,4 +74,34 @@ public class ElectroAssaultingBatteryTest extends CardTestPlayerBase {
assertLife(playerB, 20 - 4);
}
@Test
public void testElectroAssaultingBatteryFinalShowdown() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, electroAssaultingBattery);
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
addCard(Zone.BATTLEFIELD, playerB, "Plains", 7);
addCard(Zone.HAND, playerB, finalShowdown);
addCard(Zone.HAND, playerA, lightningBolt);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, playerB);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, finalShowdown);
setModeChoice(playerB, "1");
setModeChoice(playerB, "3");
setModeChoice(playerB, TestPlayer.MODE_SKIP);
checkManaPool("Should have 1 red mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 1);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, lightningBolt, 1);
assertGraveyardCount(playerB, finalShowdown, 1);
assertGraveyardCount(playerA, electroAssaultingBattery, 1);
assertManaPool(playerA, ManaType.RED, 0); // Electro's ability is gone
}
}

View file

@ -2,7 +2,6 @@ package org.mage.test.cards.single.who;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
@ -18,7 +17,6 @@ public class DayOfTheMoonTest extends CardTestPlayerBase {
*/
private static final String day = "Day of the Moon";
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay() {
addCard(Zone.HAND, playerA, day, 1);

View file

@ -2,7 +2,6 @@ package org.mage.test.cards.single.who;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
@ -20,7 +19,6 @@ public class TheWarGamesTest extends CardTestPlayerBase {
*/
private static final String war = "The War Games";
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay_NoExile() {
addCard(Zone.HAND, playerA, war, 1);
@ -63,7 +61,6 @@ public class TheWarGamesTest extends CardTestPlayerBase {
assertLife(playerB, 20 - 6 - 9);
}
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay_Exile() {
addCard(Zone.HAND, playerA, war, 1);

View file

@ -2,7 +2,6 @@ package org.mage.test.cards.single.who;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
@ -20,7 +19,6 @@ public class TrialOfATimeLordTest extends CardTestPlayerBase {
*/
private static final String trial = "Trial of a Time Lord";
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay() {
addCard(Zone.HAND, playerA, trial, 1);

View file

@ -3,7 +3,6 @@ package org.mage.test.cards.single.woe;
import mage.abilities.keyword.FlyingAbility;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
@ -21,7 +20,6 @@ public class ThePrincessTakesFlightTest extends CardTestPlayerBase {
*/
private static final String flight = "The Princess Takes Flight";
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void test_SimplePlay() {
addCard(Zone.HAND, playerA, flight, 1);
@ -54,7 +52,6 @@ public class ThePrincessTakesFlightTest extends CardTestPlayerBase {
assertExileCount(playerB, "Memnite", 0);
assertPermanentCount(playerB, "Memnite", 1);
}
@Ignore // TODO: goal of #11619 is to fix this nicely
@Test
public void testFlicker() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
@ -82,4 +79,73 @@ public class ThePrincessTakesFlightTest extends CardTestPlayerBase {
assertPermanentCount(playerA, "Memnite", 1);
assertGraveyardCount(playerA, flight, 1);
}
@Test
public void test_TokenCopy() {
addCard(Zone.HAND, playerA, flight, 1);
addCard(Zone.HAND, playerA, "Swords to Plowshares", 1);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
addCard(Zone.BATTLEFIELD, playerA, "Ondu Spiritdancer", 1);
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, flight);
setChoice(playerA, "I - ");
addTarget(playerA, "Memnite");
setChoice(playerA, true);
addTarget(playerA, "Grizzly Bears");
checkExileCount("after I, exiled Memnite", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Memnite", 1);
checkExileCount("after I, exiled Grizzly Bears", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Grizzly Bears", 1);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Swords to Plowshares", "Ondu Spiritdancer");
// turn 3
setChoice(playerA, "II - ");
// No targets available
// turn 5
setChoice(playerA, "III - ");
setStrictChooseMode(true);
setStopAt(5, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertExileCount(playerA, "Grizzly Bears", 0);
assertExileCount(playerB, "Memnite", 0);
assertPermanentCount(playerA, "Grizzly Bears", 1);
assertPermanentCount(playerB, "Memnite", 1);
}
@Test
public void test_SpellCopy() {
addCard(Zone.HAND, playerA, flight, 1);
addCard(Zone.HAND, playerA, "Swords to Plowshares", 1);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
addCard(Zone.BATTLEFIELD, playerA, "The Sixth Doctor", 1);
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 1);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, flight);
addTarget(playerA, "Memnite");
addTarget(playerA, "Grizzly Bears");
checkExileCount("after I, exiled Memnite", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Memnite", 1);
checkExileCount("after I, exiled Grizzly Bears", 1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Grizzly Bears", 1);
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Swords to Plowshares", "The Sixth Doctor");
// turn 3
setChoice(playerA, "II - ");
// No targets available
// turn 5
setChoice(playerA, "III - ");
setStrictChooseMode(true);
setStopAt(5, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertExileCount(playerA, "Grizzly Bears", 0);
assertExileCount(playerB, "Memnite", 0);
assertPermanentCount(playerA, "Grizzly Bears", 1);
assertPermanentCount(playerB, "Memnite", 1);
}
}

View file

@ -143,6 +143,28 @@ public class CommanderDeckValidationTest extends MageTestPlayerBase {
);
}
@Test
public void testPartnerVariants() {
DeckTester deckTester = new DeckTester(new Commander());
deckTester.addMaindeck("Swamp", 98);
deckTester.addSideboard("Ellie, Vengeful Hunter", 1);
deckTester.addSideboard("Joel, Resolute Survivor", 1);
deckTester.validate("You can have two commanders if they both have the same Partner variant");
}
@Test(expected = AssertionError.class)
public void testPartnerVariants2() {
DeckTester deckTester = new DeckTester(new Commander());
deckTester.addMaindeck("Mountain", 98);
deckTester.addSideboard("Ellie, Vengeful Hunter", 1);
deckTester.addSideboard("Atreus, Impulsive Son", 1);
deckTester.validate("You can't have two commanders if they don't have the same Partner variant");
}
@Test()
public void testVehicles1() {
DeckTester deckTester = new DeckTester(new Commander());

View file

@ -56,8 +56,9 @@ public class AliasesApiTest extends CardTestPlayerBase {
Assert.assertTrue(CardUtil.haveSameNames(splitCard1, "Armed // Dangerous", currentGame));
Assert.assertTrue(CardUtil.haveSameNames(splitCard1, splitCard1));
Assert.assertFalse(CardUtil.haveSameNames(splitCard1, "Other", currentGame));
Assert.assertFalse(CardUtil.haveSameNames(splitCard1, "Other // Dangerous", currentGame));
Assert.assertFalse(CardUtil.haveSameNames(splitCard1, "Armed // Other", currentGame));
// The below don't seem to matter/be correct, so they've been disabled.
//Assert.assertFalse(CardUtil.haveSameNames(splitCard1, "Other // Dangerous", currentGame));
//Assert.assertFalse(CardUtil.haveSameNames(splitCard1, "Armed // Other", currentGame));
Assert.assertFalse(CardUtil.haveSameNames(splitCard1, splitCard2));
// name with face down spells: face down spells don't have names, see https://github.com/magefree/mage/issues/6569

View file

@ -33,6 +33,7 @@ import mage.game.events.BatchEvent;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentToken;
import mage.game.stack.Spell;
import mage.game.stack.StackAbility;
import mage.players.Player;
@ -1707,15 +1708,17 @@ public abstract class AbilityImpl implements Ability {
private int getCurrentSourceObjectZoneChangeCounter(Game game){
int zcc = game.getState().getZoneChangeCounter(getSourceId());
// TODO: Enable this, #13710
/*if (game.getPermanentEntering(getSourceId()) != null){
Permanent p = game.getPermanentEntering(getSourceId());
if (p != null && !(p instanceof PermanentToken)){
// If the triggered ability triggered while the permanent is entering the battlefield
// then add 1 zcc so that it triggers as if the permanent was already on the battlefield
// So "Enters with counters" causes "Whenever counters are placed" to trigger with battlefield zcc
// Particularly relevant for Sagas, which always involve both
// Note that this does NOT apply to "As ~ ETB" effects, those still use the stack zcc
// TODO: JayDi doesn't like this solution, consider finding another one.
zcc += 1;
}*/
// However, tokens don't change their zcc upon entering the battlefield, so don't add for them
}
return zcc;
}

View file

@ -9,7 +9,6 @@ import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
/**
* TODO: This only triggers off of enchantments entering as the room mechanic hasn't been implemented yet
*
* @author TheElk801
*/
@ -41,15 +40,23 @@ public class EerieAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD;
return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD
|| event.getType() == GameEvent.EventType.ROOM_FULLY_UNLOCKED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!isControlledBy(event.getPlayerId())) {
return false;
if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD) {
if (!isControlledBy(event.getPlayerId())) {
return false;
}
Permanent permanent = game.getPermanent(event.getTargetId());
return permanent != null && permanent.isEnchantment(game);
}
Permanent permanent = game.getPermanent(event.getTargetId());
return permanent != null && permanent.isEnchantment(game);
if (event.getType() == GameEvent.EventType.ROOM_FULLY_UNLOCKED) {
return isControlledBy(event.getPlayerId());
}
return false;
}
}

View file

@ -1,5 +1,6 @@
package mage.abilities.common;
import mage.MageObjectReference;
import mage.abilities.BatchTriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
@ -13,13 +14,17 @@ import mage.game.events.DamagedPlayerEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import mage.target.targetpointer.FixedTargets;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Xanderhall, xenohedron
*/
public class OneOrMoreDamagePlayerTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPlayerEvent> {
public class OneOrMoreDamagePlayerTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility<DamagedPlayerEvent> {
private final SetTargetPointer setTargetPointer;
private final FilterPermanent filter;
@ -85,6 +90,16 @@ public class OneOrMoreDamagePlayerTriggeredAbility extends TriggeredAbilityImpl
case PLAYER:
this.getAllEffects().setTargetPointer(new FixedTarget(event.getTargetId()));
break;
case PERMANENT:
Set<MageObjectReference> attackerSet = events
.stream()
.map(GameEvent::getSourceId)
.map(game::getPermanent)
.filter(Objects::nonNull)
.map(permanent -> new MageObjectReference(permanent, game))
.collect(Collectors.toSet());
this.getAllEffects().setTargetPointer(new FixedTargets(attackerSet));
break;
case NONE:
break;
default:

View file

@ -0,0 +1,106 @@
package mage.abilities.common;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
import mage.abilities.condition.common.RoomHalfLockedCondition;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.constants.TimingRule;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author oscscull
* Special action for Room cards to unlock a locked half by paying its
* mana
* cost.
* This ability is only present if the corresponding half is currently
* locked.
*/
public class RoomUnlockAbility extends SpecialAction {
private final boolean isLeftHalf;
public RoomUnlockAbility(ManaCosts costs, boolean isLeftHalf) {
super(Zone.BATTLEFIELD, null);
this.addCost(costs);
this.isLeftHalf = isLeftHalf;
this.timing = TimingRule.SORCERY;
// only works if the relevant half is *locked*
if (isLeftHalf) {
this.setCondition(RoomHalfLockedCondition.LEFT);
} else {
this.setCondition(RoomHalfLockedCondition.RIGHT);
}
// Adds the effect to pay + unlock the half
this.addEffect(new RoomUnlockHalfEffect(isLeftHalf));
}
protected RoomUnlockAbility(final RoomUnlockAbility ability) {
super(ability);
this.isLeftHalf = ability.isLeftHalf;
}
@Override
public RoomUnlockAbility copy() {
return new RoomUnlockAbility(this);
}
@Override
public String getRule() {
StringBuilder sb = new StringBuilder();
sb.append(getManaCostsToPay().getText()).append(": ");
sb.append("Unlock the ");
sb.append(isLeftHalf ? "left" : "right").append(" half.");
sb.append(" <i>(Activate only as a sorcery, and only if the ");
sb.append(isLeftHalf ? "left" : "right").append(" half is locked.)</i>");
return sb.toString();
}
}
/**
* Allows you to pay to unlock the door
*/
class RoomUnlockHalfEffect extends OneShotEffect {
private final boolean isLeftHalf;
public RoomUnlockHalfEffect(boolean isLeftHalf) {
super(Outcome.Neutral);
this.isLeftHalf = isLeftHalf;
staticText = "unlock the " + (isLeftHalf ? "left" : "right") + " half";
}
private RoomUnlockHalfEffect(final RoomUnlockHalfEffect effect) {
super(effect);
this.isLeftHalf = effect.isLeftHalf;
}
@Override
public RoomUnlockHalfEffect copy() {
return new RoomUnlockHalfEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return false;
}
if (isLeftHalf && permanent.isLeftDoorUnlocked()) {
return false;
}
if (!isLeftHalf && permanent.isRightDoorUnlocked()) {
return false;
}
return permanent.unlockDoor(game, source, isLeftHalf);
}
}

View file

@ -0,0 +1,43 @@
package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
/**
* Triggered ability for "when you unlock this door" effects
*
* @author oscscull
*/
public class UnlockThisDoorTriggeredAbility extends TriggeredAbilityImpl {
private final boolean isLeftHalf;
public UnlockThisDoorTriggeredAbility(Effect effect, boolean optional, boolean isLeftHalf) {
super(Zone.BATTLEFIELD, effect, optional);
this.isLeftHalf = isLeftHalf;
this.setTriggerPhrase("When you unlock this door, ");
}
private UnlockThisDoorTriggeredAbility(final UnlockThisDoorTriggeredAbility ability) {
super(ability);
this.isLeftHalf = ability.isLeftHalf;
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DOOR_UNLOCKED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return event.getTargetId().equals(getSourceId()) && event.getFlag() == isLeftHalf;
}
@Override
public UnlockThisDoorTriggeredAbility copy() {
return new UnlockThisDoorTriggeredAbility(this);
}
}

View file

@ -0,0 +1,34 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author oscscull
* Checks if a Permanent's specified half is LOCKED (i.e., NOT unlocked).
*/
public enum RoomHalfLockedCondition implements Condition {
LEFT(true),
RIGHT(false);
private final boolean checkLeft;
RoomHalfLockedCondition(boolean checkLeft) {
this.checkLeft = checkLeft;
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return false;
}
// Return true if the specified half is NOT unlocked
return checkLeft ? !permanent.isLeftDoorUnlocked() : !permanent.isRightDoorUnlocked();
}
}

View file

@ -0,0 +1,142 @@
package mage.abilities.effects.common;
import mage.MageObject;
import mage.Mana;
import mage.abilities.Ability;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.Card;
import mage.cards.SplitCard;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
/**
* @author oscscull
* Continuous effect that sets the name and mana value of a Room permanent based
* on its unlocked halves.
*
* Functions as a characteristic-defining ability.
* 709.5. Some split cards are permanent cards with a single shared type line.
* A shared type line on such an object represents two static abilities that
* function on the battlefield.
* These are "As long as this permanent doesn't have the 'left half unlocked'
* designation, it doesn't have the name, mana cost, or rules text of this
* object's left half"
* and "As long as this permanent doesn't have the 'right half unlocked'
* designation, it doesn't have the name, mana cost, or rules text of this
* object's right half."
* These abilities, as well as which half of that permanent a characteristic is
* in, are part of that object's copiable values.
*/
public class RoomCharacteristicsEffect extends ContinuousEffectImpl {
public RoomCharacteristicsEffect() {
super(Duration.WhileOnBattlefield, Layer.PTChangingEffects_7, SubLayer.CharacteristicDefining_7a,
Outcome.Neutral);
staticText = "";
}
private RoomCharacteristicsEffect(final RoomCharacteristicsEffect effect) {
super(effect);
}
@Override
public RoomCharacteristicsEffect copy() {
return new RoomCharacteristicsEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return false;
}
Card roomCardBlueprint;
// Handle copies
if (permanent.isCopy()) {
MageObject copiedObject = permanent.getCopyFrom();
if (copiedObject instanceof PermanentCard) {
roomCardBlueprint = ((PermanentCard) copiedObject).getCard();
} else if (copiedObject instanceof Card) {
roomCardBlueprint = (Card) copiedObject;
} else {
roomCardBlueprint = permanent.getMainCard();
}
} else {
roomCardBlueprint = permanent.getMainCard();
}
if (!(roomCardBlueprint instanceof SplitCard)) {
return false;
}
SplitCard roomCard = (SplitCard) roomCardBlueprint;
// Set the name based on unlocked halves
String newName = "";
boolean isLeftUnlocked = permanent.isLeftDoorUnlocked();
if (isLeftUnlocked && roomCard.getLeftHalfCard() != null) {
newName += roomCard.getLeftHalfCard().getName();
}
boolean isRightUnlocked = permanent.isRightDoorUnlocked();
if (isRightUnlocked && roomCard.getRightHalfCard() != null) {
if (!newName.isEmpty()) {
newName += " // "; // Split card name separator
}
newName += roomCard.getRightHalfCard().getName();
}
permanent.setName(newName);
// Set the mana value based on unlocked halves
// Create a new Mana object to accumulate the costs
Mana totalManaCost = new Mana();
// Add the mana from the left half's cost to our total Mana object
if (isLeftUnlocked) {
ManaCosts leftHalfManaCost = null;
if (roomCard.getLeftHalfCard() != null && roomCard.getLeftHalfCard().getSpellAbility() != null) {
leftHalfManaCost = roomCard.getLeftHalfCard().getSpellAbility().getManaCosts();
}
if (leftHalfManaCost != null) {
totalManaCost.add(leftHalfManaCost.getMana());
}
}
// Add the mana from the right half's cost to our total Mana object
if (isRightUnlocked) {
ManaCosts rightHalfManaCost = null;
if (roomCard.getRightHalfCard() != null && roomCard.getRightHalfCard().getSpellAbility() != null) {
rightHalfManaCost = roomCard.getRightHalfCard().getSpellAbility().getManaCosts();
}
if (rightHalfManaCost != null) {
totalManaCost.add(rightHalfManaCost.getMana());
}
}
String newManaCostString = totalManaCost.toString();
ManaCostsImpl newManaCosts;
// If both halves are locked or total 0, it's 0mv.
if (newManaCostString.isEmpty() || totalManaCost.count() == 0) {
newManaCosts = new ManaCostsImpl<>("");
} else {
newManaCosts = new ManaCostsImpl<>(newManaCostString);
}
permanent.setManaCost(newManaCosts);
return true;
}
}

View file

@ -34,6 +34,10 @@ public class SetBasePowerToughnessAllEffect extends ContinuousEffectImpl {
this(StaticValue.get(power), StaticValue.get(toughness), duration, filter);
}
public SetBasePowerToughnessAllEffect(DynamicValue stats, Duration duration, FilterPermanent filter) {
this(stats, stats, duration, filter);
}
public SetBasePowerToughnessAllEffect(DynamicValue power, DynamicValue toughness, Duration duration, FilterPermanent filter) {
super(duration, Layer.PTChangingEffects_7, SubLayer.SetPT_7b, Outcome.BoostCreature);
this.power = power;

View file

@ -1,38 +0,0 @@
package mage.abilities.keyword;
import mage.abilities.MageSingleton;
import mage.abilities.StaticAbility;
import mage.constants.Zone;
import java.io.ObjectStreamException;
/**
* @author LevelX2
*/
public class PartnerFatherAndSonAbility extends StaticAbility implements MageSingleton {
private static final PartnerFatherAndSonAbility instance = new PartnerFatherAndSonAbility();
private Object readResolve() throws ObjectStreamException {
return instance;
}
public static PartnerFatherAndSonAbility getInstance() {
return instance;
}
private PartnerFatherAndSonAbility() {
super(Zone.BATTLEFIELD, null);
}
@Override
public String getRule() {
return "Partner&mdash;Father & son <i>(You can have two commanders if both have this ability.)</i>";
}
@Override
public PartnerFatherAndSonAbility copy() {
return instance;
}
}

View file

@ -1,38 +0,0 @@
package mage.abilities.keyword;
import mage.abilities.MageSingleton;
import mage.abilities.StaticAbility;
import mage.constants.Zone;
import java.io.ObjectStreamException;
/**
* @author LevelX2
*/
public class PartnerSurvivorsAbility extends StaticAbility implements MageSingleton {
private static final PartnerSurvivorsAbility instance = new PartnerSurvivorsAbility();
private Object readResolve() throws ObjectStreamException {
return instance;
}
public static PartnerSurvivorsAbility getInstance() {
return instance;
}
private PartnerSurvivorsAbility() {
super(Zone.BATTLEFIELD, null);
}
@Override
public String getRule() {
return "Partner&mdash;Survivors <i>(You can have two commanders if both have this ability.)</i>";
}
@Override
public PartnerSurvivorsAbility copy() {
return instance;
}
}

View file

@ -0,0 +1,77 @@
package mage.abilities.keyword;
import mage.MageIdentifier;
import mage.abilities.SpellAbility;
import mage.abilities.costs.common.ReturnToHandChosenControlledPermanentCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
import mage.constants.PhaseStep;
import mage.constants.SpellAbilityType;
import mage.constants.TimingRule;
import mage.constants.Zone;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.permanent.AttackingPredicate;
import mage.filter.predicate.permanent.UnblockedPredicate;
import mage.game.Game;
import mage.target.common.TargetControlledPermanent;
import java.util.Set;
/**
* @author TheElk801
*/
public class SneakAbility extends SpellAbility {
public static final String SNEAK_ACTIVATION_VALUE_KEY = "sneakActivation";
private static final FilterControlledPermanent filter = new FilterControlledPermanent("unblocked attacker you control");
static {
filter.add(UnblockedPredicate.instance);
filter.add(AttackingPredicate.instance);
}
public SneakAbility(Card card, String manaString) {
super(card.getSpellAbility());
this.newId();
this.setCardName(card.getName() + " with Sneak");
timing = TimingRule.INSTANT;
zone = Zone.HAND;
spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.clearManaCosts();
this.clearManaCostsToPay();
this.addCost(new ManaCostsImpl<>(manaString));
this.addCost(new ReturnToHandChosenControlledPermanentCost(new TargetControlledPermanent(filter)));
this.setRuleAtTheTop(true);
}
protected SneakAbility(final SneakAbility ability) {
super(ability);
}
@Override
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (!super.activate(game, allowedIdentifiers, noMana)
|| game.getStep().getType() != PhaseStep.DECLARE_BLOCKERS) {
return false;
}
this.setCostsTag(SNEAK_ACTIVATION_VALUE_KEY, null);
return true;
}
@Override
public SneakAbility copy() {
return new SneakAbility(this);
}
@Override
public String getRule() {
StringBuilder sb = new StringBuilder("Sneak ");
sb.append(getManaCosts().getText());
sb.append(" <i>(You may cast this spell for ");
sb.append(getManaCosts().getText());
sb.append(" if you also return an unblocked attacker you control to hand during the declare blockers step.)</i>");
return sb.toString();
}
}

View file

@ -0,0 +1,215 @@
package mage.cards;
import java.util.UUID;
import mage.abilities.Abilities;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.common.RoomUnlockAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.common.UnlockThisDoorTriggeredAbility;
import mage.abilities.condition.common.RoomHalfLockedCondition;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.decorator.ConditionalContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.RoomCharacteristicsEffect;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.SpellAbilityType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentToken;
import mage.abilities.effects.common.continuous.LoseAbilitySourceEffect;
/**
* @author oscscull
*/
public abstract class RoomCard extends SplitCard {
private SpellAbilityType lastCastHalf = null;
protected RoomCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, String costsLeft,
String costsRight, SpellAbilityType spellAbilityType) {
super(ownerId, setInfo, costsLeft, costsRight, spellAbilityType, types);
String[] names = setInfo.getName().split(" // ");
leftHalfCard = new RoomCardHalfImpl(
this.getOwnerId(), new CardSetInfo(names[0], setInfo.getExpansionSetCode(), setInfo.getCardNumber(),
setInfo.getRarity(), setInfo.getGraphicInfo()),
types, costsLeft, this, SpellAbilityType.SPLIT_LEFT);
rightHalfCard = new RoomCardHalfImpl(
this.getOwnerId(), new CardSetInfo(names[1], setInfo.getExpansionSetCode(), setInfo.getCardNumber(),
setInfo.getRarity(), setInfo.getGraphicInfo()),
types, costsRight, this, SpellAbilityType.SPLIT_RIGHT);
}
protected RoomCard(RoomCard card) {
super(card);
this.lastCastHalf = card.lastCastHalf;
}
public SpellAbilityType getLastCastHalf() {
return lastCastHalf;
}
public void setLastCastHalf(SpellAbilityType lastCastHalf) {
this.lastCastHalf = lastCastHalf;
}
protected void addRoomAbilities(Ability leftAbility, Ability rightAbility) {
getLeftHalfCard().addAbility(leftAbility);
getRightHalfCard().addAbility(rightAbility);
this.addAbility(leftAbility.copy());
this.addAbility(rightAbility.copy());
// Add the one-shot effect to unlock a door on cast -> ETB
Ability entersAbility = new EntersBattlefieldAbility(new RoomEnterUnlockEffect());
entersAbility.setRuleVisible(false);
this.addAbility(entersAbility);
// Remove locked door abilities - keeping unlock triggers (or they won't trigger
// when unlocked)
if (leftAbility != null && !(leftAbility instanceof UnlockThisDoorTriggeredAbility)) {
Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new ConditionalContinuousEffect(
new LoseAbilitySourceEffect(leftAbility, Duration.WhileOnBattlefield),
RoomHalfLockedCondition.LEFT, "")).setRuleVisible(false);
this.addAbility(ability);
}
if (rightAbility != null && !(rightAbility instanceof UnlockThisDoorTriggeredAbility)) {
Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new ConditionalContinuousEffect(
new LoseAbilitySourceEffect(rightAbility, Duration.WhileOnBattlefield),
RoomHalfLockedCondition.RIGHT, "")).setRuleVisible(false);
this.addAbility(ability);
}
// Add the Special Action to unlock doors.
// These will ONLY be active if the corresponding half is LOCKED!
if (leftAbility != null) {
ManaCosts leftHalfManaCost = null;
if (this.getLeftHalfCard() != null && this.getLeftHalfCard().getSpellAbility() != null) {
leftHalfManaCost = this.getLeftHalfCard().getSpellAbility().getManaCosts();
}
RoomUnlockAbility leftUnlockAbility = new RoomUnlockAbility(leftHalfManaCost, true);
this.addAbility(leftUnlockAbility.setRuleAtTheTop(true));
}
if (rightAbility != null) {
ManaCosts rightHalfManaCost = null;
if (this.getRightHalfCard() != null && this.getRightHalfCard().getSpellAbility() != null) {
rightHalfManaCost = this.getRightHalfCard().getSpellAbility().getManaCosts();
}
RoomUnlockAbility rightUnlockAbility = new RoomUnlockAbility(rightHalfManaCost, false);
this.addAbility(rightUnlockAbility.setRuleAtTheTop(true));
}
this.addAbility(new RoomAbility());
}
@Override
public Abilities<Ability> getAbilities() {
return this.abilities;
}
@Override
public Abilities<Ability> getAbilities(Game game) {
return this.abilities;
}
@Override
public void setZone(Zone zone, Game game) {
super.setZone(zone, game);
if (zone == Zone.BATTLEFIELD) {
game.setZone(getLeftHalfCard().getId(), Zone.OUTSIDE);
game.setZone(getRightHalfCard().getId(), Zone.OUTSIDE);
return;
}
game.setZone(getLeftHalfCard().getId(), zone);
game.setZone(getRightHalfCard().getId(), zone);
}
}
class RoomEnterUnlockEffect extends OneShotEffect {
public RoomEnterUnlockEffect() {
super(Outcome.Neutral);
staticText = "";
}
private RoomEnterUnlockEffect(final RoomEnterUnlockEffect effect) {
super(effect);
}
@Override
public RoomEnterUnlockEffect copy() {
return new RoomEnterUnlockEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return false;
}
if (permanent.wasRoomUnlockedOnCast()) {
return false;
}
permanent.unlockRoomOnCast(game);
RoomCard roomCard = null;
// Get the parent card to access the lastCastHalf variable
if (permanent instanceof PermanentToken) {
Card mainCard = permanent.getMainCard();
if (mainCard instanceof RoomCard) {
roomCard = (RoomCard) mainCard;
}
} else {
Card card = game.getCard(permanent.getId());
if (card instanceof RoomCard) {
roomCard = (RoomCard) card;
}
}
if (roomCard == null) {
return true;
}
SpellAbilityType lastCastHalf = roomCard.getLastCastHalf();
if (lastCastHalf == SpellAbilityType.SPLIT_LEFT || lastCastHalf == SpellAbilityType.SPLIT_RIGHT) {
roomCard.setLastCastHalf(null);
return permanent.unlockDoor(game, source, lastCastHalf == SpellAbilityType.SPLIT_LEFT);
}
return true;
}
}
// For the overall Room card flavor text and mana value effect.
class RoomAbility extends SimpleStaticAbility {
public RoomAbility() {
super(Zone.ALL, null);
this.setRuleVisible(true);
this.setRuleAtTheTop(true);
this.addEffect(new RoomCharacteristicsEffect());
}
protected RoomAbility(final RoomAbility ability) {
super(ability);
}
@Override
public String getRule() {
return "<i>(You may cast either half. That door unlocks on the battlefield. " +
"As a sorcery, you may pay the mana cost of a locked door to unlock it.)</i>";
}
@Override
public RoomAbility copy() {
return new RoomAbility(this);
}
}

View file

@ -0,0 +1,9 @@
package mage.cards;
/**
* @author oscscull
*/
public interface RoomCardHalf extends SplitCardHalf {
@Override
RoomCardHalf copy();
}

View file

@ -0,0 +1,68 @@
package mage.cards;
import mage.abilities.SpellAbility;
import mage.constants.CardType;
import mage.constants.SpellAbilityType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.Game;
import java.util.UUID;
/**
* @author oscscull
*/
public class RoomCardHalfImpl extends SplitCardHalfImpl implements RoomCardHalf {
public RoomCardHalfImpl(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costs,
RoomCard splitCardParent, SpellAbilityType spellAbilityType) {
super(ownerId, setInfo, cardTypes, costs, splitCardParent, spellAbilityType);
this.addSubType(SubType.ROOM);
}
protected RoomCardHalfImpl(final RoomCardHalfImpl card) {
super(card);
}
@Override
public RoomCardHalfImpl copy() {
return new RoomCardHalfImpl(this);
}
@Override
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
SpellAbilityType abilityType = ability.getSpellAbilityType();
RoomCard parentCard = (RoomCard) this.getParentCard();
if (parentCard != null) {
if (abilityType == SpellAbilityType.SPLIT_LEFT) {
parentCard.setLastCastHalf(SpellAbilityType.SPLIT_LEFT);
} else if (abilityType == SpellAbilityType.SPLIT_RIGHT) {
parentCard.setLastCastHalf(SpellAbilityType.SPLIT_RIGHT);
} else {
parentCard.setLastCastHalf(null);
}
}
return super.cast(game, fromZone, ability, controllerId);
}
/**
* A room half is used for the spell half on the stack, similar to a normal split card.
* On the stack, it has only one name, mana cost, etc.
* However, in the hand and on the battlefield, it is the full card, which is the parent of the half.
* This code helps to ensure that the parent, and not the halves, are the only part of the card active on the battlefield.
* This is important for example when that half has a triggered ability etc that otherwise might trigger twice (once for the parent, once for the half)
* - in the case that the half was an object on the battlefield. In all other cases, they should all move together.
*/
@Override
public void setZone(Zone zone, Game game) {
if (zone == Zone.BATTLEFIELD) {
game.setZone(splitCardParent.getId(), zone);
game.setZone(splitCardParent.getLeftHalfCard().getId(), Zone.OUTSIDE);
game.setZone(splitCardParent.getRightHalfCard().getId(), Zone.OUTSIDE);
return;
}
super.setZone(zone, game);
}
}

View file

@ -36,6 +36,15 @@ public abstract class SplitCard extends CardImpl implements CardWithHalves {
rightHalfCard = new SplitCardHalfImpl(this.getOwnerId(), new CardSetInfo(names[1], setInfo.getExpansionSetCode(), setInfo.getCardNumber(), setInfo.getRarity(), setInfo.getGraphicInfo()), typesRight, costsRight, this, SpellAbilityType.SPLIT_RIGHT);
}
// Params reordered as we need the same arguments as the parent constructor, with slightly different behaviour.
// Currently only used for rooms, because they are the only current split card with a shared type line.
protected SplitCard(UUID ownerId, CardSetInfo setInfo, String costsLeft, String costsRight, SpellAbilityType spellAbilityType, CardType[] singleTypeLine) {
super(ownerId, setInfo, singleTypeLine, costsLeft + costsRight, spellAbilityType);
String[] names = setInfo.getName().split(" // ");
leftHalfCard = new SplitCardHalfImpl(this.getOwnerId(), new CardSetInfo(names[0], setInfo.getExpansionSetCode(), setInfo.getCardNumber(), setInfo.getRarity(), setInfo.getGraphicInfo()), singleTypeLine, costsLeft, this, SpellAbilityType.SPLIT_LEFT);
rightHalfCard = new SplitCardHalfImpl(this.getOwnerId(), new CardSetInfo(names[1], setInfo.getExpansionSetCode(), setInfo.getCardNumber(), setInfo.getRarity(), setInfo.getGraphicInfo()), singleTypeLine, costsRight, this, SpellAbilityType.SPLIT_RIGHT);
}
protected SplitCard(SplitCard card) {
super(card);
// make sure all parts created and parent ref added

View file

@ -9,7 +9,8 @@ public enum EmptyNames {
// TODO: replace all getName().equals to haveSameNames and haveEmptyName
FACE_DOWN_CREATURE("", "[face_down_creature]"), // "Face down creature"
FACE_DOWN_TOKEN("", "[face_down_token]"), // "Face down token"
FACE_DOWN_CARD("", "[face_down_card]"); // "Face down card"
FACE_DOWN_CARD("", "[face_down_card]"), // "Face down card"
FULLY_LOCKED_ROOM("", "[fully_locked_room]"); // "Fully locked room"
public static final String EMPTY_NAME_IN_LOGS = "face down object";
@ -40,7 +41,8 @@ public enum EmptyNames {
public static boolean isEmptyName(String objectName) {
return objectName.equals(FACE_DOWN_CREATURE.getObjectName())
|| objectName.equals(FACE_DOWN_TOKEN.getObjectName())
|| objectName.equals(FACE_DOWN_CARD.getObjectName());
|| objectName.equals(FACE_DOWN_CARD.getObjectName())
|| objectName.equals(FULLY_LOCKED_ROOM.getObjectName());
}
public static String replaceTestCommandByObjectName(String searchCommand) {

View file

@ -0,0 +1,81 @@
package mage.constants;
import mage.abilities.StaticAbility;
import mage.cards.Card;
import mage.util.CardUtil;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public enum PartnerVariantType {
FATHER_AND_SON("Father & son"),
SURVIVORS("Survivors"),
CHARACTER_SELECT("Character select");
private final String name;
PartnerVariantType(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
public PartnerVariantAbility makeAbility() {
return new PartnerVariantAbility(this);
}
private static Set<PartnerVariantType> getTypes(Card card) {
return CardUtil
.castStream(card.getAbilities(), PartnerVariantAbility.class)
.map(PartnerVariantAbility::getType)
.collect(Collectors.toSet());
}
public static boolean checkCommanders(Card commander1, Card commander2) {
Set<PartnerVariantType> types1 = getTypes(commander1);
if (types1.isEmpty()) {
return false;
}
Set<PartnerVariantType> types2 = getTypes(commander2);
if (types2.isEmpty()) {
return false;
}
types1.retainAll(types2);
return !types1.isEmpty();
}
}
class PartnerVariantAbility extends StaticAbility {
private final PartnerVariantType type;
PartnerVariantAbility(PartnerVariantType type) {
super(Zone.BATTLEFIELD, null);
this.type = type;
}
private PartnerVariantAbility(final PartnerVariantAbility ability) {
super(ability);
this.type = ability.type;
}
@Override
public PartnerVariantAbility copy() {
return new PartnerVariantAbility(this);
}
public PartnerVariantType getType() {
return type;
}
@Override
public String getRule() {
return "Partner&mdash;" + type + " <i>(You can have two commanders if both have this ability.)</i>";
}
}

View file

@ -64,6 +64,7 @@ public enum SubType {
JUNK("Junk", SubTypeSet.ArtifactType),
LANDER("Lander", SubTypeSet.ArtifactType),
MAP("Map", SubTypeSet.ArtifactType),
MUTAGEN("Mutagen", SubTypeSet.ArtifactType),
POWERSTONE("Powerstone", SubTypeSet.ArtifactType),
SPACECRAFT("Spacecraft", SubTypeSet.ArtifactType),
TREASURE("Treasure", SubTypeSet.ArtifactType),
@ -431,6 +432,7 @@ public enum SubType {
// U
UGNAUGHT("Ugnaught", SubTypeSet.CreatureType, true),
UNICORN("Unicorn", SubTypeSet.CreatureType),
UTROM("Utrom", SubTypeSet.CreatureType),
// V
VAMPIRE("Vampire", SubTypeSet.CreatureType),
VARMINT("Varmint", SubTypeSet.CreatureType),

View file

@ -1,7 +1,6 @@
package mage.filter.predicate.mageobject;
import mage.MageObject;
import mage.cards.CardWithHalves;
import mage.cards.SplitCard;
import mage.constants.SpellAbilityType;
import mage.filter.predicate.Predicate;
@ -42,6 +41,7 @@ public class NamePredicate implements Predicate<MageObject> {
if (name == null) {
return false;
}
// If a player names a card, the player may name either half of a split card, but not both.
// A split card has the chosen name if one of its two names matches the chosen name.
// This is NOT the same for double faced cards, where only the front side matches
@ -51,28 +51,54 @@ public class NamePredicate implements Predicate<MageObject> {
// including the one that you countered, because those cards have only their front-face characteristics
// (including name) in the graveyard, hand, and library. (2021-04-16)
String[] searchNames = extractNames(name);
if (input instanceof SplitCard) {
return CardUtil.haveSameNames(name, ((CardWithHalves) input).getLeftHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, ((CardWithHalves) input).getRightHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, input.getName(), this.ignoreMtgRuleForEmptyNames);
} else if (input instanceof Spell && ((Spell) input).getSpellAbility().getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) {
SplitCard splitCard = (SplitCard) input;
// Check against left half, right half, and full card name
return matchesAnyName(searchNames, new String[] {
splitCard.getLeftHalfCard().getName(),
splitCard.getRightHalfCard().getName(),
splitCard.getName()
});
} else if (input instanceof Spell
&& ((Spell) input).getSpellAbility().getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) {
SplitCard card = (SplitCard) ((Spell) input).getCard();
return CardUtil.haveSameNames(name, card.getLeftHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, card.getRightHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, card.getName(), this.ignoreMtgRuleForEmptyNames);
// Check against left half, right half, and full card name
return matchesAnyName(searchNames, new String[] {
card.getLeftHalfCard().getName(),
card.getRightHalfCard().getName(),
card.getName()
});
} else if (input instanceof Spell && ((Spell) input).isFaceDown(game)) {
// face down spells don't have names, so it's not equal, see https://github.com/magefree/mage/issues/6569
return false;
} else {
if (name.contains(" // ")) {
String leftName = name.substring(0, name.indexOf(" // "));
String rightName = name.substring(name.indexOf(" // ") + 4);
return CardUtil.haveSameNames(leftName, input.getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(rightName, input.getName(), this.ignoreMtgRuleForEmptyNames);
} else {
return CardUtil.haveSameNames(name, input.getName(), this.ignoreMtgRuleForEmptyNames);
// For regular cards, extract names from input and compare
String[] inputNames = extractNames(input.getName());
return matchesAnyName(searchNames, inputNames);
}
}
private String[] extractNames(String nameString) {
if (nameString.contains(" // ")) {
String leftName = nameString.substring(0, nameString.indexOf(" // "));
String rightName = nameString.substring(nameString.indexOf(" // ") + 4);
return new String[] { leftName, rightName };
} else {
return new String[] { nameString };
}
}
private boolean matchesAnyName(String[] searchNames, String[] targetNames) {
for (String searchName : searchNames) {
for (String targetName : targetNames) {
if (CardUtil.haveSameNames(searchName, targetName, this.ignoreMtgRuleForEmptyNames)) {
return true;
}
}
}
return false;
}
@Override

View file

@ -191,6 +191,28 @@ public final class ZonesHandler {
cardsToUpdate.get(toZone).add(mdfCard.getRightHalfCard());
break;
}
} else if (targetCard instanceof RoomCard || targetCard instanceof RoomCardHalf) {
// Room cards must be moved as single object
RoomCard roomCard = (RoomCard) targetCard.getMainCard();
cardsToMove = new CardsImpl(roomCard);
cardsToUpdate.get(toZone).add(roomCard);
switch (toZone) {
case STACK:
case BATTLEFIELD:
// We don't want room halves to ever be on the battlefield
cardsToUpdate.get(Zone.OUTSIDE).add(roomCard.getLeftHalfCard());
cardsToUpdate.get(Zone.OUTSIDE).add(roomCard.getRightHalfCard());
break;
default:
// move all parts
cardsToUpdate.get(toZone).add(roomCard.getLeftHalfCard());
cardsToUpdate.get(toZone).add(roomCard.getRightHalfCard());
// If we aren't casting onto the stack or etb'ing, we need to clear this state
// (countered, memory lapsed etc)
// This prevents the state persisting for a put into play effect later
roomCard.setLastCastHalf(null);
break;
}
} else {
cardsToMove = new CardsImpl(targetCard);
cardsToUpdate.get(toZone).addAll(cardsToMove);
@ -269,8 +291,12 @@ public final class ZonesHandler {
}
// update zone in main
game.setZone(event.getTargetId(), event.getToZone());
if (targetCard instanceof RoomCardHalf && (toZone == Zone.BATTLEFIELD)) {
game.setZone(event.getTargetId(), Zone.OUTSIDE);
} else {
game.setZone(event.getTargetId(), event.getToZone());
}
// update zone in other parts (meld cards, mdf half cards)
cardsToUpdate.entrySet().forEach(entry -> {
for (Card card : entry.getValue().getCards(game)) {
@ -378,7 +404,11 @@ public final class ZonesHandler {
Permanent permanent;
if (card instanceof MeldCard) {
permanent = new PermanentMeld(card, event.getPlayerId(), game);
} else if (card instanceof ModalDoubleFacedCard) {
} else if (card instanceof RoomCardHalf) {
// Only the main room card can etb
permanent = new PermanentCard(card.getMainCard(), event.getPlayerId(), game);
}
else if (card instanceof ModalDoubleFacedCard) {
// main mdf card must be processed before that call (e.g. only halves can be moved to battlefield)
throw new IllegalStateException("Unexpected trying of move mdf card to battlefield instead half");
} else if (card instanceof Permanent) {
@ -503,4 +533,4 @@ public final class ZonesHandler {
return card;
}
}
}

View file

@ -699,6 +699,19 @@ public class GameEvent implements Serializable {
AIRBENDED,
FIREBENDED,
WATERBENDED,
/* A room permanent has a door unlocked.
targetId the room permanent
sourceId the unlock ability
playerId the room permanent's controller
flag true = left door unlocked false = right door unlocked
*/
DOOR_UNLOCKED,
/* A room permanent has a door unlocked.
targetId the room permanent
sourceId the unlock ability
playerId the room permanent's controller
*/
ROOM_FULLY_UNLOCKED,
// custom events - must store some unique data to track
CUSTOM_EVENT;

View file

@ -474,6 +474,16 @@ public interface Permanent extends Card, Controllable {
void setHarnessed(boolean value);
boolean wasRoomUnlockedOnCast();
boolean isLeftDoorUnlocked();
boolean isRightDoorUnlocked();
boolean unlockRoomOnCast(Game game);
boolean unlockDoor(Game game, Ability source, boolean isLeftDoor);
@Override
Permanent copy();

View file

@ -50,7 +50,8 @@ public class PermanentCard extends PermanentImpl {
goodForBattlefield = false;
} else if (card instanceof SplitCard) {
// fused spells allowed (it uses main card)
if (card.getSpellAbility() != null && !card.getSpellAbility().getSpellAbilityType().equals(SpellAbilityType.SPLIT_FUSED)) {
// room spells allowed (it uses main card)
if (card.getSpellAbility() != null && !card.getSpellAbility().getSpellAbilityType().equals(SpellAbilityType.SPLIT_FUSED) && !(card instanceof RoomCard)) {
goodForBattlefield = false;
}
}

View file

@ -102,6 +102,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean deathtouched;
protected boolean solved = false;
protected boolean roomWasUnlockedOnCast = false;
protected boolean leftHalfUnlocked = false;
protected boolean rightHalfUnlocked = false;
protected Map<String, List<UUID>> connectedCards = new HashMap<>();
protected Set<MageObjectReference> dealtDamageByThisTurn;
protected UUID attachedTo;
@ -191,6 +194,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.morphed = permanent.morphed;
this.disguised = permanent.disguised;
this.leftHalfUnlocked = permanent.leftHalfUnlocked;
this.rightHalfUnlocked = permanent.rightHalfUnlocked;
this.roomWasUnlockedOnCast = permanent.roomWasUnlockedOnCast;
this.manifested = permanent.manifested;
this.cloaked = permanent.cloaked;
this.createOrder = permanent.createOrder;
@ -2086,4 +2092,65 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return ZonesHandler.moveCard(zcInfo, game, source);
}
}
@Override
public boolean wasRoomUnlockedOnCast() {
return roomWasUnlockedOnCast;
}
@Override
public boolean isLeftDoorUnlocked() {
return leftHalfUnlocked;
}
@Override
public boolean isRightDoorUnlocked() {
return rightHalfUnlocked;
}
@Override
public boolean unlockRoomOnCast(Game game) {
if (this.roomWasUnlockedOnCast) {
return false;
}
this.roomWasUnlockedOnCast = true;
return true;
}
@Override
public boolean unlockDoor(Game game, Ability source, boolean isLeftDoor) {
// Check if already unlocked
boolean thisDoorUnlocked = isLeftDoor ? leftHalfUnlocked : rightHalfUnlocked;
if (thisDoorUnlocked) {
return false;
}
// Log the unlock
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
String doorSide = isLeftDoor ? "left" : "right";
game.informPlayers(controller.getLogName() + " unlocked the " + doorSide + " door of "
+ getLogName() + CardUtil.getSourceLogName(game, source));
}
// Update unlock state
if (isLeftDoor) {
leftHalfUnlocked = true;
} else {
rightHalfUnlocked = true;
}
// Fire door unlock event
GameEvent event = new GameEvent(GameEvent.EventType.DOOR_UNLOCKED, getId(), source, source.getControllerId());
event.setFlag(isLeftDoor);
game.fireEvent(event);
// Check if room is now fully unlocked
boolean otherDoorUnlocked = isLeftDoor ? rightHalfUnlocked : leftHalfUnlocked;
if (otherDoorUnlocked) {
game.fireEvent(new GameEvent(EventType.ROOM_FULLY_UNLOCKED, getId(), source, source.getControllerId()));
}
return true;
}
}

View file

@ -138,7 +138,13 @@ public class PermanentToken extends PermanentImpl {
@Override
public Card getMainCard() {
// token don't have game card, so return itself
// Check if we have a copy source card (for tokens created from copied spells)
Card copySourceCard = token.getCopySourceCard();
if (copySourceCard != null) {
return copySourceCard;
}
// Fallback to current behavior
return this;
}

View file

@ -0,0 +1,31 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.keyword.FlyingAbility;
import mage.constants.CardType;
import mage.constants.SubType;
/**
* @author PurpleCrowbar
*/
public final class Demon66Token extends TokenImpl {
public Demon66Token() {
super("Demon Token", "6/6 black Demon creature token with flying");
cardType.add(CardType.CREATURE);
color.setBlack(true);
subtype.add(SubType.DEMON);
power = new MageInt(6);
toughness = new MageInt(6);
addAbility(FlyingAbility.getInstance());
}
private Demon66Token(final Demon66Token token) {
super(token);
}
@Override
public Demon66Token copy() {
return new Demon66Token(this);
}
}

View file

@ -0,0 +1,40 @@
package mage.game.permanent.token;
import mage.abilities.Ability;
import mage.abilities.common.ActivateAsSorceryActivatedAbility;
import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.counters.CounterType;
import mage.target.common.TargetCreaturePermanent;
/**
* @author TheElk801
*/
public final class MutagenToken extends TokenImpl {
public MutagenToken() {
super("Mutagen Token", "Mutagen token");
cardType.add(CardType.ARTIFACT);
subtype.add(SubType.MUTAGEN);
Ability ability = new ActivateAsSorceryActivatedAbility(
new AddCountersTargetEffect(CounterType.P1P1.createInstance()), new GenericManaCost(1)
);
ability.addCost(new TapSourceCost());
ability.addCost(new SacrificeSourceCost().setText("sacrifice this token"));
ability.addTarget(new TargetCreaturePermanent());
this.addAbility(ability);
}
private MutagenToken(final MutagenToken token) {
super(token);
}
public MutagenToken copy() {
return new MutagenToken(this);
}
}

View file

@ -0,0 +1,30 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.constants.CardType;
import mage.constants.SubType;
/**
* @author PurpleCrowbar
*/
public final class ToyToken extends TokenImpl {
public ToyToken() {
super("Toy Token", "1/1 white Toy artifact creature token");
cardType.add(CardType.ARTIFACT);
cardType.add(CardType.CREATURE);
subtype.add(SubType.TOY);
color.setWhite(true);
power = new MageInt(1);
toughness = new MageInt(1);
}
private ToyToken(final ToyToken token) {
super(token);
}
@Override
public ToyToken copy() {
return new ToyToken(this);
}
}

View file

@ -26,54 +26,19 @@ public class TargetOptimization {
// for up to or any amount - limit max game sims to analyse
// (it's useless to calc all possible combinations on too much targets)
static public int AI_MAX_POSSIBLE_TARGETS_TO_CHOOSE = 7;
static public int AI_MAX_POSSIBLE_TARGETS_TO_CHOOSE = 18;
public static void optimizePossibleTargets(Ability source, Game game, Set<UUID> possibleTargets, int maxPossibleTargetsToSimulate) {
// remove duplicated/same creatures
// example: distribute 3 damage between 10+ same tokens
// example: target x1 from x10 forests - it's useless to recalc each forest
if (possibleTargets.size() < maxPossibleTargetsToSimulate) {
if (possibleTargets.size() <= maxPossibleTargetsToSimulate) {
return;
}
// split targets by groups
Map<UUID, String> targetGroups = new HashMap<>();
possibleTargets.forEach(id -> {
String groupKey = "";
// player
Player player = game.getPlayer(id);
if (player != null) {
groupKey = getTargetGroupKeyAsPlayer(player);
}
// game object
MageObject object = game.getObject(id);
if (object != null) {
groupKey = object.getName();
if (object instanceof Permanent) {
groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object);
} else if (object instanceof Card) {
groupKey += getTargetGroupKeyAsCard(game, (Card) object);
} else {
groupKey += getTargetGroupKeyAsOther(game, object);
}
}
// unknown - use all
if (groupKey.isEmpty()) {
groupKey = id.toString();
}
targetGroups.put(id, groupKey);
});
Map<String, List<UUID>> groups = new HashMap<>();
targetGroups.forEach((id, groupKey) -> {
groups.computeIfAbsent(groupKey, k -> new ArrayList<>());
groups.get(groupKey).add(id);
});
Map<String, ArrayList<UUID>> targetGroups = createGroups(game, possibleTargets, maxPossibleTargetsToSimulate, false);
// optimize logic:
// - use one target from each target group all the time
@ -81,7 +46,7 @@ public class TargetOptimization {
// use one target per group
Set<UUID> newPossibleTargets = new HashSet<>();
groups.forEach((groupKey, groupTargets) -> {
targetGroups.forEach((groupKey, groupTargets) -> {
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
if (targetId != null) {
newPossibleTargets.add(targetId);
@ -91,13 +56,13 @@ public class TargetOptimization {
// use random target until fill condition
while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) {
String groupKey = RandomUtil.randomFromCollection(groups.keySet());
String groupKey = RandomUtil.randomFromCollection(targetGroups.keySet());
if (groupKey == null) {
break;
}
List<UUID> groupTargets = groups.getOrDefault(groupKey, null);
List<UUID> groupTargets = targetGroups.getOrDefault(groupKey, null);
if (groupTargets == null || groupTargets.isEmpty()) {
groups.remove(groupKey);
targetGroups.remove(groupKey);
continue;
}
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
@ -112,6 +77,49 @@ public class TargetOptimization {
possibleTargets.addAll(newPossibleTargets);
}
private static Map<String, ArrayList<UUID>> createGroups(Game game, Set<UUID> possibleTargets, int maxPossibleTargetsToSimulate, boolean isLoose) {
Map<String, ArrayList<UUID>> targetGroups = new HashMap<>();
possibleTargets.forEach(id -> {
String groupKey = "";
// player
Player player = game.getPlayer(id);
if (player != null) {
groupKey = getTargetGroupKeyAsPlayer(player);
}
// game object
MageObject object = game.getObject(id);
if (object != null) {
groupKey = object.getName();
if (object instanceof Permanent) {
groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object, isLoose);
} else if (object instanceof Card) {
groupKey += getTargetGroupKeyAsCard(game, (Card) object, isLoose);
} else {
groupKey += getTargetGroupKeyAsOther(game, object);
}
}
// unknown - use all
if (groupKey.isEmpty()) {
groupKey = id.toString();
}
targetGroups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(id);
});
if (targetGroups.size() > maxPossibleTargetsToSimulate && !isLoose) {
// If too many possible target groups, regroup with less specific characteristics
return createGroups(game, possibleTargets, maxPossibleTargetsToSimulate, true);
}
// Return appropriate target groups or, if still too many possible targets after loose grouping,
// allow optimizePossibleTargets (defined above) to choose random targets within limit
return targetGroups;
}
private static String getTargetGroupKeyAsPlayer(Player player) {
// use all
return String.join(";", Arrays.asList(
@ -120,38 +128,57 @@ public class TargetOptimization {
));
}
private static String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) {
private static String getTargetGroupKeyAsPermanent(Game game, Permanent permanent, boolean isLoose) {
// split by name and stats
// TODO: rework and combine with PermanentEvaluator (to use battlefield score)
// try to use short text/hash for lesser data on debug
return String.join(";", Arrays.asList(
permanent.getName(),
String.valueOf(permanent.getControllerId().hashCode()),
String.valueOf(permanent.getOwnerId().hashCode()),
String.valueOf(permanent.isTapped()),
String.valueOf(permanent.getPower().getValue()),
String.valueOf(permanent.getToughness().getValue()),
String.valueOf(permanent.getDamage()),
String.valueOf(permanent.getCardType(game).toString().hashCode()),
String.valueOf(permanent.getSubtype(game).toString().hashCode()),
String.valueOf(permanent.getCounters(game).getTotalCount()),
String.valueOf(permanent.getAbilities(game).size()),
String.valueOf(permanent.getRules(game).toString().hashCode())
));
if (!isLoose) {
return String.join(";", Arrays.asList(
permanent.getName(),
String.valueOf(permanent.getControllerId().hashCode()),
String.valueOf(permanent.getOwnerId().hashCode()),
String.valueOf(permanent.isTapped()),
String.valueOf(permanent.getPower().getValue()),
String.valueOf(permanent.getToughness().getValue()),
String.valueOf(permanent.getDamage()),
String.valueOf(permanent.getCardType(game).toString().hashCode()),
String.valueOf(permanent.getSubtype(game).toString().hashCode()),
String.valueOf(permanent.getCounters(game).getTotalCount()),
String.valueOf(permanent.getAbilities(game).size()),
String.valueOf(permanent.getRules(game).toString().hashCode())
));
}
else {
return String.join(";", Arrays.asList(
String.valueOf(permanent.getControllerId().hashCode()),
String.valueOf(permanent.getPower().getValue()),
String.valueOf(permanent.getToughness().getValue()),
String.valueOf(permanent.getDamage()),
String.valueOf(permanent.getCardType(game).toString().hashCode())
));
}
}
private static String getTargetGroupKeyAsCard(Game game, Card card) {
private static String getTargetGroupKeyAsCard(Game game, Card card, boolean isLoose) {
// split by name and stats
return String.join(";", Arrays.asList(
card.getName(),
String.valueOf(card.getOwnerId().hashCode()),
String.valueOf(card.getCardType(game).toString().hashCode()),
String.valueOf(card.getSubtype(game).toString().hashCode()),
String.valueOf(card.getCounters(game).getTotalCount()),
String.valueOf(card.getAbilities(game).size()),
String.valueOf(card.getRules(game).toString().hashCode())
));
if (!isLoose) {
return String.join(";", Arrays.asList(
card.getName(),
String.valueOf(card.getOwnerId().hashCode()),
String.valueOf(card.getCardType(game).toString().hashCode()),
String.valueOf(card.getSubtype(game).toString().hashCode()),
String.valueOf(card.getCounters(game).getTotalCount()),
String.valueOf(card.getAbilities(game).size()),
String.valueOf(card.getRules(game).toString().hashCode())
));
}
else {
return String.join(";", Arrays.asList(
String.valueOf(card.getOwnerId().hashCode()),
String.valueOf(card.getCardType(game).toString().hashCode())
));
}
}
private static String getTargetGroupKeyAsOther(Game game, MageObject item) {

View file

@ -1,16 +0,0 @@
package mage.util.validation;
import mage.abilities.keyword.PartnerFatherAndSonAbility;
import mage.cards.Card;
/**
* @author TheElk801
*/
public enum PartnerFatherAndSonValidator implements CommanderValidator {
instance;
@Override
public boolean checkPartner(Card commander1, Card commander2) {
return commander1.getAbilities().containsClass(PartnerFatherAndSonAbility.class);
}
}

Some files were not shown because too many files have changed in this diff Show more