From 77d0d3c07e76f4b49548340711036940222111ee Mon Sep 17 00:00:00 2001 From: jbureau88 <33074411+jbureau88@users.noreply.github.com> Date: Sun, 20 Aug 2023 13:28:17 -0400 Subject: [PATCH] Implement [BOT] Optimus Prime; adjust Bolster effect (#10129) * Added implementation for Optimus Prime * Added setCardName text and switch case for activateAlternateOrAdditionalCosts as disturb instead of default * Fixed doubling effect bug * Add only regular card number * Added back alternate card for Optimus Prime and fixed Autobot Leader colors * Convert before return from graveyard * Convert before return from graveyard * Fix images * Resolve conflict with master * don't manually set trigger phrase * add additional effect capability to BolsterEffect, adjust text, flatten logic * adjust Optimus Prime, Hero * reimplement Optimus Prime, Autobot Leader * add test --------- Co-authored-by: xenohedron --- .../cards/o/OptimusPrimeAutobotLeader.java | 130 ++++++++++++++++++ .../src/mage/cards/o/OptimusPrimeHero.java | 80 +++++++++++ Mage.Sets/src/mage/sets/Transformers.java | 4 + .../cards/single/bot/OptimusPrimeTest.java | 70 ++++++++++ .../main/java/mage/abilities/AbilityImpl.java | 1 - .../effects/keyword/BolsterEffect.java | 101 ++++++++------ 6 files changed, 346 insertions(+), 40 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/o/OptimusPrimeAutobotLeader.java create mode 100644 Mage.Sets/src/mage/cards/o/OptimusPrimeHero.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/bot/OptimusPrimeTest.java diff --git a/Mage.Sets/src/mage/cards/o/OptimusPrimeAutobotLeader.java b/Mage.Sets/src/mage/cards/o/OptimusPrimeAutobotLeader.java new file mode 100644 index 00000000000..a3d53cfe4dc --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OptimusPrimeAutobotLeader.java @@ -0,0 +1,130 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.common.AttacksWithCreaturesTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.TransformSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.keyword.BolsterEffect; +import mage.abilities.keyword.LivingMetalAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.DamagedPlayerEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author xenohedron + */ +public final class OptimusPrimeAutobotLeader extends CardImpl { + + public OptimusPrimeAutobotLeader(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, ""); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(6); + this.toughness = new MageInt(8); + this.color.setWhite(true); + this.color.setBlue(true); + this.color.setRed(true); + this.nightCard = true; + + // Living metal + this.addAbility(new LivingMetalAbility()); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Whenever you attack, bolster 2. The chosen creature gains trample until end of turn. When that creature deals combat damage to a player this turn, convert Optimus Prime. + this.addAbility(new AttacksWithCreaturesTriggeredAbility(new BolsterEffect(2) + .withAdditionalEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance())) + .withAdditionalEffect(new OptimusPrimeAutobotLeaderEffect()) + .setText("bolster 2. The chosen creature gains trample until end of turn. When that creature deals combat damage to a player this turn, convert {this}"), + 1)); + + // Transform Ability + this.addAbility(new TransformAbility()); + } + + private OptimusPrimeAutobotLeader(final OptimusPrimeAutobotLeader card) { + super(card); + } + + @Override + public OptimusPrimeAutobotLeader copy() { + return new OptimusPrimeAutobotLeader(this); + } +} + +class OptimusPrimeAutobotLeaderEffect extends OneShotEffect { + + OptimusPrimeAutobotLeaderEffect() { + super(Outcome.Transform); + } + + private OptimusPrimeAutobotLeaderEffect(final OptimusPrimeAutobotLeaderEffect effect) { + super(effect); + } + + @Override + public OptimusPrimeAutobotLeaderEffect copy() { + return new OptimusPrimeAutobotLeaderEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent creature = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (creature == null) { + return false; + } + game.addDelayedTriggeredAbility(new OptimusPrimeAutobotLeaderDelayedTriggeredAbility(new MageObjectReference(creature, game)), source); + return true; + } + +} + +class OptimusPrimeAutobotLeaderDelayedTriggeredAbility extends DelayedTriggeredAbility { + + private final MageObjectReference mor; + + OptimusPrimeAutobotLeaderDelayedTriggeredAbility(MageObjectReference mor) { + super(new TransformSourceEffect().setText("convert {this}"), Duration.EndOfTurn); + this.mor = mor; + setTriggerPhrase("When that creature deals combat damage to a player this turn, "); + } + + private OptimusPrimeAutobotLeaderDelayedTriggeredAbility(final OptimusPrimeAutobotLeaderDelayedTriggeredAbility ability) { + super(ability); + this.mor = ability.mor; + } + + @Override + public OptimusPrimeAutobotLeaderDelayedTriggeredAbility copy() { + return new OptimusPrimeAutobotLeaderDelayedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PLAYER; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (((DamagedPlayerEvent) event).isCombatDamage()) { + Permanent permanent = game.getPermanent(event.getSourceId()); + return mor.refersTo(permanent, game); + } + return false; + } + +} diff --git a/Mage.Sets/src/mage/cards/o/OptimusPrimeHero.java b/Mage.Sets/src/mage/cards/o/OptimusPrimeHero.java new file mode 100644 index 00000000000..0bb61678675 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OptimusPrimeHero.java @@ -0,0 +1,80 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.keyword.BolsterEffect; +import mage.abilities.keyword.MoreThanMeetsTheEyeAbility; +import mage.abilities.keyword.TransformAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author jbureau88 + */ +public final class OptimusPrimeHero extends CardImpl { + + public OptimusPrimeHero(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}{U}{R}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.ROBOT); + this.power = new MageInt(4); + this.toughness = new MageInt(8); + this.secondSideCardClazz = mage.cards.o.OptimusPrimeAutobotLeader.class; + + // More Than Meets the Eye {2}{U}{R}{W} + this.addAbility(new MoreThanMeetsTheEyeAbility(this, "{2}{U}{R}{W}")); + + // At the beginning of each end step, bolster 1. + this.addAbility(new BeginningOfEndStepTriggeredAbility(new BolsterEffect(1), TargetController.ANY, false)); + + // When Optimus Prime dies, return it to the battlefield converted under its owner’s control. + this.addAbility(new DiesSourceTriggeredAbility(new OptimusPrimeHeroEffect())); + } + + private OptimusPrimeHero(final OptimusPrimeHero card) { + super(card); + } + + @Override + public OptimusPrimeHero copy() { + return new OptimusPrimeHero(this); + } +} + +class OptimusPrimeHeroEffect extends OneShotEffect { + + OptimusPrimeHeroEffect() { + super(Outcome.Benefit); + staticText = "return it to the battlefield converted under its owner's control."; + } + + private OptimusPrimeHeroEffect(final OptimusPrimeHeroEffect effect) { + super(effect); + } + + @Override + public OptimusPrimeHeroEffect copy() { + return new OptimusPrimeHeroEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Card card = game.getCard(source.getSourceId()); + if (card == null || controller == null) { + return false; + } + game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + source.getSourceId(), Boolean.TRUE); + return controller.moveCards(card, Zone.BATTLEFIELD, source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/Transformers.java b/Mage.Sets/src/mage/sets/Transformers.java index 7194803d830..152d9f65331 100644 --- a/Mage.Sets/src/mage/sets/Transformers.java +++ b/Mage.Sets/src/mage/sets/Transformers.java @@ -31,6 +31,10 @@ public final class Transformers extends ExpansionSet { 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("Optimus Prime, Autobot Leader", 13, Rarity.MYTHIC, mage.cards.o.OptimusPrimeAutobotLeader.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Optimus Prime, Hero", 13, Rarity.MYTHIC, mage.cards.o.OptimusPrimeHero.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Optimus Prime, Autobot Leader", 27, Rarity.MYTHIC, mage.cards.o.OptimusPrimeAutobotLeader.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Optimus Prime, Hero", 27, Rarity.MYTHIC, mage.cards.o.OptimusPrimeHero.class, NON_FULL_USE_VARIOUS)); 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("Starscream, Power Hungry", 5, Rarity.MYTHIC, mage.cards.s.StarscreamPowerHungry.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/bot/OptimusPrimeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/bot/OptimusPrimeTest.java new file mode 100644 index 00000000000..516732177cc --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/bot/OptimusPrimeTest.java @@ -0,0 +1,70 @@ +package org.mage.test.cards.single.bot; + +import mage.abilities.keyword.TrampleAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class OptimusPrimeTest extends CardTestPlayerBase { + + private static final String optimus = "Optimus Prime, Hero"; + /* + * Optimus Prime, Hero {3}{U}{R}{W} + * Legendary Artifact Creature — Robot + * + * More Than Meets the Eye {2}{U}{R}{W} (You may cast this card converted for {2}{U}{R}{W}.) + * At the beginning of each end step, bolster 1. (Choose a creature with the least toughness among creatures you control and put a +1/+1 counter on it.) + * When Optimus Prime dies, return it to the battlefield converted under its owner’s control. + * 4/8 + * + * Optimus Prime, Autobot Leader + * Legendary Artifact — Vehicle + * + * Living metal (As long as it’s your turn, this Vehicle is also a creature.) + * Trample + * Whenever you attack, bolster 2. The chosen creature gains trample until end of turn. + * When that creature deals combat damage to a player this turn, convert Optimus Prime. + * 6/8 + */ + private static final String ghoul = "Gutless Ghoul"; // 2/2 "{1}, Sacrifice a creature: You gain 2 life." + private static final String myr = "Omega Myr"; // 1/2 + private static final String centaur = "Centaur Courser"; // 3/3 + + @Test + public void testOptimusPrime() { + addCard(Zone.BATTLEFIELD, playerA, optimus); + addCard(Zone.BATTLEFIELD, playerA, ghoul); + addCard(Zone.BATTLEFIELD, playerA, myr); + addCard(Zone.BATTLEFIELD, playerA, "Wastes"); + addCard(Zone.BATTLEFIELD, playerB, centaur); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}, Sacrifice"); // gain 2 life + setChoice(playerA, optimus); // sac, returns transformed + + attack(1, playerA, ghoul, playerB); + addTarget(playerA, ghoul); // choice for bolster, becomes a 4/4 with trample + block(1, playerB, centaur, ghoul); + setChoice(playerA, "X=3"); // assign 3 damage to centaur, 1 damage tramples over + // optimus triggers and transforms + // at end step, bolster 1, only target is myr + + setStopAt(2, PhaseStep.UPKEEP); + setStrictChooseMode(true); + execute(); + + assertLife(playerA, 20 + 2); + assertLife(playerB, 20 - 1); + assertGraveyardCount(playerB, centaur, 1); + assertGraveyardCount(playerA, optimus, 0); + assertPowerToughness(playerA, optimus, 4, 8); + assertPowerToughness(playerA, ghoul, 4, 4); + assertPowerToughness(playerA, myr, 2, 3); + assertAbility(playerA, ghoul, TrampleAbility.getInstance(), false); + + } + +} diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 2f62c532bf6..f7a29b182d7 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -430,7 +430,6 @@ public abstract class AbilityImpl implements Ability { if (this instanceof SpellAbility) { // A player can't apply two alternative methods of casting or two alternative costs to a single spell. switch (((SpellAbility) this).getSpellAbilityCastMode()) { - case FLASHBACK: case MADNESS: case TRANSFORMED: diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/BolsterEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/BolsterEffect.java index eb670149f4a..008f9393378 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/BolsterEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/BolsterEffect.java @@ -3,7 +3,9 @@ package mage.abilities.effects.keyword; import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; +import mage.abilities.effects.Effects; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.constants.ComparisonType; @@ -19,6 +21,7 @@ import mage.players.Player; import mage.target.Target; import mage.target.TargetPermanent; import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; /** * @author LevelX2 @@ -27,6 +30,8 @@ public class BolsterEffect extends OneShotEffect { private final DynamicValue amount; + private Effects additionalEffects = new Effects(); + public BolsterEffect(int amount) { this(StaticValue.get(amount)); } @@ -40,6 +45,7 @@ public class BolsterEffect extends OneShotEffect { protected BolsterEffect(final BolsterEffect effect) { super(effect); this.amount = effect.amount; + this.additionalEffects = effect.additionalEffects.copy(); } @Override @@ -47,55 +53,72 @@ public class BolsterEffect extends OneShotEffect { return new BolsterEffect(this); } + // Text must be set manually + public BolsterEffect withAdditionalEffect(Effect effect) { + additionalEffects.add(effect); + return this; + } + @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - if (amount.calculate(game, source, this) <= 0) { - return true; - } - int leastToughness = Integer.MAX_VALUE; - Permanent selectedCreature = null; - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, controller.getId(), game)) { - if (leastToughness > permanent.getToughness().getValue()) { - leastToughness = permanent.getToughness().getValue(); - selectedCreature = permanent; - } else if (leastToughness == permanent.getToughness().getValue()) { - leastToughness = permanent.getToughness().getValue(); - selectedCreature = null; - } - } - if (leastToughness != Integer.MAX_VALUE) { - if (selectedCreature == null) { - FilterPermanent filter = new FilterControlledCreaturePermanent("creature you control with toughness " + leastToughness); - filter.add(new ToughnessPredicate(ComparisonType.EQUAL_TO, leastToughness)); - Target target = new TargetPermanent(1, 1, filter, true); - if (controller.chooseTarget(outcome, target, source, game)) { - selectedCreature = game.getPermanent(target.getFirstTarget()); - } - } - if (selectedCreature != null) { - Effect effect = new AddCountersTargetEffect(CounterType.P1P1.createInstance(amount.calculate(game, source, this))); - effect.setTargetPointer(new FixedTarget(selectedCreature, game)); - return effect.apply(game, source); - } - } + if (controller == null) { + return false; + } + if (amount.calculate(game, source, this) <= 0) { return true; } - return false; + int leastToughness = Integer.MAX_VALUE; + Permanent selectedCreature = null; + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, controller.getId(), game)) { + if (leastToughness > permanent.getToughness().getValue()) { + leastToughness = permanent.getToughness().getValue(); + selectedCreature = permanent; // automatically select if only one + } else if (leastToughness == permanent.getToughness().getValue()) { + leastToughness = permanent.getToughness().getValue(); + selectedCreature = null; // more than one so need to manually select + } + } + if (leastToughness == Integer.MAX_VALUE) { + return false; // no creature found + } + if (selectedCreature == null) { + FilterPermanent filter = new FilterControlledCreaturePermanent("creature you control with toughness " + leastToughness); + filter.add(new ToughnessPredicate(ComparisonType.EQUAL_TO, leastToughness)); + Target target = new TargetPermanent(1, 1, filter, true); + if (controller.chooseTarget(outcome, target, source, game)) { + selectedCreature = game.getPermanent(target.getFirstTarget()); + } + } + if (selectedCreature == null) { + return false; + } + Effect effect = new AddCountersTargetEffect(CounterType.P1P1.createInstance(amount.calculate(game, source, this))); + FixedTarget fixedTarget = new FixedTarget(selectedCreature, game); + effect.setTargetPointer(fixedTarget); + effect.apply(game, source); + if (!additionalEffects.isEmpty()) { + for (Effect additionalEffect : additionalEffects) { + additionalEffect.setTargetPointer(fixedTarget); + if (additionalEffect instanceof OneShotEffect) { + additionalEffect.apply(game, source); + } else { + game.addEffect((ContinuousEffect) additionalEffect, source); + } + } + } + return true; } private String setText() { - StringBuilder sb = new StringBuilder("bolster "); if (amount instanceof StaticValue) { - sb.append(amount); - sb.append(". (Choose a creature with the least toughness or tied with the least toughness among creatures you control. Put "); - sb.append(amount).append(" +1/+1 counters on it.)"); + int number = ((StaticValue) amount).getValue(); + return "bolster " + number + +". (Choose a creature with the least toughness among creatures you control and put " + + CardUtil.numberToText(number, "a") + " +1/+1 counter" + (number == 1 ? "" : "s") + " on it.)"; } else { - sb.append("X, where X is the number of "); - sb.append(amount.getMessage()); - sb.append(". (Choose a creature with the least toughness among creatures you control and put X +1/+1 counters on it.)"); + return "bolster X, where X is the number of " + amount.getMessage() + + ". (Choose a creature with the least toughness among creatures you control and put X +1/+1 counters on it.)"; } - return sb.toString(); } }