forked from External/mage
[CLU] Implement Sludge Titan
Introduced TargetCardAndOrCardInGraveyard, derived from the Library one. Did not achieved everything I wanted in the tests, as the choice seems to be accepted. Tested it live, no particular issue, you can't select more than one per category.
This commit is contained in:
parent
a3b9755da9
commit
7df86e91a0
4 changed files with 422 additions and 0 deletions
87
Mage.Sets/src/mage/cards/s/SludgeTitan.java
Normal file
87
Mage.Sets/src/mage/cards/s/SludgeTitan.java
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package mage.cards.s;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.keyword.TrampleAbility;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.cards.Cards;
|
||||
import mage.cards.CardsImpl;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.target.TargetCard;
|
||||
import mage.target.common.TargetCardAndOrCardInGraveyard;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Susucr
|
||||
*/
|
||||
public final class SludgeTitan extends CardImpl {
|
||||
|
||||
public SludgeTitan(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{B/G}{B/G}");
|
||||
|
||||
this.subtype.add(SubType.ZOMBIE);
|
||||
this.subtype.add(SubType.GIANT);
|
||||
this.power = new MageInt(6);
|
||||
this.toughness = new MageInt(6);
|
||||
|
||||
// Trample
|
||||
this.addAbility(TrampleAbility.getInstance());
|
||||
|
||||
// Whenever Sludge Titan enters the battlefield or attacks, mill five cards. You may put a creature card and/or a land card from among them into your hand.
|
||||
this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new SludgeTitanEffect()));
|
||||
}
|
||||
|
||||
private SludgeTitan(final SludgeTitan card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SludgeTitan copy() {
|
||||
return new SludgeTitan(this);
|
||||
}
|
||||
}
|
||||
|
||||
class SludgeTitanEffect extends OneShotEffect {
|
||||
|
||||
SludgeTitanEffect() {
|
||||
super(Outcome.Benefit);
|
||||
this.staticText = "mill five cards. "
|
||||
+ "You may put a creature card and/or a land card from among them into your hand";
|
||||
}
|
||||
|
||||
private SludgeTitanEffect(final SludgeTitanEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SludgeTitanEffect copy() {
|
||||
return new SludgeTitanEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller == null) {
|
||||
return false;
|
||||
}
|
||||
Cards cards = controller.millCards(5, source, game);
|
||||
game.getState().processAction(game);
|
||||
if (!cards.isEmpty()) {
|
||||
TargetCard target = new TargetCardAndOrCardInGraveyard(CardType.CREATURE, CardType.LAND);
|
||||
controller.choose(Outcome.DrawCard, cards, target, source, game);
|
||||
Cards toHand = new CardsImpl();
|
||||
toHand.addAll(target.getTargets());
|
||||
controller.moveCards(toHand, Zone.HAND, source, game);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ public final class RavnicaClueEdition extends ExpansionSet {
|
|||
cards.add(new SetCardInfo("Sacred Foundry", 279, Rarity.RARE, mage.cards.s.SacredFoundry.class));
|
||||
cards.add(new SetCardInfo("Secret Passage", 20, Rarity.UNCOMMON, mage.cards.s.SecretPassage.class));
|
||||
cards.add(new SetCardInfo("Senator Peacock", 2, Rarity.RARE, mage.cards.s.SenatorPeacock.class));
|
||||
cards.add(new SetCardInfo("Sludge Titan", 43, Rarity.RARE, mage.cards.s.SludgeTitan.class));
|
||||
cards.add(new SetCardInfo("Steam Vents", 280, Rarity.RARE, mage.cards.s.SteamVents.class));
|
||||
cards.add(new SetCardInfo("Stomping Ground", 281, Rarity.RARE, mage.cards.s.StompingGround.class));
|
||||
cards.add(new SetCardInfo("Study", 21, Rarity.UNCOMMON, mage.cards.s.Study.class));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
|
||||
package org.mage.test.cards.single.clu;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
/**
|
||||
* @author Susucr
|
||||
*/
|
||||
public class SludgeTitanTest extends CardTestPlayerBase {
|
||||
|
||||
/**
|
||||
* {@link mage.cards.s.SludgeTitan Sludge Titan} {4}{B/G}{B/G}
|
||||
* Creature — Zombie Giant
|
||||
* Trample
|
||||
* Whenever Sludge Titan enters the battlefield or attacks, mill five cards. You may put a creature card and/or a land card from among them into your hand.
|
||||
* 6/6
|
||||
*/
|
||||
private static final String titan = "Sludge Titan";
|
||||
|
||||
private static final String piker = "Goblin Piker"; // creature 1
|
||||
private static final String vanguard = "Elite Vanguard"; // creature 2
|
||||
private static final String divination = "Divination"; // sorcery
|
||||
private static final String dryad = "Dryad Arbor"; // land creature
|
||||
private static final String plateau = "Plateau"; // land 1
|
||||
private static final String savannah = "Savannah"; // land 2
|
||||
|
||||
// When Golgari Brownscale is put into your hand from your graveyard, you gain 2 life.
|
||||
private static final String brownscale = "Golgari Brownscale";
|
||||
|
||||
@Test
|
||||
public void testNoValidChoice() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, divination, 5);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, playerA.CHOICE_SKIP);
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
execute();
|
||||
|
||||
assertGraveyardCount(playerA, divination, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoValidChoiceInvalid() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, divination, 5);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, divination); // invalid, titan doesn't allow for choosing sorcery
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
|
||||
try {
|
||||
execute();
|
||||
Assert.fail("must throw exception on execute");
|
||||
} catch (Throwable e) {
|
||||
if (!e.getMessage().startsWith("Missing CHOICE def")) {
|
||||
Assert.fail("Unexpected exception " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreatureOnly_ChooseNone() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, piker, 5);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, playerA.CHOICE_SKIP);
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
execute();
|
||||
|
||||
assertGraveyardCount(playerA, piker, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreatureOnly_ChooseOne() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, piker, 5);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, piker);
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
execute();
|
||||
|
||||
assertGraveyardCount(playerA, piker, 4);
|
||||
assertHandCount(playerA, piker, 1);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
// The test suite does not fully validate the choice, and accept it. Work properly live preventing such choice.
|
||||
@Test
|
||||
public void testCreatureOnly_ChooseTwoInvalid() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, piker, 3);
|
||||
addCard(Zone.LIBRARY, playerA, vanguard, 2);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, piker + "^" + vanguard); // invalid, titan doesn't allow for choosing 2 creatures
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
|
||||
try {
|
||||
execute();
|
||||
Assert.fail("must throw exception on execute");
|
||||
} catch (Throwable e) {
|
||||
if (!e.getMessage().startsWith("Missing CHOICE def")) {
|
||||
Assert.fail("Unexpected exception " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBoth_ChooseTwo() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, piker);
|
||||
addCard(Zone.LIBRARY, playerA, vanguard);
|
||||
addCard(Zone.LIBRARY, playerA, divination);
|
||||
addCard(Zone.LIBRARY, playerA, savannah);
|
||||
addCard(Zone.LIBRARY, playerA, plateau);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, piker + "^" + savannah);
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
execute();
|
||||
|
||||
assertGraveyardCount(playerA, 3);
|
||||
assertHandCount(playerA, savannah, 1);
|
||||
assertHandCount(playerA, piker, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBoth_ChooseTwo_DryadArbor() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, dryad, 5);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, dryad + "^" + dryad);
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
execute();
|
||||
|
||||
assertGraveyardCount(playerA, dryad, 3);
|
||||
assertHandCount(playerA, dryad, 2);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
// The test suite does not fully validate the choice, and accept it. Work properly live preventing selecting 3 dryads.
|
||||
@Test
|
||||
public void testBoth_ChooseThree_DryadArbor_Invalid() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, dryad, 5);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, dryad + "^" + dryad + "^" + dryad); // invalid, titan doesn't allow for choosing a creature and/or a land
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
|
||||
try {
|
||||
execute();
|
||||
Assert.fail("must throw exception on execute");
|
||||
} catch (Throwable e) {
|
||||
if (!e.getMessage().startsWith("Missing CHOICE def")) {
|
||||
Assert.fail("Unexpected exception " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Brownscale_triggers() {
|
||||
setStrictChooseMode(true);
|
||||
skipInitShuffling();
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, titan);
|
||||
addCard(Zone.LIBRARY, playerA, brownscale, 5);
|
||||
|
||||
attack(1, playerA, titan, playerB);
|
||||
setChoice(playerA, brownscale);
|
||||
|
||||
setStopAt(1, PhaseStep.DECLARE_BLOCKERS);
|
||||
execute();
|
||||
|
||||
assertGraveyardCount(playerA, brownscale, 4);
|
||||
assertHandCount(playerA, brownscale, 1);
|
||||
assertLife(playerA, 20 + 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package mage.target.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.dynamicvalue.common.PredicateCardAssignment;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.Cards;
|
||||
import mage.cards.CardsImpl;
|
||||
import mage.constants.CardType;
|
||||
import mage.filter.FilterCard;
|
||||
import mage.filter.predicate.Predicate;
|
||||
import mage.filter.predicate.Predicates;
|
||||
import mage.game.Game;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Susucr
|
||||
* <p>
|
||||
* almost identical to {@link TargetCardAndOrCardInLibrary}
|
||||
*/
|
||||
public class TargetCardAndOrCardInGraveyard extends TargetCardInGraveyard {
|
||||
|
||||
private static FilterCard makeFilter(Predicate<? super Card> firstPredicate,
|
||||
Predicate<? super Card> secondPredicate,
|
||||
String filterText) {
|
||||
FilterCard filter = new FilterCard(filterText);
|
||||
filter.add(Predicates.or(
|
||||
firstPredicate, secondPredicate
|
||||
));
|
||||
return filter;
|
||||
}
|
||||
|
||||
private static String makeFilterText(String first, String second) {
|
||||
return CardUtil.addArticle(first) + " card and/or " + CardUtil.addArticle(second) + " card";
|
||||
}
|
||||
|
||||
private final PredicateCardAssignment assignment;
|
||||
|
||||
/**
|
||||
* a [firstType] card and/or a [secondType] card
|
||||
*/
|
||||
protected TargetCardAndOrCardInGraveyard(Predicate<? super Card> firstPredicate, Predicate<? super Card> secondPredicate, String filterText) {
|
||||
super(0, 2, makeFilter(firstPredicate, secondPredicate, filterText));
|
||||
this.assignment = new PredicateCardAssignment(firstPredicate, secondPredicate);
|
||||
}
|
||||
|
||||
public TargetCardAndOrCardInGraveyard(CardType firstType, CardType secondType) {
|
||||
this(firstType.getPredicate(), secondType.getPredicate(), makeFilterText(
|
||||
CardUtil.getTextWithFirstCharLowerCase(firstType.toString()),
|
||||
CardUtil.getTextWithFirstCharLowerCase(secondType.toString())));
|
||||
}
|
||||
|
||||
protected TargetCardAndOrCardInGraveyard(final TargetCardAndOrCardInGraveyard target) {
|
||||
super(target);
|
||||
this.assignment = target.assignment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetCardAndOrCardInGraveyard copy() {
|
||||
return new TargetCardAndOrCardInGraveyard(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) {
|
||||
if (!super.canTarget(playerId, id, source, game)) {
|
||||
return false;
|
||||
}
|
||||
Card card = game.getCard(id);
|
||||
if (card == null) {
|
||||
return false;
|
||||
}
|
||||
if (this.getTargets().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
Cards cards = new CardsImpl(this.getTargets());
|
||||
cards.add(card);
|
||||
return assignment.getRoleCount(cards, game) >= cards.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<UUID> possibleTargets(UUID sourceControllerId, Ability source, Game game) {
|
||||
Set<UUID> possibleTargets = super.possibleTargets(sourceControllerId, source, game);
|
||||
// assuming max targets = 2, need to expand this code if not
|
||||
Card card = game.getCard(this.getFirstTarget());
|
||||
if (card == null) {
|
||||
return possibleTargets; // no further restriction if no target yet chosen
|
||||
}
|
||||
Cards cards = new CardsImpl(card);
|
||||
if (assignment.getRoleCount(cards, game) == 2) {
|
||||
// if the first chosen target is both types, no further restriction
|
||||
return possibleTargets;
|
||||
}
|
||||
Set<UUID> leftPossibleTargets = new HashSet<>();
|
||||
for (UUID possibleId : possibleTargets) {
|
||||
Card possibleCard = game.getCard(possibleId);
|
||||
Cards checkCards = cards.copy();
|
||||
checkCards.add(possibleCard);
|
||||
if (assignment.getRoleCount(checkCards, game) == 2) {
|
||||
// if the possible target and the existing target have both types, it's legal
|
||||
// but this prevents the case of both targets with the same type
|
||||
leftPossibleTargets.add(possibleId);
|
||||
}
|
||||
}
|
||||
return leftPossibleTargets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return filter.getMessage();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue