[MH2] Implemented Carth the Lion (#7848)

* [MH2] Implemented Carth the Lion

* [MH2] Carth the Lion - Fixed loyalty cost modification

* Fix copy constructor and add getters/setters

* Call sourceObject.adjustCosts before checking cost modifications

* Add unit test

* Added additional comments, checks and tests;

Co-authored-by: Oleg Agafonov <jaydi85@gmail.com>
This commit is contained in:
Daniel Bomar 2021-07-14 15:12:25 -05:00 committed by GitHub
parent 3ea08e1e7b
commit 29d3f96340
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 353 additions and 24 deletions

View file

@ -0,0 +1,130 @@
package mage.cards.c;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.LoyaltyAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.LookLibraryAndPickControllerEffect;
import mage.abilities.effects.common.cost.CostModificationEffectImpl;
import mage.constants.*;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.filter.common.FilterPlaneswalkerCard;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
/**
*
* @author weirddan455
*/
public final class CarthTheLion extends CardImpl {
public CarthTheLion(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{G}");
this.addSuperType(SuperType.LEGENDARY);
this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.WARRIOR);
this.power = new MageInt(3);
this.toughness = new MageInt(5);
// Whenever Carth the Lion enters the battlefield, or a planeswalker you control dies, look at the top seven cards of your library.
// You may reveal a planeswalker card from among them and put it into your hand. Put the rest on the bottom of your library in a random order.
this.addAbility(new CarthTheLionTriggeredAbility());
// Planeswalkers' loyalty abilities you activate cost an additonal {+1} to activate.
this.addAbility(new SimpleStaticAbility(new CarthTheLionLoyaltyCostEffect()));
}
private CarthTheLion(final CarthTheLion card) {
super(card);
}
@Override
public CarthTheLion copy() {
return new CarthTheLion(this);
}
}
class CarthTheLionTriggeredAbility extends TriggeredAbilityImpl {
private static final FilterPlaneswalkerCard filter = new FilterPlaneswalkerCard("a planeswalker card");
public CarthTheLionTriggeredAbility() {
super(Zone.BATTLEFIELD, new LookLibraryAndPickControllerEffect(
7, 1, filter, true, false, Zone.HAND, true)
.setBackInRandomOrder(true)
);
}
private CarthTheLionTriggeredAbility(final CarthTheLionTriggeredAbility ability) {
super(ability);
}
@Override
public CarthTheLionTriggeredAbility copy() {
return new CarthTheLionTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD
|| event.getType() == GameEvent.EventType.ZONE_CHANGE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD
&& event.getTargetId().equals(getSourceId())) {
return true;
}
if (event.getType() == GameEvent.EventType.ZONE_CHANGE && event.getPlayerId().equals(getControllerId())) {
ZoneChangeEvent zEvent = (ZoneChangeEvent) event;
if (zEvent.isDiesEvent()) {
Permanent permanent = game.getPermanentOrLKIBattlefield(zEvent.getTargetId());
return permanent != null && permanent.isPlaneswalker();
}
}
return false;
}
@Override
public String getRule() {
return "Whenever {this} enters the battlefield, or a planeswalker you control dies, " + super.getRule();
}
}
class CarthTheLionLoyaltyCostEffect extends CostModificationEffectImpl {
public CarthTheLionLoyaltyCostEffect() {
super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.INCREASE_COST);
this.staticText = "Planeswalkers' loyalty abilities you activate cost an additonal +1 to activate";
}
private CarthTheLionLoyaltyCostEffect(final CarthTheLionLoyaltyCostEffect effect) {
super(effect);
}
@Override
public CarthTheLionLoyaltyCostEffect copy() {
return new CarthTheLionLoyaltyCostEffect(this);
}
@Override
public boolean apply(Game game, Ability source, Ability abilityToModify) {
if (abilityToModify instanceof LoyaltyAbility) {
((LoyaltyAbility) abilityToModify).increaseLoyaltyCost(1);
return true;
}
return false;
}
@Override
public boolean applies(Ability abilityToModify, Ability source, Game game) {
return abilityToModify instanceof LoyaltyAbility && abilityToModify.getControllerId().equals(source.getControllerId());
}
}

View file

