[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 <matthew_w@vaadin.com>
Co-authored-by: xenohedron <xenohedron@users.noreply.github.com>
This commit is contained in:
Matthew Wilson 2024-01-29 06:41:23 +02:00 committed by GitHub
parent 25a08c736f
commit f8d15cd6ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 941 additions and 22 deletions

View file

@ -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<UUID, Set<MageObjectReference>> 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;
}
}

View file

@ -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).";
}
}

View file

@ -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);
}
}

View file

@ -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).";
}
}

View file

@ -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));

View file

@ -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, "<i>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, "<i>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, "<i>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, "<i>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();
}
}

View file

@ -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());

View file

@ -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].
* <ul>
* <li>Each Case has two special keyword abilities: to solve and solved.</li>
* <li>"To Solve — [condition]" means "At the beginning of your end step,
* if [condition] and this Case is not solved, it becomes solved."</li>
* <li>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]."</li>
* <li>"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]."</li>
* <li>"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."</li>
* <li>"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.</li>
* <li>Once a Case becomes solved, it stays solved until it leaves the battlefield.</li>
* <li>Cases don't lose their other abilities when they become solved.</li>
* <li>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.</li>
* </ul>
*
* @author DominionSpy
*/
public class CaseAbility extends SimpleStaticAbility {
/**
* Constructs a Case with three abilities:
* <ul>
* <li>A initial ability the Case has at all times</li>
* <li>A "To solve" ability that will conditionally solve the Case
* at the beginning of the controller's end step</li>
* <li>A "Solved" ability the Case has when solved</li>
* </ul>
* The "Solved" ability must be one of the following:
* <ul>
* <li>{@link ConditionalActivatedAbility} using the condition {@link SolvedSourceCondition}.SOLVED</li>
* <li>{@link ConditionalTriggeredAbility} using the condition {@link SolvedSourceCondition}.SOLVED</li>
* <li>{@link SimpleStaticAbility} with only {@link ConditionalAsThoughEffect} or {@link ConditionalContinuousEffect} effects</li>
* </ul>
*
* @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() + ". <i>(If unsolved, solve at the beginning of your end step.)</i>";
}
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);
}
}

View file

@ -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");
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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 "";
}
}

View file

@ -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);
}
}

View file

@ -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),

View file

@ -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

View file

@ -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();

View file

@ -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<String, List<UUID>> connectedCards = new HashMap<>();
protected Set<MageObjectReference> 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);