diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/TyvarKellTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/TyvarKellTest.java new file mode 100644 index 00000000000..99f76d984f8 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/khm/TyvarKellTest.java @@ -0,0 +1,87 @@ +package org.mage.test.cards.single.khm; + +import mage.abilities.keyword.HasteAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class TyvarKellTest extends CardTestPlayerBase { + + @Test + public void test_Emblem_Normal() { + removeAllCardsFromHand(playerA); + + // −6: You get an emblem with "Whenever you cast an Elf spell, it gains haste until end of turn and you draw two cards." + addCard(Zone.BATTLEFIELD, playerA, "Tyvar Kell"); + // + addCard(Zone.HAND, playerA, "Arbor Elf", 1); // {G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + + // prepare emblem + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tyvar Kell", CounterType.LOYALTY, 10); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-6:"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkHandCount("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // 1x elf + + // cast elf and draw + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arbor Elf"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkHandCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // 2x from draw + checkAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arbor Elf", HasteAbility.class, true); + + // check that it can attack + attack(1, playerA, "Arbor Elf", playerB); + + // haste must end on next turn + checkAbility("end", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Arbor Elf", HasteAbility.class, false); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerB, 20 - 1); // 1x from elf + } + + @Test + public void test_Emblem_Blink() { + removeAllCardsFromHand(playerA); + + // −6: You get an emblem with "Whenever you cast an Elf spell, it gains haste until end of turn and you draw two cards." + addCard(Zone.BATTLEFIELD, playerA, "Tyvar Kell"); + // + addCard(Zone.HAND, playerA, "Arbor Elf", 1); // {G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + // + // Exile target creature you control, then return that card to the battlefield under your control. + addCard(Zone.HAND, playerA, "Cloudshift", 1); // {W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + + // prepare emblem + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tyvar Kell", CounterType.LOYALTY, 10); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-6:"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkHandCount("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // 1x elf + 1x shift + + // cast elf and draw + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arbor Elf"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkHandCount("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 3); // 2x from draw + 1x shift + checkAbility("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arbor Elf", HasteAbility.class, true); + + // blink elf, so it must lose haste + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cloudshift", "Arbor Elf"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkAbility("blink", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arbor Elf", HasteAbility.class, false); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/watchers/HallOfTheBanditLordTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/watchers/HallOfTheBanditLordTest.java index e6191f7b371..d9b5f918828 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/watchers/HallOfTheBanditLordTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/watchers/HallOfTheBanditLordTest.java @@ -29,8 +29,10 @@ public class HallOfTheBanditLordTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Goblin Roughrider"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); this.assertAbility(playerA, "Goblin Roughrider", HasteAbility.getInstance(), true); } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 8e27ddeed28..726b255362e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -1392,7 +1392,12 @@ public class TestPlayer implements Player { } private void assertHandCount(PlayerAction action, Game game, Player player, int count) { - Assert.assertEquals(action.getActionName() + " - hand must contain " + count, count, player.getHand().size()); + if (player.getHand().size() != count) { + printStart("Hand of " + player.getName()); + printCards(player.getHand().getCards(game)); + printEnd(); + Assert.fail(action.getActionName() + " - hand must contain " + count + ", but found " + player.getHand().size()); + } } private void assertHandCardCount(PlayerAction action, Game game, Player player, String cardName, int count) { @@ -1416,7 +1421,12 @@ public class TestPlayer implements Player { } } - Assert.assertEquals(action.getActionName() + " - command zone must contain " + count + " cards of " + cardName, count, realCount); + if (realCount != count) { + printStart("Cards in command zone from " + player.getName()); + printCards(game.getCommanderCardsFromCommandZone(player)); + printEnd(); + Assert.fail(action.getActionName() + " - must have " + count + " cards with name " + cardName + ", but found " + realCount); + } } private void assertColor(PlayerAction action, Game game, Player player, String permanentName, String colors, boolean mustHave) { diff --git a/Mage/src/main/java/mage/abilities/common/SpellCastControllerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/SpellCastControllerTriggeredAbility.java index c4b34b53d56..9bd79453583 100644 --- a/Mage/src/main/java/mage/abilities/common/SpellCastControllerTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/SpellCastControllerTriggeredAbility.java @@ -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; } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index 37671eed41a..4558b0496f5 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -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 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 @@ -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 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. + if (AbilityType.STATIC != source.getAbilityType()) { if (layer != null) { switch (layer) { case CopyEffects_1: diff --git a/Mage/src/main/java/mage/abilities/effects/EffectImpl.java b/Mage/src/main/java/mage/abilities/effects/EffectImpl.java index 65e0bbada81..e27858f214e 100644 --- a/Mage/src/main/java/mage/abilities/effects/EffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/EffectImpl.java @@ -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 values; protected String concatPrefix = ""; // combines multiple effects in text rule diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java index 51204ed8854..a6a39f14206 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityTargetEffect.java @@ -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 newWaitingPermanents = new ArrayList<>(); + for (Iterator 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(); } - } diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index 0a9c8eba509..68d69ec4027 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -1,8 +1,5 @@ package mage.cards; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.*; import mage.MageObject; import mage.MageObjectImpl; import mage.Mana; @@ -19,6 +16,7 @@ import mage.game.*; import mage.game.command.CommandObject; import mage.game.events.*; import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentCard; import mage.game.stack.Spell; import mage.game.stack.StackObject; import mage.util.CardUtil; @@ -26,6 +24,10 @@ import mage.util.GameLog; import mage.watchers.Watcher; import org.apache.log4j.Logger; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.*; + public abstract class CardImpl extends MageObjectImpl implements Card { private static final long serialVersionUID = 1L; @@ -310,6 +312,12 @@ public abstract class CardImpl extends MageObjectImpl implements Card { abilities.add(subAbility); } + // dynamic check: you can't add ability to the PermanentCard, use permanent.addAbility(a, source, game) instead + // reason: triggered abilities are not processing here + if (this instanceof PermanentCard) { + throw new IllegalArgumentException("Wrong code usage. Don't use that method for permanents, use permanent.addAbility(a, source, game) instead."); + } + // verify check: draw effect can't be rollback after mana usage (example: Chromatic Sphere) // (player can cheat with cancel button to see next card) // verify test will catch that errors @@ -325,8 +333,6 @@ public abstract class CardImpl extends MageObjectImpl implements Card { } } } - - } protected void addAbilities(List abilities) { diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index d187a5ff4d5..0d44f2c825d 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2978,7 +2978,7 @@ public abstract class GameImpl implements Game, Serializable { } // remembers if a object was in a zone during the resolution of an effect // e.g. Wrath destroys all and you the question is is the replacement effect to apply because the source was also moved by the same effect - // because it ahppens all at the same time the replcaement effect has still to be applied + // because it happens all at the same time the replacement effect has still to be applied Set idSet = shortLivingLKI.computeIfAbsent(zone, k -> new HashSet<>()); idSet.add(objectId); if (object instanceof Permanent) { diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 7ad046582c5..d675cb4d444 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1,9 +1,5 @@ package mage.game; -import java.io.Serializable; -import java.util.*; -import static java.util.Collections.emptyList; -import java.util.stream.Collectors; import mage.MageObject; import mage.MageObjectReference; import mage.abilities.*; @@ -25,6 +21,7 @@ import mage.game.command.Plane; import mage.game.events.*; import mage.game.permanent.Battlefield; import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentCard; import mage.game.permanent.PermanentToken; import mage.game.stack.SpellStack; import mage.game.stack.StackObject; @@ -39,6 +36,12 @@ import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import mage.watchers.Watchers; +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; + /** * @author BetaSteward_at_googlemail.com *

@@ -1067,6 +1070,8 @@ public class GameState implements Serializable, Copyable { * @param ability */ public void addOtherAbility(Card attachedTo, Ability ability) { + checkWrongDynamicAbilityUsage(attachedTo, ability); + addOtherAbility(attachedTo, ability, true); } @@ -1079,6 +1084,8 @@ public class GameState implements Serializable, Copyable { * state */ public void addOtherAbility(Card attachedTo, Ability ability, boolean copyAbility) { + checkWrongDynamicAbilityUsage(attachedTo, ability); + Ability newAbility; if (ability instanceof MageSingleton || !copyAbility) { newAbility = ability; @@ -1094,6 +1101,16 @@ public class GameState implements Serializable, Copyable { addAbility(newAbility, attachedTo.getId(), attachedTo); } + private void checkWrongDynamicAbilityUsage(Card attachedTo, Ability ability) { + // dynamic abilities for card only + // permanent's abilities are static and generated each reset cycle + if (attachedTo instanceof PermanentCard) { + throw new IllegalArgumentException("Error, wrong code usage. If you want to add new ability to the " + + "permanent then use a permanent.addAbility(a, source, game): " + + ability.getClass().getCanonicalName() + " - " + ability.toString()); + } + } + /** * Removes Triggered abilities that belong to sourceId This is used if a * token leaves the battlefield diff --git a/Mage/src/main/java/mage/game/command/emblems/TyvarKellEmblem.java b/Mage/src/main/java/mage/game/command/emblems/TyvarKellEmblem.java index 560a9483881..b99d0d8affc 100644 --- a/Mage/src/main/java/mage/game/command/emblems/TyvarKellEmblem.java +++ b/Mage/src/main/java/mage/game/command/emblems/TyvarKellEmblem.java @@ -1,23 +1,17 @@ package mage.game.command.emblems; -import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.SpellCastControllerTriggeredAbility; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.abilities.keyword.HasteAbility; -import mage.cards.Card; -import mage.constants.*; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.Zone; import mage.filter.FilterSpell; -import mage.game.Game; import mage.game.command.Emblem; -import mage.game.permanent.Permanent; -import mage.game.stack.Spell; - -import java.util.Iterator; /** - * * @author weirddan455 */ public final class TyvarKellEmblem extends Emblem { @@ -32,74 +26,12 @@ public final class TyvarKellEmblem extends Emblem { public TyvarKellEmblem() { this.setName("Emblem Tyvar"); this.setExpansionSetCodeForImage("KHM"); - Ability ability = new SpellCastControllerTriggeredAbility(Zone.COMMAND, new TyvarKellEmblemEffect( - HasteAbility.getInstance(), Duration.EndOfTurn), filter, false, true + + Ability ability = new SpellCastControllerTriggeredAbility(Zone.COMMAND, + new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.EndOfTurn, null, true), + filter, false, true, true ); ability.addEffect(new DrawCardSourceControllerEffect(2, "you").concatBy("and")); this.getAbilities().add(ability); } } - -class TyvarKellEmblemEffect extends ContinuousEffectImpl { - - protected Ability ability; - - public TyvarKellEmblemEffect(Ability ability, Duration duration) { - super(duration, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); - this.ability = ability; - this.generateGainAbilityDependencies(ability, null); - this.staticText = "it gains haste until end of turn"; - } - - public TyvarKellEmblemEffect(final TyvarKellEmblemEffect effect) { - super(effect); - this.ability = effect.ability.copy(); - } - - @Override - public TyvarKellEmblemEffect copy() { - return new TyvarKellEmblemEffect(this); - } - - @Override - public void init (Ability source, Game game) { - super.init(source, game); - if (this.affectedObjectsSet) { - Spell spell = game.getStack().getSpell(targetPointer.getFirst(game, source)); - if (spell != null) { - Card card = game.getCard(spell.getSourceId()); - if (card != null) { - affectedObjectList.add(new MageObjectReference(card, game)); - } - } - } - } - - @Override - public boolean apply(Game game, Ability source) { - if (this.affectedObjectsSet) { - for (Iterator it = affectedObjectList.iterator(); it.hasNext(); ) { - MageObjectReference mor = it.next(); - Card card = mor.getCard(game); - Permanent perm = game.getPermanent(mor.getSourceId()); - boolean applied = false; - if (card != null && !card.hasAbility(ability, game)) { - game.getState().addOtherAbility(card, ability); - applied = true; - } - if (perm != null && perm.getZoneChangeCounter(game) == mor.getZoneChangeCounter() + 1) { - perm.addAbility(ability, source.getSourceId(), game); - applied = true; - } - if (!applied) { - it.remove(); - if (affectedObjectList.isEmpty()) { - discard(); - } - } - } - return true; - } - return false; - } -} diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index f484eb00688..1e119403756 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -176,6 +176,13 @@ public interface Permanent extends Card, Controllable { String getValue(GameState state); + /** + * Add abilities to the permanent, can be used in effects + * + * @param ability + * @param sourceId + * @param game + */ void addAbility(Ability ability, UUID sourceId, Game game); void removeAllAbilities(UUID sourceId, Game game); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentCard.java b/Mage/src/main/java/mage/game/permanent/PermanentCard.java index e8f2b638153..545b4fa3df5 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentCard.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentCard.java @@ -47,9 +47,6 @@ public class PermanentCard extends PermanentImpl { if (otherAbilities != null) { abilities.addAll(otherAbilities); } - /*if (card.getCardType().contains(CardType.PLANESWALKER)) { - this.loyalty = new MageInt(card.getLoyalty().getValue()); - }*/ if (card instanceof LevelerCard) { maxLevelCounters = ((LevelerCard) card).getMaxLevelCounters(); } @@ -89,6 +86,7 @@ public class PermanentCard extends PermanentImpl { } } } else { + // copy only own abilities; all dynamic added abilities must be added in the parent call this.abilities = card.getAbilities().copy(); // only set spellAbility to null if it has no targets IE: Dance of the Dead bug #7031 if (this.getSpellAbility() != null @@ -96,21 +94,6 @@ public class PermanentCard extends PermanentImpl { this.spellAbility = null; // will be set on first getSpellAbility call if card has one. } } - // adventure cards must show adventure spell info on battlefield too - /* - if (card instanceof AdventureCard) { - // Adventure card spell abilities should not appear on permanents. - List toRemove = new ArrayList(); - for (Ability ability : this.abilities) { - if (ability instanceof SpellAbility) { - if (((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.ADVENTURE_SPELL) { - toRemove.add(ability); - } - } - } - toRemove.forEach(ability -> this.abilities.remove(ability)); - } - */ this.abilities.setControllerId(this.controllerId); this.abilities.setSourceId(objectId); this.cardType.clear();