forked from External/mage
[WOC] Implement Court of Locthwain (#10973)
This commit is contained in:
parent
9cc0a8a9c2
commit
cf395f9f66
3 changed files with 449 additions and 0 deletions
263
Mage.Sets/src/mage/cards/c/CourtOfLocthwain.java
Normal file
263
Mage.Sets/src/mage/cards/c/CourtOfLocthwain.java
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
package mage.cards.c;
|
||||
|
||||
import mage.MageIdentifier;
|
||||
import mage.MageObject;
|
||||
import mage.MageObjectReference;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.BeginningOfUpkeepTriggeredAbility;
|
||||
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
|
||||
import mage.abilities.condition.common.MonarchIsSourceControllerCondition;
|
||||
import mage.abilities.decorator.ConditionalOneShotEffect;
|
||||
import mage.abilities.effects.AsThoughEffectImpl;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.BecomesMonarchSourceEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.*;
|
||||
import mage.game.ExileZone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
import mage.target.common.TargetOpponent;
|
||||
import mage.util.CardUtil;
|
||||
import mage.watchers.Watcher;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Susucr
|
||||
*/
|
||||
public final class CourtOfLocthwain extends CardImpl {
|
||||
|
||||
static UUID getExileZoneId(MageObjectReference mor, Game game) {
|
||||
return CardUtil.getExileZoneId("CourtOfLocthwain::" + mor.getSourceId() + "::" + mor.getZoneChangeCounter(), game);
|
||||
}
|
||||
|
||||
public CourtOfLocthwain(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{B}{B}");
|
||||
|
||||
// When Court of Locthwain enters the battlefield, you become the monarch.
|
||||
this.addAbility(new EntersBattlefieldTriggeredAbility(new BecomesMonarchSourceEffect()));
|
||||
|
||||
// At the beginning of your upkeep, exile the top card of target opponent's library. You may play that card for as long as it remains exiled, and mana of any type can be spent to cast it. If you're the monarch, until end of turn, you may cast a spell from among cards exiled with Court of Locthwain without paying its mana cost.
|
||||
Ability ability = new BeginningOfUpkeepTriggeredAbility(
|
||||
new CourtOfLocthwainFirstEffect(),
|
||||
TargetController.YOU, false
|
||||
);
|
||||
ability.addTarget(new TargetOpponent());
|
||||
ability.addEffect(new ConditionalOneShotEffect(
|
||||
new CourtOfLocthwainSecondEffect(),
|
||||
MonarchIsSourceControllerCondition.instance
|
||||
));
|
||||
|
||||
this.addAbility(ability, new CourtOfLocthwainWatcher());
|
||||
}
|
||||
|
||||
private CourtOfLocthwain(final CourtOfLocthwain card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CourtOfLocthwain copy() {
|
||||
return new CourtOfLocthwain(this);
|
||||
}
|
||||
}
|
||||
|
||||
class CourtOfLocthwainFirstEffect extends OneShotEffect {
|
||||
|
||||
CourtOfLocthwainFirstEffect() {
|
||||
super(Outcome.Benefit);
|
||||
staticText = "exile the top card of target opponent's library. You may play that "
|
||||
+ "card for as long as it remains exiled, and mana of any type can be spent to cast it";
|
||||
}
|
||||
|
||||
private CourtOfLocthwainFirstEffect(final CourtOfLocthwainFirstEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CourtOfLocthwainFirstEffect copy() {
|
||||
return new CourtOfLocthwainFirstEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source));
|
||||
if (controller == null || opponent == null || source == null) {
|
||||
return false;
|
||||
}
|
||||
Card card = opponent.getLibrary().getFromTop(game);
|
||||
if (card == null) {
|
||||
return false;
|
||||
}
|
||||
MageObject sourceObject = source.getSourceObject(game);
|
||||
if (sourceObject == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UUID exileId = CourtOfLocthwain.getExileZoneId(new MageObjectReference(sourceObject, game), game);
|
||||
String exileName = sourceObject.getIdName();
|
||||
controller.moveCardsToExile(card, source, game, true, exileId, exileName);
|
||||
|
||||
if (game.getState().getZone(card.getId()) == Zone.EXILED) {
|
||||
CardUtil.makeCardPlayable(
|
||||
game, source, card, Duration.EndOfGame,
|
||||
true, controller.getId(), null
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class CourtOfLocthwainSecondEffect extends OneShotEffect {
|
||||
|
||||
CourtOfLocthwainSecondEffect() {
|
||||
super(Outcome.Benefit);
|
||||
staticText = "until end of turn, you may cast a spell from among cards exiled "
|
||||
+ "with {this} without paying its mana cost";
|
||||
}
|
||||
|
||||
private CourtOfLocthwainSecondEffect(final CourtOfLocthwainSecondEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CourtOfLocthwainSecondEffect copy() {
|
||||
return new CourtOfLocthwainSecondEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
CourtOfLocthwainWatcher watcher = game.getState().getWatcher(CourtOfLocthwainWatcher.class);
|
||||
Permanent sourceObject = game.getPermanentOrLKIBattlefield(source.getSourceId());
|
||||
if (controller == null || watcher == null || sourceObject == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MageObjectReference mor = new MageObjectReference(sourceObject, game);
|
||||
|
||||
// We do copy the effect, to set the identifier.
|
||||
Ability sourceWithIdentifier = source.copy().setIdentifier(MageIdentifier.CourtOfLocthwainWatcher);
|
||||
game.addEffect(new CourtOfLocthwainCastForFreeEffect(mor), sourceWithIdentifier);
|
||||
|
||||
// Can cast another spell among the exiled ones this turn.
|
||||
watcher.setOrIncrementCastAvailable(controller.getId(), mor);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CourtOfLocthwainCastForFreeEffect extends AsThoughEffectImpl {
|
||||
|
||||
private final MageObjectReference mor;
|
||||
|
||||
public CourtOfLocthwainCastForFreeEffect(MageObjectReference mor) {
|
||||
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit);
|
||||
this.mor = mor;
|
||||
}
|
||||
|
||||
private CourtOfLocthwainCastForFreeEffect(final CourtOfLocthwainCastForFreeEffect effect) {
|
||||
super(effect);
|
||||
this.mor = effect.mor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CourtOfLocthwainCastForFreeEffect copy() {
|
||||
return new CourtOfLocthwainCastForFreeEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) {
|
||||
// Only applies for the controller of the ability.
|
||||
if (!affectedControllerId.equals(source.getControllerId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
CourtOfLocthwainWatcher watcher = game.getState().getWatcher(CourtOfLocthwainWatcher.class);
|
||||
Permanent sourceObject = game.getPermanentOrLKIBattlefield(source.getSourceId());
|
||||
if (controller == null || watcher == null || sourceObject == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UUID exileId = CourtOfLocthwain.getExileZoneId(mor, game);
|
||||
ExileZone exileZone = game.getExile().getExileZone(exileId);
|
||||
// Is the card attempted to be played in the ExiledZone?
|
||||
if (exileZone == null || !exileZone.contains(objectId)) {
|
||||
return false;
|
||||
}
|
||||
// can this ability still be used this turn?
|
||||
if (1 > watcher.castStillAvailable(controller.getId(), new MageObjectReference(sourceObject, game))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.CourtOfLocthwainWatcher, game);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class CourtOfLocthwainWatcher extends Watcher {
|
||||
|
||||
// player -> permanent's mor -> number of free cast remaining for that turn.
|
||||
private final Map<UUID, Map<MageObjectReference, Integer>> usageRemaining = new HashMap<>();
|
||||
|
||||
public CourtOfLocthwainWatcher() {
|
||||
super(WatcherScope.GAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void watch(GameEvent event, Game game) {
|
||||
UUID playerId = event.getPlayerId();
|
||||
if (event.getType() == GameEvent.EventType.SPELL_CAST
|
||||
&& event.hasApprovingIdentifier(MageIdentifier.CourtOfLocthwainWatcher)
|
||||
&& playerId != null) {
|
||||
decrementCastAvailable(
|
||||
playerId,
|
||||
event.getAdditionalReference().getApprovingMageObjectReference()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
usageRemaining.clear();
|
||||
super.reset();
|
||||
}
|
||||
|
||||
private void decrementCastAvailable(UUID playerId, MageObjectReference mor) {
|
||||
if (usageRemaining.containsKey(playerId)) {
|
||||
Map<MageObjectReference, Integer> usageForPlayer = usageRemaining.get(playerId);
|
||||
if (usageForPlayer.containsKey(mor)) {
|
||||
int newValue = usageForPlayer.get(mor) - 1;
|
||||
if (newValue > 0) {
|
||||
usageForPlayer.put(mor, newValue);
|
||||
} else {
|
||||
usageForPlayer.remove(mor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setOrIncrementCastAvailable(UUID playerId, MageObjectReference mor) {
|
||||
usageRemaining.computeIfAbsent(playerId, k -> new HashMap<>());
|
||||
usageRemaining.get(playerId).compute(mor, CardUtil::setOrIncrementValue);
|
||||
}
|
||||
|
||||
int castStillAvailable(UUID playerId, MageObjectReference mor) {
|
||||
return usageRemaining
|
||||
.getOrDefault(playerId, new HashMap<>())
|
||||
.getOrDefault(mor, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ public final class WildsOfEldraineCommander extends ExpansionSet {
|
|||
cards.add(new SetCardInfo("Court of Ardenvale", 21, Rarity.RARE, mage.cards.c.CourtOfArdenvale.class));
|
||||
cards.add(new SetCardInfo("Court of Embereth", 24, Rarity.RARE, mage.cards.c.CourtOfEmbereth.class));
|
||||
cards.add(new SetCardInfo("Court of Garenbrig", 25, Rarity.RARE, mage.cards.c.CourtOfGarenbrig.class));
|
||||
cards.add(new SetCardInfo("Court of Locthwain", 23, Rarity.RARE, mage.cards.c.CourtOfLocthwain.class));
|
||||
cards.add(new SetCardInfo("Court of Vantress", 22, Rarity.RARE, mage.cards.c.CourtOfVantress.class));
|
||||
cards.add(new SetCardInfo("Danitha Capashen, Paragon", 64, Rarity.UNCOMMON, mage.cards.d.DanithaCapashenParagon.class));
|
||||
cards.add(new SetCardInfo("Darkwater Catacombs", 157, Rarity.RARE, mage.cards.d.DarkwaterCatacombs.class));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
package org.mage.test.cards.single.woc;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
public class CourtOfLocthwainTest extends CardTestPlayerBase {
|
||||
|
||||
/**
|
||||
* Court of Locthwain
|
||||
* {2}{B}{B}
|
||||
* Enchantment
|
||||
*
|
||||
* When Court of Locthwain enters the battlefield, you become the monarch.
|
||||
*
|
||||
* At the beginning of your upkeep, exile the top card of target opponent's library. You may play that card for as long as it remains exiled, and mana of any type can be spent to cast it. If you're the monarch, until end of turn, you may cast a spell from among cards exiled with Court of Locthwain without paying its mana cost.
|
||||
*/
|
||||
private static String court = "Court of Locthwain";
|
||||
|
||||
/**
|
||||
* Armageddon
|
||||
* {3}{W}
|
||||
* Sorcery
|
||||
*
|
||||
* Destroy all lands.
|
||||
*/
|
||||
private static String armageddon = "Armageddon";
|
||||
|
||||
private static String evangel = "Cabal Evangel"; // 2/2
|
||||
private static String reveler = "Falkenrath Reaver"; // 2/2
|
||||
|
||||
@Test
|
||||
public void testNoMonarch() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.HAND, playerA, court);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Scrubland", 4);
|
||||
addCard(Zone.BATTLEFIELD, playerB, evangel);
|
||||
addCard(Zone.LIBRARY, playerB, reveler);
|
||||
addCard(Zone.LIBRARY, playerB, "Island", 2); // playerB will draw those.
|
||||
skipInitShuffling();
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, court);
|
||||
attack(2, playerB, evangel, playerA); // B takes the monarch
|
||||
addTarget(playerA, playerB); // trigger target.
|
||||
|
||||
setStopAt(3, PhaseStep.DRAW);
|
||||
execute();
|
||||
|
||||
assertExileCount(reveler, 1);
|
||||
|
||||
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, reveler);
|
||||
|
||||
setStopAt(3, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, reveler, 1);
|
||||
assertTappedCount("Scrubland", true, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMonarchChoiceCastForFree() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.HAND, playerA, court);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Scrubland", 4);
|
||||
addCard(Zone.LIBRARY, playerB, reveler);
|
||||
addCard(Zone.LIBRARY, playerB, "Island"); // playerB will draw it.
|
||||
skipInitShuffling();
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, court);
|
||||
|
||||
addTarget(playerA, playerB); // trigger target for turn 3
|
||||
checkExileCount("reveler got exiled", 3, PhaseStep.PRECOMBAT_MAIN, playerA, reveler, 1);
|
||||
|
||||
// We need to choose the proper AsThough, even if only one is valid.
|
||||
setChoice(playerA, "Without paying manacost: ");
|
||||
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, reveler);
|
||||
|
||||
setStopAt(3, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, reveler, 1);
|
||||
assertTappedCount("Scrubland", true, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMonarchChoiceCastForMana() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.HAND, playerA, court);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Scrubland", 4);
|
||||
addCard(Zone.LIBRARY, playerB, reveler);
|
||||
addCard(Zone.LIBRARY, playerB, "Island"); // playerB will draw it.
|
||||
skipInitShuffling();
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, court);
|
||||
|
||||
addTarget(playerA, playerB); // trigger target for turn 3
|
||||
checkExileCount("reveler got exiled", 3, PhaseStep.PRECOMBAT_MAIN, playerA, reveler, 1);
|
||||
|
||||
// We need to choose the proper AsThough, even if only one is valid.
|
||||
setChoice(playerA, "Court");
|
||||
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, reveler);
|
||||
|
||||
setStopAt(3, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, reveler, 1);
|
||||
assertTappedCount("Scrubland", true, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMonarchArmageddon() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.HAND, playerA, court);
|
||||
addCard(Zone.HAND, playerA, armageddon);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Scrubland", 8);
|
||||
|
||||
addCard(Zone.LIBRARY, playerB, reveler); // will be exiled by Court
|
||||
addCard(Zone.LIBRARY, playerB, "Island"); // playerB will draw it.
|
||||
addCard(Zone.LIBRARY, playerB, evangel); // will be exiled by Court
|
||||
addCard(Zone.LIBRARY, playerB, "Island"); // playerB will draw it.
|
||||
skipInitShuffling();
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, court, true);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, armageddon);
|
||||
|
||||
addTarget(playerA, playerB); // trigger target for turn 3
|
||||
checkExileCount("evangel got exiled", 3, PhaseStep.PRECOMBAT_MAIN, playerA, evangel, 1);
|
||||
checkExileCount("reveler is not yet exiled", 3, PhaseStep.PRECOMBAT_MAIN, playerA, reveler, 0);
|
||||
|
||||
addTarget(playerA, playerB); // trigger target for turn 5.
|
||||
checkExileCount("evangel still exiled", 5, PhaseStep.PRECOMBAT_MAIN, playerA, evangel, 1);
|
||||
checkExileCount("reveler got exiled", 5, PhaseStep.PRECOMBAT_MAIN, playerA, reveler, 1);
|
||||
|
||||
castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, reveler, true);
|
||||
setChoice(playerA, "Without paying manacost");
|
||||
|
||||
setStopAt(5, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, reveler, 1);
|
||||
assertPermanentCount(playerA, "Scrubland", 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMonarchDoubleCast() {
|
||||
setStrictChooseMode(true);
|
||||
|
||||
addCard(Zone.HAND, playerA, court);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Scrubland", 4);
|
||||
|
||||
addCard(Zone.LIBRARY, playerB, reveler); // will be exiled by Court
|
||||
addCard(Zone.LIBRARY, playerB, "Island"); // playerB will draw it.
|
||||
addCard(Zone.LIBRARY, playerB, evangel); // will be exiled by Court
|
||||
addCard(Zone.LIBRARY, playerB, "Island"); // playerB will draw it.
|
||||
skipInitShuffling();
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, court);
|
||||
|
||||
addTarget(playerA, playerB); // trigger target for turn 3
|
||||
checkExileCount("evangel got exiled", 3, PhaseStep.PRECOMBAT_MAIN, playerA, evangel, 1);
|
||||
checkExileCount("reveler is not yet exiled", 3, PhaseStep.PRECOMBAT_MAIN, playerA, reveler, 0);
|
||||
|
||||
addTarget(playerA, playerB); // trigger target for turn 5.
|
||||
checkExileCount("evangel still exiled", 5, PhaseStep.PRECOMBAT_MAIN, playerA, evangel, 1);
|
||||
checkExileCount("reveler got exiled", 5, PhaseStep.PRECOMBAT_MAIN, playerA, reveler, 1);
|
||||
|
||||
setChoice(playerA, "Court");
|
||||
castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, evangel, true);
|
||||
|
||||
setChoice(playerA, "Without paying manacost");
|
||||
castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, reveler);
|
||||
|
||||
setStopAt(5, PhaseStep.POSTCOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertPermanentCount(playerA, evangel, 1);
|
||||
assertPermanentCount(playerA, reveler, 1);
|
||||
assertTappedCount("Scrubland", true, 2);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue