From c2a636e2b22d3087215cf6a3d619ef691c72a27b Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 24 Nov 2020 23:49:19 +0400 Subject: [PATCH] [CMR] implemented Opposition Agent and other changes: * You may play cards and you may spend mana of any color - refactored cards to use same code; * Library search event allows to change searching controller (gives full game control for another player); * Library searched event allows to remove founded cards from result; * Improved library searching effects with Panglacial Wurm's effects; * Little changes to test framework; --- .../mage/cards/b/BreechesBrazenPlunderer.java | 80 +----- Mage.Sets/src/mage/cards/c/CovetousUrge.java | 84 +----- .../src/mage/cards/d/DaxosOfMeletis.java | 91 +------ .../src/mage/cards/d/DireFleetDaredevil.java | 56 +--- .../src/mage/cards/g/GrenzoHavocRaiser.java | 89 +----- Mage.Sets/src/mage/cards/h/HostageTaker.java | 94 +------ Mage.Sets/src/mage/cards/m/Mindslaver.java | 9 +- .../src/mage/cards/o/OppositionAgent.java | 128 +++++++++ .../src/mage/cards/s/StolenStrategy.java | 52 +--- Mage.Sets/src/mage/cards/t/TobiasBeckett.java | 92 +------ Mage.Sets/src/mage/cards/w/WordOfCommand.java | 16 +- Mage.Sets/src/mage/sets/CommanderLegends.java | 2 + .../cards/asthough/SpendOtherManaTest.java | 4 + .../TakeControlWhileSearchingLibraryTest.java | 254 ++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 5 +- .../CanPlayCardControllerEffect.java | 53 ++++ ...SpendManaAsAnyColorToCastTargetEffect.java | 55 ++++ ...onentsWhileSearchingReplacementEffect.java | 48 ++++ .../game/events/LibrarySearchedEvent.java | 30 +++ .../mage/game/events/SearchLibraryEvent.java | 32 +++ .../main/java/mage/players/PlayerImpl.java | 198 +++++++------- Mage/src/main/java/mage/util/CardUtil.java | 55 ++++ 22 files changed, 806 insertions(+), 721 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/o/OppositionAgent.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/asthought/CanPlayCardControllerEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/asthought/YouMaySpendManaAsAnyColorToCastTargetEffect.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/replacement/YouControlYourOpponentsWhileSearchingReplacementEffect.java create mode 100644 Mage/src/main/java/mage/game/events/LibrarySearchedEvent.java create mode 100644 Mage/src/main/java/mage/game/events/SearchLibraryEvent.java diff --git a/Mage.Sets/src/mage/cards/b/BreechesBrazenPlunderer.java b/Mage.Sets/src/mage/cards/b/BreechesBrazenPlunderer.java index 7da4f15eb5..42a7e558f6 100644 --- a/Mage.Sets/src/mage/cards/b/BreechesBrazenPlunderer.java +++ b/Mage.Sets/src/mage/cards/b/BreechesBrazenPlunderer.java @@ -1,11 +1,8 @@ package mage.cards.b; import mage.MageInt; -import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.AsThoughManaEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.MenaceAbility; import mage.abilities.keyword.PartnerAbility; @@ -16,9 +13,7 @@ import mage.game.events.DamagedEvent; import mage.game.events.DamagedPlayerBatchEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; -import mage.players.ManaPoolItem; import mage.players.Player; -import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; import java.util.HashSet; @@ -149,79 +144,8 @@ class BreechesBrazenPlundererEffect extends OneShotEffect { return false; } for (Card card : cards.getCards(game)) { - game.addEffect(new BreechesBrazenPlundererCastEffect(new MageObjectReference(card, game)), source); - game.addEffect(new BreechesBrazenPlundererManaEffect().setTargetPointer(new FixedTarget(card, game)), source); + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, card, Duration.EndOfTurn); } return true; } -} - -class BreechesBrazenPlundererCastEffect extends AsThoughEffectImpl { - - private final MageObjectReference mor; - - BreechesBrazenPlundererCastEffect(MageObjectReference mor) { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit); - this.mor = mor; - } - - private BreechesBrazenPlundererCastEffect(final BreechesBrazenPlundererCastEffect effect) { - super(effect); - this.mor = effect.mor; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public BreechesBrazenPlundererCastEffect copy() { - return new BreechesBrazenPlundererCastEffect(this); - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (mor.getCard(game) == null) { - discard(); - return false; - } - return mor.refersTo(sourceId, game) && source.isControlledBy(affectedControllerId); - } -} - -class BreechesBrazenPlundererManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { - - BreechesBrazenPlundererManaEffect() { - super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.EndOfTurn, Outcome.Benefit); - } - - private BreechesBrazenPlundererManaEffect(final BreechesBrazenPlundererManaEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public BreechesBrazenPlundererManaEffect copy() { - return new BreechesBrazenPlundererManaEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - objectId = CardUtil.getMainCardId(game, objectId); // for split cards - FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); - return source.isControlledBy(affectedControllerId) - && Objects.equals(objectId, fixedTarget.getTarget()) - && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 - && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); - } - - @Override - public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { - return mana.getFirstAvailable(); - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/c/CovetousUrge.java b/Mage.Sets/src/mage/cards/c/CovetousUrge.java index f92effd19e..90921c0970 100644 --- a/Mage.Sets/src/mage/cards/c/CovetousUrge.java +++ b/Mage.Sets/src/mage/cards/c/CovetousUrge.java @@ -1,24 +1,21 @@ package mage.cards.c; -import mage.MageObjectReference; import mage.abilities.Ability; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.AsThoughManaEffect; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; import mage.filter.StaticFilters; import mage.game.Game; -import mage.players.ManaPoolItem; import mage.players.Player; import mage.target.TargetCard; import mage.target.common.TargetOpponent; -import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; -import java.util.Objects; import java.util.UUID; /** @@ -94,78 +91,7 @@ class CovetousUrgeEffect extends OneShotEffect { if (card.getSpellAbility() == null) { return true; } - game.addEffect(new CovetousUrgeCastFromExileEffect(new MageObjectReference(card, game)), source); - game.addEffect(new CovetousUrgeSpendAnyManaEffect().setTargetPointer(new FixedTarget(card, game)), source); + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, card, Duration.Custom); return true; } } - -class CovetousUrgeCastFromExileEffect extends AsThoughEffectImpl { - - private final MageObjectReference mor; - - CovetousUrgeCastFromExileEffect(MageObjectReference mor) { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.Benefit); - this.mor = mor; - } - - private CovetousUrgeCastFromExileEffect(final CovetousUrgeCastFromExileEffect effect) { - super(effect); - this.mor = effect.mor; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public CovetousUrgeCastFromExileEffect copy() { - return new CovetousUrgeCastFromExileEffect(this); - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (mor.getCard(game) == null) { - discard(); - return false; - } - return mor.refersTo(sourceId, game) && source.isControlledBy(affectedControllerId); - } -} - -class CovetousUrgeSpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { - - CovetousUrgeSpendAnyManaEffect() { - super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.Custom, Outcome.Benefit); - } - - private CovetousUrgeSpendAnyManaEffect(final CovetousUrgeSpendAnyManaEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public CovetousUrgeSpendAnyManaEffect copy() { - return new CovetousUrgeSpendAnyManaEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - objectId = CardUtil.getMainCardId(game, objectId); // for split cards - FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); - return source.isControlledBy(affectedControllerId) - && Objects.equals(objectId, fixedTarget.getTarget()) - && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 - && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); - } - - @Override - public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { - return mana.getFirstAvailable(); - } -} diff --git a/Mage.Sets/src/mage/cards/d/DaxosOfMeletis.java b/Mage.Sets/src/mage/cards/d/DaxosOfMeletis.java index 2611031369..ed3225770f 100644 --- a/Mage.Sets/src/mage/cards/d/DaxosOfMeletis.java +++ b/Mage.Sets/src/mage/cards/d/DaxosOfMeletis.java @@ -5,11 +5,7 @@ import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; import mage.abilities.common.SimpleEvasionAbility; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.AsThoughManaEffect; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect; import mage.cards.Card; import mage.cards.CardImpl; @@ -17,14 +13,10 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.PowerPredicate; -import mage.game.ExileZone; import mage.game.Game; -import mage.players.ManaPoolItem; import mage.players.Player; -import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; -import java.util.Objects; import java.util.UUID; /** @@ -100,13 +92,8 @@ class DaxosOfMeletisEffect extends OneShotEffect { // Add effects only if the card has a spellAbility (e.g. not for lands). if (card.getSpellAbility() != null) { // allow to cast the card - ContinuousEffect effect = new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, Duration.EndOfTurn); - effect.setTargetPointer(new FixedTarget(card, game)); - game.addEffect(effect, source); // and you may spend mana as though it were mana of any color to cast it - effect = new DaxosOfMeletisSpendAnyManaEffect(); - effect.setTargetPointer(new FixedTarget(card, game)); - game.addEffect(effect, source); + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, card, Duration.EndOfTurn); } } return true; @@ -115,79 +102,3 @@ class DaxosOfMeletisEffect extends OneShotEffect { return false; } } - -class DaxosOfMeletisCastFromExileEffect extends AsThoughEffectImpl { - - private UUID cardId; - private UUID exileId; - - public DaxosOfMeletisCastFromExileEffect(UUID cardId, UUID exileId) { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit); - staticText = "Until end of turn, you may cast that card and you may spend mana as though it were mana of any color to cast it"; - this.cardId = cardId; - this.exileId = exileId; - } - - public DaxosOfMeletisCastFromExileEffect(final DaxosOfMeletisCastFromExileEffect effect) { - super(effect); - this.cardId = effect.cardId; - this.exileId = effect.exileId; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public DaxosOfMeletisCastFromExileEffect copy() { - return new DaxosOfMeletisCastFromExileEffect(this); - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (sourceId.equals(cardId) && source.isControlledBy(affectedControllerId)) { - ExileZone exileZone = game.getState().getExile().getExileZone(exileId); - return exileZone != null && exileZone.contains(cardId); - } - return false; - } -} - -class DaxosOfMeletisSpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { - - public DaxosOfMeletisSpendAnyManaEffect() { - super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.EndOfTurn, Outcome.Benefit); - staticText = "you may spend mana as though it were mana of any color to cast it"; - } - - public DaxosOfMeletisSpendAnyManaEffect(final DaxosOfMeletisSpendAnyManaEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public DaxosOfMeletisSpendAnyManaEffect copy() { - return new DaxosOfMeletisSpendAnyManaEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - objectId = CardUtil.getMainCardId(game, objectId); // for split cards - FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); - return source.isControlledBy(affectedControllerId) - && Objects.equals(objectId, fixedTarget.getTarget()) - && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 - && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); - } - - @Override - public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { - return mana.getFirstAvailable(); - } - -} diff --git a/Mage.Sets/src/mage/cards/d/DireFleetDaredevil.java b/Mage.Sets/src/mage/cards/d/DireFleetDaredevil.java index 664420628b..928949628f 100644 --- a/Mage.Sets/src/mage/cards/d/DireFleetDaredevil.java +++ b/Mage.Sets/src/mage/cards/d/DireFleetDaredevil.java @@ -3,8 +3,9 @@ package mage.cards.d; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.effects.*; -import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.keyword.FirstStrikeAbility; import mage.cards.Card; import mage.cards.CardImpl; @@ -16,13 +17,11 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.game.events.GameEvent.EventType; import mage.game.events.ZoneChangeEvent; -import mage.players.ManaPoolItem; import mage.players.Player; import mage.target.common.TargetCardInOpponentsGraveyard; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; -import java.util.Objects; import java.util.UUID; /** @@ -94,13 +93,10 @@ class DireFleetDaredevilEffect extends OneShotEffect { if (controller.moveCards(targetCard, Zone.EXILED, source, game)) { targetCard = game.getCard(targetCard.getId()); if (targetCard != null) { - ContinuousEffect effect = new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, Duration.EndOfTurn); - effect.setTargetPointer(new FixedTarget(targetCard, game)); - game.addEffect(effect, source); - effect = new DireFleetDaredevilSpendAnyManaEffect(); - effect.setTargetPointer(new FixedTarget(targetCard, game)); - game.addEffect(effect, source); - effect = new DireFleetDaredevilReplacementEffect(); + // you may play and spend any mana + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, targetCard, Duration.EndOfTurn); + // exile from graveyard + ContinuousEffect effect = new DireFleetDaredevilReplacementEffect(); effect.setTargetPointer(new FixedTarget(targetCard, game)); game.addEffect(effect, source); return true; @@ -112,44 +108,6 @@ class DireFleetDaredevilEffect extends OneShotEffect { } } -class DireFleetDaredevilSpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { - - public DireFleetDaredevilSpendAnyManaEffect() { - super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.EndOfTurn, Outcome.Benefit); - staticText = "you may spend mana as though it were mana of any color"; - } - - public DireFleetDaredevilSpendAnyManaEffect(final DireFleetDaredevilSpendAnyManaEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public DireFleetDaredevilSpendAnyManaEffect copy() { - return new DireFleetDaredevilSpendAnyManaEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - objectId = CardUtil.getMainCardId(game, objectId); // for split cards - FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); - return source.isControlledBy(affectedControllerId) - && Objects.equals(objectId, ((FixedTarget) getTargetPointer()).getTarget()) - && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 - && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); - } - - @Override - public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { - return mana.getFirstAvailable(); - } - -} - class DireFleetDaredevilReplacementEffect extends ReplacementEffectImpl { public DireFleetDaredevilReplacementEffect() { diff --git a/Mage.Sets/src/mage/cards/g/GrenzoHavocRaiser.java b/Mage.Sets/src/mage/cards/g/GrenzoHavocRaiser.java index dc1fbbc254..6a84b2badc 100644 --- a/Mage.Sets/src/mage/cards/g/GrenzoHavocRaiser.java +++ b/Mage.Sets/src/mage/cards/g/GrenzoHavocRaiser.java @@ -5,7 +5,8 @@ import mage.MageObject; import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.effects.*; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.combat.GoadTargetEffect; import mage.cards.Card; import mage.cards.CardImpl; @@ -13,18 +14,15 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.permanent.ControllerIdPredicate; -import mage.game.ExileZone; import mage.game.Game; import mage.game.events.DamagedEvent; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; -import mage.players.ManaPoolItem; import mage.players.Player; import mage.target.common.TargetCreaturePermanent; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; -import java.util.Objects; import java.util.UUID; /** @@ -158,11 +156,8 @@ class GrenzoHavocRaiserEffect extends OneShotEffect { // Add effects only if the card has a spellAbility (e.g. not for lands). if (card.getSpellAbility() != null) { // allow to cast the card - game.addEffect(new GrenzoHavocRaiserCastFromExileEffect(card.getId(), exileId), source); // and you may spend mana as though it were mana of any color to cast it - ContinuousEffect effect = new GrenzoHavocRaiserSpendAnyManaEffect(); - effect.setTargetPointer(new FixedTarget(card.getId())); - game.addEffect(effect, source); + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, card, Duration.EndOfTurn); } } return true; @@ -170,80 +165,4 @@ class GrenzoHavocRaiserEffect extends OneShotEffect { } return false; } -} - -class GrenzoHavocRaiserCastFromExileEffect extends AsThoughEffectImpl { - - private UUID cardId; - private UUID exileId; - - public GrenzoHavocRaiserCastFromExileEffect(UUID cardId, UUID exileId) { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit); - staticText = "Until end of turn, you may cast that card and you may spend mana as though it were mana of any color to cast it"; - this.cardId = cardId; - this.exileId = exileId; - } - - public GrenzoHavocRaiserCastFromExileEffect(final GrenzoHavocRaiserCastFromExileEffect effect) { - super(effect); - this.cardId = effect.cardId; - this.exileId = effect.exileId; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public GrenzoHavocRaiserCastFromExileEffect copy() { - return new GrenzoHavocRaiserCastFromExileEffect(this); - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (sourceId.equals(cardId) && source.isControlledBy(affectedControllerId)) { - ExileZone exileZone = game.getState().getExile().getExileZone(exileId); - return exileZone != null && exileZone.contains(cardId); - } - return false; - } -} - -class GrenzoHavocRaiserSpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { - - public GrenzoHavocRaiserSpendAnyManaEffect() { - super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.EndOfTurn, Outcome.Benefit); - staticText = "you may spend mana as though it were mana of any color to cast it"; - } - - public GrenzoHavocRaiserSpendAnyManaEffect(final GrenzoHavocRaiserSpendAnyManaEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public GrenzoHavocRaiserSpendAnyManaEffect copy() { - return new GrenzoHavocRaiserSpendAnyManaEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - objectId = CardUtil.getMainCardId(game, objectId); // for split cards - FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); - return source.isControlledBy(affectedControllerId) - && Objects.equals(objectId, ((FixedTarget) getTargetPointer()).getTarget()) - && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 - && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); - } - - @Override - public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { - return mana.getFirstAvailable(); - } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/h/HostageTaker.java b/Mage.Sets/src/mage/cards/h/HostageTaker.java index f91fdf13b3..df269b88dd 100644 --- a/Mage.Sets/src/mage/cards/h/HostageTaker.java +++ b/Mage.Sets/src/mage/cards/h/HostageTaker.java @@ -4,9 +4,6 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.delayed.OnLeaveReturnExiledToBattlefieldAbility; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.AsThoughManaEffect; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; import mage.cards.CardImpl; @@ -15,16 +12,12 @@ import mage.constants.*; import mage.filter.FilterPermanent; import mage.filter.predicate.Predicates; import mage.filter.predicate.permanent.AnotherPredicate; -import mage.game.ExileZone; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.ManaPoolItem; import mage.players.Player; import mage.target.TargetPermanent; -import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; -import java.util.Objects; import java.util.UUID; /** @@ -99,89 +92,8 @@ class HostageTakerExileEffect extends OneShotEffect { // move card to exile UUID exileId = CardUtil.getCardExileZoneId(game, source); controller.moveCardToExileWithInfo(card, exileId, permanent.getIdName(), source.getSourceId(), game, Zone.BATTLEFIELD, true); - // allow to cast the card - game.addEffect(new HostageTakerCastFromExileEffect(card.getId(), exileId), source); - // and you may spend mana as though it were mana of any color to cast it - ContinuousEffect effect = new HostageTakerSpendAnyManaEffect(); - effect.setTargetPointer(new FixedTarget(card.getId(), game)); - game.addEffect(effect, source); + // allow to cast the card and you may spend mana as though it were mana of any color to cast it + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, card, Duration.Custom); return true; } -} - -class HostageTakerCastFromExileEffect extends AsThoughEffectImpl { - - private UUID cardId; - private UUID exileId; - - HostageTakerCastFromExileEffect(UUID cardId, UUID exileId) { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.Custom, Outcome.Benefit); - this.cardId = cardId; - this.exileId = exileId; - } - - private HostageTakerCastFromExileEffect(final HostageTakerCastFromExileEffect effect) { - super(effect); - this.cardId = effect.cardId; - this.exileId = effect.exileId; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public HostageTakerCastFromExileEffect copy() { - return new HostageTakerCastFromExileEffect(this); - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (!sourceId.equals(cardId) || !source.isControlledBy(affectedControllerId)) { - return false; - } - ExileZone exileZone = game.getState().getExile().getExileZone(exileId); - if (exileZone != null && exileZone.contains(cardId)) { - return true; - } - discard(); - return false; - } -} - -class HostageTakerSpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { - - HostageTakerSpendAnyManaEffect() { - super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.Custom, Outcome.Benefit); - } - - private HostageTakerSpendAnyManaEffect(final HostageTakerSpendAnyManaEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public HostageTakerSpendAnyManaEffect copy() { - return new HostageTakerSpendAnyManaEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - objectId = CardUtil.getMainCardId(game, objectId); // for split cards - FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); - return source.isControlledBy(affectedControllerId) - && Objects.equals(objectId, fixedTarget.getTarget()) - && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 - && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); - } - - @Override - public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { - return mana.getFirstAvailable(); - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/Mindslaver.java b/Mage.Sets/src/mage/cards/m/Mindslaver.java index 15caea569a..6670717b28 100644 --- a/Mage.Sets/src/mage/cards/m/Mindslaver.java +++ b/Mage.Sets/src/mage/cards/m/Mindslaver.java @@ -1,7 +1,5 @@ - package mage.cards.m; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.SacrificeSourceCost; @@ -15,17 +13,18 @@ import mage.constants.SuperType; import mage.constants.Zone; import mage.target.TargetPlayer; +import java.util.UUID; + /** - * * @author nantuko */ public final class Mindslaver extends CardImpl { public Mindslaver(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT},"{6}"); + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{6}"); addSuperType(SuperType.LEGENDARY); - // {4}, {tap}, Sacrifice Mindslaver: You control target player during that player's next turn. + // {4}, {T}, Sacrifice Mindslaver: You control target player during that player's next turn. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new ControlTargetPlayerNextTurnEffect(), new GenericManaCost(4)); ability.addCost(new TapSourceCost()); ability.addCost(new SacrificeSourceCost()); diff --git a/Mage.Sets/src/mage/cards/o/OppositionAgent.java b/Mage.Sets/src/mage/cards/o/OppositionAgent.java new file mode 100644 index 0000000000..27a80c9e3f --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OppositionAgent.java @@ -0,0 +1,128 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.replacement.YouControlYourOpponentsWhileSearchingReplacementEffect; +import mage.abilities.keyword.FlashAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.LibrarySearchedEvent; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author JayDi85 + */ +public final class OppositionAgent extends CardImpl { + + public OppositionAgent(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}"); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // You control your opponents while they’re searching their libraries. + this.addAbility(new SimpleStaticAbility(new YouControlYourOpponentsWhileSearchingReplacementEffect())); + + // While an opponent is searching their library, they exile each card they find. You may play those cards for as long as they remain exiled, and you may spend mana as though it were mana of any color to cast them. + this.addAbility(new SimpleStaticAbility(new OppositionAgentReplacementEffect())); + + } + + public OppositionAgent(final OppositionAgent card) { + super(card); + } + + @Override + public OppositionAgent copy() { + return new OppositionAgent(this); + } +} + +class OppositionAgentReplacementEffect extends ReplacementEffectImpl { + + public OppositionAgentReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "While an opponent is searching their library, they exile each card they find. You may play " + + "those cards for as long as they remain exiled, and you may spend mana as though it were mana " + + "of any color to cast them"; + } + + OppositionAgentReplacementEffect(final OppositionAgentReplacementEffect effect) { + super(effect); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + LibrarySearchedEvent se = (LibrarySearchedEvent) event; + + // opponent is searching their library + if (!event.getTargetId().equals(event.getPlayerId())) { + return false; + } + + Player targetPlayer = game.getPlayer(event.getTargetId()); + if (targetPlayer == null) { + return false; + } + + Set cardsToExile = se.getSearchedTarget().getTargets().stream() + .map(id -> targetPlayer.getLibrary().getCard(id, game)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (cardsToExile.isEmpty()) { + return false; + } + + // exile each card they find + targetPlayer.moveCards(cardsToExile, Zone.EXILED, source, game); + cardsToExile.removeIf(card -> game.getState().getZone(card.getId()) != Zone.EXILED); + if (cardsToExile.isEmpty()) { + return false; + } + + // remove exiled cards from library searched result + cardsToExile.forEach(card -> se.getSearchedTarget().remove(card.getId())); + + // You may play those cards for as long as they remain exiled, and you may spend mana as though it were mana of any color to cast them + for (Card card : cardsToExile) { + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, card, Duration.Custom); + } + + // return false all the time + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.LIBRARY_SEARCHED; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + return controller != null && game.isOpponent(controller, event.getPlayerId()); + } + + @Override + public OppositionAgentReplacementEffect copy() { + return new OppositionAgentReplacementEffect(this); + } +} + + diff --git a/Mage.Sets/src/mage/cards/s/StolenStrategy.java b/Mage.Sets/src/mage/cards/s/StolenStrategy.java index d9b04a41c1..067df28273 100644 --- a/Mage.Sets/src/mage/cards/s/StolenStrategy.java +++ b/Mage.Sets/src/mage/cards/s/StolenStrategy.java @@ -4,8 +4,6 @@ import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.AsThoughManaEffect; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.CardImpl; @@ -13,12 +11,9 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.ExileZone; import mage.game.Game; -import mage.players.ManaPoolItem; import mage.players.Player; -import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; -import java.util.Objects; import java.util.UUID; /** @@ -84,11 +79,8 @@ class StolenStrategyEffect extends OneShotEffect { // Add effects only if the card has a spellAbility (e.g. not for lands). if (!card.isLand() && card.getSpellAbility() != null) { // allow to cast the card - game.addEffect(new StolenStrategyCastFromExileEffect(card.getId(), exileId), source); // and you may spend mana as though it were mana of any color to cast it - ContinuousEffect effect = new StolenStrategySpendAnyManaEffect(); - effect.setTargetPointer(new FixedTarget(card.getId())); - game.addEffect(effect, source); + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, card, Duration.EndOfTurn); } } } @@ -98,8 +90,8 @@ class StolenStrategyEffect extends OneShotEffect { class StolenStrategyCastFromExileEffect extends AsThoughEffectImpl { - private UUID cardId; - private UUID exileId; + private final UUID cardId; + private final UUID exileId; public StolenStrategyCastFromExileEffect(UUID cardId, UUID exileId) { super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfTurn, Outcome.Benefit); @@ -133,41 +125,3 @@ class StolenStrategyCastFromExileEffect extends AsThoughEffectImpl { return false; } } - -class StolenStrategySpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { - - public StolenStrategySpendAnyManaEffect() { - super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.EndOfTurn, Outcome.Benefit); - staticText = "you may spend mana as though it were mana of any color to cast it"; - } - - public StolenStrategySpendAnyManaEffect(final StolenStrategySpendAnyManaEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public StolenStrategySpendAnyManaEffect copy() { - return new StolenStrategySpendAnyManaEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - objectId = CardUtil.getMainCardId(game, objectId); // for split cards - FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); - return source.isControlledBy(affectedControllerId) - && Objects.equals(objectId, fixedTarget.getTarget()) - && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 - && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); - } - - @Override - public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { - return mana.getFirstAvailable(); - } - -} diff --git a/Mage.Sets/src/mage/cards/t/TobiasBeckett.java b/Mage.Sets/src/mage/cards/t/TobiasBeckett.java index 749b1eaa4d..26dffb17d0 100644 --- a/Mage.Sets/src/mage/cards/t/TobiasBeckett.java +++ b/Mage.Sets/src/mage/cards/t/TobiasBeckett.java @@ -4,9 +4,6 @@ import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.AsThoughManaEffect; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.abilities.keyword.BountyAbility; @@ -15,16 +12,12 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.counters.CounterType; -import mage.game.ExileZone; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.ManaPoolItem; import mage.players.Player; import mage.target.common.TargetOpponentsCreaturePermanent; -import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; -import java.util.Objects; import java.util.UUID; /** @@ -89,11 +82,8 @@ class TobiasBeckettEffect extends OneShotEffect { // Add effects only if the card has a spellAbility (e.g. not for lands). if (card.getSpellAbility() != null) { // allow to cast the card - game.addEffect(new TobiasBeckettCastFromExileEffect(card.getId(), exileId), source); // and you may spend mana as though it were mana of any color to cast it - ContinuousEffect effect = new TobiasBeckettSpendAnyManaEffect(); - effect.setTargetPointer(new FixedTarget(card.getId())); - game.addEffect(effect, source); + CardUtil.makeCardPlayableAndSpendManaAsAnyColor(game, source, card, Duration.Custom); } } return true; @@ -107,82 +97,4 @@ class TobiasBeckettEffect extends OneShotEffect { public TobiasBeckettEffect copy() { return new TobiasBeckettEffect(this); } -} - -// Based on GrenzoHavocRaiserCastFromExileEffect -class TobiasBeckettCastFromExileEffect extends AsThoughEffectImpl { - - private UUID cardId; - private UUID exileId; - - public TobiasBeckettCastFromExileEffect(UUID cardId, UUID exileId) { - super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit); - staticText = "You may cast that card and you may spend mana as though it were mana of any color to cast it"; - this.cardId = cardId; - this.exileId = exileId; - } - - public TobiasBeckettCastFromExileEffect(final TobiasBeckettCastFromExileEffect effect) { - super(effect); - this.cardId = effect.cardId; - this.exileId = effect.exileId; - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public TobiasBeckettCastFromExileEffect copy() { - return new TobiasBeckettCastFromExileEffect(this); - } - - @Override - public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { - if (sourceId.equals(cardId) && source.isControlledBy(affectedControllerId)) { - ExileZone exileZone = game.getState().getExile().getExileZone(exileId); - return exileZone != null && exileZone.contains(cardId); - } - return false; - } -} - -// Based on GrenzoHavocRaiserSpendAnyManaEffect -class TobiasBeckettSpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { - - public TobiasBeckettSpendAnyManaEffect() { - super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.EndOfTurn, Outcome.Benefit); - staticText = "you may spend mana as though it were mana of any color to cast it"; - } - - public TobiasBeckettSpendAnyManaEffect(final TobiasBeckettSpendAnyManaEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - return true; - } - - @Override - public TobiasBeckettSpendAnyManaEffect copy() { - return new TobiasBeckettSpendAnyManaEffect(this); - } - - @Override - public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { - objectId = CardUtil.getMainCardId(game, objectId); // for split cards - FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); - return source.isControlledBy(affectedControllerId) - && Objects.equals(objectId, fixedTarget.getTarget()) - && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 - && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); - } - - @Override - public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { - return mana.getFirstAvailable(); - } - -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/w/WordOfCommand.java b/Mage.Sets/src/mage/cards/w/WordOfCommand.java index cc90a8c295..42bd219fa2 100644 --- a/Mage.Sets/src/mage/cards/w/WordOfCommand.java +++ b/Mage.Sets/src/mage/cards/w/WordOfCommand.java @@ -1,5 +1,6 @@ package mage.cards.w; +import mage.ApprovingObject; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.SpellAbility; @@ -22,9 +23,9 @@ import mage.players.Player; import mage.target.TargetCard; import mage.target.common.TargetOpponent; import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; import java.util.UUID; -import mage.ApprovingObject; /** * @author L_J @@ -95,13 +96,7 @@ class WordOfCommandEffect extends OneShotEffect { } // You control that player until Word of Command finishes resolving - controller.controlPlayersTurn(game, targetPlayer.getId()); - while (controller.canRespond()) { - if (controller.chooseUse(Outcome.Benefit, "Resolve " + sourceObject.getLogName() + " now" + (card != null ? " and play " + card.getLogName() : "") + '?', source, game)) { - // this is used to give the controller a little space to utilize their player controlling effect (look at face down creatures, hand, etc.) - break; - } - } + CardUtil.takeControlUnderPlayerStart(game, controller, targetPlayer, true); // The player plays that card if able if (card != null) { @@ -156,10 +151,7 @@ class WordOfCommandEffect extends OneShotEffect { if (wordOfCommand != null) { wordOfCommand.setCommandedBy(controller.getId()); // You control the player until Word of Command finishes resolving } else { - targetPlayer.setGameUnderYourControl(true, false); - if (!targetPlayer.getTurnControlledBy().equals(controller.getId())) { - controller.getPlayersUnderYourControl().remove(targetPlayer.getId()); - } + CardUtil.takeControlUnderPlayerEnd(game, controller, targetPlayer); } return true; } diff --git a/Mage.Sets/src/mage/sets/CommanderLegends.java b/Mage.Sets/src/mage/sets/CommanderLegends.java index c4d6bbd359..012a370df7 100644 --- a/Mage.Sets/src/mage/sets/CommanderLegends.java +++ b/Mage.Sets/src/mage/sets/CommanderLegends.java @@ -456,6 +456,8 @@ public final class CommanderLegends extends ExpansionSet { cards.add(new SetCardInfo("On Serra's Wings", 380, Rarity.UNCOMMON, mage.cards.o.OnSerrasWings.class)); cards.add(new SetCardInfo("Opal Palace", 352, Rarity.COMMON, mage.cards.o.OpalPalace.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Opal Palace", 707, Rarity.COMMON, mage.cards.o.OpalPalace.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Opposition Agent", 141, Rarity.RARE, mage.cards.o.OppositionAgent.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Opposition Agent", 651, Rarity.RARE, mage.cards.o.OppositionAgent.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Open the Armory", 34, Rarity.UNCOMMON, mage.cards.o.OpenTheArmory.class)); cards.add(new SetCardInfo("Ordeal of Nylea", 247, Rarity.UNCOMMON, mage.cards.o.OrdealOfNylea.class)); cards.add(new SetCardInfo("Oreskos Explorer", 381, Rarity.UNCOMMON, mage.cards.o.OreskosExplorer.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/asthough/SpendOtherManaTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/asthough/SpendOtherManaTest.java index a25c1ea544..9b992062cd 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/asthough/SpendOtherManaTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/asthough/SpendOtherManaTest.java @@ -120,6 +120,10 @@ public class SpendOtherManaTest extends CardTestPlayerBase { // You may cast that card as long as it remains exiled, and you may spend mana as though it were mana of any type to cast that spell. addCard(Zone.HAND, playerA, "Hostage Taker"); // {2}{U}{B} + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}."); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}."); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}."); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}."); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Hostage Taker"); addTarget(playerA, "Silvercoat Lion"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java new file mode 100644 index 0000000000..8859f44a2a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/control/TakeControlWhileSearchingLibraryTest.java @@ -0,0 +1,254 @@ +package org.mage.test.cards.control; + +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.replacement.YouControlYourOpponentsWhileSearchingReplacementEffect; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase { + + @Test + public void test_SimpleSearchingLibrary_Normal() { + removeAllCardsFromLibrary(playerA); + removeAllCardsFromLibrary(playerB); + + addCard(Zone.LIBRARY, playerA, "Balduvian Bears", 1); + addCard(Zone.LIBRARY, playerB, "Kitesail Corsair", 1); + // + // Search your library for up to three creature cards and put them into your graveyard. Then shuffle your library. + addCard(Zone.HAND, playerA, "Buried Alive", 1); // {2}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + + // before + checkGraveyardCount("before a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); + checkGraveyardCount("before b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Kitesail Corsair", 0); + + // search as normal + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); + addTarget(playerA, "Balduvian Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // after + checkGraveyardCount("after a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + checkGraveyardCount("after b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Kitesail Corsair", 0); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_SimpleSearchingLibrary_TakeControl() { + removeAllCardsFromLibrary(playerA); + removeAllCardsFromLibrary(playerB); + + addCard(Zone.LIBRARY, playerA, "Balduvian Bears", 1); + addCard(Zone.LIBRARY, playerB, "Kitesail Corsair", 1); + // + // Search your library for up to three creature cards and put them into your graveyard. Then shuffle your library. + addCard(Zone.HAND, playerA, "Buried Alive", 1); // {2}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + // + // You control your opponents while they’re searching their libraries. + addCustomCardWithAbility("control", playerB, new SimpleStaticAbility( + new YouControlYourOpponentsWhileSearchingReplacementEffect()) + ); + // + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + + // before + checkGraveyardCount("before a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); + checkGraveyardCount("before b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Kitesail Corsair", 0); + + // search under control of B + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); + setChoice(playerB, "Yes"); // continue + addTarget(playerB, "Balduvian Bears"); // player B must take control for searching + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // after + checkGraveyardCount("after a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + checkGraveyardCount("after b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Kitesail Corsair", 0); + + // check that control returned + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt"); + addTarget(playerA, playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_CastCardWhileSearchingLibrary_Normal() { + removeAllCardsFromLibrary(playerA); + removeAllCardsFromLibrary(playerB); + + addCard(Zone.LIBRARY, playerA, "Balduvian Bears", 1); + addCard(Zone.LIBRARY, playerB, "Kitesail Corsair", 1); + // + // While you're searching your library, you may cast Panglacial Wurm from your library. + addCard(Zone.LIBRARY, playerA, "Panglacial Wurm", 1); // {5}{G}{G} + addCard(Zone.BATTLEFIELD, playerA, "Panglacial Wurm", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 7); + // + // Search your library for up to three creature cards and put them into your graveyard. Then shuffle your library. + addCard(Zone.HAND, playerA, "Buried Alive", 1); // {2}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + // + + // before + checkGraveyardCount("before a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); + checkGraveyardCount("before b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Kitesail Corsair", 0); + + // search as normal and cast + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); + setChoice(playerA, "Yes"); // yes, try to cast a creature card from lib + setChoice(playerA, "Panglacial Wurm"); // try to cast + addTarget(playerA, "Balduvian Bears"); // choice for searching + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // after + checkGraveyardCount("after a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + checkGraveyardCount("after b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Kitesail Corsair", 0); + checkPermanentCount("must cast Panglacial Wurm", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Panglacial Wurm", 2); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + } + + @Test + @Ignore // unsupported by unit tests, see test_UnderControlMustUseTestCommandsCorrectrly + public void test_CastCardWhileSearchingLibrary_TakeControl() { + /* + [test control manually] + // use case: login by p1 and p2 clients, cast Buried Alive -> control under p2 -> try cast Panglacial Wurm and search for Balduvian Bears + library:p1:Balduvian Bears:1 + library:p2:Kitesail Corsair:1 + library:p1:Panglacial Wurm:1 + battlefield:p1:Panglacial Wurm:1 + battlefield:p1:Forest:7 + hand:p1:Buried Alive:1 + battlefield:p1:Swamp:3 + battlefield:p2:Opposition Agent:1 + */ + + removeAllCardsFromLibrary(playerA); + removeAllCardsFromLibrary(playerB); + + addCard(Zone.LIBRARY, playerA, "Balduvian Bears", 1); + addCard(Zone.LIBRARY, playerB, "Kitesail Corsair", 1); + // + // While you're searching your library, you may cast Panglacial Wurm from your library. + addCard(Zone.LIBRARY, playerA, "Panglacial Wurm", 1); // {5}{G}{G} + addCard(Zone.BATTLEFIELD, playerA, "Panglacial Wurm", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 7); + // + // Search your library for up to three creature cards and put them into your graveyard. Then shuffle your library. + addCard(Zone.HAND, playerA, "Buried Alive", 1); // {2}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + + // before + checkGraveyardCount("before a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); + checkGraveyardCount("before b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Kitesail Corsair", 0); + + // search under control of B and cast under B too + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 3); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); + setChoice(playerB, "Yes"); // continue + setChoice(playerB, "Yes"); // yes, try to cast a creature card from lib + setChoice(playerB, "Panglacial Wurm"); // try to cast + addTarget(playerB, "Balduvian Bears"); // choice for searching + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // after + checkGraveyardCount("after a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + checkGraveyardCount("after b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Kitesail Corsair", 0); + checkPermanentCount("must cast Panglacial Wurm", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Panglacial Wurm", 2); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + } + + @Test + @Ignore + // TODO: current "take player under control" implemented in GameController and HumanPlayer, + // not "game part" - so tests and AI ignore it and must be tested manually + // see another problems with control in HumanPlayer.priority(Game game) and https://github.com/magefree/mage/issues/2088 + public void test_UnderControlMustUseTestCommandsCorrectrly() { + // {4}, {T}, Sacrifice Mindslaver: You control target player during that player's next turn. + addCard(Zone.BATTLEFIELD, playerA, "Mindslaver", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + // + addCard(Zone.HAND, playerB, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1); + + // activate and take control + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}, {T}"); + addTarget(playerA, playerB); + + // check control + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt"); + addTarget(playerA, playerB); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_OppositionAgent() { + // You control your opponents while they’re searching their libraries. + // While an opponent is searching their library, they exile each card they find. You may play those cards + // for as long as they remain exiled, and you may spend mana as though it were mana of any color to cast them. + addCard(Zone.BATTLEFIELD, playerB, "Opposition Agent", 1); + // + // Search your library for up to three creature cards and put them into your graveyard. Then shuffle your library. + addCard(Zone.HAND, playerA, "Buried Alive", 1); // {2}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + // + addCard(Zone.LIBRARY, playerA, "Balduvian Bears", 1); // {1}{G} + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 2); // B can cast green bear for red mana + + // before + checkPermanentCount("before a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); + checkPermanentCount("before b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 0); + + // start searching under B (bears must go to exile instead graveyard) + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Buried Alive"); + setChoice(playerB, "Yes"); // continue after new control + addTarget(playerB, "Balduvian Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkGraveyardCount("after grave a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 0); + checkGraveyardCount("after grave b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 0); + checkExileCount("after exile a", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + checkExileCount("after exile b", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears", 0); + + // B can cast green bear for red mana + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerB, "Balduvian Bears", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 80052a4776..9ccf4d56ad 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -1302,7 +1302,7 @@ public class TestPlayer implements Player { printStart("Permanents of " + player.getName()); printPermanents(game, game.getBattlefield().getAllActivePermanents(player.getId())); printEnd(); - Assert.fail(action.getActionName() + " - permanent " + permanentName + " must exists in " + count + " instances"); + Assert.fail(action.getActionName() + " - permanent " + permanentName + " must exists in " + count + " instances, but founded " + foundedCount); } } @@ -2526,7 +2526,8 @@ public class TestPlayer implements Player { this.chooseStrictModeFailed("choice", game, getInfo(source, game) + "\nMessage: " + message - + "\nChoices: " + (trueText != null ? trueText : "Yes") + " - " + (falseText != null ? falseText : "No")); + + "\nChoices: " + (trueText != null ? trueText : "Yes") + " - " + (falseText != null ? falseText : "No") + + ((trueText != null || falseText != null) ? "\nUse Yes/No in unit tests for text choices." : "")); return computerPlayer.chooseUse(outcome, message, secondMessage, trueText, falseText, source, game); } diff --git a/Mage/src/main/java/mage/abilities/effects/common/asthought/CanPlayCardControllerEffect.java b/Mage/src/main/java/mage/abilities/effects/common/asthought/CanPlayCardControllerEffect.java new file mode 100644 index 0000000000..badff50258 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/asthought/CanPlayCardControllerEffect.java @@ -0,0 +1,53 @@ +package mage.abilities.effects.common.asthought; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.constants.AsThoughEffectType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; + +import java.util.UUID; + +/** + * Play card from current zone. Will be discarded on any card movements or blinks. + *

+ * Recommends to use combo effects from CardUtil.makeCardPlayableAndSpendManaAsAnyColor instead signle effect + * + * @author JayDi85 + */ +public class CanPlayCardControllerEffect extends AsThoughEffectImpl { + + protected final MageObjectReference mor; + + public CanPlayCardControllerEffect(Game game, UUID cardId, int cardZCC, Duration duration) { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, duration, Outcome.Benefit); + this.staticText = "You may play those card"; + this.mor = new MageObjectReference(cardId, cardZCC, game); + } + + public CanPlayCardControllerEffect(final CanPlayCardControllerEffect effect) { + super(effect); + this.mor = effect.mor; + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public CanPlayCardControllerEffect copy() { + return new CanPlayCardControllerEffect(this); + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + if (mor.getCard(game) == null) { + discard(); + return false; + } + return mor.refersTo(sourceId, game) && source.isControlledBy(affectedControllerId); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/effects/common/asthought/YouMaySpendManaAsAnyColorToCastTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/asthought/YouMaySpendManaAsAnyColorToCastTargetEffect.java new file mode 100644 index 0000000000..31be9b8ff4 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/asthought/YouMaySpendManaAsAnyColorToCastTargetEffect.java @@ -0,0 +1,55 @@ +package mage.abilities.effects.common.asthought; + +import mage.abilities.Ability; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.AsThoughManaEffect; +import mage.constants.*; +import mage.game.Game; +import mage.players.ManaPoolItem; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.UUID; + +/** + * Spend mana as any color to cast targeted card. Will not affected after any card movements or blinks. + * + * @author JayDi85 + */ +public class YouMaySpendManaAsAnyColorToCastTargetEffect extends AsThoughEffectImpl implements AsThoughManaEffect { + + public YouMaySpendManaAsAnyColorToCastTargetEffect(Duration duration) { + super(AsThoughEffectType.SPEND_OTHER_MANA, duration, Outcome.Benefit); + this.staticText = "You may spend mana as though it were mana of any color to cast it"; + } + + public YouMaySpendManaAsAnyColorToCastTargetEffect(final YouMaySpendManaAsAnyColorToCastTargetEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public YouMaySpendManaAsAnyColorToCastTargetEffect copy() { + return new YouMaySpendManaAsAnyColorToCastTargetEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + objectId = CardUtil.getMainCardId(game, objectId); // for split cards + FixedTarget fixedTarget = ((FixedTarget) getTargetPointer()); + return source.isControlledBy(affectedControllerId) + && Objects.equals(objectId, fixedTarget.getTarget()) + && game.getState().getZoneChangeCounter(objectId) <= fixedTarget.getZoneChangeCounter() + 1 + && (game.getState().getZone(objectId) == Zone.STACK || game.getState().getZone(objectId) == Zone.EXILED); + } + + @Override + public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { + return mana.getFirstAvailable(); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/effects/common/replacement/YouControlYourOpponentsWhileSearchingReplacementEffect.java b/Mage/src/main/java/mage/abilities/effects/common/replacement/YouControlYourOpponentsWhileSearchingReplacementEffect.java new file mode 100644 index 0000000000..4d65f0cde4 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/replacement/YouControlYourOpponentsWhileSearchingReplacementEffect.java @@ -0,0 +1,48 @@ +package mage.abilities.effects.common.replacement; + +import mage.abilities.Ability; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.SearchLibraryEvent; +import mage.players.Player; + +/** + * @author JayDi85 + */ +public class YouControlYourOpponentsWhileSearchingReplacementEffect extends ReplacementEffectImpl { + + public YouControlYourOpponentsWhileSearchingReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "You control your opponents while they’re searching their libraries"; + } + + YouControlYourOpponentsWhileSearchingReplacementEffect(final YouControlYourOpponentsWhileSearchingReplacementEffect effect) { + super(effect); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + SearchLibraryEvent se = (SearchLibraryEvent) event; + se.setSearchingControllerId(source.getControllerId()); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SEARCH_LIBRARY; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + return controller != null && game.isOpponent(controller, event.getPlayerId()); + } + + @Override + public YouControlYourOpponentsWhileSearchingReplacementEffect copy() { + return new YouControlYourOpponentsWhileSearchingReplacementEffect(this); + } +} diff --git a/Mage/src/main/java/mage/game/events/LibrarySearchedEvent.java b/Mage/src/main/java/mage/game/events/LibrarySearchedEvent.java new file mode 100644 index 0000000000..b344b97e6e --- /dev/null +++ b/Mage/src/main/java/mage/game/events/LibrarySearchedEvent.java @@ -0,0 +1,30 @@ +package mage.game.events; + +import mage.target.Target; + +import java.util.UUID; + +/** + * @author JayDi85 + */ +public class LibrarySearchedEvent extends GameEvent { + + protected Target searchedTarget; + + /** + * Searched library event (after library searching finished). Return false on replaceEvent to + * + * @param targetPlayerId whose library searched + * @param sourceId source of the searching effect + * @param playerId who must search the library + * @param searchedTarget founded cards (targets list can be changed by replace events, see Opposition Agent) + */ + public LibrarySearchedEvent(UUID targetPlayerId, UUID sourceId, UUID playerId, Target searchedTarget) { + super(EventType.LIBRARY_SEARCHED, targetPlayerId, sourceId, playerId, searchedTarget.getTargets().size(), false); + this.searchedTarget = searchedTarget; + } + + public Target getSearchedTarget() { + return this.searchedTarget; + } +} diff --git a/Mage/src/main/java/mage/game/events/SearchLibraryEvent.java b/Mage/src/main/java/mage/game/events/SearchLibraryEvent.java new file mode 100644 index 0000000000..d36a2e71d5 --- /dev/null +++ b/Mage/src/main/java/mage/game/events/SearchLibraryEvent.java @@ -0,0 +1,32 @@ +package mage.game.events; + +import java.util.UUID; + +/** + * @author JayDi85 + */ +public class SearchLibraryEvent extends GameEvent { + + protected UUID searchingControllerId; // who controls the searching process, see Opposition Agent + + /** + * Searching library event + * + * @param targetPlayerId whose library will be searched + * @param sourceId source of the searching effect + * @param playerId who must search the library (also see searchingControllerId) + * @param amount cards amount to search + */ + public SearchLibraryEvent(UUID targetPlayerId, UUID sourceId, UUID playerId, int amount) { + super(GameEvent.EventType.SEARCH_LIBRARY, targetPlayerId, sourceId, playerId, amount, false); + this.searchingControllerId = playerId; + } + + public UUID getSearchingControllerId() { + return this.searchingControllerId; + } + + public void setSearchingControllerId(UUID searchingControllerId) { + this.searchingControllerId = searchingControllerId; + } +} diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index c30717eaa0..340b2368b0 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1,10 +1,7 @@ package mage.players; import com.google.common.collect.ImmutableMap; -import mage.ApprovingObject; -import mage.ConditionalMana; -import mage.MageObject; -import mage.Mana; +import mage.*; import mage.abilities.*; import mage.abilities.ActivatedAbility.ActivationStatus; import mage.abilities.common.PassAbility; @@ -28,7 +25,6 @@ import mage.abilities.mana.ManaOptions; import mage.actions.MageDrawAction; import mage.cards.*; import mage.cards.decks.Deck; -import mage.choices.ChoiceImpl; import mage.constants.*; import mage.counters.Counter; import mage.counters.CounterType; @@ -38,6 +34,7 @@ import mage.designations.DesignationType; import mage.filter.FilterCard; import mage.filter.FilterMana; import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; import mage.filter.common.FilterControlledPermanent; import mage.filter.common.FilterCreatureForCombat; import mage.filter.common.FilterCreatureForCombatBlock; @@ -2609,73 +2606,97 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId) { //20091005 - 701.14c - Library searchedLibrary = null; - String searchInfo = null; - if (targetPlayerId.equals(playerId)) { - searchInfo = getLogName() + " searches their library"; - searchedLibrary = library; + + // searching control can be intercepted by another player, see Opposition Agent + SearchLibraryEvent searchEvent = new SearchLibraryEvent(targetPlayerId, source.getSourceId(), playerId, Integer.MAX_VALUE); + if (game.replaceEvent(searchEvent)) { + return false; + } + + Player targetPlayer = game.getPlayer(targetPlayerId); + Player searchingPlayer = this; + Player searchingController = game.getPlayer(searchEvent.getSearchingControllerId()); + if (targetPlayer == null || searchingController == null) { + return false; + } + + String searchInfo = searchingPlayer.getLogName(); + if (!searchingPlayer.getId().equals(searchingController.getId())) { + searchInfo = searchInfo + " under control of " + searchingPlayer.getLogName(); + } + if (targetPlayer.getId().equals(searchingPlayer.getId())) { + searchInfo = searchInfo + " searches their library"; } else { - Player targetPlayer = game.getPlayer(targetPlayerId); - if (targetPlayer != null) { - searchInfo = getLogName() + " searches the library of " + targetPlayer.getLogName(); - searchedLibrary = targetPlayer.getLibrary(); - } - } - if (searchedLibrary == null) { - return false; - } - GameEvent event = GameEvent.getEvent(GameEvent.EventType.SEARCH_LIBRARY, - targetPlayerId, source.getSourceId(), playerId, Integer.MAX_VALUE); - if (game.replaceEvent(event)) { - return false; + searchInfo = searchInfo + " searches the library of " + targetPlayer.getLogName(); } + if (!game.isSimulation()) { game.informPlayers(searchInfo); } + + // https://www.reddit.com/r/magicTCG/comments/jj8gh9/opposition_agent_and_panglacial_wurm_interaction/ + // You must take full player control while searching, e.g. you can cast opponent's cards by Panglacial Wurm effect: + // * While you’re searching your library, you may cast Panglacial Wurm from your library. + // So use here same code as Word of Command + // P.S. no needs in searchingController, but it helps with unit tests, see TakeControlWhileSearchingLibraryTest + boolean takeControl = false; + if (!searchingPlayer.getId().equals(searchingController.getId())) { + CardUtil.takeControlUnderPlayerStart(game, searchingController, searchingPlayer, true); + takeControl = true; + } + + Library searchingLibrary = targetPlayer.getLibrary(); TargetCardInLibrary newTarget = target.copy(); int count; - int librarySearchLimit = event.getAmount(); + int librarySearchLimit = searchEvent.getAmount(); List cardsFromTop = null; do { // TODO: prevent shuffling from moving the visualized cards if (librarySearchLimit == Integer.MAX_VALUE) { - count = searchedLibrary.count(target.getFilter(), game); + count = searchingLibrary.count(target.getFilter(), game); } else { - Player targetPlayer = game.getPlayer(targetPlayerId); - if (targetPlayer == null) { - return false; - } if (cardsFromTop == null) { - cardsFromTop = new ArrayList<>(targetPlayer.getLibrary().getTopCards(game, librarySearchLimit)); + cardsFromTop = new ArrayList<>(searchingLibrary.getTopCards(game, librarySearchLimit)); } else { - cardsFromTop.retainAll(targetPlayer.getLibrary().getCards(game)); + cardsFromTop.retainAll(searchingLibrary.getCards(game)); } newTarget.setCardLimit(Math.min(librarySearchLimit, cardsFromTop.size())); - count = Math.min(searchedLibrary.count(target.getFilter(), game), librarySearchLimit); + count = Math.min(searchingLibrary.count(target.getFilter(), game), librarySearchLimit); } if (count < target.getNumberOfTargets()) { newTarget.setMinNumberOfTargets(count); } - if (newTarget.choose(Outcome.Neutral, playerId, targetPlayerId, game)) { - if (targetPlayerId.equals(playerId) && handleLibraryCastableCards(library, - game, targetPlayerId)) { // for handling Panglacial Wurm + + // handling Panglacial Wurm - cast cards while searching from own library + if (targetPlayer.getId().equals(searchingPlayer.getId())) { + if (handleCastableCardsWhileLibrarySearching(library, game, targetPlayer)) { + // clear all choices to start from scratch (casted cards must be removed from library) newTarget.clearChosen(); continue; } + } + + if (newTarget.choose(Outcome.Neutral, searchingController.getId(), targetPlayer.getId(), game)) { target.getTargets().clear(); for (UUID targetId : newTarget.getTargets()) { target.add(targetId, game); } - - } else if (targetPlayerId.equals(playerId) && handleLibraryCastableCards(library, - game, targetPlayerId)) { // for handling Panglacial Wurm - newTarget.clearChosen(); - continue; } - game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIBRARY_SEARCHED, targetPlayerId, playerId)); + + // END SEARCH + if (takeControl) { + CardUtil.takeControlUnderPlayerEnd(game, searchingController, searchingPlayer); + game.informPlayers("Control of " + searchingPlayer.getLogName() + " is back"); + } + + LibrarySearchedEvent searchedEvent = new LibrarySearchedEvent(targetPlayer.getId(), source.getSourceId(), searchingPlayer.getId(), target); + if (game.replaceEvent(searchedEvent)) { + return false; + } break; } while (true); + return true; } @@ -2690,58 +2711,53 @@ public abstract class PlayerImpl implements Player, Serializable { } } - private boolean handleLibraryCastableCards(Library library, Game game, UUID targetPlayerId) { - // for handling Panglacial Wurm - boolean alreadyChosenUse = false; - Map libraryCastableCardTracker = new HashMap<>(); - searchForCards: - do { - for (Card card : library.getCards(game)) { - for (Ability ability : card.getAbilities()) { - if (ability.getClass() == WhileSearchingPlayFromLibraryAbility.class) { - libraryCastableCardTracker.put(card.getId(), card.getIdName()); - } - } + private boolean handleCastableCardsWhileLibrarySearching(Library library, Game game, Player targetPlayer) { + // must return true after cast try (to restart searching process without casted cards) + // uses for handling Panglacial Wurm: + // * While you're searching your library, you may cast Panglacial Wurm from your library. + + List castableCards = library.getCards(game).stream() + .filter(card -> card.getAbilities(game).containsClass(WhileSearchingPlayFromLibraryAbility.class)) + .map(MageItem::getId) + .collect(Collectors.toList()); + if (castableCards.size() == 0) { + return false; + } + + // only humans can use it + if (!targetPlayer.isHuman() && !targetPlayer.isTestMode()) { + return false; + } + + if (!targetPlayer.chooseUse(Outcome.AIDontUseIt, "Library have " + castableCards.size() + " castable cards on searching. Do you want to cast it?", null, game)) { + return false; + } + + boolean casted = false; + TargetCard targetCard = new TargetCard(0, 1, Zone.LIBRARY, StaticFilters.FILTER_CARD); + targetCard.setTargetName("card to cast from library"); + targetCard.setNotTarget(true); + while (castableCards.size() > 0) { + targetCard.clearChosen(); + if (!targetPlayer.choose(Outcome.AIDontUseIt, new CardsImpl(castableCards), targetCard, game)) { + break; } - if (!libraryCastableCardTracker.isEmpty()) { - Player player = game.getPlayer(targetPlayerId); - if (player != null) { - if (player.isHuman() && (alreadyChosenUse || player.chooseUse(Outcome.AIDontUseIt, - "Cast a creature card from your library? (choose \"No\" to finish search)", null, game))) { - ChoiceImpl chooseCard = new ChoiceImpl(); - chooseCard.setMessage("Which creature do you wish to cast from your library?"); - Set choice = new LinkedHashSet<>(); - for (Entry entry : libraryCastableCardTracker.entrySet()) { - choice.add(new AbstractMap.SimpleEntry<>(entry).getValue()); - } - chooseCard.setChoices(choice); - while (!choice.isEmpty()) { - if (player.choose(Outcome.AIDontUseIt, chooseCard, game)) { - String chosenCard = chooseCard.getChoice(); - for (Entry entry : libraryCastableCardTracker.entrySet()) { - if (chosenCard.equals(entry.getValue())) { - Card card = game.getCard(entry.getKey()); - if (card != null) { - // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) - player.cast(card.getSpellAbility(), game, false, null); - } - chooseCard.clearChoice(); - libraryCastableCardTracker.clear(); - alreadyChosenUse = true; - continue searchForCards; - } - } - continue; - } - break; - } - return true; - } - } + + Card card = game.getCard(targetCard.getFirstTarget()); + if (card == null) { + break; } - break; - } while (alreadyChosenUse); - return alreadyChosenUse; + + // AI NOTICE: if you want AI implement here then remove selected card from castable after each + // choice (otherwise you catch infinite freeze on uncastable use case) + + // casting selected card + // TODO: fix costs (why is Panglacial Wurm automatically accepting payment?) + targetPlayer.cast(targetPlayer.chooseAbilityForCast(card, game, false), game, false, null); + castableCards.remove(card.getId()); + casted = true; + } + return casted; } @Override diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index bae18c8d37..2298d1c660 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -10,6 +10,8 @@ import mage.abilities.SpellAbility; import mage.abilities.costs.VariableCost; import mage.abilities.costs.mana.*; import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.common.asthought.CanPlayCardControllerEffect; +import mage.abilities.effects.common.asthought.YouMaySpendManaAsAnyColorToCastTargetEffect; import mage.abilities.hint.Hint; import mage.abilities.hint.HintUtils; import mage.cards.Card; @@ -28,6 +30,7 @@ import mage.game.permanent.token.Token; import mage.game.stack.Spell; import mage.players.Player; import mage.target.Target; +import mage.target.targetpointer.FixedTarget; import mage.util.functions.CopyTokenFunction; import org.apache.log4j.Logger; @@ -955,4 +958,56 @@ public final class CardUtil { } return RULES_ERROR_INFO; } + + /** + * Take control under another player, use it in inner effects like Word of Commands. Don't forget to end it in same code. + * + * @param game + * @param controller + * @param targetPlayer + * @param givePauseForResponse if you want to give controller time to watch opponent's hand (if you remove control effect in the end of code) + */ + public static void takeControlUnderPlayerStart(Game game, Player controller, Player targetPlayer, boolean givePauseForResponse) { + controller.controlPlayersTurn(game, targetPlayer.getId()); + if (givePauseForResponse) { + while (controller.canRespond()) { + if (controller.chooseUse(Outcome.Benefit, "You got control of " + targetPlayer.getLogName() + + ". Use switch hands button to view opponent's hand.", null, + "Continue", "Wait", null, game)) { + break; + } + } + } + } + + /** + * Return control under another player, use it in inner effects like Word of Commands. + * + * @param game + * @param controller + * @param targetPlayer + */ + public static void takeControlUnderPlayerEnd(Game game, Player controller, Player targetPlayer) { + targetPlayer.setGameUnderYourControl(true, false); + if (!targetPlayer.getTurnControlledBy().equals(controller.getId())) { + controller.getPlayersUnderYourControl().remove(targetPlayer.getId()); + } + } + + /** + * Add effects to game that allows to play/cast card from current zone and spend mana as any type for it. + * Effects will be discarded/ignored on any card movements or blinks (after ZCC change) + * + * @param game + * @param card + * @param duration + */ + public static void makeCardPlayableAndSpendManaAsAnyColor(Game game, Ability source, Card card, Duration duration) { + // Effect can be used for cards in zones and permanents on battlefield + // So there is a workaround to get real ZCC (PermanentCard's ZCC is static, but it must be from Card's ZCC) + // Example: Hostage Taker + int zcc = game.getState().getZoneChangeCounter(card.getId()); + game.addEffect(new CanPlayCardControllerEffect(game, card.getId(), zcc, duration), source); + game.addEffect(new YouMaySpendManaAsAnyColorToCastTargetEffect(duration).setTargetPointer(new FixedTarget(card.getId(), zcc)), source); + } }