[OTJ] Implement Tinybones, the Pickpocket + refactor MayCastTargetThenExileEffect (#12040)

This commit is contained in:
Susucre 2024-04-05 00:16:53 +02:00 committed by GitHub
parent 3e75f93c20
commit d1de8b8cd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 412 additions and 150 deletions

View file

@ -8,7 +8,7 @@ import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.SacrificeTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.effects.common.counter.AddCountersAllEffect;
@ -61,7 +61,7 @@ public final class ChandraAcolyteOfFlame extends CardImpl {
this.addAbility(new LoyaltyAbility(new ChandraAcolyteOfFlameEffect(), 0));
// -2: You may cast target instant or sorcery card with converted mana cost 3 or less from your graveyard. If that card would be put into your graveyard this turn, exile it instead.
Ability ability = new LoyaltyAbility(new MayCastTargetThenExileEffect(false), -2);
Ability ability = new LoyaltyAbility(new MayCastTargetCardEffect(true), -2);
ability.addTarget(new TargetCardInYourGraveyard(filter2));
this.addAbility(ability);
}

View file

@ -6,7 +6,7 @@ import mage.abilities.LoyaltyAbility;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DamagePlayersEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.continuous.CastFromHandWithoutPayingManaCostEffect;
import mage.abilities.effects.common.discard.DiscardHandControllerEffect;
import mage.cards.CardImpl;
@ -33,7 +33,7 @@ public final class ChandraFlamesCatalyst extends CardImpl {
public ChandraFlamesCatalyst(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{4}{R}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.CHANDRA);
this.setStartingLoyalty(5);
@ -42,7 +42,7 @@ public final class ChandraFlamesCatalyst extends CardImpl {
this.addAbility(new LoyaltyAbility(new DamagePlayersEffect(3, TargetController.OPPONENT), 1));
// 2: You may cast target red instant or sorcery card from your graveyard. If that spell would be put into your graveyard this turn, exile it instead.
Ability ability = new LoyaltyAbility(new MayCastTargetThenExileEffect(false), -2);
Ability ability = new LoyaltyAbility(new MayCastTargetCardEffect(true), -2);
ability.addTarget(new TargetCardInYourGraveyard(filter));
this.addAbility(ability);

View file

@ -3,7 +3,7 @@ package mage.cards.d;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.SimpleEvasionAbility;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.abilities.keyword.CrewAbility;
@ -65,7 +65,7 @@ public final class DeluxeDragster extends CardImpl {
class DeluxeDragsterTriggeredAbility extends TriggeredAbilityImpl {
DeluxeDragsterTriggeredAbility() {
super(Zone.BATTLEFIELD, new MayCastTargetThenExileEffect(true)
super(Zone.BATTLEFIELD, new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true)
.setText("you may cast target instant or sorcery card from "
+ "that player's graveyard without paying its mana cost. "
+ ThatSpellGraveyardExileReplacementEffect.RULE_A), false);

View file

@ -4,12 +4,13 @@ import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CastManaAdjustment;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.filter.FilterCard;
@ -106,7 +107,8 @@ class DiluvianPrimordialEffect extends OneShotEffect {
if (target instanceof TargetCardInOpponentsGraveyard) {
Card targetCard = game.getCard(target.getFirstTarget());
if (targetCard != null) {
new MayCastTargetThenExileEffect(true).setTargetPointer(new FixedTarget(targetCard, game)).apply(game, source);
new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true)
.setTargetPointer(new FixedTarget(targetCard, game)).apply(game, source);
}
}
}

View file

@ -3,11 +3,12 @@ package mage.cards.d;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.keyword.TrampleAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CastManaAdjustment;
import mage.constants.SubType;
import mage.filter.FilterCard;
import mage.filter.common.FilterInstantOrSorceryCard;
@ -42,7 +43,7 @@ public final class DreadhordeArcanist extends CardImpl {
// Whenever Dreadhorde Arcanist attacks, you may cast target instant or sorcery card with converted mana cost less than or equal to Dreadhorde Arcanist's power from your graveyard without paying its mana cost.
// If that card would be put into your graveyard this turn, exile it instead.
Ability ability = new AttacksTriggeredAbility(new MayCastTargetThenExileEffect(true), false);
Ability ability = new AttacksTriggeredAbility(new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true), false);
ability.addTarget(new TargetCardInYourGraveyard(filter));
this.addAbility(ability);
}

View file

@ -3,11 +3,12 @@ package mage.cards.e;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.keyword.DoubleStrikeAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CastManaAdjustment;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import mage.target.common.TargetCardInYourGraveyard;
@ -21,7 +22,7 @@ public final class EfreetFlamepainter extends CardImpl {
public EfreetFlamepainter(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}");
this.subtype.add(SubType.EFREET);
this.subtype.add(SubType.SHAMAN);
this.power = new MageInt(1);
@ -31,7 +32,7 @@ public final class EfreetFlamepainter extends CardImpl {
this.addAbility(DoubleStrikeAbility.getInstance());
// Whenever Efreet Flamepainter deals combat damage to a player, you may cast target instant or sorcery card from your graveyard without paying its mana cost. If that spell would be put into your graveyard, exile it instead.
Ability ability = new DealsCombatDamageToAPlayerTriggeredAbility(new MayCastTargetThenExileEffect(true), false);
Ability ability = new DealsCombatDamageToAPlayerTriggeredAbility(new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true), false);
ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD));
this.addAbility(ability);
}

View file

@ -3,11 +3,12 @@ package mage.cards.f;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.common.ManacostVariableValue;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CastManaAdjustment;
import mage.constants.ComparisonType;
import mage.constants.Outcome;
import mage.filter.FilterCard;
@ -133,7 +134,8 @@ class FinaleOfPromiseEffect extends OneShotEffect {
for (UUID id : cardsToCast) {
Card card = game.getCard(id);
if (card != null) {
new MayCastTargetThenExileEffect(true).setTargetPointer(new FixedTarget(card, game)).apply(game, source);
new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true)
.setTargetPointer(new FixedTarget(card, game)).apply(game, source);
}
}

View file

@ -3,11 +3,12 @@ package mage.cards.g;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.keyword.MenaceAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CastManaAdjustment;
import mage.constants.ComparisonType;
import mage.constants.SubType;
import mage.filter.common.FilterInstantOrSorceryCard;
@ -39,7 +40,7 @@ public final class GoblinDarkDwellers extends CardImpl {
// When Goblin Dark-Dwellers enters the battlefield, you may cast target instant or sorcery card with converted mana cost 3 or less from your graveyard without paying its mana cost.
// If that card would be put into your graveyard this turn, exile it instead.
Ability ability = new EntersBattlefieldTriggeredAbility(new MayCastTargetThenExileEffect(true));
Ability ability = new EntersBattlefieldTriggeredAbility(new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true));
ability.addTarget(new TargetCardInYourGraveyard(filter));
this.addAbility(ability);
}

View file

@ -8,15 +8,12 @@ import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.ComparisonType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.common.FilterInstantOrSorceryCard;
import mage.filter.predicate.mageobject.ManaValuePredicate;
@ -90,7 +87,10 @@ class HaloForagerPayEffect extends OneShotEffect {
"instant or sorcery card with mana value " + costX + " from a graveyard"
);
filter.add(new ManaValuePredicate(ComparisonType.EQUAL_TO, costX));
ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(new MayCastTargetThenExileEffect(true), false);
ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(
new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true),
false
);
ability.addTarget(new TargetCardInGraveyard(filter));
game.fireReflexiveTriggeredAbility(ability, source);
return true;

