- 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
@ -33,9 +33,10 @@ public final class MasterThief extends CardImpl {
new GainControlTargetEffect(Duration.Custom), new GainControlTargetEffect(Duration.Custom),
new SourceOnBattlefieldControlUnchangedCondition(), new SourceOnBattlefieldControlUnchangedCondition(),
"gain control of target artifact for as long as you control {this}"), "gain control of target artifact for as long as you control {this}"),
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);
@ -44,10 +44,11 @@ public final class WillowSatyr extends CardImpl {
this.addAbility(new SkipUntapOptionalAbility()); this.addAbility(new SkipUntapOptionalAbility());
// {tap}: Gain control of target legendary creature for as long as you control Willow Satyr and Willow Satyr remains tapped. // {tap}: Gain control of target legendary creature for as long as you control Willow Satyr and Willow Satyr remains tapped.
ConditionalContinuousEffect effect = new ConditionalContinuousEffect( ConditionalContinuousEffect effect = new ConditionalContinuousEffect(
new GainControlTargetEffect(Duration.Custom), new CompoundCondition(SourceTappedCondition.instance, new SourceOnBattlefieldControlUnchangedCondition()), new GainControlTargetEffect(Duration.Custom), new CompoundCondition(SourceTappedCondition.instance, new SourceOnBattlefieldControlUnchangedCondition()),
"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.*;
/** /**
@ -193,7 +191,7 @@ public class TestPlayer implements Player {
/** /**
* @param maxCallsWithoutAction max number of priority passes a player may * @param maxCallsWithoutAction max number of priority passes a player may
* have for this test (default = 100) * have for this test (default = 100)
*/ */
public void setMaxCallsWithoutAction(int maxCallsWithoutAction) { public void setMaxCallsWithoutAction(int maxCallsWithoutAction) {
this.maxCallsWithoutAction = maxCallsWithoutAction; this.maxCallsWithoutAction = maxCallsWithoutAction;
@ -1046,13 +1044,13 @@ public class TestPlayer implements Player {
List<String> data = cards.stream() List<String> data = cards.stream()
.map(c -> (((c instanceof PermanentToken) ? "[T] " : "[C] ") .map(c -> (((c instanceof PermanentToken) ? "[T] " : "[C] ")
+ c.getIdName() + c.getIdName()
+ (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "") + (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "")
+ " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue()
+ (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") + (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "")
+ ", " + (c.isTapped() ? "Tapped" : "Untapped") + ", " + (c.isTapped() ? "Tapped" : "Untapped")
+ getPrintableAliases(", [", c.getId(), "]") + getPrintableAliases(", [", c.getId(), "]")
+ (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName()))) + (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName())))
.sorted() .sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -1076,12 +1074,12 @@ public class TestPlayer implements Player {
List<String> data = abilities.stream() List<String> data = abilities.stream()
.map(a -> (a.getZone() + " -> " .map(a -> (a.getZone() + " -> "
+ a.getSourceObject(game).getIdName() + " -> " + a.getSourceObject(game).getIdName() + " -> "
+ (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified + (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified
+ (a.toString().length() > 0 + (a.toString().length() > 0
? a.toString().substring(0, Math.min(20, a.toString().length())) ? a.toString().substring(0, Math.min(20, a.toString().length()))
: a.getClass().getSimpleName()) : a.getClass().getSimpleName())
+ "...")) + "..."))
.sorted() .sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -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
@ -2021,7 +2019,7 @@ public class TestPlayer implements Player {
// skip targets // skip targets
if (targets.get(0).equals(TARGET_SKIP)) { if (targets.get(0).equals(TARGET_SKIP)) {
Assert.assertTrue("found skip target, but it require more targets, needs " Assert.assertTrue("found skip target, but it require more targets, needs "
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets()); target.getTargets().size() >= target.getMinNumberOfTargets());
targets.remove(0); targets.remove(0);
return true; return true;
@ -2326,7 +2324,7 @@ public class TestPlayer implements Player {
this.chooseStrictModeFailed("choice", game, this.chooseStrictModeFailed("choice", game,
"Triggered list (total " + abilities.size() + "):\n" "Triggered list (total " + abilities.size() + "):\n"
+ abilities.stream().map(a -> getInfo(a, game)).collect(Collectors.joining("\n"))); + abilities.stream().map(a -> getInfo(a, game)).collect(Collectors.joining("\n")));
return computerPlayer.chooseTriggeredAbility(abilities, game); return computerPlayer.chooseTriggeredAbility(abilities, game);
} }
@ -3496,7 +3494,7 @@ public class TestPlayer implements Player {
@Override @Override
public boolean choose(Outcome outcome, Target target, public boolean choose(Outcome outcome, Target target,
UUID sourceId, Game game UUID sourceId, Game game
) { ) {
// needed to call here the TestPlayer because it's overwitten // needed to call here the TestPlayer because it's overwitten
return choose(outcome, target, sourceId, game, null); return choose(outcome, target, sourceId, game, null);
@ -3504,7 +3502,7 @@ public class TestPlayer implements Player {
@Override @Override
public boolean choose(Outcome outcome, Cards cards, public boolean choose(Outcome outcome, Cards cards,
TargetCard target, Game game TargetCard target, Game game
) { ) {
assertAliasSupportInChoices(false); assertAliasSupportInChoices(false);
if (!choices.isEmpty()) { if (!choices.isEmpty()) {
@ -3541,7 +3539,7 @@ public class TestPlayer implements Player {
@Override @Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, public boolean chooseTargetAmount(Outcome outcome, TargetAmount target,
Ability source, Game game Ability source, Game game
) { ) {
// chooseTargetAmount calls for EACH target cycle (e.g. one target per click, see TargetAmount) // chooseTargetAmount calls for EACH target cycle (e.g. one target per click, see TargetAmount)
// if use want to stop choosing then chooseTargetAmount must return false (example: up to xxx) // if use want to stop choosing then chooseTargetAmount must return false (example: up to xxx)
@ -3554,7 +3552,7 @@ public class TestPlayer implements Player {
// skip targets // skip targets
if (targets.get(0).equals(TARGET_SKIP)) { if (targets.get(0).equals(TARGET_SKIP)) {
Assert.assertTrue("found skip target, but it require more targets, needs " Assert.assertTrue("found skip target, but it require more targets, needs "
+ (target.getMinNumberOfTargets() - target.getTargets().size()) + " more", + (target.getMinNumberOfTargets() - target.getTargets().size()) + " more",
target.getTargets().size() >= target.getMinNumberOfTargets()); target.getTargets().size() >= target.getMinNumberOfTargets());
targets.remove(0); targets.remove(0);
return false; // false in chooseTargetAmount = stop to choose return false; // false in chooseTargetAmount = stop to choose
@ -3607,15 +3605,15 @@ public class TestPlayer implements Player {
@Override @Override
public boolean choosePile(Outcome outcome, String message, public boolean choosePile(Outcome outcome, String message,
List<? extends Card> pile1, List<? extends Card> pile2, List<? extends Card> pile1, List<? extends Card> pile2,
Game game Game game
) { ) {
return computerPlayer.choosePile(outcome, message, pile1, pile2, game); return computerPlayer.choosePile(outcome, message, pile1, pile2, game);
} }
@Override @Override
public boolean playMana(Ability ability, ManaCost unpaid, public boolean playMana(Ability ability, ManaCost unpaid,
String promptText, Game game String promptText, Game game
) { ) {
groupsForTargetHandling = null; groupsForTargetHandling = null;
@ -3665,15 +3663,15 @@ public class TestPlayer implements Player {
@Override @Override
public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup, public UUID chooseBlockerOrder(List<Permanent> blockers, CombatGroup combatGroup,
List<UUID> blockerOrder, Game game List<UUID> blockerOrder, Game game
) { ) {
return computerPlayer.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game); return computerPlayer.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
} }
@Override @Override
public void assignDamage(int damage, List<UUID> targets, public void assignDamage(int damage, List<UUID> targets,
String singleTargetName, UUID sourceId, String singleTargetName, UUID sourceId,
Game game Game game
) { ) {
computerPlayer.assignDamage(damage, targets, singleTargetName, sourceId, game); computerPlayer.assignDamage(damage, targets, singleTargetName, sourceId, game);
} }
@ -3692,14 +3690,14 @@ public class TestPlayer implements Player {
@Override @Override
public void pickCard(List<Card> cards, Deck deck, public void pickCard(List<Card> cards, Deck deck,
Draft draft Draft draft
) { ) {
computerPlayer.pickCard(cards, deck, draft); computerPlayer.pickCard(cards, deck, draft);
} }
@Override @Override
public boolean scry(int value, Ability source, public boolean scry(int value, Ability source,
Game game Game game
) { ) {
// Don't scry at the start of the game. // Don't scry at the start of the game.
if (game.getTurnNum() == 1 && game.getStep() == null) { if (game.getTurnNum() == 1 && game.getStep() == null) {
@ -3710,44 +3708,44 @@ public class TestPlayer implements Player {
@Override @Override
public boolean surveil(int value, Ability source, public boolean surveil(int value, Ability source,
Game game Game game
) { ) {
return computerPlayer.surveil(value, source, game); return computerPlayer.surveil(value, source, game);
} }
@Override @Override
public boolean moveCards(Card card, Zone toZone, public boolean moveCards(Card card, Zone toZone,
Ability source, Game game Ability source, Game game
) { ) {
return computerPlayer.moveCards(card, toZone, source, game); return computerPlayer.moveCards(card, toZone, source, game);
} }
@Override @Override
public boolean moveCards(Card card, Zone toZone, public boolean moveCards(Card card, Zone toZone,
Ability source, Game game, Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) { ) {
return computerPlayer.moveCards(card, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); return computerPlayer.moveCards(card, toZone, source, game, tapped, faceDown, byOwner, appliedEffects);
} }
@Override @Override
public boolean moveCards(Cards cards, Zone toZone, public boolean moveCards(Cards cards, Zone toZone,
Ability source, Game game Ability source, Game game
) { ) {
return computerPlayer.moveCards(cards, toZone, source, game); return computerPlayer.moveCards(cards, toZone, source, game);
} }
@Override @Override
public boolean moveCards(Set<Card> cards, Zone toZone, public boolean moveCards(Set<Card> cards, Zone toZone,
Ability source, Game game Ability source, Game game
) { ) {
return computerPlayer.moveCards(cards, toZone, source, game); return computerPlayer.moveCards(cards, toZone, source, game);
} }
@Override @Override
public boolean moveCards(Set<Card> cards, Zone toZone, public boolean moveCards(Set<Card> cards, Zone toZone,
Ability source, Game game, Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) { ) {
return computerPlayer.moveCards(cards, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); return computerPlayer.moveCards(cards, toZone, source, game, tapped, faceDown, byOwner, appliedEffects);
} }

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;
@ -768,8 +758,8 @@ public class ContinuousEffects implements Serializable {
* @param event * @param event
* @param targetAbility ability the event is attached to. can be null. * @param targetAbility ability the event is attached to. can be null.
* @param game * @param game
* @param silentMode true if the event does not really happen but it's * @param silentMode true if the event does not really happen but it's
* checked if the event would be replaced * checked if the event would be replaced
* @return * @return
*/ */
public boolean preventedByRuleModification(GameEvent event, Ability targetAbility, Game game, boolean silentMode) { public boolean preventedByRuleModification(GameEvent event, Ability targetAbility, Game game, boolean silentMode) {
@ -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());
@ -1002,7 +992,7 @@ public class ContinuousEffects implements Serializable {
.entrySet() .entrySet()
.stream() .stream()
.filter(entry -> dependentTo.contains(entry.getKey().getId()) .filter(entry -> dependentTo.contains(entry.getKey().getId())
&& entry.getValue().contains(effect.getId())) && entry.getValue().contains(effect.getId()))
.forEach(entry -> { .forEach(entry -> {
entry.getValue().remove(effect.getId()); entry.getValue().remove(effect.getId());
dependentTo.remove(entry.getKey().getId()); dependentTo.remove(entry.getKey().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,18 +1273,19 @@ 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) {
Set<Ability> abilities = effects.getAbility(effect.getId()); if (!effect.getDuration().isFixedController()) {
for (Ability ability : abilities) { Set<Ability> abilities = effects.getAbility(effect.getId());
if (ability.getSourceId() != null) { for (Ability ability : abilities) {
if (ability.getSourceId().equals(sourceId)) { if (ability.getSourceId() != null) {
ability.setControllerId(controllerId); if (ability.getSourceId().equals(sourceId)) {
ability.setControllerId(controllerId);
}
} else if (ability.getZone() != Zone.COMMAND) {
logger.fatal("Continuous effect for ability with no sourceId Ability: " + ability);
} }
} else if (ability.getZone() != Zone.COMMAND) {
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()) {
@ -109,9 +99,8 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
those objects are exiled. This is not a state-based action. It happens as soon as the player leaves the game. those objects are exiled. This is not a state-based action. It happens as soon as the player leaves the game.
If the player who left the game had priority at the time they left, priority passes to the next player in turn If the player who left the game had priority at the time they left, priority passes to the next player in turn
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;
@ -1549,7 +1548,7 @@ public abstract class GameImpl implements Game, Serializable {
/** /**
* @param emblem * @param emblem
* @param sourceObject * @param sourceObject
* @param toPlayerId controller and owner of the emblem * @param toPlayerId controller and owner of the emblem
*/ */
@Override @Override
public void addEmblem(Emblem emblem, MageObject sourceObject, UUID toPlayerId) { public void addEmblem(Emblem emblem, MageObject sourceObject, UUID toPlayerId) {
@ -1567,8 +1566,8 @@ public abstract class GameImpl implements Game, Serializable {
/** /**
* @param plane * @param plane
* @param sourceObject * @param sourceObject
* @param toPlayerId controller and owner of the plane (may only be one per * @param toPlayerId controller and owner of the plane (may only be one per
* game..) * game..)
* @return boolean - whether the plane was added successfully or not * @return boolean - whether the plane was added successfully or not
*/ */
@Override @Override
@ -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();
@ -1030,7 +1026,7 @@ public class GameState implements Serializable, Copyable<GameState> {
* @param attachedTo * @param attachedTo
* @param ability * @param ability
* @param copyAbility copies non MageSingleton abilities before adding to * @param copyAbility copies non MageSingleton abilities before adding to
* state * state
*/ */
public void addOtherAbility(Card attachedTo, Ability ability, boolean copyAbility) { public void addOtherAbility(Card attachedTo, Ability ability, boolean copyAbility) {
Ability newAbility; Ability newAbility;

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
*/ */
@ -346,8 +345,8 @@ public class Combat implements Serializable, Copyable<Combat> {
if (game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, attackingPlayerId, attackingPlayerId)) if (game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, attackingPlayerId, attackingPlayerId))
|| (!canBand && !canBandWithOther) || (!canBand && !canBandWithOther)
|| !player.chooseUse(Outcome.Benefit, || !player.chooseUse(Outcome.Benefit,
"Do you wish to " + (isBanded ? "band " + attacker.getLogName() "Do you wish to " + (isBanded ? "band " + attacker.getLogName()
+ " with another " : "form a band with " + attacker.getLogName() + " and an ") + " with another " : "form a band with " + attacker.getLogName() + " and an ")
+ "attacking creature?", null, game)) { + "attacking creature?", null, game)) {
break; break;
} }
@ -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(", ");
@ -565,7 +565,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* Handle the blocker selection process * Handle the blocker selection process
* *
* @param blockController player that controls how to block, if null the * @param blockController player that controls how to block, if null the
* defender is the controller * defender is the controller
* @param game * @param game
*/ */
public void selectBlockers(Player blockController, Game game) { public void selectBlockers(Player blockController, Game game) {
@ -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
@ -1379,7 +1379,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* @param playerId * @param playerId
* @param game * @param game
* @param solveBanding check whether also add creatures banded with * @param solveBanding check whether also add creatures banded with
* attackerId * attackerId
*/ */
public void addBlockingGroup(UUID blockerId, UUID attackerId, UUID playerId, Game game, boolean solveBanding) { public void addBlockingGroup(UUID blockerId, UUID attackerId, UUID playerId, Game game, boolean solveBanding) {
Permanent blocker = game.getPermanent(blockerId); Permanent blocker = game.getPermanent(blockerId);

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);
@ -612,9 +611,9 @@ public abstract class PlayerImpl implements Player, Serializable {
&& this.hasOpponent(sourceControllerId, game) && this.hasOpponent(sourceControllerId, game)
&& game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null
&& abilities.stream() && abilities.stream()
.filter(HexproofBaseAbility.class::isInstance) .filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast) .map(HexproofBaseAbility.class::cast)
.anyMatch(ability -> ability.checkObject(source, game))) { .anyMatch(ability -> ability.checkObject(source, game))) {
return false; return false;
} }
@ -654,7 +653,7 @@ public abstract class PlayerImpl implements Player, Serializable {
game.informPlayers(getLogName() + " discards down to " game.informPlayers(getLogName() + " discards down to "
+ this.maxHandSize + this.maxHandSize
+ (this.maxHandSize == 1 + (this.maxHandSize == 1
? " hand card" : " hand cards")); ? " hand card" : " hand cards"));
} }
discard(hand.size() - this.maxHandSize, false, null, game); discard(hand.size() - this.maxHandSize, false, null, game);
} }
@ -803,7 +802,7 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD, GameEvent gameEvent = GameEvent.getEvent(GameEvent.EventType.DISCARD_CARD,
card.getId(), source == null card.getId(), source == null
? null : source.getSourceId(), playerId); ? null : source.getSourceId(), playerId);
gameEvent.setFlag(source != null); // event from effect or from cost (source == null) gameEvent.setFlag(source != null); // event from effect or from cost (source == null)
if (game.replaceEvent(gameEvent, source)) { if (game.replaceEvent(gameEvent, source)) {
return false; return false;
@ -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;
@ -1810,9 +1811,9 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
private List<Permanent> getPermanentsThatCanBeUntapped(Game game, private List<Permanent> getPermanentsThatCanBeUntapped(Game game,
List<Permanent> canBeUntapped, List<Permanent> canBeUntapped,
RestrictionUntapNotMoreThanEffect handledEffect, RestrictionUntapNotMoreThanEffect handledEffect,
Map<Entry<RestrictionUntapNotMoreThanEffect, Set<Ability>>, Integer> notMoreThanEffectsUsage) { Map<Entry<RestrictionUntapNotMoreThanEffect, Set<Ability>>, Integer> notMoreThanEffectsUsage) {
List<Permanent> leftForUntap = new ArrayList<>(); List<Permanent> leftForUntap = new ArrayList<>();
// select permanents that can still be untapped // select permanents that can still be untapped
for (Permanent permanent : canBeUntapped) { for (Permanent permanent : canBeUntapped) {
@ -2521,7 +2522,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId, public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId,
boolean triggerEvents) { boolean triggerEvents) {
//20091005 - 701.14c //20091005 - 701.14c
Library searchedLibrary = null; Library searchedLibrary = null;
String searchInfo = null; String searchInfo = null;
@ -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()) {
@ -2721,7 +2724,7 @@ public abstract class PlayerImpl implements Player, Serializable {
/** /**
* @param game * @param game
* @param appliedEffects * @param appliedEffects
* @param numSides Number of sides the dice has * @param numSides Number of sides the dice has
* @return the number that the player rolled * @return the number that the player rolled
*/ */
@Override @Override
@ -2758,16 +2761,16 @@ public abstract class PlayerImpl implements Player, Serializable {
/** /**
* @param game * @param game
* @param appliedEffects * @param appliedEffects
* @param numberChaosSides The number of chaos sides the planar die * @param numberChaosSides The number of chaos sides the planar die
* currently has (normally 1 but can be 5) * currently has (normally 1 but can be 5)
* @param numberPlanarSides The number of chaos sides the planar die * @param numberPlanarSides The number of chaos sides the planar die
* currently has (normally 1) * currently has (normally 1)
* @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll * @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll
* or NilRoll * or NilRoll
*/ */
@Override @Override
public PlanarDieRoll rollPlanarDie(Game game, List<UUID> appliedEffects, int numberChaosSides, public PlanarDieRoll rollPlanarDie(Game game, List<UUID> appliedEffects, int numberChaosSides,
int numberPlanarSides) { int numberPlanarSides) {
int result = RandomUtil.nextInt(9) + 1; int result = RandomUtil.nextInt(9) + 1;
PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL; PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL;
if (numberChaosSides + numberPlanarSides > 9) { if (numberChaosSides + numberPlanarSides > 9) {
@ -2924,14 +2927,14 @@ public abstract class PlayerImpl implements Player, Serializable {
/** /**
* @param ability * @param ability
* @param available if null, it won't be checked if enough mana is available * @param available if null, it won't be checked if enough mana is available
* @param sourceObject * @param sourceObject
* @param game * @param game
* @return * @return
*/ */
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 {
@ -3620,7 +3621,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId, public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId,
UUID controllerId, Game game UUID controllerId, Game game
) { ) {
return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game); return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game);
} }
@ -3773,8 +3774,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCards(Card card, Zone toZone, public boolean moveCards(Card card, Zone toZone,
Ability source, Game game, Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) { ) {
Set<Card> cardList = new HashSet<>(); Set<Card> cardList = new HashSet<>();
if (card != null) { if (card != null) {
@ -3785,22 +3786,22 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCards(Cards cards, Zone toZone, public boolean moveCards(Cards cards, Zone toZone,
Ability source, Game game Ability source, Game game
) { ) {
return moveCards(cards.getCards(game), toZone, source, game); return moveCards(cards.getCards(game), toZone, source, game);
} }
@Override @Override
public boolean moveCards(Set<Card> cards, Zone toZone, public boolean moveCards(Set<Card> cards, Zone toZone,
Ability source, Game game Ability source, Game game
) { ) {
return moveCards(cards, toZone, source, game, false, false, false, null); return moveCards(cards, toZone, source, game, false, false, false, null);
} }
@Override @Override
public boolean moveCards(Set<Card> cards, Zone toZone, public boolean moveCards(Set<Card> cards, Zone toZone,
Ability source, Game game, Ability source, Game game,
boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects boolean tapped, boolean faceDown, boolean byOwner, List<UUID> appliedEffects
) { ) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
return true; return true;
@ -3902,8 +3903,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardsToExile(Card card, Ability source, public boolean moveCardsToExile(Card card, Ability source,
Game game, boolean withName, UUID exileId, Game game, boolean withName, UUID exileId,
String exileZoneName String exileZoneName
) { ) {
Set<Card> cards = new HashSet<>(); Set<Card> cards = new HashSet<>();
cards.add(card); cards.add(card);
@ -3912,8 +3913,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardsToExile(Set<Card> cards, Ability source, public boolean moveCardsToExile(Set<Card> cards, Ability source,
Game game, boolean withName, UUID exileId, Game game, boolean withName, UUID exileId,
String exileZoneName String exileZoneName
) { ) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
return true; return true;
@ -3929,14 +3930,14 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardToHandWithInfo(Card card, UUID sourceId, public boolean moveCardToHandWithInfo(Card card, UUID sourceId,
Game game Game game
) { ) {
return this.moveCardToHandWithInfo(card, sourceId, game, true); return this.moveCardToHandWithInfo(card, sourceId, game, true);
} }
@Override @Override
public boolean moveCardToHandWithInfo(Card card, UUID sourceId, public boolean moveCardToHandWithInfo(Card card, UUID sourceId,
Game game, boolean withName Game game, boolean withName
) { ) {
boolean result = false; boolean result = false;
Zone fromZone = game.getState().getZone(card.getId()); Zone fromZone = game.getState().getZone(card.getId());
@ -3961,7 +3962,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public Set<Card> moveCardsToGraveyardWithInfo(Set<Card> allCards, Ability source, public Set<Card> moveCardsToGraveyardWithInfo(Set<Card> allCards, Ability source,
Game game, Zone fromZone Game game, Zone fromZone
) { ) {
UUID sourceId = source == null ? null : source.getSourceId(); UUID sourceId = source == null ? null : source.getSourceId();
Set<Card> movedCards = new LinkedHashSet<>(); Set<Card> movedCards = new LinkedHashSet<>();
@ -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();
@ -4032,7 +4033,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId, public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId,
Game game, Zone fromZone Game game, Zone fromZone
) { ) {
if (card == null) { if (card == null) {
return false; return false;
@ -4061,8 +4062,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId, public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId,
Game game, Zone fromZone, Game game, Zone fromZone,
boolean toTop, boolean withName boolean toTop, boolean withName
) { ) {
if (card == null) { if (card == null) {
return false; return false;
@ -4127,7 +4128,7 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override @Override
public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, UUID sourceId, public boolean moveCardToExileWithInfo(Card card, UUID exileId, String exileName, UUID sourceId,
Game game, Zone fromZone, boolean withName) { Game game, Zone fromZone, boolean withName) {
if (card == null) { if (card == null) {
return false; return false;
} }
@ -4150,7 +4151,7 @@ public abstract class PlayerImpl implements Player, Serializable {
game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName()
+ (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' '
+ (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH)
+ ' ' : "") + "to the exile zone"); + ' ' : "") + "to the exile zone");
} }
result = true; result = true;

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));
}
}