Implemented Idol of Endurance

This commit is contained in:
Evan Kranzler 2020-06-29 20:05:38 -04:00
parent 0960794db4
commit e914c38986
3 changed files with 391 additions and 0 deletions

View file

@ -0,0 +1,265 @@
package mage.cards.i;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.*;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.filter.common.FilterCreatureCard;
import mage.filter.predicate.mageobject.ConvertedManaCostPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.watchers.Watcher;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
public final class IdolOfEndurance extends CardImpl {
public IdolOfEndurance(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{W}");
// When Idol of Endurance enters the battlefield, exile all creature cards with converted mana cost 3 or less from your graveyard until Idol of Endurance leaves the battlefield.
this.addAbility(new EntersBattlefieldTriggeredAbility(new IdolOfEnduranceExileEffect()));
// {1}{W}, {T}: Until end of turn, you may cast a creature spell from among the cards exiled with Idol of Endurance without paying its mana cost.
Ability ability = new SimpleActivatedAbility(
new IdolOfEnduranceCastFromExileEffect(), new ManaCostsImpl<>("{1}{W}")
);
ability.addCost(new TapSourceCost());
this.addAbility(ability, new IdolOfEnduranceWatcher());
}
private IdolOfEndurance(final IdolOfEndurance card) {
super(card);
}
@Override
public IdolOfEndurance copy() {
return new IdolOfEndurance(this);
}
}
class IdolOfEnduranceExileEffect extends OneShotEffect {
private static final FilterCard filter = new FilterCreatureCard();
static {
filter.add(new ConvertedManaCostPredicate(ComparisonType.FEWER_THAN, 4));
}
IdolOfEnduranceExileEffect() {
super(Outcome.Benefit);
staticText = "exile all creature cards with converted mana cost 3 or less from your graveyard until {this} leaves the battlefield";
}
private IdolOfEnduranceExileEffect(final IdolOfEnduranceExileEffect effect) {
super(effect);
}
@Override
public IdolOfEnduranceExileEffect copy() {
return new IdolOfEnduranceExileEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (player == null || permanent == null) {
return false;
}
Cards cards = new CardsImpl(player.getGraveyard().getCards(filter, game));
MageObjectReference mor = new MageObjectReference(permanent, game);
player.moveCards(cards, Zone.EXILED, source, game);
Set<MageObjectReference> morSet = cards
.getCards(game)
.stream()
.filter(Objects::nonNull)
.map(card -> new MageObjectReference(card, game))
.collect(Collectors.toSet());
game.getState().setValue("" + mor.getSourceId() + mor.getZoneChangeCounter(), morSet);
game.addDelayedTriggeredAbility(new IdolOfEnduranceDelayedTrigger(morSet), source);
return true;
}
}
class IdolOfEnduranceDelayedTrigger extends DelayedTriggeredAbility {
IdolOfEnduranceDelayedTrigger(Set<MageObjectReference> morSet) {
super(new IdolOfEnduranceLeaveEffect(morSet), Duration.Custom, true, false);
this.usesStack = false;
this.setRuleVisible(false);
}
private IdolOfEnduranceDelayedTrigger(final IdolOfEnduranceDelayedTrigger ability) {
super(ability);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getTargetId().equals(this.getSourceId())) {
ZoneChangeEvent zEvent = (ZoneChangeEvent) event;
if (zEvent.getFromZone() == Zone.BATTLEFIELD) {
return true;
}
}
return false;
}
@Override
public IdolOfEnduranceDelayedTrigger copy() {
return new IdolOfEnduranceDelayedTrigger(this);
}
}
class IdolOfEnduranceLeaveEffect extends OneShotEffect {
private final Set<MageObjectReference> morSet = new HashSet<>();
IdolOfEnduranceLeaveEffect(Set<MageObjectReference> morSet) {
super(Outcome.Benefit);
this.morSet.addAll(morSet);
}
private IdolOfEnduranceLeaveEffect(final IdolOfEnduranceLeaveEffect effect) {
super(effect);
}
@Override
public IdolOfEnduranceLeaveEffect copy() {
return new IdolOfEnduranceLeaveEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
return player != null && player.moveCards(
morSet.stream()
.map(mor -> mor.getCard(game))
.filter(Objects::nonNull)
.collect(Collectors.toSet()),
Zone.GRAVEYARD, source, game
);
}
}
class IdolOfEnduranceCastFromExileEffect extends AsThoughEffectImpl {
IdolOfEnduranceCastFromExileEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit);
staticText = "until end of turn, you may cast a creature spell from among the cards exiled with {this} without paying its mana cost";
}
private IdolOfEnduranceCastFromExileEffect(final IdolOfEnduranceCastFromExileEffect effect) {
super(effect);
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public IdolOfEnduranceCastFromExileEffect copy() {
return new IdolOfEnduranceCastFromExileEffect(this);
}
@Override
public void init(Ability source, Game game) {
super.init(source, game);
IdolOfEnduranceWatcher watcher = game.getState().getWatcher(IdolOfEnduranceWatcher.class);
if (watcher != null) {
watcher.addPlayable(source, game);
}
}
@Override
public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) {
IdolOfEnduranceWatcher watcher = game.getState().getWatcher(IdolOfEnduranceWatcher.class);
if (watcher == null || !watcher.checkPermission(affectedControllerId, source, game)) {
return false;
}
Object value = game.getState().getValue("" + source.getSourceId() + source.getSourceObjectZoneChangeCounter());
if (!(value instanceof Set)) {
discard();
return false;
}
Set<MageObjectReference> morSet = (Set<MageObjectReference>) value;
if (game.getState().getZone(sourceId) != Zone.EXILED
|| morSet.stream().noneMatch(mor -> mor.refersTo(sourceId, game))) {
return false;
}
Card card = game.getCard(sourceId);
if (card == null || !card.isCreature() || card.isLand()) {
return false;
}
return allowCardToPlayWithoutMana(sourceId, source, affectedControllerId, game);
}
}
class IdolOfEnduranceWatcher extends Watcher {
private final Map<MageObjectReference, Map<UUID, Integer>> morMap = new HashMap<>();
IdolOfEnduranceWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.SPELL_CAST) {
if (event.getAdditionalReference() == null) {
return;
}
morMap.computeIfAbsent(event.getAdditionalReference(), m -> new HashMap<>())
.compute(event.getPlayerId(), (u, i) -> i == null ? 0 : Integer.sum(i, -1));
return;
}
}
@Override
public void reset() {
morMap.clear();
super.reset();
}
boolean checkPermission(UUID playerId, Ability source, Game game) {
if (!playerId.equals(source.getControllerId())) {
return false;
}
MageObjectReference mor = new MageObjectReference(
source.getSourceId(), source.getSourceObjectZoneChangeCounter(), game
);
if (!morMap.containsKey(mor)) {
return false;
}
return morMap.get(mor).getOrDefault(playerId, 0) > 0;
}
void addPlayable(Ability source, Game game) {
MageObjectReference mor = new MageObjectReference(
source.getSourceId(), source.getSourceObjectZoneChangeCounter(), game
);
morMap.computeIfAbsent(mor, m -> new HashMap<>())
.compute(source.getControllerId(), (u, i) -> i == null ? 1 : Integer.sum(i, 1));
}
}

