From 4856c6544328a576990287cbfa6c2b6b54fa9f77 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sat, 29 Jul 2023 19:45:09 +0200 Subject: [PATCH] [BOT] Implement Megatron, Tyrant // Megatron, Destructive Force (#10666) * [BOT] Implement Megatron, Tyrant // Megatron, Destructive Force * fix verify test. * cleanup AbilityCastMode for Disturb & MoreThanMeetsTheEye * cleanup unecessary checks. * fix duration of silence static effect * fix Disturb tests --- .../cards/m/MegatronDestructiveForce.java | 161 ++++++++++++++++++ .../src/mage/cards/m/MegatronTyrant.java | 111 ++++++++++++ Mage.Sets/src/mage/sets/Transformers.java | 2 + .../cards/abilities/keywords/DisturbTest.java | 22 +-- .../main/java/mage/abilities/AbilityImpl.java | 2 + .../abilities/keyword/DisturbAbility.java | 5 +- .../keyword/MoreThanMeetsTheEyeAbility.java | 7 +- .../mage/constants/SpellAbilityCastMode.java | 17 +- Mage/src/main/java/mage/game/stack/Spell.java | 2 +- 9 files changed, 310 insertions(+), 19 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/m/MegatronDestructiveForce.java create mode 100644 Mage.Sets/src/mage/cards/m/MegatronTyrant.java diff --git a/Mage.Sets/src/mage/cards/m/MegatronDestructiveForce.java b/Mage.Sets/src/mage/cards/m/MegatronDestructiveForce.java new file mode 100644 index 00000000000..fb0cc217da0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MegatronDestructiveForce.java @@ -0,0 +1,161 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.hint.StaticHint; +import mage.abilities.keyword.LivingMetalAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class MegatronDestructiveForce extends CardImpl { + public MegatronDestructiveForce(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, ""); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + this.color.setRed(true); + this.color.setWhite(true); + this.color.setBlack(true); + this.nightCard = true; + + // Living metal + this.addAbility(new LivingMetalAbility()); + + // Whenever Megatron attacks, you may sacrifice another artifact. When you do, Megatron deals damage equal to the sacrificed artifact's mana value to target creature. If excess damage would be dealt to that creature this way, instead that damage is dealt to that creature's controller and you convert Megatron. + this.addAbility(new AttacksTriggeredAbility(new MegatronDestructiveForceEffect())); + } + + private MegatronDestructiveForce(final MegatronDestructiveForce card) { + super(card); + } + + @Override + public MegatronDestructiveForce copy() { + return new MegatronDestructiveForce(this); + } +} + +class MegatronDestructiveForceEffect extends OneShotEffect { + + MegatronDestructiveForceEffect() { + super(Outcome.Benefit); + staticText = "you may sacrifice another artifact. When you do, {this} deals damage equal to the sacrificed artifact's mana value to target creature. If excess damage would be dealt to that creature this way, instead that damage is dealt to that creature's controller and you convert {this}."; + } + + private MegatronDestructiveForceEffect(final MegatronDestructiveForceEffect effect) { + super(effect); + } + + @Override + public MegatronDestructiveForceEffect copy() { + return new MegatronDestructiveForceEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetPermanent target = new TargetPermanent( + 0, 1, StaticFilters.FILTER_CONTROLLED_ANOTHER_ARTIFACT_SHORT_TEXT, true + ); + player.choose(outcome, target, source, game); + Permanent permanent = game.getPermanent(target.getFirstTarget()); + if (permanent == null) { + return false; + } + + int manaValue = Math.max(permanent.getManaValue(), 0); + if (!permanent.sacrifice(source, game)) { + return false; + } + + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( + new MegatronDestructiveForceReflexiveEffect(manaValue), false + ); + ability.addHint(new StaticHint("Sacrificed artifact mana value: " + manaValue)); + ability.addTarget(new TargetCreaturePermanent()); + game.fireReflexiveTriggeredAbility(ability, source); + return true; + } +} + +class MegatronDestructiveForceReflexiveEffect extends OneShotEffect { + + private final int value; + + MegatronDestructiveForceReflexiveEffect(int value) { + super(Outcome.Damage); + staticText = "{this} deals damage equal to the sacrificed artifact's mana value to target " + + "creature. If excess damage would be dealt to that creature this way, instead that damage " + + "is dealt to that creature's controller and you convert {this}."; + this.value = value; + } + + private MegatronDestructiveForceReflexiveEffect(final MegatronDestructiveForceReflexiveEffect effect) { + super(effect); + this.value = effect.value; + } + + @Override + public MegatronDestructiveForceReflexiveEffect copy() { + return new MegatronDestructiveForceReflexiveEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent sourcePermanent = source.getSourcePermanentOrLKI(game); + if (sourcePermanent == null) { + return false; + } + + if (value < 1) { + return false; + } + + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + + int lethal = permanent.getLethalDamage(source.getSourceId(), game); + int excess = value - lethal; + if (excess <= 0) { + // no excess damage. + permanent.damage(value, source.getSourceId(), source, game); + return true; + } + + // excess damage. dealing excess to controller's instead. And convert Megatron. + permanent.damage(lethal, source.getSourceId(), source, game); + Player player = game.getPlayer(permanent.getControllerId()); + if (player != null) { + player.damage(excess, source, game); + } + new TransformSourceEffect().apply(game, source); + + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/m/MegatronTyrant.java b/Mage.Sets/src/mage/cards/m/MegatronTyrant.java new file mode 100644 index 00000000000..acbd1c0d3ce --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MegatronTyrant.java @@ -0,0 +1,111 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.MageObject; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.common.BeginningOfPostCombatMainTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.OpponentsLostLifeCount; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.effects.mana.DynamicManaEffect; +import mage.abilities.keyword.MoreThanMeetsTheEyeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class MegatronTyrant extends CardImpl { + + public MegatronTyrant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}{R}{W}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ROBOT); + this.power = new MageInt(7); + this.toughness = new MageInt(5); + this.secondSideCardClazz = mage.cards.m.MegatronDestructiveForce.class; + + // More Than Meets the Eye {1}{R}{W}{B} + this.addAbility(new MoreThanMeetsTheEyeAbility(this, "{1}{R}{W}{B}")); + + // Your opponents can't cast spells during combat. + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new MegatronTyrantCantCastSpellsEffect())); + + // At the beginning of your postcombat main phase, you may convert Megatron. If you do, add {C} for each 1 life your opponents have lost this turn. + TriggeredAbility trigger = new BeginningOfPostCombatMainTriggeredAbility( + new TransformSourceEffect().setText("convert {this}"), + TargetController.YOU, + true + ); + trigger.addEffect( + new DynamicManaEffect( + Mana.ColorlessMana(1), + OpponentsLostLifeCount.instance, + "add {C} for each 1 life your opponents have lost this turn" + ).concatBy("If you do, ") + ); + + this.addAbility(trigger); + } + + private MegatronTyrant(final MegatronTyrant card) { + super(card); + } + + @Override + public MegatronTyrant copy() { + return new MegatronTyrant(this); + } +} + +class MegatronTyrantCantCastSpellsEffect extends ContinuousRuleModifyingEffectImpl { + + MegatronTyrantCantCastSpellsEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "Your opponents can't cast spells during combat."; + } + + private MegatronTyrantCantCastSpellsEffect(final MegatronTyrantCantCastSpellsEffect effect) { + super(effect); + } + + @Override + public MegatronTyrantCantCastSpellsEffect copy() { + return new MegatronTyrantCantCastSpellsEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public String getInfoMessage(Ability source, GameEvent event, Game game) { + MageObject mageObject = game.getObject(source); + if (mageObject != null) { + return "You can't cast spells during combat (" + mageObject.getIdName() + ")."; + } + return null; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CAST_SPELL; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return game.getOpponents(source.getControllerId()).contains(event.getPlayerId()) + && game.getTurnPhaseType() == TurnPhase.COMBAT; + } + +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Transformers.java b/Mage.Sets/src/mage/sets/Transformers.java index 70860300557..e4323473389 100644 --- a/Mage.Sets/src/mage/sets/Transformers.java +++ b/Mage.Sets/src/mage/sets/Transformers.java @@ -29,6 +29,8 @@ public final class Transformers extends ExpansionSet { cards.add(new SetCardInfo("Goldbug, Scrappy Scout", 11, Rarity.MYTHIC, mage.cards.g.GoldbugScrappyScout.class)); cards.add(new SetCardInfo("Jetfire, Air Guardian", 3, Rarity.MYTHIC, mage.cards.j.JetfireAirGuardian.class)); cards.add(new SetCardInfo("Jetfire, Ingenious Scientist", 3, Rarity.MYTHIC, mage.cards.j.JetfireIngeniousScientist.class)); + cards.add(new SetCardInfo("Megatron, Destructive Force", 12, Rarity.MYTHIC, mage.cards.m.MegatronDestructiveForce.class)); + cards.add(new SetCardInfo("Megatron, Tyrant", 12, Rarity.MYTHIC, mage.cards.m.MegatronTyrant.class)); cards.add(new SetCardInfo("Ratchet, Field Medic", 2, Rarity.MYTHIC, mage.cards.r.RatchetFieldMedic.class)); cards.add(new SetCardInfo("Ratchet, Rescue Racer", 2, Rarity.MYTHIC, mage.cards.r.RatchetRescueRacer.class)); cards.add(new SetCardInfo("Ultra Magnus, Armored Carrier", 15, Rarity.MYTHIC, mage.cards.u.UltraMagnusArmoredCarrier.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisturbTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisturbTest.java index 6f415cc488b..33a041301ef 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisturbTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisturbTest.java @@ -31,12 +31,12 @@ public class DisturbTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Lightning Bolt", 1); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); - checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb", true); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb", true); // cast with disturb activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 2); - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb"); - checkStackObject("on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb", 1); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb"); + checkStackObject("on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb", 1); runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { // Stack must contain another card side, so spell/card characteristics must be diff from main side (only mana value is same) Spell spell = (Spell) game.getStack().getFirst(); @@ -92,10 +92,10 @@ public class DisturbTest extends CardTestPlayerBase { addCustomEffect_SpellCostModification(playerA, -1); - checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb", true); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb", true); // cast with disturb - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb"); runCode("check stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { Spell spell = (Spell) game.getStack().getFirst(); Assert.assertEquals("mana value must be from main side", 2, spell.getManaValue()); @@ -121,7 +121,7 @@ public class DisturbTest extends CardTestPlayerBase { // addCustomEffect_SpellCostModification(playerA, 1); - checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb", false); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb", false); setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); @@ -145,11 +145,11 @@ public class DisturbTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Island", 1); addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); - checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb", true); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb", true); // cast with disturb activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 2); - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb"); // prepare copy of spell castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Hook-Haunt Drifter", "Hook-Haunt Drifter"); checkStackSize("before copy spell", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); @@ -181,10 +181,10 @@ public class DisturbTest extends CardTestPlayerBase { addCard(Zone.HAND, playerA, "Counterspell", 1); // {U}{U} addCard(Zone.BATTLEFIELD, playerA, "Island", 2); - checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb", true); + checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb", true); // cast with disturb - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter with Disturb"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Hook-Haunt Drifter using Disturb"); // counter it castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Counterspell", "Hook-Haunt Drifter", "Hook-Haunt Drifter"); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); @@ -213,7 +213,7 @@ public class DisturbTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); addCard(Zone.HAND, playerA, lightningBolt); - activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + ghastlyMimictry + " with Disturb"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + ghastlyMimictry + " using Disturb"); setStopAt(1, PhaseStep.PRECOMBAT_MAIN); execute(); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index c151996d6c6..1ae63289b97 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -434,6 +434,8 @@ public abstract class AbilityImpl implements Ability { case FLASHBACK: case MADNESS: case TRANSFORMED: + case DISTURB: + case MORE_THAN_MEETS_THE_EYE: // from Snapcaster Mage: // If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs // (such as that of Foil). (2018-12-07) diff --git a/Mage/src/main/java/mage/abilities/keyword/DisturbAbility.java b/Mage/src/main/java/mage/abilities/keyword/DisturbAbility.java index f2b4d00575e..20ec3f5999e 100644 --- a/Mage/src/main/java/mage/abilities/keyword/DisturbAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/DisturbAbility.java @@ -34,10 +34,11 @@ public class DisturbAbility extends SpellAbility { this.newId(); // getSecondFaceSpellAbility() already verified that second face exists - this.setCardName(card.getSecondCardFace().getName() + " with Disturb"); + this.setCardName(card.getSecondCardFace().getName()); + this.zone = Zone.GRAVEYARD; this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE; - this.spellAbilityCastMode = SpellAbilityCastMode.TRANSFORMED; + this.setSpellAbilityCastMode(SpellAbilityCastMode.DISTURB); this.manaCost = manaCost; this.getManaCosts().clear(); diff --git a/Mage/src/main/java/mage/abilities/keyword/MoreThanMeetsTheEyeAbility.java b/Mage/src/main/java/mage/abilities/keyword/MoreThanMeetsTheEyeAbility.java index ab19446e1f6..f1c8a698546 100644 --- a/Mage/src/main/java/mage/abilities/keyword/MoreThanMeetsTheEyeAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/MoreThanMeetsTheEyeAbility.java @@ -22,9 +22,10 @@ public class MoreThanMeetsTheEyeAbility extends SpellAbility { this.newId(); // getSecondFaceSpellAbility() already verified that second face exists - this.setCardName(card.getSecondCardFace().getName() + " with Disturb"); + this.setCardName(card.getSecondCardFace().getName()); + this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE; - this.spellAbilityCastMode = SpellAbilityCastMode.TRANSFORMED; + this.setSpellAbilityCastMode(SpellAbilityCastMode.MORE_THAN_MEETS_THE_EYE); this.manaCost = manaCost; this.getManaCosts().clear(); @@ -63,7 +64,7 @@ public class MoreThanMeetsTheEyeAbility extends SpellAbility { @Override public String getRule() { return "More Than Meets The Eye " + this.manaCost - + " (You may cast this card converted for" + this.manaCost + ".)"; + + " (You may cast this card converted for " + this.manaCost + ".)"; } } diff --git a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java index 200c99fd91a..e073465fea0 100644 --- a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java +++ b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java @@ -5,7 +5,6 @@ import mage.cards.Card; import mage.game.Game; /** - * * @author LevelX2 */ public enum SpellAbilityCastMode { @@ -13,12 +12,26 @@ public enum SpellAbilityCastMode { MADNESS("Madness"), FLASHBACK("Flashback"), BESTOW("Bestow"), - TRANSFORMED("Transformed"); + TRANSFORMED("Transformed", true), + DISTURB("Disturb", true), + MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true); private final String text; + // Should the cast mode use the second face? + private final boolean isTransformed; + + public boolean isTransformed() { + return this.isTransformed; + } + SpellAbilityCastMode(String text) { + this(text, false); + } + + SpellAbilityCastMode(String text, boolean isTransformed) { this.text = text; + this.isTransformed = isTransformed; } @Override diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 0e7da518544..6d8314f40bf 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -75,7 +75,7 @@ public class Spell extends StackObjectImpl implements Card { Card affectedCard = card; // TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides) - if (ability.getSpellAbilityCastMode() == SpellAbilityCastMode.TRANSFORMED && affectedCard.getSecondCardFace() != null) { + if (ability.getSpellAbilityCastMode().isTransformed() && affectedCard.getSecondCardFace() != null) { // simulate another side as new card (another code part in continues effect from disturb ability) affectedCard = TransformAbility.transformCardSpellStatic(card, card.getSecondCardFace(), game); }