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 affectedObjectList = new ArrayList<>(); protected boolean temporary = false; protected EnumSet dependencyTypes; // this effect has the dependencyTypes defined here protected EnumSet 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. *

* 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 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 isDependentTo(List allEffectsInLayer) { Set 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 getDependencyTypes() { return dependencyTypes; } @Override public EnumSet 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 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; } }