View file

@ -142,6 +142,7 @@ public final class CoreSet2021 extends ExpansionSet {
cards.add(new SetCardInfo("Hobblefiend", 152, Rarity.COMMON, mage.cards.h.Hobblefiend.class));
cards.add(new SetCardInfo("Hooded Blightfang", 104, Rarity.RARE, mage.cards.h.HoodedBlightfang.class));
cards.add(new SetCardInfo("Hunter's Edge", 189, Rarity.COMMON, mage.cards.h.HuntersEdge.class));
cards.add(new SetCardInfo("Idol of Endurance", 23, Rarity.RARE, mage.cards.i.IdolOfEndurance.class));
cards.add(new SetCardInfo("Igneous Cur", 153, Rarity.COMMON, mage.cards.i.IgneousCur.class));
cards.add(new SetCardInfo("Indulging Patrician", 219, Rarity.UNCOMMON, mage.cards.i.IndulgingPatrician.class));
cards.add(new SetCardInfo("Infernal Scarring", 105, Rarity.COMMON, mage.cards.i.InfernalScarring.class));

View file

@ -0,0 +1,125 @@
package org.mage.test.cards.single;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author TheElk801
*/
public class IdolOfEnduranceTest extends CardTestPlayerBase {
private static final String idol = "Idol of Endurance";
private static final String key = "Voltaic Key";
private static final String sqr = "Squire";
private static final String glrskr = "Glory Seeker";
@Test
public void testIdol() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
addCard(Zone.HAND, playerA, idol);
addCard(Zone.GRAVEYARD, playerA, sqr);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, idol);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sqr);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, sqr, 1);
}
@Test
public void testIdol2() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
addCard(Zone.HAND, playerA, idol);
addCard(Zone.GRAVEYARD, playerA, sqr);
addCard(Zone.GRAVEYARD, playerA, glrskr);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, idol);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sqr);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, glrskr);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, sqr, 1);
assertPermanentCount(playerA, glrskr, 0);
}
@Test
public void testIdolTwice() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 8);
addCard(Zone.BATTLEFIELD, playerA, key);
addCard(Zone.HAND, playerA, idol);
addCard(Zone.GRAVEYARD, playerA, sqr);
addCard(Zone.GRAVEYARD, playerA, glrskr);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, idol);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1},", idol);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sqr);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, glrskr);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, sqr, 1);
assertPermanentCount(playerA, glrskr, 1);
}
@Test
public void testIdolTwice2() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 8);
addCard(Zone.BATTLEFIELD, playerA, key);
addCard(Zone.HAND, playerA, idol);
addCard(Zone.GRAVEYARD, playerA, sqr);
addCard(Zone.GRAVEYARD, playerA, glrskr);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, idol);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sqr);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1},", idol);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{W}");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, glrskr);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, sqr, 1);
assertPermanentCount(playerA, glrskr, 1);
}
}