mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 12:48:00 +00:00
Merge branch 'mutate' into 'master'
Mutate See merge request core-developers/forge!3772
This commit is contained in:
@@ -105,6 +105,7 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.MultiplyCounter, CountersMultiplyAi.class)
|
||||
.put(ApiType.MustAttack, MustAttackAi.class)
|
||||
.put(ApiType.MustBlock, MustBlockAi.class)
|
||||
.put(ApiType.Mutate, MutateAi.class)
|
||||
.put(ApiType.NameCard, ChooseCardNameAi.class)
|
||||
.put(ApiType.NoteCounters, AlwaysPlayAi.class)
|
||||
.put(ApiType.PeekAndReveal, PeekAndRevealAi.class)
|
||||
|
||||
68
forge-ai/src/main/java/forge/ai/ability/MutateAi.java
Normal file
68
forge-ai/src/main/java/forge/ai/ability/MutateAi.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class MutateAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
CardCollectionView mutateTgts = CardLists.getTargetableCards(aiPlayer.getCreaturesInPlay(), sa);
|
||||
|
||||
// Filter out some abilities that are useless
|
||||
// TODO: add other stuff useless for Mutate here
|
||||
mutateTgts = CardLists.filter(mutateTgts, Predicates.not(Predicates.or(
|
||||
CardPredicates.hasKeyword(Keyword.DEFENDER),
|
||||
CardPredicates.hasKeyword("CARDNAME can't attack."),
|
||||
CardPredicates.hasKeyword("CARDNAME can't block."),
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(aiPlayer, card);
|
||||
}
|
||||
}
|
||||
)));
|
||||
|
||||
if (mutateTgts.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Choose the best target
|
||||
// TODO: maybe, instead of the standard evaluator, this could inspect the abilities and decide
|
||||
// which are better in context, but that's a bit complicated for the time being (not sure if necessary?).
|
||||
Card mutateTgt = ComputerUtilCard.getBestCreatureAI(mutateTgts);
|
||||
sa.getTargets().add(mutateTgt);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
// Decide which card goes on top here. Pretty rudimentary, feel free to improve.
|
||||
Card choice = null;
|
||||
|
||||
for (Card c : options) {
|
||||
if (choice == null || c.getBasePower() > choice.getBasePower() || c.getBaseToughness() > choice.getBaseToughness()) {
|
||||
choice = c;
|
||||
}
|
||||
}
|
||||
|
||||
return choice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class GameAction {
|
||||
return c;
|
||||
}
|
||||
|
||||
boolean toBattlefield = zoneTo.is(ZoneType.Battlefield);
|
||||
boolean toBattlefield = zoneTo.is(ZoneType.Battlefield) || zoneTo.is(ZoneType.Merged);
|
||||
boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield);
|
||||
boolean wasFacedown = c.isFaceDown();
|
||||
|
||||
@@ -132,6 +132,7 @@ public class GameAction {
|
||||
|
||||
Card copied = null;
|
||||
Card lastKnownInfo = null;
|
||||
Card commanderEffect = null; // The effect card of commander replacement effect
|
||||
|
||||
// get the LKI from above like ChangeZoneEffect
|
||||
if (params != null && params.containsKey(AbilityKey.CardLKI)) {
|
||||
@@ -203,7 +204,7 @@ public class GameAction {
|
||||
lastKnownInfo = CardUtil.getLKICopy(c);
|
||||
}
|
||||
|
||||
if (!c.isToken()) {
|
||||
if (!c.isRealToken()) {
|
||||
copied = CardFactory.copyCard(c, false);
|
||||
|
||||
if (!zoneTo.is(ZoneType.Stack)) {
|
||||
@@ -231,6 +232,26 @@ public class GameAction {
|
||||
copied.updateStateForView();
|
||||
|
||||
if (!suppress) {
|
||||
// Temporary disable commander replacement effect
|
||||
// 903.9a
|
||||
if (fromBattlefield && !toBattlefield && c.isCommander() && c.hasMergedCard()) {
|
||||
// Find the commander replacement effect "card"
|
||||
CardCollectionView comCards = c.getOwner().getCardsIn(ZoneType.Command);
|
||||
for (final Card effCard : comCards) {
|
||||
for (final ReplacementEffect re : effCard.getReplacementEffects()) {
|
||||
if (re.hasSVar("CommanderMoveReplacement")) {
|
||||
commanderEffect = effCard;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (commanderEffect != null) break;
|
||||
}
|
||||
// Disable the commander replacement effect
|
||||
for (final ReplacementEffect re : commanderEffect.getReplacementEffects()) {
|
||||
re.setSuppressed(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneFrom == null) {
|
||||
copied.getOwner().addInboundToken(copied);
|
||||
}
|
||||
@@ -277,6 +298,38 @@ public class GameAction {
|
||||
|
||||
copied.getOwner().removeInboundToken(copied);
|
||||
|
||||
// Handle merged permanent here so all replacement effects are already applied.
|
||||
CardCollection mergedCards = null;
|
||||
if (fromBattlefield && !toBattlefield && c.hasMergedCard()) {
|
||||
CardCollection cards = new CardCollection(c.getMergedCards());
|
||||
// replace top card with copied card for correct name for human to choose.
|
||||
cards.set(cards.indexOf(c), copied);
|
||||
// 721.3b
|
||||
if (cause != null && zoneTo.getZoneType() == ZoneType.Exile) {
|
||||
cards = (CardCollection) cause.getHostCard().getController().getController().orderMoveToZoneList(cards, zoneTo.getZoneType());
|
||||
} else {
|
||||
cards = (CardCollection) c.getOwner().getController().orderMoveToZoneList(cards, zoneTo.getZoneType());
|
||||
}
|
||||
cards.set(cards.indexOf(copied), c);
|
||||
if (zoneTo.is(ZoneType.Library)) {
|
||||
java.util.Collections.reverse(cards);
|
||||
}
|
||||
mergedCards = cards;
|
||||
if (cause != null) {
|
||||
// Replace sa targeting cards
|
||||
final SpellAbility saTargeting = cause.getSATargetingCard();
|
||||
if (saTargeting != null) {
|
||||
saTargeting.getTargets().replaceTargetCard(c, cards);
|
||||
}
|
||||
// Replace host rememberd cards
|
||||
Card hostCard = cause.getHostCard();
|
||||
if (hostCard.isRemembered(c)) {
|
||||
hostCard.removeRemembered(c);
|
||||
hostCard.addRemembered(cards);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suppress) {
|
||||
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
|
||||
}
|
||||
@@ -292,6 +345,11 @@ public class GameAction {
|
||||
&& zoneFrom == zoneTo && position.equals(zoneFrom.size()) && position != 0) {
|
||||
position--;
|
||||
}
|
||||
if (mergedCards != null) {
|
||||
for (final Card card : mergedCards) {
|
||||
c.getOwner().getZone(ZoneType.Merged).remove(card);
|
||||
}
|
||||
}
|
||||
zoneFrom.remove(c);
|
||||
if (!zoneTo.is(ZoneType.Exile) && !zoneTo.is(ZoneType.Stack)) {
|
||||
c.setExiledWith(null);
|
||||
@@ -328,10 +386,50 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
|
||||
// "enter the battlefield as a copy" - apply code here
|
||||
// but how to query for input here and continue later while the callers assume synchronous result?
|
||||
zoneTo.add(copied, position, lastKnownInfo); // the modified state of the card is also reported here (e.g. for Morbid + Awaken)
|
||||
c.setZone(zoneTo);
|
||||
if (mergedCards != null) {
|
||||
// Move components of merged permanet here
|
||||
// Also handle 721.3e and 903.9a
|
||||
boolean wasToken = c.isToken();
|
||||
if (commanderEffect != null) {
|
||||
for (final ReplacementEffect re : commanderEffect.getReplacementEffects()) {
|
||||
re.setSuppressed(false);
|
||||
}
|
||||
}
|
||||
// Change zone of original card so components isToken() and isCommander() return correct value
|
||||
// when running replacement effects here
|
||||
c.setZone(zoneTo);
|
||||
for (final Card card : mergedCards) {
|
||||
if (card.isRealCommander()) {
|
||||
card.setMoveToCommandZone(true);
|
||||
}
|
||||
// 721.3e & 903.9a
|
||||
if (wasToken && !card.isRealToken() || card.isRealCommander()) {
|
||||
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(card);
|
||||
repParams.put(AbilityKey.CardLKI, card);
|
||||
repParams.put(AbilityKey.Cause, cause);
|
||||
repParams.put(AbilityKey.Origin, zoneFrom != null ? zoneFrom.getZoneType() : null);
|
||||
repParams.put(AbilityKey.Destination, zoneTo.getZoneType());
|
||||
|
||||
if (params != null) {
|
||||
repParams.putAll(params);
|
||||
}
|
||||
|
||||
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams);
|
||||
if (repres != ReplacementResult.NotReplaced) continue;
|
||||
}
|
||||
if (card == c) {
|
||||
zoneTo.add(copied, position, lastKnownInfo); // the modified state of the card is also reported here (e.g. for Morbid + Awaken)
|
||||
} else {
|
||||
zoneTo.add(card, position);
|
||||
}
|
||||
card.setZone(zoneTo);
|
||||
}
|
||||
} else {
|
||||
// "enter the battlefield as a copy" - apply code here
|
||||
// but how to query for input here and continue later while the callers assume synchronous result?
|
||||
zoneTo.add(copied, position, lastKnownInfo); // the modified state of the card is also reported here (e.g. for Morbid + Awaken)
|
||||
c.setZone(zoneTo);
|
||||
}
|
||||
|
||||
// do ETB counters after zone add
|
||||
if (!suppress) {
|
||||
@@ -376,6 +474,7 @@ public class GameAction {
|
||||
runParams.put(AbilityKey.Destination, zoneTo.getZoneType().name());
|
||||
runParams.put(AbilityKey.SpellAbilityStackInstance, game.stack.peek());
|
||||
runParams.put(AbilityKey.IndividualCostPaymentInstance, game.costPaymentStack.peek());
|
||||
runParams.put(AbilityKey.MergedCards, mergedCards);
|
||||
|
||||
if (params != null) {
|
||||
runParams.putAll(params);
|
||||
@@ -400,7 +499,7 @@ public class GameAction {
|
||||
return copied;
|
||||
}
|
||||
|
||||
if (!c.isToken() && !toBattlefield) {
|
||||
if (!c.isRealToken() && !toBattlefield) {
|
||||
copied.clearDevoured();
|
||||
copied.clearDelved();
|
||||
copied.clearConvoked();
|
||||
@@ -415,13 +514,13 @@ public class GameAction {
|
||||
}
|
||||
|
||||
if (fromBattlefield) {
|
||||
if (!c.isToken()) {
|
||||
if (!c.isRealToken()) {
|
||||
copied.setState(CardStateName.Original, true);
|
||||
}
|
||||
// Soulbond unpairing
|
||||
if (c.isPaired()) {
|
||||
c.getPairedWith().setPairedWith(null);
|
||||
if (!c.isToken()) {
|
||||
if (!c.isRealToken()) {
|
||||
c.setPairedWith(null);
|
||||
}
|
||||
}
|
||||
@@ -550,7 +649,7 @@ public class GameAction {
|
||||
AttachEffect.attachAuraOnIndirectEnterBattlefield(c);
|
||||
}
|
||||
|
||||
if (c.isCommander()) {
|
||||
if (c.isRealCommander()) {
|
||||
c.setMoveToCommandZone(true);
|
||||
}
|
||||
|
||||
@@ -1173,7 +1272,7 @@ public class GameAction {
|
||||
}
|
||||
|
||||
private boolean stateBasedAction903_9a(Card c) {
|
||||
if (c.isCommander() && c.canMoveToCommandZone()) {
|
||||
if (c.isRealCommander() && c.canMoveToCommandZone()) {
|
||||
c.setMoveToCommandZone(false);
|
||||
if (c.getOwner().getController().confirmAction(c.getSpellPermanent(), PlayerActionConfirmMode.ChangeZoneToAltDestination, c.getName() + ": If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.")) {
|
||||
moveTo(c.getOwner().getZone(ZoneType.Command), c, null);
|
||||
@@ -1205,7 +1304,7 @@ public class GameAction {
|
||||
// If a token is in a zone other than the battlefield, it ceases to exist.
|
||||
private boolean stateBasedAction704_5d(Card c) {
|
||||
boolean checkAgain = false;
|
||||
if (c.isToken()) {
|
||||
if (c.isRealToken()) {
|
||||
final Zone zoneFrom = game.getZoneOf(c);
|
||||
if (!zoneFrom.is(ZoneType.Battlefield)) {
|
||||
zoneFrom.remove(c);
|
||||
|
||||
@@ -74,6 +74,7 @@ public enum AbilityKey {
|
||||
LifeAmount("LifeAmount"), //TODO confirm that this and LifeGained can be merged
|
||||
LifeGained("LifeGained"),
|
||||
Mana("Mana"),
|
||||
MergedCards("MergedCards"),
|
||||
MonstrosityAmount("MonstrosityAmount"),
|
||||
NewCard("NewCard"),
|
||||
NewCounterAmount("NewCounterAmount"),
|
||||
|
||||
@@ -395,7 +395,7 @@ public class AbilityUtils {
|
||||
* <p>
|
||||
* calculateAmount.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param card
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param amount
|
||||
@@ -792,7 +792,7 @@ public class AbilityUtils {
|
||||
* <p>
|
||||
* getDefinedObjects.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param card
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param def
|
||||
@@ -822,7 +822,7 @@ public class AbilityUtils {
|
||||
|
||||
/**
|
||||
* Filter list by type.
|
||||
*
|
||||
*
|
||||
* @param list
|
||||
* a CardList
|
||||
* @param type
|
||||
@@ -951,7 +951,7 @@ public class AbilityUtils {
|
||||
* <p>
|
||||
* getDefinedPlayers.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param card
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param def
|
||||
@@ -1054,6 +1054,10 @@ public class AbilityUtils {
|
||||
if (c instanceof SpellAbility) {
|
||||
o = ((SpellAbility) c).getActivatingPlayer();
|
||||
}
|
||||
// For merged permanent
|
||||
if (c instanceof CardCollection) {
|
||||
o = ((CardCollection) c).get(0).getController();
|
||||
}
|
||||
}
|
||||
else if (defParsed.endsWith("Opponent")) {
|
||||
String triggeringType = defParsed.substring(9);
|
||||
@@ -1065,6 +1069,10 @@ public class AbilityUtils {
|
||||
if (c instanceof SpellAbility) {
|
||||
o = ((SpellAbility) c).getActivatingPlayer().getOpponents();
|
||||
}
|
||||
// For merged permanent
|
||||
if (c instanceof CardCollection) {
|
||||
o = ((CardCollection) c).get(0).getController().getOpponents();;
|
||||
}
|
||||
}
|
||||
else if (defParsed.endsWith("Owner")) {
|
||||
String triggeringType = defParsed.substring(9);
|
||||
@@ -1073,6 +1081,10 @@ public class AbilityUtils {
|
||||
if (c instanceof Card) {
|
||||
o = ((Card) c).getOwner();
|
||||
}
|
||||
// For merged permanent
|
||||
if (c instanceof CardCollection) {
|
||||
o = ((CardCollection) c).get(0).getOwner();
|
||||
}
|
||||
}
|
||||
else {
|
||||
final String triggeringType = defParsed.substring(9);
|
||||
@@ -1225,7 +1237,7 @@ public class AbilityUtils {
|
||||
* <p>
|
||||
* getDefinedSpellAbilities.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param card
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param def
|
||||
@@ -1489,7 +1501,7 @@ public class AbilityUtils {
|
||||
* <p>
|
||||
* handleRemembering.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param sa
|
||||
* a SpellAbility object.
|
||||
*/
|
||||
@@ -1554,7 +1566,7 @@ public class AbilityUtils {
|
||||
* <p>
|
||||
* Parse non-mana X variables.
|
||||
* </p>
|
||||
*
|
||||
*
|
||||
* @param c
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param s
|
||||
@@ -1721,7 +1733,7 @@ public class AbilityUtils {
|
||||
final String payingMana = StringUtils.join(sa.getRootAbility().getPayingMana());
|
||||
final int num = sq[0].length() > 7 ? Integer.parseInt(sq[0].split("_")[1]) : 3;
|
||||
final boolean adamant = StringUtils.countMatches(payingMana, MagicColor.toShortString(sq[1])) >= num;
|
||||
return CardFactoryUtil.doXMath(Integer.parseInt(sq[adamant ? 2 : 3]), expr, c);
|
||||
return CardFactoryUtil.doXMath(Integer.parseInt(sq[adamant ? 2 : 3]), expr, c);
|
||||
}
|
||||
|
||||
if (l[0].startsWith("LastStateBattlefield")) {
|
||||
@@ -1974,7 +1986,7 @@ public class AbilityUtils {
|
||||
}
|
||||
return cause;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static SpellAbility addSpliceEffects(final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
@@ -2034,7 +2046,7 @@ public class AbilityUtils {
|
||||
|
||||
if (spliceCost == null)
|
||||
return;
|
||||
|
||||
|
||||
SpellAbility firstSpell = c.getFirstSpellAbility();
|
||||
Map<String, String> params = Maps.newHashMap(firstSpell.getMapParams());
|
||||
ApiType api = AbilityRecordType.getRecordType(params).getApiTypeOf(params);
|
||||
|
||||
@@ -105,6 +105,7 @@ public enum ApiType {
|
||||
MultiplyCounter (CountersMultiplyEffect.class),
|
||||
MustAttack (MustAttackEffect.class),
|
||||
MustBlock (MustBlockEffect.class),
|
||||
Mutate (MutateEffect.class),
|
||||
NameCard (ChooseCardNameEffect.class),
|
||||
NoteCounters (CountersNoteEffect.class),
|
||||
PeekAndReveal (PeekAndRevealEffect.class),
|
||||
|
||||
@@ -726,12 +726,24 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
hostCard.addRemembered(meld);
|
||||
}
|
||||
}
|
||||
if (gameCard.hasMergedCard()) {
|
||||
for (final Card c : gameCard.getMergedCards()) {
|
||||
if (c == gameCard) continue;
|
||||
hostCard.addRemembered(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (forget != null) {
|
||||
hostCard.removeRemembered(movedCard);
|
||||
}
|
||||
if (imprint != null) {
|
||||
hostCard.addImprintedCard(movedCard);
|
||||
if (gameCard.hasMergedCard()) {
|
||||
for (final Card c : gameCard.getMergedCards()) {
|
||||
if (c == gameCard) continue;
|
||||
hostCard.addImprintedCard(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1280,6 +1292,12 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
source.addRemembered(meld);
|
||||
}
|
||||
}
|
||||
if (c.hasMergedCard()) {
|
||||
for (final Card card : c.getMergedCards()) {
|
||||
if (card == c) continue;
|
||||
source.addRemembered(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (forget) {
|
||||
source.removeRemembered(movedCard);
|
||||
@@ -1287,6 +1305,12 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
||||
// for imprinted since this doesn't use Target
|
||||
if (imprint) {
|
||||
source.addImprintedCard(movedCard);
|
||||
if (c.hasMergedCard()) {
|
||||
for (final Card card : c.getMergedCards()) {
|
||||
if (card == c) continue;
|
||||
source.addImprintedCard(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package forge.game.ability.effects;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.card.*;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Localizer;
|
||||
|
||||
public class MutateEffect extends SpellAbilityEffect {
|
||||
|
||||
@Override
|
||||
public void resolve(SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Player p = host.getOwner();
|
||||
final Game game = host.getGame();
|
||||
// 111.11. A copy of a permanent spell becomes a token as it resolves.
|
||||
// The token has the characteristics of the spell that became that token.
|
||||
// The token is not “created” for the purposes of any replacement effects or triggered abilities that refer to creating a token.
|
||||
if (host.isCopiedSpell()) {
|
||||
host.setCopiedSpell(false);
|
||||
host.setToken(true);
|
||||
}
|
||||
|
||||
final List<GameObject> targets = getDefinedOrTargeted(sa, "Defined");
|
||||
final Card target = (Card)targets.get(0);
|
||||
|
||||
CardCollectionView view = CardCollection.getView(Lists.newArrayList(host, target));
|
||||
final Card topCard = host.getController().getController().chooseSingleEntityForEffect(
|
||||
view,
|
||||
sa,
|
||||
Localizer.getInstance().getMessage("lblChooseCreatureToBeTop"),
|
||||
false,
|
||||
new HashMap<>()
|
||||
);
|
||||
final boolean putOnTop = (topCard == host);
|
||||
|
||||
// There shouldn't be any mutate abilities, but for now.
|
||||
if (sa.isSpell()) {
|
||||
host.setController(p, 0);
|
||||
}
|
||||
|
||||
host.setMergedToCard(target);
|
||||
// If first time mutate, add target first.
|
||||
if (!target.hasMergedCard()) {
|
||||
target.addMergedCard(target);
|
||||
}
|
||||
if (putOnTop) {
|
||||
target.addMergedCardToTop(host);
|
||||
} else {
|
||||
target.addMergedCard(host);
|
||||
}
|
||||
|
||||
// First remove current mutated states
|
||||
if (target.getMutatedTimestamp() != -1) {
|
||||
target.removeCloneState(target.getMutatedTimestamp());
|
||||
}
|
||||
// Now add all abilities from bottom cards
|
||||
final Long ts = game.getNextTimestamp();
|
||||
target.setMutatedTimestamp(ts);
|
||||
if (topCard.getCurrentStateName() != CardStateName.FaceDown) {
|
||||
final CardCloneStates mutatedStates = CardFactory.getMutatedCloneStates(target, sa);
|
||||
target.addCloneState(mutatedStates, ts);
|
||||
}
|
||||
// Re-register triggers for target card
|
||||
game.getTriggerHandler().clearActiveTriggers(target, null);
|
||||
game.getTriggerHandler().registerActiveTrigger(target, false);
|
||||
|
||||
game.getAction().moveTo(p.getZone(ZoneType.Merged), host, sa);
|
||||
|
||||
host.setTapped(target.isTapped());
|
||||
host.setFlipped(target.isFlipped());
|
||||
target.setTimesMutated(target.getTimesMutated() + 1);
|
||||
target.updateTokenView();
|
||||
if (host.isCommander()) {
|
||||
host.getOwner().updateMergedCommanderInfo(target, host);
|
||||
target.updateCommanderView();
|
||||
}
|
||||
|
||||
game.getTriggerHandler().runTrigger(TriggerType.Mutates, AbilityKey.mapFromCard(target), false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -72,13 +72,45 @@ public class SetStateEffect extends SpellAbilityEffect {
|
||||
}
|
||||
|
||||
// facedown cards that are not Permanent, can't turn faceup there
|
||||
if ("TurnFace".equals(mode) && gameCard.isFaceDown() && gameCard.isInZone(ZoneType.Battlefield)
|
||||
&& !gameCard.getState(CardStateName.Original).getType().isPermanent()) {
|
||||
Card lki = CardUtil.getLKICopy(gameCard);
|
||||
lki.forceTurnFaceUp();
|
||||
game.getAction().reveal(new CardCollection(lki), lki.getOwner(), true, Localizer.getInstance().getMessage("lblFaceDownCardCantTurnFaceUp"));
|
||||
if ("TurnFace".equals(mode) && gameCard.isFaceDown() && gameCard.isInZone(ZoneType.Battlefield)) {
|
||||
if (gameCard.hasMergedCard()) {
|
||||
boolean hasNonPermanent = false;
|
||||
Card nonPermanentCard = null;
|
||||
for (final Card c : gameCard.getMergedCards()) {
|
||||
if (!c.getState(CardStateName.Original).getType().isPermanent()) {
|
||||
hasNonPermanent = true;
|
||||
nonPermanentCard = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasNonPermanent) {
|
||||
Card lki = CardUtil.getLKICopy(nonPermanentCard);
|
||||
lki.forceTurnFaceUp();
|
||||
game.getAction().reveal(new CardCollection(lki), lki.getOwner(), true, Localizer.getInstance().getMessage("lblFaceDownCardCantTurnFaceUp"));
|
||||
continue;
|
||||
}
|
||||
} else if (!gameCard.getState(CardStateName.Original).getType().isPermanent()) {
|
||||
Card lki = CardUtil.getLKICopy(gameCard);
|
||||
lki.forceTurnFaceUp();
|
||||
game.getAction().reveal(new CardCollection(lki), lki.getOwner(), true, Localizer.getInstance().getMessage("lblFaceDownCardCantTurnFaceUp"));
|
||||
|
||||
continue;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Merged faceup permanent that have double faced cards can't turn face down
|
||||
if ("TurnFace".equals(mode) && !gameCard.isFaceDown() && gameCard.isInZone(ZoneType.Battlefield)
|
||||
&& gameCard.hasMergedCard()) {
|
||||
boolean hasBackSide = false;
|
||||
for (final Card c : gameCard.getMergedCards()) {
|
||||
if (c.hasBackSide()) {
|
||||
hasBackSide = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasBackSide) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// for reasons it can't transform, skip
|
||||
|
||||
@@ -100,7 +100,7 @@ public class SubgameEffect extends SpellAbilityEffect {
|
||||
final FCollectionView<Player> players = subgame.getPlayers();
|
||||
final FCollectionView<Player> maingamePlayers = maingame.getPlayers();
|
||||
final List<ZoneType> outsideZones = Arrays.asList(ZoneType.Hand, ZoneType.Battlefield,
|
||||
ZoneType.Graveyard, ZoneType.Exile, ZoneType.Stack, ZoneType.Sideboard, ZoneType.Ante);
|
||||
ZoneType.Graveyard, ZoneType.Exile, ZoneType.Stack, ZoneType.Sideboard, ZoneType.Ante, ZoneType.Merged);
|
||||
|
||||
for (int i = 0; i < players.size(); i++) {
|
||||
final Player player = players.get(i);
|
||||
|
||||
@@ -98,9 +98,11 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
// cards attached or otherwise linked to this card
|
||||
private CardCollection hauntedBy, devouredCards, exploitedCards, delvedCards, convokedCards, imprintedCards, encodedCards;
|
||||
private CardCollection mustBlockCards, gainControlTargets, chosenCards, blockedThisTurn, blockedByThisTurn;
|
||||
private CardCollection mergedCards;
|
||||
|
||||
// if this card is attached or linked to something, what card is it currently attached to
|
||||
private Card encoding, cloneOrigin, haunting, effectSource, pairedWith, meldedWith;
|
||||
private Card mergedTo;
|
||||
|
||||
private SpellAbility effectSourceAbility = null;
|
||||
|
||||
@@ -183,6 +185,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
|
||||
private long bestowTimestamp = -1;
|
||||
private long transformedTimestamp = 0;
|
||||
private long mutatedTimestamp = -1;
|
||||
private int timesMutated = 0;
|
||||
private boolean tributed = false;
|
||||
private boolean embalmed = false;
|
||||
private boolean eternalized = false;
|
||||
@@ -547,18 +551,30 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
// and then any effect have it turn upface again and demand its former flip state to be restored
|
||||
// Proof: Morph cards never have ability that makes them flip, Ixidron does not suppose cards to be turned face up again,
|
||||
// Illusionary Mask affects cards in hand.
|
||||
CardStateName oldState = getCurrentStateName();
|
||||
if (mode.equals("Transform") && isDoubleFaced()) {
|
||||
if (mode.equals("Transform") && (isDoubleFaced() || hasMergedCard())) {
|
||||
if (!canTransform()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
backside = !backside;
|
||||
if (hasMergedCard()) {
|
||||
removeMutatedStates();
|
||||
}
|
||||
CardCollectionView cards = hasMergedCard() ? getMergedCards() : new CardCollection(this);
|
||||
boolean retResult = false;
|
||||
for (final Card c : cards) {
|
||||
if (!c.isDoubleFaced()) {
|
||||
continue;
|
||||
}
|
||||
c.backside = !c.backside;
|
||||
|
||||
boolean result = changeToState(backside ? CardStateName.Transformed : CardStateName.Original);
|
||||
boolean result = c.changeToState(c.backside ? CardStateName.Transformed : CardStateName.Original);
|
||||
retResult = retResult || result;
|
||||
}
|
||||
if (hasMergedCard()) {
|
||||
rebuildMutatedStates(cause);
|
||||
}
|
||||
|
||||
// do the Transform trigger there, it can also happen if the resulting state doesn't change
|
||||
|
||||
// Clear old dfc trigger from the trigger handler
|
||||
getGame().getTriggerHandler().clearActiveTriggers(this, null);
|
||||
getGame().getTriggerHandler().registerActiveTrigger(this, false);
|
||||
@@ -567,23 +583,35 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
getGame().getTriggerHandler().runTrigger(TriggerType.Transformed, runParams, false);
|
||||
incrementTransformedTimestamp();
|
||||
|
||||
return result;
|
||||
return retResult;
|
||||
|
||||
} else if (mode.equals("Flip") && isFlipCard()) {
|
||||
} else if (mode.equals("Flip") && (isFlipCard() || hasMergedCard())) {
|
||||
// 709.4. Flipping a permanent is a one-way process.
|
||||
if (isFlipped()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flipped = true;
|
||||
|
||||
// a facedown card does flip but the state doesn't change
|
||||
if (isFaceDown()) {
|
||||
return false;
|
||||
if (hasMergedCard()) {
|
||||
removeMutatedStates();
|
||||
}
|
||||
CardCollectionView cards = hasMergedCard() ? getMergedCards() : new CardCollection(this);
|
||||
boolean retResult = false;
|
||||
for (final Card c : cards) {
|
||||
c.flipped = true;
|
||||
// a facedown card does flip but the state doesn't change
|
||||
if (c.facedown) continue;
|
||||
|
||||
return changeToState(CardStateName.Flipped);
|
||||
boolean result = c.changeToState(CardStateName.Flipped);
|
||||
retResult = retResult || result;
|
||||
}
|
||||
if (retResult && hasMergedCard()) {
|
||||
rebuildMutatedStates(cause);
|
||||
game.getTriggerHandler().clearActiveTriggers(this, null);
|
||||
game.getTriggerHandler().registerActiveTrigger(this, false);
|
||||
}
|
||||
return retResult;
|
||||
} else if (mode.equals("TurnFace")) {
|
||||
CardStateName oldState = getCurrentStateName();
|
||||
if (oldState == CardStateName.Original || oldState == CardStateName.Flipped) {
|
||||
return turnFaceDown();
|
||||
} else if (isFaceDown()) {
|
||||
@@ -625,14 +653,26 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
}
|
||||
|
||||
public boolean turnFaceDown(boolean override) {
|
||||
if (override || !hasBackSide()) {
|
||||
facedown = true;
|
||||
if (setState(CardStateName.FaceDown, true)) {
|
||||
runFacedownCommands();
|
||||
return true;
|
||||
if (hasMergedCard()) {
|
||||
removeMutatedStates();
|
||||
}
|
||||
CardCollectionView cards = hasMergedCard() ? getMergedCards() : new CardCollection(this);
|
||||
boolean retResult = false;
|
||||
for (final Card c : cards) {
|
||||
if (override || !c.hasBackSide()) {
|
||||
c.facedown = true;
|
||||
if (c.setState(CardStateName.FaceDown, true)) {
|
||||
c.runFacedownCommands();
|
||||
retResult = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (hasMergedCard()) {
|
||||
rebuildMutatedStates(null);
|
||||
game.getTriggerHandler().clearActiveTriggers(this, null);
|
||||
game.getTriggerHandler().registerActiveTrigger(this, false);
|
||||
}
|
||||
return retResult;
|
||||
}
|
||||
|
||||
public boolean turnFaceDownNoUpdate() {
|
||||
@@ -653,22 +693,34 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result;
|
||||
if (isFlipped() && isFlipCard()) {
|
||||
result = setState(CardStateName.Flipped, true);
|
||||
} else {
|
||||
result = setState(CardStateName.Original, true);
|
||||
if (hasMergedCard()) {
|
||||
removeMutatedStates();
|
||||
}
|
||||
CardCollectionView cards = hasMergedCard() ? getMergedCards() : new CardCollection(this);
|
||||
boolean retResult = false;
|
||||
for (final Card c : cards) {
|
||||
boolean result;
|
||||
if (c.isFlipped() && c.isFlipCard()) {
|
||||
result = c.setState(CardStateName.Flipped, true);
|
||||
} else {
|
||||
result = c.setState(CardStateName.Original, true);
|
||||
}
|
||||
|
||||
facedown = false;
|
||||
updateStateForView(); //fixes cards with backside viewable
|
||||
// need to run faceup commands, currently
|
||||
// it does cleanup the modified facedown state
|
||||
if (result) {
|
||||
runFaceupCommands();
|
||||
c.facedown = false;
|
||||
c.updateStateForView(); //fixes cards with backside viewable
|
||||
// need to run faceup commands, currently
|
||||
// it does cleanup the modified facedown state
|
||||
if (result) {
|
||||
c.runFaceupCommands();
|
||||
}
|
||||
retResult = retResult || result;
|
||||
}
|
||||
|
||||
if (result && runTriggers) {
|
||||
if (hasMergedCard()) {
|
||||
rebuildMutatedStates(cause);
|
||||
game.getTriggerHandler().clearActiveTriggers(this, null);
|
||||
game.getTriggerHandler().registerActiveTrigger(this, false);
|
||||
}
|
||||
if (retResult && runTriggers) {
|
||||
// Run replacement effects
|
||||
getGame().getReplacementHandler().run(ReplacementType.TurnFaceUp, AbilityKey.mapFromAffected(this));
|
||||
|
||||
@@ -679,17 +731,33 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
getGame().getTriggerHandler().registerActiveTrigger(this, false);
|
||||
getGame().getTriggerHandler().runTrigger(TriggerType.TurnFaceUp, runParams, false);
|
||||
}
|
||||
return result;
|
||||
return retResult;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canTransform() {
|
||||
if (isFaceDown() || !isDoubleFaced()) {
|
||||
if (isFaceDown()) {
|
||||
return false;
|
||||
}
|
||||
Card transformCard = this;
|
||||
if (hasMergedCard()) {
|
||||
boolean hasTransformCard = false;
|
||||
for (final Card c : getMergedCards()) {
|
||||
if (c.isDoubleFaced()) {
|
||||
hasTransformCard = true;
|
||||
transformCard = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasTransformCard) {
|
||||
return false;
|
||||
}
|
||||
} else if (!isDoubleFaced()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CardStateName destState = backside ? CardStateName.Original : CardStateName.Transformed;
|
||||
CardStateName destState = transformCard.backside ? CardStateName.Original : CardStateName.Transformed;
|
||||
|
||||
// below only when in play
|
||||
if (!isInPlay()) {
|
||||
@@ -697,7 +765,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
}
|
||||
|
||||
// use Original State for the transform check
|
||||
if (!getOriginalState(destState).getType().isPermanent()) {
|
||||
if (!transformCard.getOriginalState(destState).getType().isPermanent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -814,7 +882,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
}
|
||||
|
||||
public boolean isCloned() {
|
||||
return !clonedStates.isEmpty();
|
||||
return !clonedStates.isEmpty() && clonedStates.lastEntry().getKey() != mutatedTimestamp;
|
||||
}
|
||||
|
||||
public final CardCollectionView getDevouredCards() {
|
||||
@@ -988,6 +1056,70 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
encoding = e;
|
||||
}
|
||||
|
||||
public final CardCollectionView getMergedCards() {
|
||||
return CardCollection.getView(mergedCards);
|
||||
}
|
||||
public final Card getTopMergedCard() {
|
||||
return mergedCards.get(0);
|
||||
}
|
||||
public final boolean hasMergedCard() {
|
||||
return FCollection.hasElements(mergedCards);
|
||||
}
|
||||
public final void addMergedCard(final Card c) {
|
||||
if (mergedCards == null) {
|
||||
mergedCards = new CardCollection();
|
||||
}
|
||||
mergedCards.add(c);
|
||||
}
|
||||
public final void addMergedCardToTop(final Card c) {
|
||||
mergedCards.add(0, c);
|
||||
}
|
||||
public final void removeMergedCard(final Card c) {
|
||||
mergedCards.remove(c);
|
||||
}
|
||||
public final void clearMergedCards() {
|
||||
mergedCards.clear();
|
||||
}
|
||||
|
||||
public final Card getMergedToCard() {
|
||||
return mergedTo;
|
||||
}
|
||||
public final void setMergedToCard(final Card c) {
|
||||
mergedTo = c;
|
||||
}
|
||||
public final boolean isMerged() {
|
||||
return getMergedToCard() != null;
|
||||
}
|
||||
|
||||
public final boolean isMutated() {
|
||||
return mutatedTimestamp != -1;
|
||||
}
|
||||
public final long getMutatedTimestamp() {
|
||||
return mutatedTimestamp;
|
||||
}
|
||||
public final void setMutatedTimestamp(final long t) {
|
||||
mutatedTimestamp = t;
|
||||
}
|
||||
|
||||
public final int getTimesMutated() {
|
||||
return timesMutated;
|
||||
}
|
||||
public final void setTimesMutated(final int t) {
|
||||
timesMutated = t;
|
||||
}
|
||||
|
||||
public final void removeMutatedStates() {
|
||||
if (getMutatedTimestamp() != -1) {
|
||||
removeCloneState(getMutatedTimestamp());
|
||||
}
|
||||
}
|
||||
public final void rebuildMutatedStates(final CardTraitBase sa) {
|
||||
if (getCurrentStateName() != CardStateName.FaceDown) {
|
||||
final CardCloneStates mutatedStates = CardFactory.getMutatedCloneStates(this, sa);
|
||||
addCloneState(mutatedStates, getMutatedTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
public final String getFlipResult(final Player flipper) {
|
||||
if (flipResult == null) {
|
||||
return null;
|
||||
@@ -1836,7 +1968,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt")
|
||||
|| keyword.startsWith("Transfigure") || keyword.startsWith("Aura swap")
|
||||
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")
|
||||
|| keyword.startsWith("Encore")) {
|
||||
|| keyword.startsWith("Encore") || keyword.startsWith("Mutate")) {
|
||||
// keyword parsing takes care of adding a proper description
|
||||
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
|
||||
sbLong.append(getName()).append(" can't be blocked ");
|
||||
@@ -2619,6 +2751,12 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
|
||||
// is this "Card" supposed to be a token?
|
||||
public final boolean isToken() {
|
||||
if (isInZone(ZoneType.Battlefield) && hasMergedCard()) {
|
||||
return getTopMergedCard().token;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
public final boolean isRealToken() {
|
||||
return token;
|
||||
}
|
||||
public final void setToken(boolean token0) {
|
||||
@@ -2626,6 +2764,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
token = token0;
|
||||
view.updateToken(this);
|
||||
}
|
||||
public final void updateTokenView() {
|
||||
view.updateToken(this);
|
||||
}
|
||||
|
||||
public final Card getCopiedPermanent() {
|
||||
return copiedPermanent;
|
||||
@@ -2644,7 +2785,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
}
|
||||
|
||||
public final boolean isFaceDown() {
|
||||
//return currentStateName == CardStateName.FaceDown;
|
||||
if (hasMergedCard()) {
|
||||
return getTopMergedCard().facedown;
|
||||
}
|
||||
return facedown;
|
||||
}
|
||||
|
||||
@@ -3356,7 +3499,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
|
||||
public final Card getCloner() {
|
||||
CardCloneStates clStates = getLastClonedState();
|
||||
if (clStates == null) {
|
||||
if (!isCloned() || clStates == null) {
|
||||
return null;
|
||||
}
|
||||
return clStates.getHost();
|
||||
@@ -6081,6 +6224,13 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
public boolean isCommander() {
|
||||
if (this.getMeldedWith() != null && this.getMeldedWith().isCommander())
|
||||
return true;
|
||||
if (isInZone(ZoneType.Battlefield) && hasMergedCard()) {
|
||||
for (final Card c : getMergedCards())
|
||||
if (c.isCommander) return true;
|
||||
}
|
||||
return isCommander;
|
||||
}
|
||||
public boolean isRealCommander() {
|
||||
return isCommander;
|
||||
}
|
||||
public void setCommander(boolean b) {
|
||||
@@ -6088,6 +6238,20 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
isCommander = b;
|
||||
view.updateCommander(this);
|
||||
}
|
||||
public void updateCommanderView() {
|
||||
view.updateCommander(this);
|
||||
}
|
||||
public Card getRealCommander() {
|
||||
if (isCommander)
|
||||
return this;
|
||||
if (this.getMeldedWith() != null && this.getMeldedWith().isCommander())
|
||||
return this.getMeldedWith();
|
||||
if (isInZone(ZoneType.Battlefield) && hasMergedCard()) {
|
||||
for (final Card c : getMergedCards())
|
||||
if (c.isCommander) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean canMoveToCommandZone() {
|
||||
return canMoveToCommandZone;
|
||||
|
||||
@@ -101,7 +101,7 @@ public class CardFactory {
|
||||
for (final Card o : in.getImprintedCards()) {
|
||||
out.addImprintedCard(o);
|
||||
}
|
||||
out.setCommander(in.isCommander());
|
||||
out.setCommander(in.isRealCommander());
|
||||
//out.setFaceDown(in.isFaceDown());
|
||||
|
||||
return out;
|
||||
@@ -820,4 +820,36 @@ public class CardFactory {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static CardCloneStates getMutatedCloneStates(final Card card, final CardTraitBase sa) {
|
||||
final Card top = card.getTopMergedCard();
|
||||
final CardStateName state = top.getCurrentStateName();
|
||||
final CardState ret = new CardState(card, state);
|
||||
if (top.isCloned()) {
|
||||
ret.copyFrom(top.getState(state, true), false);
|
||||
} else {
|
||||
ret.copyFrom(top.getOriginalState(state), false);
|
||||
}
|
||||
|
||||
boolean first = true;
|
||||
for (final Card c : card.getMergedCards()) {
|
||||
if (first) {
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
ret.addAbilitiesFrom(c.getCurrentState(), false);
|
||||
}
|
||||
|
||||
final CardCloneStates result = new CardCloneStates(top, sa);
|
||||
result.put(state, ret);
|
||||
|
||||
// For transformed card or melded card, also copy the original state to avoid crash
|
||||
if (state == CardStateName.Transformed || state == CardStateName.Meld) {
|
||||
final CardState ret1 = new CardState(card, CardStateName.Original);
|
||||
ret1.copyFrom(top.getState(CardStateName.Original, true), false);
|
||||
result.put(CardStateName.Original, ret1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // end class AbstractCardFactory
|
||||
|
||||
@@ -1319,6 +1319,9 @@ public class CardFactoryUtil {
|
||||
if (sq[0].contains("TimesPseudokicked")) {
|
||||
return doXMath(c.getPseudoKickerMagnitude(), m, c);
|
||||
}
|
||||
if (sq[0].contains("TimesMutated")) {
|
||||
return doXMath(c.getTimesMutated(), m, c);
|
||||
}
|
||||
|
||||
// Count$IfCastInOwnMainPhase.<numMain>.<numNotMain> // 7/10
|
||||
if (sq[0].contains("IfCastInOwnMainPhase")) {
|
||||
@@ -4273,6 +4276,22 @@ public class CardFactoryUtil {
|
||||
} else {
|
||||
sa.addAnnounceVar("Multikicker");
|
||||
}
|
||||
} else if (keyword.startsWith("Mutate")) {
|
||||
final String[] params = keyword.split(":");
|
||||
final String cost = params[1];
|
||||
|
||||
final StringBuilder sbMutate = new StringBuilder();
|
||||
sbMutate.append("SP$ Mutate | Cost$ ");
|
||||
sbMutate.append(cost);
|
||||
sbMutate.append(" | ValidTgts$ Creature.sharesOwnerWith+nonHuman");
|
||||
|
||||
final SpellAbility sa = AbilityFactory.getAbility(sbMutate.toString(), card);
|
||||
sa.setDescription("Mutate " + ManaCostParser.parse(cost) +
|
||||
" (" + inst.getReminderText() + ")");
|
||||
sa.setStackDescription("Mutate - " + card.getName());
|
||||
sa.setAlternativeCost(AlternativeCost.Mutate);
|
||||
sa.setIntrinsic(intrinsic);
|
||||
inst.addSpellAbility(sa);
|
||||
} else if (keyword.startsWith("Ninjutsu")) {
|
||||
final String[] k = keyword.split(":");
|
||||
final String manacost = k[1];
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -68,7 +68,7 @@ public class CardState extends GameObject implements IHasSVars {
|
||||
private Map<String, String> sVars = Maps.newTreeMap();
|
||||
|
||||
private KeywordCollection cachedKeywords = new KeywordCollection();
|
||||
|
||||
|
||||
private CardRarity rarity = CardRarity.Unknown;
|
||||
private String setCode = CardEdition.UNKNOWN.getCode();
|
||||
|
||||
@@ -138,7 +138,7 @@ public class CardState extends GameObject implements IHasSVars {
|
||||
view.updateType(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public final void setCreatureTypes(Collection<String> ctypes) {
|
||||
if (type.setCreatureTypes(ctypes)) {
|
||||
view.updateType(this);
|
||||
@@ -563,6 +563,42 @@ public class CardState extends GameObject implements IHasSVars {
|
||||
}
|
||||
}
|
||||
|
||||
public final void addAbilitiesFrom(final CardState source, final boolean lki) {
|
||||
for (SpellAbility sa : source.manaAbilities) {
|
||||
if (sa.isIntrinsic()) {
|
||||
manaAbilities.add(sa.copy(card, lki));
|
||||
}
|
||||
}
|
||||
|
||||
for (SpellAbility sa : source.nonManaAbilities) {
|
||||
if (sa.isIntrinsic()) {
|
||||
nonManaAbilities.add(sa.copy(card, lki));
|
||||
}
|
||||
}
|
||||
|
||||
for (KeywordInterface k : source.intrinsicKeywords) {
|
||||
intrinsicKeywords.insert(k.copy(card, lki));
|
||||
}
|
||||
|
||||
for (Trigger tr : source.triggers) {
|
||||
if (tr.isIntrinsic()) {
|
||||
triggers.add(tr.copy(card, lki));
|
||||
}
|
||||
}
|
||||
|
||||
for (ReplacementEffect re : source.replacementEffects) {
|
||||
if (re.isIntrinsic()) {
|
||||
replacementEffects.add(re.copy(card, lki));
|
||||
}
|
||||
}
|
||||
|
||||
for (StaticAbility sa : source.staticAbilities) {
|
||||
if (sa.isIntrinsic()) {
|
||||
staticAbilities.add(sa.copy(card, lki));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CardState copy(final Card host, CardStateName name, final boolean lki) {
|
||||
CardState result = new CardState(host, name);
|
||||
result.copyFrom(this, lki);
|
||||
@@ -580,11 +616,11 @@ public class CardState extends GameObject implements IHasSVars {
|
||||
public String getSetCode() {
|
||||
return setCode;
|
||||
}
|
||||
|
||||
|
||||
public CardTypeView getTypeWithChanges() {
|
||||
return getType().getTypeWithChanges(card.getChangedCardTypes());
|
||||
}
|
||||
|
||||
|
||||
public void setSetCode(String setCode0) {
|
||||
setCode = setCode0;
|
||||
view.updateSetCode(this);
|
||||
|
||||
@@ -714,6 +714,11 @@ public class CardView extends GameEntityView {
|
||||
sb.append("\r\nCloned by: ").append(cloner);
|
||||
}
|
||||
|
||||
String mergedCards = get(TrackableProperty.MergedCards);
|
||||
if (!mergedCards.isEmpty()) {
|
||||
sb.append("\r\n\r\nMerged Cards: ").append(mergedCards);
|
||||
}
|
||||
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
@@ -781,6 +786,18 @@ public class CardView extends GameEntityView {
|
||||
//CardStateView cloner = CardView.getState(c, CardStateName.Cloner);
|
||||
set(TrackableProperty.Cloner, cloner == null ? null : cloner.getName() + " (" + cloner.getId() + ")");
|
||||
|
||||
if (c.hasMergedCard()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
CardCollectionView mergedCards = c.getMergedCards();
|
||||
for (int i = 1; i < mergedCards.size(); i++) {
|
||||
final Card card = mergedCards.get(i);
|
||||
if (i > 1) sb.append(", ");
|
||||
sb.append(card.getOriginalState(card.getCurrentStateName()).getName());
|
||||
sb.append(" (").append(card.getId()).append(")");
|
||||
}
|
||||
set(TrackableProperty.MergedCards, sb.toString());
|
||||
}
|
||||
|
||||
CardState currentState = c.getCurrentState();
|
||||
if (isSplitCard) {
|
||||
set(TrackableProperty.LeftSplitState, c.getState(CardStateName.LeftSplit).getView());
|
||||
|
||||
@@ -102,6 +102,7 @@ public enum Keyword {
|
||||
MODULAR("Modular", Modular.class, false, "This creature enters the battlefield with {%d:+1/+1 counter} on it. When it dies, you may put its +1/+1 counters on target artifact creature."),
|
||||
MORPH("Morph", KeywordWithCost.class, false, "You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its morph cost."),
|
||||
MULTIKICKER("Multikicker", KeywordWithCost.class, false, "You may pay an additional %s any number of times as you cast this spell."),
|
||||
MUTATE("Mutate", KeywordWithCost.class, true, "If you cast this spell for its mutate cost, put it over or under target non-Human creature you own. They mutate into the creature on top plus all abilities from under it."),
|
||||
MYRIAD("Myriad", SimpleKeyword.class, false, "Whenever this creature attacks, for each opponent other than defending player, you may create a token that's a copy of this creature that's tapped and attacking that player or a planeswalker they control. Exile the tokens at end of combat."),
|
||||
NINJUTSU("Ninjutsu", Ninjutsu.class, false, "%s, Return an unblocked attacker you control to hand: Put this card onto the battlefield from your %s tapped and attacking."),
|
||||
OUTLAST("Outlast", KeywordWithCost.class, false, "%s, {T}: Put a +1/+1 counter on this creature. Outlast only as a sorcery."),
|
||||
|
||||
@@ -78,7 +78,7 @@ import java.util.concurrent.ConcurrentSkipListMap;
|
||||
public class Player extends GameEntity implements Comparable<Player> {
|
||||
public static final List<ZoneType> ALL_ZONES = Collections.unmodifiableList(Arrays.asList(ZoneType.Battlefield,
|
||||
ZoneType.Library, ZoneType.Graveyard, ZoneType.Hand, ZoneType.Exile, ZoneType.Command, ZoneType.Ante,
|
||||
ZoneType.Sideboard, ZoneType.PlanarDeck, ZoneType.SchemeDeck, ZoneType.Subgame));
|
||||
ZoneType.Sideboard, ZoneType.PlanarDeck, ZoneType.SchemeDeck, ZoneType.Merged, ZoneType.Subgame));
|
||||
|
||||
private final Map<Card, Integer> commanderDamage = Maps.newHashMap();
|
||||
|
||||
@@ -610,8 +610,14 @@ public class Player extends GameEntity implements Comparable<Player> {
|
||||
&& !this.getGame().getRules().hasAppliedVariant(GameType.Oathbreaker)
|
||||
&& !this.getGame().getRules().hasAppliedVariant(GameType.TinyLeaders)
|
||||
&& !this.getGame().getRules().hasAppliedVariant(GameType.Brawl)) {
|
||||
commanderDamage.put(source, getCommanderDamage(source) + amount);
|
||||
// In case that commander is merged permanent, get the real commander card
|
||||
final Card realCommander = source.getRealCommander();
|
||||
int damage = getCommanderDamage(realCommander) + amount;
|
||||
commanderDamage.put(realCommander, damage);
|
||||
view.updateCommanderDamage(this);
|
||||
if (realCommander != source) {
|
||||
view.updateMergedCommanderDamage(source, realCommander);
|
||||
}
|
||||
}
|
||||
|
||||
int old = assignedDamage.containsKey(source) ? assignedDamage.get(source) : 0;
|
||||
@@ -2886,6 +2892,11 @@ public class Player extends GameEntity implements Comparable<Player> {
|
||||
getGame().fireEvent(new GameEventPlayerStatsChanged(this, false));
|
||||
}
|
||||
|
||||
public void updateMergedCommanderInfo(Card target, Card commander) {
|
||||
getView().updateMergedCommanderCast(this, target, commander);
|
||||
getView().updateMergedCommanderDamage(target, commander);
|
||||
}
|
||||
|
||||
public int getTotalCommanderCast() {
|
||||
int result = 0;
|
||||
for (Integer i : commanderCast.values()) {
|
||||
@@ -3021,7 +3032,7 @@ public class Player extends GameEntity implements Comparable<Player> {
|
||||
Set<CardStateName> cardStateNames = c.isSplitCard() ? EnumSet.of(CardStateName.LeftSplit, CardStateName.RightSplit) : EnumSet.of(CardStateName.Original);
|
||||
Set<ManaCostShard> coloredManaSymbols = new HashSet<>();
|
||||
Set<Integer> genericManaSymbols = new HashSet<>();
|
||||
|
||||
|
||||
for (final CardStateName cardStateName : cardStateNames) {
|
||||
final ManaCost manaCost = c.getState(cardStateName).getManaCost();
|
||||
for (final ManaCostShard manaSymbol : manaCost) {
|
||||
|
||||
@@ -334,6 +334,15 @@ public class PlayerView extends GameEntityView {
|
||||
}
|
||||
set(TrackableProperty.CommanderDamage, map);
|
||||
}
|
||||
void updateMergedCommanderDamage(Card card, Card commander) {
|
||||
// Add commander damage to top card for card view panel info
|
||||
for (final PlayerView p : Iterables.concat(Collections.singleton(this), getOpponents())) {
|
||||
Map<Integer, Integer> map = p.get(TrackableProperty.CommanderDamage);
|
||||
if (map == null) continue;
|
||||
Integer damage = map.get(commander.getId());
|
||||
map.put(card.getId(), damage);
|
||||
}
|
||||
}
|
||||
|
||||
public int getCommanderCast(CardView commander) {
|
||||
Map<Integer, Integer> map = get(TrackableProperty.CommanderCast);
|
||||
@@ -351,6 +360,15 @@ public class PlayerView extends GameEntityView {
|
||||
set(TrackableProperty.CommanderCast, map);
|
||||
}
|
||||
|
||||
void updateMergedCommanderCast(Player p, Card target, Card commander) {
|
||||
Map<Integer, Integer> map = get(TrackableProperty.CommanderCast);
|
||||
if (map == null) {
|
||||
map = Maps.newHashMap();
|
||||
}
|
||||
map.put(target.getId(), p.getCommanderCast(commander));
|
||||
set(TrackableProperty.CommanderCast, map);
|
||||
}
|
||||
|
||||
public PlayerView getMindSlaveMaster() {
|
||||
return get(TrackableProperty.MindSlaveMaster);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public enum AlternativeCost {
|
||||
Flashback,
|
||||
Foretold,
|
||||
Madness,
|
||||
Mutate,
|
||||
Offering,
|
||||
Outlast, // ActivatedAbility
|
||||
Prowl,
|
||||
|
||||
@@ -1316,6 +1316,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
|
||||
return isAlternativeCost(AlternativeCost.Madness);
|
||||
}
|
||||
|
||||
public final boolean isMutate() {
|
||||
return isAlternativeCost(AlternativeCost.Mutate);
|
||||
}
|
||||
|
||||
public final boolean isProwl() {
|
||||
return isAlternativeCost(AlternativeCost.Prowl);
|
||||
}
|
||||
|
||||
@@ -121,6 +121,11 @@ public class TargetChoices extends ForwardingList<GameObject> implements Cloneab
|
||||
return Iterables.getFirst(getTargetSpells(), null);
|
||||
}
|
||||
|
||||
public final void replaceTargetCard(final Card old, final CardCollectionView replace) {
|
||||
targets.remove(old);
|
||||
targets.addAll(replace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetChoices clone() {
|
||||
TargetChoices tc = new TargetChoices();
|
||||
|
||||
@@ -195,7 +195,7 @@ public class TriggerChangesZone extends Trigger {
|
||||
// TODO use better way to always copy both Card and CardLKI
|
||||
if ("Battlefield".equals(getParam("Origin"))) {
|
||||
sa.setTriggeringObject(AbilityKey.Card, runParams.get(AbilityKey.CardLKI));
|
||||
sa.setTriggeringObject(AbilityKey.NewCard, CardUtil.getLKICopy((Card)runParams.get(AbilityKey.Card)));
|
||||
sa.setTriggeringObject(AbilityKey.NewCard, runParams.get(AbilityKey.Card));
|
||||
} else {
|
||||
sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import forge.game.ability.ApiType;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardZoneTable;
|
||||
@@ -471,10 +472,35 @@ public class TriggerHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void runSingleTrigger(final Trigger regtrig, final Map<AbilityKey, Object> runParams) {
|
||||
// If the runParams contains MergedCards, it is called from GameAction.changeZone()
|
||||
if (runParams.get(AbilityKey.MergedCards) != null) {
|
||||
// Check if the trigger cares the origin is from battlefield
|
||||
Card original = (Card) runParams.get(AbilityKey.Card);
|
||||
CardCollection mergedCards = (CardCollection) runParams.get(AbilityKey.MergedCards);
|
||||
mergedCards.set(mergedCards.indexOf(original), original);
|
||||
Map<AbilityKey, Object> newParams = AbilityKey.mapFromCard(original);
|
||||
newParams.putAll(runParams);
|
||||
if ("Battlefield".equals(regtrig.getParam("Origin"))) {
|
||||
// If yes, only trigger once
|
||||
newParams.put(AbilityKey.Card, mergedCards);
|
||||
runSingleTriggerInternal(regtrig, newParams);
|
||||
} else {
|
||||
// Else, trigger for each merged components
|
||||
for (final Card c : mergedCards) {
|
||||
newParams.put(AbilityKey.Card, c);
|
||||
runSingleTriggerInternal(regtrig, newParams);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runSingleTriggerInternal(regtrig, runParams);
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the conditions are right for a single trigger to go off, and
|
||||
// runs it if so.
|
||||
// Return true if the trigger went off, false otherwise.
|
||||
private void runSingleTrigger(final Trigger regtrig, final Map<AbilityKey, Object> runParams) {
|
||||
private void runSingleTriggerInternal(final Trigger regtrig, final Map<AbilityKey, Object> runParams) {
|
||||
|
||||
// All tests passed, execute ability.
|
||||
if (regtrig instanceof TriggerTapsForMana) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package forge.game.trigger;
|
||||
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import java.util.*;
|
||||
|
||||
public class TriggerMutates extends Trigger {
|
||||
public TriggerMutates(final Map<String, String> params, final Card host, final boolean intrinsic) {
|
||||
super(params, host, intrinsic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performTest(Map<AbilityKey, Object> runParams) {
|
||||
if (hasParam("ValidCard")) {
|
||||
return matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","),
|
||||
this.getHostCard());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTriggeringObjects(SpellAbility sa, Map<AbilityKey, Object> runParams) {
|
||||
sa.setTriggeringObject(AbilityKey.Card, runParams.get(AbilityKey.Card));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImportantStackObjects(SpellAbility sa) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Mutates").append(": ").append(sa.getTriggeringObject(AbilityKey.Card));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ public enum TriggerType {
|
||||
LifeGained(TriggerLifeGained.class),
|
||||
LifeLost(TriggerLifeLost.class),
|
||||
LosesGame(TriggerLosesGame.class),
|
||||
Mutates(TriggerMutates.class),
|
||||
NewGame(TriggerNewGame.class),
|
||||
PayCumulativeUpkeep(TriggerPayCumulativeUpkeep.class),
|
||||
PayEcho(TriggerPayEcho.class),
|
||||
|
||||
@@ -470,6 +470,12 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
|
||||
first.setActivatingPlayer(sa.getActivatingPlayer());
|
||||
game.fireEvent(new GameEventCardStatsChanged(source));
|
||||
AbilityUtils.resolve(first);
|
||||
} else if (sa.isMutate()) {
|
||||
SpellAbility first = source.getFirstSpellAbility();
|
||||
// need to set activating player
|
||||
first.setActivatingPlayer(sa.getActivatingPlayer());
|
||||
game.fireEvent(new GameEventCardStatsChanged(source));
|
||||
AbilityUtils.resolve(first);
|
||||
} else {
|
||||
// TODO: Spell fizzles, what's the best way to alert player?
|
||||
Log.debug(source.getName() + " ability fizzles.");
|
||||
|
||||
@@ -22,6 +22,7 @@ public enum ZoneType {
|
||||
Stack(false, "lblStackZone"),
|
||||
Sideboard(true, "lblSideboardZone"),
|
||||
Ante(false, "lblAnteZone"),
|
||||
Merged(false, "lblBattlefieldZone"),
|
||||
SchemeDeck(true, "lblSchemeDeckZone"),
|
||||
PlanarDeck(true, "lblPlanarDeckZone"),
|
||||
Subgame(true, "lblSubgameZone"),
|
||||
|
||||
@@ -34,6 +34,7 @@ public enum TrackableProperty {
|
||||
Cloned(TrackableTypes.BooleanType),
|
||||
FlipCard(TrackableTypes.BooleanType),
|
||||
SplitCard(TrackableTypes.BooleanType),
|
||||
MergedCards(TrackableTypes.StringType),
|
||||
|
||||
Attacking(TrackableTypes.BooleanType),
|
||||
Blocking(TrackableTypes.BooleanType),
|
||||
|
||||
9
forge-gui/res/cardsfolder/d/dreamtail_heron.txt
Normal file
9
forge-gui/res/cardsfolder/d/dreamtail_heron.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
Name:Dreamtail Heron
|
||||
ManaCost:4 U
|
||||
Types:Creature Elemental Bird
|
||||
PT:3/4
|
||||
K:Mutate:3 U
|
||||
K:Flying
|
||||
T:Mode$ Mutates | ValidCard$ Card.Self | Execute$ TrigDraw | TriggerDescription$ Whenever this creature mutates, draw a card.
|
||||
SVar:TrigDraw:DB$ Draw | NumCards$ 1
|
||||
Oracle:Mutate {3}{U} (If you cast this spell for its mutate cost, put it over or under target non-Human creature you own. They mutate into the creature on top plus all abilities from under it.)\nFlying\nWhenever this creature mutates, draw a card.
|
||||
11
forge-gui/res/cardsfolder/i/insatiable_hemophage.txt
Normal file
11
forge-gui/res/cardsfolder/i/insatiable_hemophage.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Name:Insatiable Hemophage
|
||||
ManaCost:3 B
|
||||
Types:Creature Nightmare
|
||||
PT:3/3
|
||||
K:Mutate:2 B
|
||||
K:Deathtouch
|
||||
T:Mode$ Mutates | ValidCard$ Card.Self | Execute$ TrigLoseLife | TriggerDescription$ Whenever this creature mutates, each opponent loses X life and you gain X life, where X is the number of times this creature has mutated.
|
||||
SVar:TrigLoseLife:DB$ LoseLife | Defined$ Opponent | LifeAmount$ X | References$ X | SubAbility$ DBGainLife
|
||||
SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ X | References$ X
|
||||
SVar:X:Count$TimesMutated
|
||||
Oracle:Mutate {2}{B} (If you cast this spell for its mutate cost, put it over or under target non-Human creature you own. They mutate into the creature on top plus all abilities from under it.)\nDeathtouch\nWhenever this creature mutates, each opponent loses X life and you gain X life, where X is the number of times this creature has mutated.
|
||||
7
forge-gui/res/cardsfolder/p/parcelbeast.txt
Normal file
7
forge-gui/res/cardsfolder/p/parcelbeast.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Name:Parcelbeast
|
||||
ManaCost:2 G U
|
||||
Types:Creature Elemental Beast
|
||||
PT:2/4
|
||||
K:Mutate:G U
|
||||
A:AB$ Dig | Cost$ 1 T | DigNum$ 1 | ChangeNum$ 1 | ChangeValid$ Land | Optional$ True | DestinationZone$ Battlefield | DestinationZone2$ Hand | StackDescription$ SpellDescription | SpellDescription$ Look at the top card of your library. If it's a land card, you may put it onto the battlefield. If you don't put the card onto the battlefield, put it into your hand.
|
||||
Oracle:Mutate {G}{U} (If you cast this spell for its mutate cost, put it over or under target non-Human creature you own. They mutate into the creature on top plus all abilities from under it.)\n{1}, {T}: Look at the top card of your library. If it's a land card, you may put it onto the battlefield. If you don't put the card onto the battlefield, put it into your hand.
|
||||
10
forge-gui/res/cardsfolder/p/pollywog_symbiote.txt
Normal file
10
forge-gui/res/cardsfolder/p/pollywog_symbiote.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Name:Pollywog Symbiote
|
||||
ManaCost:1 U
|
||||
Types:Creature Frog
|
||||
PT:1/3
|
||||
S:Mode$ ReduceCost | ValidCard$ Creature.withMutate | Type$ Spell | Activator$ You | Amount$ 1 | Description$ Each creature spell you cast costs {1} less to cast if it has mutate.
|
||||
T:Mode$ SpellCast | ValidCard$ Creature.withMutate | ValidActivatingPlayer$ You | Execute$ TrigLoot | TriggerZones$ Battlefield | TriggerDescription$ Whenever you cast a creature spell, if it has mutate, draw a card, then discard a card.
|
||||
SVar:TrigLoot:DB$ Draw | Defined$ You | NumCards$ 1 | SubAbility$ DBDiscard
|
||||
SVar:DBDiscard:DB$ Discard | Defined$ You | NumCards$ 1 | Mode$ TgtChoose
|
||||
SVar:BuffedBy:Card.withMutate
|
||||
Oracle:Each creature spell you cast costs {1} less to cast if it has mutate.\nWhenever you cast a creature spell, if it has mutate, draw a card, then discard a card.
|
||||
9
forge-gui/res/cardsfolder/v/vulpikeet.txt
Normal file
9
forge-gui/res/cardsfolder/v/vulpikeet.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
Name:Vulpikeet
|
||||
ManaCost:3 W
|
||||
Types:Creature Fox Bird
|
||||
PT:2/3
|
||||
K:Mutate:2 W
|
||||
K:Flying
|
||||
T:Mode$ Mutates | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever this creature mutates, put a +1/+1 counter on it.
|
||||
SVar:TrigPutCounter:DB$ PutCounter | Defined$ TriggeredCardLKICopy | CounterType$ P1P1 | CounterNum$ 1
|
||||
Oracle:Mutate {2}{W} (If you cast this spell for its mutate cost, put it over or under target non-Human creature you own. They mutate into the creature on top plus all abilities from under it.)\nFlying\nWhenever this creature mutates, put a +1/+1 counter on it.
|
||||
@@ -1236,6 +1236,7 @@ lblPutCardOnTopOrBottomLibrary=Lege {0} auf oder unter deine Bibliothek?
|
||||
lblChooseOrderCardsPutIntoLibrary=Wähle die Reihenfolge der Karten, in der sie in die Bibliothek gelegt werden
|
||||
lblClosestToTop=Zuoberst
|
||||
lblChooseOrderCardsPutOntoBattlefield=Wähle die Reihenfolge der Karten, in der sie auf das Spielfeld gebracht werden
|
||||
lblChooseOrderCardsPutIntoExile=Choose order of cards to put into the exile zone
|
||||
lblPutFirst=Lege zuerst
|
||||
lblChooseOrderCardsPutIntoGraveyard=Wähle die Reihenfolge der Karten, in der sie in den Friedhof gelegt werden
|
||||
lblClosestToBottom=Zuunterst
|
||||
@@ -1864,6 +1865,8 @@ lblChooseCardToMeld=Wähle Karte zum Verschmelzen mit
|
||||
lblDoYouWantPutLibraryCardsTo=Lege Karte(n) von der Bibliothek nach
|
||||
#MultiplePilesEffect.java
|
||||
lblChooseCardsInTargetPile=Wähle Karten in Stapel {0}?
|
||||
#MutateEffect.java
|
||||
lblChooseCreatureToBeTop=Choose which creature to be the top
|
||||
#PeekAndRevealEffect.java
|
||||
lblRevealingCardFrom=Zeige Karten von
|
||||
lblRevealCardToOtherPlayers=Zeige die Karten den anderen Spielern?
|
||||
|
||||
@@ -1236,6 +1236,7 @@ lblPutCardOnTopOrBottomLibrary=Put {0} on the top or bottom of your library?
|
||||
lblChooseOrderCardsPutIntoLibrary=Choose order of cards to put into the library
|
||||
lblClosestToTop=Closest to top
|
||||
lblChooseOrderCardsPutOntoBattlefield=Choose order of cards to put onto the battlefield
|
||||
lblChooseOrderCardsPutIntoExile=Choose order of cards to put into the exile zone
|
||||
lblPutFirst=Put first
|
||||
lblChooseOrderCardsPutIntoGraveyard=Choose order of cards to put into the graveyard
|
||||
lblClosestToBottom=Closest to bottom
|
||||
@@ -1864,6 +1865,8 @@ lblChooseCardToMeld=Choose card to meld with
|
||||
lblDoYouWantPutLibraryCardsTo=Do you want to put card(s) from library to {0}?
|
||||
#MultiplePilesEffect.java
|
||||
lblChooseCardsInTargetPile=Choose cards in Pile {0}?
|
||||
#MutateEffect.java
|
||||
lblChooseCreatureToBeTop=Choose which creature to be the top
|
||||
#PeekAndRevealEffect.java
|
||||
lblRevealingCardFrom=Revealing cards from
|
||||
lblRevealCardToOtherPlayers=Reveal cards to other players?
|
||||
|
||||
@@ -1236,6 +1236,7 @@ lblPutCardOnTopOrBottomLibrary=¿Poner {0} en la parte superior o inferior de tu
|
||||
lblChooseOrderCardsPutIntoLibrary=Elige el orden de las cartas para poner en la biblioteca
|
||||
lblClosestToTop=Más cerca de la parte superior
|
||||
lblChooseOrderCardsPutOntoBattlefield=Elige el orden de las cartas que quieres poner en el campo de batalla
|
||||
lblChooseOrderCardsPutIntoExile=Choose order of cards to put into the exile zone
|
||||
lblPutFirst=Poner en primer lugar
|
||||
lblChooseOrderCardsPutIntoGraveyard=Elige el orden de las cartas para poner en el cementerio
|
||||
lblClosestToBottom=Más cerca de la parte inferior
|
||||
@@ -1864,6 +1865,8 @@ lblChooseCardToMeld=Elige una carta para fundirla con
|
||||
lblDoYouWantPutLibraryCardsTo=¿Quieres poner la(s) carta(s) de la biblioteca a {0}?
|
||||
#MultiplePilesEffect.java
|
||||
lblChooseCardsInTargetPile=¿Elegir las cartas en la Pila {0}?
|
||||
#MutateEffect.java
|
||||
lblChooseCreatureToBeTop=Choose which creature to be the top
|
||||
#PeekAndRevealEffect.java
|
||||
lblRevealingCardFrom=Mostrando las cartas de
|
||||
lblRevealCardToOtherPlayers=¿Mostrar las cartas a otros jugadores?
|
||||
|
||||
@@ -1236,6 +1236,7 @@ lblPutCardOnTopOrBottomLibrary=Metti %s nella parte superiore o inferiore della
|
||||
lblChooseOrderCardsPutIntoLibrary=Scegli l''ordine delle carte da mettere nel grimorio
|
||||
lblClosestToTop=Più vicino all''inizio
|
||||
lblChooseOrderCardsPutOntoBattlefield=Scegli l''ordine delle carte da mettere sul campo di battaglia
|
||||
lblChooseOrderCardsPutIntoExile=Choose order of cards to put into the exile zone
|
||||
lblPutFirst=Metti per primo
|
||||
lblChooseOrderCardsPutIntoGraveyard=Scegli l''ordine delle carte da mettere nel cimitero
|
||||
lblClosestToBottom=Più vicino al fondo
|
||||
@@ -1864,6 +1865,8 @@ lblChooseCardToMeld=Choose card to meld with
|
||||
lblDoYouWantPutLibraryCardsTo=Do you want to put card(s) from library to {0}?
|
||||
#MultiplePilesEffect.java
|
||||
lblChooseCardsInTargetPile=Choose cards in Pile {0}?
|
||||
#MutateEffect.java
|
||||
lblChooseCreatureToBeTop=Choose which creature to be the top
|
||||
#PeekAndRevealEffect.java
|
||||
lblRevealingCardFrom=Revealing cards from
|
||||
lblRevealCardToOtherPlayers=Reveal cards to other players?
|
||||
|
||||
@@ -1236,6 +1236,7 @@ lblPutCardOnTopOrBottomLibrary=将{0}放到牌库顶还是底?
|
||||
lblChooseOrderCardsPutIntoLibrary=选择要放入牌库中的牌的顺序
|
||||
lblClosestToTop=最接近顶部
|
||||
lblChooseOrderCardsPutOntoBattlefield=选择要放入战场中的牌的顺序
|
||||
lblChooseOrderCardsPutIntoExile=Choose order of cards to put into the exile zone
|
||||
lblPutFirst=放在最前
|
||||
lblChooseOrderCardsPutIntoGraveyard=选择要放入坟场中的牌的顺序
|
||||
lblClosestToBottom=最接近底部
|
||||
@@ -1864,6 +1865,8 @@ lblChooseCardToMeld=选择要融合的牌
|
||||
lblDoYouWantPutLibraryCardsTo=你想要从牌库中把牌放入{0}吗?
|
||||
#MultiplePilesEffect.java
|
||||
lblChooseCardsInTargetPile=选择堆{0}中的牌?
|
||||
#MutateEffect.java
|
||||
lblChooseCreatureToBeTop=Choose which creature to be the top
|
||||
#PeekAndRevealEffect.java
|
||||
lblRevealingCardFrom=展示牌自
|
||||
lblRevealCardToOtherPlayers=向其他玩家展示牌?
|
||||
|
||||
@@ -971,6 +971,9 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
|
||||
case Graveyard:
|
||||
choices = getGui().order(localizer.getMessage("lblChooseOrderCardsPutIntoGraveyard"), localizer.getMessage("lblClosestToBottom"), choices, null);
|
||||
break;
|
||||
case Exile:
|
||||
choices = getGui().order(localizer.getMessage("lblChooseOrderCardsPutIntoExile"), localizer.getMessage("lblPutFirst"), choices, null);
|
||||
break;
|
||||
case PlanarDeck:
|
||||
choices = getGui().order(localizer.getMessage("lblChooseOrderCardsPutIntoPlanarDeck"), localizer.getMessage("lblClosestToTop"), choices, null);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user