fixed issue with Kruphix, God of Horizons and Horizon Stone causing endless replacement effect loop

This commit is contained in:
Evan Kranzler 2021-02-23 08:41:54 -05:00
parent 51c9121f5e
commit 2ffa719278
7 changed files with 137 additions and 75 deletions

View file

@ -2,14 +2,12 @@ package mage.cards.h;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.*;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.players.Player;
import java.util.UUID; import java.util.UUID;
@ -35,11 +33,11 @@ public final class HorizonStone extends CardImpl {
} }
} }
class HorizonStoneEffect extends ReplacementEffectImpl { class HorizonStoneEffect extends ContinuousEffectImpl {
HorizonStoneEffect() { HorizonStoneEffect() {
super(Duration.WhileOnBattlefield, Outcome.Benefit); super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit);
staticText = "If you would lose unspent mana, that mana becomes colorless instead."; staticText = "if you would lose unspent mana, that mana becomes colorless instead";
} }
private HorizonStoneEffect(final HorizonStoneEffect effect) { private HorizonStoneEffect(final HorizonStoneEffect effect) {
@ -53,21 +51,10 @@ class HorizonStoneEffect extends ReplacementEffectImpl {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
return false; Player player = game.getPlayer(source.getControllerId());
if (player != null) {
player.getManaPool().setManaBecomesColorless(true);
} }
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
return true; return true;
} }
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.EMPTY_MANA_POOL;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
return event.getPlayerId().equals(source.getControllerId());
}
} }

View file

@ -4,7 +4,7 @@ import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.common.DevotionCount; import mage.abilities.dynamicvalue.common.DevotionCount;
import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.common.continuous.LoseCreatureTypeSourceEffect; import mage.abilities.effects.common.continuous.LoseCreatureTypeSourceEffect;
import mage.abilities.effects.common.continuous.MaximumHandSizeControllerEffect; import mage.abilities.effects.common.continuous.MaximumHandSizeControllerEffect;
import mage.abilities.keyword.IndestructibleAbility; import mage.abilities.keyword.IndestructibleAbility;
@ -12,7 +12,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.*; import mage.constants.*;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.players.Player;
import java.util.UUID; import java.util.UUID;
@ -41,9 +41,9 @@ public final class KruphixGodOfHorizons extends CardImpl {
Integer.MAX_VALUE, Duration.WhileOnBattlefield, Integer.MAX_VALUE, Duration.WhileOnBattlefield,
MaximumHandSizeControllerEffect.HandSizeModification.SET MaximumHandSizeControllerEffect.HandSizeModification.SET
))); )));
// If unused mana would empty from your mana pool, that mana becomes colorless instead. // If unused mana would empty from your mana pool, that mana becomes colorless instead.
this.addAbility(new SimpleStaticAbility(new KruphixGodOfHorizonsEffect())); this.addAbility(new SimpleStaticAbility(new KruphixGodOfHorizonsEffect()));
} }
private KruphixGodOfHorizons(final KruphixGodOfHorizons card) { private KruphixGodOfHorizons(final KruphixGodOfHorizons card) {
@ -56,11 +56,11 @@ public final class KruphixGodOfHorizons extends CardImpl {
} }
} }
class KruphixGodOfHorizonsEffect extends ReplacementEffectImpl { class KruphixGodOfHorizonsEffect extends ContinuousEffectImpl {
KruphixGodOfHorizonsEffect() { KruphixGodOfHorizonsEffect() {
super(Duration.WhileOnBattlefield, Outcome.Benefit); super(Duration.WhileOnBattlefield, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit);
staticText = "If you would lose unspent mana, that mana becomes colorless instead."; staticText = "if you would lose unspent mana, that mana becomes colorless instead";
} }
private KruphixGodOfHorizonsEffect(final KruphixGodOfHorizonsEffect effect) { private KruphixGodOfHorizonsEffect(final KruphixGodOfHorizonsEffect effect) {
@ -74,21 +74,10 @@ class KruphixGodOfHorizonsEffect extends ReplacementEffectImpl {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
return false; Player player = game.getPlayer(source.getControllerId());
if (player != null) {
player.getManaPool().setManaBecomesColorless(true);
} }
@Override
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
return true; return true;
} }
@Override
public boolean checksEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.EMPTY_MANA_POOL;
}
@Override
public boolean applies(GameEvent event, Ability source, Game game) {
return event.getPlayerId().equals(source.getControllerId());
}
} }

