[OTJ] Implement Plot mechanic (+8 cards) (#12017)

[OTJ] Implement Aloe Alchemist
[OTJ] Implement Aven Interrupter
[OTJ] Implement Longhorn Shapshooter
[OTJ] Implement Kellan Joins Up
[OTJ] Implement Make Your Own Luck
[OTJ] Implement Jace Reawakened
[OTJ] Implement Lilah, Undefeated Slickshot
[OTJ] Implement Doc Aurlock, Grizzled Genius
This commit is contained in:
Susucre 2024-03-31 17:06:55 +02:00 committed by GitHub
parent ed3d6e3078
commit 97ab8074b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1476 additions and 25 deletions

View file

@ -429,6 +429,7 @@ public abstract class AbilityImpl implements Ability {
case BESTOW:
case MORPH:
case DISGUISE:
case PLOT:
// from Snapcaster Mage:
// If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs
// (such as that of Foil). (2018-12-07)
@ -521,7 +522,7 @@ public abstract class AbilityImpl implements Ability {
String message = controller.getLogName() + " announces a value of " + xValue + " (" + variableCost.getActionText() + ')'
+ CardUtil.getSourceLogName(game, this);
announceString.append(message);
setCostsTag("X",xValue);
setCostsTag("X", xValue);
}
}
return announceString.toString();
@ -626,7 +627,7 @@ public abstract class AbilityImpl implements Ability {
}
addManaCostsToPay(new ManaCostsImpl<>(manaString.toString()));
getManaCostsToPay().setX(xValue * xValueMultiplier, amountMana);
setCostsTag("X",xValue * xValueMultiplier);
setCostsTag("X", xValue * xValueMultiplier);
}
variableManaCost.setPaid();
}
@ -718,7 +719,8 @@ public abstract class AbilityImpl implements Ability {
public Map<String, Object> getCostsTagMap() {
return costsTagMap;
}
public void setCostsTag(String tag, Object value){
public void setCostsTag(String tag, Object value) {
if (costsTagMap == null) {
costsTagMap = new HashMap<>();
}

View file

@ -0,0 +1,44 @@
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;
/**
* @author Susucr
*/
public class BecomesPlottedSourceTriggeredAbility extends TriggeredAbilityImpl {
public BecomesPlottedSourceTriggeredAbility(Effect effect, boolean optional) {
super(Zone.EXILED, effect, optional);
setTriggerPhrase("When {this} becomes plotted, ");
}
public BecomesPlottedSourceTriggeredAbility(Effect effect) {
this(effect, false);
}
protected BecomesPlottedSourceTriggeredAbility(final BecomesPlottedSourceTriggeredAbility ability) {
super(ability);
}
@Override
public BecomesPlottedSourceTriggeredAbility copy() {
return new BecomesPlottedSourceTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.BECOME_PLOTTED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getTargetId().equals(this.getSourceId())) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,48 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.PlotAbility;
import mage.cards.Card;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInHand;
public class MayExileCardFromHandPlottedEffect extends OneShotEffect {
private final FilterCard filter;
public MayExileCardFromHandPlottedEffect(FilterCard filter) {
super(Outcome.PutCardInPlay);
this.filter = filter;
this.staticText = "you may exile a " + filter.getMessage() + " from your hand. If you do, it becomes plotted";
}
private MayExileCardFromHandPlottedEffect(final MayExileCardFromHandPlottedEffect effect) {
super(effect);
this.filter = effect.filter;
}
@Override
public MayExileCardFromHandPlottedEffect copy() {
return new MayExileCardFromHandPlottedEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
TargetCardInHand target = new TargetCardInHand(0, 1, filter);
if (player.chooseTarget(outcome, target, source, game)) {
Card card = game.getCard(target.getFirstTarget());
if (card != null) {
PlotAbility.doExileAndPlotCard(card, game, source);
}
}
return true;
}
}

View file

@ -332,17 +332,17 @@ public class ForetellAbility extends SpecialAction {
if (game.getState().getZone(mainCardId) != Zone.EXILED) {
return ActivationStatus.getFalse();
}
Integer foretoldTurn = (Integer) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number");
UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility");
// Card must be Foretold
if (game.getState().getValue(mainCardId.toString() + "Foretell Turn Number") == null
&& game.getState().getValue(mainCardId + "foretellAbility") == null) {
if (foretoldTurn == null || exileId == null) {
return ActivationStatus.getFalse();
}
// Can't be cast if the turn it was Foretold is the same
if ((int) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number") == game.getTurnNum()) {
if (foretoldTurn == game.getTurnNum()) {
return ActivationStatus.getFalse();
}
// Check that the card is actually in the exile zone (ex: Oblivion Ring exiles it after it was Foretold, etc)
UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility");
ExileZone exileZone = game.getState().getExile().getExileZone(exileId);
if (exileZone != null
&& exileZone.isEmpty()) {

View file

@ -1,26 +1,43 @@
package mage.abilities.keyword;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
import mage.constants.TimingRule;
import mage.constants.Zone;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.*;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.List;
import java.util.UUID;
/**
* TODO: Implement this
*
* @author TheElk801
* @author Susucr
*/
public class PlotAbility extends SpecialAction {
private final String rule;
public PlotAbility(String plotCost) {
super(Zone.HAND);
this.addCost(new ManaCostsImpl<>(plotCost));
this.addEffect(new PlotSourceExileEffect());
this.setTiming(TimingRule.SORCERY);
this.usesStack = false;
this.rule = "Plot " + plotCost;
}
private PlotAbility(final PlotAbility ability) {
super(ability);
this.rule = ability.rule;
}
@Override
@ -30,6 +47,258 @@ public class PlotAbility extends SpecialAction {
@Override
public String getRule() {
return "Plot";
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
Card card = game.getCard(getSourceId());
if (card == null) {
return ActivationStatus.getFalse();
}
if (!card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game)) {
return ActivationStatus.getFalse();
}
return super.canActivate(playerId, game);
}
static UUID getPlotExileId(UUID playerId, Game game) {
UUID exileId = (UUID) game.getState().getValue("PlotExileId" + playerId.toString());
if (exileId == null) {
exileId = UUID.randomUUID();
game.getState().setValue("PlotExileId" + playerId, exileId);
}
return exileId;
}
static String getPlotTurnKeyForCard(UUID cardId) {
return cardId.toString() + "|" + "Plotted Turn";
}
/**
* To be used in an OneShotEffect's apply.
* 'Plot' the provided card. The card is exiled in it's owner plot zone,
* and may be cast by that player without paying its mana cost at sorcery
* speed on a future turn.
*/
public static boolean doExileAndPlotCard(Card card, Game game, Ability source) {
if (card == null) {
return false;
}
Player owner = game.getPlayer(card.getOwnerId());
if (owner == null) {
return false;
}
UUID exileId = PlotAbility.getPlotExileId(owner.getId(), game);
String exileZoneName = "Plots of " + owner.getName();
Card mainCard = card.getMainCard();
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.fireEvent(GameEvent.getEvent(GameEvent.EventType.BECOME_PLOTTED, mainCard.getId(), source, owner.getId()));
}
return true;
}
}
/**
* Exile the source card in the plot exile zone of its owner
* and allow its owner to cast it at sorcery speed starting
* next turn.
*/
class PlotSourceExileEffect extends OneShotEffect {
PlotSourceExileEffect() {
super(Outcome.Benefit);
}
private PlotSourceExileEffect(final PlotSourceExileEffect effect) {
super(effect);
}
@Override
public PlotSourceExileEffect copy() {
return new PlotSourceExileEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
return PlotAbility.doExileAndPlotCard(game.getCard(source.getSourceId()), game, source);
}
}
class PlotAddSpellAbilityEffect extends ContinuousEffectImpl {
private final MageObjectReference mor;
PlotAddSpellAbilityEffect(MageObjectReference mor) {
super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.mor = mor;
staticText = "Plot card";
}
private PlotAddSpellAbilityEffect(final PlotAddSpellAbilityEffect effect) {
super(effect);
this.mor = effect.mor;
}
@Override
public PlotAddSpellAbilityEffect copy() {
return new PlotAddSpellAbilityEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Card card = mor.getCard(game);
if (card == null) {
discard();
return true;
}
Card mainCard = card.getMainCard();
UUID mainCardId = mainCard.getId();
Player player = game.getPlayer(card.getOwnerId());
if (game.getState().getZone(mainCardId) != Zone.EXILED || player == null) {
discard();
return true;
}
List<Card> faces = CardUtil.getCastableComponents(mainCard, null, source, player, game, null, false);
for (Card face : faces) {
// Add the spell ability to each castable face to have the proper name/paramaters.
PlotSpellAbility ability = new PlotSpellAbility(face.getName());
ability.setSourceId(face.getId());
ability.setControllerId(player.getId());
ability.setSpellAbilityType(face.getSpellAbility().getSpellAbilityType());
game.getState().addOtherAbility(face, ability);
}
return true;
}
}
/**
* This is inspired (after a little cleanup) by how {@link ForetellAbility} does it.
*/
class PlotSpellAbility extends SpellAbility {
private String faceCardName; // Same as with Foretell, we identify the proper face with its spell name.
private SpellAbility spellAbilityToResolve;
PlotSpellAbility(String faceCardName) {
super(null, faceCardName, Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.PLOT);
this.setRuleVisible(false);
this.setAdditionalCostsRuleVisible(false);
this.faceCardName = faceCardName;
this.addCost(new ManaCostsImpl<>("{0}"));
}
private PlotSpellAbility(final PlotSpellAbility ability) {
super(ability);
this.faceCardName = ability.faceCardName;
this.spellAbilityToResolve = ability.spellAbilityToResolve;
}
@Override
public PlotSpellAbility copy() {
return new PlotSpellAbility(this);
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
if (super.canActivate(playerId, game).canActivate()) {
Card card = game.getCard(getSourceId());
if (card != null) {
Card mainCard = card.getMainCard();
UUID mainCardId = mainCard.getId();
// Card must be in the exile zone
if (game.getState().getZone(mainCardId) != Zone.EXILED) {
return ActivationStatus.getFalse();
}
Integer plottedTurn = (Integer) game.getState().getValue(PlotAbility.getPlotTurnKeyForCard(mainCardId));
// Card must have been plotted
if (plottedTurn == null) {
return ActivationStatus.getFalse();
}
// Can't be cast if the turn it was last Plotted is the same
if (plottedTurn == game.getTurnNum()) {
return ActivationStatus.getFalse();
}
// Only allow the cast at sorcery speed
if (!game.canPlaySorcery(playerId)) {
return ActivationStatus.getFalse();
}
// Check that the proper face can be cast.
// TODO: As with Foretell, this does not look very clean. Is the face card sometimes incorrect on calling canActivate?
if (mainCard instanceof CardWithHalves) {
if (((CardWithHalves) mainCard).getLeftHalfCard().getName().equals(faceCardName)) {
return ((CardWithHalves) mainCard).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((CardWithHalves) mainCard).getRightHalfCard().getName().equals(faceCardName)) {
return ((CardWithHalves) mainCard).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
} else if (card instanceof AdventureCard) {
if (card.getMainCard().getName().equals(faceCardName)) {
return card.getMainCard().getSpellAbility().canActivate(playerId, game);
} else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
}
}
return ActivationStatus.getFalse();
}
@Override
public SpellAbility getSpellAbilityToResolve(Game game) {
Card card = game.getCard(getSourceId());
if (card != null) {
if (spellAbilityToResolve == null) {
SpellAbility spellAbilityCopy = null;
// TODO: As with Foretell, this does not look very clean. Is the face card sometimes incorrect on calling getSpellAbilityToResolve?
if (card instanceof CardWithHalves) {
if (((CardWithHalves) card).getLeftHalfCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithHalves) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((CardWithHalves) card).getRightHalfCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithHalves) card).getRightHalfCard().getSpellAbility().copy();
}
} else if (card instanceof AdventureCard) {
if (card.getMainCard().getName().equals(faceCardName)) {
spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
} else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();
}
if (spellAbilityCopy == null) {
return null;
}
spellAbilityCopy.setId(this.getId());
spellAbilityCopy.clearManaCosts();
spellAbilityCopy.clearManaCostsToPay();
spellAbilityCopy.addCost(this.getCosts().copy());
spellAbilityCopy.addCost(this.getManaCosts().copy());
spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
spellAbilityToResolve = spellAbilityCopy;
}
}
return spellAbilityToResolve;
}
@Override
public Costs<Cost> getCosts() {
if (spellAbilityToResolve == null) {
return super.getCosts();
}
return spellAbilityToResolve.getCosts();
}
}