1
0
Fork 0
mirror of https://github.com/correl/mage.git synced 2025-04-05 09:12:29 -09:00

* MorphAbility - Fixed copying a face-down creature (fixes ). Morph cards are indicated as playable now if you have the needed mana to play it by Morph. Cast of Morph spell is now colorless (fixes ).

This commit is contained in:
LevelX2 2014-09-23 17:01:09 +02:00
parent f9afd91209
commit 5b5344a1a0
9 changed files with 340 additions and 49 deletions
Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords
Mage/src/mage

View file

@ -0,0 +1,179 @@
/*
* Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of BetaSteward_at_googlemail.com.
*/
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author levelX2
*/
public class MorphTest extends CardTestPlayerBase {
/**
* Tests if a creature with Morph is cast normal, it behaves as normal creature
*
*/
@Test
public void testCastMoprhCreatureWithoutMorph() {
/*
Pine Walker
Creature - Elemental
5/5
Morph {4}{G} (You may cast this card face down as a 2/2 creature for . Turn it face up any time for its morph cost.)
Whenever Pine Walker or another creature you control is turned face up, untap that creature.
*/
addCard(Zone.HAND, playerA, "Pine Walker");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pine Walker");
setChoice(playerA, "No"); // cast it normal as 5/5
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Pine Walker", 1);
assertPowerToughness(playerA, "Pine Walker", 5, 5);
}
/**
* Cast the creature face down as a 2/2
*/
@Test
public void testCastFaceDown() {
addCard(Zone.HAND, playerA, "Pine Walker");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pine Walker");
setChoice(playerA, "Yes"); // cast it face down as 2/2 creature
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "", 1);
assertPowerToughness(playerA, "", 2, 2);
}
/**
* Test triggered turn face up ability of Pine Walker
*/
@Test
public void testCopyFaceDwonMorphCreature() {
addCard(Zone.HAND, playerA, "Pine Walker");
addCard(Zone.BATTLEFIELD, playerA, "Forest", 5);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pine Walker");
setChoice(playerA, "Yes"); // cast it face down as 2/2 creature
attack(3, playerA, "");
activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{4}{G}: Turn this face-down permanent face up.");
setStopAt(3, PhaseStep.END_TURN);
execute();
assertLife(playerB, 18);
assertPermanentCount(playerA, "", 0);
assertPermanentCount(playerA, "Pine Walker", 1);
assertPowerToughness(playerA, "Pine Walker", 5, 5);
assertTapped("Pine Walker", false);
}
/**
* Test that Morph creature do not trigger abilities with their face up attributes
*
*/
@Test
public void testMorphedRemovesAttributesCreature() {
// Ponyback Brigade {3}{R}{W}{B}
// Creature - Goblin Warrior
// 2/2
// When Ponyback Brigade enters the battlefield or is turned face up, put three 1/1 red Goblin creature tokens onto the battlefield.
// Morph {2}{R}{W}{B}(You may cast this card face down as a 2/2 creature for . Turn it face up any time for its morph cost.)
addCard(Zone.HAND, playerA, "Ponyback Brigade");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2);
addCard(Zone.BATTLEFIELD, playerB, "Soldier of the Pantheon", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ponyback Brigade");
setChoice(playerA, "Yes"); // cast it face down as 2/2 creature
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerB, 20); // and not 21
assertPermanentCount(playerA, "", 1);
assertPermanentCount(playerB, "Soldier of the Pantheon", 1);
}
/**
* Test to copy a morphed 2/2 creature
*
*/
@Test
public void testCopyAMorphedCreature() {
addCard(Zone.HAND, playerA, "Pine Walker");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
// Clever Impersonator {2}{U}{U}
// Creature - Shapeshifter
// 0/0
// You may have Clever Impersonator enter the battlefield as a copy of any nonland permanent on the battlefield.
addCard(Zone.HAND, playerB, "Clever Impersonator", 1);
addCard(Zone.BATTLEFIELD, playerB, "Island", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pine Walker");
setChoice(playerA, "Yes"); // cast it face down as 2/2 creature
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Clever Impersonator");
setChoice(playerB, "Yes"); // use to copy a nonland permanent
addTarget(playerB, ""); // Morphed creature
setStopAt(2, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerB, 20);
assertPermanentCount(playerA, "", 1);
assertPowerToughness(playerA, "", 2,2);
assertPermanentCount(playerB, "", 1);
assertPowerToughness(playerB, "", 2,2);
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of BetaSteward_at_googlemail.com.
*/
package mage.abilities.condition.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.cards.Card;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
*
* @author LevelX2
*/
public class FaceDownSourceCondition implements Condition {
private final static FaceDownSourceCondition fInstance = new FaceDownSourceCondition();
public static Condition getInstance() {
return fInstance;
}
@Override
public boolean apply(Game game, Ability source) {
MageObject mageObject = game.getObject(source.getSourceId());
if (mageObject != null) {
if (mageObject instanceof Permanent) {
return ((Permanent)mageObject).isFaceDown();
}
if (mageObject instanceof Card) {
return ((Card)mageObject).isFaceDown();
}
}
return false;
}
}

View file

@ -52,7 +52,7 @@ public class TwoOrMoreSpellsWereCastLastTurnCondition implements Condition {
return true;
}
}
// no one cast two or more spells this turn
// no one cast two or more spells last turn
return false;
}
}