@ -72,6 +72,7 @@ public final class ModernHorizons2 extends ExpansionSet {
cards.add(new SetCardInfo("Burdened Aerialist", 38, Rarity.COMMON, mage.cards.b.BurdenedAerialist.class)); cards.add(new SetCardInfo("Burdened Aerialist", 38, Rarity.COMMON, mage.cards.b.BurdenedAerialist.class));
cards.add(new SetCardInfo("Cabal Coffers", 301, Rarity.MYTHIC, mage.cards.c.CabalCoffers.class)); cards.add(new SetCardInfo("Cabal Coffers", 301, Rarity.MYTHIC, mage.cards.c.CabalCoffers.class));
cards.add(new SetCardInfo("Cabal Initiate", 78, Rarity.COMMON, mage.cards.c.CabalInitiate.class)); cards.add(new SetCardInfo("Cabal Initiate", 78, Rarity.COMMON, mage.cards.c.CabalInitiate.class));
cards.add(new SetCardInfo("Carth the Lion", 189, Rarity.RARE, mage.cards.c.CarthTheLion.class));
cards.add(new SetCardInfo("Calibrated Blast", 118, Rarity.RARE, mage.cards.c.CalibratedBlast.class)); cards.add(new SetCardInfo("Calibrated Blast", 118, Rarity.RARE, mage.cards.c.CalibratedBlast.class));
cards.add(new SetCardInfo("Caprichrome", 9, Rarity.UNCOMMON, mage.cards.c.Caprichrome.class)); cards.add(new SetCardInfo("Caprichrome", 9, Rarity.UNCOMMON, mage.cards.c.Caprichrome.class));
cards.add(new SetCardInfo("Captain Ripley Vance", 119, Rarity.UNCOMMON, mage.cards.c.CaptainRipleyVance.class)); cards.add(new SetCardInfo("Captain Ripley Vance", 119, Rarity.UNCOMMON, mage.cards.c.CaptainRipleyVance.class));

View file

@ -154,7 +154,7 @@ public class SpliceOnArcaneTest extends CardTestPlayerBase {
@Test @Test
@Ignore @Ignore
public void testCounteredBecauseOfNoLegalTarget() { public void testCounteredBecauseOfNoLegalTarget() {
// TODO: rewrite test, it's wrong and misleading-- user report about Griselbrand was destroyed by Terminate after splice anounce, but tests don't use it at all (Griselbrand legal target all the time) // TODO: rewrite test, it's wrong and misleading-- user report about Griselbrand was destroyed by Terminate after splice announce, but tests don't use it at all (Griselbrand legal target all the time)
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 8); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 8);

View file

@ -1,8 +1,12 @@
package org.mage.test.cards.cost.modification; package org.mage.test.cards.cost.modification;
import mage.abilities.LoyaltyAbility;
import mage.abilities.costs.common.PayVariableLoyaltyCost;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.game.permanent.Permanent;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
@ -382,4 +386,100 @@ public class CostModificationTest extends CardTestPlayerBase {
execute(); execute();
assertAllCommandsUsed(); assertAllCommandsUsed();
} }
@Test
public void test_PlaneswalkerLoyalty_CostModification_Single() {
// Carth the Lion
// Planeswalkers' loyalty abilities you activate cost an additional {+1} to activate.
addCard(Zone.BATTLEFIELD, playerA, "Carth the Lion", 1);
//
// Vivien Reid
// 5 Loyalty
// +1, -3, -8 Abilities
addCard(Zone.BATTLEFIELD, playerA, "Vivien Reid", 1);
//
// Huatli, Warrior Poet
// 3 Loyalty
// Testing X Ability
// X: Huatli, Warrior Poet deals X damage divided as you choose among any number of target creatures. Creatures dealt damage this way cant block this turn.
addCard(Zone.BATTLEFIELD, playerA, "Huatli, Warrior Poet", 1);
//
// 2 toughness creatures for Huatli to kill
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1);
addCard(Zone.BATTLEFIELD, playerB, "Ghitu Lavarunner", 1);
// Vivien: make cost +2 instead +1 (total 7 counters)
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Look at the top four");
setChoice(playerA, "No");
checkPermanentCounters("Vivien Reid counter check", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Vivien Reid", CounterType.LOYALTY, 7);
// loyalty cost modification doesn't affect card rule's text, so it still shown an old cost value for a user
// Vivien: make cost -7 instead -8
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "-8: You get an emblem");
// Huatli: check x cost changes
runCode("check x cost", 3, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> {
Permanent huatli = game.getBattlefield().getAllActivePermanents().stream().filter(p -> p.getName().equals("Huatli, Warrior Poet")).findFirst().orElse(null);
Assert.assertNotNull("must have huatli on battlefield", huatli);
LoyaltyAbility ability = (LoyaltyAbility) huatli.getAbilities(game).stream().filter(a -> a.getRule().startsWith("-X: ")).findFirst().orElse(null);
Assert.assertNotNull("must have loyalty ability", ability);
// counters: 3
// cost modification: +1
// max possible X to pay: 3 + 1 = 4
PayVariableLoyaltyCost cost = (PayVariableLoyaltyCost) ability.getCosts().get(0);
Assert.assertEquals("must have max possible X as 4", 4, cost.getMaxValue(ability, game));
});
// Huatli: make x cost -3 instead -4
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "-X: {this} deals X damage divided as you choose");
setChoice(playerA, "X=4");
addTargetAmount(playerA, "Grizzly Bears", 2);
addTargetAmount(playerA, "Ghitu Lavarunner", 2);
setStrictChooseMode(true);
setStopAt(3, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerA, "Vivien Reid", 1);
assertGraveyardCount(playerA, "Huatli, Warrior Poet", 1);
assertGraveyardCount(playerB, "Grizzly Bears", 1);
assertGraveyardCount(playerB, "Ghitu Lavarunner", 1);
}
@Test
public void test_PlaneswalkerLoyalty_CostModification_Multiple() {
// Carth the Lion
// Planeswalkers' loyalty abilities you activate cost an additional {+1} to activate.
addCard(Zone.BATTLEFIELD, playerA, "Carth the Lion", 1);
//
// Vivien Reid
// 5 Loyalty
// +1, -3, -8 Abilities
addCard(Zone.BATTLEFIELD, playerA, "Vivien Reid", 1);
//
// You may have Spark Double enter the battlefield as a copy of a creature or planeswalker you control...
addCard(Zone.HAND, playerA, "Spark Double", 2); // {3}{U}
addCard(Zone.BATTLEFIELD, playerA, "Island", 2 * 4);
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "-8:", false);
// prepare duplicates
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spark Double");
setChoice(playerA, "Yes"); // copy
setChoice(playerA, "Carth the Lion"); // copy target
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Spark Double");
setChoice(playerA, "Yes"); // copy
setChoice(playerA, "Carth the Lion"); // copy target
// x3 lions gives +3 in cost reduction (-8 -> -5)
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
checkPlayableAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "-8:", true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
}
} }

View file

@ -1,6 +1,6 @@
package mage.abilities; package mage.abilities;
import mage.abilities.costs.Cost;
import mage.abilities.costs.common.PayLoyaltyCost; import mage.abilities.costs.common.PayLoyaltyCost;
import mage.abilities.costs.common.PayVariableLoyaltyCost; import mage.abilities.costs.common.PayVariableLoyaltyCost;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
@ -9,7 +9,6 @@ import mage.constants.TimingRule;
import mage.constants.Zone; import mage.constants.Zone;
/** /**
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class LoyaltyAbility extends ActivatedAbilityImpl { public class LoyaltyAbility extends ActivatedAbilityImpl {
@ -43,4 +42,49 @@ public class LoyaltyAbility extends ActivatedAbilityImpl {
return new LoyaltyAbility(this); return new LoyaltyAbility(this);
} }
/**
* Change loyalty cost by amount value
*
* @param amount
*/
public void increaseLoyaltyCost(int amount) {
// loyalty cost modification rules from Carth the Lion
// If a planeswalkers loyalty ability normally has a cost of [+1], Carths ability makes it cost [+2] instead.
// A cost of [0] would become [+1], and a cost of [-6] would become [-5].
// (2021-06-18)
//
// If you somehow manage to control two Carths (perhaps because of Spark Double), the cost-changing effect is
// cumulative. In total, loyalty abilities will cost an additional [+2] to activate.
// (2021-06-18)
//
// The total cost of a planeswalkers loyalty ability is calculated before any counters are added or removed.
// If a loyalty ability normally costs [-3] to activate, you do not remove three counters from it and then
// put one counter on it. You remove two counters at one time when you pay the cost.
// (2021-06-18)
//
// If an effect replaces the number of counters that would be placed on a planeswalker, such as that of
// Vorinclex, Monstrous Raider, that replacement happens only once, at the time payment is made.
// (2021-06-18)
// cost modification support only 1 cost item
int staticCount = 0;
for (Cost cost : costs) {
if (cost instanceof PayLoyaltyCost) {
// static cost
PayLoyaltyCost staticCost = (PayLoyaltyCost) cost;
staticCost.setAmount(staticCost.getAmount() + amount);
staticCount++;
} else if (cost instanceof PayVariableLoyaltyCost) {
// x cost (after x announce: x cost + static cost)
PayVariableLoyaltyCost xCost = (PayVariableLoyaltyCost) cost;
xCost.setCostModification(xCost.getCostModification() + amount);
}
}
if (staticCount > 1) {
throw new IllegalArgumentException(String.format("Loyalty ability must have only 1 static cost, but has %d: %s",
staticCount, this.getRule()));
}
}
} }

View file

@ -1,7 +1,7 @@
package mage.abilities.costs.common; package mage.abilities.costs.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.LoyaltyAbility;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl; import mage.abilities.costs.CostImpl;
import mage.counters.CounterType; import mage.counters.CounterType;
@ -11,19 +11,14 @@ import mage.game.permanent.Permanent;
import java.util.UUID; import java.util.UUID;
/** /**
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class PayLoyaltyCost extends CostImpl { public class PayLoyaltyCost extends CostImpl {
private final int amount; private int amount;
public PayLoyaltyCost(int amount) { public PayLoyaltyCost(int amount) {
this.amount = amount; setAmount(amount);
this.text = Integer.toString(amount);
if (amount > 0) {
this.text = '+' + this.text;
}
} }
public PayLoyaltyCost(PayLoyaltyCost cost) { public PayLoyaltyCost(PayLoyaltyCost cost) {
@ -34,7 +29,26 @@ public class PayLoyaltyCost extends CostImpl {
@Override @Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
Permanent planeswalker = game.getPermanent(source.getSourceId()); Permanent planeswalker = game.getPermanent(source.getSourceId());
return planeswalker != null && planeswalker.getCounters(game).getCount(CounterType.LOYALTY) + amount >= 0 && planeswalker.canLoyaltyBeUsed(game); if (planeswalker == null) {
return false;
}
int loyaltyCost = amount;
// apply cost modification
if (ability instanceof LoyaltyAbility) {
LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy();
planeswalker.adjustCosts(copiedAbility, game);
game.getContinuousEffects().costModification(copiedAbility, game);
loyaltyCost = 0;
for (Cost cost : copiedAbility.getCosts()) {
if (cost instanceof PayLoyaltyCost) {
loyaltyCost += ((PayLoyaltyCost) cost).getAmount();
}
}
}
return planeswalker.getCounters(game).getCount(CounterType.LOYALTY) + loyaltyCost >= 0 && planeswalker.canLoyaltyBeUsed(game);
} }
/** /**
@ -43,7 +57,6 @@ public class PayLoyaltyCost extends CostImpl {
* ability whose cost has you put loyalty counters on a planeswalker, the * ability whose cost has you put loyalty counters on a planeswalker, the
* number you put on isn't doubled. This is because those counters are put * number you put on isn't doubled. This is because those counters are put
* on as a cost, not as an effect. * on as a cost, not as an effect.
*
*/ */
@Override @Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
@ -65,4 +78,16 @@ public class PayLoyaltyCost extends CostImpl {
return new PayLoyaltyCost(this); return new PayLoyaltyCost(this);
} }
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
this.text = Integer.toString(this.amount);
if (this.amount > 0) {
this.text = '+' + this.text;
}
}
} }

View file

@ -1,21 +1,28 @@
package mage.abilities.costs.common; package mage.abilities.costs.common;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.LoyaltyAbility;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCostImpl; import mage.abilities.costs.VariableCostImpl;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import java.util.UUID;
/** /**
*
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
public class PayVariableLoyaltyCost extends VariableCostImpl { public class PayVariableLoyaltyCost extends VariableCostImpl {
// dynamic x cost modification from effects like Carth the Lion
// GUI only (applies to -X value on X announce)
// Example:
// - counters: 3
// - cost modification: +1
// - max possible X to pay: 4
private int costModification = 0;
public PayVariableLoyaltyCost() { public PayVariableLoyaltyCost() {
super("loyality counters to remove"); super("loyality counters to remove");
this.text = "-X"; this.text = "-X";
@ -23,6 +30,7 @@ public class PayVariableLoyaltyCost extends VariableCostImpl {
public PayVariableLoyaltyCost(final PayVariableLoyaltyCost cost) { public PayVariableLoyaltyCost(final PayVariableLoyaltyCost cost) {
super(cost); super(cost);
this.costModification = cost.costModification;
} }
@Override @Override
@ -43,12 +51,33 @@ public class PayVariableLoyaltyCost extends VariableCostImpl {
@Override @Override
public int getMaxValue(Ability source, Game game) { public int getMaxValue(Ability source, Game game) {
int maxValue = 0;
Permanent permanent = game.getPermanent(source.getSourceId()); Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) { if (permanent == null) {
maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY.getName()); return 0;
}
return maxValue;
} }
int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY.getName());
// apply cost modification
if (source instanceof LoyaltyAbility) {
LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy();
permanent.adjustCosts(copiedAbility, game);
game.getContinuousEffects().costModification(copiedAbility, game);
for (Cost cost : copiedAbility.getCosts()) {
if (cost instanceof PayVariableLoyaltyCost) {
maxValue += ((PayVariableLoyaltyCost) cost).getCostModification();
}
}
}
return Math.max(0, maxValue);
}
public int getCostModification() {
return costModification;
}
public void setCostModification(int costModification) {
this.costModification = costModification;
}
} }