From f8d15cd6baea6522b8cdefce1d8be31b99e250be Mon Sep 17 00:00:00 2001 From: Matthew Wilson Date: Mon, 29 Jan 2024 06:41:23 +0200 Subject: [PATCH] [MKM] Implement Cases (#11713) * Implementing "case" mechanic * [MKM] Implement Case of the Burning Masks * [MKM] Implement Case of the Filched Falcon * [MKM] Implement Case of the Crimson Pulse * [MKM] Implement Case of the Locked Hothouse * Address PR comments * some minor adjustments * adjustments to hints --------- Co-authored-by: Matthew Wilson Co-authored-by: xenohedron --- .../mage/cards/c/CaseOfTheBurningMasks.java | 196 ++++++++++++++++++ .../mage/cards/c/CaseOfTheCrimsonPulse.java | 88 ++++++++ .../mage/cards/c/CaseOfTheFilchedFalcon.java | 120 +++++++++++ .../mage/cards/c/CaseOfTheLockedHothouse.java | 107 ++++++++++ .../src/mage/sets/MurdersAtKarlovManor.java | 4 + .../test/cards/enchantments/CaseTest.java | 102 +++++++++ .../mage/abilities/TriggeredAbilityImpl.java | 2 +- .../mage/abilities/common/CaseAbility.java | 166 +++++++++++++++ .../common/SolvedSourceCondition.java | 32 +++ .../ConditionalActivatedAbility.java | 27 ++- .../ConditionalTriggeredAbility.java | 7 + .../mage/abilities/hint/ConditionHint.java | 16 +- .../abilities/hint/common/CaseSolvedHint.java | 50 +++++ .../hint/common/ConditionPermanentHint.java | 6 +- .../src/main/java/mage/constants/SubType.java | 1 + .../main/java/mage/game/events/GameEvent.java | 6 + .../java/mage/game/permanent/Permanent.java | 4 + .../mage/game/permanent/PermanentImpl.java | 29 +++ 18 files changed, 941 insertions(+), 22 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CaseOfTheBurningMasks.java create mode 100644 Mage.Sets/src/mage/cards/c/CaseOfTheCrimsonPulse.java create mode 100644 Mage.Sets/src/mage/cards/c/CaseOfTheFilchedFalcon.java create mode 100644 Mage.Sets/src/mage/cards/c/CaseOfTheLockedHothouse.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/enchantments/CaseTest.java create mode 100644 Mage/src/main/java/mage/abilities/common/CaseAbility.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/SolvedSourceCondition.java create mode 100644 Mage/src/main/java/mage/abilities/hint/common/CaseSolvedHint.java diff --git a/Mage.Sets/src/mage/cards/c/CaseOfTheBurningMasks.java b/Mage.Sets/src/mage/cards/c/CaseOfTheBurningMasks.java new file mode 100644 index 00000000000..9cae3d2f84c --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CaseOfTheBurningMasks.java @@ -0,0 +1,196 @@ +package mage.cards.c; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.CaseAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SolvedSourceCondition; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.decorator.ConditionalActivatedAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.hint.common.CaseSolvedHint; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.WatcherScope; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInExile; +import mage.target.common.TargetOpponentsCreaturePermanent; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +/** + * Case of the Burning Masks {1}{R}{R} + * Enchantment - Case + * When this Case enters the battlefield, it deals 3 damage to target creature an opponent controls. + * To solve -- Three or more sources you controlled dealt damage this turn. + * Solved -- Sacrifice this Case: Exile the top three cards of your library. Choose one of them. You may play that card this turn. + * + * @author DominionSpy + */ +public final class CaseOfTheBurningMasks extends CardImpl { + + public CaseOfTheBurningMasks(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}{R}"); + + this.subtype.add(SubType.CASE); + + // When this Case enters the battlefield, it deals 3 damage to target creature an opponent controls. + Ability initialAbility = new EntersBattlefieldTriggeredAbility(new DamageTargetEffect(3)); + initialAbility.addTarget(new TargetOpponentsCreaturePermanent()); + // To solve -- Three or more sources you controlled dealt damage this turn. + // Solved -- Sacrifice this Case: Exile the top three cards of your library. Choose one of them. You may play that card this turn. + Ability solvedAbility = new ConditionalActivatedAbility(new CaseOfTheBurningMasksEffect(), + new SacrificeSourceCost().setText("sacrifice this Case"), SolvedSourceCondition.SOLVED); + + this.addAbility(new CaseAbility(initialAbility, CaseOfTheBurningMasksCondition.instance, solvedAbility) + .addHint(new CaseOfTheBurningMasksHint()), + new CaseOfTheBurningMasksWatcher()); + } + + private CaseOfTheBurningMasks(final CaseOfTheBurningMasks card) { + super(card); + } + + @Override + public CaseOfTheBurningMasks copy() { + return new CaseOfTheBurningMasks(this); + } +} + +enum CaseOfTheBurningMasksCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + CaseOfTheBurningMasksWatcher watcher = game.getState().getWatcher(CaseOfTheBurningMasksWatcher.class); + return watcher != null && watcher.damagingCountByController(source.getControllerId()) >= 3; + } + + @Override + public String toString() { + return "Three or more sources you controlled dealt damage this turn"; + } +} + +class CaseOfTheBurningMasksHint extends CaseSolvedHint { + + CaseOfTheBurningMasksHint() { + super(CaseOfTheBurningMasksCondition.instance); + } + + private CaseOfTheBurningMasksHint(final CaseOfTheBurningMasksHint hint) { + super(hint); + } + + @Override + public CaseOfTheBurningMasksHint copy() { + return new CaseOfTheBurningMasksHint(this); + } + + @Override + public String getConditionText(Game game, Ability ability) { + int sources = game.getState() + .getWatcher(CaseOfTheBurningMasksWatcher.class) + .damagingCountByController(ability.getControllerId()); + return "Sources that dealt damage: " + sources + " (need 3)."; + } +} + +class CaseOfTheBurningMasksWatcher extends Watcher { + + private final Map> damagingObjects; + + CaseOfTheBurningMasksWatcher() { + super(WatcherScope.GAME); + this.damagingObjects = new HashMap<>(); + } + + @Override + public void watch(GameEvent event, Game game) { + switch (event.getType()) { + case DAMAGED_PERMANENT: + case DAMAGED_PLAYER: { + damagingObjects + .computeIfAbsent(game.getControllerId(event.getSourceId()), k -> new HashSet<>()) + .add(new MageObjectReference(event.getSourceId(), game)); + } + } + } + + @Override + public void reset() { + super.reset(); + damagingObjects.clear(); + } + + public int damagingCountByController(UUID controllerId) { + return damagingObjects.getOrDefault(controllerId, Collections.emptySet()).size(); + } +} + +class CaseOfTheBurningMasksEffect extends OneShotEffect { + + CaseOfTheBurningMasksEffect() { + super(Outcome.Benefit); + staticText = "Exile the top three cards of your library. Choose one of them. You may play that card this turn."; + } + + private CaseOfTheBurningMasksEffect(final CaseOfTheBurningMasksEffect effect) { + super(effect); + } + + @Override + public CaseOfTheBurningMasksEffect copy() { + return new CaseOfTheBurningMasksEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 3)); + player.moveCards(cards, Zone.EXILED, source, game); + cards.retainZone(Zone.EXILED, game); + + Card card; + switch (cards.size()) { + case 0: + return false; + case 1: + card = cards.getRandom(game); + break; + default: + TargetCard target = new TargetCardInExile(StaticFilters.FILTER_CARD); + target.withNotTarget(true); + player.choose(outcome, cards, target, source, game); + card = game.getCard(target.getFirstTarget()); + } + if (card != null) { + CardUtil.makeCardPlayable(game, source, card, Duration.EndOfTurn, false); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/c/CaseOfTheCrimsonPulse.java b/Mage.Sets/src/mage/cards/c/CaseOfTheCrimsonPulse.java new file mode 100644 index 00000000000..56c08e5bd27 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CaseOfTheCrimsonPulse.java @@ -0,0 +1,88 @@ +package mage.cards.c; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.CaseAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.HellbentCondition; +import mage.abilities.condition.common.SolvedSourceCondition; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.discard.DiscardControllerEffect; +import mage.abilities.effects.common.discard.DiscardHandControllerEffect; +import mage.abilities.hint.common.CaseSolvedHint; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.game.Game; +import mage.players.Player; + +/** + * Case of the Crimson Pulse {2}{R} + * Enchantment - Case + * When this Case enters the battlefield, discard a card, then draw two cards. + * To solve -- You have no cards in hand. + * Solved -- At the beginning of your upkeep, discard your hand, then draw two cards. + * + * @author DominionSpy + */ +public final class CaseOfTheCrimsonPulse extends CardImpl { + + public CaseOfTheCrimsonPulse(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{R}"); + + this.subtype.add(SubType.CASE); + + // When this Case enters the battlefield, discard a card, then draw two cards. + Ability initialAbility = new EntersBattlefieldTriggeredAbility(new DiscardControllerEffect(1)); + initialAbility.addEffect(new DrawCardSourceControllerEffect(2).setText(", then draw two cards.")); + // To solve -- You have no cards in hand. + // Solved -- At the beginning of your upkeep, discard your hand, then draw two cards. + Ability solvedAbility = new ConditionalTriggeredAbility(new BeginningOfUpkeepTriggeredAbility( + new DiscardHandControllerEffect(), TargetController.YOU, false), + SolvedSourceCondition.SOLVED, null); + solvedAbility.addEffect(new DrawCardSourceControllerEffect(2).concatBy(", then")); + + this.addAbility(new CaseAbility(initialAbility, HellbentCondition.instance, solvedAbility) + .addHint(new CaseOfTheCrimsonPulseHint())); + } + + private CaseOfTheCrimsonPulse(final CaseOfTheCrimsonPulse card) { + super(card); + } + + @Override + public CaseOfTheCrimsonPulse copy() { + return new CaseOfTheCrimsonPulse(this); + } +} + +class CaseOfTheCrimsonPulseHint extends CaseSolvedHint { + + CaseOfTheCrimsonPulseHint() { + super(HellbentCondition.instance); + } + + private CaseOfTheCrimsonPulseHint(final CaseOfTheCrimsonPulseHint hint) { + super(hint); + } + + @Override + public CaseOfTheCrimsonPulseHint copy() { + return new CaseOfTheCrimsonPulseHint(this); + } + + @Override + public String getConditionText(Game game, Ability ability) { + Player controller = game.getPlayer(ability.getControllerId()); + if (controller == null) { + return ""; + } + int handSize = controller.getHand().size(); + return "Cards in hand: " + handSize + " (need 0)."; + } +} diff --git a/Mage.Sets/src/mage/cards/c/CaseOfTheFilchedFalcon.java b/Mage.Sets/src/mage/cards/c/CaseOfTheFilchedFalcon.java new file mode 100644 index 00000000000..2acca20cc61 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CaseOfTheFilchedFalcon.java @@ -0,0 +1,120 @@ +package mage.cards.c; + +import java.util.UUID; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.CaseAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.condition.common.SolvedSourceCondition; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalActivatedAbility; +import mage.abilities.effects.common.continuous.BecomesCreatureTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.effects.keyword.InvestigateEffect; +import mage.abilities.hint.common.CaseSolvedHint; +import mage.abilities.keyword.FlyingAbility; +import mage.constants.ComparisonType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterArtifactPermanent; +import mage.game.Game; +import mage.game.permanent.token.TokenImpl; +import mage.target.TargetPermanent; + +/** + * + * @author DominionSpy + */ +public final class CaseOfTheFilchedFalcon extends CardImpl { + + private static final FilterPermanent filter = new FilterArtifactPermanent("You control three or more artifacts"); + + public CaseOfTheFilchedFalcon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}"); + + this.subtype.add(SubType.CASE); + + // When this Case enters the battlefield, investigate. + Ability initialAbility = new EntersBattlefieldTriggeredAbility(new InvestigateEffect()); + // To solve -- You control three or more artifacts. + Condition toSolveCondition = new PermanentsOnTheBattlefieldCondition( + filter, ComparisonType.MORE_THAN, 2, true); + // Solved -- {2}{U}, Sacrifice this Case: Put four +1/+1 counters on target noncreature artifact. It becomes a 0/0 Bird creature with flying in addition to its other types. + Ability solvedAbility = new ConditionalActivatedAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance(4)), + new ManaCostsImpl<>("{2}{U}"), SolvedSourceCondition.SOLVED); + solvedAbility.addEffect(new BecomesCreatureTargetEffect(new CaseOfTheFilchedFalconToken(), + false, false, Duration.WhileOnBattlefield) + .setText("It becomes a 0/0 Bird creature with flying in addition to its other types")); + solvedAbility.addCost(new SacrificeSourceCost().setText("sacrifice this Case")); + solvedAbility.addTarget(new TargetPermanent(StaticFilters.FILTER_ARTIFACT_NON_CREATURE)); + + this.addAbility(new CaseAbility(initialAbility, toSolveCondition, solvedAbility) + .addHint(new CaseOfTheFilchedFalconHint(toSolveCondition))); + } + + private CaseOfTheFilchedFalcon(final CaseOfTheFilchedFalcon card) { + super(card); + } + + @Override + public CaseOfTheFilchedFalcon copy() { + return new CaseOfTheFilchedFalcon(this); + } +} + +class CaseOfTheFilchedFalconHint extends CaseSolvedHint { + + CaseOfTheFilchedFalconHint(Condition condition) { + super(condition); + } + + private CaseOfTheFilchedFalconHint(final CaseOfTheFilchedFalconHint hint) { + super(hint); + } + + @Override + public CaseOfTheFilchedFalconHint copy() { + return new CaseOfTheFilchedFalconHint(this); + } + + @Override + public String getConditionText(Game game, Ability ability) { + int artifacts = game.getBattlefield() + .count(StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT, ability.getControllerId(), + ability, game); + return "Artifacts: " + artifacts + " (need 3)."; + } +} + +class CaseOfTheFilchedFalconToken extends TokenImpl { + + public CaseOfTheFilchedFalconToken() { + super("", "0/0 Bird creature with flying"); + this.cardType.add(CardType.CREATURE); + + this.subtype.add(SubType.BIRD); + this.power = new MageInt(0); + this.toughness = new MageInt(0); + this.addAbility(FlyingAbility.getInstance()); + } + + private CaseOfTheFilchedFalconToken(final CaseOfTheFilchedFalconToken token) { + super(token); + } + + @Override + public CaseOfTheFilchedFalconToken copy() { + return new CaseOfTheFilchedFalconToken(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CaseOfTheLockedHothouse.java b/Mage.Sets/src/mage/cards/c/CaseOfTheLockedHothouse.java new file mode 100644 index 00000000000..9cf8c65397c --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CaseOfTheLockedHothouse.java @@ -0,0 +1,107 @@ +package mage.cards.c; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.CaseAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.condition.common.SolvedSourceCondition; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; +import mage.abilities.effects.common.continuous.PlayAdditionalLandsControllerEffect; +import mage.abilities.effects.common.continuous.PlayTheTopCardEffect; +import mage.abilities.hint.common.CaseSolvedHint; +import mage.constants.ComparisonType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.filter.FilterCard; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterLandPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; + +/** + * Case of the Locked Hothouse {3}{G} + * Enchantment - Case + * You may play an additional land on each of your turns. + * To solve -- You control seven or more lands. + * Solved -- You may look at the top card of your library any time, and you may play lands and cast creature and enchantment spells from the top of your library. + * + * @author DominionSpy + */ +public final class CaseOfTheLockedHothouse extends CardImpl { + + private static final FilterPermanent filter = new FilterLandPermanent("You control seven or more lands"); + private static final FilterCard filter2 = new FilterCard("play lands and cast creature and enchantment spells"); + + static { + filter2.add(Predicates.or( + CardType.LAND.getPredicate(), + CardType.CREATURE.getPredicate(), + CardType.ENCHANTMENT.getPredicate() + )); + } + + public CaseOfTheLockedHothouse(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}"); + + this.subtype.add(SubType.CASE); + + // You may play an additional land on each of your turns. + Ability initialAbility = new SimpleStaticAbility(new PlayAdditionalLandsControllerEffect(1, Duration.WhileOnBattlefield)); + // To solve -- You control seven or more lands. + Condition toSolveCondition = new PermanentsOnTheBattlefieldCondition( + filter, ComparisonType.MORE_THAN, 6, true); + // Solved -- You may look at the top card of your library any time, and you may play lands and cast creature and enchantment spells from the top of your library. + Ability solvedAbility = new SimpleStaticAbility(new ConditionalContinuousEffect( + new LookAtTopCardOfLibraryAnyTimeEffect(), SolvedSourceCondition.SOLVED, "")); + solvedAbility.addEffect(new ConditionalAsThoughEffect( + new PlayTheTopCardEffect(TargetController.YOU, filter2, false), + SolvedSourceCondition.SOLVED) + .setText(", and you may play lands and cast creature and enchantment spells from the top of your library.")); + + this.addAbility(new CaseAbility(initialAbility, toSolveCondition, solvedAbility) + .addHint(new CaseOfTheLockedHothouseHint(toSolveCondition))); + } + + private CaseOfTheLockedHothouse(final CaseOfTheLockedHothouse card) { + super(card); + } + + @Override + public CaseOfTheLockedHothouse copy() { + return new CaseOfTheLockedHothouse(this); + } +} + +class CaseOfTheLockedHothouseHint extends CaseSolvedHint { + + CaseOfTheLockedHothouseHint(Condition condition) { + super(condition); + } + + private CaseOfTheLockedHothouseHint(final CaseOfTheLockedHothouseHint hint) { + super(hint); + } + + @Override + public CaseOfTheLockedHothouseHint copy() { + return new CaseOfTheLockedHothouseHint(this); + } + + @Override + public String getConditionText(Game game, Ability ability) { + int lands = game.getBattlefield() + .count(StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND, ability.getControllerId(), + ability, game); + return "Lands: " + lands + " (need 7)."; + } +} diff --git a/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java b/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java index 2c77d067e56..87c602500bf 100644 --- a/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java +++ b/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java @@ -47,6 +47,10 @@ public final class MurdersAtKarlovManor extends ExpansionSet { cards.add(new SetCardInfo("Branch of Vitu-Ghazi", 258, Rarity.UNCOMMON, mage.cards.b.BranchOfVituGhazi.class)); cards.add(new SetCardInfo("Burden of Proof", 42, Rarity.UNCOMMON, mage.cards.b.BurdenOfProof.class)); cards.add(new SetCardInfo("Candlestick", 43, Rarity.UNCOMMON, mage.cards.c.Candlestick.class)); + cards.add(new SetCardInfo("Case of the Burning Masks", 113, Rarity.UNCOMMON, mage.cards.c.CaseOfTheBurningMasks.class)); + cards.add(new SetCardInfo("Case of the Crimson Pulse", 114, Rarity.RARE, mage.cards.c.CaseOfTheCrimsonPulse.class)); + cards.add(new SetCardInfo("Case of the Filched Falcon", 44, Rarity.UNCOMMON, mage.cards.c.CaseOfTheFilchedFalcon.class)); + cards.add(new SetCardInfo("Case of the Locked Hothouse", 155, Rarity.RARE, mage.cards.c.CaseOfTheLockedHothouse.class)); cards.add(new SetCardInfo("Caught Red-Handed", 115, Rarity.UNCOMMON, mage.cards.c.CaughtRedHanded.class)); cards.add(new SetCardInfo("Cease // Desist", 246, Rarity.UNCOMMON, mage.cards.c.CeaseDesist.class)); cards.add(new SetCardInfo("Cerebral Confiscation", 81, Rarity.COMMON, mage.cards.c.CerebralConfiscation.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/CaseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/CaseTest.java new file mode 100644 index 00000000000..74bc6754b13 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/CaseTest.java @@ -0,0 +1,102 @@ +package org.mage.test.cards.enchantments; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class CaseTest extends CardTestPlayerBase { + + @Test + public void test_CaseOfTheBurningMasks() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + addCard(Zone.BATTLEFIELD, playerA, "Aminatou, the Fateshifter"); + addCard(Zone.HAND, playerA, "Case of the Burning Masks"); + addCard(Zone.HAND, playerA, "Lightning Bolt", 3); + addCard(Zone.HAND, playerA, "Impact Tremors"); + addCard(Zone.HAND, playerA, "Goblin Chainwhirler"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Case of the Burning Masks"); + + checkStackSize("case is not solved", 1, PhaseStep.END_TURN, playerA, 0); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Impact Tremors"); + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin Chainwhirler"); + setChoice(playerA, "Whenever"); // Choose trigger order + + checkStackObject("case is solved", 3, PhaseStep.END_TURN, playerA, "To solve", 1); + + // Activate Aminatou, the Fateshifter ability to put Lightning Bolt on top of library + activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "+1"); + addTarget(playerA, "Lightning Bolt"); + waitStackResolved(5, PhaseStep.PRECOMBAT_MAIN, playerA); + // Activate Case of the Burning Masks "Solved" ability + activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Solved"); + setChoice(playerA, "Lightning Bolt"); + waitStackResolved(5, PhaseStep.PRECOMBAT_MAIN, playerA); + + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Case of the Burning Masks", 0); + assertLife(playerB, 9); + } + + @Test + public void test_CaseOfTheCrimsonPulse() { + addCard(Zone.BATTLEFIELD, playerA, "Badlands", 3 + 7); + addCard(Zone.HAND, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Case of the Crimson Pulse"); + addCard(Zone.HAND, playerA, "Wit's End"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Case of the Crimson Pulse"); + setChoice(playerA, "Mountain"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + checkGraveyardCount("mountain in graveyard", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain", 1); + checkStackSize("case is not solved", 1, PhaseStep.END_TURN, playerA, 0); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Wit's End", playerA); + + checkStackObject("case is solved", 3, PhaseStep.END_TURN, playerA, "To solve", 1); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.UPKEEP); + execute(); + + assertHandCount(playerA, 2); + } + + @Test + public void test_CaseOfTheLockedHothouse() { + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Case of the Locked Hothouse"); + addCard(Zone.BATTLEFIELD, playerA, "Llanowar Elves"); + addCard(Zone.HAND, playerA, "Plains", 1); + addCard(Zone.HAND, playerA, "Island", 1); + addCard(Zone.HAND, playerA, "Griptide"); + + checkStackSize("case is not solved", 1, PhaseStep.END_TURN, playerA, 0); + + playLand(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Plains"); + playLand(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Island"); + + checkStackObject("case is solved", 3, PhaseStep.END_TURN, playerA, "To solve", 1); + + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Griptide", "Llanowar Elves"); + waitStackResolved(5, PhaseStep.PRECOMBAT_MAIN); + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Llanowar Elves"); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + execute(); + } +} diff --git a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java index 1ac02b3c0ad..b654a610e3f 100644 --- a/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/TriggeredAbilityImpl.java @@ -370,7 +370,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge } public static boolean isInUseableZoneDiesTrigger(TriggeredAbility source, GameEvent event, Game game) { - // Get the source permanent of the ability + // Get the source permanent of the ability MageObject sourceObject = null; if (game.getState().getZone(source.getSourceId()) == Zone.BATTLEFIELD) { sourceObject = game.getPermanent(source.getSourceId()); diff --git a/Mage/src/main/java/mage/abilities/common/CaseAbility.java b/Mage/src/main/java/mage/abilities/common/CaseAbility.java new file mode 100644 index 00000000000..d47e32e4803 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/CaseAbility.java @@ -0,0 +1,166 @@ +package mage.abilities.common; + +import mage.abilities.Ability; +import mage.abilities.condition.CompoundCondition; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SolvedSourceCondition; +import mage.abilities.decorator.ConditionalActivatedAbility; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.constants.Outcome; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.util.CardUtil; + +/** + * The Case mechanic was added in Murders at Karlov Manor [MKM]. + *
    + *
  • Each Case has two special keyword abilities: to solve and solved.
  • + *
  • "To Solve — [condition]" means "At the beginning of your end step, + * if [condition] and this Case is not solved, it becomes solved."
  • + *
  • The meaning of "solved" differs based on what type of ability follows it. + * "Solved — [activated ability]" means "[Activated ability]. + * Activate only if this Case is solved." Activated abilities contain a colon. + * They're generally written "[Cost]: [Effect]."
  • + *
  • "Solved — [Triggered ability]" means "[Triggered ability]. + * This ability triggers only if this Case is solved." + * Triggered abilities use the word "when," "whenever," or "at." + * They're often written as "[Trigger condition], [effect]."
  • + *
  • "Solved — [static ability]" means "As long as this Case is solved, [static ability]." + * Static abilities are written as statements, such as "Creatures you control get +1/+1" + * or "Instant and sorcery spells you cast cost {1} less to cast."
  • + *
  • "To solve" abilities will check for their condition twice: + * once when the ability would trigger, and once when it resolves. + * If the condition isn't true at the beginning of your end step, + * the ability won't trigger at all. + * If the condition isn't true when the ability resolves, the Case won't become solved.
  • + *
  • Once a Case becomes solved, it stays solved until it leaves the battlefield.
  • + *
  • Cases don't lose their other abilities when they become solved.
  • + *
  • Being solved is not part of a permanent's copiable values. + * A permanent that becomes a copy of a solved Case is not solved. + * A solved Case that somehow becomes a copy of a different Case stays solved.
  • + *
+ * + * @author DominionSpy + */ +public class CaseAbility extends SimpleStaticAbility { + + /** + * Constructs a Case with three abilities: + *
    + *
  • A initial ability the Case has at all times
  • + *
  • A "To solve" ability that will conditionally solve the Case + * at the beginning of the controller's end step
  • + *
  • A "Solved" ability the Case has when solved
  • + *
+ * The "Solved" ability must be one of the following: + *
    + *
  • {@link ConditionalActivatedAbility} using the condition {@link SolvedSourceCondition}.SOLVED
  • + *
  • {@link ConditionalTriggeredAbility} using the condition {@link SolvedSourceCondition}.SOLVED
  • + *
  • {@link SimpleStaticAbility} with only {@link ConditionalAsThoughEffect} or {@link ConditionalContinuousEffect} effects
  • + *
+ * + * @param initialAbility The ability that a Case has at all times + * @param toSolveCondition The condition to be checked when solving + * @param solvedAbility The ability that a solved Case has + */ + public CaseAbility(Ability initialAbility, Condition toSolveCondition, Ability solvedAbility) { + super(Zone.ALL, null); + + if (initialAbility instanceof EntersBattlefieldTriggeredAbility) { + ((EntersBattlefieldTriggeredAbility) initialAbility).setTriggerPhrase("When this Case enters the battlefield, "); + } + addSubAbility(initialAbility); + + addSubAbility(new CaseSolveAbility(toSolveCondition)); + + if (solvedAbility instanceof ConditionalActivatedAbility) { + ((ConditionalActivatedAbility) solvedAbility).hideCondition(); + } else if (!(solvedAbility instanceof ConditionalTriggeredAbility)) { + if (solvedAbility instanceof SimpleStaticAbility) { + for (Effect effect : solvedAbility.getEffects()) { + if (!(effect instanceof ConditionalContinuousEffect || + effect instanceof ConditionalAsThoughEffect)) { + throw new IllegalArgumentException("solvedAbility must be one of ConditionalActivatedAbility, " + + "ConditionalTriggeredAbility, or StaticAbility with conditional effects."); + } + } + } else { + throw new IllegalArgumentException("solvedAbility must be one of ConditionalActivatedAbility, " + + "ConditionalTriggeredAbility, or StaticAbility with conditional effects."); + } + } + addSubAbility(solvedAbility.withFlavorWord("Solved")); // TODO: Technically this shouldn't be italicized + } + + protected CaseAbility(final CaseAbility ability) { + super(ability); + } + + @Override + public CaseAbility copy() { + return new CaseAbility(this); + } + +} + +class CaseSolveAbility extends BeginningOfEndStepTriggeredAbility { + + CaseSolveAbility(Condition condition) { + super(new SolveEffect(), TargetController.YOU, + new CompoundCondition(condition, SolvedSourceCondition.UNSOLVED), false); + withFlavorWord("To solve"); // TODO: technically this shouldn't be italicized + setTriggerPhrase(CardUtil.getTextWithFirstCharUpperCase(trimIf(condition.toString()))); + } + + private CaseSolveAbility(final CaseSolveAbility ability) { + super(ability); + } + + @Override + public CaseSolveAbility copy() { + return new CaseSolveAbility(this); + } + + @Override + public String getRule() { + return super.getRule() + ". (If unsolved, solve at the beginning of your end step.)"; + } + + private static String trimIf(String text) { + if (text.startsWith("if ")) { + return text.substring(3); + } + return text; + } +} + +class SolveEffect extends OneShotEffect { + + SolveEffect() { + super(Outcome.Benefit); + } + + private SolveEffect(final SolveEffect effect) { + super(effect); + } + + @Override + public SolveEffect copy() { + return new SolveEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null || permanent.isSolved()) { + return false; + } + return permanent.solve(game, source); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/SolvedSourceCondition.java b/Mage/src/main/java/mage/abilities/condition/common/SolvedSourceCondition.java new file mode 100644 index 00000000000..76b784166d1 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/SolvedSourceCondition.java @@ -0,0 +1,32 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * Checks if a Permanent is solved + * + * @author DominionSpy + */ +public enum SolvedSourceCondition implements Condition { + SOLVED(true), + UNSOLVED(false); + private final boolean solved; + + SolvedSourceCondition(boolean solved) { + this.solved = solved; + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getSourceId()); + return permanent != null && permanent.isSolved() == solved; + } + + @Override + public String toString() { + return "{this} is " + (solved ? "solved" : "unsolved"); + } +} diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalActivatedAbility.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalActivatedAbility.java index 05069943f1a..6f7d8681e50 100644 --- a/Mage/src/main/java/mage/abilities/decorator/ConditionalActivatedAbility.java +++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalActivatedAbility.java @@ -18,6 +18,7 @@ public class ConditionalActivatedAbility extends ActivatedAbilityImpl { private static final Effects emptyEffects = new Effects(); private String ruleText = null; + private boolean showCondition = true; public ConditionalActivatedAbility(Effect effect, Cost cost, Condition condition) { this(Zone.BATTLEFIELD, effect, cost, condition); @@ -36,6 +37,7 @@ public class ConditionalActivatedAbility extends ActivatedAbilityImpl { protected ConditionalActivatedAbility(final ConditionalActivatedAbility ability) { super(ability); this.ruleText = ability.ruleText; + this.showCondition = ability.showCondition; } @Override @@ -51,22 +53,29 @@ public class ConditionalActivatedAbility extends ActivatedAbilityImpl { return new ConditionalActivatedAbility(this); } + public ConditionalActivatedAbility hideCondition() { + this.showCondition = false; + return this; + } + @Override public String getRule() { if (ruleText != null && !ruleText.isEmpty()) { return ruleText; } StringBuilder sb = new StringBuilder(super.getRule()); - sb.append(" Activate only "); - if (timing == TimingRule.SORCERY) { - sb.append("as a sorcery and only "); + if (showCondition) { + sb.append(" Activate only "); + if (timing == TimingRule.SORCERY) { + sb.append("as a sorcery and only "); + } + String conditionText = condition.toString(); + if (!conditionText.startsWith("during") && !conditionText.startsWith("before") && !conditionText.startsWith("if")) { + sb.append("if "); + } + sb.append(conditionText); + sb.append('.'); } - String conditionText = condition.toString(); - if (!conditionText.startsWith("during") && !conditionText.startsWith("before") && !conditionText.startsWith("if")) { - sb.append("if "); - } - sb.append(conditionText); - sb.append('.'); return sb.toString(); } } diff --git a/Mage/src/main/java/mage/abilities/decorator/ConditionalTriggeredAbility.java b/Mage/src/main/java/mage/abilities/decorator/ConditionalTriggeredAbility.java index 08654abcb99..455af0a5f2a 100644 --- a/Mage/src/main/java/mage/abilities/decorator/ConditionalTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/decorator/ConditionalTriggeredAbility.java @@ -1,5 +1,6 @@ package mage.abilities.decorator; +import mage.abilities.Ability; import mage.abilities.Modes; import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbilityImpl; @@ -108,4 +109,10 @@ public class ConditionalTriggeredAbility extends TriggeredAbilityImpl { return ability.isOptional(); } + @Override + public Ability withFlavorWord(String flavorWord) { + ability.withFlavorWord(flavorWord); + return this; + } + } diff --git a/Mage/src/main/java/mage/abilities/hint/ConditionHint.java b/Mage/src/main/java/mage/abilities/hint/ConditionHint.java index 6e20561bb41..2cdac23c408 100644 --- a/Mage/src/main/java/mage/abilities/hint/ConditionHint.java +++ b/Mage/src/main/java/mage/abilities/hint/ConditionHint.java @@ -12,12 +12,12 @@ import java.awt.*; */ public class ConditionHint implements Hint { - private Condition condition; - private String trueText; - private Color trueColor; - private String falseText; - private Color falseColor; - private Boolean useIcons; + private final Condition condition; + private final String trueText; + private final Color trueColor; + private final String falseText; + private final Color falseColor; + private final boolean useIcons; public ConditionHint(Condition condition) { this(condition, condition.toString()); @@ -27,7 +27,7 @@ public class ConditionHint implements Hint { this(condition, textWithIcons, null, textWithIcons, null, true); } - public ConditionHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, Boolean useIcons) { + public ConditionHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, boolean useIcons) { this.condition = condition; this.trueText = CardUtil.getTextWithFirstCharUpperCase(trueText); this.trueColor = trueColor; @@ -58,7 +58,7 @@ public class ConditionHint implements Hint { } @Override - public Hint copy() { + public ConditionHint copy() { return new ConditionHint(this); } } diff --git a/Mage/src/main/java/mage/abilities/hint/common/CaseSolvedHint.java b/Mage/src/main/java/mage/abilities/hint/common/CaseSolvedHint.java new file mode 100644 index 00000000000..977d8bd3611 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/hint/common/CaseSolvedHint.java @@ -0,0 +1,50 @@ +package mage.abilities.hint.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SolvedSourceCondition; +import mage.abilities.hint.ConditionHint; +import mage.game.Game; +import mage.game.permanent.Permanent; + +public class CaseSolvedHint extends ConditionHint { + + private final Condition condition; + + /** + * Hint for use with CaseAbility + * @param condition Same condition added to CaseAbility + */ + public CaseSolvedHint(Condition condition) { + super(SolvedSourceCondition.SOLVED, "Case is solved.", null, "Case is unsolved.", null, true); + this.condition = condition; + } + + protected CaseSolvedHint(final CaseSolvedHint hint) { + super(hint); + this.condition = hint.condition; + } + + @Override + public String getText(Game game, Ability ability) { + Permanent permanent = game.getPermanent(ability.getSourceId()); + if (permanent == null) { + return ""; + } + String text = super.getText(game, ability); + if (!permanent.isSolved()) { + text += " " + getConditionText(game, ability); + if (condition.apply(game, ability) && game.isActivePlayer(ability.getControllerId())) { + text += " Case will be solved at the end step."; + } + } + return text; + } + + /** + * Override to add specific information on satisfying the condition. + */ + protected String getConditionText(Game game, Ability ability) { + return ""; + } +} diff --git a/Mage/src/main/java/mage/abilities/hint/common/ConditionPermanentHint.java b/Mage/src/main/java/mage/abilities/hint/common/ConditionPermanentHint.java index d45c3b25bc3..64ce25ba93b 100644 --- a/Mage/src/main/java/mage/abilities/hint/common/ConditionPermanentHint.java +++ b/Mage/src/main/java/mage/abilities/hint/common/ConditionPermanentHint.java @@ -3,7 +3,6 @@ package mage.abilities.hint.common; import mage.abilities.Ability; import mage.abilities.condition.Condition; import mage.abilities.hint.ConditionHint; -import mage.abilities.hint.Hint; import mage.game.Game; import java.awt.*; @@ -23,7 +22,7 @@ public class ConditionPermanentHint extends ConditionHint { super(condition, textWithIcons); } - public ConditionPermanentHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, Boolean useIcons) { + public ConditionPermanentHint(Condition condition, String trueText, Color trueColor, String falseText, Color falseColor, boolean useIcons) { super(condition, trueText, trueColor, falseText, falseColor, useIcons); } @@ -36,12 +35,11 @@ public class ConditionPermanentHint extends ConditionHint { if (game.getPermanent(ability.getSourceId()) == null) { return ""; } - return super.getText(game, ability); } @Override - public Hint copy() { + public ConditionPermanentHint copy() { return new ConditionPermanentHint(this); } } diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index 2b8c7b3e944..7ac1f312714 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -39,6 +39,7 @@ public enum SubType { AURA("Aura", SubTypeSet.EnchantmentType), BACKGROUND("Background", SubTypeSet.EnchantmentType), CARTOUCHE("Cartouche", SubTypeSet.EnchantmentType), + CASE("Case", SubTypeSet.EnchantmentType), CLASS("Class", SubTypeSet.EnchantmentType), CURSE("Curse", SubTypeSet.EnchantmentType), ROLE("Role", SubTypeSet.EnchantmentType), diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index a1640d142b6..4041044f925 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, + /* Solving a Case + targetId the permanent being solved + sourceId of the ability solving + playerId the player solving + */ + SOLVE_CASE, CASE_SOLVED, /* Become suspected targetId the permanent being suspected sourceId of the ability suspecting diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index ea5c3972ca7..94146a544df 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -446,6 +446,10 @@ public interface Permanent extends Card, Controllable { void setRingBearer(Game game, boolean value); + boolean isSolved(); + + boolean solve(Game game, Ability source); + @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 fe5d985f030..d9f82ce9f01 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -95,6 +95,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { // maximal number of creatures the creature can be blocked by 0 = no restriction protected int maxBlockedBy = 0; protected boolean deathtouched; + protected boolean solved = false; protected Map> connectedCards = new HashMap<>(); protected Set dealtDamageByThisTurn; @@ -145,6 +146,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.blocking = permanent.blocking; this.maxBlocks = permanent.maxBlocks; this.deathtouched = permanent.deathtouched; + this.solved = permanent.solved; this.markedLifelink = permanent.markedLifelink; this.connectedCards = CardUtil.deepCopyObject(permanent.connectedCards); this.dealtDamageByThisTurn = CardUtil.deepCopyObject(permanent.dealtDamageByThisTurn); @@ -1913,6 +1915,33 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { return ringBearerFlag; } + @Override + public boolean isSolved() { + return solved; + } + + @Override + public boolean solve(Game game, Ability source) { + if (this.solved) { + return false; + } + GameEvent event = new GameEvent(GameEvent.EventType.SOLVE_CASE, getId(), + source, source.getControllerId()); + if (game.replaceEvent(event)) { + return false; + } + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + game.informPlayers(controller.getLogName() + " solved " + this.getLogName() + + CardUtil.getSourceLogName(game, source)); + } + + this.solved = true; + game.fireEvent(new GameEvent(EventType.CASE_SOLVED, getId(), source, + source.getControllerId())); + return true; + } + @Override public boolean fight(Permanent fightTarget, Ability source, Game game) { return this.fight(fightTarget, source, game, true);