diff --git a/Mage.Sets/src/mage/cards/h/HowltoothHollow.java b/Mage.Sets/src/mage/cards/h/HowltoothHollow.java index 183bd2b9ca5..adf9b5c9f2d 100644 --- a/Mage.Sets/src/mage/cards/h/HowltoothHollow.java +++ b/Mage.Sets/src/mage/cards/h/HowltoothHollow.java @@ -1,6 +1,7 @@ package mage.cards.h; import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTappedAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.Condition; import mage.abilities.condition.common.CardsInHandCondition; @@ -30,7 +31,8 @@ public final class HowltoothHollow extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway - this.addAbility(new HideawayAbility()); + this.addAbility(new HideawayAbility(4)); + this.addAbility(new EntersBattlefieldTappedAbility()); // {tap}: Add {B}. this.addAbility(new BlackManaAbility()); diff --git a/Mage.Sets/src/mage/cards/m/MosswortBridge.java b/Mage.Sets/src/mage/cards/m/MosswortBridge.java index d6aa8d96cec..1e1344c9c86 100644 --- a/Mage.Sets/src/mage/cards/m/MosswortBridge.java +++ b/Mage.Sets/src/mage/cards/m/MosswortBridge.java @@ -1,6 +1,7 @@ package mage.cards.m; import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTappedAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.Condition; import mage.abilities.costs.common.TapSourceCost; @@ -27,7 +28,8 @@ public final class MosswortBridge extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway (This land enters the battlefield tapped. When it does, look at the top four cards of your library, exile one face down, then put the rest on the bottom of your library.) - this.addAbility(new HideawayAbility()); + this.addAbility(new HideawayAbility(4)); + this.addAbility(new EntersBattlefieldTappedAbility()); // {T}: Add {G}. this.addAbility(new GreenManaAbility()); diff --git a/Mage.Sets/src/mage/cards/s/ShelldockIsle.java b/Mage.Sets/src/mage/cards/s/ShelldockIsle.java index 5a16505a061..3fbc09f3700 100644 --- a/Mage.Sets/src/mage/cards/s/ShelldockIsle.java +++ b/Mage.Sets/src/mage/cards/s/ShelldockIsle.java @@ -1,6 +1,7 @@ package mage.cards.s; import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTappedAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.Condition; import mage.abilities.condition.common.CardsInAnyLibraryCondition; @@ -28,7 +29,8 @@ public final class ShelldockIsle extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway - this.addAbility(new HideawayAbility()); + this.addAbility(new HideawayAbility(4)); + this.addAbility(new EntersBattlefieldTappedAbility()); // {tap}: Add {U}. this.addAbility(new BlueManaAbility()); diff --git a/Mage.Sets/src/mage/cards/s/SpinerockKnoll.java b/Mage.Sets/src/mage/cards/s/SpinerockKnoll.java index f2c0b8a2fde..0a82ef2e68f 100644 --- a/Mage.Sets/src/mage/cards/s/SpinerockKnoll.java +++ b/Mage.Sets/src/mage/cards/s/SpinerockKnoll.java @@ -1,6 +1,7 @@ package mage.cards.s; import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTappedAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.Condition; import mage.abilities.costs.common.TapSourceCost; @@ -31,7 +32,8 @@ public final class SpinerockKnoll extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway - this.addAbility(new HideawayAbility()); + this.addAbility(new HideawayAbility(4)); + this.addAbility(new EntersBattlefieldTappedAbility()); // {tap}: Add {R}. this.addAbility(new RedManaAbility()); diff --git a/Mage.Sets/src/mage/cards/w/WatcherForTomorrow.java b/Mage.Sets/src/mage/cards/w/WatcherForTomorrow.java index a6751c12d3e..3b3e5e2145b 100644 --- a/Mage.Sets/src/mage/cards/w/WatcherForTomorrow.java +++ b/Mage.Sets/src/mage/cards/w/WatcherForTomorrow.java @@ -2,13 +2,12 @@ package mage.cards.w; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTappedAbility; import mage.abilities.common.LeavesBattlefieldTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.HideawayAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.cards.Cards; -import mage.cards.CardsImpl; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; @@ -19,7 +18,6 @@ import mage.players.Player; import mage.util.CardUtil; import java.util.UUID; -import mage.game.permanent.Permanent; /** * @author TheElk801 @@ -35,7 +33,8 @@ public final class WatcherForTomorrow extends CardImpl { this.toughness = new MageInt(1); // Hideaway - this.addAbility(new HideawayAbility("creatures")); + this.addAbility(new HideawayAbility(4)); + this.addAbility(new EntersBattlefieldTappedAbility()); // When Watcher for Tomorrow leaves the battlefield, put the exiled card into its owner's hand. this.addAbility(new LeavesBattlefieldTriggeredAbility(new WatcherForTomorrowEffect(), false)); @@ -70,18 +69,10 @@ class WatcherForTomorrowEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); - if (player == null) { + ExileZone zone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source, -1)); + if (player == null || zone == null || zone.isEmpty()) { return false; } - Permanent permanentLeftBattlefield = (Permanent) getValue("permanentLeftBattlefield"); - if (permanentLeftBattlefield == null) { - return false; - } - ExileZone zone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source.getSourceId(), permanentLeftBattlefield.getZoneChangeCounter(game))); - if (zone == null) { - return false; - } - Cards cards = new CardsImpl(zone.getCards(game)); - return player.moveCards(cards, Zone.HAND, source, game); + return player.moveCards(zone, Zone.HAND, source, game); } } diff --git a/Mage.Sets/src/mage/cards/w/WindbriskHeights.java b/Mage.Sets/src/mage/cards/w/WindbriskHeights.java index c5c821f4c69..a1e998e525f 100644 --- a/Mage.Sets/src/mage/cards/w/WindbriskHeights.java +++ b/Mage.Sets/src/mage/cards/w/WindbriskHeights.java @@ -1,6 +1,7 @@ package mage.cards.w; import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTappedAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.condition.Condition; import mage.abilities.costs.common.TapSourceCost; @@ -26,7 +27,8 @@ public final class WindbriskHeights extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); // Hideaway (This land enters the battlefield tapped. When it does, look at the top four cards of your library, exile one face down, then put the rest on the bottom of your library.) - this.addAbility(new HideawayAbility()); + this.addAbility(new HideawayAbility(4)); + this.addAbility(new EntersBattlefieldTappedAbility()); // {tap}: Add {W}. this.addAbility(new WhiteManaAbility()); diff --git a/Mage/src/main/java/mage/abilities/effects/common/HideawayPlayEffect.java b/Mage/src/main/java/mage/abilities/effects/common/HideawayPlayEffect.java index ec54ab19a98..b9687198dc6 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/HideawayPlayEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/HideawayPlayEffect.java @@ -1,18 +1,16 @@ package mage.abilities.effects.common; +import mage.ApprovingObject; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.constants.Outcome; import mage.game.ExileZone; import mage.game.Game; -import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; -import java.util.Set; import java.util.UUID; -import mage.ApprovingObject; /** * @author LevelX2 @@ -35,46 +33,38 @@ public class HideawayPlayEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - ExileZone zone = null; - Permanent permanent = game.getPermanentOrLKIBattlefield(source.getSourceId()); - if (permanent != null) { - zone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source.getSourceId(), permanent.getZoneChangeCounter(game))); - } - - if (zone == null) { - return true; - } - Set cards = zone.getCards(game); - if (cards.isEmpty()) { - return true; - } - - Card card = cards.iterator().next(); Player controller = game.getPlayer(source.getControllerId()); - if (card != null && controller != null) { - if (controller.chooseUse(Outcome.PlayForFree, "Play " + card.getIdName() + " for free?", source, game)) { - card.setFaceDown(false, game); - int zcc = card.getZoneChangeCounter(game); - - /* 702.74. Hideaway, rulings: - * If the removed card is a land, you may play it as a result of the last ability only if it's your turn - * and you haven't already played a land that turn. This counts as your land play for the turn. - */ - if (card.isLand(game)) { - UUID playerId = controller.getId(); - if (!game.isActivePlayer(playerId) || !game.getPlayer(playerId).canPlayLand()) { - return false; - } - } - - if (!controller.playCard(card, game, true, new ApprovingObject(source, game))) { - if (card.getZoneChangeCounter(game) == zcc) { - card.setFaceDown(true, game); - } - } - } + ExileZone zone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source)); + if (controller == null || zone == null || zone.isEmpty()) { return true; } - return false; + Card card = zone.getRandom(game); + if (card == null) { + return false; + } + if (!controller.chooseUse(Outcome.PlayForFree, "Play " + card.getIdName() + " for free?", source, game)) { + return false; + } + card.setFaceDown(false, game); + int zcc = card.getZoneChangeCounter(game); + + /* 702.74. Hideaway, rulings: + * If the removed card is a land, you may play it as a result of the last ability only if it's your turn + * and you haven't already played a land that turn. This counts as your land play for the turn. + * TODO: this doesn't work correctly with the back half of MDFCs + */ + if (card.isLand(game)) { + UUID playerId = controller.getId(); + if (!game.isActivePlayer(playerId) || !game.getPlayer(playerId).canPlayLand()) { + return false; + } + } + + if (!controller.playCard(card, game, true, new ApprovingObject(source, game))) { + if (card.getZoneChangeCounter(game) == zcc) { + card.setFaceDown(true, game); + } + } + return true; } } diff --git a/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java b/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java index 4cc3f4b1d38..17367db3054 100644 --- a/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/HideawayAbility.java @@ -1,28 +1,26 @@ package mage.abilities.keyword; -import java.util.UUID; +import mage.MageObjectReference; import mage.abilities.Ability; -import mage.abilities.StaticAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.EntersBattlefieldEffect; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.TapSourceEffect; import mage.cards.Card; import mage.cards.Cards; import mage.cards.CardsImpl; -import mage.constants.AsThoughEffectType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.FilterCard; -import mage.game.ExileZone; import mage.game.Game; +import mage.game.events.EntersTheBattlefieldEvent; +import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.TargetCard; +import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.*; /** * @author LevelX2 @@ -35,34 +33,26 @@ import mage.util.CardUtil; * controlled the permanent that exiled this card may look at this card in the * exile zone.'" */ -public class HideawayAbility extends StaticAbility { +public class HideawayAbility extends EntersBattlefieldTriggeredAbility { - public HideawayAbility() { - this("land"); - } + private final int amount; - public HideawayAbility(String name) { - super(Zone.ALL, new EntersBattlefieldEffect(new TapSourceEffect(true))); - Ability ability = new EntersBattlefieldTriggeredAbility(new HideawayExileEffect(), false); - ability.setRuleVisible(false); - addSubAbility(ability); - // Allow controller to look at face down card - ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new HideawayLookAtFaceDownCardEffect()); - ability.setRuleVisible(false); - addSubAbility(ability); - this.name = name; + public HideawayAbility(int amount) { + super(new HideawayExileEffect(amount)); + this.amount = amount; + this.addWatcher(new HideawayWatcher()); } private HideawayAbility(final HideawayAbility ability) { super(ability); - this.name = ability.name; + this.amount = ability.amount; } @Override public String getRule() { - return "Hideaway (This " + this.name + " enters the battlefield tapped. " - + "When it does, look at the top four cards of your library, exile " - + "one face down, then put the rest on the bottom of your library.)"; + return "Hideaway " + this.amount + " (When this permanent enters the battlefield, look at the top " + + CardUtil.numberToText(this.amount) + " cards of your library, exile one face down, " + + "then put the rest on the bottom of your library in a random order.)"; } @Override @@ -73,16 +63,17 @@ public class HideawayAbility extends StaticAbility { class HideawayExileEffect extends OneShotEffect { - private static FilterCard filter1 = new FilterCard("card to exile face down"); + private static final FilterCard filter = new FilterCard("card to exile face down"); + private final int amount; - public HideawayExileEffect() { + HideawayExileEffect(int amount) { super(Outcome.Benefit); - this.staticText = "look at the top four cards of your library, " - + "exile one face down, then put the rest on the bottom of your library"; + this.amount = amount; } - public HideawayExileEffect(final HideawayExileEffect effect) { + private HideawayExileEffect(final HideawayExileEffect effect) { super(effect); + this.amount = effect.amount; } @Override @@ -93,45 +84,36 @@ class HideawayExileEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - - // LKI is required for this ruling - /* - If Watcher for Tomorrow leaves the battlefield before its - triggered ability from hideaway resolves, its leaves-the-battlefield - ability resolves and does nothing. Then its enters-the-battlefield - ability resolves and you exile a card with no way to return it to your hand. - */ - Permanent hideawaySource = game.getPermanentOrLKIBattlefield(source.getSourceId()); - if (hideawaySource == null - || controller == null) { + if (controller == null) { return false; } - Cards cards = new CardsImpl(); - cards.addAll(controller.getLibrary().getTopCards(game, 4)); - if (!cards.isEmpty()) { - TargetCard target1 = new TargetCard(Zone.LIBRARY, filter1); - target1.setNotTarget(true); - if (controller.choose(Outcome.Detriment, cards, target1, game)) { - Card card = cards.get(target1.getFirstTarget(), game); - if (card != null) { - cards.remove(card); - UUID exileId = CardUtil.getExileZoneId(game, source.getSourceId(), source.getSourceObjectZoneChangeCounter()); - controller.moveCardToExileWithInfo(card, exileId, "Hideaway (" + hideawaySource.getIdName() + ')', source, game, Zone.LIBRARY, false); - card.setFaceDown(true, game); - } - } - controller.putCardsOnBottomOfLibrary(cards, game, source, true); + Cards cards = new CardsImpl(controller.getLibrary().getTopCards(game, amount)); + if (cards.isEmpty()) { + return true; } - + TargetCard target = new TargetCard(Zone.LIBRARY, filter); + target.setNotTarget(true); + controller.choose(Outcome.Detriment, cards, target, game); + Card card = cards.get(target.getFirstTarget(), game); + if (card != null) { + controller.moveCardsToExile( + card, source, game, false, + CardUtil.getExileZoneId(game, source), + "Hideaway (" + CardUtil.getSourceName(game, source) + ')' + ); + game.addEffect(new HideawayLookAtFaceDownCardEffect().setTargetPointer(new FixedTarget(card, game)), source); + card.setFaceDown(true, game); + } + cards.retainZone(Zone.LIBRARY, game); + controller.putCardsOnBottomOfLibrary(cards, game, source, false); return true; } } class HideawayLookAtFaceDownCardEffect extends AsThoughEffectImpl { - public HideawayLookAtFaceDownCardEffect() { + HideawayLookAtFaceDownCardEffect() { super(AsThoughEffectType.LOOK_AT_FACE_DOWN, Duration.EndOfGame, Outcome.Benefit); - staticText = "You may look at the cards exiled with {this}"; } private HideawayLookAtFaceDownCardEffect(final HideawayLookAtFaceDownCardEffect effect) { @@ -150,24 +132,48 @@ class HideawayLookAtFaceDownCardEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - if (game.getState().getZone(objectId) != Zone.EXILED - || !game.getState().getCardState(objectId).isFaceDown()) { - return false; - } - // TODO: Does not handle if a player had the control of the land permanent some time before - // we would need to add a watcher to handle this - Permanent sourcePermanet = game.getPermanentOrLKIBattlefield(source.getSourceId()); - if (sourcePermanet != null && sourcePermanet.isControlledBy(affectedControllerId)) { - ExileZone exile = game.getExile().getExileZone(CardUtil.getCardExileZoneId(game, source)); - Card card = game.getCard(objectId); - if (exile != null && exile.contains(objectId) && card != null) { - Player player = game.getPlayer(affectedControllerId); - if (player != null) { - player.lookAtCards("Hideaway by " + sourcePermanet.getIdName(), card, game); - } - } - } - // only the current or a previous controller can see the card, so always return false for reveal request - return false; + return Objects.equals(objectId, getTargetPointer().getFirst(game, source)) + && HideawayWatcher.check(affectedControllerId, source, game); + } +} + +class HideawayWatcher extends Watcher { + + private final Map> morMap = new HashMap<>(); + + HideawayWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + Permanent permanent; + UUID playerId; + switch (event.getType()) { + case GAINED_CONTROL: + permanent = game.getPermanent(event.getTargetId()); + playerId = event.getPlayerId(); + break; + case ENTERS_THE_BATTLEFIELD: + permanent = ((EntersTheBattlefieldEvent) event).getTarget(); + playerId = permanent.getControllerId(); + break; + case BEGINNING_PHASE_PRE: + if (game.getTurnNum() == 1) { + morMap.clear(); + } + default: + return; + } + morMap.computeIfAbsent(new MageObjectReference(permanent, game), x -> new HashSet<>()).add(playerId); + } + + static boolean check(UUID playerId, Ability source, Game game) { + return game + .getState() + .getWatcher(HideawayWatcher.class) + .morMap + .getOrDefault(new MageObjectReference(source), Collections.emptySet()) + .contains(playerId); } } diff --git a/Utils/keywords.txt b/Utils/keywords.txt index 763ff0d0f61..e752919eeb5 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -53,6 +53,7 @@ Foretell|card, manaString| Friends forever|instance| Haste|instance| Hexproof|instance| +Hideaway|number| Improvise|new| Indestructible|instance| Infect|instance|