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:
Oleg Agafonov 2021-01-31 22:32:23 +04:00
parent b8a95765fc
commit 2d96d36ec8
28 changed files with 375 additions and 217 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ public enum TrampleAbilityIcon implements CardIcon {
@Override
public String getHint() {
return "Trumple ability";
return "Trample ability";
}
@Override

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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