[Ready for review] Implementing Warp mechanic (#13847)

* add initial warp mechanic implementation

* a few small changes

* add hand restriction

* add void support

* add test

* [EOE] Implement Timeline Culler

* add void test

* [EOE] Implement Close Encounter

* [EOE] Implement Tannuk, Steadfast Second

* a few requested changes

* add comment

* [EOE] Implement Full Bore

* small rewrite

* merge fix

* remove reminder text

* small code rewrite
This commit is contained in:
Evan Kranzler 2025-07-18 21:01:50 -04:00 committed by GitHub
parent ae0e4e1483
commit df70ab7c8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 752 additions and 15 deletions

View file

@ -0,0 +1,174 @@
package mage.cards.c;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.DamageTargetEffect;
import mage.abilities.keyword.WarpAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.filter.common.FilterCreatureCard;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.TargetPermanent;
import mage.target.common.TargetCardInExile;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetCreaturePermanent;
import mage.util.CardUtil;
import java.util.Optional;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class CloseEncounter extends CardImpl {
public CloseEncounter(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}");
// As an additional cost to cast this spell, choose a creature you control or a warped creature card you own in exile.
this.getSpellAbility().addCost(new CloseEncounterCost());
// Close Encounter deals damage equal to the power of the chosen creature or card to target creature.
this.getSpellAbility().addEffect(new DamageTargetEffect(CloseEncounterValue.instance));
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
}
private CloseEncounter(final CloseEncounter card) {
super(card);
}
@Override
public CloseEncounter copy() {
return new CloseEncounter(this);
}
}
enum CloseEncounterValue implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return Optional
.ofNullable((Card) effect.getValue("closeEncounterCost"))
.map(MageObject::getPower)
.map(MageInt::getValue)
.orElse(0);
}
@Override
public CloseEncounterValue copy() {
return this;
}
@Override
public String getMessage() {
return "the power of the chosen creature or card";
}
@Override
public String toString() {
return "1";
}
}
class CloseEncounterCost extends CostImpl {
private static final FilterCard filterCard = new FilterCreatureCard("warped creature card you own in exile");
public CloseEncounterCost() {
super();
this.text = "choose a creature you control or a warped creature card you own in exile";
}
private CloseEncounterCost(final CloseEncounterCost cost) {
super(cost);
}
@Override
public CloseEncounterCost copy() {
return new CloseEncounterCost(this);
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return Optional
.ofNullable(CardUtil.getExileZoneId(WarpAbility.makeWarpString(controllerId), game))
.map(game.getExile()::getExileZone)
.filter(exileZone -> !exileZone.getCards(filterCard, game).isEmpty())
.isPresent()
|| game
.getBattlefield()
.contains(StaticFilters.FILTER_CONTROLLED_CREATURE, controllerId, source, game, 1);
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player player = game.getPlayer(controllerId);
if (player == null) {
paid = false;
return paid;
}
boolean hasPermanent = game
.getBattlefield()
.contains(StaticFilters.FILTER_CONTROLLED_CREATURE, controllerId, source, game, 1);
boolean hasWarp = Optional
.ofNullable(CardUtil.getExileZoneId(WarpAbility.makeWarpString(controllerId), game))
.map(game.getExile()::getExileZone)
.filter(exileZone -> !exileZone.getCards(filterCard, game).isEmpty())
.isPresent();
boolean usePermanent;
if (hasPermanent && hasWarp) {
usePermanent = player.chooseUse(
Outcome.Neutral, "Choose a creature you control or a warped creature you own in exile?",
null, "Choose controlled", "Choose from exile", source, game
);
} else if (hasPermanent) {
usePermanent = true;
} else if (hasWarp) {
usePermanent = false;
} else {
paid = false;
return paid;
}
if (usePermanent) {
TargetPermanent target = new TargetControlledCreaturePermanent();
target.withNotTarget(true);
player.choose(Outcome.Neutral, target, source, game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent == null) {
paid = false;
return paid;
}
game.informPlayers(player.getLogName() + " chooses " + permanent.getLogName() + " on the battlefield");
source.getEffects().setValue("closeEncounterCost", permanent);
paid = true;
return true;
}
TargetCard target = new TargetCardInExile(
filterCard, CardUtil.getExileZoneId(WarpAbility.makeWarpString(controllerId), game)
);
player.choose(Outcome.Neutral, target, source, game);
Card card = game.getCard(target.getFirstTarget());
if (card == null) {
paid = false;
return paid;
}
game.informPlayers(player.getLogName() + " chooses " + card.getLogName() + " from exile");
source.getEffects().setValue("closeEncounterCost", card);
paid = true;
return paid;
}
}

