Tyvar Kell and gain ability fixes:

* GainAbilityTargetEffect - reworked to support static/dynamic targets, added support of spells (card + related permanent);
* SpellCastControllerTriggeredAbility - now it can setup the target to a card instead a spell;
* Added checks/errors on wrong ability adding code (example: if you add permanent's ability by game state instead permanent's method);
* Tyvar Kell Emblem now use a standard code;
* Test framework: added additional logs for some errors;
This commit is contained in:
Oleg Agafonov 2021-01-12 04:37:13 +04:00
parent f131fd0d12
commit 6dcbcbe962
13 changed files with 291 additions and 140 deletions

View file

@ -15,14 +15,14 @@ import mage.target.targetpointer.FixedTarget;
public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl {
private static final FilterSpell spellCard = new FilterSpell("a spell");
protected FilterSpell filter;
protected String rule;
/**
* If true, the source that triggered the ability will be set as target to
* effect.
*/
// The source SPELL that triggered the ability will be set as target to effect
protected boolean rememberSource = false;
// Use it if you want remember CARD instead spell
protected boolean rememberSourceAsCard = false;
public SpellCastControllerTriggeredAbility(Effect effect, boolean optional) {
this(Zone.BATTLEFIELD, effect, spellCard, optional, false);
@ -42,16 +42,22 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl {
}
public SpellCastControllerTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, boolean rememberSource) {
this(zone, effect, filter, optional, rememberSource, false);
}
public SpellCastControllerTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, boolean rememberSource, boolean rememberSourceAsCard) {
super(zone, effect, optional);
this.filter = filter;
this.rememberSource = rememberSource;
this.rememberSourceAsCard = rememberSourceAsCard;
}
public SpellCastControllerTriggeredAbility(final SpellCastControllerTriggeredAbility ability) {
super(ability);
this.filter = ability.filter;
this.rememberSource = ability.rememberSource;
this.rule = ability.rule;
this.rememberSource = ability.rememberSource;
this.rememberSourceAsCard = ability.rememberSourceAsCard;
}
@Override
@ -65,7 +71,12 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl {
Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell != null && filter.match(spell, getSourceId(), getControllerId(), game)) {
if (rememberSource) {
this.getEffects().get(0).setTargetPointer(new FixedTarget(spell.getId(), game));
if (rememberSourceAsCard) {
this.getEffects().get(0).setTargetPointer(new FixedTarget(spell.getCard().getId(), game));
} else {
this.getEffects().get(0).setTargetPointer(new FixedTarget(spell.getId(), game));
}
}
return true;
}

View file

