Merge branch 'mutate' into 'master'

Mutate

See merge request core-developers/forge!3772
This commit is contained in:
Sol
2021-02-13 20:13:37 +00:00
39 changed files with 853 additions and 81 deletions

View File

@@ -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)

View 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;
}
}

View File

@@ -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);

View File

@@ -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"),

View File

@@ -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);

View File

@@ -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),

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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];

View File

@@ -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);

View File

@@ -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());

View File

@@ -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."),

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -11,6 +11,7 @@ public enum AlternativeCost {
Flashback,
Foretold,
Madness,
Mutate,
Offering,
Outlast, // ActivatedAbility
Prowl,

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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),

View File

@@ -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.");

View File

@@ -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"),

View File

@@ -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),

View 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.

View 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.

View 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.

View 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.

View 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.

View File

@@ -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?

View File

@@ -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?

View File

@@ -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?

View File

@@ -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?

View File

@@ -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=向其他玩家展示牌?

View File

@@ -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;