View file

@ -0,0 +1,79 @@
package mage.cards.f;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.continuous.BoostTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.abilities.keyword.WarpAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.targetpointer.FixedTarget;
import java.util.Collections;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class FullBore extends CardImpl {
public FullBore(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}");
// Target creature you control gets +3/+2 until end of turn. If that creature was cast for its warp cost, it also gains trample and haste until end of turn.
this.getSpellAbility().addEffect(new BoostTargetEffect(3, 2));
this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent());
this.getSpellAbility().addEffect(new FullBoreEffect());
}
private FullBore(final FullBore card) {
super(card);
}
@Override
public FullBore copy() {
return new FullBore(this);
}
}
class FullBoreEffect extends OneShotEffect {
FullBoreEffect() {
super(Outcome.Benefit);
staticText = "If that creature was cast for its warp cost, it also gains trample and haste until end of turn";
}
private FullBoreEffect(final FullBoreEffect effect) {
super(effect);
}
@Override
public FullBoreEffect copy() {
return new FullBoreEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent == null
|| !game
.getPermanentCostsTags()
.getOrDefault(new MageObjectReference(permanent, game, -1), Collections.emptyMap())
.containsKey(WarpAbility.WARP_ACTIVATION_VALUE_KEY)) {
return false;
}
game.addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance())
.setTargetPointer(new FixedTarget(permanent, game)), source);
game.addEffect(new GainAbilityTargetEffect(HasteAbility.getInstance())
.setTargetPointer(new FixedTarget(permanent, game)), source);
return true;
}
}

View file

@ -0,0 +1,97 @@
package mage.cards.t;
import mage.MageInt;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.common.continuous.GainAbilityControlledEffect;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.WarpAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.ColorPredicate;
import mage.game.Game;
import mage.players.Player;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class TannukSteadfastSecond extends CardImpl {
public TannukSteadfastSecond(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.KAVU);
this.subtype.add(SubType.PILOT);
this.power = new MageInt(3);
this.toughness = new MageInt(5);
// Other creatures you control have haste.
this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect(
HasteAbility.getInstance(), Duration.WhileOnBattlefield,
StaticFilters.FILTER_PERMANENT_CREATURES, true
)));
// Artifact cards and red creature cards in your hand have warp {2}{R}.
this.addAbility(new SimpleStaticAbility(new TannukSteadfastSecondEffect()));
}
private TannukSteadfastSecond(final TannukSteadfastSecond card) {
super(card);
}
@Override
public TannukSteadfastSecond copy() {
return new TannukSteadfastSecond(this);
}
}
class TannukSteadfastSecondEffect extends ContinuousEffectImpl {
private static final FilterCard filter = new FilterCard();
static {
filter.add(Predicates.or(
CardType.ARTIFACT.getPredicate(),
Predicates.and(
new ColorPredicate(ObjectColor.RED),
CardType.CREATURE.getPredicate()
)
));
}
TannukSteadfastSecondEffect() {
super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.staticText = "artifact cards and red creature cards in your hand have warp {2}{R}";
}
private TannukSteadfastSecondEffect(final TannukSteadfastSecondEffect effect) {
super(effect);
}
@Override
public TannukSteadfastSecondEffect copy() {
return new TannukSteadfastSecondEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
for (Card card : controller.getHand().getCards(filter, game)) {
game.getState().addOtherAbility(card, new WarpAbility(card, "{2}{R}"));
}
return true;
}
}

View file

@ -0,0 +1,86 @@
package mage.cards.t;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.PayLifeCost;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.WarpAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.Game;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class TimelineCuller extends CardImpl {
public TimelineCuller(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{B}{B}");
this.subtype.add(SubType.DRIX);
this.subtype.add(SubType.WARLOCK);
this.power = new MageInt(2);
this.toughness = new MageInt(2);
// Haste
this.addAbility(HasteAbility.getInstance());
// You may cast this card from your graveyard using its warp ability.
this.addAbility(new SimpleStaticAbility(Zone.ALL, new TimelineCullerEffect()));
// Warp--{B}, Pay 2 life.
Ability ability = new WarpAbility(this, "{B}", true);
ability.addCost(new PayLifeCost(2));
this.addAbility(ability);
}
private TimelineCuller(final TimelineCuller card) {
super(card);
}
@Override
public TimelineCuller copy() {
return new TimelineCuller(this);
}
}
class TimelineCullerEffect extends AsThoughEffectImpl {
TimelineCullerEffect() {
super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.PutCreatureInPlay);
staticText = "you may cast this card from your graveyard using its warp ability";
}
private TimelineCullerEffect(final TimelineCullerEffect effect) {
super(effect);
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public TimelineCullerEffect copy() {
return new TimelineCullerEffect(this);
}
@Override
public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) {
return objectId.equals(source.getSourceId())
&& source.isControlledBy(playerId)
&& affectedAbility instanceof WarpAbility
&& game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD
&& game.getCard(source.getSourceId()) != null;
}
@Override
public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) {
return false;
}
}

View file