View file

@ -70,6 +70,6 @@ public interface AlternativeSourceCosts {
* @param game
* @return
*/
String getCastMessageSuffix(Game game);
String getCastMessageSuffix(Game game);
}

View file

@ -41,6 +41,7 @@ import mage.abilities.costs.AlternativeCost2Impl;
import mage.abilities.costs.AlternativeSourceCosts;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.decorator.ConditionalTriggeredAbility;
import mage.abilities.effects.common.SacrificeSourceEffect;
@ -190,4 +191,11 @@ public class EvokeAbility extends StaticAbility implements AlternativeSourceCost
}
return sb.toString();
}
@Override
public Costs<Cost> getCosts() {
Costs<Cost> alterCosts = new CostsImpl<>();
alterCosts.addAll(evokeCosts);
return alterCosts;
}
}

View file

@ -30,7 +30,6 @@ package mage.abilities.keyword;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import mage.ObjectColor;
import mage.abilities.Ability;
@ -38,13 +37,13 @@ import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.common.TurnFaceUpAbility;
import mage.abilities.costs.AlternativeCost2;
import mage.abilities.costs.AlternativeCost2Impl;
import mage.abilities.costs.AlternativeSourceCosts;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.costs.mana.ManaCosts;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
@ -54,6 +53,7 @@ import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.Rarity;
import mage.constants.SubLayer;
import mage.constants.Zone;
import mage.game.Game;
@ -105,7 +105,7 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost
protected static final String ABILITY_KEYWORD = "Morph";
protected static final String REMINDER_TEXT = "(You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its morph cost.)";
protected String ruleText;
protected List<AlternativeCost2> alternateCosts = new LinkedList<>();
protected AlternativeCost2Impl alternateCosts = new AlternativeCost2Impl(ABILITY_KEYWORD, REMINDER_TEXT, new GenericManaCost(3));
// needed to check activation status, if card changes zone after casting it
private int zoneChangeCounter = 0;
@ -131,8 +131,7 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost
sb.append(REMINDER_TEXT);
ruleText = sb.toString();
alternateCosts.add(new AlternativeCost2Impl(ABILITY_KEYWORD, REMINDER_TEXT, new GenericManaCost(3)));
// alternateCosts.add(new AlternativeCost2Impl(ABILITY_KEYWORD, REMINDER_TEXT, new GenericManaCost(3)));
Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new BecomesFaceDownCreatureEffect(morphCosts));
ability.setRuleVisible(false);
card.addAbility(ability);
@ -141,13 +140,13 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost
public MorphAbility(final MorphAbility ability) {
super(ability);
this.alternateCosts.addAll(ability.alternateCosts);
this.zoneChangeCounter = ability.zoneChangeCounter;
this.ruleText = ability.ruleText;
this.alternateCosts = ability.alternateCosts.copy();
}
private static Costs createCosts(Cost cost) {
Costs costs = new CostsImpl();
private static Costs<Cost> createCosts(Cost cost) {
Costs<Cost> costs = new CostsImpl<>();
costs.add(cost);
return costs;
}
@ -158,9 +157,7 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost
}
public void resetMorph() {
for (AlternativeCost2 cost: alternateCosts) {
cost.reset();
}
alternateCosts.reset();
zoneChangeCounter = 0;
}
@ -168,11 +165,7 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost
public boolean isActivated(Ability ability, Game game) {
Card card = game.getCard(sourceId);
if (card != null && card.getZoneChangeCounter() <= zoneChangeCounter +1) {
for (AlternativeCost2 cost: alternateCosts) {
if(cost.isActivated(game)) {
return true;
}
}
return alternateCosts.isActivated(game);
}
return false;
}
@ -190,20 +183,27 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost
if (player != null && spell != null) {
this.resetMorph();
spell.setFaceDown(true); // so only the back is visible
for (AlternativeCost2 alternateCastingCost: alternateCosts) {
if (alternateCastingCost.canPay(ability, sourceId, controllerId, game) &&
player.chooseUse(Outcome.Benefit, new StringBuilder("Cast this card as a 2/2 face-down creature for ").append(alternateCastingCost.getText(true)).append(" ?").toString(), game)) {
activateMorph(alternateCastingCost, game);
if (alternateCosts.canPay(ability, sourceId, controllerId, game)) {
if (player.chooseUse(Outcome.Benefit, new StringBuilder("Cast this card as a 2/2 face-down creature for ").append(getCosts().getText()).append(" ?").toString(), game)) {
activateMorph(game);
// change mana costs
ability.getManaCostsToPay().clear();
ability.getCosts().clear();
for (Iterator it = ((Costs) alternateCastingCost).iterator(); it.hasNext();) {
for (Iterator it = this.alternateCosts.iterator(); it.hasNext();) {
Cost cost = (Cost) it.next();
if (cost instanceof ManaCosts) {
ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy());
if (cost instanceof ManaCost) {
ability.getManaCostsToPay().add((ManaCost)cost.copy());
} else {
ability.getCosts().add(cost.copy());
}
}
// change spell colors
ObjectColor spellColor = spell.getColor();
spellColor.setBlack(false);
spellColor.setRed(false);
spellColor.setGreen(false);
spellColor.setWhite(false);
spellColor.setBlue(false);
} else {
spell.setFaceDown(false);
}
@ -213,8 +213,8 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost
return isActivated(ability, game);
}
private void activateMorph(AlternativeCost2 cost, Game game) {
cost.activate();
private void activateMorph(Game game) {
alternateCosts.activate();
// remember zone change counter
if (zoneChangeCounter == 0) {
Card card = game.getCard(getSourceId());
@ -240,14 +240,31 @@ public class MorphAbility extends StaticAbility implements AlternativeSourceCost
public String getCastMessageSuffix(Game game) {
StringBuilder sb = new StringBuilder();
int position = 0;
for (AlternativeCost2 cost : alternateCosts) {
if (cost.isActivated(game)) {
sb.append(cost.getCastSuffixMessage(position));
++position;
}
}
sb.append(alternateCosts.getCastSuffixMessage(position));
return sb.toString();
}
@Override
@SuppressWarnings({"unchecked"})
public Costs<Cost> getCosts() {
return alternateCosts;
}
public static void setPermanentToMorph(Permanent permanent) {
permanent.getPower().initValue(2);
permanent.getToughness().initValue(2);
permanent.getAbilities().clear();
permanent.getColor().setColor(new ObjectColor());
permanent.setName("");
permanent.getCardType().clear();
permanent.getCardType().add(CardType.CREATURE);
permanent.getSubtype().clear();
permanent.getSupertype().clear();
permanent.getManaCost().clear();
permanent.setExpansionSetCode("KTK");
permanent.setRarity(Rarity.NA);
}
}
/**
@ -348,3 +365,4 @@ class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl implements Sour
}
}

View file

@ -93,6 +93,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import mage.abilities.keyword.MorphAbility;
public abstract class GameImpl implements Game, Serializable {
@ -1237,6 +1238,9 @@ public abstract class GameImpl implements Game, Serializable {
//getState().addCard(permanent);
permanent.reset(this);
if (copyFromPermanent.isMorphCard()) {
MorphAbility.setPermanentToMorph(permanent);
}
permanent.assignNewId();
if (copyFromPermanent.isTransformed()) {
TransformAbility.transform(permanent, copyFromPermanent.getSecondCardFace(), this);

View file

@ -1907,7 +1907,7 @@ public abstract class PlayerImpl implements Player, Serializable {
return result;
}
protected boolean canPlay(ActivatedAbility ability, ManaOptions available, Game game) {
protected boolean canPlay(ActivatedAbility ability, ManaOptions available, MageObject sourceObject, Game game) {
if (!(ability instanceof ManaAbility)) {
ActivatedAbility copy = ability.copy();
copy.setCheckPlayableMode(); // prevents from endless loops for asking player to use effects by checking this mode
@ -1956,6 +1956,20 @@ public abstract class PlayerImpl implements Player, Serializable {
return true;
}
}
if (!(sourceObject instanceof Permanent)) {
for (Ability alternateSourceCostsAbility : sourceObject.getAbilities()) {
// if cast for noMana no Alternative costs are allowed
if (alternateSourceCostsAbility instanceof AlternativeSourceCosts) {
if (((AlternativeSourceCosts)alternateSourceCostsAbility).isAvailable(ability, game)) {
if (alternateSourceCostsAbility.getCosts().canPay(ability, playerId, playerId, game)) {
return true;
}
}
}
}
}
}
return false;
}
@ -1971,14 +1985,19 @@ public abstract class PlayerImpl implements Player, Serializable {
if (hidden) {
for (Card card : hand.getUniqueCards(game)) {
for (ActivatedAbility ability : card.getAbilities().getActivatedAbilities(Zone.HAND)) {
if (ability instanceof PlayLandAbility) {
if (game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), ability.getSourceId(), playerId), ability, game, true)) {
break;
for (Ability ability : card.getAbilities()) {
if (ability instanceof ActivatedAbility) {
if (ability instanceof PlayLandAbility) {
if (game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), ability.getSourceId(), playerId), ability, game, true)) {
break;
}
}
if (canPlay((ActivatedAbility) ability, availableMana, card, game)) {
playable.add(ability);
}
}
if (canPlay(ability, availableMana, game)) {
playable.add(ability);
if (ability instanceof AlternativeSourceCosts) {
}
}
}
@ -1994,7 +2013,7 @@ public abstract class PlayerImpl implements Player, Serializable {
possible = true;
}
}
if (possible && canPlay(ability, availableMana, game)) {
if (possible && canPlay(ability, availableMana, card, game)) {
playable.add(ability);
}
}
@ -2036,7 +2055,7 @@ public abstract class PlayerImpl implements Player, Serializable {
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) {
for (ActivatedAbility ability : permanent.getAbilities().getActivatedAbilities(Zone.BATTLEFIELD)) {
if (!playableActivated.containsKey(ability.toString())) {
if (canPlay(ability, availableMana, game)) {
if (canPlay(ability, availableMana, permanent, game)) {
playableActivated.put(ability.toString(), ability);
}
}
@ -2049,7 +2068,7 @@ public abstract class PlayerImpl implements Player, Serializable {
MageObject object = game.getObject(this.getCommanderId());
if (object != null) {
for (ActivatedAbility ability : ((Commander) object).getAbilities().getActivatedAbilities(Zone.COMMAND)) {
if (canPlay(ability, availableMana, game)) {
if (canPlay(ability, availableMana, object, game)) {
playableActivated.put(ability.toString(), ability);
}
}
@ -2082,13 +2101,13 @@ public abstract class PlayerImpl implements Player, Serializable {
break;
}
}
if (canPlay(ability, available, game)) {
if (canPlay(ability, available, card, game)) {
playable.add(card.getId());
break;
}
}
for (ActivatedAbility ability : card.getAbilities().getActivatedAbilities(Zone.HAND)) {
if (!playable.contains(ability.getSourceId()) && canPlay(ability, available, game)) {
if (!playable.contains(ability.getSourceId()) && canPlay(ability, available, card, game)) {
playable.add(card.getId());
break;
}

View file

@ -70,9 +70,9 @@ public class CastSpellLastTurnWatcher extends Watcher {
if (playerId != null) {
Integer amount = amountOfSpellsCastOnCurrentTurn.get(playerId);
if (amount == null) {
amount = Integer.valueOf(1);
amount = 1;
} else {
amount = Integer.valueOf(amount+1);
amount = amount+1;
}
amountOfSpellsCastOnCurrentTurn.put(playerId, amount);
}
@ -98,7 +98,7 @@ public class CastSpellLastTurnWatcher extends Watcher {
public int getAmountOfSpellsPlayerCastOnCurrentTurn(UUID playerId) {
Integer value = amountOfSpellsCastOnCurrentTurn.get(playerId);
if (value != null) {
return value.intValue();
return value;
} else {
return 0;
}