View file

@ -0,0 +1,62 @@
package org.mage.test.cards.single.jou;
import mage.constants.ManaType;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author TheElk801
*/
public class KruphixGodOfHorizonsTest extends CardTestPlayerBase {
private static final String kruphix = "Kruphix, God of Horizons";
private static final String sliver = "Metallic Sliver";
private static final String repeal = "Mystic Repeal";
@Test
public void testKruphixNormal() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.BATTLEFIELD, playerA, kruphix);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}");
setStrictChooseMode(true);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertManaPool(playerA, ManaType.COLORLESS, 3);
assertManaPool(playerA, ManaType.GREEN, 0);
assertPermanentCount(playerA, kruphix, 1);
}
@Test
public void testKruphixRemoved() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
addCard(Zone.BATTLEFIELD, playerA, kruphix);
addCard(Zone.HAND, playerA, sliver);
addCard(Zone.HAND, playerA, repeal);
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}");
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, sliver);
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, repeal, kruphix);
setStrictChooseMode(true);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertManaPool(playerA, ManaType.COLORLESS, 0);
assertManaPool(playerA, ManaType.GREEN, 0);
assertPermanentCount(playerA, sliver, 1);
assertPermanentCount(playerA, kruphix, 0);
assertTappedCount("Forest", true, 1);
}
}

View file