@ -4,15 +4,11 @@ import mage.cards.ExpansionSet;
import mage.constants.Rarity; import mage.constants.Rarity;
import mage.constants.SetType; import mage.constants.SetType;
import java.util.Arrays;
import java.util.List;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
public final class EdgeOfEternities extends ExpansionSet { public final class EdgeOfEternities extends ExpansionSet {
private static final List<String> unfinished = Arrays.asList("All-Fates Stalker", "Anticausal Vestige", "Astelli Reclaimer", "Broodguard Elite", "Bygone Colossus", "Codecracker Hound", "Drix Fatemaker", "Eusocial Engineering", "Exalted Sunborn", "Germinating Wurm", "Haliya, Guided by Light", "Knight Luminary", "Loading Zone", "Mechanozoa", "Memorial Team Leader", "Mightform Harmonizer", "Nova Hellkite", "Perigee Beckoner", "Pinnacle Emissary", "Possibility Technician", "Quantum Riddler", "Rayblade Trooper", "Red Tiger Mechan", "Sinister Cryologist", "Starbreach Whale", "Starfield Shepherd", "Starfield Vocalist", "Starwinder", "Susurian Voidborn", "Timeline Culler", "Weftblade Enhancer", "Weftstalker Ardent");
private static final EdgeOfEternities instance = new EdgeOfEternities(); private static final EdgeOfEternities instance = new EdgeOfEternities();
public static EdgeOfEternities getInstance() { public static EdgeOfEternities getInstance() {
@ -71,6 +67,7 @@ public final class EdgeOfEternities extends ExpansionSet {
cards.add(new SetCardInfo("Chorale of the Void", 331, Rarity.RARE, mage.cards.c.ChoraleOfTheVoid.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Chorale of the Void", 331, Rarity.RARE, mage.cards.c.ChoraleOfTheVoid.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Chorale of the Void", 91, Rarity.RARE, mage.cards.c.ChoraleOfTheVoid.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Chorale of the Void", 91, Rarity.RARE, mage.cards.c.ChoraleOfTheVoid.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Chrome Companion", 236, Rarity.COMMON, mage.cards.c.ChromeCompanion.class)); cards.add(new SetCardInfo("Chrome Companion", 236, Rarity.COMMON, mage.cards.c.ChromeCompanion.class));
cards.add(new SetCardInfo("Close Encounter", 176, Rarity.UNCOMMON, mage.cards.c.CloseEncounter.class));
cards.add(new SetCardInfo("Cloudsculpt Technician", 49, Rarity.COMMON, mage.cards.c.CloudsculptTechnician.class)); cards.add(new SetCardInfo("Cloudsculpt Technician", 49, Rarity.COMMON, mage.cards.c.CloudsculptTechnician.class));
cards.add(new SetCardInfo("Codecracker Hound", 50, Rarity.UNCOMMON, mage.cards.c.CodecrackerHound.class)); cards.add(new SetCardInfo("Codecracker Hound", 50, Rarity.UNCOMMON, mage.cards.c.CodecrackerHound.class));
cards.add(new SetCardInfo("Comet Crawler", 92, Rarity.COMMON, mage.cards.c.CometCrawler.class)); cards.add(new SetCardInfo("Comet Crawler", 92, Rarity.COMMON, mage.cards.c.CometCrawler.class));
@ -142,6 +139,7 @@ public final class EdgeOfEternities extends ExpansionSet {
cards.add(new SetCardInfo("Frenzied Baloth", 183, Rarity.RARE, mage.cards.f.FrenziedBaloth.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Frenzied Baloth", 183, Rarity.RARE, mage.cards.f.FrenziedBaloth.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Frenzied Baloth", 342, Rarity.RARE, mage.cards.f.FrenziedBaloth.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Frenzied Baloth", 342, Rarity.RARE, mage.cards.f.FrenziedBaloth.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Frontline War-Rager", 134, Rarity.COMMON, mage.cards.f.FrontlineWarRager.class)); cards.add(new SetCardInfo("Frontline War-Rager", 134, Rarity.COMMON, mage.cards.f.FrontlineWarRager.class));
cards.add(new SetCardInfo("Full Bore", 135, Rarity.UNCOMMON, mage.cards.f.FullBore.class));
cards.add(new SetCardInfo("Fungal Colossus", 184, Rarity.COMMON, mage.cards.f.FungalColossus.class)); cards.add(new SetCardInfo("Fungal Colossus", 184, Rarity.COMMON, mage.cards.f.FungalColossus.class));
cards.add(new SetCardInfo("Galactic Wayfarer", 185, Rarity.COMMON, mage.cards.g.GalacticWayfarer.class)); cards.add(new SetCardInfo("Galactic Wayfarer", 185, Rarity.COMMON, mage.cards.g.GalacticWayfarer.class));
cards.add(new SetCardInfo("Galvanizing Sawship", 136, Rarity.UNCOMMON, mage.cards.g.GalvanizingSawship.class)); cards.add(new SetCardInfo("Galvanizing Sawship", 136, Rarity.UNCOMMON, mage.cards.g.GalvanizingSawship.class));
@ -361,6 +359,8 @@ public final class EdgeOfEternities extends ExpansionSet {
cards.add(new SetCardInfo("Syr Vondam, the Lucent", 232, Rarity.UNCOMMON, mage.cards.s.SyrVondamTheLucent.class)); cards.add(new SetCardInfo("Syr Vondam, the Lucent", 232, Rarity.UNCOMMON, mage.cards.s.SyrVondamTheLucent.class));
cards.add(new SetCardInfo("Systems Override", 161, Rarity.UNCOMMON, mage.cards.s.SystemsOverride.class)); cards.add(new SetCardInfo("Systems Override", 161, Rarity.UNCOMMON, mage.cards.s.SystemsOverride.class));
cards.add(new SetCardInfo("Tannuk, Memorial Ensign", 233, Rarity.UNCOMMON, mage.cards.t.TannukMemorialEnsign.class)); cards.add(new SetCardInfo("Tannuk, Memorial Ensign", 233, Rarity.UNCOMMON, mage.cards.t.TannukMemorialEnsign.class));
cards.add(new SetCardInfo("Tannuk, Steadfast Second", 162, Rarity.MYTHIC, mage.cards.t.TannukSteadfastSecond.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Tannuk, Steadfast Second", 296, Rarity.MYTHIC, mage.cards.t.TannukSteadfastSecond.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Tapestry Warden", 209, Rarity.UNCOMMON, mage.cards.t.TapestryWarden.class)); cards.add(new SetCardInfo("Tapestry Warden", 209, Rarity.UNCOMMON, mage.cards.t.TapestryWarden.class));
cards.add(new SetCardInfo("Temporal Intervention", 120, Rarity.COMMON, mage.cards.t.TemporalIntervention.class)); cards.add(new SetCardInfo("Temporal Intervention", 120, Rarity.COMMON, mage.cards.t.TemporalIntervention.class));
cards.add(new SetCardInfo("Terminal Velocity", 163, Rarity.RARE, mage.cards.t.TerminalVelocity.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Terminal Velocity", 163, Rarity.RARE, mage.cards.t.TerminalVelocity.class, NON_FULL_USE_VARIOUS));
@ -387,6 +387,7 @@ public final class EdgeOfEternities extends ExpansionSet {
cards.add(new SetCardInfo("The Seriema", 35, Rarity.RARE, mage.cards.t.TheSeriema.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Seriema", 35, Rarity.RARE, mage.cards.t.TheSeriema.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Thrumming Hivepool", 247, Rarity.RARE, mage.cards.t.ThrummingHivepool.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Thrumming Hivepool", 247, Rarity.RARE, mage.cards.t.ThrummingHivepool.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Thrumming Hivepool", 356, Rarity.RARE, mage.cards.t.ThrummingHivepool.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Thrumming Hivepool", 356, Rarity.RARE, mage.cards.t.ThrummingHivepool.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Timeline Culler", 121, Rarity.UNCOMMON, mage.cards.t.TimelineCuller.class));
cards.add(new SetCardInfo("Tractor Beam", 82, Rarity.UNCOMMON, mage.cards.t.TractorBeam.class)); cards.add(new SetCardInfo("Tractor Beam", 82, Rarity.UNCOMMON, mage.cards.t.TractorBeam.class));
cards.add(new SetCardInfo("Tragic Trajectory", 122, Rarity.UNCOMMON, mage.cards.t.TragicTrajectory.class)); cards.add(new SetCardInfo("Tragic Trajectory", 122, Rarity.UNCOMMON, mage.cards.t.TragicTrajectory.class));
cards.add(new SetCardInfo("Umbral Collar Zealot", 123, Rarity.UNCOMMON, mage.cards.u.UmbralCollarZealot.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Umbral Collar Zealot", 123, Rarity.UNCOMMON, mage.cards.u.UmbralCollarZealot.class, NON_FULL_USE_VARIOUS));
@ -421,7 +422,5 @@ public final class EdgeOfEternities extends ExpansionSet {
cards.add(new SetCardInfo("Zero Point Ballad", 128, Rarity.RARE, mage.cards.z.ZeroPointBallad.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Zero Point Ballad", 128, Rarity.RARE, mage.cards.z.ZeroPointBallad.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Zero Point Ballad", 335, Rarity.RARE, mage.cards.z.ZeroPointBallad.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Zero Point Ballad", 335, Rarity.RARE, mage.cards.z.ZeroPointBallad.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Zookeeper Mechan", 170, Rarity.COMMON, mage.cards.z.ZookeeperMechan.class)); cards.add(new SetCardInfo("Zookeeper Mechan", 170, Rarity.COMMON, mage.cards.z.ZookeeperMechan.class));
cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName()));
} }
} }

View file

@ -0,0 +1,190 @@
package org.mage.test.cards.abilities.keywords;
import mage.abilities.keyword.HasteAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author TheElk801
*/
public class WarpTest extends CardTestPlayerBase {
private static final String colossus = "Bygone Colossus";
@Test
public void testRegular() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 9);
addCard(Zone.HAND, playerA, colossus);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, colossus);
waitStackResolved(1, PhaseStep.END_TURN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, colossus, 1);
}
@Test
public void testWarpExile() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
addCard(Zone.HAND, playerA, colossus);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, colossus + " with Warp");
waitStackResolved(1, PhaseStep.END_TURN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertExileCount(playerA, colossus, 1);
}
@Test
public void testWarpExileCast() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 9);
addCard(Zone.HAND, playerA, colossus);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, colossus + " with Warp");
waitStackResolved(1, PhaseStep.END_TURN, playerA);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, colossus);
setStrictChooseMode(true);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, colossus, 1);
}
@Test
public void testWarpExileOptions() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
addCard(Zone.HAND, playerA, colossus);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, colossus + " with Warp");
waitStackResolved(1, PhaseStep.END_TURN, playerA);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, colossus + " with Warp");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.END_TURN);
try {
execute();
} catch (Throwable e) {
Assert.assertEquals(
"Should fail to be able to cast " + colossus + " with warp",
"Can't find ability to activate command: Cast " + colossus + " with Warp",
e.getMessage()
);
}
}
private static final String bolt = "Plasma Bolt";
@Test
public void testVoid() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 1);
addCard(Zone.HAND, playerA, colossus);
addCard(Zone.HAND, playerA, bolt);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, colossus + " with Warp");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, bolt, playerB);
waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertPermanentCount(playerA, colossus, 1);
assertLife(playerB, 20 - 3);
}
@Test
public void testNoVoid() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 9 + 1);
addCard(Zone.HAND, playerA, colossus);
addCard(Zone.HAND, playerA, bolt);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, colossus);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, bolt, playerB);
waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertPermanentCount(playerA, colossus, 1);
assertLife(playerB, 20 - 2);
}
private static final String culler = "Timeline Culler";
@Test
public void testTimelineCuller() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp");
addCard(Zone.GRAVEYARD, playerA, culler);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, culler + " with Warp");
waitStackResolved(1, PhaseStep.END_TURN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertExileCount(playerA, culler, 1);
assertLife(playerA, 20 - 2);
}
private static final String bore = "Full Bore";
@Test
public void testFullBoreWithoutWarp() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 9 + 1);
addCard(Zone.HAND, playerA, colossus);
addCard(Zone.HAND, playerA, bore);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, colossus);
castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, bore, colossus);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertPowerToughness(playerA, colossus, 9 + 3, 9 + 2);
assertAbility(playerA, colossus, TrampleAbility.getInstance(), false);
assertAbility(playerA, colossus, HasteAbility.getInstance(), false);
}
@Test
public void testFullBoreWithWarp() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 1);
addCard(Zone.HAND, playerA, colossus);
addCard(Zone.HAND, playerA, bore);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, colossus + " with Warp");
castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, bore, colossus);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertPowerToughness(playerA, colossus, 9 + 3, 9 + 2);
assertAbility(playerA, colossus, TrampleAbility.getInstance(), true);
assertAbility(playerA, colossus, HasteAbility.getInstance(), true);
}
}

