From 5a809f6fe4108b201e3a5507d402269c26ab5bc4 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Thu, 25 Jan 2024 20:30:51 -0500 Subject: [PATCH] Implementing "suspected" mechanic (#11670) * [MKM] Implement Agrus Kos, Spirit of Justice * rework effects * [MKM] Implement Airtight Alibi * [MKM] Implement Convenient Target * [MKM] Implement Repeat Offender * add test * add more tests * add tooltip for suspected * implement requested changes --- .../mage/cards/a/AgrusKosSpiritOfJustice.java | 84 +++++++++++ Mage.Sets/src/mage/cards/a/AirtightAlibi.java | 138 ++++++++++++++++++ .../src/mage/cards/c/ConvenientTarget.java | 90 ++++++++++++ .../src/mage/cards/r/RepeatOffender.java | 74 ++++++++++ .../src/mage/sets/MurdersAtKarlovManor.java | 4 + .../test/cards/continuous/SuspectTest.java | 124 ++++++++++++++++ ...tersEffect.java => ApplyStatusEffect.java} | 16 +- .../abilities/effects/ContinuousEffects.java | 15 +- .../permanent/SuspectedPredicate.java | 17 +++ Mage/src/main/java/mage/game/GameImpl.java | 5 +- .../main/java/mage/game/events/GameEvent.java | 6 + .../java/mage/game/permanent/Permanent.java | 7 +- .../mage/game/permanent/PermanentImpl.java | 37 ++++- 13 files changed, 594 insertions(+), 23 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/a/AgrusKosSpiritOfJustice.java create mode 100644 Mage.Sets/src/mage/cards/a/AirtightAlibi.java create mode 100644 Mage.Sets/src/mage/cards/c/ConvenientTarget.java create mode 100644 Mage.Sets/src/mage/cards/r/RepeatOffender.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/SuspectTest.java rename Mage/src/main/java/mage/abilities/effects/{ApplyCountersEffect.java => ApplyStatusEffect.java} (74%) create mode 100644 Mage/src/main/java/mage/filter/predicate/permanent/SuspectedPredicate.java diff --git a/Mage.Sets/src/mage/cards/a/AgrusKosSpiritOfJustice.java b/Mage.Sets/src/mage/cards/a/AgrusKosSpiritOfJustice.java new file mode 100644 index 00000000000..52232498317 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AgrusKosSpiritOfJustice.java @@ -0,0 +1,84 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AgrusKosSpiritOfJustice extends CardImpl { + + public AgrusKosSpiritOfJustice(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.DETECTIVE); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Double strike + this.addAbility(DoubleStrikeAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Whenever Agrus Kos, Spirit of Justice enters the battlefield or attacks, choose up to one target creature. If it's suspected, exile it. Otherwise, suspect it. + Ability ability = new EntersBattlefieldOrAttacksSourceTriggeredAbility(new AgrusKosSpiritOfJusticeEffect()); + ability.addTarget(new TargetCreaturePermanent(0, 1)); + this.addAbility(ability); + } + + private AgrusKosSpiritOfJustice(final AgrusKosSpiritOfJustice card) { + super(card); + } + + @Override + public AgrusKosSpiritOfJustice copy() { + return new AgrusKosSpiritOfJustice(this); + } +} + +class AgrusKosSpiritOfJusticeEffect extends OneShotEffect { + + AgrusKosSpiritOfJusticeEffect() { + super(Outcome.Benefit); + staticText = "choose up to one target creature. If it's suspected, exile it. Otherwise, suspect it"; + } + + private AgrusKosSpiritOfJusticeEffect(final AgrusKosSpiritOfJusticeEffect effect) { + super(effect); + } + + @Override + public AgrusKosSpiritOfJusticeEffect copy() { + return new AgrusKosSpiritOfJusticeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + if (!permanent.isSuspected()) { + permanent.setSuspected(true, game, source); + return true; + } + Player player = game.getPlayer(source.getControllerId()); + return player != null && player.moveCards(permanent, Zone.EXILED, source, game); + } +} diff --git a/Mage.Sets/src/mage/cards/a/AirtightAlibi.java b/Mage.Sets/src/mage/cards/a/AirtightAlibi.java new file mode 100644 index 00000000000..b534379c2fc --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AirtightAlibi.java @@ -0,0 +1,138 @@ +package mage.cards.a; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.UntapAttachedEffect; +import mage.abilities.effects.common.continuous.BoostEnchantedEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.HexproofAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AirtightAlibi extends CardImpl { + + public AirtightAlibi(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); + + this.subtype.add(SubType.AURA); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget)); + + // When Airtight Alibi enters the battlefield, untap enchanted creature. It gains hexproof until end of turn. If it's suspected, it's no longer suspected. + Ability ability = new EntersBattlefieldTriggeredAbility(new UntapAttachedEffect()); + ability.addEffect(new GainAbilityAttachedEffect( + HexproofAbility.getInstance(), AttachmentType.AURA, Duration.EndOfTurn + ).setText("It gains hexproof until end of turn")); + ability.addEffect(new AirtightAlibiBecomeSuspectedEffect()); + this.addAbility(ability); + + // Enchanted creature gets +2/+2 and can't become suspected. + ability = new SimpleStaticAbility(new BoostEnchantedEffect(2, 2)); + ability.addEffect(new AirtightAlibiReplacementEffect()); + this.addAbility(ability); + } + + private AirtightAlibi(final AirtightAlibi card) { + super(card); + } + + @Override + public AirtightAlibi copy() { + return new AirtightAlibi(this); + } +} + +class AirtightAlibiBecomeSuspectedEffect extends OneShotEffect { + + AirtightAlibiBecomeSuspectedEffect() { + super(Outcome.Benefit); + staticText = "If it's suspected, it's no longer suspected"; + } + + private AirtightAlibiBecomeSuspectedEffect(final AirtightAlibiBecomeSuspectedEffect effect) { + super(effect); + } + + @Override + public AirtightAlibiBecomeSuspectedEffect copy() { + return new AirtightAlibiBecomeSuspectedEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = Optional + .ofNullable(source.getSourcePermanentOrLKI(game)) + .filter(Objects::nonNull) + .map(Permanent::getAttachedTo) + .map(game::getPermanent) + .orElse(null); + if (permanent == null || !permanent.isSuspected()) { + return false; + } + permanent.setSuspected(false, game, source); + return true; + } +} + +class AirtightAlibiReplacementEffect extends ReplacementEffectImpl { + + AirtightAlibiReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "and can't become suspected"; + } + + private AirtightAlibiReplacementEffect(final AirtightAlibiReplacementEffect effect) { + super(effect); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + return true; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.BECOME_SUSPECTED; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return Optional + .ofNullable(source.getSourcePermanentIfItStillExists(game)) + .filter(Objects::nonNull) + .map(Permanent::getAttachedTo) + .map(event.getTargetId()::equals) + .orElse(false); + } + + @Override + public AirtightAlibiReplacementEffect copy() { + return new AirtightAlibiReplacementEffect(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/ConvenientTarget.java b/Mage.Sets/src/mage/cards/c/ConvenientTarget.java new file mode 100644 index 00000000000..7ae1cd1f9fc --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ConvenientTarget.java @@ -0,0 +1,90 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.ReturnSourceFromGraveyardToHandEffect; +import mage.abilities.effects.common.continuous.BoostEnchantedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ConvenientTarget extends CardImpl { + + public ConvenientTarget(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{R}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget)); + + // When Convenient Target enters the battlefield, suspect enchanted creature. + this.addAbility(new EntersBattlefieldTriggeredAbility(new ConvenientTargetEffect())); + + // Enchanted creature gets +1/+1. + this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(1, 1))); + + // {2}{R}: Return Convenient Target from your graveyard to your hand. + this.addAbility(new SimpleActivatedAbility( + Zone.GRAVEYARD, new ReturnSourceFromGraveyardToHandEffect(), new ManaCostsImpl<>("{2}{R}") + )); + } + + private ConvenientTarget(final ConvenientTarget card) { + super(card); + } + + @Override + public ConvenientTarget copy() { + return new ConvenientTarget(this); + } +} + +class ConvenientTargetEffect extends OneShotEffect { + + ConvenientTargetEffect() { + super(Outcome.Benefit); + staticText = "suspect enchanted creature"; + } + + private ConvenientTargetEffect(final ConvenientTargetEffect effect) { + super(effect); + } + + @Override + public ConvenientTargetEffect copy() { + return new ConvenientTargetEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Optional + .ofNullable(source.getSourcePermanentOrLKI(game)) + .map(Permanent::getAttachedTo) + .map(game::getPermanent) + .ifPresent(permanent -> permanent.setSuspected(true, game, source)); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/r/RepeatOffender.java b/Mage.Sets/src/mage/cards/r/RepeatOffender.java new file mode 100644 index 00000000000..592d0b74864 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RepeatOffender.java @@ -0,0 +1,74 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RepeatOffender extends CardImpl { + + public RepeatOffender(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ASSASSIN); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // {2}{B}: If Repeat Offender is suspected, put a +1/+1 counter on it. Otherwise, suspect it. + this.addAbility(new SimpleActivatedAbility(new RepeatOffenderEffect(), new ManaCostsImpl<>("{2}{B}"))); + } + + private RepeatOffender(final RepeatOffender card) { + super(card); + } + + @Override + public RepeatOffender copy() { + return new RepeatOffender(this); + } +} + +class RepeatOffenderEffect extends OneShotEffect { + + RepeatOffenderEffect() { + super(Outcome.Benefit); + staticText = "if {this} is suspected, put a +1/+1 counter on it. Otherwise, suspect it"; + } + + private RepeatOffenderEffect(final RepeatOffenderEffect effect) { + super(effect); + } + + @Override + public RepeatOffenderEffect copy() { + return new RepeatOffenderEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + return false; + } + if (permanent.isSuspected()) { + return permanent.addCounters(CounterType.P1P1.createInstance(), source, game); + } + permanent.setSuspected(true, game, source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java b/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java index 825b77fe363..883ab07b2a3 100644 --- a/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java +++ b/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java @@ -26,7 +26,9 @@ public final class MurdersAtKarlovManor extends ExpansionSet { this.hasBasicLands = true; this.hasBoosters = false; // temporary + cards.add(new SetCardInfo("Agrus Kos, Spirit of Justice", 184, Rarity.MYTHIC, mage.cards.a.AgrusKosSpiritOfJustice.class)); cards.add(new SetCardInfo("Aftermath Analyst", 148, Rarity.UNCOMMON, mage.cards.a.AftermathAnalyst.class)); + cards.add(new SetCardInfo("Airtight Alibi", 149, Rarity.COMMON, mage.cards.a.AirtightAlibi.class)); cards.add(new SetCardInfo("Alley Assailant", 76, Rarity.COMMON, mage.cards.a.AlleyAssailant.class)); cards.add(new SetCardInfo("Alquist Proft, Master Sleuth", 185, Rarity.MYTHIC, mage.cards.a.AlquistProftMasterSleuth.class)); cards.add(new SetCardInfo("Anzrag, the Quake-Mole", 186, Rarity.MYTHIC, mage.cards.a.AnzragTheQuakeMole.class)); @@ -45,6 +47,7 @@ public final class MurdersAtKarlovManor extends ExpansionSet { cards.add(new SetCardInfo("Cold Case Cracker", 46, Rarity.COMMON, mage.cards.c.ColdCaseCracker.class)); cards.add(new SetCardInfo("Commercial District", 259, Rarity.RARE, mage.cards.c.CommercialDistrict.class)); cards.add(new SetCardInfo("Concealed Weapon", 117, Rarity.UNCOMMON, mage.cards.c.ConcealedWeapon.class)); + cards.add(new SetCardInfo("Convenient Target", 119, Rarity.UNCOMMON, mage.cards.c.ConvenientTarget.class)); cards.add(new SetCardInfo("Crime Novelist", 121, Rarity.UNCOMMON, mage.cards.c.CrimeNovelist.class)); cards.add(new SetCardInfo("Culvert Ambusher", 158, Rarity.UNCOMMON, mage.cards.c.CulvertAmbusher.class)); cards.add(new SetCardInfo("Curious Cadaver", 194, Rarity.UNCOMMON, mage.cards.c.CuriousCadaver.class)); @@ -124,6 +127,7 @@ public final class MurdersAtKarlovManor extends ExpansionSet { cards.add(new SetCardInfo("Rakish Scoundrel", 225, Rarity.COMMON, mage.cards.r.RakishScoundrel.class)); cards.add(new SetCardInfo("Raucous Theater", 266, Rarity.RARE, mage.cards.r.RaucousTheater.class)); cards.add(new SetCardInfo("Red Herring", 142, Rarity.COMMON, mage.cards.r.RedHerring.class)); + cards.add(new SetCardInfo("Repeat Offender", 101, Rarity.COMMON, mage.cards.r.RepeatOffender.class)); cards.add(new SetCardInfo("Riftburst Hellion", 228, Rarity.COMMON, mage.cards.r.RiftburstHellion.class)); cards.add(new SetCardInfo("Rot Farm Mortipede", 102, Rarity.COMMON, mage.cards.r.RotFarmMortipede.class)); cards.add(new SetCardInfo("Rubblebelt Maverick", 174, Rarity.COMMON, mage.cards.r.RubblebeltMaverick.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/SuspectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/SuspectTest.java new file mode 100644 index 00000000000..3424d05ce20 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/SuspectTest.java @@ -0,0 +1,124 @@ +package org.mage.test.cards.continuous; + +import mage.abilities.keyword.MenaceAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class SuspectTest extends CardTestPlayerBase { + + private static final String offender = "Repeat Offender"; + private static final String bear = "Grizzly Bears"; + private static final String alibi = "Airtight Alibi"; + + private final void assertSuspected(String name, boolean suspected) { + Permanent permanent = getPermanent(name); + Assert.assertEquals("Permanent should " + (suspected ? "" : "not ") + "be suspected", suspected, permanent.isSuspected()); + if (suspected) { + permanent.getAbilities().containsClass(MenaceAbility.class); + } + } + + @Test + public void testNoSuspect() { + addCard(Zone.BATTLEFIELD, playerA, offender); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertSuspected(offender, false); + } + + @Test + public void testSuspect() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerA, offender); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B}"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertSuspected(offender, true); + } + + @Test + public void testIsSuspected() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3 + 3); + addCard(Zone.BATTLEFIELD, playerA, offender); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B}"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B}"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertSuspected(offender, true); + assertCounterCount(playerA, offender, CounterType.P1P1, 1); + } + + @Test + public void testCantBlock() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + addCard(Zone.BATTLEFIELD, playerA, offender); + addCard(Zone.BATTLEFIELD, playerB, bear); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B}"); + + attack(2, playerB, bear); + block(2, playerA, offender, bear); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertSuspected(offender, true); + assertTapped(bear, true); + assertLife(playerA, 20 - 2); + } + + @Test + public void testAlibiRemoveSuspect() { + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 3 + 3); + addCard(Zone.BATTLEFIELD, playerA, offender); + addCard(Zone.HAND, playerA, alibi); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B}"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, alibi, offender); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertSuspected(offender, false); + } + + @Test + public void testAlibiPreventSuspect() { + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 3 + 3); + addCard(Zone.BATTLEFIELD, playerA, offender); + addCard(Zone.HAND, playerA, alibi); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, alibi, offender); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{2}{B}"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertSuspected(offender, false); + assertCounterCount(offender, CounterType.P1P1, 0); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/ApplyCountersEffect.java b/Mage/src/main/java/mage/abilities/effects/ApplyStatusEffect.java similarity index 74% rename from Mage/src/main/java/mage/abilities/effects/ApplyCountersEffect.java rename to Mage/src/main/java/mage/abilities/effects/ApplyStatusEffect.java index 087b591b5f6..591c3f0153b 100644 --- a/Mage/src/main/java/mage/abilities/effects/ApplyCountersEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/ApplyStatusEffect.java @@ -1,6 +1,7 @@ package mage.abilities.effects; import mage.abilities.Ability; +import mage.abilities.keyword.MenaceAbility; import mage.constants.*; import mage.counters.AbilityCounter; import mage.counters.BoostCounter; @@ -9,14 +10,16 @@ import mage.game.permanent.Permanent; /** * @author BetaSteward_at_googlemail.com + *

+ * Applies boost from from boost counters and also adds abilities from ability counters and suspected mechanic */ -public class ApplyCountersEffect extends ContinuousEffectImpl { +public class ApplyStatusEffect extends ContinuousEffectImpl { - ApplyCountersEffect() { + ApplyStatusEffect() { super(Duration.EndOfGame, Outcome.BoostCreature); } - private ApplyCountersEffect(ApplyCountersEffect effect) { + private ApplyStatusEffect(ApplyStatusEffect effect) { super(effect); } @@ -32,6 +35,9 @@ public class ApplyCountersEffect extends ContinuousEffectImpl { for (AbilityCounter counter : permanent.getCounters(game).getAbilityCounters()) { permanent.addAbility(counter.getAbility(), source == null ? permanent.getId() : source.getSourceId(), game); } + if (permanent.isSuspected()) { + permanent.addAbility(new MenaceAbility(false), source == null ? permanent.getId() : source.getSourceId(), game); + } } } if (layer == Layer.PTChangingEffects_7 && sublayer == SubLayer.Counters_7d) { @@ -51,7 +57,7 @@ public class ApplyCountersEffect extends ContinuousEffectImpl { } @Override - public ApplyCountersEffect copy() { - return new ApplyCountersEffect(this); + public ApplyStatusEffect copy() { + return new ApplyStatusEffect(this); } } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 3f8ea6bf3ff..4f8f360faa5 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1,7 +1,6 @@ package mage.abilities.effects; import mage.ApprovingObject; -import mage.MageIdentifier; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.MageSingleton; @@ -9,8 +8,6 @@ import mage.abilities.StaticAbility; import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect; import mage.abilities.effects.common.continuous.CommanderReplacementEffect; import mage.cards.*; -import mage.choices.Choice; -import mage.choices.ChoiceImpl; import mage.constants.*; import mage.filter.FilterCard; import mage.filter.predicate.Predicate; @@ -55,19 +52,19 @@ public class ContinuousEffects implements Serializable { private final Map> asThoughEffectsMap = new EnumMap<>(AsThoughEffectType.class); public final List> allEffectsLists = new ArrayList<>(); // contains refs to real effect's list - private final ApplyCountersEffect applyCounters; + private final ApplyStatusEffect applyStatus; private final AuraReplacementEffect auraReplacementEffect; private final Map> lastEffectsListOnLayer = new HashMap<>(); // helps to find out new effect timestamps on layers public ContinuousEffects() { - applyCounters = new ApplyCountersEffect(); + applyStatus = new ApplyStatusEffect(); auraReplacementEffect = new AuraReplacementEffect(); collectAllEffects(); } protected ContinuousEffects(final ContinuousEffects effect) { - applyCounters = effect.applyCounters.copy(); + applyStatus = effect.applyStatus.copy(); auraReplacementEffect = effect.auraReplacementEffect.copy(); layeredEffects = effect.layeredEffects.copy(); continuousRuleModifyingEffects = effect.continuousRuleModifyingEffects.copy(); @@ -995,7 +992,7 @@ public class ContinuousEffects implements Serializable { boolean done = false; Map> waitingEffects = new LinkedHashMap<>(); Set appliedEffects = new HashSet<>(); - applyCounters.apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, null, game); + applyStatus.apply(Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, null, game); activeLayerEffects = getLayeredEffects(game, "layer_6"); while (!done) { // loop needed if a added effect adds again an effect (e.g. Level 5- of Joraga Treespeaker) @@ -1108,7 +1105,7 @@ public class ContinuousEffects implements Serializable { } } - applyCounters.apply(Layer.PTChangingEffects_7, SubLayer.Counters_7d, null, game); + applyStatus.apply(Layer.PTChangingEffects_7, SubLayer.Counters_7d, null, game); for (ContinuousEffect effect : layer) { Set abilities = layeredEffects.getAbility(effect.getId()); @@ -1410,7 +1407,7 @@ public class ContinuousEffects implements Serializable { for (Map.Entry> entry : asThoughEffectsMap.entrySet()) { logger.info("... " + entry.getKey().toString() + ": " + entry.getValue().size()); } - logger.info("applyCounters ....................: " + (applyCounters != null ? "exists" : "null")); + logger.info("applyStatus ....................: " + (applyStatus != null ? "exists" : "null")); logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists" : "null")); Map orderedEffects = new TreeMap<>(); traceAddContinuousEffects(orderedEffects, layeredEffects, game, "layeredEffects................"); diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/SuspectedPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/SuspectedPredicate.java new file mode 100644 index 00000000000..1d778fbcf09 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/permanent/SuspectedPredicate.java @@ -0,0 +1,17 @@ +package mage.filter.predicate.permanent; + +import mage.filter.predicate.Predicate; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * @author TheElk801 + */ +public enum SuspectedPredicate implements Predicate { + instance; + + @Override + public boolean apply(Permanent input, Game game) { + return input.isSuspected(); + } +} diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 2f9e48cf098..aa1f5df9ce4 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1993,7 +1993,7 @@ public abstract class GameImpl implements Game { } newBluePrint.assignNewId(); if (copyFromPermanent.isTransformed()) { - TransformAbility.transformPermanent(newBluePrint,this, source); + TransformAbility.transformPermanent(newBluePrint, this, source); } if (copyFromPermanent.isPrototyped()) { Abilities abilities = copyFromPermanent.getAbilities(); @@ -3552,8 +3552,9 @@ public abstract class GameImpl implements Game { public Map> getPermanentCostsTags() { return state.getPermanentCostsTags(); } + @Override - public void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source){ + public void storePermanentCostsTags(MageObjectReference permanentMOR, Ability source) { state.storePermanentCostsTags(permanentMOR, source); } diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index f4c1016a948..9e43083a098 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -567,6 +567,12 @@ public class GameEvent implements Serializable { playerId the player crafting */ EXILED_WHILE_CRAFTING, + /* Become suspected + targetId the permanent being suspected + sourceId of the ability suspecting + playerId the player suspecting + */ + BECOME_SUSPECTED, //custom events CUSTOM_EVENT } diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 681ebde902d..ea5c3972ca7 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 isSuspected(); + + void setSuspected(boolean value, Game game, Ability source); + boolean isPrototyped(); void setPrototyped(boolean value); @@ -222,6 +226,7 @@ public interface Permanent extends Card, Controllable { * @return can be null for exists abilities */ Ability addAbility(Ability ability, UUID sourceId, Game game); + Ability addAbility(Ability ability, UUID sourceId, Game game, boolean fromExistingObject); void removeAllAbilities(UUID sourceId, Game game); @@ -313,7 +318,7 @@ public interface Permanent extends Card, Controllable { /** * Fast check for attacking possibilities (is it possible to attack permanent/planeswalker/battle) * - * @param attackerId creature to attack, can be null + * @param attackerId creature to attack, can be null * @param defendingPlayerId defending player * @param game * @return diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index a7ab9162066..fe5d985f030 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -69,6 +69,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { protected boolean transformed; protected boolean monstrous; protected boolean renowned; + protected boolean suspected; protected boolean manifested = false; protected boolean morphed = false; protected boolean ringBearerFlag = false; @@ -162,6 +163,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.transformed = permanent.transformed; this.monstrous = permanent.monstrous; this.renowned = permanent.renowned; + this.suspected = permanent.suspected; this.ringBearerFlag = permanent.ringBearerFlag; this.classLevel = permanent.classLevel; this.goadingPlayers.addAll(permanent.goadingPlayers); @@ -392,8 +394,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { /** * Add an ability to the permanent. When copying from an existing source * you should use the fromExistingObject variant of this function to prevent double-copying subabilities - * @param ability The ability to be added - * @param sourceId id of the source doing the added (for the effect created to add it) + * + * @param ability The ability to be added + * @param sourceId id of the source doing the added (for the effect created to add it) * @param game * @return The newly added ability copy */ @@ -403,11 +406,11 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { } /** - * @param ability The ability to be added - * @param sourceId id of the source doing the added (for the effect created to add it) + * @param ability The ability to be added + * @param sourceId id of the source doing the added (for the effect created to add it) * @param game * @param fromExistingObject if copying abilities from an existing source then must ignore sub-abilities because they're already on the source object - * Otherwise sub-abilities will be added twice to the resulting object + * Otherwise sub-abilities will be added twice to the resulting object * @return The newly added ability copy */ @Override @@ -1490,7 +1493,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { @Override public boolean canBlock(UUID attackerId, Game game) { - if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty() || isBattle(game)) { + if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty() || isBattle(game) || isSuspected()) { return false; } Permanent attacker = game.getPermanent(attackerId); @@ -1671,6 +1674,28 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.renowned = value; } + @Override + public boolean isSuspected() { + return suspected; + } + + private static final String suspectedInfoKey = "IS_SUSPECTED"; + + @Override + public void setSuspected(boolean value, Game game, Ability source) { + if (!value || !game.replaceEvent(GameEvent.getEvent( + EventType.BECOME_SUSPECTED, getId(), + source, source.getControllerId() + ))) { + this.suspected = value; + } + if (this.suspected) { + addInfo(suspectedInfoKey, CardUtil.addToolTipMarkTags("Suspected (has menace and can't block)"), game); + } else { + addInfo(suspectedInfoKey, null, game); + } + } + // Used as key for the ring bearer info. private static final String ringbearerInfoKey = "IS_RINGBEARER";