@ -17,7 +17,7 @@ import java.util.UUID;
/** /**
* @author nantuko * @author nantuko
*/ */
public class ConditionalMana extends Mana implements Serializable { public class ConditionalMana extends Mana implements Serializable, Emptiable {
/** /**
* Conditions that should be met (all or any depending on comparison scope) * Conditions that should be met (all or any depending on comparison scope)

View file

@ -0,0 +1,15 @@
package mage;
import mage.constants.ManaType;
/**
* @author TheElk801
*/
public interface Emptiable {
public void add(ManaType manaType, int amount);
public void clear(ManaType manaType);
public int get(final ManaType manaType);
}

View file

@ -1,6 +1,7 @@
package mage.players; package mage.players;
import mage.ConditionalMana; import mage.ConditionalMana;
import mage.Emptiable;
import mage.MageObject; import mage.MageObject;
import mage.Mana; import mage.Mana;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -37,6 +38,7 @@ public class ManaPool implements Serializable {
private final List<ManaPoolItem> poolBookmark = new ArrayList<>(); // mana pool bookmark for rollback purposes private final List<ManaPoolItem> poolBookmark = new ArrayList<>(); // mana pool bookmark for rollback purposes
private final Set<ManaType> doNotEmptyManaTypes = new HashSet<>(); private final Set<ManaType> doNotEmptyManaTypes = new HashSet<>();
private boolean manaBecomesColorless = false;
private static final class ConditionalManaInfo { private static final class ConditionalManaInfo {
private final ManaType manaType; private final ManaType manaType;
@ -73,6 +75,7 @@ public class ManaPool implements Serializable {
} }
this.doNotEmptyManaTypes.addAll(pool.doNotEmptyManaTypes); this.doNotEmptyManaTypes.addAll(pool.doNotEmptyManaTypes);
this.lastPaymentWasSnow = pool.lastPaymentWasSnow; this.lastPaymentWasSnow = pool.lastPaymentWasSnow;
this.manaBecomesColorless = pool.manaBecomesColorless;
} }
public int getRed() { public int getRed() {
@ -229,12 +232,21 @@ public class ManaPool implements Serializable {
public void clearEmptyManaPoolRules() { public void clearEmptyManaPoolRules() {
doNotEmptyManaTypes.clear(); doNotEmptyManaTypes.clear();
this.manaBecomesColorless = false;
} }
public void addDoNotEmptyManaType(ManaType manaType) { public void addDoNotEmptyManaType(ManaType manaType) {
doNotEmptyManaTypes.add(manaType); doNotEmptyManaTypes.add(manaType);
} }
public void setManaBecomesColorless(boolean manaBecomesColorless) {
this.manaBecomesColorless = manaBecomesColorless;
}
public boolean isManaBecomesColorless() {
return manaBecomesColorless;
}
public void init() { public void init() {
manaItems.clear(); manaItems.clear();
} }
@ -246,35 +258,15 @@ public class ManaPool implements Serializable {
ManaPoolItem item = it.next(); ManaPoolItem item = it.next();
ConditionalMana conditionalItem = item.getConditionalMana(); ConditionalMana conditionalItem = item.getConditionalMana();
for (ManaType manaType : ManaType.values()) { for (ManaType manaType : ManaType.values()) {
if (!doNotEmptyManaTypes.contains(manaType)) { if (doNotEmptyManaTypes.contains(manaType)) {
continue;
}
if (item.get(manaType) > 0) { if (item.get(manaType) > 0) {
if (item.getDuration() != Duration.EndOfTurn total += emptyItem(item, item, game, manaType);
|| game.getPhase().getType() == TurnPhase.END) {
if (game.replaceEvent(new GameEvent(GameEvent.EventType.EMPTY_MANA_POOL, playerId, null, playerId))) {
int amount = item.get(manaType);
item.clear(manaType);
item.add(ManaType.COLORLESS, amount);
} else {
total += item.get(manaType);
item.clear(manaType);
}
}
}
if (conditionalItem != null) {
if (conditionalItem.get(manaType) > 0) {
if (item.getDuration() != Duration.EndOfTurn
|| game.getPhase().getType() == TurnPhase.END) {
if (game.replaceEvent(new GameEvent(GameEvent.EventType.EMPTY_MANA_POOL, playerId, null, playerId))) {
int amount = conditionalItem.get(manaType);
conditionalItem.clear(manaType);
conditionalItem.add(ManaType.COLORLESS, amount);
} else {
total += conditionalItem.get(manaType);
conditionalItem.clear(manaType);
}
}
}
} }
if (conditionalItem != null
&& conditionalItem.get(manaType) > 0) {
total += emptyItem(item, conditionalItem, game, manaType);
} }
} }
if (item.count() == 0) { if (item.count() == 0) {
@ -284,6 +276,22 @@ public class ManaPool implements Serializable {
return total; return total;
} }
private int emptyItem(ManaPoolItem item, Emptiable toEmpty, Game game, ManaType manaType) {
if (item.getDuration() == Duration.EndOfTurn
&& game.getPhase().getType() != TurnPhase.END) {
return 0;
}
if (!manaBecomesColorless) {
int amount = toEmpty.get(manaType);
toEmpty.clear(manaType);
return amount;
}
int amount = toEmpty.get(manaType);
toEmpty.clear(manaType);
toEmpty.add(ManaType.COLORLESS, amount);
return 0;
}
public Mana getMana() { public Mana getMana() {
Mana m = new Mana(); Mana m = new Mana();
for (ManaPoolItem item : manaItems) { for (ManaPoolItem item : manaItems) {

View file

@ -5,6 +5,7 @@ import java.util.UUID;
import mage.ConditionalMana; import mage.ConditionalMana;
import mage.MageObject; import mage.MageObject;
import mage.Mana; import mage.Mana;
import mage.Emptiable;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.ManaType; import mage.constants.ManaType;
@ -12,7 +13,7 @@ import mage.constants.ManaType;
* *
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class ManaPoolItem implements Serializable { public class ManaPoolItem implements Serializable, Emptiable {
private int red = 0; private int red = 0;
private int green = 0; private int green = 0;