mirror of
https://github.com/magefree/mage.git
synced 2025-12-26 21:42:07 -08: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
|
|
@ -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,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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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…
Add table
Add a link
Reference in a new issue