- Reworked SourceOnBattlefieldControlUnchangedCondition checking now the LOST_CONTROL event which solves the problem with the old code to not be able to detect all controller changes of layered changeController effects when applied later.

- Simplified and fixed some problems of the handling of the "Until end of your next turn" duration.
- Fixed that some continous effects changed controller but shouldn't dependant from their duration type. Controller chnage will now done duration type dependant.
  (that change fixes #6581 in a more general way undoing the effect specific changes of 2e8ece1dbd).
This commit is contained in:
LevelX2 2020-06-10 22:28:23 +02:00
parent 25802dc105
commit 1e36b39434
30 changed files with 534 additions and 469 deletions

View file

@ -1,4 +1,3 @@
package mage.cards.a; package mage.cards.a;
import java.util.UUID; import java.util.UUID;
@ -18,6 +17,7 @@ import mage.constants.SubType;
import mage.filter.FilterPermanent; import mage.filter.FilterPermanent;
import mage.filter.predicate.permanent.AnotherPredicate; import mage.filter.predicate.permanent.AnotherPredicate;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.watchers.common.LostControlWatcher;
/** /**
* @author Loki * @author Loki
@ -47,6 +47,7 @@ public final class AegisAngel extends CardImpl {
"another target permanent is indestructible for as long as you control Aegis Angel"); "another target permanent is indestructible for as long as you control Aegis Angel");
Ability ability = new EntersBattlefieldTriggeredAbility(effect, false); Ability ability = new EntersBattlefieldTriggeredAbility(effect, false);
ability.addTarget(new TargetPermanent(filter)); ability.addTarget(new TargetPermanent(filter));
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.a; package mage.cards.a;
import java.util.UUID; import java.util.UUID;
@ -17,6 +16,7 @@ import mage.constants.Duration;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.target.common.TargetArtifactPermanent; import mage.target.common.TargetArtifactPermanent;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -25,7 +25,7 @@ import mage.target.common.TargetArtifactPermanent;
public final class Aladdin extends CardImpl { public final class Aladdin extends CardImpl {
public Aladdin(UUID ownerId, CardSetInfo setInfo) { public Aladdin(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{R}{R}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{R}");
this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.ROGUE); this.subtype.add(SubType.ROGUE);
this.power = new MageInt(1); this.power = new MageInt(1);
@ -39,6 +39,7 @@ public final class Aladdin extends CardImpl {
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, effect, new ManaCostsImpl("{1}{R}{R}")); Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, effect, new ManaCostsImpl("{1}{R}{R}"));
ability.addCost(new TapSourceCost()); ability.addCost(new TapSourceCost());
ability.addTarget(new TargetArtifactPermanent()); ability.addTarget(new TargetArtifactPermanent());
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.d; package mage.cards.d;
import java.util.UUID; import java.util.UUID;
@ -16,9 +15,9 @@ import mage.abilities.keyword.FlyingAbility;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
@ -26,6 +25,7 @@ import mage.players.Player;
import mage.target.common.TargetCreatureOrPlaneswalker; import mage.target.common.TargetCreatureOrPlaneswalker;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.GameLog; import mage.util.GameLog;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -50,6 +50,7 @@ public final class DragonlordSilumgar extends CardImpl {
// When Dragonlord Silumgar enters the battlefield, gain control of target creature or planeswalker for as long as you control Dragonlord Silumgar. // When Dragonlord Silumgar enters the battlefield, gain control of target creature or planeswalker for as long as you control Dragonlord Silumgar.
Ability ability = new EntersBattlefieldTriggeredAbility(new DragonlordSilumgarEffect(), false); Ability ability = new EntersBattlefieldTriggeredAbility(new DragonlordSilumgarEffect(), false);
ability.addTarget(new TargetCreatureOrPlaneswalker()); ability.addTarget(new TargetCreatureOrPlaneswalker());
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.m; package mage.cards.m;
import java.util.UUID; import java.util.UUID;
@ -11,9 +10,10 @@ import mage.abilities.effects.common.continuous.GainControlTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.SubType;
import mage.target.common.TargetArtifactPermanent; import mage.target.common.TargetArtifactPermanent;
import mage.watchers.common.LostControlWatcher;
/** /**
* @author Loki, JayDi85 * @author Loki, JayDi85
@ -36,6 +36,7 @@ public final class MasterThief extends CardImpl {
false); false);
ability.addTarget(new TargetArtifactPermanent()); ability.addTarget(new TargetArtifactPermanent());
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.m; package mage.cards.m;
import java.util.UUID; import java.util.UUID;
@ -25,6 +24,7 @@ import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -50,6 +50,7 @@ public final class MeriekeRiBerit extends CardImpl {
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, MeriekeRiBeritGainControlEffect, new TapSourceCost()); Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, MeriekeRiBeritGainControlEffect, new TapSourceCost());
ability.addTarget(new TargetPermanent(new FilterCreaturePermanent("target creature"))); ability.addTarget(new TargetPermanent(new FilterCreaturePermanent("target creature")));
ability.addEffect(new MeriekeRiBeritCreateDelayedTriggerEffect()); ability.addEffect(new MeriekeRiBeritCreateDelayedTriggerEffect());
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.o; package mage.cards.o;
import java.util.UUID; import java.util.UUID;
@ -9,14 +8,15 @@ import mage.abilities.condition.common.SourceOnBattlefieldControlUnchangedCondit
import mage.abilities.decorator.ConditionalContinuousEffect; import mage.abilities.decorator.ConditionalContinuousEffect;
import mage.abilities.effects.common.continuous.AssignNoCombatDamageSourceEffect; import mage.abilities.effects.common.continuous.AssignNoCombatDamageSourceEffect;
import mage.abilities.effects.common.continuous.GainControlTargetEffect; import mage.abilities.effects.common.continuous.GainControlTargetEffect;
import mage.constants.SubType;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.SubType;
import mage.filter.common.FilterLandPermanent; import mage.filter.common.FilterLandPermanent;
import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate; import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -45,6 +45,7 @@ public final class OrcishSquatters extends CardImpl {
), true); ), true);
ability.addEffect(new AssignNoCombatDamageSourceEffect(Duration.EndOfTurn, true)); ability.addEffect(new AssignNoCombatDamageSourceEffect(Duration.EndOfTurn, true));
ability.addTarget(new TargetPermanent(filter)); ability.addTarget(new TargetPermanent(filter));
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.q; package mage.cards.q;
import java.util.UUID; import java.util.UUID;
@ -14,12 +13,13 @@ import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.common.FilterControlledArtifactPermanent; import mage.filter.common.FilterControlledArtifactPermanent;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.common.TargetAnyTarget; import mage.target.common.TargetAnyTarget;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -44,6 +44,7 @@ public final class QuicksmithRebel extends CardImpl {
"target artifact you control gains \"{T}: This artifact deals 2 damage to any target\" for as long as you control {this}"); "target artifact you control gains \"{T}: This artifact deals 2 damage to any target\" for as long as you control {this}");
Ability ability = new EntersBattlefieldTriggeredAbility(effect, false); Ability ability = new EntersBattlefieldTriggeredAbility(effect, false);
ability.addTarget(new TargetPermanent(new FilterControlledArtifactPermanent())); ability.addTarget(new TargetPermanent(new FilterControlledArtifactPermanent()));
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.q; package mage.cards.q;
import java.util.UUID; import java.util.UUID;
@ -14,11 +13,12 @@ import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.common.FilterControlledArtifactPermanent; import mage.filter.common.FilterControlledArtifactPermanent;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -42,6 +42,7 @@ public final class QuicksmithSpy extends CardImpl {
"target artifact you control gains \"{T}: Draw a card\" for as long as you control {this}"); "target artifact you control gains \"{T}: Draw a card\" for as long as you control {this}");
Ability ability = new EntersBattlefieldTriggeredAbility(effect, false); Ability ability = new EntersBattlefieldTriggeredAbility(effect, false);
ability.addTarget(new TargetPermanent(new FilterControlledArtifactPermanent())); ability.addTarget(new TargetPermanent(new FilterControlledArtifactPermanent()));
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.r; package mage.cards.r;
import java.util.UUID; import java.util.UUID;
@ -13,6 +12,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.*; import mage.constants.*;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -36,6 +36,7 @@ public final class RoilElemental extends CardImpl {
ConditionalContinuousEffect effect = new ConditionalContinuousEffect(new GainControlTargetEffect(Duration.Custom), new SourceOnBattlefieldControlUnchangedCondition(), rule); ConditionalContinuousEffect effect = new ConditionalContinuousEffect(new GainControlTargetEffect(Duration.Custom), new SourceOnBattlefieldControlUnchangedCondition(), rule);
Ability ability = new LandfallAbility(Zone.BATTLEFIELD, effect, true); Ability ability = new LandfallAbility(Zone.BATTLEFIELD, effect, true);
ability.addTarget(new TargetCreaturePermanent()); ability.addTarget(new TargetCreaturePermanent());
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,6 +1,6 @@
package mage.cards.t; package mage.cards.t;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -13,44 +13,51 @@ import mage.abilities.effects.common.continuous.GainControlTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.SubType;
import mage.game.Game; import mage.game.Game;
import mage.game.combat.CombatGroup; import mage.game.combat.CombatGroup;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import mage.watchers.common.BlockedAttackerWatcher; import mage.watchers.common.BlockedAttackerWatcher;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
* @author jeffwadsworth * @author jeffwadsworth
* *
5/1/2009 The ability grants you control of all creatures that are blocking it as the ability resolves. This will include * 5/1/2009 The ability grants you control of all creatures that are blocking it
* any creatures that were put onto the battlefield blocking it. * as the ability resolves. This will include any creatures that were put onto
5/1/2009 Any blocking creatures that regenerated during combat will have been removed from combat. Since such creatures * the battlefield blocking it. 5/1/2009 Any blocking creatures that regenerated
* are no longer in combat, they cannot be blocking The Wretched, which means you won't be able to gain control of them. * during combat will have been removed from combat. Since such creatures are no
5/1/2009 If The Wretched itself regenerated during combat, then it will have been removed from combat. Since it is no longer * longer in combat, they cannot be blocking The Wretched, which means you won't
* in combat, there cannot be any creatures blocking it, which means you won't be able to gain control of any creatures. * be able to gain control of them. 5/1/2009 If The Wretched itself regenerated
10/1/2009 The Wretched's ability triggers only if it's still on the battlefield when the end of combat step begins (after the * during combat, then it will have been removed from combat. Since it is no
* combat damage step). For example, if it's blocked by a 7/7 creature and is destroyed, its ability won't trigger at all. * longer in combat, there cannot be any creatures blocking it, which means you
10/1/2009 If The Wretched leaves the battlefield, you no longer control it, so the duration of its control-change effect ends. * won't be able to gain control of any creatures. 10/1/2009 The Wretched's
10/1/2009 If you lose control of The Wretched before its ability resolves, you won't gain control of the creatures blocking it at all. * ability triggers only if it's still on the battlefield when the end of combat
10/1/2009 Once the ability resolves, it doesn't care whether the permanents you gained control of remain creatures, only that * step begins (after the combat damage step). For example, if it's blocked by a
* they remain on the battlefield. * 7/7 creature and is destroyed, its ability won't trigger at all. 10/1/2009 If
* The Wretched leaves the battlefield, you no longer control it, so the
* duration of its control-change effect ends. 10/1/2009 If you lose control of
* The Wretched before its ability resolves, you won't gain control of the
* creatures blocking it at all. 10/1/2009 Once the ability resolves, it doesn't
* care whether the permanents you gained control of remain creatures, only that
* they remain on the battlefield.
*/ */
public final class TheWretched extends CardImpl { public final class TheWretched extends CardImpl {
public TheWretched(UUID ownerId, CardSetInfo setInfo) { public TheWretched(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{B}{B}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}{B}");
this.subtype.add(SubType.DEMON); this.subtype.add(SubType.DEMON);
this.power = new MageInt(2); this.power = new MageInt(2);
this.toughness = new MageInt(5); this.toughness = new MageInt(5);
// At end of combat, gain control of all creatures blocking The Wretched for as long as you control The Wretched. // At end of combat, gain control of all creatures blocking The Wretched for as long as you control The Wretched.
this.addAbility(new EndOfCombatTriggeredAbility(new TheWretchedEffect(), false), new BlockedAttackerWatcher()); Ability ability = new EndOfCombatTriggeredAbility(new TheWretchedEffect(), false);
this.addAbility(ability, new BlockedAttackerWatcher());
ability.addWatcher(new LostControlWatcher());
} }
public TheWretched(final TheWretched card) { public TheWretched(final TheWretched card) {
@ -83,16 +90,20 @@ class TheWretchedEffect extends OneShotEffect {
if (theWretched.isRemovedFromCombat() || !theWretched.isAttacking()) { if (theWretched.isRemovedFromCombat() || !theWretched.isAttacking()) {
return false; return false;
} }
if (!new SourceOnBattlefieldControlUnchangedCondition().apply(game, source)) { // Check if control of source has changed since ability triggered????? (does it work is it neccessary???)
Permanent permanent = game.getBattlefield().getPermanent(source.getSourceId());
if (permanent != null && !Objects.equals(permanent.getControllerId(), source.getControllerId())) {
return false; return false;
} }
for (CombatGroup combatGroup :game.getCombat().getGroups()) { for (CombatGroup combatGroup : game.getCombat().getGroups()) {
if (combatGroup.getAttackers().contains(source.getSourceId())) { if (combatGroup.getAttackers().contains(source.getSourceId())) {
for(UUID creatureId: combatGroup.getBlockers()) { for (UUID creatureId : combatGroup.getBlockers()) {
Permanent blocker = game.getPermanent(creatureId); Permanent blocker = game.getPermanent(creatureId);
if (blocker != null && blocker.getBlocking() > 0) { if (blocker != null && blocker.getBlocking() > 0) {
ContinuousEffect effect = new ConditionalContinuousEffect(new GainControlTargetEffect(Duration.Custom, source.getControllerId()), new SourceOnBattlefieldControlUnchangedCondition(), ""); ContinuousEffect effect = new ConditionalContinuousEffect(
new GainControlTargetEffect(Duration.Custom, source.getControllerId()),
new SourceOnBattlefieldControlUnchangedCondition(), "");
effect.setTargetPointer(new FixedTarget(blocker.getId())); effect.setTargetPointer(new FixedTarget(blocker.getId()));
game.addEffect(effect, source); game.addEffect(effect, source);

View file

@ -1,4 +1,3 @@
package mage.cards.t; package mage.cards.t;
import java.util.UUID; import java.util.UUID;
@ -19,6 +18,7 @@ import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -33,7 +33,7 @@ public final class ThrullChampion extends CardImpl {
} }
public ThrullChampion(UUID ownerId, CardSetInfo setInfo) { public ThrullChampion(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{B}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{B}");
this.subtype.add(SubType.THRULL); this.subtype.add(SubType.THRULL);
this.power = new MageInt(2); this.power = new MageInt(2);
this.toughness = new MageInt(2); this.toughness = new MageInt(2);
@ -47,6 +47,7 @@ public final class ThrullChampion extends CardImpl {
"Gain control of target Thrull for as long as you control {this}"); "Gain control of target Thrull for as long as you control {this}");
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, ThrullChampionGainControlEffect, new TapSourceCost()); Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, ThrullChampionGainControlEffect, new TapSourceCost());
ability.addTarget(new TargetPermanent(filter)); ability.addTarget(new TargetPermanent(filter));
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -1,4 +1,3 @@
package mage.cards.w; package mage.cards.w;
import java.util.UUID; import java.util.UUID;
@ -11,8 +10,8 @@ import mage.abilities.effects.common.continuous.GainControlTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
@ -20,6 +19,7 @@ import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -28,7 +28,7 @@ import mage.target.targetpointer.FixedTarget;
public final class Willbreaker extends CardImpl { public final class Willbreaker extends CardImpl {
public Willbreaker(UUID ownerId, CardSetInfo setInfo) { public Willbreaker(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{U}{U}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{U}");
this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.WIZARD); this.subtype.add(SubType.WIZARD);
this.power = new MageInt(2); this.power = new MageInt(2);
@ -37,7 +37,7 @@ public final class Willbreaker extends CardImpl {
// Whenever a creature an opponent controls becomes the target of a spell or ability you control, gain control of that creature for as long as you control Willbreaker. // Whenever a creature an opponent controls becomes the target of a spell or ability you control, gain control of that creature for as long as you control Willbreaker.
ConditionalContinuousEffect effect = new ConditionalContinuousEffect(new GainControlTargetEffect(Duration.Custom), new SourceOnBattlefieldControlUnchangedCondition(), null); ConditionalContinuousEffect effect = new ConditionalContinuousEffect(new GainControlTargetEffect(Duration.Custom), new SourceOnBattlefieldControlUnchangedCondition(), null);
effect.setText("gain control of that creature for as long as you control {this}"); effect.setText("gain control of that creature for as long as you control {this}");
this.addAbility(new WillbreakerTriggeredAbility(effect)); this.addAbility(new WillbreakerTriggeredAbility(effect), new LostControlWatcher());
} }
public Willbreaker(final Willbreaker card) { public Willbreaker(final Willbreaker card) {

View file

@ -1,4 +1,3 @@
package mage.cards.w; package mage.cards.w;
import java.util.UUID; import java.util.UUID;
@ -15,12 +14,13 @@ import mage.abilities.effects.common.continuous.GainControlTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.common.FilterCreaturePermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import mage.watchers.common.LostControlWatcher;
/** /**
* *
@ -35,7 +35,7 @@ public final class WillowSatyr extends CardImpl {
} }
public WillowSatyr(UUID ownerId, CardSetInfo setInfo) { public WillowSatyr(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{G}{G}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{G}");
this.subtype.add(SubType.SATYR); this.subtype.add(SubType.SATYR);
this.power = new MageInt(1); this.power = new MageInt(1);
this.toughness = new MageInt(1); this.toughness = new MageInt(1);
@ -48,6 +48,7 @@ public final class WillowSatyr extends CardImpl {
"Gain control of target legendary creature for as long as you control {this} and {this} remains tapped"); "Gain control of target legendary creature for as long as you control {this} and {this} remains tapped");
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, effect, new TapSourceCost()); Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, effect, new TapSourceCost());
ability.addTarget(new TargetCreaturePermanent(filter)); ability.addTarget(new TargetCreaturePermanent(filter));
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -14,6 +14,8 @@ import mage.game.FreeForAll;
import mage.game.Game; import mage.game.Game;
import mage.game.GameException; import mage.game.GameException;
import mage.game.mulligan.MulliganType; import mage.game.mulligan.MulliganType;
import mage.game.permanent.Permanent;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestMultiPlayerBase; import org.mage.test.serverside.base.CardTestMultiPlayerBase;
@ -34,6 +36,39 @@ public class GoadTest extends CardTestMultiPlayerBase {
return game; return game;
} }
@Test
public void goadWithOwnedCreatureTest() {
// Your opponents can't cast spells during combat.
// Whenever a creature you control deals combat damage to a player, goad each creature that player controls
// (Until your next turn, that creature attacks each combat if able and attacks a player other than you if able.)
addCard(Zone.BATTLEFIELD, playerD, "Marisi, Breaker of the Coil", 1); // Creature 5/4
addCard(Zone.BATTLEFIELD, playerC, "Abbey Griffin", 3); // Creature 2/2
attack(2, playerD, "Marisi, Breaker of the Coil", playerC);
setStopAt(3, PhaseStep.BEGIN_COMBAT);
setStrictChooseMode(true);
execute();
assertAllCommandsUsed();
Permanent griffinPermanent = getPermanent("Abbey Griffin");
Assert.assertFalse("Griffin can attack playerD but should not be able",
griffinPermanent.canAttack(playerD.getId(), currentGame));
Assert.assertTrue("Griffin can't attack playerA but should be able",
griffinPermanent.canAttack(playerA.getId(), currentGame));
Assert.assertTrue("Griffin can't attack playerB but should be able",
griffinPermanent.canAttack(playerB.getId(), currentGame));
assertLife(playerC, 35);
assertLife(playerD, 40); // player D can not be attacked from C because the creatures are goaded
assertLife(playerA, 40);
assertLife(playerB, 40);
}
/** /**
* In a game of commander, my opponent gained control of Marisi, Breaker of * In a game of commander, my opponent gained control of Marisi, Breaker of
* Coils (until end of turn) and did combat damage to another player. This * Coils (until end of turn) and did combat damage to another player. This
@ -54,28 +89,32 @@ public class GoadTest extends CardTestMultiPlayerBase {
addCard(Zone.HAND, playerD, "Ray of Command"); // Instant {3}{U} addCard(Zone.HAND, playerD, "Ray of Command"); // Instant {3}{U}
addCard(Zone.BATTLEFIELD, playerD, "Island", 4); addCard(Zone.BATTLEFIELD, playerD, "Island", 4);
addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion", 3); // Creature 2/2 addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion", 1); // Creature 2/2
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Ray of Command", "Marisi, Breaker of the Coil"); castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Ray of Command", "Marisi, Breaker of the Coil");
attack(2, playerD, "Marisi, Breaker of the Coil", playerC); attack(2, playerD, "Marisi, Breaker of the Coil", playerC);
attack(3, playerC, "Silvercoat Lion", playerA); setStopAt(3, PhaseStep.BEGIN_COMBAT);
attack(3, playerC, "Silvercoat Lion", playerB); setStrictChooseMode(true);
attack(3, playerC, "Silvercoat Lion", playerD);
setStopAt(4, PhaseStep.BEGIN_COMBAT);
execute(); execute();
assertAllCommandsUsed();
assertGraveyardCount(playerD, "Ray of Command", 1); assertGraveyardCount(playerD, "Ray of Command", 1);
assertPermanentCount(playerA, "Marisi, Breaker of the Coil", 1); assertPermanentCount(playerA, "Marisi, Breaker of the Coil", 1);
assertLife(playerC, 35); Permanent lion = getPermanent("Silvercoat Lion", playerC);
Assert.assertFalse("Silvercoat lion shouldn't be able to attack player D but can", lion.canAttack(playerD.getId(), currentGame));
Assert.assertTrue("Silvercoat lion should be able to attack player A but can't", lion.canAttack(playerA.getId(), currentGame));
Assert.assertTrue("Silvercoat lion should be able to attack player B but can't", lion.canAttack(playerB.getId(), currentGame));
assertLife(playerD, 40);
assertLife(playerC, 35);
assertLife(playerA, 40);
assertLife(playerB, 40);
assertLife(playerB, 38);
assertLife(playerA, 38);
assertLife(playerD, 38);
} }
} }

View file

@ -26,7 +26,7 @@ public class TheWretchedTest extends CardTestPlayerBase {
attack(3, playerA, "The Wretched"); attack(3, playerA, "The Wretched");
block(3, playerB, "Wall of Pine Needles", "The Wretched"); block(3, playerB, "Wall of Pine Needles", "The Wretched");
block(3, playerB, "Living Wall", "The Wretched"); block(3, playerB, "Living Wall", "The Wretched");
setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); setStopAt(3, PhaseStep.END_TURN);
execute(); execute();
assertPermanentCount(playerA, "The Wretched", 1); assertPermanentCount(playerA, "The Wretched", 1);
@ -60,12 +60,11 @@ public class TheWretchedTest extends CardTestPlayerBase {
assertPermanentCount(playerB, "Wall of Pine Needles", 1); assertPermanentCount(playerB, "Wall of Pine Needles", 1);
} }
@Test @Test
public void testLoseControlOfTheWretched() { public void testLoseControlOfTheWretched() {
// At end of combat, gain control of all creatures blocking The Wretched for as long as you control The Wretched.
addCard(Zone.BATTLEFIELD, playerA, "The Wretched"); addCard(Zone.BATTLEFIELD, playerA, "The Wretched");
addCard(Zone.BATTLEFIELD, playerB, "Wall of Pine Needles"); // a 3/3 with regeneration addCard(Zone.BATTLEFIELD, playerB, "Wall of Pine Needles"); // a 3/3 with regeneration
@ -83,6 +82,7 @@ public class TheWretchedTest extends CardTestPlayerBase {
execute(); execute();
assertPermanentCount(playerB, "The Wretched", 1); assertPermanentCount(playerB, "The Wretched", 1);
assertPermanentCount(playerA, "Wall of Pine Needles", 0);
assertPermanentCount(playerB, "Wall of Pine Needles", 1); assertPermanentCount(playerB, "Wall of Pine Needles", 1);
assertPermanentCount(playerB, "Living Wall", 1); assertPermanentCount(playerB, "Living Wall", 1);
} }
@ -97,7 +97,6 @@ public class TheWretchedTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerB, "Wall of Pine Needles"); // a 3/3 with regeneration addCard(Zone.BATTLEFIELD, playerB, "Wall of Pine Needles"); // a 3/3 with regeneration
addCard(Zone.BATTLEFIELD, playerB, "Wall of Spears"); // 3/2 addCard(Zone.BATTLEFIELD, playerB, "Wall of Spears"); // 3/2
attack(3, playerA, "The Wretched"); attack(3, playerA, "The Wretched");
block(3, playerB, "Wall of Pine Needles", "The Wretched"); block(3, playerB, "Wall of Pine Needles", "The Wretched");
block(3, playerB, "Wall of Spears", "The Wretched"); block(3, playerB, "Wall of Spears", "The Wretched");

View file

@ -1,9 +1,9 @@
package org.mage.test.cards.copy; package org.mage.test.cards.copy;
import mage.abilities.keyword.DeathtouchAbility; import mage.abilities.keyword.DeathtouchAbility;
import mage.constants.SubType;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import org.junit.Assert; import org.junit.Assert;
@ -14,12 +14,11 @@ import org.mage.test.serverside.base.CardTestPlayerBase;
* *
* Lazav, Dimir Mastermind * Lazav, Dimir Mastermind
* *
* Legendary Creature Shapeshifter 3/3, UUBB * Legendary Creature Shapeshifter 3/3, UUBB Hexproof Whenever a creature card
* Hexproof * is put into an opponent's graveyard from anywhere, you may have Lazav, Dimir
* Whenever a creature card is put into an opponent's graveyard from anywhere, you may have * Mastermind become a copy of that card except its name is still Lazav, Dimir
* Lazav, Dimir Mastermind become a copy of that card except its name is still * Mastermind, it's legendary in addition to its other types, and it gains
* Lazav, Dimir Mastermind, it's legendary in addition to its other types, and * hexproof and this ability.
* it gains hexproof and this ability.
* *
* @author LevelX2 * @author LevelX2
*/ */
@ -32,25 +31,26 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
public void testCopySimpleCreature() { public void testCopySimpleCreature() {
addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1);
// Codex Shredder - Artifact // Codex Shredder - Artifact
// {T}: Target player mills a card. // {T}: Target player puts the top card of their library into their graveyard.
// {5}, {T}, Sacrifice Codex Shredder: Return target card from your graveyard to your hand. // {5}, {T}, Sacrifice Codex Shredder: Return target card from your graveyard to your hand.
addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1);
// Flying 3/2 // Flying 3/2
addCard(Zone.LIBRARY, playerB, "Assault Griffin",5); addCard(Zone.LIBRARY, playerB, "Assault Griffin", 5);
skipInitShuffling(); skipInitShuffling();
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player mills a card.", playerB); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player puts the top card", playerB);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Lazav, Dimir Mastermind", 1); assertPermanentCount(playerA, "Lazav, Dimir Mastermind", 1);
assertPowerToughness(playerA, "Lazav, Dimir Mastermind", 3, 2); assertPowerToughness(playerA, "Lazav, Dimir Mastermind", 3, 2);
Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId()); Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId());
Assert.assertTrue(lazav.getSubtype(currentGame).contains(SubType.GRIFFIN)); Assert.assertTrue(lazav.getSubtype(currentGame).contains(SubType.GRIFFIN));
Assert.assertTrue("Lazav, Dimir Mastermind must have flying",lazav.getAbilities().contains(FlyingAbility.getInstance())); Assert.assertTrue("Lazav, Dimir Mastermind must have flying", lazav.getAbilities().contains(FlyingAbility.getInstance()));
} }
/** /**
@ -64,10 +64,10 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
// Whenever another nontoken creature dies, you may put a 1/1 black Rat creature token onto the battlefield. // Whenever another nontoken creature dies, you may put a 1/1 black Rat creature token onto the battlefield.
// Rats you control have deathtouch. // Rats you control have deathtouch.
addCard(Zone.LIBRARY, playerB, "Ogre Slumlord",5); addCard(Zone.LIBRARY, playerB, "Ogre Slumlord", 5);
skipInitShuffling(); skipInitShuffling();
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player mills a card.", playerB); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player puts the top card", playerB);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
execute(); execute();
@ -86,11 +86,10 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
/** /**
* Tests copy Nightveil Specter * Tests copy Nightveil Specter
* *
* Nightveil Specter * Nightveil Specter Creature Specter 2/3, {U/B}{U/B}{U/B} Flying Whenever
* Creature Specter 2/3, {U/B}{U/B}{U/B} * Nightveil Specter deals combat damage to a player, that player exiles the
* Flying * top card of their library. You may play cards exiled with Nightveil
* Whenever Nightveil Specter deals combat damage to a player, that player exiles the top card of their library. * Specter.
* You may play cards exiled with Nightveil Specter.
* *
*/ */
@Test @Test
@ -99,11 +98,11 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1);
addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1);
addCard(Zone.LIBRARY, playerB, "Silvercoat Lion",2); addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 2);
addCard(Zone.LIBRARY, playerB, "Nightveil Specter",1); addCard(Zone.LIBRARY, playerB, "Nightveil Specter", 1);
skipInitShuffling(); skipInitShuffling();
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player mills a card.", playerB); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player puts the top card", playerB);
attack(3, playerA, "Lazav, Dimir Mastermind"); attack(3, playerA, "Lazav, Dimir Mastermind");
@ -130,15 +129,15 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1);
addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1);
addCard(Zone.LIBRARY, playerB, "Silvercoat Lion",2); addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 2);
addCard(Zone.LIBRARY, playerB, "Nightveil Specter",1); addCard(Zone.LIBRARY, playerB, "Nightveil Specter", 1);
skipInitShuffling(); skipInitShuffling();
// Lazav becomes a Nightveil Specter // Lazav becomes a Nightveil Specter
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player mills a card.", playerB); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player puts the top card", playerB);
// Lazav becomes a Silvercoat Lion // Lazav becomes a Silvercoat Lion
activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player mills a card.", playerB); activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player puts the top card", playerB);
setStopAt(3, PhaseStep.END_TURN); setStopAt(3, PhaseStep.END_TURN);
execute(); execute();
@ -151,29 +150,30 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
Assert.assertTrue(lazav.isLegendary()); Assert.assertTrue(lazav.isLegendary());
} }
/** /**
* Tests old copy is discarded after reanmiation of Lazav * Tests old copy is discarded after reanmiation of Lazav
*/ */
@Test @Test
public void testCopyAfterReanimation() { public void testCopyAfterReanimation() {
addCard(Zone.BATTLEFIELD, playerA ,"Swamp"); addCard(Zone.BATTLEFIELD, playerA, "Swamp");
// Put target creature card from a graveyard onto the battlefield under your control. You lose life equal to its converted mana cost. // Put target creature card from a graveyard onto the battlefield under your control. You lose life equal to its converted mana cost.
addCard(Zone.HAND, playerA ,"Reanimate"); addCard(Zone.HAND, playerA, "Reanimate");
addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1);
// Codex Shredder - Artifact // Codex Shredder - Artifact
// {T}: Target player mills a card. // {T}: Target player puts the top card of their library into their graveyard.
// {5}, {T}, Sacrifice Codex Shredder: Return target card from your graveyard to your hand. // {5}, {T}, Sacrifice Codex Shredder: Return target card from your graveyard to your hand.
addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1);
addCard(Zone.BATTLEFIELD, playerB ,"Swamp", 3); addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3);
// Flying 3/2 // Flying 3/2
addCard(Zone.LIBRARY, playerB, "Assault Griffin",1); addCard(Zone.LIBRARY, playerB, "Assault Griffin", 1);
// Target opponent sacrifices a creature. You gain life equal to that creature's toughness. // Target opponent sacrifices a creature. You gain life equal to that creature's toughness.
addCard(Zone.HAND, playerB ,"Tribute to Hunger"); addCard(Zone.HAND, playerB, "Tribute to Hunger");
skipInitShuffling(); skipInitShuffling();
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player mills a card.", playerB); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player puts the top card", playerB);
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Tribute to Hunger"); castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Tribute to Hunger");
@ -192,8 +192,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
assertPowerToughness(playerA, "Lazav, Dimir Mastermind", 3, 3); assertPowerToughness(playerA, "Lazav, Dimir Mastermind", 3, 3);
Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId()); Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId());
Assert.assertFalse(lazav.getSubtype(currentGame).contains(SubType.GRIFFIN)); // no Griffin type Assert.assertFalse(lazav.getSubtype(currentGame).contains(SubType.GRIFFIN)); // no Griffin type
Assert.assertFalse("Lazav, Dimir Mastermind must have flying",lazav.getAbilities().contains(FlyingAbility.getInstance())); Assert.assertFalse("Lazav, Dimir Mastermind must have flying", lazav.getAbilities().contains(FlyingAbility.getInstance()));
} }
@ -204,7 +203,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
public void testCopyCreatureExiled() { public void testCopyCreatureExiled() {
addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1);
// Codex Shredder - Artifact // Codex Shredder - Artifact
// {T}: Target player mills a card. // {T}: Target player puts the top card of their library into their graveyard.
// {5}, {T}, Sacrifice Codex Shredder: Return target card from your graveyard to your hand. // {5}, {T}, Sacrifice Codex Shredder: Return target card from your graveyard to your hand.
addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1);
@ -213,10 +212,10 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
addCard(Zone.HAND, playerA, "Rest in Peace", 1); addCard(Zone.HAND, playerA, "Rest in Peace", 1);
// Flying 3/2 // Flying 3/2
addCard(Zone.LIBRARY, playerB, "Assault Griffin",5); addCard(Zone.LIBRARY, playerB, "Assault Griffin", 5);
skipInitShuffling(); skipInitShuffling();
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player mills a card.", playerB); activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Target player puts the top card", playerB);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rest in Peace"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Rest in Peace");
@ -228,6 +227,6 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId()); Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId());
Assert.assertTrue(lazav.getSubtype(currentGame).contains(SubType.GRIFFIN)); Assert.assertTrue(lazav.getSubtype(currentGame).contains(SubType.GRIFFIN));
Assert.assertTrue("Lazav, Dimir Mastermind must have flying",lazav.getAbilities().contains(FlyingAbility.getInstance())); Assert.assertTrue("Lazav, Dimir Mastermind must have flying", lazav.getAbilities().contains(FlyingAbility.getInstance()));
} }
} }

