forked from External/mage
[OTJ] Implement Fblthp, Lost on the Range (#12042)
This commit is contained in:
parent
feacb55caf
commit
4bbdc3c543
7 changed files with 302 additions and 14 deletions
116
Mage.Sets/src/mage/cards/f/FblthpLostOnTheRange.java
Normal file
116
Mage.Sets/src/mage/cards/f/FblthpLostOnTheRange.java
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package mage.cards.f;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.effects.ContinuousEffectImpl;
|
||||
import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect;
|
||||
import mage.abilities.keyword.PlotAbility;
|
||||
import mage.abilities.keyword.WardAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Susucr
|
||||
*/
|
||||
public final class FblthpLostOnTheRange extends CardImpl {
|
||||
|
||||
public FblthpLostOnTheRange(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}{U}");
|
||||
|
||||
this.supertype.add(SuperType.LEGENDARY);
|
||||
this.subtype.add(SubType.HOMUNCULUS);
|
||||
this.power = new MageInt(1);
|
||||
this.toughness = new MageInt(1);
|
||||
|
||||
// Ward {2}
|
||||
this.addAbility(new WardAbility(new ManaCostsImpl<>("{2}")));
|
||||
|
||||
// You may look at the top card of your library any time.
|
||||
this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect()));
|
||||
|
||||
// The top card of your library has plot. The plot cost is equal to its mana cost.
|
||||
this.addAbility(new SimpleStaticAbility(new FblthpLostOnTheRangePlotGivingEffect()));
|
||||
|
||||
// You may plot nonland cards from the top of your library.
|
||||
this.addAbility(new SimpleStaticAbility(new FblthpLostOnTheRangePermissionEffect()));
|
||||
}
|
||||
|
||||
private FblthpLostOnTheRange(final FblthpLostOnTheRange card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FblthpLostOnTheRange copy() {
|
||||
return new FblthpLostOnTheRange(this);
|
||||
}
|
||||
}
|
||||
|
||||
class FblthpLostOnTheRangePlotGivingEffect extends ContinuousEffectImpl {
|
||||
|
||||
FblthpLostOnTheRangePlotGivingEffect() {
|
||||
super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
|
||||
this.staticText = "The top card of your library has plot. The plot cost is equal to its mana cost.";
|
||||
}
|
||||
|
||||
private FblthpLostOnTheRangePlotGivingEffect(final FblthpLostOnTheRangePlotGivingEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FblthpLostOnTheRangePlotGivingEffect copy() {
|
||||
return new FblthpLostOnTheRangePlotGivingEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller == null) {
|
||||
return false;
|
||||
}
|
||||
Card card = controller.getLibrary().getFromTop(game);
|
||||
if (card == null) {
|
||||
return false;
|
||||
}
|
||||
game.getState().addOtherAbility(card, new PlotAbility(card.getManaCost().getText()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class FblthpLostOnTheRangePermissionEffect extends ContinuousEffectImpl {
|
||||
|
||||
FblthpLostOnTheRangePermissionEffect() {
|
||||
this(Duration.WhileOnBattlefield);
|
||||
}
|
||||
|
||||
public FblthpLostOnTheRangePermissionEffect(Duration duration) {
|
||||
super(duration, Layer.PlayerEffects, SubLayer.NA, Outcome.Benefit);
|
||||
staticText = "You may plot nonland cards from the top of your library";
|
||||
}
|
||||
|
||||
private FblthpLostOnTheRangePermissionEffect(final FblthpLostOnTheRangePermissionEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FblthpLostOnTheRangePermissionEffect copy() {
|
||||
return new FblthpLostOnTheRangePermissionEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller != null) {
|
||||
controller.setPlotFromTopOfLibrary(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -92,6 +92,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
|
|||
cards.add(new SetCardInfo("Explosive Derailment", 122, Rarity.COMMON, mage.cards.e.ExplosiveDerailment.class));
|
||||
cards.add(new SetCardInfo("Failed Fording", 47, Rarity.COMMON, mage.cards.f.FailedFording.class));
|
||||
cards.add(new SetCardInfo("Fake Your Own Death", 87, Rarity.COMMON, mage.cards.f.FakeYourOwnDeath.class));
|
||||
cards.add(new SetCardInfo("Fblthp, Lost on the Range", 48, Rarity.RARE, mage.cards.f.FblthpLostOnTheRange.class));
|
||||
cards.add(new SetCardInfo("Ferocification", 123, Rarity.UNCOMMON, mage.cards.f.Ferocification.class));
|
||||
cards.add(new SetCardInfo("Festering Gulch", 257, Rarity.COMMON, mage.cards.f.FesteringGulch.class));
|
||||
cards.add(new SetCardInfo("Final Showdown", 11, Rarity.MYTHIC, mage.cards.f.FinalShowdown.class));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
package org.mage.test.cards.single.otj;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
/**
|
||||
* @author Susucr
|
||||
*/
|
||||
public class FblthpLostOnTheRangeTest extends CardTestPlayerBase {
|
||||
|
||||
/**
|
||||
* {@link mage.cards.f.FblthpLostOnTheRange Fblthp, Lost on the Range} {1}{U}{U}
|
||||
* Legendary Creature — Homunculus
|
||||
* Ward {2}
|
||||
* You may look at the top card of your library any time.
|
||||
* The top card of your library has plot. The plot cost is equal to its mana cost.
|
||||
* You may plot nonland cards from the top of your library.
|
||||
* 1/1
|
||||
*/
|
||||
private static final String fblthp = "Fblthp, Lost on the Range";
|
||||
|
||||
@Test
|
||||
public void Test_Plot_FromTop_LightningBolt() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, fblthp);
|
||||
addCard(Zone.LIBRARY, playerA, "Lightning Bolt");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
|
||||
|
||||
assertHandCount(playerA, 0); // no card in hand, Bolt is on top.
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertExileCount(playerA, "Lightning Bolt", 1);
|
||||
assertTappedCount("Mountain", true, 1); // cost {R} to plot
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Test_Plot_FromTop_RegularPlot() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, fblthp);
|
||||
addCard(Zone.LIBRARY, playerA, "Beastbond Outcaster"); // {2}{G}, plot {1}{G}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
|
||||
assertHandCount(playerA, 0); // no card in hand, Outcaster is on top.
|
||||
checkPlayableAbility("regular Plot {1}{G}", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot {1}{G}", true);
|
||||
checkPlayableAbility("no mana for added Plot {2}{G}", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot {2}{G}", false);
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot {1}{G}");
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertExileCount(playerA, "Beastbond Outcaster", 1);
|
||||
assertTappedCount("Forest", true, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Test_Plot_FromTop_AddedPlot() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, fblthp);
|
||||
addCard(Zone.LIBRARY, playerA, "Beastbond Outcaster"); // {2}{G}, plot {1}{G}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
|
||||
|
||||
assertHandCount(playerA, 0); // no card in hand, Outcaster is on top.
|
||||
checkPlayableAbility("regular Plot {1}{G}", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot {1}{G}", true);
|
||||
checkPlayableAbility("added Plot {2}{G}", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot {2}{G}", true);
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot {2}{G}");
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertExileCount(playerA, "Beastbond Outcaster", 1);
|
||||
assertTappedCount("Forest", true, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Test_Plot_FromTop_Adventure() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, fblthp);
|
||||
addCard(Zone.LIBRARY, playerA, "Bonecrusher Giant");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
|
||||
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot {2}{R}");
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertExileCount(playerA, "Bonecrusher Giant", 1);
|
||||
assertTappedCount("Mountain", true, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void Test_Plot_FromTop_Split() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, fblthp);
|
||||
addCard(Zone.LIBRARY, playerA, "Life // Death"); // split {G} / {1}{B}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Bayou", 3);
|
||||
|
||||
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot");
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertExileCount(playerA, "Life // Death", 1);
|
||||
assertTappedCount("Bayou", true, 3);
|
||||
}
|
||||
}
|
||||
|
|
@ -1225,10 +1225,10 @@ public class TestPlayer implements Player {
|
|||
+ ", " + (c.isTapped() ? "Tapped" : "Untapped")
|
||||
+ getPrintableAliases(", [", c.getId(), "]")
|
||||
+ (c.getAttachedTo() == null ? ""
|
||||
: ", attached to "
|
||||
+ (game.getObject(c.getAttachedTo()) == null
|
||||
? game.getPlayer(c.getAttachedTo()).getName()
|
||||
: game.getObject(c.getAttachedTo()).getIdName()))))
|
||||
: ", attached to "
|
||||
+ (game.getObject(c.getAttachedTo()) == null
|
||||
? game.getPlayer(c.getAttachedTo()).getName()
|
||||
: game.getObject(c.getAttachedTo()).getIdName()))))
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
|
@ -3833,6 +3833,16 @@ public class TestPlayer implements Player {
|
|||
computerPlayer.setDrawsOnOpponentsTurn(drawsOnOpponentsTurn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlotFromTopOfLibrary() {
|
||||
return computerPlayer.canPlotFromTopOfLibrary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlotFromTopOfLibrary(boolean canPlotFromTopOfLibrary) {
|
||||
computerPlayer.setPlotFromTopOfLibrary(canPlotFromTopOfLibrary);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDrawsOnOpponentsTurn() {
|
||||
return computerPlayer.isDrawsOnOpponentsTurn();
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public class PlotAbility extends SpecialAction {
|
|||
private final String rule;
|
||||
|
||||
public PlotAbility(String plotCost) {
|
||||
super(Zone.HAND);
|
||||
super(Zone.ALL); // Usually, plot only works from hand. However [[Fblthp, Lost on the Range]] allows plotting from library
|
||||
this.addCost(new ManaCostsImpl<>(plotCost));
|
||||
this.addEffect(new PlotSourceExileEffect());
|
||||
this.setTiming(TimingRule.SORCERY);
|
||||
|
|
@ -50,19 +50,34 @@ public class PlotAbility extends SpecialAction {
|
|||
return rule;
|
||||
}
|
||||
|
||||
// TODO: handle [[Fblthp, Lost on the Range]] allowing player to plot from library.
|
||||
@Override
|
||||
public ActivationStatus canActivate(UUID playerId, Game game) {
|
||||
// plot can only be activated from a hand
|
||||
// TODO: change that for Fblthp.
|
||||
if (game.getState().getZone(getSourceId()) != Zone.HAND) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
// suspend uses card's timing restriction
|
||||
// Plot ability uses card's timing restriction
|
||||
Card card = game.getCard(getSourceId());
|
||||
if (card == null) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
// plot can only be activated from hand or from top of library if allowed to.
|
||||
Zone zone = game.getState().getZone(getSourceId());
|
||||
if (zone == Zone.HAND) {
|
||||
// Allowed from hand
|
||||
} else if (zone == Zone.LIBRARY) {
|
||||
// Allowed only if permitted for top card, and only if the card is on top and is nonland
|
||||
// Note: if another effect changes zones where permitted, or if different card categories are permitted,
|
||||
// it would be better to refactor this as an unique AsThoughEffect.
|
||||
// As of now, only Fblthp, Lost on the Range changes permission of plot.
|
||||
Player player = game.getPlayer(getControllerId());
|
||||
if (player == null || !player.canPlotFromTopOfLibrary()) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
Card topCardLibrary = player.getLibrary().getFromTop(game);
|
||||
if (topCardLibrary == null || !topCardLibrary.getId().equals(card.getId()) || card.isLand()) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
} else {
|
||||
// Not Allowed from other zones
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
if (!card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game)) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
|
|
@ -99,11 +114,17 @@ public class PlotAbility extends SpecialAction {
|
|||
UUID exileId = PlotAbility.getPlotExileId(owner.getId(), game);
|
||||
String exileZoneName = "Plots of " + owner.getName();
|
||||
Card mainCard = card.getMainCard();
|
||||
Zone zone = game.getState().getZone(mainCard.getId());
|
||||
if (mainCard.moveToExile(exileId, exileZoneName, source, game)) {
|
||||
// Remember on which turn the card was last plotted.
|
||||
game.getState().setValue(PlotAbility.getPlotTurnKeyForCard(mainCard.getId()), game.getTurnNum());
|
||||
game.addEffect(new PlotAddSpellAbilityEffect(new MageObjectReference(mainCard, game)), source);
|
||||
game.informPlayers(owner.getLogName() + " plots " + mainCard.getLogName());
|
||||
game.informPlayers(
|
||||
owner.getLogName()
|
||||
+ " plots " + mainCard.getLogName()
|
||||
+ " from " + zone.toString().toLowerCase()
|
||||
+ CardUtil.getSourceLogName(game, source, card.getId())
|
||||
);
|
||||
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.BECOME_PLOTTED, mainCard.getId(), source, owner.getId()));
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -195,6 +195,10 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
|
||||
boolean canPlayCardsFromGraveyard();
|
||||
|
||||
void setPlotFromTopOfLibrary(boolean canPlotFromTopOfLibrary);
|
||||
|
||||
boolean canPlotFromTopOfLibrary();
|
||||
|
||||
void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn);
|
||||
|
||||
boolean isDrawsOnOpponentsTurn();
|
||||
|
|
@ -363,6 +367,7 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
|
||||
/**
|
||||
* Return player's turn control to prev player
|
||||
*
|
||||
* @param value
|
||||
* @param fullRestore return turn control to own
|
||||
*/
|
||||
|
|
@ -988,7 +993,7 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
* @param source
|
||||
* @param game
|
||||
* @param fromZone
|
||||
* @param withName for face down: used to hide card name in game logs before real face down status apply
|
||||
* @param withName for face down: used to hide card name in game logs before real face down status apply
|
||||
* @return
|
||||
*/
|
||||
@Deprecated
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
protected PayLifeCostLevel payLifeCostLevel = PayLifeCostLevel.allAbilities;
|
||||
protected boolean loseByZeroOrLessLife = true;
|
||||
protected boolean canPlayCardsFromGraveyard = true;
|
||||
protected boolean canPlotFromTopOfLibrary = false;
|
||||
protected boolean drawsOnOpponentsTurn = false;
|
||||
|
||||
protected FilterPermanent sacrificeCostFilter;
|
||||
|
|
@ -251,6 +252,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.canLoseLife = player.canLoseLife;
|
||||
this.loseByZeroOrLessLife = player.loseByZeroOrLessLife;
|
||||
this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard;
|
||||
this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary;
|
||||
this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn;
|
||||
|
||||
this.attachments.addAll(player.attachments);
|
||||
|
|
@ -360,6 +362,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
? player.getSacrificeCostFilter().copy() : null;
|
||||
this.loseByZeroOrLessLife = player.canLoseByZeroOrLessLife();
|
||||
this.canPlayCardsFromGraveyard = player.canPlayCardsFromGraveyard();
|
||||
this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary();
|
||||
this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn();
|
||||
this.alternativeSourceCosts.clear();
|
||||
this.alternativeSourceCosts.addAll(player.getAlternativeSourceCosts());
|
||||
|
|
@ -474,6 +477,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.payLifeCostLevel = PayLifeCostLevel.allAbilities;
|
||||
this.loseByZeroOrLessLife = true;
|
||||
this.canPlayCardsFromGraveyard = true;
|
||||
this.canPlotFromTopOfLibrary = false;
|
||||
this.drawsOnOpponentsTurn = false;
|
||||
|
||||
this.sacrificeCostFilter = null;
|
||||
|
|
@ -516,6 +520,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.sacrificeCostFilter = null;
|
||||
this.loseByZeroOrLessLife = true;
|
||||
this.canPlayCardsFromGraveyard = false;
|
||||
this.canPlotFromTopOfLibrary = false;
|
||||
this.drawsOnOpponentsTurn = false;
|
||||
this.topCardRevealed = false;
|
||||
this.alternativeSourceCosts.clear();
|
||||
|
|
@ -4526,6 +4531,16 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.canPlayCardsFromGraveyard = playCardsFromGraveyard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlotFromTopOfLibrary() {
|
||||
return canPlotFromTopOfLibrary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlotFromTopOfLibrary(boolean canPlotFromTopOfLibrary) {
|
||||
this.canPlotFromTopOfLibrary = canPlotFromTopOfLibrary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDrawsOnOpponentsTurn(boolean drawsOnOpponentsTurn) {
|
||||
this.drawsOnOpponentsTurn = drawsOnOpponentsTurn;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue