Add several new cards (#1619)

* Add several new cards:

Balustrade Spy
Corpse Augur
Galvanoth
Gift of Growth
Living Artifact
Marchesa, the Black Rose
Phytotitan
Pyromancer's Assault
Settle the Score

* Fix indentation in Galvanoth

* Refactor Pyromancer's Assault, remove trigger condition from action text.

* Generalize milling actions to fixed amount milling and "mill until certain number of cartain cards are milled".

* Refactor cards to use new MillLibraryUntilAction

* Add parser for MillLibraryUntilAction, so it can be used without need for groovy.

* Add new parseable cards:
Mind Funeral
Consuming Aberration
Destroy the Evidence
Undercity Informer
(no groovy needed)

* Balustrade Spy now does not need groovy anymore.
master
Martin Petricek 2018-10-22 15:09:12 +02:00 committed by Melvin Zhang
parent d61d92cede
commit d7e856da68
29 changed files with 414 additions and 70 deletions

View File

@ -11,4 +11,3 @@ ability=Flying;\
When SN enters the battlefield, target player reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
timing=main
oracle=Flying\nWhen Balustrade Spy enters the battlefield, target player reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
status=needs groovy

View File

@ -11,4 +11,3 @@ ability=SN's power and toughness are each equal to the number of cards in your o
Whenever you cast a spell, each opponent reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
timing=main
oracle=Consuming Aberration's power and toughness are each equal to the number of cards in your opponents' graveyards.\nWhenever you cast a spell, each opponent reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
status=needs groovy

View File

@ -0,0 +1,23 @@
// When SN dies, you draw X cards and you lose X life, where X is the number of creature cards in target player's graveyard.
[
new ThisDiesTrigger() {
@Override
public MagicEvent executeTrigger(final MagicGame game,final MagicPermanent source, final MagicPermanent died) {
return new MagicEvent(
source,
TARGET_PLAYER,
this,
"PN draws X cards and loses X life, where X is the number of creature cards in target player's graveyard"
);
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
event.processTargetPlayer(game, {
int creatureCards = it.getGraveyard().getNrOf(MagicType.Creature);
game.doAction(new DrawAction(event.getPlayer(), creatureCards));
game.doAction(new ChangeLifeAction(event.getPlayer(), -creatureCards));
});
}
}
]

View File

@ -7,7 +7,6 @@ type=Creature
subtype=Zombie,Wizard
cost={3}{B}
pt=4/2
ability=When SN dies, you draw X cards and you lose X life, where X is the number of creature cards in target player's graveyard.
timing=main
requires_groovy_code
oracle=When Corpse Augur dies, you draw X cards and you lose X life, where X is the number of creature cards in target player's graveyard.
status=needs groovy

View File

@ -4,7 +4,6 @@ value=2.180
rarity=C
type=Sorcery
cost={4}{B}
effect=Destroy target land. Its controller reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
effect=Destroy target land.~Its controller reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
timing=main
oracle=Destroy target land. Its controller reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
status=needs groovy

View File

@ -0,0 +1,40 @@
def action = {
final MagicGame game, final MagicEvent event ->
if (event.isYes()) {
game.doAction(CastCardAction.WithoutManaCost(event.getPlayer(), event.getRefCard(), MagicLocationType.OwnersLibrary, MagicLocationType.Graveyard));
}
}
[
new AtYourUpkeepTrigger() {
@Override
public MagicEvent executeTrigger(final MagicGame game,final MagicPermanent permanent,final MagicPlayer upkeepPlayer) {
return new MagicEvent(
permanent,
new MagicMayChoice("look at the top card of your library?"),
this,
"PN may\$ looks at the top card of PN's library."
);
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
if (event.isYes()) {
final MagicPlayer player = event.getPlayer();
final MagicCardList cards = player.getLibrary().getCardsFromTop(1);
for (final MagicCard card : cards) {
game.doAction(new LookAction(card, player, "top card of your library"));
if (card.hasType(MagicType.Instant) || card.hasType(MagicType.Sorcery)) {
game.addEvent(new MagicEvent(
event.getSource(),
new MagicMayChoice("Cast the card without paying its mana cost?"),
card,
action,
"If it's an instant or sorcery card, PN may\$ cast it without paying its mana cost."
));
}
}
}
}
}
]

View File

@ -6,7 +6,6 @@ type=Creature
subtype=Beast
cost={3}{R}{R}
pt=3/3
ability=At the beginning of your upkeep, you may look at the top card of your library. If it's an instant or sorcery card, you may cast it without paying its mana cost.
timing=main
oracle=At the beginning of your upkeep, you may look at the top card of your library. If it's an instant or sorcery card, you may cast it without paying its mana cost.
status=needs groovy
requires_groovy_code

View File

@ -0,0 +1,22 @@
[
new MagicSpellCardEvent() {
@Override
public MagicEvent getEvent(final MagicCardOnStack cardOnStack, final MagicPayedCost payedCost) {
return new MagicEvent(
cardOnStack,
POS_TARGET_CREATURE,
this,
"Untap target creature\$. It gets +2/+2 until end of turn. " +
"If SN was kicked, that creature gets +4/+4 instead."
);
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
event.processTargetPermanent(game, {
game.doAction(new UntapAction(it));
int amount = event.isKicked() ? 4 : 2;
game.doAction(new ChangeTurnPTAction(it, amount, amount));
});
}
}
]

View File

@ -5,6 +5,6 @@ rarity=C
type=Instant
cost={1}{G}
ability=Kicker {2}
effect=Untap target creature. It gets +2/+2 until end of turn. If this spell was kicked, that creature gets +4/+4 until end of turn instead.
timing=removal
timing=pump
requires_groovy_code
oracle=Kicker {2}\nUntap target creature. It gets +2/+2 until end of turn. If this spell was kicked, that creature gets +4/+4 until end of turn instead.

View File

@ -0,0 +1,19 @@
[
new DamageIsDealtTrigger() {
@Override
public MagicEvent executeTrigger(final MagicGame game,final MagicPermanent permanent,final MagicDamage damage) {
return permanent.isController(damage.getTarget()) ?
new MagicEvent(
permanent,
damage.getDealtAmount(),
this,
"Put RN vitality counters on SN."
):
MagicEvent.NONE;
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
game.doAction(new ChangeCountersAction(event.getPlayer(),event.getPermanent(),MagicCounterType.Vitality,event.getRefInt()));
}
}
]

View File

@ -6,9 +6,8 @@ type=Enchantment
subtype=Aura
cost={G}
ability=Enchant artifact;\
Whenever you're dealt damage, put that many vitality counters on SN.;\
At the beginning of your upkeep, you may remove a vitality counter from SN. If you do, you gain 1 life.
timing=aura
enchant=default,artifact
oracle=Enchant artifact\nWhenever you're dealt damage, put that many vitality counters on Living Artifact.\nAt the beginning of your upkeep, you may remove a vitality counter from Living Artifact. If you do, you gain 1 life.
status=needs groovy
requires_groovy_code

View File

@ -0,0 +1,51 @@
def DelayedTrigger = {
final MagicPermanent staleSource, final MagicPlayer stalePlayer, final MagicCard staleCard ->
return new AtEndOfTurnTrigger() {
@Override
public MagicEvent executeTrigger(final MagicGame game, final MagicPermanent permanent, final MagicPlayer eotPlayer) {
game.addDelayedAction(new RemoveTriggerAction(this));
final MagicCard mappedCard = staleCard.getOwner().map(game).getGraveyard().getCard(staleCard.getId());
return mappedCard.isInGraveyard() ?
new MagicEvent(
game.createDelayedSource(staleSource, stalePlayer),
mappedCard,
this,
"Return RN to the battlefield under PN's control."
):
MagicEvent.NONE;
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
game.doAction(new ReanimateAction(
event.getRefCard(),
event.getPlayer()
));
}
};
}
[
new OtherDiesTrigger() {
@Override
public MagicEvent executeTrigger(final MagicGame game,final MagicPermanent permanent,final MagicPermanent otherPermanent) {
return otherPermanent.isCreature() && otherPermanent.isNonToken() && otherPermanent.isFriend(permanent) && otherPermanent.getCounters(MagicCounterType.PlusOne) ?
new MagicEvent(
permanent,
otherPermanent.getCard(),
this,
"Return RN to the battlefield under PN's control at the beginning of the next end step."
) :
MagicEvent.NONE;
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
game.doAction(new AddTriggerAction(DelayedTrigger(
event.getPermanent(),
event.getPlayer(),
event.getRefCard()
)));
}
}
]

View File

@ -7,8 +7,7 @@ subtype=Human,Wizard
cost={1}{U}{B}{R}
pt=3/3
ability=Dethrone;\
Other creatures you control have dethrone.;\
Whenever a creature you control with a +1/+1 counter on it dies, return that card to the battlefield under your control at the beginning of the next end step.
Other creatures you control have dethrone.
timing=main
oracle=Dethrone\nOther creatures you control have dethrone.\nWhenever a creature you control with a +1/+1 counter on it dies, return that card to the battlefield under your control at the beginning of the next end step.
status=needs groovy
requires_groovy_code

View File

@ -4,7 +4,6 @@ value=3.833
rarity=U
type=Sorcery
cost={1}{U}{B}
effect=Target opponent reveals cards from the top of his or her library until four land cards are revealed. That player puts all cards revealed this way into his or her graveyard.
effect=Target opponent reveals cards from the top of his or her library until he or she reveals four land cards, then puts those cards into his or her graveyard.
timing=main
oracle=Target opponent reveals cards from the top of his or her library until four land cards are revealed. That player puts all cards revealed this way into his or her graveyard.
status=needs groovy

View File

@ -15,14 +15,7 @@
public void executeEvent(final MagicGame game, final MagicEvent event) {
final int amount = event.getRefInt();
final MagicPlayer player = event.getPlayer();
final MagicCardList library = player.getLibrary();
int landCards = 0;
while (landCards < amount && library.size() > 0) {
if (library.getCardAtTop().hasType(MagicType.Land)) {
landCards++;
}
game.doAction(new MillLibraryAction(player,1));
}
game.doAction(new MillLibraryUntilAction(player, MagicType.Land, amount));
}
}
]

View File

@ -17,15 +17,7 @@
public void executeEvent(final MagicGame game, final MagicEvent event) {
final int amount = 4;
final MagicPlayer player = event.getRefPlayer();
final MagicCardList library = player.getLibrary();
int landCards = 0;
while (landCards < amount && library.size() > 0) {
game.doAction(new RevealAction(library.getCardAtTop()));
if (library.getCardAtTop().hasType(MagicType.Land)) {
landCards++;
}
game.doAction(new MillLibraryAction(player,1));
}
game.doAction(new MillLibraryUntilAction(player, MagicType.Land, amount));
}
}
]

View File

@ -0,0 +1,48 @@
def DelayedTrigger = {
final MagicSource staleSource, final MagicPlayer stalePlayer, final MagicCard staleCard ->
return new AtUpkeepTrigger() {
@Override
public boolean accept(final MagicPermanent permanent, final MagicPlayer upkeepPlayer) {
return stalePlayer.getId() == upkeepPlayer.getId();
}
@Override
public MagicEvent executeTrigger(final MagicGame game,final MagicPermanent permanent,final MagicPlayer upkeepPlayer) {
game.addDelayedAction(new RemoveTriggerAction(this));
final MagicCard mappedCard = staleCard.getOwner().map(game).getGraveyard().getCard(staleCard.getId());
return mappedCard.isInGraveyard() ?
new MagicEvent(
game.createDelayedSource(staleSource, stalePlayer),
mappedCard,
this,
"PN returns RN to the battlefield tapped."
):
MagicEvent.NONE;
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
game.doAction(new ReanimateAction(event.getRefCard(),event.getPlayer(),MagicPlayMod.TAPPED));
}
}
}
[
new ThisDiesTrigger() {
@Override
public MagicEvent executeTrigger(final MagicGame game,final MagicPermanent source, final MagicPermanent died) {
return new MagicEvent(
source,
source.getOwner(),
source.getCard(),
this,
"Return SN to the battlefield tapped under its owner's control at the beginning of PN's next upkeep."
);
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
game.doAction(new AddTriggerAction(
DelayedTrigger(event.getSource(), event.getPlayer(), event.getRefCard())
));
}
}
]

View File

@ -6,7 +6,6 @@ type=Creature
subtype=Plant,Elemental
cost={4}{G}{G}
pt=7/2
ability=When SN dies, return it to the battlefield tapped under its owner's control at the beginning of his or her next upkeep.
timing=main
requires_groovy_code
oracle=When Phytotitan dies, return it to the battlefield tapped under its owner's control at the beginning of his or her next upkeep.
status=needs groovy

View File

@ -0,0 +1,25 @@
[
new OtherSpellIsCastTrigger() {
@Override
public boolean accept(final MagicPermanent permanent, final MagicCardOnStack spell) {
return permanent.isController(spell.getController()) && permanent.getController().getSpellsCast() == 1;
}
@Override
public MagicEvent executeTrigger(final MagicGame game, final MagicPermanent permanent, final MagicCardOnStack cardOnStack) {
return new MagicEvent(
permanent,
NEG_TARGET_CREATURE_OR_PLAYER,
this,
"SN deals 2 damage to target creature or player\$."
);
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
event.processTarget(game, {
game.doAction(new DealDamageAction(event.getSource(),it,2));
});
}
}
]

View File

@ -5,7 +5,6 @@ value=2.500
rarity=U
type=Enchantment
cost={3}{R}
ability=Whenever you cast your second spell each turn, SN deals 2 damage to target creature or player.
timing=enchantment
oracle=Whenever you cast your second spell each turn, Pyromancer's Assault deals 2 damage to target creature or player.
status=needs groovy
requires_groovy_code

View File

@ -0,0 +1,24 @@
def effect = MagicRuleEventAction.create("Put two loyalty counters on a planeswalker you control.")
[
new MagicSpellCardEvent() {
@Override
public MagicEvent getEvent(final MagicCardOnStack cardOnStack,final MagicPayedCost payedCost) {
return new MagicEvent(
cardOnStack,
NEG_TARGET_CREATURE,
MagicExileTargetPicker.create(),
this,
"Exile target creature\$."
);
}
@Override
public void executeEvent(final MagicGame game, final MagicEvent event) {
event.processTargetPermanent(game, {
game.doAction(new RemoveFromPlayAction(it, MagicLocationType.Exile));
game.addEvent(effect.getEvent(event));
});
}
}
]

View File

@ -4,6 +4,6 @@ value=2.500
rarity=U
type=Sorcery
cost={2}{B}{B}
effect=Exile target creature. Put two loyalty counters on a planeswalker you control.
timing=main
requires_groovy_code
oracle=Exile target creature. Put two loyalty counters on a planeswalker you control.

View File

@ -9,4 +9,3 @@ pt=2/3
ability={1}, Sacrifice a creature: Target player reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
timing=main
oracle={1}, Sacrifice a creature: Target player reveals cards from the top of his or her library until he or she reveals a land card, then puts those cards into his or her graveyard.
status=needs groovy

View File

@ -23,6 +23,7 @@ public class ARG {
public static final String THING = "(permanent|creature|artifact|land|spell or ability|spell|ability)";
public static final String PLAYER = "(player|opponent)";
public static final String EVENQUOTES = "(?=([^\"]*'[^\"]*')*[^\"]*$)";
public static final String CARDTYPE = "(?<cardtype>(creature|artifact|land|enchantment|conspiracy|instant|phenomenon|plane|planeswalker|scheme|sorcery|tribal|vanguard))";
public static final String ENERGY = "(?<energy>(\\{E\\})+)";
public static int energy(final Matcher m) {
@ -55,6 +56,10 @@ public class ARG {
return MagicAmountParser.build(m.group("amount"));
}
public static MagicType cardType(final Matcher m) {
return MagicType.getType(m.group("cardtype"));
}
public static final String AMOUNT2 = "(?<amount2>[^ ]+?)";
public static int amount2(final Matcher m) {
return EnglishToInt.convert(m.group("amount2"));

View File

@ -0,0 +1,56 @@
package magic.model.action;
import magic.ai.ArtificialScoringSystem;
import magic.model.MagicCard;
import magic.model.MagicCardList;
import magic.model.MagicGame;
import magic.model.MagicMessage;
import magic.model.MagicPlayer;
import java.util.List;
/**
* Ancestor of various library-milling actions
*/
public abstract class AbstractMillAction extends MagicAction {
protected final MagicPlayer player;
protected final MagicCardList milledCards = new MagicCardList();
protected AbstractMillAction(final MagicPlayer player) {
this.player = player;
}
public List<MagicCard> getMilledCards() {
return milledCards;
}
@Override
public void undoAction(final MagicGame game) {
}
protected String getMillDescription(int finalCount) {
return String.format("top %d cards", finalCount);
}
protected abstract void setCardsToMill(MagicGame game);
@Override
public void doAction(final MagicGame game) {
getMilledCards().clear();
setCardsToMill(game);
final int count = getMilledCards().size();
if (count > 0) {
setScore(player,ArtificialScoringSystem.getMillScore(count));
game.logAppendMessage(
player,
String.format(
"%s puts the %s of his or her library into his or her graveyard. (%s)",
player,
getMillDescription(count),
count > 5 ? "..." : MagicMessage.getTokenizedCardNames(getMilledCards())
)
);
}
}
}

View File

@ -1,28 +1,30 @@
package magic.model.action;
import magic.ai.ArtificialScoringSystem;
import magic.model.MagicCard;
import magic.model.MagicCardList;
import magic.model.MagicGame;
import magic.model.MagicLocationType;
import magic.model.MagicPlayer;
import java.util.List;
import magic.model.MagicMessage;
/**
* Action that removes fixed amount of cards from players library and moves them to that player's graveyard.
*/
public class MillLibraryAction extends AbstractMillAction {
public class MillLibraryAction extends MagicAction {
private final MagicPlayer player;
private final int amount;
private final MagicCardList milledCards = new MagicCardList();
/**
* Mill target player's library (move cards to that player's graveyard).
*
* @param aPlayer target player
* @param aAmount number of cards to move
*/
public MillLibraryAction(final MagicPlayer aPlayer,final int aAmount) {
player = aPlayer;
super(aPlayer);
amount = aAmount;
}
@Override
public void doAction(final MagicGame game) {
protected void setCardsToMill(MagicGame game) {
final MagicCardList topN = player.getLibrary().getCardsFromTop(amount);
for (final MagicCard card : topN) {
milledCards.add(card);
@ -32,25 +34,5 @@ public class MillLibraryAction extends MagicAction {
MagicLocationType.Graveyard
));
}
final int count = topN.size();
if (count > 0) {
setScore(player,ArtificialScoringSystem.getMillScore(count));
game.logAppendMessage(
player,
String.format(
"%s puts the top %d cards of his or her library into his or her graveyard. (%s)",
player,
count,
count > 5 ? "..." : MagicMessage.getTokenizedCardNames(topN)
)
);
}
}
public List<MagicCard> getMilledCards() {
return milledCards;
}
@Override
public void undoAction(final MagicGame game) {}
}

View File

@ -0,0 +1,55 @@
package magic.model.action;
import magic.model.MagicCard;
import magic.model.MagicCardList;
import magic.model.MagicGame;
import magic.model.MagicLocationType;
import magic.model.MagicPlayer;
import magic.model.MagicType;
/**
* Action that removes cards from players library and moves them to that player's graveyard,
* until certain card type is seen given number of times (or whole library is milled).
*/
public class MillLibraryUntilAction extends AbstractMillAction {
private final MagicType cardType;
private final int cards;
/**
* Mill target player's library (move cards to that player's graveyard) until certain amount of certain cards are
* moved.
*
* @param aPlayer target player
* @param aCardType type of card to check
* @param aCards number of those cards to encounter in order for milling to stop
*/
public MillLibraryUntilAction(final MagicPlayer aPlayer, final MagicType aCardType, int aCards) {
super(aPlayer);
cardType = aCardType;
cards = aCards;
}
@Override
protected String getMillDescription(int finalCount) {
return String.format("top %d cards - until %d %s(s) are seen -", finalCount, cards, cardType.getDisplayName());
}
protected void setCardsToMill(MagicGame game) {
final MagicCardList all = player.getLibrary().getCardsFromTop(Integer.MAX_VALUE);
int seenTargets = 0;
for (final MagicCard card : all) {
milledCards.add(card);
game.doAction(new ShiftCardAction(
card,
MagicLocationType.OwnersLibrary,
MagicLocationType.Graveyard
));
if (card.hasType(cardType)) {
seenTargets++;
}
if (seenTargets >= cards) {
break;
}
}
}
}

View File

@ -2097,6 +2097,25 @@ public enum MagicRuleEventAction {
};
}
},
MillUntil(
ARG.PLAYERS + "( )?reveal(s)? cards from the top of (your|his or her) library until (you|he or she) reveal(s) "
+ ARG.AMOUNT + "?( )?" + ARG.CARDTYPE + " card(s)?, then puts those cards into (your|his or her) graveyard",
MagicTiming.Draw,
"MillUntil"
) {
@Override
public MagicEventAction getAction(final Matcher matcher) {
final MagicAmount count = ARG.amountObj(matcher);
final MagicTargetFilter<MagicPlayer> filter = ARG.playersParse(matcher);
final MagicType cardType = ARG.cardType(matcher);
return (game, event) -> {
final int amount = count.getAmount(event);
for (final MagicPlayer it : ARG.players(event, matcher, filter)) {
game.doAction(new MillLibraryUntilAction(it, cardType, amount));
}
};
}
},
CantCastSpells(
ARG.PLAYERS + " can't cast spells this turn",
MagicTargetHint.Negative,

View File

@ -2384,6 +2384,7 @@ public class MagicTargetFilterFactory {
// <color|type|subtype> card from an opponent's graveyard
add("card from an opponent's graveyard", CARD_FROM_OPPONENTS_GRAVEYARD);
add("card in an opponent's graveyard", CARD_FROM_OPPONENTS_GRAVEYARD);
add("creature card in an opponent's graveyard", CREATURE_CARD_FROM_OPPONENTS_GRAVEYARD);
// <color|type|subtype> card from your hand