View file

@ -1,10 +1,21 @@
package mage.abilities.keyword; package mage.abilities.keyword;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.SpellAbility; import mage.abilities.SpellAbility;
import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.SpellAbilityType; import mage.constants.*;
import mage.constants.TimingRule; import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.Set;
import java.util.UUID;
/** /**
* @author TheElk801 * @author TheElk801
@ -12,16 +23,60 @@ import mage.constants.TimingRule;
public class WarpAbility extends SpellAbility { public class WarpAbility extends SpellAbility {
public static final String WARP_ACTIVATION_VALUE_KEY = "warpActivation"; public static final String WARP_ACTIVATION_VALUE_KEY = "warpActivation";
private final boolean allowGraveyard;
public WarpAbility(Card card, String manaString) { public WarpAbility(Card card, String manaString) {
super(new ManaCostsImpl<>(manaString), card.getName() + " with Warp"); this(card, manaString, false);
}
public WarpAbility(Card card, String manaString, boolean allowGraveyard) {
super(card.getSpellAbility());
this.newId();
this.setCardName(card.getName() + " with Warp");
this.zone = Zone.HAND;
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE; this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.setAdditionalCostsRuleVisible(false);
this.timing = TimingRule.SORCERY; this.timing = TimingRule.SORCERY;
this.clearManaCosts();
this.clearManaCostsToPay();
this.addCost(new ManaCostsImpl<>(manaString));
this.setAdditionalCostsRuleVisible(false);
this.allowGraveyard = allowGraveyard;
} }
private WarpAbility(final WarpAbility ability) { private WarpAbility(final WarpAbility ability) {
super(ability); super(ability);
this.allowGraveyard = ability.allowGraveyard;
}
// The ability sets up a delayed trigger which can't be set up using the cost tag system
public static void addDelayedTrigger(SpellAbility spellAbility, Game game) {
if (spellAbility instanceof WarpAbility) {
game.addDelayedTriggeredAbility(
new AtTheBeginOfNextEndStepDelayedTriggeredAbility(new WarpExileEffect()), spellAbility
);
}
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
switch (game.getState().getZone(getSourceId())) {
case GRAVEYARD:
if (!allowGraveyard) {
break;
}
case HAND:
return super.canActivate(playerId, game);
}
return ActivationStatus.getFalse();
}
@Override
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (!super.activate(game, allowedIdentifiers, noMana)) {
return false;
}
this.setCostsTag(WARP_ACTIVATION_VALUE_KEY, null);
return true;
} }
@Override @Override
@ -43,9 +98,60 @@ public class WarpAbility extends SpellAbility {
sb.append(getCosts().getText()); sb.append(getCosts().getText());
sb.append('.'); sb.append('.');
} }
sb.append(" <i>(You may cast this card from your hand for its warp cost. ");
sb.append("Exile this creature at the beginning of the next end step, ");
sb.append("then you may cast it from exile on a later turn.)</i>");
return sb.toString(); return sb.toString();
} }
public static String makeWarpString(UUID playerId) {
return playerId + "- Warped";
}
}
class WarpExileEffect extends OneShotEffect {
private static class WarpCondition implements Condition {
private final int turnNumber;
WarpCondition(Game game) {
this.turnNumber = game.getTurnNum();
}
@Override
public boolean apply(Game game, Ability source) {
return game.getTurnNum() > turnNumber;
}
}
WarpExileEffect() {
super(Outcome.Benefit);
staticText = "exile this creature if it was cast for its warp cost";
}
private WarpExileEffect(final WarpExileEffect effect) {
super(effect);
}
@Override
public WarpExileEffect copy() {
return new WarpExileEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent == null || permanent.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter() + 1) {
return false;
}
player.moveCardsToExile(
permanent, source, game, true,
CardUtil.getExileZoneId(WarpAbility.makeWarpString(player.getId()), game),
"Warped by " + player.getLogName()
);
CardUtil.makeCardPlayable(
game, source, permanent.getMainCard(), true,
Duration.Custom, false, player.getId(), new WarpCondition(game)
);
return true;
}
} }

View file

@ -8,6 +8,7 @@ import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.keyword.BestowAbility; import mage.abilities.keyword.BestowAbility;
import mage.abilities.keyword.PrototypeAbility; import mage.abilities.keyword.PrototypeAbility;
import mage.abilities.keyword.TransformAbility; import mage.abilities.keyword.TransformAbility;
import mage.abilities.keyword.WarpAbility;
import mage.cards.*; import mage.cards.*;
import mage.constants.*; import mage.constants.*;
import mage.counters.Counter; import mage.counters.Counter;
@ -421,6 +422,7 @@ public class Spell extends StackObjectImpl implements Card {
} else { } else {
MageObjectReference mor = new MageObjectReference(getSpellAbility()); MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility()); game.storePermanentCostsTags(mor, getSpellAbility());
WarpAbility.addDelayedTrigger(getSpellAbility(), game);
return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null); return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
} }
} }

View file

@ -1,10 +1,12 @@
package mage.watchers.common; package mage.watchers.common;
import mage.abilities.keyword.WarpAbility;
import mage.constants.WatcherScope; import mage.constants.WatcherScope;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.game.stack.Spell;
import mage.watchers.Watcher; import mage.watchers.Watcher;
import java.util.HashSet; import java.util.HashSet;
@ -12,8 +14,6 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
/** /**
* TODO: this doesn't handle warp yet
*
* @author TheElk801 * @author TheElk801
*/ */
public class VoidWatcher extends Watcher { public class VoidWatcher extends Watcher {
@ -29,6 +29,10 @@ public class VoidWatcher extends Watcher {
public void watch(GameEvent event, Game game) { public void watch(GameEvent event, Game game) {
switch (event.getType()) { switch (event.getType()) {
case SPELL_CAST: case SPELL_CAST:
Spell spell = game.getSpell(event.getTargetId());
if (spell != null && spell.getSpellAbility() instanceof WarpAbility) {
players.addAll(game.getState().getPlayersInRange(spell.getControllerId(), game));
}
return; return;
case ZONE_CHANGE: case ZONE_CHANGE:
ZoneChangeEvent zEvent = (ZoneChangeEvent) event; ZoneChangeEvent zEvent = (ZoneChangeEvent) event;