View file

@ -1,11 +1,10 @@
package mage.cards.j;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.LoyaltyAbility;
import mage.abilities.effects.*;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.GetEmblemEffect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.continuous.BoostTargetEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
@ -18,8 +17,9 @@ import mage.game.command.emblems.JaceTelepathUnboundEmblem;
import mage.target.common.TargetCardInYourGraveyard;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/**
*
* @author LevelX2
*/
public final class JaceTelepathUnbound extends CardImpl {
@ -42,7 +42,7 @@ public final class JaceTelepathUnbound extends CardImpl {
this.addAbility(ability);
// -3: You may cast target instant or sorcery card from your graveyard this turn. If that card would be put into your graveyard this turn, exile it instead.
ability = new LoyaltyAbility(new MayCastTargetThenExileEffect(Duration.EndOfTurn), -3);
ability = new LoyaltyAbility(new MayCastTargetCardEffect(Duration.EndOfTurn, true), -3);
ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD));
this.addAbility(ability);

View file

@ -6,7 +6,7 @@ import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.LimitedTimesPerTurnActivatedAbility;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.cost.CostModificationEffectImpl;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.Card;
@ -61,10 +61,10 @@ public final class MavindaStudentsAdvocate extends CardImpl {
}
}
class MavindaStudentsAdvocateEffect extends MayCastTargetThenExileEffect {
class MavindaStudentsAdvocateEffect extends MayCastTargetCardEffect {
MavindaStudentsAdvocateEffect() {
super(Duration.EndOfTurn);
super(Duration.EndOfTurn, true);
staticText = "you may cast target instant or sorcery card from your graveyard this turn. " +
"If that spell doesn't target a creature you control, it costs {8} more to cast this way. " +
"If that spell would be put into your graveyard, exile it instead";

View file

@ -2,7 +2,7 @@ package mage.cards.m;
import mage.abilities.Ability;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.cards.CardImpl;
@ -68,7 +68,7 @@ class MissionBriefingEffect extends OneShotEffect {
player.surveil(2, source, game);
Target target = new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD);
player.choose(outcome, target, source, game);
Effect effect = new MayCastTargetThenExileEffect(Duration.EndOfTurn);
Effect effect = new MayCastTargetCardEffect(Duration.EndOfTurn, true);
effect.setTargetPointer(new FixedTarget(target.getFirstTarget(), game));
effect.apply(game, source);
return true;

View file

@ -1,11 +1,12 @@
package mage.cards.s;
import mage.abilities.effects.common.ExileSpellEffect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CastManaAdjustment;
import mage.filter.StaticFilters;
import mage.target.common.TargetCardInYourGraveyard;
@ -20,7 +21,7 @@ public final class SinsOfThePast extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{B}{B}");
// Until end of turn, you may cast target instant or sorcery card from your graveyard without paying its mana cost. If that card would be put into your graveyard this turn, exile it instead. Exile Sins of the Past.
this.getSpellAbility().addEffect(new MayCastTargetThenExileEffect(true)
this.getSpellAbility().addEffect(new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true)
.setText("Until end of turn, you may cast target instant or sorcery card from your graveyard without paying its mana cost. "
+ ThatSpellGraveyardExileReplacementEffect.RULE_YOUR));
this.getSpellAbility().addEffect(new ExileSpellEffect());

View file

@ -3,13 +3,14 @@ package mage.cards.s;
import mage.abilities.Ability;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.MillCardsTargetEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.abilities.keyword.DelveAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CastManaAdjustment;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.filter.common.FilterInstantOrSorceryCard;
@ -24,7 +25,6 @@ import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
*
* @author notgreat
*/
public final class SorcerousSquall extends CardImpl {
@ -57,7 +57,7 @@ class SorcerousSquallEffect extends OneShotEffect {
SorcerousSquallEffect() {
super(Outcome.Detriment);
setText("you may cast an instant or sorcery spell from that player's graveyard without paying its mana cost. "+ ThatSpellGraveyardExileReplacementEffect.RULE_A);
setText("you may cast an instant or sorcery spell from that player's graveyard without paying its mana cost. " + ThatSpellGraveyardExileReplacementEffect.RULE_A);
}
private SorcerousSquallEffect(final SorcerousSquallEffect effect) {
@ -79,7 +79,7 @@ class SorcerousSquallEffect extends OneShotEffect {
filter.add(new OwnerIdPredicate(source.getFirstTarget()));
Target target = new TargetCardInGraveyard(1, 1, filter, true);
player.choose(outcome, target, source, game);
Effect effect = new MayCastTargetThenExileEffect(true);
Effect effect = new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true);
effect.setTargetPointer(new FixedTarget(target.getFirstTarget(), game));
effect.apply(game, source);
return true;

View file

@ -0,0 +1,101 @@
package mage.cards.t;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.keyword.DeathtouchAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.common.FilterPermanentCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.card.OwnerIdPredicate;
import mage.game.Game;
import mage.game.events.DamagedPlayerEvent;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.target.Target;
import mage.target.common.TargetCardInGraveyard;
import java.util.UUID;
/**
* @author Susucr
*/
public final class TinybonesThePickpocket extends CardImpl {
public TinybonesThePickpocket(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{B}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.SKELETON);
this.subtype.add(SubType.ROGUE);
this.power = new MageInt(1);
this.toughness = new MageInt(1);
// Deathtouch
this.addAbility(DeathtouchAbility.getInstance());
// Whenever Tinybones, the Pickpocket deals combat damage to a player, you may cast target nonland permanent card from that player's graveyard, and mana of any type can be spent to cast that spell.
this.addAbility(new TinybonesThePickpocketTriggeredAbility());
}
private TinybonesThePickpocket(final TinybonesThePickpocket card) {
super(card);
}
@Override
public TinybonesThePickpocket copy() {
return new TinybonesThePickpocket(this);
}
}
/**
* Similar to {@link mage.cards.w.WrexialTheRisenDeep}
*/
class TinybonesThePickpocketTriggeredAbility extends TriggeredAbilityImpl {
TinybonesThePickpocketTriggeredAbility() {
super(
Zone.BATTLEFIELD,
new MayCastTargetCardEffect(CastManaAdjustment.AS_THOUGH_ANY_MANA_TYPE, false)
.setText("you may cast target nonland permanent card from "
+ "that player's graveyard, and mana of any type can be spent to cast that spell"),
false
);
setTriggerPhrase("Whenever {this} deals combat damage to a player, ");
}
private TinybonesThePickpocketTriggeredAbility(final TinybonesThePickpocketTriggeredAbility ability) {
super(ability);
}
@Override
public TinybonesThePickpocketTriggeredAbility copy() {
return new TinybonesThePickpocketTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_PLAYER;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!event.getSourceId().equals(this.sourceId) || !((DamagedPlayerEvent) event).isCombatDamage()) {
return false;
}
Player damagedPlayer = game.getPlayer(event.getTargetId());
if (damagedPlayer == null) {
return false;
}
FilterCard filter = new FilterPermanentCard("nonland permanent card from " + damagedPlayer.getName() + "'s graveyard");
filter.add(new OwnerIdPredicate(damagedPlayer.getId()));
filter.add(Predicates.not(CardType.LAND.getPredicate()));
Target target = new TargetCardInGraveyard(filter);
this.getTargets().clear();
this.addTarget(target);
return true;
}
}

View file

@ -3,11 +3,12 @@ package mage.cards.t;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.keyword.FlashAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.CastManaAdjustment;
import mage.constants.SubType;
import mage.filter.FilterCard;
import mage.target.common.TargetCardInYourGraveyard;
@ -37,7 +38,7 @@ public final class TorrentialGearhulk extends CardImpl {
// When Torrential Gearhulk enters the battlefield, you may cast target
// instant card from your graveyard without paying its mana cost.
// If that card would be put into your graveyard this turn, exile it instead.
Ability ability = new EntersBattlefieldTriggeredAbility(new MayCastTargetThenExileEffect(true));
Ability ability = new EntersBattlefieldTriggeredAbility(new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true));
ability.addTarget(new TargetCardInYourGraveyard(filter));
this.addAbility(ability);
}

View file

@ -3,7 +3,7 @@ package mage.cards.t;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.DiesCreatureTriggeredAbility;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.abilities.keyword.BushidoAbility;
import mage.cards.CardImpl;
@ -39,7 +39,7 @@ public final class ToshiroUmezawa extends CardImpl {
// Bushido 1
this.addAbility(new BushidoAbility(1));
// Whenever a creature an opponent controls dies, you may cast target instant card from your graveyard. If that card would be put into a graveyard this turn, exile it instead.
Ability ability = new DiesCreatureTriggeredAbility(new MayCastTargetThenExileEffect(false)
Ability ability = new DiesCreatureTriggeredAbility(new MayCastTargetCardEffect(true)
.setText("you may cast target instant card from your graveyard. "
+ ThatSpellGraveyardExileReplacementEffect.RULE_A),
true, StaticFilters.FILTER_OPPONENTS_PERMANENT_A_CREATURE);

View file

@ -1,6 +1,5 @@
package mage.cards.v;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.ActivateAsSorceryActivatedAbility;
@ -8,19 +7,20 @@ import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.SacrificeSourceCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.cards.Card;
import mage.constants.*;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInYourGraveyard;
import java.util.UUID;
/**
*
* @author weirddan455
*/
public final class VoharVodalianDesecrator extends CardImpl {
@ -40,7 +40,7 @@ public final class VoharVodalianDesecrator extends CardImpl {
// {2}, Sacrifice Vohar, Vodalian Desecrator: You may cast target instant or sorcery card from your graveyard this turn. If that spell would be put into your graveyard, exile it instead. Activate only as a sorcery.
Ability ability = new ActivateAsSorceryActivatedAbility(
new MayCastTargetThenExileEffect(Duration.EndOfTurn), new GenericManaCost(2)
new MayCastTargetCardEffect(Duration.EndOfTurn, true), new GenericManaCost(2)
);
ability.addCost(new SacrificeSourceCost());
ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_INSTANT_OR_SORCERY_FROM_YOUR_GRAVEYARD));

View file

@ -2,7 +2,7 @@ package mage.cards.w;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.common.MayCastTargetThenExileEffect;
import mage.abilities.effects.common.MayCastTargetCardEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.abilities.keyword.IslandwalkAbility;
import mage.abilities.keyword.SwampwalkAbility;
@ -57,7 +57,7 @@ public final class WrexialTheRisenDeep extends CardImpl {
class WrexialTheRisenDeepTriggeredAbility extends TriggeredAbilityImpl {
WrexialTheRisenDeepTriggeredAbility() {
super(Zone.BATTLEFIELD, new MayCastTargetThenExileEffect(true)
super(Zone.BATTLEFIELD, new MayCastTargetCardEffect(CastManaAdjustment.WITHOUT_PAYING_MANA_COST, true)
.setText("you may cast target instant or sorcery card from "
+ "that player's graveyard without paying its mana cost. "
+ ThatSpellGraveyardExileReplacementEffect.RULE_A), false);
@ -92,7 +92,7 @@ class WrexialTheRisenDeepTriggeredAbility extends TriggeredAbilityImpl {
filter.add(Predicates.or(
CardType.INSTANT.getPredicate(),
CardType.SORCERY.getPredicate()
));
));
Target target = new TargetCardInGraveyard(filter);
this.getTargets().clear();
this.addTarget(target);

View file

@ -283,6 +283,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet {
cards.add(new SetCardInfo("Thunder Lasso", 35, Rarity.UNCOMMON, mage.cards.t.ThunderLasso.class));
cards.add(new SetCardInfo("Thunder Salvo", 150, Rarity.COMMON, mage.cards.t.ThunderSalvo.class));
cards.add(new SetCardInfo("Tinybones Joins Up", 108, Rarity.RARE, mage.cards.t.TinybonesJoinsUp.class));
cards.add(new SetCardInfo("Tinybones, the Pickpocket", 109, Rarity.MYTHIC, mage.cards.t.TinybonesThePickpocket.class));
cards.add(new SetCardInfo("Tomb Trawler", 250, Rarity.UNCOMMON, mage.cards.t.TombTrawler.class));
cards.add(new SetCardInfo("Trained Arynx", 36, Rarity.COMMON, mage.cards.t.TrainedArynx.class));
cards.add(new SetCardInfo("Trash the Town", 186, Rarity.UNCOMMON, mage.cards.t.TrashTheTown.class));

View file

@ -0,0 +1,59 @@
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 TinybonesThePickpocketTest extends CardTestPlayerBase {
/**
* {@link mage.cards.t.TinybonesThePickpocket Tinybones, the Pickpocket} {B}
* Legendary Creature Skeleton Rogue
* Deathtouch
* Whenever Tinybones, the Pickpocket deals combat damage to a player, you may cast target nonland permanent card from that player's graveyard, and mana of any type can be spent to cast that spell.
* 1/1
*/
private static final String tinybones = "Tinybones, the Pickpocket";
@Test
public void test_CastPermanent_WithOtherType() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, tinybones);
addCard(Zone.GRAVEYARD, playerB, "Raging Goblin");
addCard(Zone.BATTLEFIELD, playerA, "Swamp");
attack(1, playerA, tinybones, playerB);
addTarget(playerA, "Raging Goblin"); // target card for the trigger
setChoice(playerA, true); // yes to "you may cast"
setStopAt(1, PhaseStep.END_COMBAT);
execute();
assertPermanentCount(playerA, "Raging Goblin", 1);
assertTapped("Swamp", true); // It did cost 1 mana
}
@Test
public void test_NoToCast() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, tinybones);
addCard(Zone.GRAVEYARD, playerB, "Raging Goblin");
addCard(Zone.BATTLEFIELD, playerA, "Swamp");
attack(1, playerA, tinybones, playerB);
addTarget(playerA, "Raging Goblin"); // target card for the trigger
setChoice(playerA, false); // no to "you may cast"
setStopAt(1, PhaseStep.END_COMBAT);
execute();
assertGraveyardCount(playerB, "Raging Goblin", 1); // card did not move.
assertTapped("Swamp", false);
}
}

View file

@ -0,0 +1,158 @@
package mage.abilities.effects.common;
import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.asthought.YouMaySpendManaAsAnyColorToCastTargetEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.cards.Card;
import mage.constants.CastManaAdjustment;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
/**
* @author xenohedron, Susucr
*/
public class MayCastTargetCardEffect extends OneShotEffect {
private final Duration duration;
private final CastManaAdjustment manaAdjustment;
private final boolean thenExile; // Should the spell be exiled by a replacement effect if cast and it resolves?
/**
* Allows to cast the target card immediately, for its manacost.
*/
public MayCastTargetCardEffect(boolean exileOnResolve) {
this(CastManaAdjustment.NONE, exileOnResolve);
}
/**
* Allows to cast the target card immediately, either for its cost or with a modifier (like for free, or mana as any type).
*/
public MayCastTargetCardEffect(CastManaAdjustment manaAdjustment, boolean exileOnResolve) {
this(Duration.OneUse, manaAdjustment, exileOnResolve);
}
/**
* Makes the target card playable for the specified duration as long as it remains in that zone.
*/
public MayCastTargetCardEffect(Duration duration, boolean exileOnResolve) {
this(duration, CastManaAdjustment.NONE, exileOnResolve);
}
protected MayCastTargetCardEffect(Duration duration, CastManaAdjustment manaAdjustment, boolean thenExile) {
super(Outcome.Benefit);
this.duration = duration;
this.manaAdjustment = manaAdjustment;
this.thenExile = thenExile;
// TODO: support the non-yet-supported combinations.
// for now the constructor chains won't allow those.
if (duration != Duration.OneUse && manaAdjustment != CastManaAdjustment.NONE) {
throw new IllegalStateException(
"Wrong code usage, not yet supported "
+ "duration={" + duration.name() + "}, "
+ "manaAdjustment={" + manaAdjustment.name() + "}"
);
}
}
protected MayCastTargetCardEffect(final MayCastTargetCardEffect effect) {
super(effect);
this.duration = effect.duration;
this.manaAdjustment = effect.manaAdjustment;
this.thenExile = effect.thenExile;
}
@Override
public MayCastTargetCardEffect copy() {
return new MayCastTargetCardEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Card card = game.getCard(getTargetPointer().getFirst(game, source));
if (card == null) {
return false;
}
if (duration == Duration.OneUse) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null || !controller.chooseUse(outcome, "Cast " + card.getLogName() + '?', source, game)) {
return false;
}
switch (manaAdjustment) {
case NONE:
case WITHOUT_PAYING_MANA_COST:
break;
case AS_THOUGH_ANY_MANA_COLOR:
case AS_THOUGH_ANY_MANA_TYPE:
// TODO: untangle why there is a confusion between the two.
ContinuousEffect effect =
new YouMaySpendManaAsAnyColorToCastTargetEffect(Duration.Custom, controller.getId(), null);
effect.setTargetPointer(new FixedTarget(card, game));
game.addEffect(effect, source);
break;
default:
throw new IllegalArgumentException("Wrong code usage, manaAdjustment is not yet supported: " + manaAdjustment);
}
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
boolean noMana = manaAdjustment == CastManaAdjustment.WITHOUT_PAYING_MANA_COST;
controller.cast(controller.chooseAbilityForCast(card, game, noMana),
game, noMana, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
} else {
// TODO: support (and add tests!) for the non-NONE manaAdjustment
CardUtil.makeCardPlayable(game, source, card, duration, false);
}
if (thenExile) {
ContinuousEffect effect = new ThatSpellGraveyardExileReplacementEffect(true);
effect.setTargetPointer(new FixedTarget(card, game));
game.addEffect(effect, source);
}
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
String text = "you may cast " + getTargetPointer().describeTargets(mode.getTargets(), "it");
if (duration == Duration.EndOfTurn) {
text += " this turn";
} else if (!duration.toString().isEmpty()) {
text += duration.toString();
}
switch (manaAdjustment) {
case NONE:
break;
case WITHOUT_PAYING_MANA_COST:
text += " without paying its mana cost";
break;
case AS_THOUGH_ANY_MANA_COLOR:
text += ", and mana of any color can be spent to cast that spell";
break;
case AS_THOUGH_ANY_MANA_TYPE:
text += ", and mana of any type can be spent to cast that spell";
break;
default:
throw new IllegalArgumentException("Wrong code usage, manaAdjustment is not yet supported: " + manaAdjustment);
}
text += ".";
if (thenExile) {
text += " " + ThatSpellGraveyardExileReplacementEffect.RULE_YOUR;
}
return text;
}
}

View file

@ -1,98 +0,0 @@
package mage.abilities.effects.common;
import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.cards.Card;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
/**
* @author xenohedron
*/
public class MayCastTargetThenExileEffect extends OneShotEffect {
private final Duration duration;
private final boolean noMana;
/**
* Allows to cast the target card immediately, either for its cost or for free.
* If resulting spell would be put into graveyard, exiles it instead.
*/
public MayCastTargetThenExileEffect(boolean noMana) {
super(Outcome.Benefit);
this.duration = Duration.OneUse;
this.noMana = noMana;
}
/**
* Makes the target card playable for the specified duration as long as it remains in that zone.
* If resulting spell would be put into graveyard, exiles it instead.
*/
public MayCastTargetThenExileEffect(Duration duration) {
super(Outcome.Benefit);
this.duration = duration;
this.noMana = false;
}
protected MayCastTargetThenExileEffect(final MayCastTargetThenExileEffect effect) {
super(effect);
this.duration = effect.duration;
this.noMana = effect.noMana;
}
@Override
public MayCastTargetThenExileEffect copy() {
return new MayCastTargetThenExileEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Card card = game.getCard(getTargetPointer().getFirst(game, source));
if (card == null) {
return false;
}
FixedTarget fixedTarget = new FixedTarget(card, game);
if (duration == Duration.OneUse) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null || !controller.chooseUse(outcome, "Cast " + card.getLogName() + '?', source, game)) {
return false;
}
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
controller.cast(controller.chooseAbilityForCast(card, game, noMana),
game, noMana, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
} else {
CardUtil.makeCardPlayable(game, source, card, duration, false);
}
ContinuousEffect effect = new ThatSpellGraveyardExileReplacementEffect(true);
effect.setTargetPointer(fixedTarget);
game.addEffect(effect, source);
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
String text = "you may cast " + getTargetPointer().describeTargets(mode.getTargets(), "it");
if (duration == Duration.EndOfTurn) {
text += " this turn";
} else if (!duration.toString().isEmpty()) {
text += duration.toString();
}
if (noMana) {
text += " without paying its mana cost";
}
return text + ". " + ThatSpellGraveyardExileReplacementEffect.RULE_YOUR;
}
}

View file

@ -16,6 +16,7 @@ import java.util.UUID;
/**
* Spend mana as any color to cast targeted card. Will not affected after any card movements or blinks.
* Affects to all card's parts
* TODO: AnyType and AnyColor are confused there.
*
* @author JayDi85
*/

View file

@ -0,0 +1,29 @@
package mage.constants;
/**
* Groups together the most usual ways a card's payment is adjusted
* by card effects that allow play or cast.
* <p>
* Effects should attempt to support those for all the various ways
* to play/cast cards/spells in Effects
*
* @author Susucr
*/
public enum CastManaAdjustment {
/**
* No adjustment to play/cast
*/
NONE,
/**
* Mana can be used as any mana type to pay for the mana cost
*/
AS_THOUGH_ANY_MANA_TYPE,
/**
* Mana can be used as any mana color to pay for the mana cost
*/
AS_THOUGH_ANY_MANA_COLOR,
/**
* The card is play/cast without paying for its mana cost
*/
WITHOUT_PAYING_MANA_COST,
}

View file

@ -1280,6 +1280,7 @@ public final class CardUtil {
}
}
// TODO: use CastManaAdjustment instead of boolean anyColor
public static void makeCardPlayable(Game game, Ability source, Card card, Duration duration, boolean anyColor) {
makeCardPlayable(game, source, card, duration, anyColor, null, null);
}
@ -1296,6 +1297,7 @@ public final class CardUtil {
* @param anyColor
* @param condition can be null
*/
// TODO: use CastManaAdjustment instead of boolean anyColor
public static void makeCardPlayable(Game game, Ability source, Card card, Duration duration, boolean anyColor, UUID playerId, Condition condition) {
// Effect can be used for cards in zones and permanents on battlefield
// PermanentCard's ZCC is static, but we need updated ZCC from the card (after moved to another zone)