View file

@ -1,5 +1,10 @@
package org.mage.test.player; package org.mage.test.player;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import mage.MageItem; import mage.MageItem;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
@ -57,13 +62,6 @@ import mage.util.CardUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Ignore; import org.junit.Ignore;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*;
/** /**
@ -1446,7 +1444,7 @@ public class TestPlayer implements Player {
UUID defenderId = null; UUID defenderId = null;
boolean mustAttackByAction = false; boolean mustAttackByAction = false;
boolean madeAttackByAction = false; boolean madeAttackByAction = false;
for (Iterator<org.mage.test.player.PlayerAction> it = actions.iterator(); it.hasNext(); ) { for (Iterator<org.mage.test.player.PlayerAction> it = actions.iterator(); it.hasNext();) {
PlayerAction action = it.next(); PlayerAction action = it.next();
// aiXXX commands // aiXXX commands

View file

@ -1,31 +1,43 @@
package mage.abilities.condition.common; package mage.abilities.condition.common;
import java.util.Objects;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.watchers.common.LostControlWatcher;
/** /**
* This condition remembers controller on the first apply. * This condition checks if ever since first call of the apply method the
* As long as this controller keeps unchanged and the source is * controller of the source has changed
* on the battlefield, the condition is true. *
* Monitoring the LOST_CONTROL event has the advantage that also all layered
* effects can correctly check for controller change because comparing old and
* new controller during their apply time does not take into account layered
* cahnge control effects that will be applied later.
*
* This condition needs the LostControlWatcher, so be sure to add it to the card
* that uses the condition
* *
* @author LevelX2 * @author LevelX2
*/ */
public class SourceOnBattlefieldControlUnchangedCondition implements Condition { public class SourceOnBattlefieldControlUnchangedCondition implements Condition {
private UUID controllerId; private Long checkingSince;
private int startingZoneChangeCounter;
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
if (controllerId == null) { if (checkingSince == null) {
controllerId = source.getControllerId(); checkingSince = System.currentTimeMillis() - 1;
startingZoneChangeCounter = game.getState().getZoneChangeCounter(source.getSourceId());
} }
Permanent permanent = game.getBattlefield().getPermanent(source.getSourceId()); if (game.getState().getZoneChangeCounter(source.getSourceId()) > startingZoneChangeCounter) {
return (permanent != null && Objects.equals(controllerId, source.getControllerId())); return false;
}
LostControlWatcher watcher = game.getState().getWatcher(LostControlWatcher.class);
if (watcher != null) {
return checkingSince > watcher.getOrderOfLastLostControl(source.getSourceId());
}
throw new UnsupportedOperationException("LostControlWatcher not found!");
} }
} }

