From d705fa0e4198c93f868e1bd4b2e6828bbdca0e06 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 14 Oct 2023 15:27:45 -0400 Subject: [PATCH] Implement Villainous Choice mechanic (#11304) * [WHO] Implement Great Intelligence's Plan * [WHO] Implement The Valeyard * add comment for villainous choice event --- .../mage/cards/g/GreatIntelligencesPlan.java | 96 +++++++++++++++ Mage.Sets/src/mage/cards/t/TheValeyard.java | 111 ++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 2 + .../mage/choices/FaceVillainousChoice.java | 50 ++++++++ .../java/mage/choices/VillainousChoice.java | 40 +++++++ .../main/java/mage/game/events/GameEvent.java | 8 ++ 6 files changed, 307 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/g/GreatIntelligencesPlan.java create mode 100644 Mage.Sets/src/mage/cards/t/TheValeyard.java create mode 100644 Mage/src/main/java/mage/choices/FaceVillainousChoice.java create mode 100644 Mage/src/main/java/mage/choices/VillainousChoice.java diff --git a/Mage.Sets/src/mage/cards/g/GreatIntelligencesPlan.java b/Mage.Sets/src/mage/cards/g/GreatIntelligencesPlan.java new file mode 100644 index 00000000000..d3b2e0576c9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GreatIntelligencesPlan.java @@ -0,0 +1,96 @@ +package mage.cards.g; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; +import mage.choices.FaceVillainousChoice; +import mage.choices.VillainousChoice; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetOpponent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GreatIntelligencesPlan extends CardImpl { + + public GreatIntelligencesPlan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{U}{B}"); + + // Draw three cards. Then target opponent faces a villainous choice -- They discard three cards, or you may cast a spell from your hand without paying its mana cost. + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(3)); + this.getSpellAbility().addEffect(new GreatIntelligencesPlanEffect()); + this.getSpellAbility().addTarget(new TargetOpponent()); + } + + private GreatIntelligencesPlan(final GreatIntelligencesPlan card) { + super(card); + } + + @Override + public GreatIntelligencesPlan copy() { + return new GreatIntelligencesPlan(this); + } +} + +class GreatIntelligencesPlanEffect extends OneShotEffect { + + private static final FaceVillainousChoice choice = new FaceVillainousChoice( + Outcome.Discard, new GreatIntelligencesPlanFirstChoice(), new GreatIntelligencesPlanSecondChoice() + ); + + GreatIntelligencesPlanEffect() { + super(Outcome.Benefit); + staticText = "then target opponent " + choice.generateRule(); + } + + private GreatIntelligencesPlanEffect(final GreatIntelligencesPlanEffect effect) { + super(effect); + } + + @Override + public GreatIntelligencesPlanEffect copy() { + return new GreatIntelligencesPlanEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(getTargetPointer().getFirst(game, source)); + return player != null && choice.faceChoice(player, game, source); + } +} + +class GreatIntelligencesPlanFirstChoice extends VillainousChoice { + GreatIntelligencesPlanFirstChoice() { + super("They discard three cards", "You discard three cards"); + } + + @Override + public boolean doChoice(Player player, Game game, Ability source) { + return !player.discard(3, false, false, source, game).isEmpty(); + } +} + +class GreatIntelligencesPlanSecondChoice extends VillainousChoice { + GreatIntelligencesPlanSecondChoice() { + super("you may cast a spell from your hand without paying its mana cost", "{controller} may cast a free spell from their hand"); + } + + @Override + public boolean doChoice(Player player, Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + return controller != null && !controller.getHand().isEmpty() + && CardUtil.castSpellWithAttributesForFree( + controller, source, game, new CardsImpl(controller.getHand()), StaticFilters.FILTER_CARD + ); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TheValeyard.java b/Mage.Sets/src/mage/cards/t/TheValeyard.java new file mode 100644 index 00000000000..96b479f3624 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheValeyard.java @@ -0,0 +1,111 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.VoteEvent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TheValeyard extends CardImpl { + + public TheValeyard(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.TIME_LORD); + this.subtype.add(SubType.NOBLE); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // If an opponent would face a villainous choice, they face that choice an additional time. + this.addAbility(new SimpleStaticAbility(new TheValeyardChoiceEffect())); + + // While voting, you may vote an additional time. + this.addAbility(new SimpleStaticAbility(new TheValeyardVoteEffect())); + } + + private TheValeyard(final TheValeyard card) { + super(card); + } + + @Override + public TheValeyard copy() { + return new TheValeyard(this); + } +} + +class TheValeyardChoiceEffect extends ReplacementEffectImpl { + + TheValeyardChoiceEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "if an opponent would face a villainous choice, they face that choice an additional time"; + } + + private TheValeyardChoiceEffect(final TheValeyardChoiceEffect effect) { + super(effect); + } + + @Override + public TheValeyardChoiceEffect copy() { + return new TheValeyardChoiceEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.FACE_VILLAINOUS_CHOICE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return game.getOpponents(event.getTargetId()).contains(source.getControllerId()); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + event.setAmount(event.getAmount() + 1); + return false; + } +} + +class TheValeyardVoteEffect extends ReplacementEffectImpl { + + TheValeyardVoteEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "while voting, you may vote an additional time"; + } + + private TheValeyardVoteEffect(final TheValeyardVoteEffect effect) { + super(effect); + } + + @Override + public TheValeyardVoteEffect copy() { + return new TheValeyardVoteEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.VOTE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return source.isControlledBy(event.getTargetId()); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + ((VoteEvent) event).incrementOptionalExtraVotes(); + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index 99c1feb14b3..9a5f6f6074d 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -93,6 +93,7 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("Glacial Fortress", 285, Rarity.RARE, mage.cards.g.GlacialFortress.class)); cards.add(new SetCardInfo("Graham O'Brien", 104, Rarity.RARE, mage.cards.g.GrahamOBrien.class)); cards.add(new SetCardInfo("Grasp of Fate", 208, Rarity.RARE, mage.cards.g.GraspOfFate.class)); + cards.add(new SetCardInfo("Great Intelligence's Plan", 133, Rarity.UNCOMMON, mage.cards.g.GreatIntelligencesPlan.class)); cards.add(new SetCardInfo("Growth Spiral", 237, Rarity.COMMON, mage.cards.g.GrowthSpiral.class)); cards.add(new SetCardInfo("Haunted Ridge", 286, Rarity.RARE, mage.cards.h.HauntedRidge.class)); cards.add(new SetCardInfo("Heaven Sent", 134, Rarity.RARE, mage.cards.h.HeavenSent.class)); @@ -185,6 +186,7 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("The Flux", 86, Rarity.RARE, mage.cards.t.TheFlux.class)); cards.add(new SetCardInfo("The Sixth Doctor", 159, Rarity.RARE, mage.cards.t.TheSixthDoctor.class)); cards.add(new SetCardInfo("The Thirteenth Doctor", 4, Rarity.MYTHIC, mage.cards.t.TheThirteenthDoctor.class)); + cards.add(new SetCardInfo("The Valeyard", 165, Rarity.RARE, mage.cards.t.TheValeyard.class)); cards.add(new SetCardInfo("Thespian's Stage", 323, Rarity.RARE, mage.cards.t.ThespiansStage.class)); cards.add(new SetCardInfo("Think Twice", 220, Rarity.COMMON, mage.cards.t.ThinkTwice.class)); cards.add(new SetCardInfo("Thought Vessel", 255, Rarity.UNCOMMON, mage.cards.t.ThoughtVessel.class)); diff --git a/Mage/src/main/java/mage/choices/FaceVillainousChoice.java b/Mage/src/main/java/mage/choices/FaceVillainousChoice.java new file mode 100644 index 00000000000..cebb798a74e --- /dev/null +++ b/Mage/src/main/java/mage/choices/FaceVillainousChoice.java @@ -0,0 +1,50 @@ +package mage.choices; + +import mage.abilities.Ability; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; + +/** + * @author TheElk801 + */ +public class FaceVillainousChoice { + + private final Outcome outcome; + private final VillainousChoice firstChoice; + private final VillainousChoice secondChoice; + + public FaceVillainousChoice(Outcome outcome, VillainousChoice firstChoice, VillainousChoice secondChoice) { + this.outcome = outcome; + this.firstChoice = firstChoice; + this.secondChoice = secondChoice; + } + + public boolean faceChoice(Player player, Game game, Ability source) { + GameEvent event = GameEvent.getEvent( + GameEvent.EventType.FACE_VILLAINOUS_CHOICE, + player.getId(), source, source.getControllerId(), 1 + ); + if (game.replaceEvent(event)) { + return false; + } + for (int i = 0; i < event.getAmount(); i++) { + handleChoice(player, game, source); + } + return true; + } + + private boolean handleChoice(Player player, Game game, Ability source) { + VillainousChoice chosenChoice = player.chooseUse( + outcome, "You face a villanous choice:", null, + firstChoice.getMessage(game, source), secondChoice.getMessage(game, source), source, game + ) ? firstChoice : secondChoice; + return chosenChoice.doChoice(player, game, source); + } + + public String generateRule() { + return "faces a villanous choice — " + firstChoice.getRule() + ", or " + secondChoice.getRule(); + } +} + diff --git a/Mage/src/main/java/mage/choices/VillainousChoice.java b/Mage/src/main/java/mage/choices/VillainousChoice.java new file mode 100644 index 00000000000..3c2719f4084 --- /dev/null +++ b/Mage/src/main/java/mage/choices/VillainousChoice.java @@ -0,0 +1,40 @@ +package mage.choices; + +import mage.abilities.Ability; +import mage.game.Game; +import mage.players.Player; + +import java.util.Objects; +import java.util.Optional; + +/** + * @author TheElk801 + */ +public abstract class VillainousChoice { + + private final String rule; + private final String message; + + protected VillainousChoice(String rule, String message) { + this.rule = rule; + this.message = message; + } + + public abstract boolean doChoice(Player player, Game game, Ability source); + + public String getRule() { + return rule; + } + + public String getMessage(Game game, Ability source) { + if (!message.contains("{controller}")) { + return message; + } + String controllerName = Optional + .ofNullable(game.getPlayer(source.getControllerId())) + .filter(Objects::nonNull) + .map(Player::getName) + .orElse("Opponent"); + return message.replace("{controller}", controllerName); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index e6bd27c7ad4..1d97b0f19ec 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -508,6 +508,14 @@ public class GameEvent implements Serializable { REMOVED_FROM_COMBAT, // targetId id of permanent removed from combat FORETOLD, // targetId id of card foretold FORETELL, // targetId id of card foretell playerId id of the controller + /* villainous choice + targetId player making the choice + sourceId sourceId of the ability forcing the choice + playerId controller of the ability forcing the choice + amount numner of times choice is repeated + flag not used for this event + */ + FACE_VILLAINOUS_CHOICE, //custom events CUSTOM_EVENT }