From 3503513c4ed70b65f94cfa98ccfcb9352db1818c Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 7 May 2023 14:32:28 -0400 Subject: [PATCH] Implement The Ring Tempts You mechanic (#10320) * remove skip * initial implementation of the ring mechanic * some changes * rework ring-bearer choosing * [LTR] Implement Call of the Ring * update ring-bearer condition --- Mage.Sets/src/mage/cards/c/CallOfTheRing.java | 71 +++++++ Mage.Sets/src/mage/cards/f/FrodoBaggins.java | 16 +- .../src/mage/cards/f/FrodoSauronsBane.java | 4 +- .../mage/cards/s/SauronTheNecromancer.java | 18 +- .../TheLordOfTheRingsTalesOfMiddleEarth.java | 7 +- .../java/org/mage/test/player/TestPlayer.java | 15 ++ .../java/org/mage/test/stub/PlayerStub.java | 14 ++ ...cksCreatureYouControlTriggeredAbility.java | 21 +- .../common/SourceIsRingBearerCondition.java | 30 +++ .../keyword/TheRingTemptsYouEffect.java | 4 +- Mage/src/main/java/mage/game/Game.java | 8 +- Mage/src/main/java/mage/game/GameImpl.java | 30 +++ .../game/command/emblems/TheRingEmblem.java | 191 ++++++++++++++++++ .../main/java/mage/game/events/GameEvent.java | 1 + .../java/mage/game/permanent/Permanent.java | 2 + .../mage/game/permanent/PermanentImpl.java | 7 +- Mage/src/main/java/mage/players/Player.java | 6 + .../main/java/mage/players/PlayerImpl.java | 62 ++++++ .../common/TemptedByTheRingWatcher.java | 49 +++++ 19 files changed, 508 insertions(+), 48 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CallOfTheRing.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/SourceIsRingBearerCondition.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/TheRingEmblem.java create mode 100644 Mage/src/main/java/mage/watchers/common/TemptedByTheRingWatcher.java diff --git a/Mage.Sets/src/mage/cards/c/CallOfTheRing.java b/Mage.Sets/src/mage/cards/c/CallOfTheRing.java new file mode 100644 index 0000000000..17719c4097 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CallOfTheRing.java @@ -0,0 +1,71 @@ +package mage.cards.c; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.keyword.TheRingTemptsYouEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CallOfTheRing extends CardImpl { + + public CallOfTheRing(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}"); + + // At the beginning of your upkeep, the Ring tempts you. + this.addAbility(new BeginningOfUpkeepTriggeredAbility( + new TheRingTemptsYouEffect(), TargetController.YOU, false + )); + + // Whenever you choose a creature as your Ring-bearer, you may pay 2 life. If you do, draw a card. + this.addAbility(new CallOfTheRingTriggeredAbility()); + } + + private CallOfTheRing(final CallOfTheRing card) { + super(card); + } + + @Override + public CallOfTheRing copy() { + return new CallOfTheRing(this); + } +} + +class CallOfTheRingTriggeredAbility extends TriggeredAbilityImpl { + + CallOfTheRingTriggeredAbility() { + super(Zone.BATTLEFIELD, new DoIfCostPaid(new DrawCardSourceControllerEffect(1), new PayLifeCost(2))); + setTriggerPhrase("Whenever you choose a creature as your Ring-bearer, "); + } + + private CallOfTheRingTriggeredAbility(final CallOfTheRingTriggeredAbility ability) { + super(ability); + } + + @Override + public CallOfTheRingTriggeredAbility copy() { + return new CallOfTheRingTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.RING_BEARER_CHOSEN; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return isControlledBy(event.getPlayerId()); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FrodoBaggins.java b/Mage.Sets/src/mage/cards/f/FrodoBaggins.java index c129180ed0..c96a9c2f6a 100644 --- a/Mage.Sets/src/mage/cards/f/FrodoBaggins.java +++ b/Mage.Sets/src/mage/cards/f/FrodoBaggins.java @@ -1,10 +1,9 @@ package mage.cards.f; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldThisOrAnotherTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SourceIsRingBearerCondition; import mage.abilities.decorator.ConditionalRequirementEffect; import mage.abilities.effects.common.combat.MustBeBlockedByAtLeastOneSourceEffect; import mage.abilities.effects.keyword.TheRingTemptsYouEffect; @@ -15,7 +14,6 @@ import mage.constants.SubType; import mage.constants.SuperType; import mage.filter.FilterPermanent; import mage.filter.common.FilterCreaturePermanent; -import mage.game.Game; import java.util.UUID; @@ -46,7 +44,7 @@ public final class FrodoBaggins extends CardImpl { // As long as Frodo is your Ring-bearer, it must be blocked if able. this.addAbility(new SimpleStaticAbility(new ConditionalRequirementEffect( - new MustBeBlockedByAtLeastOneSourceEffect(), FrodoBagginsCondition.instance, + new MustBeBlockedByAtLeastOneSourceEffect(), SourceIsRingBearerCondition.instance, "as long as {this} is your Ring-bearer, it must be blocked if able" ))); } @@ -60,13 +58,3 @@ public final class FrodoBaggins extends CardImpl { return new FrodoBaggins(this); } } - -enum FrodoBagginsCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - // TODO: Implement this - return false; - } -} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/f/FrodoSauronsBane.java b/Mage.Sets/src/mage/cards/f/FrodoSauronsBane.java index 64b208a933..ade7c36415 100644 --- a/Mage.Sets/src/mage/cards/f/FrodoSauronsBane.java +++ b/Mage.Sets/src/mage/cards/f/FrodoSauronsBane.java @@ -19,6 +19,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; +import mage.watchers.common.TemptedByTheRingWatcher; import java.util.UUID; @@ -80,7 +81,6 @@ enum FrodoSauronsBaneCondition implements Condition { @Override public boolean apply(Game game, Ability source) { - // TODO: Implement when mechanic is known - return false; + return TemptedByTheRingWatcher.getCount(source.getControllerId(), game) >= 4; } } diff --git a/Mage.Sets/src/mage/cards/s/SauronTheNecromancer.java b/Mage.Sets/src/mage/cards/s/SauronTheNecromancer.java index 3609851f96..c6d8d2e53d 100644 --- a/Mage.Sets/src/mage/cards/s/SauronTheNecromancer.java +++ b/Mage.Sets/src/mage/cards/s/SauronTheNecromancer.java @@ -6,6 +6,8 @@ import mage.abilities.Ability; import mage.abilities.common.AttacksTriggeredAbility; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; import mage.abilities.condition.Condition; +import mage.abilities.condition.InvertCondition; +import mage.abilities.condition.common.SourceIsRingBearerCondition; import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; @@ -38,7 +40,7 @@ public final class SauronTheNecromancer extends CardImpl { this.toughness = new MageInt(4); // Menace - this.addAbility(new MenaceAbility()); + this.addAbility(new MenaceAbility(false)); // Whenever Sauron, the Necromancer attacks, exile target creature card from your graveyard. Create a tapped and attacking token that's a copy of that card, except it's a 3/3 black Wraith with menace. At the beginning of the next end step, exile that token unless Sauron is your Ring-bearer. Ability ability = new AttacksTriggeredAbility(new SauronTheNecromancerEffect()); @@ -56,18 +58,10 @@ public final class SauronTheNecromancer extends CardImpl { } } -enum SauronTheNecromancerCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - // TODO: Implement this - return true; - } -} - class SauronTheNecromancerEffect extends OneShotEffect { + private static final Condition condition = new InvertCondition(SourceIsRingBearerCondition.instance); + SauronTheNecromancerEffect() { super(Outcome.Benefit); staticText = "exile target creature card from your graveyard. Create a tapped and attacking " + @@ -105,7 +99,7 @@ class SauronTheNecromancerEffect extends OneShotEffect { effect.apply(game, source); game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility( new ConditionalOneShotEffect( - new ExileTargetEffect(), SauronTheNecromancerCondition.instance, + new ExileTargetEffect(), condition, "exile that token unless {this} is your Ring-bearer" ).setTargetPointer(new FixedTargets(effect.getAddedPermanents(), game)) ), source); diff --git a/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java b/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java index 8245e33934..19d4f4a5ea 100644 --- a/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java +++ b/Mage.Sets/src/mage/sets/TheLordOfTheRingsTalesOfMiddleEarth.java @@ -4,12 +4,8 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - public final class TheLordOfTheRingsTalesOfMiddleEarth extends ExpansionSet { - private static final List unfinished = Arrays.asList("Bilbo, Retired Burglar", "Call of the Ring", "Frodo Baggins", "Frodo, Sauron's Bane", "Gollum, Patient Plotter", "Samwise the Stouthearted"); private static final TheLordOfTheRingsTalesOfMiddleEarth instance = new TheLordOfTheRingsTalesOfMiddleEarth(); public static TheLordOfTheRingsTalesOfMiddleEarth getInstance() { @@ -24,6 +20,7 @@ public final class TheLordOfTheRingsTalesOfMiddleEarth extends ExpansionSet { cards.add(new SetCardInfo("Aragorn and Arwen, Wed", 287, Rarity.MYTHIC, mage.cards.a.AragornAndArwenWed.class)); cards.add(new SetCardInfo("Bilbo, Retired Burglar", 403, Rarity.UNCOMMON, mage.cards.b.BilboRetiredBurglar.class)); + cards.add(new SetCardInfo("Call of the Ring", 79, Rarity.RARE, mage.cards.c.CallOfTheRing.class)); cards.add(new SetCardInfo("Forest", 280, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Frodo Baggins", 404, Rarity.UNCOMMON, mage.cards.f.FrodoBaggins.class)); cards.add(new SetCardInfo("Frodo, Sauron's Bane", 18, Rarity.RARE, mage.cards.f.FrodoSauronsBane.class)); @@ -45,7 +42,5 @@ public final class TheLordOfTheRingsTalesOfMiddleEarth extends ExpansionSet { cards.add(new SetCardInfo("Trailblazer's Boots", 398, Rarity.RARE, mage.cards.t.TrailblazersBoots.class)); cards.add(new SetCardInfo("Wizard's Rockets", 400, Rarity.COMMON, mage.cards.w.WizardsRockets.class)); cards.add(new SetCardInfo("You Cannot Pass!", 38, Rarity.UNCOMMON, mage.cards.y.YouCannotPass.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); // remove when mechanic is implemented } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index f5d116a09f..b8a53e0f3e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -4361,6 +4361,21 @@ public class TestPlayer implements Player { return computerPlayer.getPhyrexianColors(); } + @Override + public UUID getRingBearerId() { + return computerPlayer.getRingBearerId(); + } + + @Override + public Permanent getRingBearer(Game game) { + return computerPlayer.getRingBearer(game); + } + + @Override + public void chooseRingBearer(Game game) { + computerPlayer.chooseRingBearer(game); + } + @Override public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) { assertAliasSupportInChoices(false); diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index 5064fb3e1c..d434e329a6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -1419,6 +1419,20 @@ public class PlayerStub implements Player { return (new FilterMana()); } + @Override + public UUID getRingBearerId() { + return null; + } + + @Override + public Permanent getRingBearer(Game game) { + return null; + } + + @Override + public void chooseRingBearer(Game game) { + } + @Override public UserData getControllingPlayersUserData(Game game) { return null; diff --git a/Mage/src/main/java/mage/abilities/common/AttacksCreatureYouControlTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/AttacksCreatureYouControlTriggeredAbility.java index a203316ba8..d10ca5a7df 100644 --- a/Mage/src/main/java/mage/abilities/common/AttacksCreatureYouControlTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/AttacksCreatureYouControlTriggeredAbility.java @@ -4,7 +4,8 @@ import mage.MageObjectReference; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.Zone; -import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; @@ -16,7 +17,7 @@ import mage.util.CardUtil; */ public class AttacksCreatureYouControlTriggeredAbility extends TriggeredAbilityImpl { - protected final FilterControlledCreaturePermanent filter; + protected final FilterPermanent filter; protected final boolean setTargetPointer; protected boolean once = false; @@ -25,25 +26,29 @@ public class AttacksCreatureYouControlTriggeredAbility extends TriggeredAbilityI } public AttacksCreatureYouControlTriggeredAbility(Effect effect, boolean optional) { - this(effect, optional, new FilterControlledCreaturePermanent()); + this(effect, optional, StaticFilters.FILTER_CONTROLLED_CREATURE); } public AttacksCreatureYouControlTriggeredAbility(Effect effect, boolean optional, boolean setTargetPointer) { - this(effect, optional, new FilterControlledCreaturePermanent(), setTargetPointer); + this(effect, optional, StaticFilters.FILTER_CONTROLLED_CREATURE, setTargetPointer); } - public AttacksCreatureYouControlTriggeredAbility(Effect effect, boolean optional, FilterControlledCreaturePermanent filter) { + public AttacksCreatureYouControlTriggeredAbility(Effect effect, boolean optional, FilterPermanent filter) { this(effect, optional, filter, false); } - public AttacksCreatureYouControlTriggeredAbility(Effect effect, boolean optional, FilterControlledCreaturePermanent filter, boolean setTargetPointer) { - super(Zone.BATTLEFIELD, effect, optional); + public AttacksCreatureYouControlTriggeredAbility(Effect effect, boolean optional, FilterPermanent filter, boolean setTargetPointer) { + this(Zone.BATTLEFIELD, effect, optional, filter, setTargetPointer); + } + + public AttacksCreatureYouControlTriggeredAbility(Zone zone, Effect effect, boolean optional, FilterPermanent filter, boolean setTargetPointer) { + super(zone, effect, optional); this.filter = filter; this.setTargetPointer = setTargetPointer; setTriggerPhrase("Whenever " + CardUtil.addArticle(filter.getMessage()) + " attacks, "); } - public AttacksCreatureYouControlTriggeredAbility(AttacksCreatureYouControlTriggeredAbility ability) { + private AttacksCreatureYouControlTriggeredAbility(final AttacksCreatureYouControlTriggeredAbility ability) { super(ability); this.filter = ability.filter; this.setTargetPointer = ability.setTargetPointer; diff --git a/Mage/src/main/java/mage/abilities/condition/common/SourceIsRingBearerCondition.java b/Mage/src/main/java/mage/abilities/condition/common/SourceIsRingBearerCondition.java new file mode 100644 index 0000000000..045998c2a4 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/SourceIsRingBearerCondition.java @@ -0,0 +1,30 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; + +import java.util.Objects; +import java.util.Optional; + +/** + * @author TheElk801 + */ +public enum SourceIsRingBearerCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + return Optional + .ofNullable(source.getSourcePermanentIfItStillExists(game)) + .filter(Objects::nonNull) + .filter(permanent -> permanent.isControlledBy(source.getControllerId())) + .map(permanent -> permanent.isRingBearer(game)) + .orElse(false); + } + + @Override + public String toString() { + return "{this} is your Ring-bearer"; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/TheRingTemptsYouEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/TheRingTemptsYouEffect.java index ac12b00e50..35b6815bc6 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/TheRingTemptsYouEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/TheRingTemptsYouEffect.java @@ -26,7 +26,7 @@ public class TheRingTemptsYouEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - // TODO: Implement when we know what the mechanic does - return false; + game.temptWithTheRing(source.getControllerId()); + return true; } } diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 613dda36da..0178445dfc 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -406,6 +406,8 @@ public interface Game extends MageItem, Serializable, Copyable { void ventureIntoDungeon(UUID playerId, boolean undercity); + void temptWithTheRing(UUID playerId); + /** * Tells whether the current game has day or night, defaults to false */ @@ -552,8 +554,8 @@ public interface Game extends MageItem, Serializable, Copyable { /** * Function to call for a player to take the initiative. * - * @param source The ability granting initiative. - * @param initiativeId UUID of the player taking the initiative + * @param source The ability granting initiative. + * @param initiativeId UUID of the player taking the initiative */ void takeInitiative(Ability source, UUID initiativeId); @@ -681,6 +683,6 @@ public interface Game extends MageItem, Serializable, Copyable { void setGameStopped(boolean gameStopped); boolean isGameStopped(); - + boolean isTurnOrderReversed(); } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index e019922f51..0092e4228b 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -41,6 +41,7 @@ import mage.game.combat.Combat; import mage.game.combat.CombatGroup; import mage.game.command.*; import mage.game.command.dungeons.UndercityDungeon; +import mage.game.command.emblems.TheRingEmblem; import mage.game.events.*; import mage.game.events.TableEvent.EventType; import mage.game.mulligan.Mulligan; @@ -563,6 +564,34 @@ public abstract class GameImpl implements Game { fireEvent(GameEvent.getEvent(GameEvent.EventType.VENTURED, playerId, null, playerId)); } + private TheRingEmblem getOrCreateTheRing(UUID playerId) { + TheRingEmblem emblem = state + .getCommand() + .stream() + .filter(TheRingEmblem.class::isInstance) + .map(TheRingEmblem.class::cast) + .filter(commandObject -> commandObject.isControlledBy(playerId)) + .findFirst() + .orElse(null); + if (emblem != null) { + return emblem; + } + TheRingEmblem newEmblem = new TheRingEmblem(playerId); + state.addCommandObject(newEmblem); + return newEmblem; + } + + @Override + public void temptWithTheRing(UUID playerId) { + Player player = getPlayer(playerId); + if (player == null) { + return; + } + player.chooseRingBearer(this); + getOrCreateTheRing(playerId).addNextAbility(this); + fireEvent(GameEvent.getEvent(GameEvent.EventType.TEMPTED_BY_RING, playerId, null, playerId)); + } + @Override public boolean hasDayNight() { return state.isHasDayNight(); @@ -1302,6 +1331,7 @@ public abstract class GameImpl implements Game { newWatchers.add(new EndStepCountWatcher()); newWatchers.add(new CommanderPlaysCountWatcher()); // commander plays count uses in non commander games by some cards newWatchers.add(new CreaturesDiedWatcher()); + newWatchers.add(new TemptedByTheRingWatcher()); // runtime check - allows only GAME scope (one watcher per game) newWatchers.forEach(watcher -> { diff --git a/Mage/src/main/java/mage/game/command/emblems/TheRingEmblem.java b/Mage/src/main/java/mage/game/command/emblems/TheRingEmblem.java new file mode 100644 index 0000000000..002ca15676 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/TheRingEmblem.java @@ -0,0 +1,191 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.AttacksCreatureYouControlTriggeredAbility; +import mage.abilities.common.DealsDamageToAPlayerAllTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.delayed.AtTheEndOfCombatDelayedTriggeredAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.RestrictionEffect; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.effects.common.DrawDiscardControllerEffect; +import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.abilities.effects.common.SacrificeTargetEffect; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicate; +import mage.game.Game; +import mage.game.command.Emblem; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.targetpointer.FixedTarget; +import mage.watchers.common.TemptedByTheRingWatcher; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TheRingEmblem extends Emblem { + private static final FilterPermanent filter = new FilterControlledPermanent("your Ring-bearer"); + + static { + filter.add(TheRingEmblemPredicate.instance); + } + + public TheRingEmblem(UUID controllerId) { + super(); + this.setName("The Ring"); + this.setExpansionSetCodeForImage("LTR"); + this.setControllerId(controllerId); + } + + public void addNextAbility(Game game) { + Ability ability; + switch (TemptedByTheRingWatcher.getCount(this.getControllerId(), game)) { + case 0: + // Your Ring-bearer is legendary and can't be blocked by creatures with greater power. + ability = new SimpleStaticAbility(Zone.COMMAND, new TheRingEmblemLegendaryEffect()); + ability.addEffect(new TheRingEmblemEvasionEffect()); + break; + case 1: + // Whenever your Ring-bearer attacks, draw a card, then discard a card. + ability = new AttacksCreatureYouControlTriggeredAbility( + Zone.COMMAND, + new DrawDiscardControllerEffect(1, 1), + false, filter, false + ).setTriggerPhrase("Whenever your Ring-bearer attacks, "); + break; + case 2: + // Whenever your Ring-bearer becomes blocked by a creature, that creature's controller sacrifices it at end of combat. + ability = new TheRingEmblemTriggeredAbility(); + break; + case 3: + // Whenever your Ring-bearer deals combat damage to a player, each opponent loses 3 life. + ability = new DealsDamageToAPlayerAllTriggeredAbility( + Zone.COMMAND, new LoseLifeOpponentsEffect(3), filter, false, + SetTargetPointer.NONE, true, false + ); + break; + default: + return; + } + this.getAbilities().add(ability); + ability.setSourceId(this.getId()); + ability.setControllerId(this.getControllerId()); + game.getState().addAbility(ability, this); + } +} + +enum TheRingEmblemPredicate implements Predicate { + instance; + + @Override + public boolean apply(Permanent input, Game game) { + return input.isRingBearer(game); + } +} + +class TheRingEmblemLegendaryEffect extends ContinuousEffectImpl { + + TheRingEmblemLegendaryEffect() { + super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Benefit); + staticText = "your Ring-bearer is legendary"; + } + + private TheRingEmblemLegendaryEffect(final TheRingEmblemLegendaryEffect effect) { + super(effect); + } + + @Override + public TheRingEmblemLegendaryEffect copy() { + return new TheRingEmblemLegendaryEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = Optional + .ofNullable(game.getPlayer(source.getControllerId())) + .filter(Objects::nonNull) + .map(player -> player.getRingBearer(game)) + .orElse(null); + if (permanent == null) { + return false; + } + permanent.addSuperType(SuperType.LEGENDARY); + return true; + } +} + +class TheRingEmblemEvasionEffect extends RestrictionEffect { + + TheRingEmblemEvasionEffect() { + super(Duration.WhileOnBattlefield); + staticText = "and can't be blocked by creatures with greater power"; + } + + private TheRingEmblemEvasionEffect(final TheRingEmblemEvasionEffect effect) { + super(effect); + } + + @Override + public TheRingEmblemEvasionEffect copy() { + return new TheRingEmblemEvasionEffect(this); + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return permanent.isControlledBy(source.getControllerId()) + && permanent.isRingBearer(game); + } + + @Override + public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) { + return blocker.getPower().getValue() <= attacker.getPower().getValue(); + } +} + +class TheRingEmblemTriggeredAbility extends TriggeredAbilityImpl { + + TheRingEmblemTriggeredAbility() { + super(Zone.COMMAND, new CreateDelayedTriggeredAbilityEffect(new AtTheEndOfCombatDelayedTriggeredAbility(new SacrificeTargetEffect()))); + } + + private TheRingEmblemTriggeredAbility(final TheRingEmblemTriggeredAbility ability) { + super(ability); + } + + @Override + public TheRingEmblemTriggeredAbility copy() { + return new TheRingEmblemTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CREATURE_BLOCKED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Permanent attacker = game.getPermanent(event.getTargetId()); + Permanent blocker = game.getPermanent(event.getSourceId()); + if (attacker == null + || blocker == null + || attacker.isControlledBy(getControllerId()) + || !attacker.isRingBearer(game)) { + return false; + } + this.getEffects().setTargetPointer(new FixedTarget(blocker, game)); + return true; + } + + @Override + public String getRule() { + return "Whenever your Ring-bearer becomes blocked by a creature, " + + "that creature's controller sacrifices it at end of combat."; + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index f9ee0714fe..d1bb13d8a4 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -493,6 +493,7 @@ public class GameEvent implements Serializable { ROOM_ENTERED, VENTURE, VENTURED, DUNGEON_COMPLETED, + TEMPTED_BY_RING, RING_BEARER_CHOSEN, 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 diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 4026dc42bc..c7902d412f 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -419,6 +419,8 @@ public interface Permanent extends Card, Controllable { boolean isManifested(); + boolean isRingBearer(Game game); + @Override Permanent copy(); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index 9f8785fcd6..affd3017a3 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -37,7 +37,6 @@ import mage.players.Player; import mage.target.TargetPlayer; import mage.util.CardUtil; import mage.util.GameLog; -import mage.util.RandomUtil; import mage.util.ThreadLocalStringBuilder; import org.apache.log4j.Logger; @@ -1813,6 +1812,12 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.secondSideCard = card; } + @Override + public boolean isRingBearer(Game game) { + Player player = game.getPlayer(getControllerId()); + return player != null && this.equals(player.getRingBearer(game)); + } + @Override public boolean fight(Permanent fightTarget, Ability source, Game game) { return this.fight(fightTarget, source, game, true); diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index f71611ba90..16de75751f 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -1080,6 +1080,12 @@ public interface Player extends MageItem, Copyable { */ FilterMana getPhyrexianColors(); + UUID getRingBearerId(); + + Permanent getRingBearer(Game game); + + void chooseRingBearer(Game game); + /** * Function to query if the player has strictChooseMode enabled. Only the test player can have it. * Function is added here so that the test suite project does not have to be imported into the client/server project. diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 3082001de0..36624034e1 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -58,6 +58,7 @@ import mage.target.TargetAmount; import mage.target.TargetCard; import mage.target.TargetPermanent; import mage.target.common.TargetCardInLibrary; +import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetDiscard; import mage.util.CardUtil; import mage.util.GameLog; @@ -176,6 +177,8 @@ public abstract class PlayerImpl implements Player, Serializable { // mana colors the player can handle like Phyrexian mana protected FilterMana phyrexianColors; + protected UUID ringBearerId = null; + // Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy) protected final List> availableTriggeredManaList = new ArrayList<>(); @@ -285,6 +288,7 @@ public abstract class PlayerImpl implements Player, Serializable { } this.payManaMode = player.payManaMode; this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null; + this.ringBearerId = player.ringBearerId; for (Designation object : player.designations) { this.designations.add(object.copy()); } @@ -372,6 +376,8 @@ public abstract class PlayerImpl implements Player, Serializable { this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null; + this.ringBearerId = player.getRingBearerId(); + this.designations.clear(); for (Designation object : player.getDesignations()) { this.designations.add(object.copy()); @@ -5127,6 +5133,62 @@ public abstract class PlayerImpl implements Player, Serializable { return this.phyrexianColors; } + @Override + public UUID getRingBearerId() { + return ringBearerId; + } + + @Override + public Permanent getRingBearer(Game game) { + if (ringBearerId == null) { + return null; + } + Permanent bearer = game.getPermanent(ringBearerId); + if (bearer != null && bearer.isControlledBy(getId())) { + return bearer; + } + ringBearerId = null; + return null; + } + + @Override + public void chooseRingBearer(Game game) { + Permanent currentBearer = getRingBearer(game); + int creatureCount = game.getBattlefield().count( + StaticFilters.FILTER_CONTROLLED_CREATURE, getId(), null, game + ); + boolean mustChoose; + if (currentBearer == null) { + if (creatureCount > 0) { + mustChoose = true; + } else { + return; + } + } else if (currentBearer.isCreature(game)) { + if (creatureCount > 1) { + mustChoose = false; + } else { + return; + } + } else if (creatureCount > 0) { + mustChoose = false; + } else { + return; + } + if (!mustChoose && !chooseUse(Outcome.Neutral, "Choose a new Ring-bearer?", null, game)) { + return; + } + TargetPermanent target = new TargetControlledCreaturePermanent(); + target.setNotTarget(true); + target.withChooseHint("to be your Ring-bearer"); + choose(Outcome.Neutral, target, null, game); + UUID newBearerId = target.getFirstTarget(); + if (game.getPermanent(newBearerId) != null) { + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.RING_BEARER_CHOSEN, newBearerId, null, getId())); + this.ringBearerId = newBearerId; + } + } + @Override public ActivatedAbility chooseLandOrSpellAbility(Card card, Game game, boolean noMana) { return card.getSpellAbility(); diff --git a/Mage/src/main/java/mage/watchers/common/TemptedByTheRingWatcher.java b/Mage/src/main/java/mage/watchers/common/TemptedByTheRingWatcher.java new file mode 100644 index 0000000000..f8864fd47d --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/TemptedByTheRingWatcher.java @@ -0,0 +1,49 @@ +package mage.watchers.common; + +import mage.constants.WatcherScope; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class TemptedByTheRingWatcher extends Watcher { + + private final Map map = new HashMap<>(); + + public TemptedByTheRingWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + switch (event.getType()) { + case TEMPTED_BY_RING: + map.compute(event.getPlayerId(), CardUtil::setOrIncrementValue); + return; + case BEGINNING_PHASE_PRE: + if (game.getTurnNum() == 1) { + map.clear(); + } + } + } + + @Override + public void reset() { + super.reset(); + } + + public static int getCount(UUID playerId, Game game) { + return game + .getState() + .getWatcher(TemptedByTheRingWatcher.class) + .map + .getOrDefault(playerId, 0); + } +}