View file

@ -1,5 +1,9 @@
package mage.abilities.effects; package mage.abilities.effects;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.constants.DependencyType; import mage.constants.DependencyType;
@ -9,11 +13,6 @@ import mage.constants.SubLayer;
import mage.game.Game; import mage.game.Game;
import mage.target.targetpointer.TargetPointer; import mage.target.targetpointer.TargetPointer;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -67,8 +66,6 @@ public interface ContinuousEffect extends Effect {
UUID getStartingController(); UUID getStartingController();
void incYourTurnNumPlayed();
boolean isYourNextTurn(Game game); boolean isYourNextTurn(Game game);
@Override @Override

View file

@ -1,5 +1,6 @@
package mage.abilities.effects; package mage.abilities.effects;
import java.util.*;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.CompoundAbility; import mage.abilities.CompoundAbility;
@ -19,8 +20,6 @@ import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.target.targetpointer.TargetPointer; import mage.target.targetpointer.TargetPointer;
import java.util.*;
/** /**
* @author BetaSteward_at_googlemail.com, JayDi85 * @author BetaSteward_at_googlemail.com, JayDi85
*/ */
@ -47,9 +46,9 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
protected boolean characterDefining = false; protected boolean characterDefining = false;
// until your next turn or until end of your next turn // until your next turn or until end of your next turn
private UUID startingControllerId; // player to checkss turns (can't different with real controller ability) private UUID startingControllerId; // player to check for turn duration (can't different with real controller ability)
private boolean startingTurnWasActive; private boolean startingTurnWasActive; // effect started during related players turn and related players turn was already active
private int yourTurnNumPlayed = 0; // turnes played after effect was created private int effectStartingOnTurn = 0; // turn the effect started
public ContinuousEffectImpl(Duration duration, Outcome outcome) { public ContinuousEffectImpl(Duration duration, Outcome outcome) {
super(outcome); super(outcome);
@ -79,7 +78,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
this.temporary = effect.temporary; this.temporary = effect.temporary;
this.startingControllerId = effect.startingControllerId; this.startingControllerId = effect.startingControllerId;
this.startingTurnWasActive = effect.startingTurnWasActive; this.startingTurnWasActive = effect.startingTurnWasActive;
this.yourTurnNumPlayed = effect.yourTurnNumPlayed; this.effectStartingOnTurn = effect.effectStartingOnTurn;
this.dependencyTypes = effect.dependencyTypes; this.dependencyTypes = effect.dependencyTypes;
this.dependendToTypes = effect.dependendToTypes; this.dependendToTypes = effect.dependendToTypes;
this.characterDefining = effect.characterDefining; this.characterDefining = effect.characterDefining;
@ -191,23 +190,13 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
this.startingControllerId = startingController; this.startingControllerId = startingController;
this.startingTurnWasActive = activePlayerId != null this.startingTurnWasActive = activePlayerId != null
&& activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too && activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too
this.yourTurnNumPlayed = 0; this.effectStartingOnTurn = game.getTurnNum();
}
@Override
public void incYourTurnNumPlayed() {
yourTurnNumPlayed++;
} }
@Override @Override
public boolean isYourNextTurn(Game game) { public boolean isYourNextTurn(Game game) {
if (this.startingTurnWasActive) { return effectStartingOnTurn < game.getTurnNum()
return yourTurnNumPlayed == 1
&& game.isActivePlayer(startingControllerId); && game.isActivePlayer(startingControllerId);
} else {
return yourTurnNumPlayed == 0
&& game.isActivePlayer(startingControllerId);
}
} }
@Override @Override
@ -367,6 +356,9 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
/** /**
* Auto-generates dependencies on different effects (what's apply first and * Auto-generates dependencies on different effects (what's apply first and
* what's apply second) * what's apply second)
*
* @param abilityToGain
* @param filterToSearch
*/ */
public void generateGainAbilityDependencies(Ability abilityToGain, Filter filterToSearch) { public void generateGainAbilityDependencies(Ability abilityToGain, Filter filterToSearch) {
this.addDependencyType(DependencyType.AddingAbility); this.addDependencyType(DependencyType.AddingAbility);

View file

@ -1,5 +1,9 @@
package mage.abilities.effects; package mage.abilities.effects;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
@ -26,11 +30,6 @@ import mage.target.common.TargetCardInHand;
import mage.util.CardUtil; import mage.util.CardUtil;
import org.apache.log4j.Logger; 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 * @author BetaSteward_at_googlemail.com
*/ */
@ -163,28 +162,17 @@ public class ContinuousEffects implements Serializable {
spliceCardEffects.removeInactiveEffects(game); spliceCardEffects.removeInactiveEffects(game);
} }
public synchronized void incYourTurnNumPlayed(Game game) {
layeredEffects.incYourTurnNumPlayed(game);
continuousRuleModifyingEffects.incYourTurnNumPlayed(game);
replacementEffects.incYourTurnNumPlayed(game);
preventionEffects.incYourTurnNumPlayed(game);
requirementEffects.incYourTurnNumPlayed(game);
restrictionEffects.incYourTurnNumPlayed(game);
for (ContinuousEffectsList asThoughtlist : asThoughEffectsMap.values()) {
asThoughtlist.incYourTurnNumPlayed(game);
}
costModificationEffects.incYourTurnNumPlayed(game);
spliceCardEffects.incYourTurnNumPlayed(game);
}
public synchronized List<ContinuousEffect> getLayeredEffects(Game game) { public synchronized List<ContinuousEffect> getLayeredEffects(Game game) {
return getLayeredEffects(game, "main"); return getLayeredEffects(game, "main");
} }
/** /**
* Return effects list ordered by timestamps (timestamps are automaticity generates from new/old lists on same layer) * Return effects list ordered by timestamps (timestamps are automaticity
* generates from new/old lists on same layer)
* *
* @param timestampGroupName workaround to fix broken timestamps on effect's add/remove between different layers * @param game
* @param timestampGroupName workaround to fix broken timestamps on effect's
* add/remove between different layers
* @return effects list ordered by timestamp * @return effects list ordered by timestamp
*/ */
public synchronized List<ContinuousEffect> getLayeredEffects(Game game, String timestampGroupName) { public synchronized List<ContinuousEffect> getLayeredEffects(Game game, String timestampGroupName) {
@ -229,8 +217,10 @@ public class ContinuousEffects implements Serializable {
* "actual" meaning it becomes turned on that is defined by * "actual" meaning it becomes turned on that is defined by
* Ability.#isInUseableZone(Game, boolean) method in * Ability.#isInUseableZone(Game, boolean) method in
* #getLayeredEffects(Game). * #getLayeredEffects(Game).
* <p> *
* It must be called with different timestamp group name (otherwise sort order will be changed for add/remove effects, see Urborg and Bloodmoon test) * It must be called with different timestamp group name (otherwise sort
* order will be changed for add/remove effects, see Urborg and Bloodmoon
* test)
* *
* @param layerEffects * @param layerEffects
*/ */
@ -359,7 +349,7 @@ public class ContinuousEffects implements Serializable {
} }
// boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT); // boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT);
//get all applicable transient Replacement effects //get all applicable transient Replacement effects
for (Iterator<ReplacementEffect> iterator = replacementEffects.iterator(); iterator.hasNext(); ) { for (Iterator<ReplacementEffect> iterator = replacementEffects.iterator(); iterator.hasNext();) {
ReplacementEffect effect = iterator.next(); ReplacementEffect effect = iterator.next();
if (!effect.checksEventType(event, game)) { if (!effect.checksEventType(event, game)) {
continue; continue;
@ -392,7 +382,7 @@ public class ContinuousEffects implements Serializable {
} }
} }
for (Iterator<PreventionEffect> iterator = preventionEffects.iterator(); iterator.hasNext(); ) { for (Iterator<PreventionEffect> iterator = preventionEffects.iterator(); iterator.hasNext();) {
PreventionEffect effect = iterator.next(); PreventionEffect effect = iterator.next();
if (!effect.checksEventType(event, game)) { if (!effect.checksEventType(event, game)) {
continue; continue;
@ -817,7 +807,7 @@ public class ContinuousEffects implements Serializable {
do { do {
Map<ReplacementEffect, Set<Ability>> rEffects = getApplicableReplacementEffects(event, game); Map<ReplacementEffect, Set<Ability>> rEffects = getApplicableReplacementEffects(event, game);
// Remove all consumed effects (ability dependant) // Remove all consumed effects (ability dependant)
for (Iterator<ReplacementEffect> it1 = rEffects.keySet().iterator(); it1.hasNext(); ) { for (Iterator<ReplacementEffect> it1 = rEffects.keySet().iterator(); it1.hasNext();) {
ReplacementEffect entry = it1.next(); ReplacementEffect entry = it1.next();
if (consumed.containsKey(entry.getId()) /*&& !(entry instanceof CommanderReplacementEffect) */) { // 903.9. if (consumed.containsKey(entry.getId()) /*&& !(entry instanceof CommanderReplacementEffect) */) { // 903.9.
Set<UUID> consumedAbilitiesIds = consumed.get(entry.getId()); Set<UUID> consumedAbilitiesIds = consumed.get(entry.getId());
@ -1036,7 +1026,7 @@ public class ContinuousEffects implements Serializable {
continue; continue;
} }
// check if waiting effects can be applied now // check if waiting effects can be applied now
for (Iterator<Map.Entry<ContinuousEffect, Set<UUID>>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext(); ) { for (Iterator<Map.Entry<ContinuousEffect, Set<UUID>>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<ContinuousEffect, Set<UUID>> entry = iterator.next(); Map.Entry<ContinuousEffect, Set<UUID>> entry = iterator.next();
if (!appliedEffects.containsAll(entry.getValue())) { // all dependent to effects are applied now so apply the effect itself if (!appliedEffects.containsAll(entry.getValue())) { // all dependent to effects are applied now so apply the effect itself
continue; continue;
@ -1283,7 +1273,8 @@ public class ContinuousEffects implements Serializable {
} }
private void setControllerForEffect(ContinuousEffectsList<?> effects, UUID sourceId, UUID controllerId) { private void setControllerForEffect(ContinuousEffectsList<?> effects, UUID sourceId, UUID controllerId) {
for (Effect effect : effects) { for (ContinuousEffect effect : effects) {
if (!effect.getDuration().isFixedController()) {
Set<Ability> abilities = effects.getAbility(effect.getId()); Set<Ability> abilities = effects.getAbility(effect.getId());
for (Ability ability : abilities) { for (Ability ability : abilities) {
if (ability.getSourceId() != null) { if (ability.getSourceId() != null) {
@ -1294,7 +1285,7 @@ public class ContinuousEffects implements Serializable {
logger.fatal("Continuous effect for ability with no sourceId Ability: " + ability); logger.fatal("Continuous effect for ability with no sourceId Ability: " + ability);
} }
} }
}
} }
} }

View file

@ -1,5 +1,6 @@
package mage.abilities.effects; package mage.abilities.effects;
import java.util.*;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.MageSingleton; import mage.abilities.MageSingleton;
@ -10,8 +11,6 @@ import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.*;
/** /**
* @param <T> * @param <T>
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -47,7 +46,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
public void removeEndOfTurnEffects(Game game) { public void removeEndOfTurnEffects(Game game) {
// calls every turn on cleanup step (only end of turn duration) // calls every turn on cleanup step (only end of turn duration)
// rules 514.2 // rules 514.2
for (Iterator<T> i = this.iterator(); i.hasNext(); ) { for (Iterator<T> i = this.iterator(); i.hasNext();) {
T entry = i.next(); T entry = i.next();
boolean canRemove = false; boolean canRemove = false;
switch (entry.getDuration()) { switch (entry.getDuration()) {
@ -67,7 +66,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
public void removeEndOfCombatEffects() { public void removeEndOfCombatEffects() {
for (Iterator<T> i = this.iterator(); i.hasNext(); ) { for (Iterator<T> i = this.iterator(); i.hasNext();) {
T entry = i.next(); T entry = i.next();
if (entry.getDuration() == Duration.EndOfCombat) { if (entry.getDuration() == Duration.EndOfCombat) {
i.remove(); i.remove();
@ -77,7 +76,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
} }
public void removeInactiveEffects(Game game) { public void removeInactiveEffects(Game game) {
for (Iterator<T> i = this.iterator(); i.hasNext(); ) { for (Iterator<T> i = this.iterator(); i.hasNext();) {
T entry = i.next(); T entry = i.next();
if (isInactive(entry, game)) { if (isInactive(entry, game)) {
i.remove(); i.remove();
@ -86,15 +85,6 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
} }
} }
public void incYourTurnNumPlayed(Game game) {
for (Iterator<T> i = this.iterator(); i.hasNext(); ) {
T entry = i.next();
if (game.isActivePlayer(entry.getStartingController())) {
entry.incYourTurnNumPlayed();
}
}
}
private boolean isInactive(T effect, Game game) { private boolean isInactive(T effect, Game game) {
// ends all inactive effects -- calls on player leave or apply new effect // ends all inactive effects -- calls on player leave or apply new effect
if (game.getState().isGameOver()) { if (game.getState().isGameOver()) {
@ -111,7 +101,6 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
order whos still in the game. order whos still in the game.
*/ */
// objects removes doing in player.leave() call... effects removes is here // objects removes doing in player.leave() call... effects removes is here
Set<Ability> set = effectAbilityMap.get(effect.getId()); Set<Ability> set = effectAbilityMap.get(effect.getId());
if (set == null) { if (set == null) {
logger.debug("No abilities for effect found: " + effect.toString()); logger.debug("No abilities for effect found: " + effect.toString());
@ -224,7 +213,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
abilities.removeAll(abilitiesToRemove); abilities.removeAll(abilitiesToRemove);
} }
if (abilities == null || abilities.isEmpty()) { if (abilities == null || abilities.isEmpty()) {
for (Iterator<T> iterator = this.iterator(); iterator.hasNext(); ) { for (Iterator<T> iterator = this.iterator(); iterator.hasNext();) {
ContinuousEffect effect = iterator.next(); ContinuousEffect effect = iterator.next();
if (effect.getId().equals(effectIdToRemove)) { if (effect.getId().equals(effectIdToRemove)) {
iterator.remove(); iterator.remove();

View file

@ -1,32 +1,23 @@
package mage.abilities.effects.common.combat; package mage.abilities.effects.common.combat;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.RestrictionEffect; import mage.abilities.effects.RestrictionEffect;
import mage.constants.Duration; import mage.constants.Duration;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import java.util.UUID;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
public class CantAttackYouEffect extends RestrictionEffect { public class CantAttackYouEffect extends RestrictionEffect {
UUID controllerId;
public CantAttackYouEffect(Duration duration) { public CantAttackYouEffect(Duration duration) {
super(duration); super(duration);
} }
public CantAttackYouEffect(Duration duration, UUID controllerId) {
super(duration);
this.controllerId = controllerId;
}
public CantAttackYouEffect(final CantAttackYouEffect effect) { public CantAttackYouEffect(final CantAttackYouEffect effect) {
super(effect); super(effect);
this.controllerId = effect.controllerId;
} }
@Override @Override
@ -44,9 +35,6 @@ public class CantAttackYouEffect extends RestrictionEffect {
if (defenderId == null) { if (defenderId == null) {
return true; return true;
} }
if (controllerId == null) { return !defenderId.equals(source.getControllerId());
controllerId = source.getControllerId();
}
return !defenderId.equals(controllerId);
} }
} }

View file

@ -41,18 +41,18 @@ public class GoadTargetEffect extends OneShotEffect {
Permanent targetCreature = game.getPermanent(getTargetPointer().getFirst(game, source)); Permanent targetCreature = game.getPermanent(getTargetPointer().getFirst(game, source));
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (targetCreature != null && controller != null) { if (targetCreature != null && controller != null) {
// TODO: Allow goad to target controller, current AttacksIfAbleTargetEffect is not support it // TODO: Allow goad to target controller, current AttacksIfAbleTargetEffect does not support it
// https://github.com/magefree/mage/issues/5283 // https://github.com/magefree/mage/issues/5283
/* /*
If the creature doesnt meet any of the above exceptions and can attack, it must attack a player other than If the creature doesnt meet any of the above exceptions and can attack, it must attack a player other than
the controller of the spell or ability that goaded it if able. It the creature cant attack any of those the controller of the spell or ability that goaded it if able. If the creature cant attack any of those
players but could otherwise attack, it must attack an opposing planeswalker (controlled by any opponent) players but could otherwise attack, it must attack an opposing planeswalker (controlled by any opponent)
or the player that goaded it. (2016-08-23) or the player that goaded it. (2016-08-23)
*/ */
ContinuousEffect effect = new AttacksIfAbleTargetEffect(Duration.UntilYourNextTurn); ContinuousEffect effect = new AttacksIfAbleTargetEffect(Duration.UntilYourNextTurn);
effect.setTargetPointer(new FixedTarget(getTargetPointer().getFirst(game, source))); effect.setTargetPointer(new FixedTarget(getTargetPointer().getFirst(game, source)));
game.addEffect(effect, source); game.addEffect(effect, source);
effect = new CantAttackYouEffect(Duration.UntilYourNextTurn, source.getControllerId()); // remember current controller effect = new CantAttackYouEffect(Duration.UntilYourNextTurn); // remember current controller
effect.setTargetPointer(new FixedTarget(getTargetPointer().getFirst(game, source))); effect.setTargetPointer(new FixedTarget(getTargetPointer().getFirst(game, source)));
game.addEffect(effect, source); game.addEffect(effect, source);
game.informPlayers(controller.getLogName() + " is goading " + targetCreature.getLogName()); game.informPlayers(controller.getLogName() + " is goading " + targetCreature.getLogName());

View file

@ -4,25 +4,27 @@ package mage.constants;
* @author North * @author North
*/ */
public enum Duration { public enum Duration {
OneUse("", true), OneUse("", true, true),
EndOfGame("for the rest of the game", false), EndOfGame("for the rest of the game", false, false),
WhileOnBattlefield("", false), WhileOnBattlefield("", false, false),
WhileOnStack("", false), WhileOnStack("", false, true),
WhileInGraveyard("", false), WhileInGraveyard("", false, false),
EndOfTurn("until end of turn", true), EndOfTurn("until end of turn", true, true),
UntilYourNextTurn("until your next turn", true), UntilYourNextTurn("until your next turn", true, true),
UntilEndOfYourNextTurn("until the end of your next turn", true), UntilEndOfYourNextTurn("until the end of your next turn", true, true),
UntilSourceLeavesBattlefield("until {source} leaves the battlefield", true), // supported for continuous layered effects UntilSourceLeavesBattlefield("until {source} leaves the battlefield", true, false), // supported for continuous layered effects
EndOfCombat("until end of combat", true), EndOfCombat("until end of combat", true, true),
EndOfStep("until end of phase step", true), EndOfStep("until end of phase step", true, true),
Custom("", true); Custom("", true, true);
private final String text; private final String text;
private final boolean onlyValidIfNoZoneChange; // defines if an effect lasts only if the source has not chnaged zone since init of the effect private final boolean onlyValidIfNoZoneChange; // defines if an effect lasts only if the source has not changed zone since init of the effect
private final boolean fixedController; // has the controller of the effect to change, if the controller of the source changes
Duration(String text, boolean onlyValidIfNoZoneChange) { Duration(String text, boolean onlyValidIfNoZoneChange, boolean fixedController) {
this.text = text; this.text = text;
this.onlyValidIfNoZoneChange = onlyValidIfNoZoneChange; this.onlyValidIfNoZoneChange = onlyValidIfNoZoneChange;
this.fixedController = fixedController;
} }
@Override @Override
@ -34,4 +36,7 @@ public enum Duration {
return onlyValidIfNoZoneChange; return onlyValidIfNoZoneChange;
} }
public boolean isFixedController() {
return fixedController;
}
} }

View file

@ -1,5 +1,9 @@
package mage.game; package mage.game;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import mage.MageException; import mage.MageException;
import mage.MageObject; import mage.MageObject;
import mage.abilities.*; import mage.abilities.*;
@ -67,11 +71,6 @@ import mage.util.functions.ApplyToPermanent;
import mage.watchers.common.*; import mage.watchers.common.*;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
public abstract class GameImpl implements Game, Serializable { public abstract class GameImpl implements Game, Serializable {
private static final int ROLLBACK_TURNS_MAX = 4; private static final int ROLLBACK_TURNS_MAX = 4;
@ -1804,7 +1803,7 @@ public abstract class GameImpl implements Game, Serializable {
break; break;
} }
// triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature // triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature
for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext(); ) { for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext();) {
TriggeredAbility triggeredAbility = it.next(); TriggeredAbility triggeredAbility = it.next();
if (!triggeredAbility.isUsesStack()) { if (!triggeredAbility.isUsesStack()) {
state.removeTriggeredAbility(triggeredAbility); state.removeTriggeredAbility(triggeredAbility);
@ -1917,8 +1916,8 @@ public abstract class GameImpl implements Game, Serializable {
*/ */
boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream() boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream()
.anyMatch(filter -> filter.match(perm, this)); .anyMatch(filter -> filter.match(perm, this));
int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ? int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality
// Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isnt destroyed unless it has at least 1 damage marked on it. ? // Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isnt destroyed unless it has at least 1 damage marked on it.
Math.max(perm.getPower().getValue(), 1) : perm.getToughness().getValue(); Math.max(perm.getPower().getValue(), 1) : perm.getToughness().getValue();
if (lethalDamageThreshold <= perm.getDamage() || perm.isDeathtouched()) { if (lethalDamageThreshold <= perm.getDamage() || perm.isDeathtouched()) {
if (perm.destroy(null, this, false)) { if (perm.destroy(null, this, false)) {
@ -2249,7 +2248,6 @@ public abstract class GameImpl implements Game, Serializable {
} }
//TODO: implement the rest //TODO: implement the rest
return somethingHappened; return somethingHappened;
} }
@ -2557,7 +2555,7 @@ public abstract class GameImpl implements Game, Serializable {
} }
//20100423 - 800.4a //20100423 - 800.4a
Set<Card> toOutside = new HashSet<>(); Set<Card> toOutside = new HashSet<>();
for (Iterator<Permanent> it = getBattlefield().getAllPermanents().iterator(); it.hasNext(); ) { for (Iterator<Permanent> it = getBattlefield().getAllPermanents().iterator(); it.hasNext();) {
Permanent perm = it.next(); Permanent perm = it.next();
if (perm.isOwnedBy(playerId)) { if (perm.isOwnedBy(playerId)) {
if (perm.getAttachedTo() != null) { if (perm.getAttachedTo() != null) {
@ -2595,14 +2593,14 @@ public abstract class GameImpl implements Game, Serializable {
} }
} }
} }
for(Card card : toOutside) { for (Card card : toOutside) {
rememberLKI(card.getId(), Zone.BATTLEFIELD, card); rememberLKI(card.getId(), Zone.BATTLEFIELD, card);
} }
// needed to send event that permanent leaves the battlefield to allow non stack effects to execute // needed to send event that permanent leaves the battlefield to allow non stack effects to execute
player.moveCards(toOutside, Zone.OUTSIDE, null, this); player.moveCards(toOutside, Zone.OUTSIDE, null, this);
// triggered abilities that don't use the stack have to be executed // triggered abilities that don't use the stack have to be executed
List<TriggeredAbility> abilities = state.getTriggered(player.getId()); List<TriggeredAbility> abilities = state.getTriggered(player.getId());
for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext(); ) { for (Iterator<TriggeredAbility> it = abilities.iterator(); it.hasNext();) {
TriggeredAbility triggeredAbility = it.next(); TriggeredAbility triggeredAbility = it.next();
if (!triggeredAbility.isUsesStack()) { if (!triggeredAbility.isUsesStack()) {
state.removeTriggeredAbility(triggeredAbility); state.removeTriggeredAbility(triggeredAbility);
@ -2622,7 +2620,7 @@ public abstract class GameImpl implements Game, Serializable {
// Remove cards from the player in all exile zones // Remove cards from the player in all exile zones
for (ExileZone exile : this.getExile().getExileZones()) { for (ExileZone exile : this.getExile().getExileZones()) {
for (Iterator<UUID> it = exile.iterator(); it.hasNext(); ) { for (Iterator<UUID> it = exile.iterator(); it.hasNext();) {
Card card = this.getCard(it.next()); Card card = this.getCard(it.next());
if (card != null && card.isOwnedBy(playerId)) { if (card != null && card.isOwnedBy(playerId)) {
it.remove(); it.remove();
@ -2632,7 +2630,7 @@ public abstract class GameImpl implements Game, Serializable {
//Remove all commander/emblems/plane the player controls //Remove all commander/emblems/plane the player controls
boolean addPlaneAgain = false; boolean addPlaneAgain = false;
for (Iterator<CommandObject> it = this.getState().getCommand().iterator(); it.hasNext(); ) { for (Iterator<CommandObject> it = this.getState().getCommand().iterator(); it.hasNext();) {
CommandObject obj = it.next(); CommandObject obj = it.next();
if (obj.isControlledBy(playerId)) { if (obj.isControlledBy(playerId)) {
if (obj instanceof Emblem) { if (obj instanceof Emblem) {

View file

@ -1,5 +1,9 @@
package mage.game; 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.MageObject;
import mage.abilities.*; import mage.abilities.*;
import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffect;
@ -35,12 +39,6 @@ import mage.util.ThreadLocalStringBuilder;
import mage.watchers.Watcher; import mage.watchers.Watcher;
import mage.watchers.Watchers; 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 * @author BetaSteward_at_googlemail.com
* <p> * <p>
@ -179,8 +177,8 @@ public class GameState implements Serializable, Copyable<GameState> {
this.copiedCards.putAll(state.copiedCards); this.copiedCards.putAll(state.copiedCards);
this.permanentOrderNumber = state.permanentOrderNumber; this.permanentOrderNumber = state.permanentOrderNumber;
this.applyEffectsCounter = state.applyEffectsCounter; this.applyEffectsCounter = state.applyEffectsCounter;
state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter) -> state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter)
this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy())); -> this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy()));
} }
public void restoreForRollBack(GameState state) { public void restoreForRollBack(GameState state) {
@ -226,8 +224,8 @@ public class GameState implements Serializable, Copyable<GameState> {
this.copiedCards = state.copiedCards; this.copiedCards = state.copiedCards;
this.permanentOrderNumber = state.permanentOrderNumber; this.permanentOrderNumber = state.permanentOrderNumber;
this.applyEffectsCounter = state.applyEffectsCounter; this.applyEffectsCounter = state.applyEffectsCounter;
state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter) -> state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter)
this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy())); -> this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy()));
} }
@Override @Override
@ -605,7 +603,6 @@ public class GameState implements Serializable, Copyable<GameState> {
delayed.removeEndOfTurnAbilities(game); delayed.removeEndOfTurnAbilities(game);
exile.cleanupEndOfTurnZones(game); exile.cleanupEndOfTurnZones(game);
game.applyEffects(); game.applyEffects();
effects.incYourTurnNumPlayed(game);
} }
public void addEffect(ContinuousEffect effect, Ability source) { public void addEffect(ContinuousEffect effect, Ability source) {
@ -623,7 +620,6 @@ public class GameState implements Serializable, Copyable<GameState> {
// public void addMessage(String message) { // public void addMessage(String message) {
// this.messages.add(message); // this.messages.add(message);
// } // }
/** /**
* Returns a list of all players of the game ignoring range or if a player * Returns a list of all players of the game ignoring range or if a player
* has lost or left the game. * has lost or left the game.
@ -797,7 +793,7 @@ public class GameState implements Serializable, Copyable<GameState> {
for (Map.Entry<ZoneChangeData, List<GameEvent>> entry : eventsByKey.entrySet()) { for (Map.Entry<ZoneChangeData, List<GameEvent>> entry : eventsByKey.entrySet()) {
Set<Card> movedCards = new LinkedHashSet<>(); Set<Card> movedCards = new LinkedHashSet<>();
Set<PermanentToken> movedTokens = new LinkedHashSet<>(); Set<PermanentToken> movedTokens = new LinkedHashSet<>();
for (Iterator<GameEvent> it = entry.getValue().iterator(); it.hasNext(); ) { for (Iterator<GameEvent> it = entry.getValue().iterator(); it.hasNext();) {
GameEvent event = it.next(); GameEvent event = it.next();
ZoneChangeEvent castEvent = (ZoneChangeEvent) event; ZoneChangeEvent castEvent = (ZoneChangeEvent) event;
UUID targetId = castEvent.getTargetId(); UUID targetId = castEvent.getTargetId();

View file

@ -1,5 +1,7 @@
package mage.game.combat; package mage.game.combat;
import java.io.Serializable;
import java.util.*;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.RequirementEffect; import mage.abilities.effects.RequirementEffect;
@ -33,9 +35,6 @@ import mage.util.Copyable;
import mage.util.trace.TraceUtil; import mage.util.trace.TraceUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*/ */
@ -412,7 +411,8 @@ public class Combat implements Serializable, Copyable<Combat> {
if (!isBanded) { if (!isBanded) {
return; return;
} }
StringBuilder sb = new StringBuilder(player.getLogName()).append(" formed a band with ").append((attacker.getBandedCards().size() + 1) + " creatures: "); StringBuilder sb = new StringBuilder(player.getLogName()).append(" formed a band with ")
.append(attacker.getBandedCards().size()).append(1).append(" creatures: ");
sb.append(attacker.getLogName()); sb.append(attacker.getLogName());
for (UUID id : attacker.getBandedCards()) { for (UUID id : attacker.getBandedCards()) {
sb.append(", "); sb.append(", ");
@ -744,15 +744,15 @@ public class Combat implements Serializable, Copyable<Combat> {
} }
/** /**
* 509.1c The defending player checks each creature they control to * 509.1c The defending player checks each creature they control to see
* see whether it's affected by any requirements (effects that say a * whether it's affected by any requirements (effects that say a creature
* creature must block, or that it must block if some condition is met). If * must block, or that it must block if some condition is met). If the
* the number of requirements that are being obeyed is fewer than the * number of requirements that are being obeyed is fewer than the maximum
* maximum possible number of requirements that could be obeyed without * possible number of requirements that could be obeyed without disobeying
* disobeying any restrictions, the declaration of blockers is illegal. If a * any restrictions, the declaration of blockers is illegal. If a creature
* creature can't block unless a player pays a cost, that player is not * can't block unless a player pays a cost, that player is not required to
* required to pay that cost, even if blocking with that creature would * pay that cost, even if blocking with that creature would increase the
* increase the number of requirements being obeyed. * number of requirements being obeyed.
* <p> * <p>
* <p> * <p>
* Example: A player controls one creature that "blocks if able" and another * Example: A player controls one creature that "blocks if able" and another

View file

@ -1,6 +1,9 @@
package mage.players; package mage.players;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
import mage.ConditionalMana; import mage.ConditionalMana;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
@ -65,10 +68,6 @@ import mage.util.GameLog;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
public abstract class PlayerImpl implements Player, Serializable { public abstract class PlayerImpl implements Player, Serializable {
private static final Logger logger = Logger.getLogger(PlayerImpl.class); private static final Logger logger = Logger.getLogger(PlayerImpl.class);
@ -1345,6 +1344,9 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean activateAbility(ActivatedAbility ability, Game game) { public boolean activateAbility(ActivatedAbility ability, Game game) {
if (ability == null) {
return false;
}
boolean result; boolean result;
if (ability instanceof PassAbility) { if (ability instanceof PassAbility) {
pass(game); pass(game);
@ -1502,14 +1504,13 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public LinkedHashMap<UUID, ActivatedAbility> getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { public LinkedHashMap<UUID, ActivatedAbility> getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) {
LinkedHashMap<UUID, ActivatedAbility> useable = new LinkedHashMap<>(); LinkedHashMap<UUID, ActivatedAbility> useable = new LinkedHashMap<>();
// It may not be possible to activate abilities of stack abilities
if (object instanceof StackAbility || object == null) {
return useable;
}
boolean previousState = game.inCheckPlayableState(); boolean previousState = game.inCheckPlayableState();
game.setCheckPlayableState(true); game.setCheckPlayableState(true);
try { try {
// It may not be possible to activate abilities of stack abilities
if (object instanceof StackAbility) {
return useable;
}
// collect and filter playable activated abilities // collect and filter playable activated abilities
// GUI: user clicks on card, but it must activate ability from any card's parts (main, left, right) // GUI: user clicks on card, but it must activate ability from any card's parts (main, left, right)
UUID needId1, needId2, needId3; UUID needId1, needId2, needId3;
@ -2699,7 +2700,9 @@ public abstract class PlayerImpl implements Player, Serializable {
(event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null),
"Heads", "Tails", source, game "Heads", "Tails", source, game
)); ));
} else event.setResult(canChooseHeads); } else {
event.setResult(canChooseHeads);
}
game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult()));
} }
if (event.isWinnable()) { if (event.isWinnable()) {
@ -2931,7 +2934,7 @@ public abstract class PlayerImpl implements Player, Serializable {
*/ */
protected boolean canPlay(ActivatedAbility ability, ManaOptions available, MageObject sourceObject, Game game) { protected boolean canPlay(ActivatedAbility ability, ManaOptions available, MageObject sourceObject, Game game) {
if (!(ability instanceof ActivatedManaAbilityImpl)) { if (!(ability instanceof ActivatedManaAbilityImpl)) {
ActivatedAbility copy = ability.copy(); // copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability
if (!copy.canActivate(playerId, game).canActivate()) { if (!copy.canActivate(playerId, game).canActivate()) {
return false; return false;
} }
@ -3117,7 +3120,7 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
protected boolean canLandPlayAlternateSourceCostsAbility(Card sourceObject, ManaOptions available, Ability ability, Game game) { protected boolean canLandPlayAlternateSourceCostsAbility(Card sourceObject, ManaOptions available, Ability ability, Game game) {
if (!(sourceObject instanceof Permanent)) { if (sourceObject != null && !(sourceObject instanceof Permanent)) {
Ability sourceAbility = sourceObject.getAbilities().stream() Ability sourceAbility = sourceObject.getAbilities().stream()
.filter(landAbility -> landAbility.getAbilityType() == AbilityType.PLAY_LAND) .filter(landAbility -> landAbility.getAbilityType() == AbilityType.PLAY_LAND)
.findFirst().orElse(null); .findFirst().orElse(null);
@ -3158,7 +3161,7 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
private void getPlayableFromCardAll(Game game, Zone fromZone, Card card, ManaOptions availableMana, List<ActivatedAbility> output) { private void getPlayableFromCardAll(Game game, Zone fromZone, Card card, ManaOptions availableMana, List<ActivatedAbility> output) {
if (fromZone == null) { if (fromZone == null || card == null) {
return; return;
} }
@ -3187,7 +3190,6 @@ public abstract class PlayerImpl implements Player, Serializable {
boolean isPlayLand = (ability instanceof PlayLandAbility); boolean isPlayLand = (ability instanceof PlayLandAbility);
// as original controller // as original controller
// play land restrictions // play land restrictions
if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( if (isPlayLand && game.getContinuousEffects().preventedByRuleModification(
GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(),
@ -3221,7 +3223,6 @@ public abstract class PlayerImpl implements Player, Serializable {
|| (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard());
// as affected controller // as affected controller
UUID savedControllerId = ability.getControllerId(); UUID savedControllerId = ability.getControllerId();
ability.setControllerId(this.getId()); ability.setControllerId(this.getId());
try { try {
@ -3969,7 +3970,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// identify cards from one owner // identify cards from one owner
Cards cards = new CardsImpl(); Cards cards = new CardsImpl();
UUID ownerId = null; UUID ownerId = null;
for (Iterator<Card> it = allCards.iterator(); it.hasNext(); ) { for (Iterator<Card> it = allCards.iterator(); it.hasNext();) {
Card card = it.next(); Card card = it.next();
if (cards.isEmpty()) { if (cards.isEmpty()) {
ownerId = card.getOwnerId(); ownerId = card.getOwnerId();

View file

@ -0,0 +1,39 @@
package mage.watchers.common;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.watchers.Watcher;
/**
*
* @author LevelX2
*/
public class LostControlWatcher extends Watcher {
private final Map<UUID, Long> lastLostControl = new HashMap<>();
public LostControlWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.LOST_CONTROL) {
lastLostControl.put(event.getTargetId(), System.currentTimeMillis());
}
}
@Override
public void reset() {
super.reset();
lastLostControl.clear();
}
public long getOrderOfLastLostControl(UUID sourceId) {
return lastLostControl.getOrDefault(sourceId, new Long(0));
}
}