@ -33,8 +33,18 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
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
protected boolean affectedObjectsSet = false;
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
@ -154,10 +164,18 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
@Override
public void init(Ability source, Game game, UUID activePlayerId) {
targetPointer.init(game, source);
//20100716 - 611.2c
if (AbilityType.ACTIVATED == source.getAbilityType()
|| AbilityType.SPELL == source.getAbilityType()
|| AbilityType.TRIGGERED == source.getAbilityType()) {
// 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 wont 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 doesnt
// modify the characteristics or change the controller of any objects modifies the
// rules of the game, so it can affect objects that werent 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 dont, the set of objects
// each part applies to is determined independently.
if (AbilityType.STATIC != source.getAbilityType()) {
if (layer != null) {
switch (layer) {
case CopyEffects_1:

View file

@ -19,7 +19,10 @@ public abstract class EffectImpl implements Effect {
protected UUID id;
protected Outcome outcome;
protected EffectType effectType;
// read related docs about static and dynamic targets in ContinuousEffectImpl.affectedObjectsSet
protected TargetPointer targetPointer = FirstTargetPointer.getInstance();
protected String staticText = "";
protected Map<String, Object> values;
protected String concatPrefix = ""; // combines multiple effects in text rule

View file

@ -1,5 +1,6 @@
package mage.abilities.effects.common.continuous;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.ContinuousEffectImpl;
@ -9,17 +10,18 @@ import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.Target;
import java.util.Locale;
import java.util.UUID;
import java.util.*;
/**
* @author BetaSteward_at_googlemail.com
* @author JayDi85
*/
public class GainAbilityTargetEffect extends ContinuousEffectImpl {
protected Ability ability;
// shall a card gain the ability (otherwise permanent)
private final boolean onCard;
// shall a card gain the ability (otherwise a permanent)
private final boolean useOnCard; // only one card per ability supported
private boolean waitingCardPermanent = false; // wait the permanent from card's resolve (for inner usage only)
// Duration until next phase step of player
private PhaseStep durationPhaseStep = null;
@ -34,15 +36,15 @@ public class GainAbilityTargetEffect extends ContinuousEffectImpl {
this(ability, duration, rule, false);
}
public GainAbilityTargetEffect(Ability ability, Duration duration, String rule, boolean onCard) {
this(ability, duration, rule, onCard, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA);
public GainAbilityTargetEffect(Ability ability, Duration duration, String rule, boolean useOnCard) {
this(ability, duration, rule, useOnCard, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA);
}
public GainAbilityTargetEffect(Ability ability, Duration duration, String rule, boolean onCard, Layer layer, SubLayer subLayer) {
public GainAbilityTargetEffect(Ability ability, Duration duration, String rule, boolean useOnCard, Layer layer, SubLayer subLayer) {
super(duration, layer, subLayer, ability.getEffects().getOutcome(ability, Outcome.AddAbility));
this.ability = ability;
staticText = rule;
this.onCard = onCard;
this.staticText = rule;
this.useOnCard = useOnCard;
this.generateGainAbilityDependencies(ability, null);
}
@ -50,8 +52,9 @@ public class GainAbilityTargetEffect extends ContinuousEffectImpl {
public GainAbilityTargetEffect(final GainAbilityTargetEffect effect) {
super(effect);
this.ability = effect.ability.copy();
ability.newId(); // This is needed if the effect is copied e.g. by a clone so the ability can be added multiple times to permanents
this.onCard = effect.onCard;
this.ability.newId(); // This is needed if the effect is copied e.g. by a clone so the ability can be added multiple times to permanents
this.useOnCard = effect.useOnCard;
this.waitingCardPermanent = effect.waitingCardPermanent;
this.durationPhaseStep = effect.durationPhaseStep;
this.durationPlayerId = effect.durationPlayerId;
this.sameStep = effect.sameStep;
@ -70,10 +73,38 @@ public class GainAbilityTargetEffect extends ContinuousEffectImpl {
@Override
public void init(Ability source, Game game) {
super.init(source, game);
if (durationPhaseStep != null) {
durationPlayerId = source.getControllerId();
sameStep = true;
}
// must support dynamic targets from static ability and static targets from activated abilities
if (this.affectedObjectsSet) {
// target permanents (by default)
targetPointer.getTargets(game, source)
.stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.forEach(permanent -> {
this.affectedObjectList.add(new MageObjectReference(permanent, game));
});
// target cards with linked permanents
if (this.useOnCard) {
targetPointer.getTargets(game, source)
.stream()
.map(game::getCard)
.filter(Objects::nonNull)
.forEach(card -> {
this.affectedObjectList.add(new MageObjectReference(card, game));
});
waitingCardPermanent = true;
if (this.affectedObjectList.size() > 1) {
throw new IllegalArgumentException("Gain ability can't target a multiple cards (unsupported)");
}
}
}
}
@Override
@ -97,29 +128,74 @@ public class GainAbilityTargetEffect extends ContinuousEffectImpl {
@Override
public boolean apply(Game game, Ability source) {
int affectedTargets = 0;
if (onCard) {
for (UUID cardId : targetPointer.getTargets(game, source)) {
Card card = game.getCard(cardId);
if (card != null) {
game.getState().addOtherAbility(card, ability);
if (affectedObjectsSet) {
// STATIC TARGETS
List<MageObjectReference> newWaitingPermanents = new ArrayList<>();
for (Iterator<MageObjectReference> it = affectedObjectList.iterator(); it.hasNext(); ) {
MageObjectReference mor = it.next();
// look for permanent
Permanent permanent = mor.getPermanent(game);
if (permanent != null) {
this.waitingCardPermanent = false;
permanent.addAbility(ability, source.getSourceId(), game);
affectedTargets++;
continue;
}
// look for card with linked permanent
if (this.useOnCard) {
Card card = mor.getCard(game);
if (card != null) {
game.getState().addOtherAbility(card, ability);
affectedTargets++;
continue;
} else {
// start waiting a spell's permanent (example: Tyvar Kell's emblem)
Permanent perm = game.getPermanent(mor.getSourceId());
if (perm != null) {
newWaitingPermanents.add(new MageObjectReference(perm, game));
this.waitingCardPermanent = false;
}
}
}
// bad target, can be removed
it.remove();
}
if (duration == Duration.OneUse) {
// add new linked permanents to targets
if (!newWaitingPermanents.isEmpty()) {
this.affectedObjectList.addAll(newWaitingPermanents);
return affectedTargets > 0;
}
// no more valid targets
if (this.affectedObjectList.isEmpty()) {
discard();
}
// no more valid permanents (card was countered without new permanent)
if (duration == Duration.Custom && affectedTargets == 0 && !this.waitingCardPermanent) {
discard();
}
} else {
for (UUID permanentId : targetPointer.getTargets(game, source)) {
Permanent permanent = game.getPermanentOrLKIBattlefield(permanentId);
// DYNAMIC TARGETS
for (UUID objectId : targetPointer.getTargets(game, source)) {
Permanent permanent = game.getPermanent(objectId);
if (permanent != null) {
permanent.addAbility(ability, source.getSourceId(), game);
affectedTargets++;
continue;
}
if (this.useOnCard) {
Card card = game.getCard(objectId);
if (card != null) {
game.getState().addOtherAbility(card, ability);
affectedTargets++;
}
}
}
}
if (duration == Duration.Custom && affectedTargets == 0) {
this.discard();
}
return affectedTargets > 0;
}
@ -157,5 +233,4 @@ public class GainAbilityTargetEffect extends ContinuousEffectImpl {
}
return sb.toString();
}
}