[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

@ -1,10 +1,21 @@
package mage.abilities.keyword;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.SpellAbilityType;
import mage.constants.TimingRule;
import mage.constants.*;
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
@ -12,16 +23,60 @@ import mage.constants.TimingRule;
public class WarpAbility extends SpellAbility {
public static final String WARP_ACTIVATION_VALUE_KEY = "warpActivation";
private final boolean allowGraveyard;
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.setAdditionalCostsRuleVisible(false);
this.timing = TimingRule.SORCERY;
this.clearManaCosts();
this.clearManaCostsToPay();
this.addCost(new ManaCostsImpl<>(manaString));
this.setAdditionalCostsRuleVisible(false);
this.allowGraveyard = allowGraveyard;
}
private WarpAbility(final WarpAbility 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
@ -43,9 +98,60 @@ public class WarpAbility extends SpellAbility {
sb.append(getCosts().getText());
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();
}
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.PrototypeAbility;
import mage.abilities.keyword.TransformAbility;
import mage.abilities.keyword.WarpAbility;
import mage.cards.*;
import mage.constants.*;
import mage.counters.Counter;
@ -421,6 +422,7 @@ public class Spell extends StackObjectImpl implements Card {
} else {
MageObjectReference mor = new MageObjectReference(getSpellAbility());
game.storePermanentCostsTags(mor, getSpellAbility());
WarpAbility.addDelayedTrigger(getSpellAbility(), game);
return controller.moveCards(card, Zone.BATTLEFIELD, ability, game, false, faceDown, false, null);
}
}

View file

@ -1,10 +1,12 @@
package mage.watchers.common;
import mage.abilities.keyword.WarpAbility;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.stack.Spell;
import mage.watchers.Watcher;
import java.util.HashSet;
@ -12,8 +14,6 @@ import java.util.Set;
import java.util.UUID;
/**
* TODO: this doesn't handle warp yet
*
* @author TheElk801
*/
public class VoidWatcher extends Watcher {
@ -29,6 +29,10 @@ public class VoidWatcher extends Watcher {
public void watch(GameEvent event, Game game) {
switch (event.getType()) {
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;
case ZONE_CHANGE:
ZoneChangeEvent zEvent = (ZoneChangeEvent) event;