mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 02:30:08 -08:00
feature: implement Rooms (#13786)
- Adds Room + Room half card types - Adds Room unlock ability - Adds Room special functionality (locking halves, unlocking, changing names, changing mana values) - Adjusts name predicate handling for Room name handling (Rooms have between 0 and 2 names on the battlefield depending on their state. They have 1 name on the stack as a normal split card does). Allows cards to match these names properly - Adds Room game events (Unlock, Fully Unlock) and unlock triggers - Updates Split card constructor to allow a single type line as with Rooms - Adds empty name constant for fully unlocked rooms - Updates Permanent to include the door unlock states (that all permanents have) that are relevant when a permanent is or becomes a Room. - Updates ZonesHandler to properly move Room card parts onto and off of the battlefield. - Updated Eerie ability to function properly with Rooms - Implemented Bottomless Pool // Locker Room - Implemented Surgical Suite // Hospital Room - Added Room card tests
This commit is contained in:
parent
cb900eb799
commit
f7be842008
22 changed files with 1857 additions and 32 deletions
59
Mage.Sets/src/mage/cards/b/BottomlessPoolLockerRoom.java
Normal file
59
Mage.Sets/src/mage/cards/b/BottomlessPoolLockerRoom.java
Normal 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 owner’s 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 owner’s 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);
|
||||
}
|
||||
}
|
||||
71
Mage.Sets/src/mage/cards/s/SurgicalSuiteHospitalRoom.java
Normal file
71
Mage.Sets/src/mage/cards/s/SurgicalSuiteHospitalRoom.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
@ -318,6 +319,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));
|
||||
|
|
|
|||
|
|
@ -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 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.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 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.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 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.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 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, "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 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, "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 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.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 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, "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 player’s 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 player’s 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
Mage/src/main/java/mage/abilities/common/RoomUnlockAbility.java
Normal file
106
Mage/src/main/java/mage/abilities/common/RoomUnlockAbility.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
215
Mage/src/main/java/mage/cards/RoomCard.java
Normal file
215
Mage/src/main/java/mage/cards/RoomCard.java
Normal 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);
|
||||
}
|
||||
}
|
||||
9
Mage/src/main/java/mage/cards/RoomCardHalf.java
Normal file
9
Mage/src/main/java/mage/cards/RoomCardHalf.java
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package mage.cards;
|
||||
|
||||
/**
|
||||
* @author oscscull
|
||||
*/
|
||||
public interface RoomCardHalf extends SplitCardHalf {
|
||||
@Override
|
||||
RoomCardHalf copy();
|
||||
}
|
||||
68
Mage/src/main/java/mage/cards/RoomCardHalfImpl.java
Normal file
68
Mage/src/main/java/mage/cards/RoomCardHalfImpl.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,7 +291,11 @@ 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 -> {
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue