mirror of
https://github.com/correl/mage.git
synced 2024-11-15 03:00:16 +00:00
AsThough effects improves and fixes:
* AsThough: added documentation about code usage and restrictions; * AsThough: added additional checks for correct usage; * AsThough: simplified some code; * PlayFromNotOwnHandZoneTargetEffect - added permanents support as targets; * Release to the Wind - fixed that it can't cast exiled cards (#7415, #7416); * Test framework: fixed that checkExileCount checking card's owner; * GUI: fixed typo in Trample card icons;
This commit is contained in:
parent
b8a95765fc
commit
2d96d36ec8
28 changed files with 375 additions and 217 deletions
|
@ -5,14 +5,15 @@ import mage.abilities.Ability;
|
|||
import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
import mage.abilities.keyword.NinjutsuAbility;
|
||||
import mage.cards.*;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
|
@ -70,6 +71,6 @@ class FallenShinobiEffect extends OneShotEffect {
|
|||
return false;
|
||||
}
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, player.getLibrary().getTopCards(game, 2),
|
||||
TargetController.YOU, Duration.EndOfTurn, true);
|
||||
TargetController.YOU, Duration.EndOfTurn, true, false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package mage.cards.f;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.BeginningOfEndStepTriggeredAbility;
|
||||
|
@ -14,18 +12,16 @@ import mage.abilities.effects.OneShotEffect;
|
|||
import mage.cards.Card;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.AsThoughEffectType;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.TargetController;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.target.targetpointer.FixedTarget;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public final class FuriousRise extends CardImpl {
|
||||
|
@ -77,7 +73,7 @@ class FuriousRiseEffect extends OneShotEffect {
|
|||
controller.moveCardsToExile(cardToExile, source, game, true, exileId, mageObject.getIdName() + " (" + source.getSourceObjectZoneChangeCounter() + ")");
|
||||
Card cardToPlay = game.getCard(cardToExile.getId());
|
||||
|
||||
endPreviousEffect(game, source);
|
||||
endPreviousEffect(game, source); // workaround for Furious Rise
|
||||
|
||||
ContinuousEffect effect = new FuriousRisePlayEffect();
|
||||
effect.setTargetPointer(new FixedTarget(cardToPlay, game));
|
||||
|
@ -93,7 +89,7 @@ class FuriousRiseEffect extends OneShotEffect {
|
|||
for (Ability ability : game.getContinuousEffects().getAsThoughEffectsAbility(effect)) {
|
||||
if (ability.getSourceId().equals(source.getSourceId())
|
||||
&& source.getSourceObjectZoneChangeCounter() == ability.getSourceObjectZoneChangeCounter()) {
|
||||
((FuriousRisePlayEffect) effect).discard();
|
||||
effect.discard();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package mage.cards.g;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import mage.MageInt;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
|
||||
|
@ -19,6 +17,9 @@ import mage.game.Game;
|
|||
import mage.players.Player;
|
||||
import mage.target.common.TargetCardInLibrary;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
|
@ -74,7 +75,7 @@ class GolosTirelessPilgrimEffect extends OneShotEffect {
|
|||
}
|
||||
Set<Card> cards = player.getLibrary().getTopCards(game, 3);
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards,
|
||||
TargetController.YOU, Duration.EndOfTurn, true);
|
||||
TargetController.YOU, Duration.EndOfTurn, true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -3,6 +3,7 @@ package mage.cards.i;
|
|||
import mage.abilities.Ability;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
import mage.abilities.keyword.FlashbackAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.CardImpl;
|
||||
|
@ -14,7 +15,6 @@ import mage.players.Player;
|
|||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
|
@ -69,6 +69,6 @@ class IgniteTheFutureEffect extends OneShotEffect {
|
|||
}
|
||||
Set<Card> cards = controller.getLibrary().getTopCards(game, 3);
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards,
|
||||
TargetController.YOU, Duration.UntilEndOfYourNextTurn, Zone.GRAVEYARD.equals(spell.getFromZone()));
|
||||
TargetController.YOU, Duration.UntilEndOfYourNextTurn, Zone.GRAVEYARD.equals(spell.getFromZone()), false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import mage.abilities.Ability;
|
|||
import mage.abilities.common.TurnedFaceUpSourceTriggeredAbility;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
import mage.abilities.keyword.MorphAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.CardImpl;
|
||||
|
@ -16,7 +17,6 @@ import mage.game.stack.StackObject;
|
|||
import mage.target.TargetSpell;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
|
||||
/**
|
||||
* @author emerald000
|
||||
|
@ -77,7 +77,8 @@ class KheruSpellsnatcherEffect extends OneShotEffect {
|
|||
if (!stackObject.isCopy()) {
|
||||
MageObject card = game.getObject(stackObject.getSourceId());
|
||||
if (card instanceof Card) {
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, (Card)card, TargetController.YOU, Duration.Custom, true);
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, (Card) card,
|
||||
TargetController.YOU, Duration.Custom, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package mage.cards.m;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import mage.MageInt;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
|
@ -14,18 +12,15 @@ import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffec
|
|||
import mage.cards.Card;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.TargetController;
|
||||
import mage.constants.Zone;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.watchers.common.CastSpellLastTurnWatcher;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author TheElk801
|
||||
*/
|
||||
public final class MagusOfTheMind extends CardImpl {
|
||||
|
@ -84,7 +79,8 @@ class MagusOfTheMindEffect extends OneShotEffect {
|
|||
controller.shuffleLibrary(source, game);
|
||||
if (controller.getLibrary().hasCards()) {
|
||||
Set<Card> cards = controller.getLibrary().getTopCards(game, stormCount);
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards, TargetController.YOU, Duration.EndOfTurn, true);
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, cards,
|
||||
TargetController.YOU, Duration.EndOfTurn, true, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -2,18 +2,18 @@ package mage.cards.m;
|
|||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
import mage.abilities.keyword.StormAbility;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.TargetController;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
import mage.constants.TargetController;
|
||||
|
||||
/**
|
||||
* @author emerald000
|
||||
|
@ -62,7 +62,7 @@ class MindsDesireEffect extends OneShotEffect {
|
|||
if (controller != null) {
|
||||
controller.shuffleLibrary(source, game);
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, controller.getLibrary().getFromTop(game),
|
||||
TargetController.YOU, Duration.EndOfTurn, true);
|
||||
TargetController.YOU, Duration.EndOfTurn, true, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package mage.cards.o;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.SimpleActivatedAbility;
|
||||
import mage.abilities.condition.common.SourceHasCounterCondition;
|
||||
|
@ -13,17 +12,14 @@ import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffec
|
|||
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.TargetController;
|
||||
import mage.constants.Zone;
|
||||
import mage.constants.*;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author fireshoes
|
||||
*/
|
||||
public final class OraclesVault extends CardImpl {
|
||||
|
@ -46,7 +42,7 @@ public final class OraclesVault extends CardImpl {
|
|||
this.addAbility(new ConditionalActivatedAbility(Zone.BATTLEFIELD,
|
||||
new OraclesVaultFreeEffect(), new TapSourceCost(), new SourceHasCounterCondition(CounterType.BRICK, 3, Integer.MAX_VALUE),
|
||||
"{T}: Exile the top card of your library. Until end of turn, you may play that card without paying its mana cost. "
|
||||
+ "Activate this ability only if there are three or more brick counters on {this}"));
|
||||
+ "Activate this ability only if there are three or more brick counters on {this}"));
|
||||
}
|
||||
|
||||
public OraclesVault(final OraclesVault card) {
|
||||
|
@ -79,7 +75,7 @@ class OraclesVaultEffect extends OneShotEffect {
|
|||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller != null) {
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, controller.getLibrary().getFromTop(game),
|
||||
TargetController.YOU, Duration.EndOfTurn, false);
|
||||
TargetController.YOU, Duration.EndOfTurn, false, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -105,7 +101,7 @@ class OraclesVaultFreeEffect extends OneShotEffect {
|
|||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller != null) {
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, controller.getLibrary().getFromTop(game),
|
||||
TargetController.YOU, Duration.EndOfTurn, true);
|
||||
TargetController.YOU, Duration.EndOfTurn, true, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
|
||||
package mage.cards.r;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
|
@ -16,8 +14,9 @@ import mage.game.permanent.Permanent;
|
|||
import mage.players.Player;
|
||||
import mage.target.common.TargetNonlandPermanent;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public final class ReleaseToTheWind extends CardImpl {
|
||||
|
@ -28,7 +27,6 @@ public final class ReleaseToTheWind extends CardImpl {
|
|||
// Exile target nonland permanent. For as long as that card remains exiled, its owner may cast it without paying its mana cost.
|
||||
getSpellAbility().addEffect(new ReleaseToTheWindEffect());
|
||||
getSpellAbility().addTarget(new TargetNonlandPermanent());
|
||||
|
||||
}
|
||||
|
||||
public ReleaseToTheWind(final ReleaseToTheWind card) {
|
||||
|
@ -63,7 +61,8 @@ class ReleaseToTheWindEffect extends OneShotEffect {
|
|||
if (controller != null) {
|
||||
Permanent targetPermanent = game.getPermanent(getTargetPointer().getFirst(game, source));
|
||||
if (targetPermanent != null) {
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, targetPermanent, TargetController.OWNER, Duration.Custom, true);
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, targetPermanent,
|
||||
TargetController.OWNER, Duration.Custom, true, true);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -81,7 +81,7 @@ class CantBeBlockedUnlessAllEffect extends RestrictionEffect {
|
|||
// check if all creatures of defender are able to block this permanent
|
||||
// permanent.canBlock() can't be used because causing recursive call
|
||||
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, blocker.getControllerId(), game)) {
|
||||
if (permanent.isTapped() && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, source, blocker.getControllerId(), game)) {
|
||||
if (permanent.isTapped() && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, blocker.getControllerId(), game)) {
|
||||
return false;
|
||||
}
|
||||
// check blocker restrictions
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package mage.cards.u;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import mage.MageInt;
|
||||
import mage.MageObject;
|
||||
import mage.Mana;
|
||||
|
@ -13,12 +11,14 @@ import mage.abilities.costs.mana.GenericManaCost;
|
|||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.CreateTokenEffect;
|
||||
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
|
||||
import mage.abilities.effects.mana.BasicManaEffect;
|
||||
import mage.abilities.hint.common.ArtifactYouControlHint;
|
||||
import mage.abilities.mana.SimpleManaAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.*;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.filter.common.FilterControlledArtifactPermanent;
|
||||
import mage.filter.common.FilterControlledPermanent;
|
||||
import mage.filter.predicate.Predicates;
|
||||
|
@ -28,9 +28,9 @@ import mage.game.permanent.token.KarnConstructToken;
|
|||
import mage.players.Player;
|
||||
import mage.target.common.TargetControlledPermanent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import mage.abilities.effects.mana.BasicManaEffect;
|
||||
import mage.filter.FilterPermanent;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
|
@ -100,7 +100,7 @@ class UrzaLordHighArtificerEffect extends OneShotEffect {
|
|||
controller.shuffleLibrary(source, game);
|
||||
Card card = controller.getLibrary().getFromTop(game);
|
||||
return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, card,
|
||||
TargetController.YOU, Duration.EndOfTurn, true);
|
||||
TargetController.YOU, Duration.EndOfTurn, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -437,12 +437,18 @@ public class PlayFromNonHandZoneTest extends CardTestPlayerBaseWithAIHelps {
|
|||
|
||||
skipInitShuffling();
|
||||
|
||||
// attack and exile 2 cards from library
|
||||
attack(2, playerB, "Fallen Shinobi");
|
||||
checkExileCount("after exile a", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Angelic Purge", 1);
|
||||
checkExileCount("after exile b", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Curious Pair", 1);
|
||||
|
||||
// cast purge from exile
|
||||
checkPlayableAbility("after exile - can play purge", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Cast Angelic Purge", true);
|
||||
castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Angelic Purge");
|
||||
setChoice(playerB, "Silvercoat Lion"); // Sacrifice for Purge
|
||||
addTarget(playerB, "Amulet of Kroog"); // Exile with Purge
|
||||
setChoice(playerB, "Silvercoat Lion"); // sacrifice cost
|
||||
addTarget(playerB, "Amulet of Kroog"); // exile target
|
||||
|
||||
// cast adventure spell from exile
|
||||
castSpell(2, PhaseStep.POSTCOMBAT_MAIN, playerB, "Treats to Share");
|
||||
|
||||
setStopAt(2, PhaseStep.END_TURN);
|
||||
|
|
|
@ -238,8 +238,7 @@ public class TakeControlWhileSearchingLibraryTest extends CardTestPlayerBase {
|
|||
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);
|
||||
checkExileCount("after exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1);
|
||||
|
||||
// B can cast green bear for red mana
|
||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Balduvian Bears");
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
package org.mage.test.cards.single.rix;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class ReleaseToTheWindTest extends CardTestPlayerBase {
|
||||
|
||||
@Test
|
||||
public void test_Exile_PermanentCard() {
|
||||
// Exile target nonland permanent. For as long as that card remains exiled, its owner may cast it without paying its mana cost.
|
||||
addCard(Zone.HAND, playerA, "Release to the Wind"); // {2}{U}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
|
||||
//
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1);
|
||||
|
||||
// exile
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Release to the Wind", "Grizzly Bears");
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||
checkExileCount("after exile", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Grizzly Bears", 1);
|
||||
checkPlayableAbility("after exile - non owner can't play 1", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", false);
|
||||
|
||||
// owner can play
|
||||
checkPlayableAbility("after exile - non owner can't play 2", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", false);
|
||||
checkPlayableAbility("after exile - owner can play", 2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cast Grizzly Bears", true);
|
||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Grizzly Bears");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(2, PhaseStep.END_TURN);
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
|
||||
assertPermanentCount(playerB, "Grizzly Bears", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Exile_ModalDoubleFacesCard() {
|
||||
// Exile target nonland permanent. For as long as that card remains exiled, its owner may cast it without paying its mana cost.
|
||||
addCard(Zone.HAND, playerA, "Release to the Wind"); // {2}{U}
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
|
||||
//
|
||||
// Akoum Warrior {5}{R} - creature
|
||||
// Akoum Teeth - land
|
||||
addCard(Zone.HAND, playerA, "Akoum Warrior");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
|
||||
|
||||
// prepare mdf
|
||||
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 6);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior");
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA);
|
||||
checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior", 1);
|
||||
|
||||
// exile mdf creature
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Release to the Wind", "Akoum Warrior");
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||
checkExileCount("after exile", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior", 1);
|
||||
|
||||
// you can cast mdf, but can't play a land
|
||||
checkPlayableAbility("after exile - can play mdf creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Akoum Warrior", true);
|
||||
checkPlayableAbility("after exile - can't play mdf land", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Akoum Teeth", false);
|
||||
|
||||
// cast mdf again for free
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Akoum Warrior");
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
|
||||
assertPermanentCount(playerA, "Akoum Warrior", 1);
|
||||
}
|
||||
}
|
|
@ -820,7 +820,7 @@ public class TestPlayer implements Player {
|
|||
|
||||
// check exile count: card name, count
|
||||
if (params[0].equals(CHECK_COMMAND_EXILE_COUNT) && params.length == 3) {
|
||||
assertExileCount(action, game, computerPlayer, params[1], Integer.parseInt(params[2]));
|
||||
assertExileCount(action, game, params[1], Integer.parseInt(params[2]));
|
||||
actions.remove(action);
|
||||
wasProccessed = true;
|
||||
}
|
||||
|
@ -1355,15 +1355,20 @@ public class TestPlayer implements Player {
|
|||
Assert.assertEquals(action.getActionName() + " - permanent " + permanentName + " must have " + count + " " + counterType.toString(), count, foundCount);
|
||||
}
|
||||
|
||||
private void assertExileCount(PlayerAction action, Game game, Player player, String permanentName, int count) {
|
||||
private void assertExileCount(PlayerAction action, Game game, String permanentName, int count) {
|
||||
int foundCount = 0;
|
||||
for (Card card : game.getExile().getAllCards(game)) {
|
||||
if (hasObjectTargetNameOrAlias(card, permanentName) && card.isOwnedBy(player.getId())) {
|
||||
if (hasObjectTargetNameOrAlias(card, permanentName)) {
|
||||
foundCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.assertEquals(action.getActionName() + " - card " + permanentName + " must exists in exile zone with " + count + " instances", count, foundCount);
|
||||
if (foundCount != count) {
|
||||
printStart("Exile cards");
|
||||
printCards(game.getExile().getAllCards(game));
|
||||
printEnd();
|
||||
Assert.fail(action.getActionName() + " - exile zone must have " + count + " cards with name " + permanentName + ", but found " + foundCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertGraveyardCount(PlayerAction action, Game game, Player player, String permanentName, int count) {
|
||||
|
@ -1374,7 +1379,12 @@ public class TestPlayer implements Player {
|
|||
}
|
||||
}
|
||||
|
||||
Assert.assertEquals(action.getActionName() + " - card " + permanentName + " must exists in graveyard zone with " + count + " instances", count, foundCount);
|
||||
if (foundCount != count) {
|
||||
printStart("Graveyard of " + player.getName());
|
||||
printCards(player.getGraveyard().getCards(game));
|
||||
printEnd();
|
||||
Assert.fail(action.getActionName() + " - graveyard zone must have " + count + " cards with name " + permanentName + ", but found " + foundCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertLibraryCount(PlayerAction action, Game game, Player player, String permanentName, int count) {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package mage.abilities;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.ApprovingObject;
|
||||
import mage.constants.AbilityType;
|
||||
import mage.constants.AsThoughEffectType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class PlayLandAbility extends ActivatedAbilityImpl {
|
||||
|
@ -25,7 +25,7 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
|
|||
|
||||
@Override
|
||||
public ActivationStatus canActivate(UUID playerId, Game game) {
|
||||
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, null, playerId, game);
|
||||
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
|
||||
if (!controlsAbility(playerId, game) && null == approvingObject) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package mage.abilities;
|
||||
|
||||
import mage.ApprovingObject;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.VariableCost;
|
||||
|
@ -15,7 +16,6 @@ import mage.players.Player;
|
|||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import mage.ApprovingObject;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
|
@ -81,14 +81,17 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
|| spellAbilityType == SpellAbilityType.SPLIT_AFTERMATH) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
// fix for Gitaxian Probe and casting opponent's spells
|
||||
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, null, playerId, game);
|
||||
|
||||
// play from not own hand
|
||||
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
|
||||
if (approvingObject == null) {
|
||||
Card card = game.getCard(sourceId);
|
||||
if (!(card != null && card.isOwnedBy(playerId))) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
}
|
||||
|
||||
// play restrict
|
||||
// Check if rule modifying events prevent to cast the spell in check playable mode
|
||||
if (game.inCheckPlayableState()) {
|
||||
if (game.getContinuousEffects().preventedByRuleModification(
|
||||
|
@ -96,6 +99,8 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
return ActivationStatus.getFalse();
|
||||
}
|
||||
}
|
||||
|
||||
// no mana restrict
|
||||
// Alternate spell abilities (Flashback, Overload) can't be cast with no mana to pay option
|
||||
if (getSpellAbilityType() == SpellAbilityType.BASE_ALTERNATE) {
|
||||
Player player = game.getPlayer(playerId);
|
||||
|
@ -104,6 +109,8 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
return ActivationStatus.getFalse();
|
||||
}
|
||||
}
|
||||
|
||||
// can pay all costs
|
||||
if (costs.canPay(this, this, playerId, game)) {
|
||||
if (getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) {
|
||||
SplitCard splitCard = (SplitCard) game.getCard(getSourceId());
|
||||
|
@ -116,7 +123,6 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
}
|
||||
}
|
||||
return ActivationStatus.getFalse();
|
||||
|
||||
} else {
|
||||
return new ActivationStatus(canChooseTarget(game), approvingObject);
|
||||
}
|
||||
|
|
|
@ -1,19 +1,39 @@
|
|||
|
||||
package mage.abilities.effects;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.AsThoughEffectType;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public interface AsThoughEffect extends ContinuousEffect {
|
||||
|
||||
/**
|
||||
* Apply to ONE affected ability from the object (sourceId)
|
||||
* <p>
|
||||
* Warning, if you don't need ability to check then ignore it (by default it calls full object check)
|
||||
*
|
||||
* @param sourceId
|
||||
* @param affectedAbility ability to check (example: check if spell ability can be cast from non hand)
|
||||
* @param source
|
||||
* @param game
|
||||
* @param playerId player to check
|
||||
* @return
|
||||
*/
|
||||
boolean applies(UUID sourceId, Ability affectedAbility, Ability source, Game game, UUID playerId);
|
||||
|
||||
/**
|
||||
* Apply to ANY ability from the object (sourceId)
|
||||
*
|
||||
* @param sourceId object to check
|
||||
* @param source
|
||||
* @param affectedControllerId player to check (example: you can activate opponent's card or ability)
|
||||
* @param game
|
||||
* @return
|
||||
*/
|
||||
boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game);
|
||||
|
||||
AsThoughEffectType getAsThoughEffectType();
|
||||
|
|
|
@ -38,12 +38,10 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
|
|||
|
||||
@Override
|
||||
public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) {
|
||||
// affectedControllerId = player to check
|
||||
if (getAsThoughEffectType().equals(AsThoughEffectType.LOOK_AT_FACE_DOWN)) {
|
||||
return applies(objectId, source, playerId, game);
|
||||
} else {
|
||||
return applies(objectId, source, playerId, game);
|
||||
}
|
||||
// affectedControllerId = player to check (example: you can activate ability from opponent's card)
|
||||
// by default it applies to full object
|
||||
// if you AsThough effect type needs affected ability then override that method
|
||||
return applies(objectId, source, playerId, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
package mage.abilities.effects;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
import mage.ApprovingObject;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
|
@ -32,6 +28,11 @@ import mage.util.CardUtil;
|
|||
import mage.util.trace.TraceInfo;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
|
@ -500,22 +501,43 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param objectId
|
||||
* @param objectId object to check
|
||||
* @param type
|
||||
* @param affectedAbility
|
||||
* @param affectedAbility null if check full object or ability if check only one ability from that object
|
||||
* @param controllerId
|
||||
* @param game
|
||||
* @return sourceId of the permitting effect if any exists otherwise returns
|
||||
* null
|
||||
* @return sourceId of the permitting effect if any exists otherwise returns null
|
||||
*/
|
||||
public ApprovingObject asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) {
|
||||
|
||||
// usage check: effect must apply for specific ability only, not to full object (example: PLAY_FROM_NOT_OWN_HAND_ZONE)
|
||||
if (type.needAffectedAbility() && affectedAbility == null) {
|
||||
throw new IllegalArgumentException("ERROR, you can't call asThough check to whole object, call it with affected ability instead: " + type);
|
||||
}
|
||||
|
||||
// usage check: effect must apply to full object, not specific ability (example: ATTACK_AS_HASTE)
|
||||
// P.S. In theory a same AsThough effect can be applied to object or to ability, so if you really, really
|
||||
// need it then disable that check or add extra param to AsThoughEffectType like needAffectedAbilityOrFullObject
|
||||
if (!type.needAffectedAbility() && affectedAbility != null) {
|
||||
throw new IllegalArgumentException("ERROR, you can't call AsThough check to affected ability, call it empty affected ability instead: " + type);
|
||||
}
|
||||
|
||||
List<AsThoughEffect> asThoughEffectsList = getApplicableAsThoughEffects(type, game);
|
||||
if (!asThoughEffectsList.isEmpty()) {
|
||||
MageObject objectToCheck;
|
||||
if (affectedAbility != null) {
|
||||
objectToCheck = affectedAbility.getSourceObject(game);
|
||||
} else {
|
||||
objectToCheck = game.getCard(objectId);
|
||||
}
|
||||
|
||||
UUID idToCheck;
|
||||
if (affectedAbility != null && affectedAbility.getSourceObject(game) instanceof SplitCardHalf) {
|
||||
idToCheck = ((SplitCardHalf) affectedAbility.getSourceObject(game)).getParentCard().getId();
|
||||
} else if (affectedAbility != null && affectedAbility.getSourceObject(game) instanceof ModalDoubleFacesCardHalf
|
||||
&& !type.needPlayCardAbility()) {
|
||||
if (objectToCheck instanceof SplitCardHalf) {
|
||||
idToCheck = ((SplitCardHalf) objectToCheck).getMainCard().getId();
|
||||
} else if (!type.needPlayCardAbility() && objectToCheck instanceof AdventureCardSpell) {
|
||||
// adventure spell uses alternative characteristics for spell/stack, all other cases must use main card
|
||||
idToCheck = ((AdventureCardSpell) objectToCheck).getMainCard().getId();
|
||||
} else if (!type.needPlayCardAbility() && objectToCheck instanceof ModalDoubleFacesCardHalf) {
|
||||
// each mdf side uses own characteristics to check for playing, all other cases must use main card
|
||||
// rules:
|
||||
// "If an effect allows you to play a land or cast a spell from among a group of cards,
|
||||
|
@ -523,26 +545,9 @@ public class ContinuousEffects implements Serializable {
|
|||
// of that effect. For example, if Sejiri Shelter / Sejiri Glacier is in your graveyard
|
||||
// and an effect allows you to play lands from your graveyard, you could play Sejiri Glacier.
|
||||
// That effect doesn't allow you to cast Sejiri Shelter."
|
||||
idToCheck = ((ModalDoubleFacesCardHalf) affectedAbility.getSourceObject(game)).getParentCard().getId();
|
||||
} else if (affectedAbility != null && affectedAbility.getSourceObject(game) instanceof AdventureCardSpell
|
||||
&& !type.needPlayCardAbility()) {
|
||||
// adventure spell uses alternative characteristics for spell/stack
|
||||
idToCheck = ((AdventureCardSpell) affectedAbility.getSourceObject(game)).getParentCard().getId();
|
||||
idToCheck = ((ModalDoubleFacesCardHalf) objectToCheck).getMainCard().getId();
|
||||
} else {
|
||||
Card card = game.getCard(objectId);
|
||||
if (card instanceof SplitCardHalf) {
|
||||
idToCheck = ((SplitCardHalf) card).getParentCard().getId();
|
||||
} else if (card instanceof ModalDoubleFacesCardHalf
|
||||
&& !type.needPlayCardAbility()) {
|
||||
// each mdf side uses own characteristics to check for playing, all other cases must use main card
|
||||
idToCheck = ((ModalDoubleFacesCardHalf) card).getParentCard().getId();
|
||||
} else if (card instanceof AdventureCardSpell
|
||||
&& !type.needPlayCardAbility()) {
|
||||
// adventure spell uses alternative characteristics for spell/stack
|
||||
idToCheck = ((AdventureCardSpell) card).getParentCard().getId();
|
||||
} else {
|
||||
idToCheck = objectId;
|
||||
}
|
||||
idToCheck = objectId;
|
||||
}
|
||||
|
||||
Set<ApprovingObject> possibleApprovingObjects = new HashSet<>();
|
||||
|
@ -550,7 +555,7 @@ public class ContinuousEffects implements Serializable {
|
|||
Set<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId());
|
||||
for (Ability ability : abilities) {
|
||||
if (affectedAbility == null) {
|
||||
// applies to own ability (one effect can be used in multiple abilities)
|
||||
// applies to full object (one effect can be used in multiple abilities)
|
||||
if (effect.applies(idToCheck, ability, controllerId, game)) {
|
||||
if (effect.isConsumable() && !game.inCheckPlayableState()) {
|
||||
possibleApprovingObjects.add(new ApprovingObject(ability, game));
|
||||
|
@ -559,7 +564,7 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// applies to affected ability
|
||||
// applies to one affected ability
|
||||
|
||||
// filter play abilities (no need to check it in every effect's code)
|
||||
if (type.needPlayCardAbility() && !affectedAbility.getAbilityType().isPlayCardAbility()) {
|
||||
|
@ -576,6 +581,7 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (possibleApprovingObjects.size() == 1) {
|
||||
return possibleApprovingObjects.iterator().next();
|
||||
} else if (possibleApprovingObjects.size() > 1) {
|
||||
|
@ -601,7 +607,6 @@ public class ContinuousEffects implements Serializable {
|
|||
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public ManaType asThoughMana(ManaType manaType, ManaPoolItem mana, UUID objectId, Ability affectedAbility, UUID controllerId, Game game) {
|
||||
|
@ -1391,16 +1396,17 @@ public class ContinuousEffects implements Serializable {
|
|||
return controllerFound;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Prints out a status of the currently existing continuous effects
|
||||
*
|
||||
* @param game
|
||||
*/
|
||||
public void traceContinuousEffects(Game game) {
|
||||
game.getContinuousEffects().getLayeredEffects(game);
|
||||
logger.info("-------------------------------------------------------------------------------------------------");
|
||||
int numberEffects = 0;
|
||||
for(ContinuousEffectsList list: allEffectsLists) {
|
||||
numberEffects += list.size();
|
||||
for (ContinuousEffectsList list : allEffectsLists) {
|
||||
numberEffects += list.size();
|
||||
}
|
||||
logger.info("Turn: " + game.getTurnNum() + " - currently existing continuous effects: " + numberEffects);
|
||||
logger.info("layeredEffects ...................: " + layeredEffects.size());
|
||||
|
@ -1416,8 +1422,8 @@ public class ContinuousEffects implements Serializable {
|
|||
for (Map.Entry<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> entry : asThoughEffectsMap.entrySet()) {
|
||||
logger.info("... " + entry.getKey().toString() + ": " + entry.getValue().size());
|
||||
}
|
||||
logger.info("applyCounters ....................: " + (applyCounters != null ? "exists":"null"));
|
||||
logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists":"null"));
|
||||
logger.info("applyCounters ....................: " + (applyCounters != null ? "exists" : "null"));
|
||||
logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists" : "null"));
|
||||
Map<String, TraceInfo> orderedEffects = new TreeMap<>();
|
||||
traceAddContinuousEffects(orderedEffects, layeredEffects, game, "layeredEffects................");
|
||||
traceAddContinuousEffects(orderedEffects, continuousRuleModifyingEffects, game, "continuousRuleModifyingEffects");
|
||||
|
@ -1441,11 +1447,12 @@ public class ContinuousEffects implements Serializable {
|
|||
+ " " + entry.getValue().getSourceName()
|
||||
+ " " + entry.getValue().getDuration().name()
|
||||
+ " " + entry.getValue().getRule()
|
||||
+ " (Order: "+entry.getValue().getOrder() +")"
|
||||
+ " (Order: " + entry.getValue().getOrder() + ")"
|
||||
);
|
||||
}
|
||||
logger.info("---- End trace Continuous effects --------------------------------------------------------------------------");
|
||||
}
|
||||
|
||||
public static void traceAddContinuousEffects(Map orderedEffects, ContinuousEffectsList<?> cel, Game game, String listName) {
|
||||
for (ContinuousEffect effect : cel) {
|
||||
Set<Ability> abilities = cel.getAbility(effect.getId());
|
||||
|
@ -1459,8 +1466,8 @@ public class ContinuousEffects implements Serializable {
|
|||
traceInfo.setPlayerName("Mage Singleton");
|
||||
traceInfo.setSourceName("Mage Singleton");
|
||||
} else {
|
||||
traceInfo.setPlayerName(controller == null ? "no controller": controller.getName());
|
||||
traceInfo.setSourceName(source == null ? "no source": source.getIdName());
|
||||
traceInfo.setPlayerName(controller == null ? "no controller" : controller.getName());
|
||||
traceInfo.setSourceName(source == null ? "no source" : source.getIdName());
|
||||
}
|
||||
traceInfo.setRule(ability.getRule());
|
||||
traceInfo.setAbilityId(ability.getId());
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
package mage.abilities.effects.common.asthought;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.PlayLandAbility;
|
||||
import mage.abilities.effects.AsThoughEffectImpl;
|
||||
import mage.abilities.effects.ContinuousEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.AsThoughEffectType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.TargetController;
|
||||
import mage.constants.Zone;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.target.targetpointer.FixedTargets;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
||||
|
@ -28,6 +26,7 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
|||
private final Zone fromZone;
|
||||
private final TargetController allowedCaster;
|
||||
private final boolean withoutMana;
|
||||
private final boolean onlyCastAllowed; // can cast spells, but can't play lands
|
||||
|
||||
public PlayFromNotOwnHandZoneTargetEffect() {
|
||||
this(Duration.EndOfTurn);
|
||||
|
@ -46,10 +45,15 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
|||
}
|
||||
|
||||
public PlayFromNotOwnHandZoneTargetEffect(Zone fromZone, TargetController allowedCaster, Duration duration, boolean withoutMana) {
|
||||
this(fromZone, allowedCaster, duration, withoutMana, false);
|
||||
}
|
||||
|
||||
public PlayFromNotOwnHandZoneTargetEffect(Zone fromZone, TargetController allowedCaster, Duration duration, boolean withoutMana, boolean onlyCastAllowed) {
|
||||
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, duration, withoutMana ? Outcome.PlayForFree : Outcome.PutCardInPlay);
|
||||
this.fromZone = fromZone;
|
||||
this.allowedCaster = allowedCaster;
|
||||
this.withoutMana = withoutMana;
|
||||
this.onlyCastAllowed = onlyCastAllowed;
|
||||
}
|
||||
|
||||
public PlayFromNotOwnHandZoneTargetEffect(final PlayFromNotOwnHandZoneTargetEffect effect) {
|
||||
|
@ -57,6 +61,7 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
|||
this.fromZone = effect.fromZone;
|
||||
this.allowedCaster = effect.allowedCaster;
|
||||
this.withoutMana = effect.withoutMana;
|
||||
this.onlyCastAllowed = effect.onlyCastAllowed;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -76,12 +81,25 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
|||
|
||||
@Override
|
||||
public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) {
|
||||
if (affectedAbility == null) {
|
||||
// ContinuousEffects.asThough already checks affectedAbility, so that error must never be called here
|
||||
// PLAY_FROM_NOT_OWN_HAND_ZONE must applies to affectedAbility only
|
||||
throw new IllegalArgumentException("ERROR, can't call applies method on empty affectedAbility");
|
||||
}
|
||||
|
||||
// invalid targets
|
||||
List<UUID> targets = getTargetPointer().getTargets(game, source);
|
||||
if (targets.isEmpty()) {
|
||||
this.discard();
|
||||
return false;
|
||||
}
|
||||
|
||||
// invalid zone
|
||||
if (!game.getState().getZone(objectId).match(fromZone)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// invalid caster
|
||||
switch (allowedCaster) {
|
||||
case YOU:
|
||||
if (playerId != source.getControllerId()) {
|
||||
|
@ -101,29 +119,36 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
|||
case ANY:
|
||||
break;
|
||||
}
|
||||
|
||||
// targets goes to main card all the time
|
||||
UUID objectIdToCast = CardUtil.getMainCardId(game, objectId);
|
||||
if (targets.contains(objectIdToCast)
|
||||
&& playerId.equals(source.getControllerId())
|
||||
&& game.getState().getZone(objectId).match(fromZone)) {
|
||||
if (withoutMana) {
|
||||
if (affectedAbility != null) {
|
||||
objectIdToCast = affectedAbility.getSourceId();
|
||||
}
|
||||
return allowCardToPlayWithoutMana(objectIdToCast, source, playerId, game);
|
||||
}
|
||||
return true;
|
||||
if (!targets.contains(objectIdToCast)) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
// if can't play lands
|
||||
if (!affectedAbility.getAbilityType().isPlayCardAbility()
|
||||
|| onlyCastAllowed && affectedAbility instanceof PlayLandAbility) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// OK, allow to play
|
||||
if (withoutMana) {
|
||||
allowCardToPlayWithoutMana(objectId, source, playerId, game);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean exileAndPlayFromExile(Game game, Ability source, Card card, TargetController allowedCaster, Duration duration, boolean withoutMana) {
|
||||
public static boolean exileAndPlayFromExile(Game game, Ability source, Card card, TargetController allowedCaster,
|
||||
Duration duration, boolean withoutMana, boolean onlyCastAllowed) {
|
||||
if (card == null) {
|
||||
return true;
|
||||
}
|
||||
Set<Card> cards = new HashSet<>();
|
||||
cards.add(card);
|
||||
return exileAndPlayFromExile(game, source, cards, allowedCaster, duration, withoutMana);
|
||||
return exileAndPlayFromExile(game, source, cards, allowedCaster, duration, withoutMana, onlyCastAllowed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exiles the cards and let the allowed player play them from exile for the given duration
|
||||
*
|
||||
|
@ -133,9 +158,11 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
|||
* @param allowedCaster
|
||||
* @param duration
|
||||
* @param withoutMana
|
||||
* @param onlyCastAllowed true for rule "cast that card" and false for rule "play that card"
|
||||
* @return
|
||||
*/
|
||||
public static boolean exileAndPlayFromExile(Game game, Ability source, Set<Card> cards, TargetController allowedCaster, Duration duration, boolean withoutMana) {
|
||||
public static boolean exileAndPlayFromExile(Game game, Ability source, Set<Card> cards, TargetController allowedCaster,
|
||||
Duration duration, boolean withoutMana, boolean onlyCastAllowed) {
|
||||
if (cards == null || cards.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -150,7 +177,7 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
|||
+ "-" + sourceObject.getIdName(), game
|
||||
);
|
||||
String exileName = sourceObject.getIdName() + " free play"
|
||||
+ (Duration.EndOfTurn.equals(duration) ? " on turn " + game.getState().getTurnNum():"")
|
||||
+ (Duration.EndOfTurn.equals(duration) ? " on turn " + game.getState().getTurnNum() : "")
|
||||
+ " for " + controller.getName();
|
||||
if (Duration.EndOfTurn.equals(duration)) {
|
||||
game.getExile().createZone(exileId, exileName).setCleanupOnEndTurn(true);
|
||||
|
@ -158,8 +185,16 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
|
|||
if (!controller.moveCardsToExile(cards, source, game, true, exileId, exileName)) {
|
||||
return false;
|
||||
}
|
||||
ContinuousEffect effect = new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, allowedCaster, duration, withoutMana);
|
||||
effect.setTargetPointer(new FixedTargets(cards, game));
|
||||
|
||||
// get real cards (if it was called on permanent instead card, example: Release to the Wind)
|
||||
Set<Card> cardsToPlay = cards
|
||||
.stream()
|
||||
.map(Card::getMainCard)
|
||||
.filter(card -> Zone.EXILED.equals(game.getState().getZone(card.getId())))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
ContinuousEffect effect = new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, allowedCaster, duration, withoutMana, onlyCastAllowed);
|
||||
effect.setTargetPointer(new FixedTargets(cardsToPlay, game));
|
||||
game.addEffect(effect, source);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ public enum TrampleAbilityIcon implements CardIcon {
|
|||
|
||||
@Override
|
||||
public String getHint() {
|
||||
return "Trumple ability";
|
||||
return "Trample ability";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -68,7 +68,7 @@ class FlyingEffect extends RestrictionEffect implements MageSingleton {
|
|||
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
|
||||
return blocker.getAbilities().containsKey(FlyingAbility.getInstance().getId())
|
||||
|| blocker.getAbilities().containsKey(ReachAbility.getInstance().getId())
|
||||
|| (null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, source, blocker.getControllerId(), game)
|
||||
|| (null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, null, blocker.getControllerId(), game)
|
||||
&& attacker.hasSubtype(SubType.DRAGON, game));
|
||||
}
|
||||
|
||||
|
|
|
@ -65,18 +65,18 @@ class LandwalkEffect extends RestrictionEffect {
|
|||
@Override
|
||||
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
|
||||
if (game.getBattlefield().contains(filter, source.getSourceId(), blocker.getControllerId(), game, 1)
|
||||
&& null == game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, source, blocker.getControllerId(), game)) {
|
||||
&& null == game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, null, blocker.getControllerId(), game)) {
|
||||
switch (filter.getMessage()) {
|
||||
case "plains":
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, source, blocker.getControllerId(), game);
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, null, blocker.getControllerId(), game);
|
||||
case "island":
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, source, blocker.getControllerId(), game);
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, null, blocker.getControllerId(), game);
|
||||
case "swamp":
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, source, blocker.getControllerId(), game);
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, null, blocker.getControllerId(), game);
|
||||
case "mountain":
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, source, blocker.getControllerId(), game);
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, null, blocker.getControllerId(), game);
|
||||
case "forest":
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, source, blocker.getControllerId(), game);
|
||||
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, null, blocker.getControllerId(), game);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ class ShadowEffect extends RestrictionEffect implements MageSingleton {
|
|||
@Override
|
||||
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
|
||||
return blocker.getAbilities().containsKey(ShadowAbility.getInstance().getId())
|
||||
|| null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, source, blocker.getControllerId(), game);
|
||||
|| null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, null, blocker.getControllerId(), game);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
package mage.constants;
|
||||
|
||||
/**
|
||||
* Allows to change game rules and logic by special conditionals from ContinuousEffects (example: play card from non hand)
|
||||
* <p>
|
||||
* Can be applied to the game, object or specific ability (affectedAbility param in ContinuousEffects.asThough)
|
||||
* <p>
|
||||
* There are two usage styles possible:
|
||||
* - object specific rules and conditionals (use ContinuousEffects.asThough)
|
||||
* - global rules (use game.getState().getContinuousEffects().getApplicableAsThoughEffects)
|
||||
* Each AsThough type supports only one usage style
|
||||
*
|
||||
* @author North
|
||||
*/
|
||||
public enum AsThoughEffectType {
|
||||
ATTACK,
|
||||
ATTACK_AS_HASTE,
|
||||
ACTIVATE_HASTE,
|
||||
ACTIVATE_HASTE(true, false),
|
||||
//
|
||||
BLOCK_TAPPED,
|
||||
BLOCK_SHADOW,
|
||||
BLOCK_DRAGON,
|
||||
|
@ -16,48 +26,55 @@ public enum AsThoughEffectType {
|
|||
BLOCK_SWAMPWALK,
|
||||
BLOCK_MOUNTAINWALK,
|
||||
BLOCK_FORESTWALK,
|
||||
//
|
||||
DAMAGE_NOT_BLOCKED,
|
||||
BE_BLOCKED,
|
||||
|
||||
//
|
||||
// PLAY_FROM_NOT_OWN_HAND_ZONE + CAST_AS_INSTANT:
|
||||
// 1. Do not use dialogs in "applies" method for that type of effect (it calls multiple times and will freeze the game)
|
||||
// 2. All effects in "applies" must checks affectedControllerId.equals(source.getControllerId()) (if not then all players will be able to play it)
|
||||
// 3. Target points to mainCard, but card's characteristics from objectId (split, adventure)
|
||||
// 2. All effects in "applies" must checks affectedControllerId/playerId.equals(source.getControllerId()) (if not then all players will be able to play it)
|
||||
// 3. Target must points to mainCard, but checking goes for every card's parts and characteristics from objectId (split, adventure)
|
||||
// 4. You must implement/override an applies method with "Ability affectedAbility" (e.g. check multiple play/cast abilities from all card's parts)
|
||||
// TODO: search all PLAY_FROM_NOT_OWN_HAND_ZONE and CAST_AS_INSTANT effects and add support of mainCard and objectId
|
||||
PLAY_FROM_NOT_OWN_HAND_ZONE(true),
|
||||
CAST_AS_INSTANT(true),
|
||||
|
||||
ACTIVATE_AS_INSTANT,
|
||||
DAMAGE,
|
||||
PLAY_FROM_NOT_OWN_HAND_ZONE(true, true),
|
||||
CAST_AS_INSTANT(true, true),
|
||||
//
|
||||
ACTIVATE_AS_INSTANT(true, false),
|
||||
//
|
||||
SHROUD,
|
||||
HEXPROOF,
|
||||
PAY_0_ECHO,
|
||||
//
|
||||
PAY_0_ECHO(true, false),
|
||||
LOOK_AT_FACE_DOWN,
|
||||
|
||||
//
|
||||
// SPEND_OTHER_MANA:
|
||||
// 1. It's uses for mana calcs at any zone, not stack only
|
||||
// 2. Compare zone change counter as "objectZCC <= targetZCC + 1"
|
||||
// 3. Compare zone with original (like exiled) and stack, not stack only
|
||||
// TODO: search all SPEND_ONLY_MANA effects and improve counters compare as SPEND_OTHER_MANA
|
||||
SPEND_OTHER_MANA,
|
||||
|
||||
SPEND_OTHER_MANA(true, false),
|
||||
//
|
||||
SPEND_ONLY_MANA,
|
||||
TARGET,
|
||||
|
||||
//
|
||||
// ALLOW_FORETELL_ANYTIME:
|
||||
// For Cosmos Charger effect
|
||||
ALLOW_FORETELL_ANYTIME;
|
||||
|
||||
private final boolean needPlayCardAbility; // mark effect type as compatible with play/cast abilities
|
||||
private final boolean needAffectedAbility; // mark what AsThough check must be called for specific ability, not full object (example: spell check)
|
||||
private final boolean needPlayCardAbility; // mark what AsThough check must be called for play/cast abilities
|
||||
|
||||
AsThoughEffectType() {
|
||||
this(false);
|
||||
this(false, false);
|
||||
}
|
||||
|
||||
AsThoughEffectType(boolean needPlayCardAbility) {
|
||||
AsThoughEffectType(boolean needAffectedAbility, boolean needPlayCardAbility) {
|
||||
this.needAffectedAbility = needAffectedAbility;
|
||||
this.needPlayCardAbility = needPlayCardAbility;
|
||||
}
|
||||
|
||||
public boolean needAffectedAbility() {
|
||||
return needAffectedAbility;
|
||||
}
|
||||
|
||||
public boolean needPlayCardAbility() {
|
||||
return needPlayCardAbility;
|
||||
}
|
||||
|
|
|
@ -169,8 +169,8 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) &&
|
||||
player.chooseUse(Outcome.Damage, "Do you wish to assign damage for "
|
||||
+ attacker.getLogName() + " as though it weren't blocked?", null, game)) ||
|
||||
game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED
|
||||
, null, attacker.getControllerId(), game) != null) {
|
||||
game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED,
|
||||
null, attacker.getControllerId(), game) != null) {
|
||||
// for handling creatures like Thorn Elemental
|
||||
blocked = false;
|
||||
unblockedDamage(first, game);
|
||||
|
@ -672,16 +672,12 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
if (attackers.contains(creatureId)) {
|
||||
attackers.remove(creatureId);
|
||||
result = true;
|
||||
if (attackerOrder.contains(creatureId)) {
|
||||
attackerOrder.remove(creatureId);
|
||||
}
|
||||
attackerOrder.remove(creatureId);
|
||||
} else if (blockers.contains(creatureId)) {
|
||||
blockers.remove(creatureId);
|
||||
result = true;
|
||||
//20100423 - 509.2a
|
||||
if (blockerOrder.contains(creatureId)) {
|
||||
blockerOrder.remove(creatureId);
|
||||
}
|
||||
blockerOrder.remove(creatureId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -854,10 +850,8 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (appliesBandsWithOther(attackers, game)) { // 702.21k - both a [quality] creature with “bands with other [quality]” and another [quality] creature (...)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// 702.21k - both a [quality] creature with “bands with other [quality]” and another [quality] creature (...)
|
||||
return appliesBandsWithOther(attackers, game);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4011,7 +4011,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
@Override
|
||||
public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) {
|
||||
if (null != game.getContinuousEffects().asThough(card.getId(),
|
||||
AsThoughEffectType.LOOK_AT_FACE_DOWN, card.getSpellAbility(), this.getId(), game)) {
|
||||
AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) {
|
||||
// two modes: look at the card or do not look and activate other abilities
|
||||
String lookMessage = "Look at " + card.getIdName();
|
||||
String lookYes = "Yes, look at the card";
|
||||
|
|
Loading…
Reference in a new issue