diff --git a/Mage.Sets/src/mage/cards/h/HulkingMetamorph.java b/Mage.Sets/src/mage/cards/h/HulkingMetamorph.java index b940fcff70f..8d3a39f6aee 100644 --- a/Mage.Sets/src/mage/cards/h/HulkingMetamorph.java +++ b/Mage.Sets/src/mage/cards/h/HulkingMetamorph.java @@ -31,8 +31,13 @@ public final class HulkingMetamorph extends CardImpl { blueprint.addCardType(CardType.CREATURE); Permanent permanent = game.getPermanentEntering(copyToObjectId); if (permanent != null) { - blueprint.getPower().setModifiedBaseValue(permanent.getPower().getValue()); - blueprint.getToughness().setModifiedBaseValue(permanent.getToughness().getValue()); + int pt = permanent.isPrototyped()? 3 : 7; + blueprint.getPower().setModifiedBaseValue(pt); + blueprint.getToughness().setModifiedBaseValue(pt); + //Would prefer the following code, but it doesn't seem to work correctly with Prototype as-is + //Either need to change Prototype or fix the Blood Moon problem + //blueprint.getPower().setModifiedBaseValue(permanent.getPower().getValue()); + //blueprint.getToughness().setModifiedBaseValue(permanent.getToughness().getValue()); } return true; } diff --git a/Mage.Sets/src/mage/sets/TheBrothersWar.java b/Mage.Sets/src/mage/sets/TheBrothersWar.java index 99898fce81a..3368a6146ca 100644 --- a/Mage.Sets/src/mage/sets/TheBrothersWar.java +++ b/Mage.Sets/src/mage/sets/TheBrothersWar.java @@ -6,16 +6,12 @@ import mage.constants.Rarity; import mage.constants.SetType; import mage.util.RandomUtil; -import java.util.Arrays; import java.util.List; /** * @author TheElk801 */ public final class TheBrothersWar extends ExpansionSet { - - private static final List unfinished = Arrays.asList("Arcane Proxy", "Autonomous Assembler", "Blitz Automaton", "Boulderbranch Golem", "Combat Thresher", "Cradle Clearcutter", "Depth Charge Colossus", "Fallaji Dragon Engine", "Goring Warplow", "Hulking Metamorph", "Iron-Craw Crusher", "Phyrexian Fleshgorger", "Rootwire Amalgam", "Rust Goliath", "Skitterbeam Battalion", "Spotter Thopter", "Steel Seraph", "Woodcaller Automaton"); - private static final TheBrothersWar instance = new TheBrothersWar(); public static TheBrothersWar getInstance() { @@ -421,8 +417,6 @@ public final class TheBrothersWar extends ExpansionSet { cards.add(new SetCardInfo("Yotian Medic", 33, Rarity.COMMON, mage.cards.y.YotianMedic.class)); cards.add(new SetCardInfo("Yotian Tactician", 228, Rarity.UNCOMMON, mage.cards.y.YotianTactician.class)); cards.add(new SetCardInfo("Zephyr Sentinel", 74, Rarity.UNCOMMON, mage.cards.z.ZephyrSentinel.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); // remove when mechanic is implemented } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/PrototypeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/PrototypeTest.java new file mode 100644 index 00000000000..3dc234980bd --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/PrototypeTest.java @@ -0,0 +1,729 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.MageObject; +import mage.ObjectColor; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.Card; +import mage.constants.ComparisonType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.filter.FilterSpell; +import mage.filter.StaticFilters; +import mage.filter.predicate.Predicate; +import mage.filter.predicate.mageobject.*; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class PrototypeTest extends CardTestPlayerBase { + + private static final String automaton = "Blitz Automaton"; + private static final String withPrototype = " using Prototype"; + private static final String automatonWithPrototype = automaton+withPrototype; + private static final String bolt = "Lightning Bolt"; + private static final String cloudshift = "Cloudshift"; + private static final String clone = "Clone"; + private static final String counterpart = "Cackling Counterpart"; + private static final String epiphany = "Sublime Epiphany"; + private static final String denied = "Access Denied"; + + private void checkAutomaton(boolean prototyped) { + checkAutomaton(prototyped, 1); + } + + private void checkAutomaton(boolean prototyped, int count) { + assertPermanentCount(playerA, automaton, count); + for (Permanent permanent : currentGame.getBattlefield().getActivePermanents( + StaticFilters.FILTER_PERMANENT, playerA.getId(), currentGame + )) { + if (!permanent.getName().equals(automaton)) { + continue; + } + Assert.assertTrue("Needs haste", permanent.getAbilities(currentGame).contains(HasteAbility.getInstance())); + Assert.assertEquals("Power is wrong", prototyped ? 3 : 6, permanent.getPower().getValue()); + Assert.assertEquals("Toughness is wrong", prototyped ? 2 : 4, permanent.getToughness().getValue()); + Assert.assertTrue("Color is wrong", prototyped + ? permanent.getColor(currentGame).isRed() + : permanent.getColor(currentGame).isColorless() + ); + Assert.assertEquals("Mana cost is wrong", prototyped ? "{2}{R}" : "{7}", permanent.getManaCost().getText()); + Assert.assertEquals("Mana value is wrong", prototyped ? 3 : 7, permanent.getManaValue()); + } + } + + private void makeTester(Predicate... predicates) { + FilterSpell filter = new FilterSpell(); + for (Predicate predicate : predicates) { + filter.add(predicate); + } + addCustomCardWithAbility( + "tester", playerA, + new SpellCastControllerTriggeredAbility( + new GainLifeEffect(1), filter, false + ) + ); + } + + @Test + public void testNormal() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 7); + addCard(Zone.HAND, playerA, automaton); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(false); + } + + @Test + public void testPrototype() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.HAND, playerA, automaton); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true); + } + + @Test + public void testLeavesBattlefield() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 1); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, bolt); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, bolt, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, automaton, 1); + Card card = playerA + .getGraveyard() + .getCards(currentGame) + .stream() + .filter(c -> c.getName().equals(automaton)) + .findFirst() + .orElse(null); + Assert.assertTrue("Card should be colorless", card.getColor(currentGame).isColorless()); + Assert.assertEquals("Card should have 6 power", 6, card.getPower().getValue()); + Assert.assertEquals("Card should have 4 toughness", 4, card.getToughness().getValue()); + } + + @Test + public void testBlink() { + addCard(Zone.BATTLEFIELD, playerA, "Plateau", 3 + 1); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, cloudshift); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, cloudshift, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(false); + } + + @Test + public void testTriggerColorlessSpell() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 7); + addCard(Zone.HAND, playerA, automaton); + + makeTester(ColorlessPredicate.instance); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20 + 1); + } + + @Test + public void testTriggerRedSpell() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.HAND, playerA, automaton); + + makeTester(new ColorPredicate(ObjectColor.RED)); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20 + 1); + } + + @Test + public void testTrigger64Spell() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 7); + addCard(Zone.HAND, playerA, automaton); + + makeTester( + new PowerPredicate(ComparisonType.EQUAL_TO, 6), + new ToughnessPredicate(ComparisonType.EQUAL_TO, 4) + ); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20 + 1); + } + + @Test + public void testTrigger32Spell() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.HAND, playerA, automaton); + + makeTester( + new PowerPredicate(ComparisonType.EQUAL_TO, 3), + new ToughnessPredicate(ComparisonType.EQUAL_TO, 2) + ); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20 + 1); + } + + @Test + public void testTrigger7MVSpell() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 7); + addCard(Zone.HAND, playerA, automaton); + + makeTester(new ManaValuePredicate(ComparisonType.EQUAL_TO, 7)); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20 + 1); + } + + @Test + public void testTrigger3MVSpell() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.HAND, playerA, automaton); + + makeTester(new ManaValuePredicate(ComparisonType.EQUAL_TO, 3)); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20 + 1); + } + + @Test + public void testCloneRegular() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 7 + 4); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, clone); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, clone); + setChoice(playerA, true); // yes to clone + setChoice(playerA, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(false, 2); + } + + @Test + public void testClonePrototype() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 3 + 4); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, clone); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, clone); + setChoice(playerA, true); // yes to clone + setChoice(playerA, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true, 2); + } + + @Test + public void testTokenCopyRegular() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 7 + 3); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, counterpart); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, counterpart, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(false, 2); + } + + @Test + public void testTokenCopyPrototype() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 3 + 3); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, counterpart); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, counterpart, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true, 2); + } + + @Test + public void testTokenCopyRegularLKI() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 7 + 6); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, epiphany); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, epiphany); + setModeChoice(playerA, "3"); // Return target nonland permanent to its owner's hand. + setModeChoice(playerA, "4"); // Create a token that's a copy of target creature you control. + addTarget(playerA, automaton); + addTarget(playerA, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertHandCount(playerA, automaton, 1); + checkAutomaton(false, 1); + } + + @Test + public void testTokenCopyPrototypeLKI() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 3 + 6); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, epiphany); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, epiphany); + setModeChoice(playerA, "3"); // Return target nonland permanent to its owner's hand. + setModeChoice(playerA, "4"); // Create a token that's a copy of target creature you control. + addTarget(playerA, automaton); + addTarget(playerA, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertHandCount(playerA, automaton, 1); + checkAutomaton(true, 1); + } + + @Test + public void testStackToughnessPrototyped() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.HAND, playerB, "Stern Scolding"); + // Counter target creature spell with power or toughness 2 or less. + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Stern Scolding"); + addTarget(playerB, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, automaton, 1); + checkAutomaton(true, 0); + } + + @Test + public void testStackColorPrototyped() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.BATTLEFIELD, playerB, "Douse"); + // {1}{U}: Counter target red spell. + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB, "{1}{U}: Counter target red spell"); + addTarget(playerB, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, automaton, 1); + checkAutomaton(true, 0); + } + + @Test + public void testStackManaValueRegular() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 7); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.BATTLEFIELD, playerB, "Island", 5); + addCard(Zone.HAND, playerB, denied); + // Counter target spell. Create X 1/1 colorless Thopter artifact creature tokens with flying, where X is that spell’s mana value. + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, denied, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertPermanentCount(playerB, "Thopter Token", 7); + assertGraveyardCount(playerA, automaton, 1); + checkAutomaton(false, 0); + } + + @Test + public void testStackManaValuePrototype() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.BATTLEFIELD, playerB, "Island", 5); + addCard(Zone.HAND, playerB, denied); + // Counter target spell. Create X 1/1 colorless Thopter artifact creature tokens with flying, where X is that spell’s mana value. + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, denied, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertPermanentCount(playerB, "Thopter Token", 3); + assertGraveyardCount(playerA, automaton, 1); + checkAutomaton(true, 0); + } + + @Test + public void testManaValueWhenCasting() { + String winnower = "Void Winnower"; // Your opponents can't cast spells with even mana values. + String evenRegOddProto = "Fallaji Dragon Engine"; // {8} 5/5; {2}{R} 1/3 + String oddRegEvenProto = "Boulderbranch Golem"; // {7} 6/5; {3}{G} 3/3, ETB gain life equal to its power + + addCard(Zone.BATTLEFIELD, playerB, winnower); + addCard(Zone.HAND, playerA, evenRegOddProto); + addCard(Zone.HAND, playerA, oddRegEvenProto); + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 8); + addCard(Zone.HAND, playerA, "Taiga"); + + // checkPlayableAbility doesn't seem to detect Void Winnower's restriction in time (probably because it checks CAST_SPELL_LATE?) + // but if you try to actually cast a spell with even mana value, it will correctly fail + + checkPlayableAbility("cast odd reg", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Boulderbranch", true); + //checkPlayableAbility("cast even reg", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Fallaji", false); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Taiga"); + + //checkPlayableAbility("cast even proto", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Boulderbranch Golem"+withPrototype, false); + checkPlayableAbility("cast odd proto", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Fallaji Dragon Engine"+withPrototype, true); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, evenRegOddProto+withPrototype); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertPowerToughness(playerA, evenRegOddProto, 1, 3); + } + @Test + public void testCopyOnStack() { + addCard(Zone.BATTLEFIELD, playerA, "Frontier Bivouac", 3+2); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Double Major"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true, 2); + } + @Test + public void testHumility() { + addCard(Zone.BATTLEFIELD, playerA, "Plateau", 4+3+2); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Humility"); + addCard(Zone.HAND, playerA, "Disenchant"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Humility"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + checkPT("Humility with Prototype", 1, PhaseStep.BEGIN_COMBAT, playerA, automaton, 1, 1); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Disenchant", "Humility"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true); + } + @Test + public void testColorCostReduction() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.BATTLEFIELD, playerA, "Ruby Medallion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true); + assertTappedCount("Mountain", true, 2); + } + @Test + public void testAbilityRemovalPre() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 5); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Dress Down"); + + castSpell(1, PhaseStep.UPKEEP, playerA, "Dress Down"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + setStrictChooseMode(true); + execute(); + + assertPowerToughness(playerA, automaton, 3, 2); + } + @Test + public void testAbilityRemovalPost() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 5); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Dress Down"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Dress Down"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertPowerToughness(playerA, automaton, 3, 2); + } + @Test + public void testEssenceOfWild() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 5); + addCard(Zone.BATTLEFIELD, playerA, "Essence of the Wild", 1); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Pyroclasm"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Pyroclasm"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertPermanentCount(playerA, "Essence of the Wild", 2); + } + @Test + public void testChainer() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 3); + addCard(Zone.BATTLEFIELD, playerA, "Chainer, Nightmare Adept", 1); + addCard(Zone.GRAVEYARD, playerA, automaton); + addCard(Zone.HAND, playerA, "Plains"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Discard a card:"); + setChoice(playerA, "Plains"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, automatonWithPrototype); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true); + } + @Test + public void testMetamorphCopyA() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 3+9); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Hulking Metamorph"); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Hulking Metamorph"); + setChoice(playerA, true); + setChoice(playerA, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertPowerToughness(playerA, automaton, 3, 2); + assertPowerToughness(playerA, automaton, 7, 7); + } + @Test + public void testMetamorphCopyB() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 7+4); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Hulking Metamorph"); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Hulking Metamorph"+withPrototype); + setChoice(playerA, true); + setChoice(playerA, automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertPowerToughness(playerA, automaton, 6, 4); + assertPowerToughness(playerA, automaton, 3, 3); + } + @Test + public void testReflectionA() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 3+6+6); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Goring Warplow"); + addCard(Zone.HAND, playerA, "Infinite Reflection"); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Infinite Reflection", automaton); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Goring Warplow"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true, 2); + } + @Test + public void testReflectionB() { + addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 7+6+2); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Goring Warplow"); + addCard(Zone.HAND, playerA, "Infinite Reflection"); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automaton); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Infinite Reflection", automaton); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Goring Warplow"+withPrototype); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(false, 2); + } + @Test + public void testProgenitor() { + addCard(Zone.BATTLEFIELD, playerA, "Frontier Bivouac", 3+6); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Progenitor Mimic"); + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Progenitor Mimic"); + setChoice(playerA, true); + setChoice(playerA, automaton); + + setStopAt(3, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(true, 3); + } + @Test + public void testInstantaneousLKI() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 3+2); + addCard(Zone.BATTLEFIELD, playerA, "Flowstone Surge", 2); + addCard(Zone.BATTLEFIELD, playerA, "Drizzt Do'Urden", 1); + addCard(Zone.BATTLEFIELD, playerA, "Warstorm Surge", 1); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Slimebind"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Slimebind", "Drizzt Do'Urden"); + checkPT("Drizzt is shrunk",1, PhaseStep.BEGIN_COMBAT, playerA, "Drizzt Do'Urden",1, 1); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, automatonWithPrototype); // 5/0 + setChoice(playerA, "Whenever a creature enters"); //Stack the trigger + addTarget(playerA, playerB); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertPowerToughness(playerA, "Drizzt Do'Urden", 5, 5); + assertGraveyardCount(playerA, automaton, 1); + assertLife(playerB, 20-5); + } + @Test + public void testReanimate() { + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 3+1+1); + addCard(Zone.HAND, playerA, automaton); + addCard(Zone.HAND, playerA, "Cut Down"); + addCard(Zone.HAND, playerA, "Reanimate"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, automatonWithPrototype); + castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, "Cut Down", automaton); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Reanimate", automaton); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + checkAutomaton(false); + assertLife(playerA, 20-7); + } + + + /* + * More tests suggested by Zerris: + * 2) Gain control of spell on the stack: Aethersnatch + * 7) Phasing: Slip Out the Back + * 8) Alternate Cost: Fires of Invention (Cannot cast at all with fires on 3 lands, cannot cast prototyped even on 7) + * NOTE: This test is probably wrong, Prototype is apparently NOT an alternate cost! https://magic.wizards.com/en/news/feature/comprehensive-rules-changes + * 15) Yixlid Jailer + Chainer, Nightmare Adept - I believe you should be able to cast your card, but not Prototype it, + * because that decision is made before it goes on the stack (and thus leaves the graveyard). + * 19) Ensure Prototype is preserved through type changes - Swift Reconfiguration + Bludgeon Brawl on a prototyped card + * (and attempt to equip to Master of Waves) + * 20) Ensure colored mana in a Prototype cost is treated properly - can be paid for by Jegantha and Somberwald Sage, + * reduced by Morophon but not Ugin, the Ineffable + * 23) Jegantha can still be your companion with Depth Charge Colossus in your deck + */ + +} diff --git a/Mage/src/main/java/mage/MageObject.java b/Mage/src/main/java/mage/MageObject.java index dd079105ee7..0c8157995d1 100644 --- a/Mage/src/main/java/mage/MageObject.java +++ b/Mage/src/main/java/mage/MageObject.java @@ -116,6 +116,8 @@ public interface MageObject extends MageItem, Serializable, Copyable ManaCosts getManaCost(); + void setManaCost(ManaCosts costs); + default List getManaCostSymbols() { List symbols = new ArrayList<>(); for (ManaCost cost : getManaCost()) { diff --git a/Mage/src/main/java/mage/MageObjectImpl.java b/Mage/src/main/java/mage/MageObjectImpl.java index 60bad851202..3d2dfafdbd2 100644 --- a/Mage/src/main/java/mage/MageObjectImpl.java +++ b/Mage/src/main/java/mage/MageObjectImpl.java @@ -281,6 +281,11 @@ public abstract class MageObjectImpl implements MageObject { return manaCost; } + @Override + public void setManaCost(ManaCosts costs) { + this.manaCost = costs.copy(); + } + @Override public int getManaValue() { if (manaCost != null) { diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 03c0d1435f6..59b6575e368 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -442,6 +442,9 @@ public abstract class AbilityImpl implements Ability { // mandatory additional costs the spell has, such as that of Tormenting Voice. (2018-12-07) canUseAdditionalCost = true; break; + case PROTOTYPE: + // Notably, casting a spell as a prototype does not count as paying an alternative cost. + // https://magic.wizards.com/en/news/feature/comprehensive-rules-changes case NORMAL: canUseAlternativeCost = true; canUseAdditionalCost = true; diff --git a/Mage/src/main/java/mage/abilities/SpellAbility.java b/Mage/src/main/java/mage/abilities/SpellAbility.java index 40ff04179aa..7a0d8ca98e7 100644 --- a/Mage/src/main/java/mage/abilities/SpellAbility.java +++ b/Mage/src/main/java/mage/abilities/SpellAbility.java @@ -322,7 +322,7 @@ public class SpellAbility extends ActivatedAbilityImpl { } if (spellCharacteristics != null) { if (getSpellAbilityCastMode() != SpellAbilityCastMode.NORMAL) { - spellCharacteristics = getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(spellCharacteristics, game); + spellCharacteristics = getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(spellCharacteristics, this); } } return spellCharacteristics; diff --git a/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java index 69202e2cc0e..53a5514476a 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CopyEffect.java @@ -146,6 +146,7 @@ public class CopyEffect extends ContinuousEffectImpl { //permanent.setSecondCardFace(targetPermanent.getSecondCardFace()); permanent.setFlipCard(targetPermanent.isFlipCard()); permanent.setFlipCardName(targetPermanent.getFlipCardName()); + permanent.setPrototyped(targetPermanent.isPrototyped()); } CardUtil.copySetAndCardNumber(permanent, copyFromObject); diff --git a/Mage/src/main/java/mage/abilities/keyword/PrototypeAbility.java b/Mage/src/main/java/mage/abilities/keyword/PrototypeAbility.java index f034775ca3e..5b08d0e625e 100644 --- a/Mage/src/main/java/mage/abilities/keyword/PrototypeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/PrototypeAbility.java @@ -1,21 +1,48 @@ package mage.abilities.keyword; +import mage.MageObject; +import mage.ObjectColor; +import mage.abilities.Ability; import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ContinuousEffectImpl; import mage.cards.Card; +import mage.constants.*; +import mage.game.Game; +import mage.game.permanent.Permanent; /** - * @author TheElk801 + * @author TheElk801, Susucr, notgreat */ public class PrototypeAbility extends SpellAbility { + private final int power; + private final int toughness; + private final String manaString; + private final String rule; + public PrototypeAbility(Card card, String manaString, int power, int toughness) { super(new ManaCostsImpl<>(manaString), card.getName()); - // TODO: implement this + this.setSpellAbilityCastMode(SpellAbilityCastMode.PROTOTYPE); + this.setTiming(TimingRule.SORCERY); + this.addSubAbility(new SimpleStaticAbility( + Zone.BATTLEFIELD, new PrototypeEffect(power, toughness, manaString) + ).setRuleVisible(false)); + this.rule = "Prototype " + manaString + " — " + power + "/" + toughness + + " (You may cast this spell with different mana cost, color, and size. It keeps its abilities and types.)"; + setRuleAtTheTop(true); + this.power = power; + this.toughness = toughness; + this.manaString = manaString; } private PrototypeAbility(final PrototypeAbility ability) { super(ability); + this.rule = ability.rule; + this.power = ability.power; + this.toughness = ability.toughness; + this.manaString = ability.manaString; } @Override @@ -25,6 +52,68 @@ public class PrototypeAbility extends SpellAbility { @Override public String getRule() { - return "Prototype"; + return rule; + } + + //based on TransformAbility + public Card prototypeCardSpell(Card original) { + Card newCard = original.copy(); + newCard.setManaCost(new ManaCostsImpl<>(manaString)); + newCard.getPower().setModifiedBaseValue(power); + newCard.getToughness().setModifiedBaseValue(toughness); + newCard.getColor().setColor(new ObjectColor(manaString)); + return newCard; + } + + public void prototypePermanent(MageObject targetObject, Game game) { + if (targetObject instanceof Permanent) { + ((Permanent)targetObject).setPrototyped(true); + } + targetObject.getColor(game).setColor(new ObjectColor(manaString)); + targetObject.setManaCost(new ManaCostsImpl<>(manaString)); + targetObject.getPower().setModifiedBaseValue(power); + targetObject.getToughness().setModifiedBaseValue(toughness); } } + +class PrototypeEffect extends ContinuousEffectImpl { + + private final int power; + private final int toughness; + private final String manaString; + private final ObjectColor color; + + PrototypeEffect(int power, int toughness, String manaString) { + super(Duration.EndOfGame, Layer.CopyEffects_1, SubLayer.CopyEffects_1a, Outcome.Benefit); + this.power = power; + this.toughness = toughness; + this.manaString = manaString; + this.color = new ObjectColor(manaString); + } + + private PrototypeEffect(final PrototypeEffect effect) { + super(effect); + this.power = effect.power; + this.toughness = effect.toughness; + this.manaString = effect.manaString; + this.color = effect.color; + } + + @Override + public PrototypeEffect copy() { + return new PrototypeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getSourceId()); + if (permanent == null || !permanent.isPrototyped()) { + return false; + } + permanent.setManaCost(new ManaCostsImpl<>(manaString)); + permanent.getColor(game).setColor(color); + permanent.getPower().setModifiedBaseValue(power); + permanent.getToughness().setModifiedBaseValue(toughness); + return true; + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java index 905794d0336..e4bc9b723bf 100644 --- a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java +++ b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java @@ -1,9 +1,10 @@ package mage.constants; +import mage.abilities.SpellAbility; import mage.abilities.keyword.BestowAbility; -import mage.abilities.keyword.MorphAbility; +import mage.abilities.keyword.PrototypeAbility; import mage.cards.Card; -import mage.game.Game; +import mage.abilities.keyword.MorphAbility; import mage.game.stack.Spell; /** @@ -14,6 +15,7 @@ public enum SpellAbilityCastMode { MADNESS("Madness"), FLASHBACK("Flashback"), BESTOW("Bestow"), + PROTOTYPE("Prototype"), MORPH("Morph"), TRANSFORMED("Transformed", true), DISTURB("Disturb", true), @@ -42,7 +44,7 @@ public enum SpellAbilityCastMode { return text; } - public Card getTypeModifiedCardObjectCopy(Card card, Game game) { + public Card getTypeModifiedCardObjectCopy(Card card, SpellAbility spellAbility) { Card cardCopy = card.copy(); if (this.equals(BESTOW)) { BestowAbility.becomeAura(cardCopy); @@ -53,6 +55,9 @@ public enum SpellAbilityCastMode { cardCopy = tmp.copy(); } } + if (this.equals(PROTOTYPE)) { + cardCopy = ((PrototypeAbility) spellAbility).prototypeCardSpell(cardCopy); + } if (this.equals(MORPH)) { if (cardCopy instanceof Spell) { //Spell doesn't support setName, so make a copy of the card (we're blowing it away anyway) diff --git a/Mage/src/main/java/mage/designations/Designation.java b/Mage/src/main/java/mage/designations/Designation.java index 4008728d13f..edab643575c 100644 --- a/Mage/src/main/java/mage/designations/Designation.java +++ b/Mage/src/main/java/mage/designations/Designation.java @@ -133,6 +133,11 @@ public abstract class Designation extends MageObjectImpl { return emptyCost; } + @Override + public void setManaCost(ManaCosts costs) { + throw new UnsupportedOperationException("Unsupported operation"); + } + @Override public int getManaValue() { return 0; diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 426d5ac93e5..2bffe9a0850 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1978,6 +1978,14 @@ public abstract class GameImpl implements Game { if (copyFromPermanent.isTransformed()) { TransformAbility.transformPermanent(newBluePrint, newBluePrint.getSecondCardFace(), this, source); } + if (copyFromPermanent.isPrototyped()) { + Abilities abilities = copyFromPermanent.getAbilities(); + for (Ability ability : abilities){ + if (ability instanceof PrototypeAbility) { + ((PrototypeAbility) ability).prototypePermanent(newBluePrint, this); + } + } + } } if (applier != null) { applier.apply(this, newBluePrint, source, copyToPermanentId); diff --git a/Mage/src/main/java/mage/game/ZonesHandler.java b/Mage/src/main/java/mage/game/ZonesHandler.java index 89c2b846ee2..96c78f0d90c 100644 --- a/Mage/src/main/java/mage/game/ZonesHandler.java +++ b/Mage/src/main/java/mage/game/ZonesHandler.java @@ -1,6 +1,7 @@ package mage.game; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.keyword.TransformAbility; import mage.cards.*; import mage.constants.Outcome; @@ -361,8 +362,16 @@ public final class ZonesHandler { // put onto battlefield with possible counters game.getPermanentsEntering().put(permanent.getId(), permanent); card.checkForCountersToAdd(permanent, source, game); + permanent.setTapped(info instanceof ZoneChangeInfo.Battlefield && ((ZoneChangeInfo.Battlefield) info).tapped); + + if (Zone.STACK == event.getFromZone()) { + Spell spell = game.getStack().getSpell(event.getTargetId()); + if (spell != null) { + permanent.setPrototyped(spell.isPrototyped()); + } + } permanent.setFaceDown(info.faceDown, game); if (info.faceDown) { diff --git a/Mage/src/main/java/mage/game/command/Commander.java b/Mage/src/main/java/mage/game/command/Commander.java index 27c9c650d88..e1c3ad6cf99 100644 --- a/Mage/src/main/java/mage/game/command/Commander.java +++ b/Mage/src/main/java/mage/game/command/Commander.java @@ -238,6 +238,11 @@ public class Commander extends CommandObjectImpl { return sourceObject.getManaCost(); } + @Override + public void setManaCost(ManaCosts costs) { + throw new UnsupportedOperationException("Unsupported operation"); + } + @Override public int getManaValue() { return sourceObject.getManaValue(); diff --git a/Mage/src/main/java/mage/game/command/Dungeon.java b/Mage/src/main/java/mage/game/command/Dungeon.java index 211efe3753d..9361f89f276 100644 --- a/Mage/src/main/java/mage/game/command/Dungeon.java +++ b/Mage/src/main/java/mage/game/command/Dungeon.java @@ -255,6 +255,11 @@ public class Dungeon extends CommandObjectImpl { return emptyCost; } + @Override + public void setManaCost(ManaCosts costs) { + throw new UnsupportedOperationException("Unsupported operation"); + } + @Override public int getManaValue() { return 0; diff --git a/Mage/src/main/java/mage/game/command/Emblem.java b/Mage/src/main/java/mage/game/command/Emblem.java index b34bb6ba2f7..06da1d9f96b 100644 --- a/Mage/src/main/java/mage/game/command/Emblem.java +++ b/Mage/src/main/java/mage/game/command/Emblem.java @@ -170,6 +170,11 @@ public abstract class Emblem extends CommandObjectImpl { return emptyCost; } + @Override + public void setManaCost(ManaCosts costs) { + throw new UnsupportedOperationException("Unsupported operation"); + } + @Override public int getManaValue() { return 0; diff --git a/Mage/src/main/java/mage/game/command/Plane.java b/Mage/src/main/java/mage/game/command/Plane.java index 309a8d683dc..730ea503f49 100644 --- a/Mage/src/main/java/mage/game/command/Plane.java +++ b/Mage/src/main/java/mage/game/command/Plane.java @@ -195,6 +195,11 @@ public abstract class Plane extends CommandObjectImpl { return emptyCost; } + @Override + public void setManaCost(ManaCosts costs) { + throw new UnsupportedOperationException("Unsupported operation"); + } + @Override public int getManaValue() { return 0; diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index bd402c7a286..c9e1494ca6f 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -75,6 +75,10 @@ public interface Permanent extends Card, Controllable { void setRenowned(boolean value); + boolean isPrototyped(); + + void setPrototyped(boolean value); + int getClassLevel(); /** diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 62e993a48c3..037aba7804e 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -109,6 +109,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { protected Map info = new LinkedHashMap<>(); // additional info for permanent's rules protected int createOrder; protected boolean legendRuleApplies = true; + protected boolean prototyped; private static final List emptyList = Collections.unmodifiableList(new ArrayList<>()); @@ -179,6 +180,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.morphed = permanent.morphed; this.manifested = permanent.manifested; this.createOrder = permanent.createOrder; + this.prototyped = permanent.prototyped; } @Override @@ -1616,6 +1618,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { return this.monstrous; } + @Override + public boolean isPrototyped() { + return this.prototyped; + } + @Override public void setMonstrous(boolean value) { this.monstrous = value; @@ -1829,6 +1836,10 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.secondSideCard = card; } + public void setPrototyped(boolean prototyped) { + this.prototyped = prototyped; + } + @Override public boolean isRingBearer() { return ringBearerFlag; diff --git a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java index 3d390323cfd..55c2862689d 100644 --- a/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java +++ b/Mage/src/main/java/mage/game/permanent/token/TokenImpl.java @@ -331,6 +331,11 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { allAddedTokens.add((PermanentToken) permanent); } + // prototyped spell tokens make prototyped permanent tokens on resolution. + if (source instanceof SpellAbility && ((SpellAbility) source).getSpellAbilityCastMode() == SpellAbilityCastMode.PROTOTYPE) { + permanent.setPrototyped(true); + } + // if token was created (not a spell copy) handle auras coming into the battlefield // that must determine what to enchant // see #9583 for the root cause issue of why this convoluted searching is necessary @@ -345,6 +350,7 @@ public abstract class TokenImpl extends MageObjectImpl implements Token { if (!(ability instanceof SpellAbility)) { continue; } + auraOutcome = ability.getEffects().getOutcome(ability); for (Effect effect : ability.getEffects()) { if (!(effect instanceof AttachEffect)) { diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 221e9b44206..a05b1f73f12 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -10,6 +10,7 @@ import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.keyword.BestowAbility; import mage.abilities.keyword.MorphAbility; +import mage.abilities.keyword.PrototypeAbility; import mage.abilities.keyword.TransformAbility; import mage.cards.*; import mage.constants.*; @@ -46,6 +47,7 @@ public class Spell extends StackObjectImpl implements Card { private final List spellAbilities = new ArrayList<>(); private final Card card; + private ManaCosts manaCost; private final ObjectColor color; private final ObjectColor frameColor; private final FrameStyle frameStyle; @@ -62,6 +64,7 @@ public class Spell extends StackObjectImpl implements Card { private boolean resolving = false; private UUID commandedByPlayerId = null; // controller of the spell resolve, example: Word of Command private String commandedByInfo; // info about spell commanded, e.g. source + private boolean prototyped; private int startingLoyalty; private int startingDefense; @@ -79,8 +82,13 @@ public class Spell extends StackObjectImpl implements Card { // simulate another side as new card (another code part in continues effect from disturb ability) affectedCard = TransformAbility.transformCardSpellStatic(card, card.getSecondCardFace(), game); } + if (ability instanceof PrototypeAbility){ + affectedCard = ((PrototypeAbility)ability).prototypeCardSpell(card); + this.prototyped = true; + } this.card = affectedCard; + this.manaCost = this.card.getManaCost().copy(); this.color = affectedCard.getColor(null).copy(); this.frameColor = affectedCard.getFrameColor(null).copy(); this.frameStyle = affectedCard.getFrameStyle(); @@ -109,6 +117,7 @@ public class Spell extends StackObjectImpl implements Card { } else { spellAbilities.add(ability); } + this.controllerId = controllerId; this.fromZone = fromZone; this.countered = false; @@ -128,6 +137,7 @@ public class Spell extends StackObjectImpl implements Card { this.card = spell.card.copy(); this.fromZone = spell.fromZone; + this.manaCost = spell.getManaCost().copy(); this.color = spell.color.copy(); this.frameColor = spell.color.copy(); this.frameStyle = spell.frameStyle; @@ -143,6 +153,7 @@ public class Spell extends StackObjectImpl implements Card { this.currentActivatingManaAbilitiesStep = spell.currentActivatingManaAbilitiesStep; this.targetChanged = spell.targetChanged; + this.prototyped = spell.prototyped; this.startingLoyalty = spell.startingLoyalty; this.startingDefense = spell.startingDefense; } @@ -632,9 +643,12 @@ public class Spell extends StackObjectImpl implements Card { @Override public ManaCosts getManaCost() { - return card.getManaCost(); + return this.manaCost; } + @Override + public void setManaCost(ManaCosts costs) { this.manaCost = costs.copy(); } + /** * 202.3b When calculating the converted mana cost of an object with an {X} * in its mana cost, X is treated as 0 while the object is not on the stack, @@ -652,7 +666,7 @@ public class Spell extends StackObjectImpl implements Card { for (SpellAbility spellAbility : spellAbilities) { cmc += spellAbility.getConvertedXManaCost(getCard()); } - cmc += getCard().getManaCost().manaValue(); + cmc += this.manaCost.manaValue(); return cmc; } @@ -789,6 +803,10 @@ public class Spell extends StackObjectImpl implements Card { return false; } + public boolean isPrototyped() { + return prototyped; + } + @Override public Spell copy() { return new Spell(this); diff --git a/Mage/src/main/java/mage/game/stack/StackAbility.java b/Mage/src/main/java/mage/game/stack/StackAbility.java index ebaf5b41159..609ad36e4a2 100644 --- a/Mage/src/main/java/mage/game/stack/StackAbility.java +++ b/Mage/src/main/java/mage/game/stack/StackAbility.java @@ -246,6 +246,11 @@ public class StackAbility extends StackObjectImpl implements Ability { return emptyCost; } + @Override + public void setManaCost(ManaCosts costs) { + throw new UnsupportedOperationException("Unsupported operation"); + } + @Override public List getManaCostSymbols() { return super.getManaCostSymbols(); diff --git a/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java b/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java index f208902ae66..851f76f38f5 100644 --- a/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java +++ b/Mage/src/main/java/mage/util/functions/CopyTokenFunction.java @@ -1,8 +1,10 @@ package mage.util.functions; import mage.MageObject; +import mage.abilities.Abilities; import mage.abilities.Ability; import mage.abilities.keyword.MorphAbility; +import mage.abilities.keyword.PrototypeAbility; import mage.cards.Card; import mage.constants.CardType; import mage.constants.SuperType; @@ -83,6 +85,14 @@ public class CopyTokenFunction { copyToToken(target.getBackFace(), ((Card) sourceObj).getSecondCardFace(), game); CardUtil.copySetAndCardNumber(target.getBackFace(), ((Card) sourceObj).getSecondCardFace()); } + if (((PermanentCard) source).isPrototyped()){ + Abilities abilities = source.getAbilities(); + for (Ability ability : abilities){ + if (ability instanceof PrototypeAbility) { + ((PrototypeAbility) ability).prototypePermanent(target, game); + } + } + } return; }