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

@ -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,25 +1,45 @@
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();
@Override
AsThoughEffect copy();
boolean isConsumable();
}

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) {
@ -1390,18 +1395,19 @@ 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());
logger.info("continuousRuleModifyingEffects ...: " + continuousRuleModifyingEffects.size());
@ -1415,10 +1421,10 @@ public class ContinuousEffects implements Serializable {
logger.info("asThoughEffects:");
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"));
Map<String, TraceInfo> orderedEffects = new TreeMap<>();
}
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");
traceAddContinuousEffects(orderedEffects, replacementEffects, game, "replacementEffects............");
@ -1430,28 +1436,29 @@ public class ContinuousEffects implements Serializable {
traceAddContinuousEffects(orderedEffects, spliceCardEffects, game, "spliceCardEffects.............");
for (Map.Entry<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> entry : asThoughEffectsMap.entrySet()) {
traceAddContinuousEffects(orderedEffects, entry.getValue(), game, entry.getKey().toString());
}
}
String playerName = "";
for (Map.Entry<String, TraceInfo> entry : orderedEffects.entrySet()) {
for (Map.Entry<String, TraceInfo> entry : orderedEffects.entrySet()) {
if (!entry.getValue().getPlayerName().equals(playerName)) {
playerName = entry.getValue().getPlayerName();
logger.info("--- Player: " + playerName + " --------------------------------");
}
logger.info(entry.getValue().getInfo()
+ " " + entry.getValue().getSourceName()
}
logger.info(entry.getValue().getInfo()
+ " " + 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());
for (Ability ability : abilities) {
Player controller = game.getPlayer(ability.getControllerId());
MageObject source = game.getObject(ability.getSourceId());
MageObject source = game.getObject(ability.getSourceId());
TraceInfo traceInfo = new TraceInfo();
traceInfo.setInfo(listName);
traceInfo.setOrder(effect.getOrder());
@ -1459,16 +1466,16 @@ 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());
traceInfo.setEffectId(effect.getId());
traceInfo.setEffectId(effect.getId());
traceInfo.setDuration(effect.getDuration());
orderedEffects.put(traceInfo.getPlayerName() + traceInfo.getSourceName() + effect.getId() + ability.getId(), traceInfo);
}
}
}
}
}

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,41 +119,50 @@ 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
*
*
* @param game
* @param source
* @param cards
* @param allowedCaster
* @param duration
* @param withoutMana
* @return
* @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;
}
@ -149,17 +176,25 @@ public class PlayFromNotOwnHandZoneTargetEffect extends AsThoughEffectImpl {
+ "-" + game.getState().getTurnNum()
+ "-" + sourceObject.getIdName(), game
);
String exileName = sourceObject.getIdName() + " free play"
+ (Duration.EndOfTurn.equals(duration) ? " on turn " + game.getState().getTurnNum():"")
String exileName = sourceObject.getIdName() + " free play"
+ (Duration.EndOfTurn.equals(duration) ? " on turn " + game.getState().getTurnNum() : "")
+ " for " + controller.getName();
if (Duration.EndOfTurn.equals(duration)) {
game.getExile().createZone(exileId, exileName).setCleanupOnEndTurn(true);
}
}
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";