[WOC] Implement Court of Locthwain (#10973)

This commit is contained in:
Susucre 2023-10-06 04:03:07 +02:00 committed by GitHub
parent 9cc0a8a9c2
commit cf395f9f66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 449 additions and 0 deletions

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

View file

@ -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));

View file

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