forked from External/mage
531 lines
19 KiB
Java
531 lines
19 KiB
Java
package mage.abilities.effects;
|
||
|
||
import mage.MageObjectReference;
|
||
import mage.abilities.Ability;
|
||
import mage.abilities.CompoundAbility;
|
||
import mage.abilities.keyword.ChangelingAbility;
|
||
import mage.constants.*;
|
||
import mage.filter.Filter;
|
||
import mage.filter.predicate.Predicate;
|
||
import mage.filter.predicate.Predicates;
|
||
import mage.game.Game;
|
||
import mage.game.stack.Spell;
|
||
import mage.game.stack.StackObject;
|
||
import mage.players.Player;
|
||
import mage.target.targetpointer.TargetPointer;
|
||
import mage.watchers.common.EndStepCountWatcher;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||
*/
|
||
public abstract class ContinuousEffectImpl extends EffectImpl implements ContinuousEffect {
|
||
|
||
protected Duration duration;
|
||
protected Layer layer;
|
||
protected SubLayer sublayer;
|
||
protected long order;
|
||
protected boolean used = false;
|
||
protected boolean discarded = false; // for manual effect discard
|
||
|
||
// 611.2c
|
||
// Two types of affected objects (targets):
|
||
// 1. Static targets - setup it on ability resolve or effect creation (effect's init method, only once)
|
||
// 2. Dynamic targets - can be changed after resolve, so use targetPointer, filters or own lists to find affected objects
|
||
//
|
||
// If your ability/effect supports multi use cases (one time use, static, target pointers) then affectedObjectsSet can be useful:
|
||
// * affectedObjectsSet - true on static objects and false on dynamic objects (see rules from 611.2c)
|
||
// Full implement example: GainAbilityTargetEffect
|
||
//
|
||
// is null before being initialized. Any access attempt computes it.
|
||
private Boolean affectedObjectsSet = null;
|
||
protected List<MageObjectReference> affectedObjectList = new ArrayList<>();
|
||
|
||
protected boolean temporary = false;
|
||
protected EnumSet<DependencyType> dependencyTypes; // this effect has the dependencyTypes defined here
|
||
protected EnumSet<DependencyType> dependendToTypes; // this effect is dependent to this types
|
||
/*
|
||
A Characteristic Defining Ability (CDA) is an ability that defines a characteristic of a card or token.
|
||
There are 3 specific rules that distinguish a CDA from other abilities.
|
||
1) A CDA can only define a characteristic of either the card or token it comes from.
|
||
2) A CDA can not be triggered, activated, or conditional.
|
||
3) A CDA must define a characteristic. Usually color, power and/or toughness, or sub-type.
|
||
*/
|
||
protected boolean characterDefining = false;
|
||
|
||
// until your next turn or until end of your next turn
|
||
private UUID startingControllerId; // player to check for turn duration (can't different with real controller ability)
|
||
private boolean startingTurnWasActive; // effect started during related players turn and related players turn was already active
|
||
private int effectStartingOnTurn = 0; // turn the effect started
|
||
private int effectStartingEndStep = 0;
|
||
private int nextTurnNumber = Integer.MAX_VALUE; // effect is waiting for a step during your next turn, we store it if found.
|
||
// set to the turn number on your next turn.
|
||
private int effectStartingStepNum = 0; // Some continuous are waiting for the next step of a kind.
|
||
// Avoid miscancelling if the start step is of that kind.
|
||
|
||
protected ContinuousEffectImpl(Duration duration, Outcome outcome) {
|
||
super(outcome);
|
||
this.duration = duration;
|
||
this.order = 0;
|
||
this.effectType = EffectType.CONTINUOUS;
|
||
this.dependencyTypes = EnumSet.noneOf(DependencyType.class);
|
||
this.dependendToTypes = EnumSet.noneOf(DependencyType.class);
|
||
}
|
||
|
||
protected ContinuousEffectImpl(Duration duration, Layer layer, SubLayer sublayer, Outcome outcome) {
|
||
this(duration, outcome);
|
||
this.layer = layer;
|
||
this.sublayer = sublayer;
|
||
}
|
||
|
||
protected ContinuousEffectImpl(final ContinuousEffectImpl effect) {
|
||
super(effect);
|
||
this.duration = effect.duration;
|
||
this.layer = effect.layer;
|
||
this.sublayer = effect.sublayer;
|
||
this.order = effect.order;
|
||
this.used = effect.used;
|
||
this.discarded = effect.discarded;
|
||
this.affectedObjectsSet = effect.affectedObjectsSet;
|
||
this.affectedObjectList.addAll(effect.affectedObjectList);
|
||
this.temporary = effect.temporary;
|
||
this.startingControllerId = effect.startingControllerId;
|
||
this.startingTurnWasActive = effect.startingTurnWasActive;
|
||
this.effectStartingOnTurn = effect.effectStartingOnTurn;
|
||
this.effectStartingEndStep = effect.effectStartingEndStep;
|
||
this.dependencyTypes = effect.dependencyTypes;
|
||
this.dependendToTypes = effect.dependendToTypes;
|
||
this.characterDefining = effect.characterDefining;
|
||
this.nextTurnNumber = effect.nextTurnNumber;
|
||
this.effectStartingStepNum = effect.effectStartingStepNum;
|
||
}
|
||
|
||
@Override
|
||
public ContinuousEffectImpl setDuration(Duration duration) {
|
||
this.duration = duration;
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public Duration getDuration() {
|
||
return duration;
|
||
}
|
||
|
||
@Override
|
||
public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) {
|
||
if (this.layer == layer && this.sublayer == sublayer) {
|
||
return apply(game, source);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
@Override
|
||
public long getOrder() {
|
||
return order;
|
||
}
|
||
|
||
@Override
|
||
public void setOrder(long order) {
|
||
this.order = order;
|
||
}
|
||
|
||
@Override
|
||
public boolean hasLayer(Layer layer) {
|
||
return this.layer == layer;
|
||
}
|
||
|
||
@Override
|
||
public boolean isUsed() {
|
||
return used;
|
||
}
|
||
|
||
@Override
|
||
public boolean isDiscarded() {
|
||
return discarded;
|
||
}
|
||
|
||
/**
|
||
* Sets the discarded state of the effect. So it will be removed on next
|
||
* check.
|
||
*/
|
||
@Override
|
||
public void discard() {
|
||
this.used = true; // to prevent further usage before effect is removed
|
||
this.discarded = true;
|
||
}
|
||
|
||
@Override
|
||
public final void initNewTargetPointer() {
|
||
// continuous effect uses init code, so do nothing here
|
||
}
|
||
|
||
@Override
|
||
public void init(Ability source, Game game) {
|
||
init(source, game, game.getActivePlayerId());
|
||
}
|
||
|
||
@Override
|
||
public void init(Ability source, Game game, UUID activePlayerId) {
|
||
getTargetPointer().init(game, source);
|
||
if (this.affectedObjectsSet == null) {
|
||
initAffectedObjectsSet(source);
|
||
}
|
||
setStartingControllerAndTurnNum(game, source.getControllerId(), activePlayerId);
|
||
}
|
||
|
||
/**
|
||
* Computes affectedObjectsSet boolean from the source object.
|
||
*/
|
||
private void initAffectedObjectsSet(Ability source) {
|
||
// 20210112 - 611.2c
|
||
// 611.2c If a continuous effect generated by the resolution of a spell or ability modifies the
|
||
// characteristics or changes the controller of any objects, the set of objects it affects is
|
||
// determined when that continuous effect begins. After that point, the set won’t change.
|
||
// (Note that this works differently than a continuous effect from a static ability.)
|
||
// A continuous effect generated by the resolution of a spell or ability that doesn’t
|
||
// modify the characteristics or change the controller of any objects modifies the
|
||
// rules of the game, so it can affect objects that weren’t affected when that continuous
|
||
// effect began.If a single continuous effect has parts that modify the characteristics or
|
||
// changes the controller of any objects and other parts that don’t, the set of objects
|
||
// each part applies to is determined independently.
|
||
this.affectedObjectsSet = false;
|
||
if (AbilityType.STATIC != source.getAbilityType()) {
|
||
if (layer != null) {
|
||
switch (layer) {
|
||
case CopyEffects_1:
|
||
case ControlChangingEffects_2:
|
||
case TextChangingEffects_3:
|
||
case TypeChangingEffects_4:
|
||
case ColorChangingEffects_5:
|
||
case AbilityAddingRemovingEffects_6:
|
||
case PTChangingEffects_7:
|
||
this.affectedObjectsSet = true;
|
||
}
|
||
} else if (hasLayer(Layer.CopyEffects_1)
|
||
|| hasLayer(Layer.ControlChangingEffects_2)
|
||
|| hasLayer(Layer.TextChangingEffects_3)
|
||
|| hasLayer(Layer.TypeChangingEffects_4)
|
||
|| hasLayer(Layer.ColorChangingEffects_5)
|
||
|| hasLayer(Layer.AbilityAddingRemovingEffects_6)
|
||
|| hasLayer(Layer.PTChangingEffects_7)) {
|
||
this.affectedObjectsSet = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* This is a workaround when trying to access affectObjectsSet during init, before
|
||
* ContinuousEffectImpl::init is called.
|
||
* <p>
|
||
* TODO: should be investigated how to modify all continuous effects to call super.init()
|
||
* before doing their own changes. At which point there is no longer need for this
|
||
* workaround.
|
||
*/
|
||
protected boolean getAffectedObjectsSetAtInit(Ability source) {
|
||
if (this.affectedObjectsSet == null) {
|
||
initAffectedObjectsSet(source);
|
||
}
|
||
return this.affectedObjectsSet;
|
||
}
|
||
|
||
/**
|
||
* Use this getter in other places than overriden calls, most likely the apply method.
|
||
*/
|
||
protected boolean getAffectedObjectsSet() {
|
||
if (this.affectedObjectsSet == null) {
|
||
return false;
|
||
}
|
||
return this.affectedObjectsSet;
|
||
}
|
||
|
||
protected void setAffectedObjectsSet(boolean affectedObjectsSet) {
|
||
this.affectedObjectsSet = affectedObjectsSet;
|
||
}
|
||
|
||
@Override
|
||
public UUID getStartingController() {
|
||
return startingControllerId;
|
||
}
|
||
|
||
protected int getEffectStartingOnTurn() {
|
||
return effectStartingOnTurn;
|
||
}
|
||
|
||
@Override
|
||
public void setStartingControllerAndTurnNum(Game game, UUID startingController, UUID activePlayerId) {
|
||
this.startingControllerId = startingController;
|
||
this.startingTurnWasActive = activePlayerId != null
|
||
&& activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too
|
||
this.effectStartingOnTurn = game.getTurnNum();
|
||
this.effectStartingEndStep = EndStepCountWatcher.getCount(startingController, game);
|
||
this.effectStartingStepNum = game.getState().getStepNum();
|
||
}
|
||
|
||
@Override
|
||
public boolean isYourNextTurn(Game game) {
|
||
return effectStartingOnTurn < game.getTurnNum()
|
||
&& game.isActivePlayer(startingControllerId);
|
||
}
|
||
|
||
@Override
|
||
public boolean isYourNextEndStep(Game game) {
|
||
return EndStepCountWatcher.getCount(startingControllerId, game) > effectStartingEndStep;
|
||
}
|
||
|
||
public boolean isEndCombatOfYourNextTurn(Game game) {
|
||
int currentTurn = game.getTurnNum();
|
||
if (nextTurnNumber != Integer.MAX_VALUE && nextTurnNumber < currentTurn) {
|
||
return false; // This is a turn after your next turn.
|
||
}
|
||
if (nextTurnNumber == Integer.MAX_VALUE && isYourNextTurn(game)) {
|
||
nextTurnNumber = currentTurn;
|
||
}
|
||
|
||
return isYourNextTurn(game)
|
||
&& game.getPhase().getType() == TurnPhase.POSTCOMBAT_MAIN;
|
||
}
|
||
|
||
public boolean isYourNextUpkeepStep(Game game) {
|
||
return (effectStartingOnTurn < game.getTurnNum() ||
|
||
effectStartingStepNum < game.getState().getStepNum())
|
||
&& game.isActivePlayer(startingControllerId)
|
||
&& game.getStep().getType() == PhaseStep.UPKEEP;
|
||
}
|
||
|
||
@Override
|
||
public boolean isInactive(Ability source, Game game) {
|
||
// YOUR turn checks, players who left the game
|
||
// until end of turn - must be checked on cleanup step, see rules 514.2
|
||
// other must checked here (active and leave players), see rules 800.4
|
||
switch (duration) {
|
||
case UntilYourNextTurn:
|
||
case UntilEndOfYourNextTurn:
|
||
case UntilYourNextEndStep:
|
||
case UntilEndCombatOfYourNextTurn:
|
||
case UntilYourNextUpkeepStep:
|
||
break;
|
||
default:
|
||
return false;
|
||
}
|
||
|
||
// cheat engine put cards without play and calls direct applyEffects with clean -- need to ignore it
|
||
if (game.getActivePlayerId() == null) {
|
||
return false;
|
||
}
|
||
|
||
boolean canDelete;
|
||
Player player = game.getPlayer(startingControllerId);
|
||
|
||
// discard on start of turn for leaved player
|
||
// 800.4i When a player leaves the game, any continuous effects with durations that last until that player's next turn
|
||
// or until a specific point in that turn will last until that turn would have begun.
|
||
// They neither expire immediately nor last indefinitely.
|
||
switch (duration) {
|
||
case UntilYourNextTurn:
|
||
case UntilEndOfYourNextTurn:
|
||
canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving());
|
||
break;
|
||
default:
|
||
canDelete = false;
|
||
}
|
||
|
||
if (canDelete) {
|
||
return true;
|
||
}
|
||
|
||
// discard on another conditions (start of your turn)
|
||
switch (duration) {
|
||
case UntilYourNextTurn:
|
||
if (player != null && player.isInGame()) {
|
||
return this.isYourNextTurn(game);
|
||
}
|
||
break;
|
||
case UntilYourNextEndStep:
|
||
if (player != null && player.isInGame()) {
|
||
return this.isYourNextEndStep(game);
|
||
}
|
||
break;
|
||
case UntilEndCombatOfYourNextTurn:
|
||
if (player != null && player.isInGame()) {
|
||
return this.isEndCombatOfYourNextTurn(game);
|
||
}
|
||
break;
|
||
case UntilYourNextUpkeepStep:
|
||
if (player != null && player.isInGame()) {
|
||
return this.isYourNextUpkeepStep(game);
|
||
}
|
||
break;
|
||
}
|
||
|
||
return canDelete;
|
||
}
|
||
|
||
@Override
|
||
public Layer getLayer() {
|
||
return layer;
|
||
}
|
||
|
||
@Override
|
||
public SubLayer getSublayer() {
|
||
return sublayer;
|
||
}
|
||
|
||
@Override
|
||
public List<MageObjectReference> getAffectedObjects() {
|
||
return affectedObjectList;
|
||
}
|
||
|
||
/**
|
||
* Returns the status if the effect is temporary added to the
|
||
* ContinuousEffects
|
||
*
|
||
* @return
|
||
*/
|
||
@Override
|
||
public boolean isTemporary() {
|
||
return temporary;
|
||
}
|
||
|
||
@Override
|
||
public void setTemporary(boolean temporary) {
|
||
this.temporary = temporary;
|
||
}
|
||
|
||
public boolean isCharacterDefining() {
|
||
return characterDefining;
|
||
}
|
||
|
||
public void setCharacterDefining(boolean characterDefining) {
|
||
this.characterDefining = characterDefining;
|
||
}
|
||
|
||
@Override
|
||
public Set<UUID> isDependentTo(List<ContinuousEffect> allEffectsInLayer) {
|
||
Set<UUID> dependentToEffects = new HashSet<>();
|
||
if (dependendToTypes != null) {
|
||
for (ContinuousEffect effect : allEffectsInLayer) {
|
||
if (!effect.getId().equals(this.getId())) {
|
||
for (DependencyType dependencyType : effect.getDependencyTypes()) {
|
||
if (dependendToTypes.contains(dependencyType)) {
|
||
dependentToEffects.add(effect.getId());
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return dependentToEffects;
|
||
}
|
||
|
||
@Override
|
||
public EnumSet<DependencyType> getDependencyTypes() {
|
||
return dependencyTypes;
|
||
}
|
||
|
||
@Override
|
||
public EnumSet<DependencyType> getDependedToTypes() {
|
||
return dependendToTypes;
|
||
}
|
||
|
||
@Override
|
||
public void addDependencyType(DependencyType dependencyType) {
|
||
if (dependencyType != null) {
|
||
dependencyTypes.add(dependencyType);
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void setDependedToType(DependencyType dependencyType) {
|
||
dependendToTypes.clear();
|
||
dependendToTypes.add(dependencyType);
|
||
}
|
||
|
||
@Override
|
||
public void addDependedToType(DependencyType dependencyType) {
|
||
dependendToTypes.add(dependencyType);
|
||
}
|
||
|
||
@Override
|
||
public ContinuousEffect setTargetPointer(TargetPointer targetPointer) {
|
||
super.setTargetPointer(targetPointer);
|
||
return this;
|
||
}
|
||
|
||
@Override
|
||
public ContinuousEffect withTargetDescription(String target) {
|
||
super.withTargetDescription(target);
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* Auto-generates dependencies on different effects (what's apply first and
|
||
* what's apply second)
|
||
*
|
||
* @param abilityToGain
|
||
* @param filterToSearch
|
||
*/
|
||
public void generateGainAbilityDependencies(Ability abilityToGain, Filter filterToSearch) {
|
||
this.addDependencyType(DependencyType.AddingAbility);
|
||
this.generateGainAbilityDependenciesFromAbility(abilityToGain);
|
||
this.generateGainAbilityDependenciesFromFilter(filterToSearch);
|
||
}
|
||
|
||
public void generateGainAbilityDependencies(CompoundAbility abilityToGain, Filter filterToSearch) {
|
||
this.addDependencyType(DependencyType.AddingAbility);
|
||
this.generateGainAbilityDependenciesFromAbility(abilityToGain);
|
||
this.generateGainAbilityDependenciesFromFilter(filterToSearch);
|
||
}
|
||
|
||
private void generateGainAbilityDependenciesFromAbility(CompoundAbility compoundAbility) {
|
||
if (compoundAbility == null) {
|
||
return;
|
||
}
|
||
for (Ability ability : compoundAbility) {
|
||
generateGainAbilityDependenciesFromAbility(ability);
|
||
}
|
||
}
|
||
|
||
private void generateGainAbilityDependenciesFromAbility(Ability ability) {
|
||
if (ability == null) {
|
||
return;
|
||
}
|
||
|
||
// 1. "Is all type" ability (changeling)
|
||
// make dependency
|
||
if (ability instanceof ChangelingAbility) {
|
||
this.addDependencyType(DependencyType.AddingCreatureType);
|
||
}
|
||
}
|
||
|
||
private void generateGainAbilityDependenciesFromFilter(Filter filter) {
|
||
if (filter == null) {
|
||
return;
|
||
}
|
||
|
||
// 1. "Is all type" ability (changeling)
|
||
// wait dependency
|
||
// extraPredicates from some filters is player related, you don't need it here
|
||
List<Predicate> list = new ArrayList<>();
|
||
Predicates.collectAllComponents(filter.getPredicates(), filter.getExtraPredicates(), list);
|
||
if (list.stream().anyMatch(SubType.SubTypePredicate.class::isInstance)) {
|
||
this.addDependedToType(DependencyType.AddingCreatureType);
|
||
}
|
||
}
|
||
|
||
public boolean canLookAtNextTopLibraryCard(Game game) {
|
||
// If the top card of your library changes while you’re casting a spell, playing a land, or activating an ability,
|
||
// you can’t look at the new top card until you finish doing so. This means that if you cast the top card of
|
||
// your library, you can’t look at the next one until you’re done paying for that spell. (2019-05-03)
|
||
StackObject stackObject = game.getStack().getFirstOrNull();
|
||
return !(stackObject instanceof Spell)
|
||
|| !Zone.LIBRARY.equals(((Spell) stackObject).getFromZone())
|
||
|| stackObject.getStackAbility().getManaCostsToPay().isPaid(); // mana payment finished
|
||
}
|
||
|
||
@Override
|
||
public ContinuousEffect setText(String staticText) {
|
||
super.setText(staticText);
|
||
return this;
|
||
}
|
||
}
|