From 1e36b394345709ea31cde9c339471e5780de3422 Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Wed, 10 Jun 2020 22:28:23 +0200 Subject: [PATCH] - 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 2e8ece1dbd4618f5e6bccaaeb6f334d55c915bba). --- Mage.Sets/src/mage/cards/a/AegisAngel.java | 3 +- Mage.Sets/src/mage/cards/a/Aladdin.java | 5 +- .../src/mage/cards/d/DragonlordSilumgar.java | 5 +- Mage.Sets/src/mage/cards/m/MasterThief.java | 7 +- .../src/mage/cards/m/MeriekeRiBerit.java | 3 +- .../src/mage/cards/o/OrcishSquatters.java | 5 +- .../src/mage/cards/q/QuicksmithRebel.java | 5 +- Mage.Sets/src/mage/cards/q/QuicksmithSpy.java | 5 +- Mage.Sets/src/mage/cards/r/RoilElemental.java | 3 +- Mage.Sets/src/mage/cards/t/TheWretched.java | 55 +++-- .../src/mage/cards/t/ThrullChampion.java | 5 +- Mage.Sets/src/mage/cards/w/Willbreaker.java | 8 +- Mage.Sets/src/mage/cards/w/WillowSatyr.java | 13 +- .../cards/abilities/keywords/GoadTest.java | 201 +++++++++++------- .../cards/conditional/TheWretchedTest.java | 41 ++-- .../cards/copy/LazavDimirMastermindTest.java | 97 +++++---- .../java/org/mage/test/player/TestPlayer.java | 86 ++++---- ...nBattlefieldControlUnchangedCondition.java | 38 ++-- .../abilities/effects/ContinuousEffect.java | 11 +- .../effects/ContinuousEffectImpl.java | 30 +-- .../abilities/effects/ContinuousEffects.java | 69 +++--- .../effects/ContinuousEffectsList.java | 23 +- .../common/combat/CantAttackYouEffect.java | 16 +- .../common/combat/GoadTargetEffect.java | 6 +- .../main/java/mage/constants/Duration.java | 33 +-- Mage/src/main/java/mage/game/GameImpl.java | 32 ++- Mage/src/main/java/mage/game/GameState.java | 24 +-- .../main/java/mage/game/combat/Combat.java | 34 +-- .../main/java/mage/players/PlayerImpl.java | 101 ++++----- .../watchers/common/LostControlWatcher.java | 39 ++++ 30 files changed, 534 insertions(+), 469 deletions(-) create mode 100644 Mage/src/main/java/mage/watchers/common/LostControlWatcher.java diff --git a/Mage.Sets/src/mage/cards/a/AegisAngel.java b/Mage.Sets/src/mage/cards/a/AegisAngel.java index 7222995d67c..bef99e72eaa 100644 --- a/Mage.Sets/src/mage/cards/a/AegisAngel.java +++ b/Mage.Sets/src/mage/cards/a/AegisAngel.java @@ -1,4 +1,3 @@ - package mage.cards.a; import java.util.UUID; @@ -18,6 +17,7 @@ import mage.constants.SubType; import mage.filter.FilterPermanent; import mage.filter.predicate.permanent.AnotherPredicate; import mage.target.TargetPermanent; +import mage.watchers.common.LostControlWatcher; /** * @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"); Ability ability = new EntersBattlefieldTriggeredAbility(effect, false); ability.addTarget(new TargetPermanent(filter)); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/a/Aladdin.java b/Mage.Sets/src/mage/cards/a/Aladdin.java index 74564226216..58b44e3ae82 100644 --- a/Mage.Sets/src/mage/cards/a/Aladdin.java +++ b/Mage.Sets/src/mage/cards/a/Aladdin.java @@ -1,4 +1,3 @@ - package mage.cards.a; import java.util.UUID; @@ -17,6 +16,7 @@ import mage.constants.Duration; import mage.constants.SubType; import mage.constants.Zone; 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 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.ROGUE); 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.addCost(new TapSourceCost()); ability.addTarget(new TargetArtifactPermanent()); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/d/DragonlordSilumgar.java b/Mage.Sets/src/mage/cards/d/DragonlordSilumgar.java index e4b4464ed33..ccf9f829139 100644 --- a/Mage.Sets/src/mage/cards/d/DragonlordSilumgar.java +++ b/Mage.Sets/src/mage/cards/d/DragonlordSilumgar.java @@ -1,4 +1,3 @@ - package mage.cards.d; import java.util.UUID; @@ -16,9 +15,9 @@ import mage.abilities.keyword.FlyingAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.SuperType; import mage.game.Game; import mage.game.permanent.Permanent; @@ -26,6 +25,7 @@ import mage.players.Player; import mage.target.common.TargetCreatureOrPlaneswalker; import mage.util.CardUtil; 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. Ability ability = new EntersBattlefieldTriggeredAbility(new DragonlordSilumgarEffect(), false); ability.addTarget(new TargetCreatureOrPlaneswalker()); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/m/MasterThief.java b/Mage.Sets/src/mage/cards/m/MasterThief.java index f7125665443..c71f95d1a2c 100644 --- a/Mage.Sets/src/mage/cards/m/MasterThief.java +++ b/Mage.Sets/src/mage/cards/m/MasterThief.java @@ -1,4 +1,3 @@ - package mage.cards.m; import java.util.UUID; @@ -11,9 +10,10 @@ import mage.abilities.effects.common.continuous.GainControlTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.target.common.TargetArtifactPermanent; +import mage.watchers.common.LostControlWatcher; /** * @author Loki, JayDi85 @@ -33,9 +33,10 @@ public final class MasterThief extends CardImpl { new GainControlTargetEffect(Duration.Custom), new SourceOnBattlefieldControlUnchangedCondition(), "gain control of target artifact for as long as you control {this}"), - false); + false); ability.addTarget(new TargetArtifactPermanent()); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/m/MeriekeRiBerit.java b/Mage.Sets/src/mage/cards/m/MeriekeRiBerit.java index 1e2a0c4a5f0..7e20fbf9282 100644 --- a/Mage.Sets/src/mage/cards/m/MeriekeRiBerit.java +++ b/Mage.Sets/src/mage/cards/m/MeriekeRiBerit.java @@ -1,4 +1,3 @@ - package mage.cards.m; import java.util.UUID; @@ -25,6 +24,7 @@ import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; 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.addTarget(new TargetPermanent(new FilterCreaturePermanent("target creature"))); ability.addEffect(new MeriekeRiBeritCreateDelayedTriggerEffect()); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/o/OrcishSquatters.java b/Mage.Sets/src/mage/cards/o/OrcishSquatters.java index fd20a8044bc..cfcbddab651 100644 --- a/Mage.Sets/src/mage/cards/o/OrcishSquatters.java +++ b/Mage.Sets/src/mage/cards/o/OrcishSquatters.java @@ -1,4 +1,3 @@ - package mage.cards.o; import java.util.UUID; @@ -9,14 +8,15 @@ import mage.abilities.condition.common.SourceOnBattlefieldControlUnchangedCondit import mage.abilities.decorator.ConditionalContinuousEffect; import mage.abilities.effects.common.continuous.AssignNoCombatDamageSourceEffect; import mage.abilities.effects.common.continuous.GainControlTargetEffect; -import mage.constants.SubType; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; +import mage.constants.SubType; import mage.filter.common.FilterLandPermanent; import mage.filter.predicate.permanent.DefendingPlayerControlsPredicate; import mage.target.TargetPermanent; +import mage.watchers.common.LostControlWatcher; /** * @@ -45,6 +45,7 @@ public final class OrcishSquatters extends CardImpl { ), true); ability.addEffect(new AssignNoCombatDamageSourceEffect(Duration.EndOfTurn, true)); ability.addTarget(new TargetPermanent(filter)); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/q/QuicksmithRebel.java b/Mage.Sets/src/mage/cards/q/QuicksmithRebel.java index 87b0caca71f..38c7600aa89 100644 --- a/Mage.Sets/src/mage/cards/q/QuicksmithRebel.java +++ b/Mage.Sets/src/mage/cards/q/QuicksmithRebel.java @@ -1,4 +1,3 @@ - package mage.cards.q; import java.util.UUID; @@ -14,12 +13,13 @@ import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterControlledArtifactPermanent; import mage.target.TargetPermanent; 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}"); Ability ability = new EntersBattlefieldTriggeredAbility(effect, false); ability.addTarget(new TargetPermanent(new FilterControlledArtifactPermanent())); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/q/QuicksmithSpy.java b/Mage.Sets/src/mage/cards/q/QuicksmithSpy.java index 2b69c073e1b..d52dfb6b076 100644 --- a/Mage.Sets/src/mage/cards/q/QuicksmithSpy.java +++ b/Mage.Sets/src/mage/cards/q/QuicksmithSpy.java @@ -1,4 +1,3 @@ - package mage.cards.q; import java.util.UUID; @@ -14,11 +13,12 @@ import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterControlledArtifactPermanent; 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}"); Ability ability = new EntersBattlefieldTriggeredAbility(effect, false); ability.addTarget(new TargetPermanent(new FilterControlledArtifactPermanent())); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/r/RoilElemental.java b/Mage.Sets/src/mage/cards/r/RoilElemental.java index 4a2b3c44c5c..a4e01d142ee 100644 --- a/Mage.Sets/src/mage/cards/r/RoilElemental.java +++ b/Mage.Sets/src/mage/cards/r/RoilElemental.java @@ -1,4 +1,3 @@ - package mage.cards.r; import java.util.UUID; @@ -13,6 +12,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; 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); Ability ability = new LandfallAbility(Zone.BATTLEFIELD, effect, true); ability.addTarget(new TargetCreaturePermanent()); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/t/TheWretched.java b/Mage.Sets/src/mage/cards/t/TheWretched.java index 61cf1d51ceb..1adfa0bd431 100644 --- a/Mage.Sets/src/mage/cards/t/TheWretched.java +++ b/Mage.Sets/src/mage/cards/t/TheWretched.java @@ -1,6 +1,6 @@ - package mage.cards.t; +import java.util.Objects; import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; @@ -13,44 +13,51 @@ import mage.abilities.effects.common.continuous.GainControlTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; import mage.constants.Outcome; +import mage.constants.SubType; import mage.game.Game; import mage.game.combat.CombatGroup; import mage.game.permanent.Permanent; import mage.target.targetpointer.FixedTarget; import mage.watchers.common.BlockedAttackerWatcher; +import mage.watchers.common.LostControlWatcher; /** * * @author jeffwadsworth * -5/1/2009 The ability grants you control of all creatures that are blocking it as the ability resolves. This will include -* any creatures that were put onto the battlefield blocking it. -5/1/2009 Any blocking creatures that regenerated during combat will have been removed from combat. Since such creatures -* are no longer in combat, they cannot be blocking The Wretched, which means you won't be able to gain control of them. -5/1/2009 If The Wretched itself regenerated during combat, then it will have been removed from combat. Since it is no longer -* in combat, there cannot be any creatures blocking it, which means you won't be able to gain control of any creatures. -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 -* combat damage step). For example, if it's blocked by a 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. + * 5/1/2009 The ability grants you control of all creatures that are blocking it + * as the ability resolves. This will include any creatures that were put onto + * the battlefield blocking it. 5/1/2009 Any blocking creatures that regenerated + * during combat will have been removed from combat. Since such creatures are no + * longer in combat, they cannot be blocking The Wretched, which means you won't + * be able to gain control of them. 5/1/2009 If The Wretched itself regenerated + * during combat, then it will have been removed from combat. Since it is no + * longer in combat, there cannot be any creatures blocking it, which means you + * won't be able to gain control of any creatures. 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 combat damage step). For example, if it's blocked by a + * 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 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.power = new MageInt(2); 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. - 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) { @@ -83,16 +90,20 @@ class TheWretchedEffect extends OneShotEffect { if (theWretched.isRemovedFromCombat() || !theWretched.isAttacking()) { 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; } - for (CombatGroup combatGroup :game.getCombat().getGroups()) { + for (CombatGroup combatGroup : game.getCombat().getGroups()) { if (combatGroup.getAttackers().contains(source.getSourceId())) { - for(UUID creatureId: combatGroup.getBlockers()) { + for (UUID creatureId : combatGroup.getBlockers()) { Permanent blocker = game.getPermanent(creatureId); 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())); game.addEffect(effect, source); diff --git a/Mage.Sets/src/mage/cards/t/ThrullChampion.java b/Mage.Sets/src/mage/cards/t/ThrullChampion.java index e5bed4d9479..a200e4607cb 100644 --- a/Mage.Sets/src/mage/cards/t/ThrullChampion.java +++ b/Mage.Sets/src/mage/cards/t/ThrullChampion.java @@ -1,4 +1,3 @@ - package mage.cards.t; import java.util.UUID; @@ -19,6 +18,7 @@ import mage.constants.SubType; import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; 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) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{B}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{B}"); this.subtype.add(SubType.THRULL); this.power = 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}"); Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, ThrullChampionGainControlEffect, new TapSourceCost()); ability.addTarget(new TargetPermanent(filter)); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/w/Willbreaker.java b/Mage.Sets/src/mage/cards/w/Willbreaker.java index 0c1faff0ba3..3a349014808 100644 --- a/Mage.Sets/src/mage/cards/w/Willbreaker.java +++ b/Mage.Sets/src/mage/cards/w/Willbreaker.java @@ -1,4 +1,3 @@ - package mage.cards.w; import java.util.UUID; @@ -11,8 +10,8 @@ import mage.abilities.effects.common.continuous.GainControlTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; @@ -20,6 +19,7 @@ import mage.game.events.GameEvent.EventType; import mage.game.permanent.Permanent; import mage.players.Player; 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 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.WIZARD); 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. 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}"); - this.addAbility(new WillbreakerTriggeredAbility(effect)); + this.addAbility(new WillbreakerTriggeredAbility(effect), new LostControlWatcher()); } public Willbreaker(final Willbreaker card) { diff --git a/Mage.Sets/src/mage/cards/w/WillowSatyr.java b/Mage.Sets/src/mage/cards/w/WillowSatyr.java index 55c3eadd1d1..fdc4d91a6b0 100644 --- a/Mage.Sets/src/mage/cards/w/WillowSatyr.java +++ b/Mage.Sets/src/mage/cards/w/WillowSatyr.java @@ -1,4 +1,3 @@ - package mage.cards.w; import java.util.UUID; @@ -15,19 +14,20 @@ import mage.abilities.effects.common.continuous.GainControlTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; +import mage.constants.SubType; import mage.constants.SuperType; import mage.constants.Zone; import mage.filter.common.FilterCreaturePermanent; import mage.target.common.TargetCreaturePermanent; +import mage.watchers.common.LostControlWatcher; /** * * @author fireshoes */ public final class WillowSatyr extends CardImpl { - + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("legendary creature"); static { @@ -35,7 +35,7 @@ public final class WillowSatyr extends CardImpl { } 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.power = new MageInt(1); this.toughness = new MageInt(1); @@ -44,10 +44,11 @@ public final class WillowSatyr extends CardImpl { this.addAbility(new SkipUntapOptionalAbility()); // {tap}: Gain control of target legendary creature for as long as you control Willow Satyr and Willow Satyr remains tapped. ConditionalContinuousEffect effect = new ConditionalContinuousEffect( - 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"); + 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"); Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, effect, new TapSourceCost()); ability.addTarget(new TargetCreaturePermanent(filter)); + ability.addWatcher(new LostControlWatcher()); this.addAbility(ability); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/GoadTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/GoadTest.java index b644006b878..02ef1d7332d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/GoadTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/GoadTest.java @@ -1,81 +1,120 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package org.mage.test.cards.abilities.keywords; - -import java.io.FileNotFoundException; -import mage.constants.MultiplayerAttackOption; -import mage.constants.PhaseStep; -import mage.constants.RangeOfInfluence; -import mage.constants.Zone; -import mage.game.FreeForAll; -import mage.game.Game; -import mage.game.GameException; -import mage.game.mulligan.MulliganType; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestMultiPlayerBase; - -/** - * - * @author LevelX2 - */ -public class GoadTest extends CardTestMultiPlayerBase { - - @Override - protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 40); - // Player order: A -> D -> C -> B - playerA = createPlayer(game, playerA, "PlayerA"); - playerB = createPlayer(game, playerB, "PlayerB"); - playerC = createPlayer(game, playerC, "PlayerC"); - playerD = createPlayer(game, playerD, "PlayerD"); - return game; - } - - /** - * 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 - * caused the creatures damaged by Marisi's controller to be goaded. - * However, when the goaded creatures went to attack, they could not attack - * me but could attack the (former) controller of Marisi. - */ - @Test - public void goadWithNotOwnedCreatureTest() { - // 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, playerA, "Marisi, Breaker of the Coil", 1); // Creature 5/4 - - // Untap target creature an opponent controls and gain control of it until end of turn. - // That creature gains haste until end of turn. - // When you lose control of the creature, tap it. - addCard(Zone.HAND, playerD, "Ray of Command"); // Instant {3}{U} - addCard(Zone.BATTLEFIELD, playerD, "Island", 4); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion", 3); // Creature 2/2 - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Ray of Command", "Marisi, Breaker of the Coil"); - - attack(2, playerD, "Marisi, Breaker of the Coil", playerC); - - attack(3, playerC, "Silvercoat Lion", playerA); - attack(3, playerC, "Silvercoat Lion", playerB); - attack(3, playerC, "Silvercoat Lion", playerD); - - setStopAt(4, PhaseStep.BEGIN_COMBAT); - execute(); - - - assertGraveyardCount(playerD, "Ray of Command", 1); - assertPermanentCount(playerA, "Marisi, Breaker of the Coil", 1); - - assertLife(playerC, 35); - - assertLife(playerB, 38); - assertLife(playerA, 38); - assertLife(playerD, 38); - } - -} +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.cards.abilities.keywords; + +import java.io.FileNotFoundException; +import mage.constants.MultiplayerAttackOption; +import mage.constants.PhaseStep; +import mage.constants.RangeOfInfluence; +import mage.constants.Zone; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.mulligan.MulliganType; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +/** + * + * @author LevelX2 + */ +public class GoadTest extends CardTestMultiPlayerBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 40); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, playerA, "PlayerA"); + playerB = createPlayer(game, playerB, "PlayerB"); + playerC = createPlayer(game, playerC, "PlayerC"); + playerD = createPlayer(game, playerD, "PlayerD"); + 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 + * Coils (until end of turn) and did combat damage to another player. This + * caused the creatures damaged by Marisi's controller to be goaded. + * However, when the goaded creatures went to attack, they could not attack + * me but could attack the (former) controller of Marisi. + */ + @Test + public void goadWithNotOwnedCreatureTest() { + // 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, playerA, "Marisi, Breaker of the Coil", 1); // Creature 5/4 + + // Untap target creature an opponent controls and gain control of it until end of turn. + // That creature gains haste until end of turn. + // When you lose control of the creature, tap it. + addCard(Zone.HAND, playerD, "Ray of Command"); // Instant {3}{U} + addCard(Zone.BATTLEFIELD, playerD, "Island", 4); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion", 1); // Creature 2/2 + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Ray of Command", "Marisi, Breaker of the Coil"); + + attack(2, playerD, "Marisi, Breaker of the Coil", playerC); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertGraveyardCount(playerD, "Ray of Command", 1); + assertPermanentCount(playerA, "Marisi, Breaker of the Coil", 1); + + 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); + + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/conditional/TheWretchedTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/conditional/TheWretchedTest.java index 7a4f461f518..b18bdaeacc1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/conditional/TheWretchedTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/conditional/TheWretchedTest.java @@ -22,11 +22,11 @@ public class TheWretchedTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "The Wretched"); addCard(Zone.BATTLEFIELD, playerB, "Wall of Pine Needles"); // a 3/3 with regeneration addCard(Zone.BATTLEFIELD, playerB, "Living Wall"); // 0/6 Wall with regeneration - + attack(3, playerA, "The Wretched"); block(3, playerB, "Wall of Pine Needles", "The Wretched"); block(3, playerB, "Living Wall", "The Wretched"); - setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + setStopAt(3, PhaseStep.END_TURN); execute(); assertPermanentCount(playerA, "The Wretched", 1); @@ -39,17 +39,17 @@ public class TheWretchedTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "The Wretched"); addCard(Zone.BATTLEFIELD, playerA, "Bad Moon"); // +1/+1 for black creatures - + addCard(Zone.BATTLEFIELD, playerB, "Forest"); // a 3/3 with regeneration addCard(Zone.BATTLEFIELD, playerB, "Wall of Pine Needles"); // a 3/3 with regeneration addCard(Zone.BATTLEFIELD, playerB, "Living Wall"); // 0/6 Wall with regeneration - + // The Wretched // Creature — Demon - Demon 2/5 attack(3, playerA, "The Wretched"); block(3, playerB, "Wall of Pine Needles", "The Wretched"); block(3, playerB, "Living Wall", "The Wretched"); - + activateAbility(3, PhaseStep.DECLARE_BLOCKERS, playerB, "{G}: Regenerate {this}."); // Wall of Pine Needles setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); @@ -57,53 +57,52 @@ public class TheWretchedTest extends CardTestPlayerBase { assertPermanentCount(playerA, "The Wretched", 1); assertPermanentCount(playerA, "Living Wall", 1); - + assertPermanentCount(playerB, "Wall of Pine Needles", 1); - } - + @Test 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, playerB, "Wall of Pine Needles"); // a 3/3 with regeneration addCard(Zone.BATTLEFIELD, playerB, "Living Wall"); // 0/6 Wall with regeneration addCard(Zone.BATTLEFIELD, playerB, "Island", 4); addCard(Zone.HAND, playerB, "Control Magic"); - + attack(3, playerA, "The Wretched"); block(3, playerB, "Wall of Pine Needles", "The Wretched"); block(3, playerB, "Living Wall", "The Wretched"); - + castSpell(4, PhaseStep.POSTCOMBAT_MAIN, playerB, "Control Magic", "The Wretched"); setStopAt(4, PhaseStep.END_TURN); execute(); assertPermanentCount(playerB, "The Wretched", 1); + assertPermanentCount(playerA, "Wall of Pine Needles", 0); assertPermanentCount(playerB, "Wall of Pine Needles", 1); assertPermanentCount(playerB, "Living Wall", 1); } - + @Test public void testRegenTheWretchedThusRemovingFromCombat() { addCard(Zone.BATTLEFIELD, playerA, "The Wretched"); - addCard(Zone.HAND, playerA, "Regenerate"); - addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); - + addCard(Zone.HAND, playerA, "Regenerate"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + 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"); block(3, playerB, "Wall of Pine Needles", "The Wretched"); block(3, playerB, "Wall of Spears", "The Wretched"); - + castSpell(3, PhaseStep.DECLARE_BLOCKERS, playerA, "Regenerate", "The Wretched"); - + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); execute(); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/copy/LazavDimirMastermindTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/copy/LazavDimirMastermindTest.java index c5b357266a0..c2bf7afa6ee 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/copy/LazavDimirMastermindTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/copy/LazavDimirMastermindTest.java @@ -1,9 +1,9 @@ package org.mage.test.cards.copy; import mage.abilities.keyword.DeathtouchAbility; -import mage.constants.SubType; import mage.abilities.keyword.FlyingAbility; import mage.constants.PhaseStep; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.permanent.Permanent; import org.junit.Assert; @@ -14,12 +14,11 @@ import org.mage.test.serverside.base.CardTestPlayerBase; * * Lazav, Dimir Mastermind * - * Legendary Creature — Shapeshifter 3/3, UUBB - * Hexproof - * Whenever a creature card is put into an opponent's graveyard from anywhere, you may have - * Lazav, Dimir Mastermind become a copy of that card except its name is still - * Lazav, Dimir Mastermind, it's legendary in addition to its other types, and - * it gains hexproof and this ability. + * Legendary Creature — Shapeshifter 3/3, UUBB Hexproof Whenever a creature card + * is put into an opponent's graveyard from anywhere, you may have Lazav, Dimir + * Mastermind become a copy of that card except its name is still Lazav, Dimir + * Mastermind, it's legendary in addition to its other types, and it gains + * hexproof and this ability. * * @author LevelX2 */ @@ -32,25 +31,26 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase { public void testCopySimpleCreature() { addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); // 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. addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); // Flying 3/2 - addCard(Zone.LIBRARY, playerB, "Assault Griffin",5); + addCard(Zone.LIBRARY, playerB, "Assault Griffin", 5); 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); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, "Lazav, Dimir Mastermind", 1); assertPowerToughness(playerA, "Lazav, Dimir Mastermind", 3, 2); Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId()); 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. // Rats you control have deathtouch. - addCard(Zone.LIBRARY, playerB, "Ogre Slumlord",5); + addCard(Zone.LIBRARY, playerB, "Ogre Slumlord", 5); 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); execute(); @@ -86,11 +86,10 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase { /** * Tests copy Nightveil Specter * - * Nightveil Specter - * Creature — Specter 2/3, {U/B}{U/B}{U/B} - * Flying - * Whenever Nightveil Specter deals combat damage to a player, that player exiles the top card of their library. - * You may play cards exiled with Nightveil Specter. + * Nightveil Specter Creature — Specter 2/3, {U/B}{U/B}{U/B} Flying Whenever + * Nightveil Specter deals combat damage to a player, that player exiles the + * top card of their library. You may play cards exiled with Nightveil + * Specter. * */ @Test @@ -99,11 +98,11 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); - addCard(Zone.LIBRARY, playerB, "Silvercoat Lion",2); - addCard(Zone.LIBRARY, playerB, "Nightveil Specter",1); + addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 2); + addCard(Zone.LIBRARY, playerB, "Nightveil Specter", 1); 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"); @@ -130,15 +129,15 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); - addCard(Zone.LIBRARY, playerB, "Silvercoat Lion",2); - addCard(Zone.LIBRARY, playerB, "Nightveil Specter",1); + addCard(Zone.LIBRARY, playerB, "Silvercoat Lion", 2); + addCard(Zone.LIBRARY, playerB, "Nightveil Specter", 1); skipInitShuffling(); // 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 - 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); execute(); @@ -151,52 +150,52 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase { Assert.assertTrue(lazav.isLegendary()); } + /** * Tests old copy is discarded after reanmiation of Lazav */ @Test 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. - addCard(Zone.HAND, playerA ,"Reanimate"); + addCard(Zone.HAND, playerA, "Reanimate"); addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); // 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. addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); - addCard(Zone.BATTLEFIELD, playerB ,"Swamp", 3); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); // 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. - addCard(Zone.HAND, playerB ,"Tribute to Hunger"); - + addCard(Zone.HAND, playerB, "Tribute to Hunger"); + 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, playerA, "Reanimate", "Lazav, Dimir Mastermind"); - + setStopAt(1, PhaseStep.END_TURN); execute(); assertGraveyardCount(playerB, "Tribute to Hunger", 1); assertGraveyardCount(playerA, "Reanimate", 1); - + assertLife(playerA, 16); // -4 from Reanmiate - assertLife(playerB, 22); // +3 from Tribute to Hunger because Lazav is 3/2 - + assertLife(playerB, 22); // +3 from Tribute to Hunger because Lazav is 3/2 + assertPermanentCount(playerA, "Lazav, Dimir Mastermind", 1); assertPowerToughness(playerA, "Lazav, Dimir Mastermind", 3, 3); Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId()); 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())); + } - + /** * Tests if Lazav remains a copy of the creature after it is exiled */ @@ -204,20 +203,20 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase { public void testCopyCreatureExiled() { addCard(Zone.BATTLEFIELD, playerA, "Lazav, Dimir Mastermind", 1); // 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. addCard(Zone.BATTLEFIELD, playerA, "Codex Shredder", 1); - + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - + addCard(Zone.HAND, playerA, "Rest in Peace", 1); // Flying 3/2 - addCard(Zone.LIBRARY, playerB, "Assault Griffin",5); + addCard(Zone.LIBRARY, playerB, "Assault Griffin", 5); 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"); setStopAt(1, PhaseStep.END_TURN); @@ -228,6 +227,6 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase { Permanent lazav = getPermanent("Lazav, Dimir Mastermind", playerA.getId()); 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())); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index a4627fe0f8d..f15f7c13630 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -1,5 +1,10 @@ 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.MageObject; import mage.MageObjectReference; @@ -57,13 +62,6 @@ import mage.util.CardUtil; import org.apache.log4j.Logger; import org.junit.Assert; 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.*; /** @@ -193,7 +191,7 @@ public class TestPlayer implements Player { /** * @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) { this.maxCallsWithoutAction = maxCallsWithoutAction; @@ -1046,13 +1044,13 @@ public class TestPlayer implements Player { List data = cards.stream() .map(c -> (((c instanceof PermanentToken) ? "[T] " : "[C] ") - + c.getIdName() - + (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "") - + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() - + (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") - + ", " + (c.isTapped() ? "Tapped" : "Untapped") - + getPrintableAliases(", [", c.getId(), "]") - + (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName()))) + + c.getIdName() + + (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "") + + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() + + (c.isPlaneswalker() ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") + + ", " + (c.isTapped() ? "Tapped" : "Untapped") + + getPrintableAliases(", [", c.getId(), "]") + + (c.getAttachedTo() == null ? "" : ", attached to " + game.getPermanent(c.getAttachedTo()).getIdName()))) .sorted() .collect(Collectors.toList()); @@ -1076,12 +1074,12 @@ public class TestPlayer implements Player { List data = abilities.stream() .map(a -> (a.getZone() + " -> " - + a.getSourceObject(game).getIdName() + " -> " - + (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified - + (a.toString().length() > 0 - ? a.toString().substring(0, Math.min(20, a.toString().length())) - : a.getClass().getSimpleName()) - + "...")) + + a.getSourceObject(game).getIdName() + " -> " + + (a.toString().startsWith("Cast ") ? "[" + a.getManaCostsToPay().getText() + "] -> " : "") // printed cost, not modified + + (a.toString().length() > 0 + ? a.toString().substring(0, Math.min(20, a.toString().length())) + : a.getClass().getSimpleName()) + + "...")) .sorted() .collect(Collectors.toList()); @@ -1446,7 +1444,7 @@ public class TestPlayer implements Player { UUID defenderId = null; boolean mustAttackByAction = false; boolean madeAttackByAction = false; - for (Iterator it = actions.iterator(); it.hasNext(); ) { + for (Iterator it = actions.iterator(); it.hasNext();) { PlayerAction action = it.next(); // aiXXX commands @@ -2021,7 +2019,7 @@ public class TestPlayer implements Player { // skip targets if (targets.get(0).equals(TARGET_SKIP)) { 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()); targets.remove(0); return true; @@ -2326,7 +2324,7 @@ public class TestPlayer implements Player { this.chooseStrictModeFailed("choice", game, "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); } @@ -3496,7 +3494,7 @@ public class TestPlayer implements Player { @Override 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 return choose(outcome, target, sourceId, game, null); @@ -3504,7 +3502,7 @@ public class TestPlayer implements Player { @Override public boolean choose(Outcome outcome, Cards cards, - TargetCard target, Game game + TargetCard target, Game game ) { assertAliasSupportInChoices(false); if (!choices.isEmpty()) { @@ -3541,7 +3539,7 @@ public class TestPlayer implements Player { @Override 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) // 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 if (targets.get(0).equals(TARGET_SKIP)) { 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()); targets.remove(0); return false; // false in chooseTargetAmount = stop to choose @@ -3607,15 +3605,15 @@ public class TestPlayer implements Player { @Override public boolean choosePile(Outcome outcome, String message, - List pile1, List pile2, - Game game + List pile1, List pile2, + Game game ) { return computerPlayer.choosePile(outcome, message, pile1, pile2, game); } @Override public boolean playMana(Ability ability, ManaCost unpaid, - String promptText, Game game + String promptText, Game game ) { groupsForTargetHandling = null; @@ -3665,15 +3663,15 @@ public class TestPlayer implements Player { @Override public UUID chooseBlockerOrder(List blockers, CombatGroup combatGroup, - List blockerOrder, Game game + List blockerOrder, Game game ) { return computerPlayer.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game); } @Override public void assignDamage(int damage, List targets, - String singleTargetName, UUID sourceId, - Game game + String singleTargetName, UUID sourceId, + Game game ) { computerPlayer.assignDamage(damage, targets, singleTargetName, sourceId, game); } @@ -3692,14 +3690,14 @@ public class TestPlayer implements Player { @Override public void pickCard(List cards, Deck deck, - Draft draft + Draft draft ) { computerPlayer.pickCard(cards, deck, draft); } @Override public boolean scry(int value, Ability source, - Game game + Game game ) { // Don't scry at the start of the game. if (game.getTurnNum() == 1 && game.getStep() == null) { @@ -3710,44 +3708,44 @@ public class TestPlayer implements Player { @Override public boolean surveil(int value, Ability source, - Game game + Game game ) { return computerPlayer.surveil(value, source, game); } @Override public boolean moveCards(Card card, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return computerPlayer.moveCards(card, toZone, source, game); } @Override public boolean moveCards(Card card, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { return computerPlayer.moveCards(card, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); } @Override public boolean moveCards(Cards cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return computerPlayer.moveCards(cards, toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return computerPlayer.moveCards(cards, toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { return computerPlayer.moveCards(cards, toZone, source, game, tapped, faceDown, byOwner, appliedEffects); } diff --git a/Mage/src/main/java/mage/abilities/condition/common/SourceOnBattlefieldControlUnchangedCondition.java b/Mage/src/main/java/mage/abilities/condition/common/SourceOnBattlefieldControlUnchangedCondition.java index b4d46d2ade0..d1e9235db03 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/SourceOnBattlefieldControlUnchangedCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/SourceOnBattlefieldControlUnchangedCondition.java @@ -1,31 +1,43 @@ - package mage.abilities.condition.common; -import java.util.Objects; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.condition.Condition; import mage.game.Game; -import mage.game.permanent.Permanent; +import mage.watchers.common.LostControlWatcher; /** - * This condition remembers controller on the first apply. - * As long as this controller keeps unchanged and the source is - * on the battlefield, the condition is true. + * This condition checks if ever since first call of the apply method the + * controller of the source has changed + * + * 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 */ public class SourceOnBattlefieldControlUnchangedCondition implements Condition { - - private UUID controllerId; + + private Long checkingSince; + private int startingZoneChangeCounter; @Override public boolean apply(Game game, Ability source) { - if (controllerId == null) { - controllerId = source.getControllerId(); + if (checkingSince == null) { + checkingSince = System.currentTimeMillis() - 1; + startingZoneChangeCounter = game.getState().getZoneChangeCounter(source.getSourceId()); } - Permanent permanent = game.getBattlefield().getPermanent(source.getSourceId()); - return (permanent != null && Objects.equals(controllerId, source.getControllerId())); + if (game.getState().getZoneChangeCounter(source.getSourceId()) > startingZoneChangeCounter) { + return false; + } + LostControlWatcher watcher = game.getState().getWatcher(LostControlWatcher.class); + if (watcher != null) { + return checkingSince > watcher.getOrderOfLastLostControl(source.getSourceId()); + } + throw new UnsupportedOperationException("LostControlWatcher not found!"); } } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java index cdea7ccc337..11a001c5275 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java @@ -1,5 +1,9 @@ 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.abilities.Ability; import mage.constants.DependencyType; @@ -9,11 +13,6 @@ import mage.constants.SubLayer; import mage.game.Game; 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 */ @@ -67,8 +66,6 @@ public interface ContinuousEffect extends Effect { UUID getStartingController(); - void incYourTurnNumPlayed(); - boolean isYourNextTurn(Game game); @Override diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index fd3f20e1635..e2f8e2f159e 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -1,5 +1,6 @@ package mage.abilities.effects; +import java.util.*; import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.CompoundAbility; @@ -19,8 +20,6 @@ import mage.game.stack.StackObject; import mage.players.Player; import mage.target.targetpointer.TargetPointer; -import java.util.*; - /** * @author BetaSteward_at_googlemail.com, JayDi85 */ @@ -47,9 +46,9 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu protected boolean characterDefining = false; // 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 boolean startingTurnWasActive; - private int yourTurnNumPlayed = 0; // turnes played after effect was created + private UUID startingControllerId; // player to check for turn duration (can't different with real controller ability) + private boolean startingTurnWasActive; // effect started during related players turn and related players turn was already active + private int effectStartingOnTurn = 0; // turn the effect started public ContinuousEffectImpl(Duration duration, Outcome outcome) { super(outcome); @@ -79,7 +78,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu this.temporary = effect.temporary; this.startingControllerId = effect.startingControllerId; this.startingTurnWasActive = effect.startingTurnWasActive; - this.yourTurnNumPlayed = effect.yourTurnNumPlayed; + this.effectStartingOnTurn = effect.effectStartingOnTurn; this.dependencyTypes = effect.dependencyTypes; this.dependendToTypes = effect.dependendToTypes; this.characterDefining = effect.characterDefining; @@ -191,23 +190,13 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu this.startingControllerId = startingController; this.startingTurnWasActive = activePlayerId != null && activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too - this.yourTurnNumPlayed = 0; - } - - @Override - public void incYourTurnNumPlayed() { - yourTurnNumPlayed++; + this.effectStartingOnTurn = game.getTurnNum(); } @Override public boolean isYourNextTurn(Game game) { - if (this.startingTurnWasActive) { - return yourTurnNumPlayed == 1 - && game.isActivePlayer(startingControllerId); - } else { - return yourTurnNumPlayed == 0 - && game.isActivePlayer(startingControllerId); - } + return effectStartingOnTurn < game.getTurnNum() + && game.isActivePlayer(startingControllerId); } @Override @@ -367,6 +356,9 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu /** * Auto-generates dependencies on different effects (what's apply first and * what's apply second) + * + * @param abilityToGain + * @param filterToSearch */ public void generateGainAbilityDependencies(Ability abilityToGain, Filter filterToSearch) { this.addDependencyType(DependencyType.AddingAbility); diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 022eefef318..516cee562bb 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -1,5 +1,9 @@ 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.MageObjectReference; import mage.abilities.Ability; @@ -26,11 +30,6 @@ import mage.target.common.TargetCardInHand; import mage.util.CardUtil; 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 */ @@ -163,28 +162,17 @@ public class ContinuousEffects implements Serializable { 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 getLayeredEffects(Game game) { 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 */ public synchronized List getLayeredEffects(Game game, String timestampGroupName) { @@ -229,8 +217,10 @@ public class ContinuousEffects implements Serializable { * "actual" meaning it becomes turned on that is defined by * Ability.#isInUseableZone(Game, boolean) method in * #getLayeredEffects(Game). - *

- * 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 */ @@ -359,7 +349,7 @@ public class ContinuousEffects implements Serializable { } // boolean checkLKI = event.getType().equals(EventType.ZONE_CHANGE) || event.getType().equals(EventType.DESTROYED_PERMANENT); //get all applicable transient Replacement effects - for (Iterator iterator = replacementEffects.iterator(); iterator.hasNext(); ) { + for (Iterator iterator = replacementEffects.iterator(); iterator.hasNext();) { ReplacementEffect effect = iterator.next(); if (!effect.checksEventType(event, game)) { continue; @@ -392,7 +382,7 @@ public class ContinuousEffects implements Serializable { } } - for (Iterator iterator = preventionEffects.iterator(); iterator.hasNext(); ) { + for (Iterator iterator = preventionEffects.iterator(); iterator.hasNext();) { PreventionEffect effect = iterator.next(); if (!effect.checksEventType(event, game)) { continue; @@ -768,8 +758,8 @@ public class ContinuousEffects implements Serializable { * @param event * @param targetAbility ability the event is attached to. can be null. * @param game - * @param silentMode true if the event does not really happen but it's - * checked if the event would be replaced + * @param silentMode true if the event does not really happen but it's + * checked if the event would be replaced * @return */ public boolean preventedByRuleModification(GameEvent event, Ability targetAbility, Game game, boolean silentMode) { @@ -817,7 +807,7 @@ public class ContinuousEffects implements Serializable { do { Map> rEffects = getApplicableReplacementEffects(event, game); // Remove all consumed effects (ability dependant) - for (Iterator it1 = rEffects.keySet().iterator(); it1.hasNext(); ) { + for (Iterator it1 = rEffects.keySet().iterator(); it1.hasNext();) { ReplacementEffect entry = it1.next(); if (consumed.containsKey(entry.getId()) /*&& !(entry instanceof CommanderReplacementEffect) */) { // 903.9. Set consumedAbilitiesIds = consumed.get(entry.getId()); @@ -1002,7 +992,7 @@ public class ContinuousEffects implements Serializable { .entrySet() .stream() .filter(entry -> dependentTo.contains(entry.getKey().getId()) - && entry.getValue().contains(effect.getId())) + && entry.getValue().contains(effect.getId())) .forEach(entry -> { entry.getValue().remove(effect.getId()); dependentTo.remove(entry.getKey().getId()); @@ -1036,7 +1026,7 @@ public class ContinuousEffects implements Serializable { continue; } // check if waiting effects can be applied now - for (Iterator>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext(); ) { + for (Iterator>> iterator = waitingEffects.entrySet().iterator(); iterator.hasNext();) { Map.Entry> entry = iterator.next(); if (!appliedEffects.containsAll(entry.getValue())) { // all dependent to effects are applied now so apply the effect itself continue; @@ -1283,18 +1273,19 @@ public class ContinuousEffects implements Serializable { } private void setControllerForEffect(ContinuousEffectsList effects, UUID sourceId, UUID controllerId) { - for (Effect effect : effects) { - Set abilities = effects.getAbility(effect.getId()); - for (Ability ability : abilities) { - if (ability.getSourceId() != null) { - if (ability.getSourceId().equals(sourceId)) { - ability.setControllerId(controllerId); + for (ContinuousEffect effect : effects) { + if (!effect.getDuration().isFixedController()) { + Set abilities = effects.getAbility(effect.getId()); + for (Ability ability : abilities) { + if (ability.getSourceId() != null) { + 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); } } - } } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java index 26e92886549..803cdc5bc14 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java @@ -1,5 +1,6 @@ package mage.abilities.effects; +import java.util.*; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.MageSingleton; @@ -10,8 +11,6 @@ import mage.game.Game; import mage.players.Player; import org.apache.log4j.Logger; -import java.util.*; - /** * @param * @author BetaSteward_at_googlemail.com @@ -47,7 +46,7 @@ public class ContinuousEffectsList extends ArrayList public void removeEndOfTurnEffects(Game game) { // calls every turn on cleanup step (only end of turn duration) // rules 514.2 - for (Iterator i = this.iterator(); i.hasNext(); ) { + for (Iterator i = this.iterator(); i.hasNext();) { T entry = i.next(); boolean canRemove = false; switch (entry.getDuration()) { @@ -67,7 +66,7 @@ public class ContinuousEffectsList extends ArrayList public void removeEndOfCombatEffects() { - for (Iterator i = this.iterator(); i.hasNext(); ) { + for (Iterator i = this.iterator(); i.hasNext();) { T entry = i.next(); if (entry.getDuration() == Duration.EndOfCombat) { i.remove(); @@ -77,7 +76,7 @@ public class ContinuousEffectsList extends ArrayList } public void removeInactiveEffects(Game game) { - for (Iterator i = this.iterator(); i.hasNext(); ) { + for (Iterator i = this.iterator(); i.hasNext();) { T entry = i.next(); if (isInactive(entry, game)) { i.remove(); @@ -86,15 +85,6 @@ public class ContinuousEffectsList extends ArrayList } } - public void incYourTurnNumPlayed(Game game) { - for (Iterator i = this.iterator(); i.hasNext(); ) { - T entry = i.next(); - if (game.isActivePlayer(entry.getStartingController())) { - entry.incYourTurnNumPlayed(); - } - } - } - private boolean isInactive(T effect, Game game) { // ends all inactive effects -- calls on player leave or apply new effect if (game.getState().isGameOver()) { @@ -109,9 +99,8 @@ public class ContinuousEffectsList extends ArrayList 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 order who’s still in the game. - */ + */ // objects removes doing in player.leave() call... effects removes is here - Set set = effectAbilityMap.get(effect.getId()); if (set == null) { logger.debug("No abilities for effect found: " + effect.toString()); @@ -224,7 +213,7 @@ public class ContinuousEffectsList extends ArrayList abilities.removeAll(abilitiesToRemove); } if (abilities == null || abilities.isEmpty()) { - for (Iterator iterator = this.iterator(); iterator.hasNext(); ) { + for (Iterator iterator = this.iterator(); iterator.hasNext();) { ContinuousEffect effect = iterator.next(); if (effect.getId().equals(effectIdToRemove)) { iterator.remove(); diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackYouEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackYouEffect.java index 462394887b2..d13c067608c 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackYouEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackYouEffect.java @@ -1,32 +1,23 @@ package mage.abilities.effects.common.combat; +import java.util.UUID; import mage.abilities.Ability; import mage.abilities.effects.RestrictionEffect; import mage.constants.Duration; import mage.game.Game; import mage.game.permanent.Permanent; -import java.util.UUID; - /** * @author TheElk801 */ public class CantAttackYouEffect extends RestrictionEffect { - UUID controllerId; - public CantAttackYouEffect(Duration duration) { super(duration); } - public CantAttackYouEffect(Duration duration, UUID controllerId) { - super(duration); - this.controllerId = controllerId; - } - public CantAttackYouEffect(final CantAttackYouEffect effect) { super(effect); - this.controllerId = effect.controllerId; } @Override @@ -44,9 +35,6 @@ public class CantAttackYouEffect extends RestrictionEffect { if (defenderId == null) { return true; } - if (controllerId == null) { - controllerId = source.getControllerId(); - } - return !defenderId.equals(controllerId); + return !defenderId.equals(source.getControllerId()); } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/GoadTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/GoadTargetEffect.java index fe5d9a89237..ffb6e25cbe6 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/combat/GoadTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/GoadTargetEffect.java @@ -41,18 +41,18 @@ public class GoadTargetEffect extends OneShotEffect { Permanent targetCreature = game.getPermanent(getTargetPointer().getFirst(game, source)); Player controller = game.getPlayer(source.getControllerId()); 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 /* If the creature doesn’t 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 can’t attack any of those + the controller of the spell or ability that goaded it if able. If the creature can’t attack any of those players but could otherwise attack, it must attack an opposing planeswalker (controlled by any opponent) or the player that goaded it. (2016-08-23) */ ContinuousEffect effect = new AttacksIfAbleTargetEffect(Duration.UntilYourNextTurn); effect.setTargetPointer(new FixedTarget(getTargetPointer().getFirst(game, 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))); game.addEffect(effect, source); game.informPlayers(controller.getLogName() + " is goading " + targetCreature.getLogName()); diff --git a/Mage/src/main/java/mage/constants/Duration.java b/Mage/src/main/java/mage/constants/Duration.java index 0ec5060ed06..1b857000ae0 100644 --- a/Mage/src/main/java/mage/constants/Duration.java +++ b/Mage/src/main/java/mage/constants/Duration.java @@ -4,25 +4,27 @@ package mage.constants; * @author North */ public enum Duration { - OneUse("", true), - EndOfGame("for the rest of the game", false), - WhileOnBattlefield("", false), - WhileOnStack("", false), - WhileInGraveyard("", false), - EndOfTurn("until end of turn", true), - UntilYourNextTurn("until your next turn", true), - UntilEndOfYourNextTurn("until the end of your next turn", true), - UntilSourceLeavesBattlefield("until {source} leaves the battlefield", true), // supported for continuous layered effects - EndOfCombat("until end of combat", true), - EndOfStep("until end of phase step", true), - Custom("", true); + OneUse("", true, true), + EndOfGame("for the rest of the game", false, false), + WhileOnBattlefield("", false, false), + WhileOnStack("", false, true), + WhileInGraveyard("", false, false), + EndOfTurn("until end of turn", true, true), + UntilYourNextTurn("until your next turn", true, true), + UntilEndOfYourNextTurn("until the end of your next turn", true, true), + UntilSourceLeavesBattlefield("until {source} leaves the battlefield", true, false), // supported for continuous layered effects + EndOfCombat("until end of combat", true, true), + EndOfStep("until end of phase step", true, true), + Custom("", true, true); 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.onlyValidIfNoZoneChange = onlyValidIfNoZoneChange; + this.fixedController = fixedController; } @Override @@ -34,4 +36,7 @@ public enum Duration { return onlyValidIfNoZoneChange; } + public boolean isFixedController() { + return fixedController; + } } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index bed1000a87e..a1f0d9e9cfa 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1,5 +1,9 @@ package mage.game; +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; import mage.MageException; import mage.MageObject; import mage.abilities.*; @@ -67,11 +71,6 @@ import mage.util.functions.ApplyToPermanent; import mage.watchers.common.*; 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 { private static final int ROLLBACK_TURNS_MAX = 4; @@ -1549,7 +1548,7 @@ public abstract class GameImpl implements Game, Serializable { /** * @param emblem * @param sourceObject - * @param toPlayerId controller and owner of the emblem + * @param toPlayerId controller and owner of the emblem */ @Override public void addEmblem(Emblem emblem, MageObject sourceObject, UUID toPlayerId) { @@ -1567,8 +1566,8 @@ public abstract class GameImpl implements Game, Serializable { /** * @param plane * @param sourceObject - * @param toPlayerId controller and owner of the plane (may only be one per - * game..) + * @param toPlayerId controller and owner of the plane (may only be one per + * game..) * @return boolean - whether the plane was added successfully or not */ @Override @@ -1804,7 +1803,7 @@ public abstract class GameImpl implements Game, Serializable { break; } // triggered abilities that don't use the stack have to be executed first (e.g. Banisher Priest Return exiled creature - for (Iterator it = abilities.iterator(); it.hasNext(); ) { + for (Iterator it = abilities.iterator(); it.hasNext();) { TriggeredAbility triggeredAbility = it.next(); if (!triggeredAbility.isUsesStack()) { state.removeTriggeredAbility(triggeredAbility); @@ -1917,8 +1916,8 @@ public abstract class GameImpl implements Game, Serializable { */ boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream() .anyMatch(filter -> filter.match(perm, this)); - int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ? - // Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isn’t destroyed unless it has at least 1 damage marked on it. + int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality + ? // Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isn’t destroyed unless it has at least 1 damage marked on it. Math.max(perm.getPower().getValue(), 1) : perm.getToughness().getValue(); if (lethalDamageThreshold <= perm.getDamage() || perm.isDeathtouched()) { if (perm.destroy(null, this, false)) { @@ -2249,7 +2248,6 @@ public abstract class GameImpl implements Game, Serializable { } //TODO: implement the rest - return somethingHappened; } @@ -2557,7 +2555,7 @@ public abstract class GameImpl implements Game, Serializable { } //20100423 - 800.4a Set toOutside = new HashSet<>(); - for (Iterator it = getBattlefield().getAllPermanents().iterator(); it.hasNext(); ) { + for (Iterator it = getBattlefield().getAllPermanents().iterator(); it.hasNext();) { Permanent perm = it.next(); if (perm.isOwnedBy(playerId)) { 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); } // needed to send event that permanent leaves the battlefield to allow non stack effects to execute player.moveCards(toOutside, Zone.OUTSIDE, null, this); // triggered abilities that don't use the stack have to be executed List abilities = state.getTriggered(player.getId()); - for (Iterator it = abilities.iterator(); it.hasNext(); ) { + for (Iterator it = abilities.iterator(); it.hasNext();) { TriggeredAbility triggeredAbility = it.next(); if (!triggeredAbility.isUsesStack()) { state.removeTriggeredAbility(triggeredAbility); @@ -2622,7 +2620,7 @@ public abstract class GameImpl implements Game, Serializable { // Remove cards from the player in all exile zones for (ExileZone exile : this.getExile().getExileZones()) { - for (Iterator it = exile.iterator(); it.hasNext(); ) { + for (Iterator it = exile.iterator(); it.hasNext();) { Card card = this.getCard(it.next()); if (card != null && card.isOwnedBy(playerId)) { it.remove(); @@ -2632,7 +2630,7 @@ public abstract class GameImpl implements Game, Serializable { //Remove all commander/emblems/plane the player controls boolean addPlaneAgain = false; - for (Iterator it = this.getState().getCommand().iterator(); it.hasNext(); ) { + for (Iterator it = this.getState().getCommand().iterator(); it.hasNext();) { CommandObject obj = it.next(); if (obj.isControlledBy(playerId)) { if (obj instanceof Emblem) { diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 1530fc06456..dabfb437001 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -1,5 +1,9 @@ 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.abilities.*; import mage.abilities.effects.ContinuousEffect; @@ -35,12 +39,6 @@ import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import mage.watchers.Watchers; -import java.io.Serializable; -import java.util.*; -import java.util.stream.Collectors; - -import static java.util.Collections.emptyList; - /** * @author BetaSteward_at_googlemail.com *

@@ -179,8 +177,8 @@ public class GameState implements Serializable, Copyable { this.copiedCards.putAll(state.copiedCards); this.permanentOrderNumber = state.permanentOrderNumber; this.applyEffectsCounter = state.applyEffectsCounter; - state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter) -> - this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy())); + state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter) + -> this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy())); } public void restoreForRollBack(GameState state) { @@ -226,8 +224,8 @@ public class GameState implements Serializable, Copyable { this.copiedCards = state.copiedCards; this.permanentOrderNumber = state.permanentOrderNumber; this.applyEffectsCounter = state.applyEffectsCounter; - state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter) -> - this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy())); + state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter) + -> this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy())); } @Override @@ -605,7 +603,6 @@ public class GameState implements Serializable, Copyable { delayed.removeEndOfTurnAbilities(game); exile.cleanupEndOfTurnZones(game); game.applyEffects(); - effects.incYourTurnNumPlayed(game); } public void addEffect(ContinuousEffect effect, Ability source) { @@ -623,7 +620,6 @@ public class GameState implements Serializable, Copyable { // public void addMessage(String message) { // this.messages.add(message); // } - /** * Returns a list of all players of the game ignoring range or if a player * has lost or left the game. @@ -797,7 +793,7 @@ public class GameState implements Serializable, Copyable { for (Map.Entry> entry : eventsByKey.entrySet()) { Set movedCards = new LinkedHashSet<>(); Set movedTokens = new LinkedHashSet<>(); - for (Iterator it = entry.getValue().iterator(); it.hasNext(); ) { + for (Iterator it = entry.getValue().iterator(); it.hasNext();) { GameEvent event = it.next(); ZoneChangeEvent castEvent = (ZoneChangeEvent) event; UUID targetId = castEvent.getTargetId(); @@ -1030,7 +1026,7 @@ public class GameState implements Serializable, Copyable { * @param attachedTo * @param ability * @param copyAbility copies non MageSingleton abilities before adding to - * state + * state */ public void addOtherAbility(Card attachedTo, Ability ability, boolean copyAbility) { Ability newAbility; diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 3dff44f3994..a3c25098d52 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -1,5 +1,7 @@ package mage.game.combat; +import java.io.Serializable; +import java.util.*; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.effects.RequirementEffect; @@ -33,9 +35,6 @@ import mage.util.Copyable; import mage.util.trace.TraceUtil; import org.apache.log4j.Logger; -import java.io.Serializable; -import java.util.*; - /** * @author BetaSteward_at_googlemail.com */ @@ -346,8 +345,8 @@ public class Combat implements Serializable, Copyable { if (game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, attackingPlayerId, attackingPlayerId)) || (!canBand && !canBandWithOther) || !player.chooseUse(Outcome.Benefit, - "Do you wish to " + (isBanded ? "band " + attacker.getLogName() - + " with another " : "form a band with " + attacker.getLogName() + " and an ") + "Do you wish to " + (isBanded ? "band " + attacker.getLogName() + + " with another " : "form a band with " + attacker.getLogName() + " and an ") + "attacking creature?", null, game)) { break; } @@ -412,7 +411,8 @@ public class Combat implements Serializable, Copyable { if (!isBanded) { 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()); for (UUID id : attacker.getBandedCards()) { sb.append(", "); @@ -565,7 +565,7 @@ public class Combat implements Serializable, Copyable { * Handle the blocker selection process * * @param blockController player that controls how to block, if null the - * defender is the controller + * defender is the controller * @param game */ public void selectBlockers(Player blockController, Game game) { @@ -744,15 +744,15 @@ public class Combat implements Serializable, Copyable { } /** - * 509.1c The defending player checks each creature they control to - * see whether it's affected by any requirements (effects that say a - * creature must block, or that it must block if some condition is met). If - * the number of requirements that are being obeyed is fewer than the - * maximum possible number of requirements that could be obeyed without - * disobeying any restrictions, the declaration of blockers is illegal. If a - * creature can't block unless a player pays a cost, that player is not - * required to pay that cost, even if blocking with that creature would - * increase the number of requirements being obeyed. + * 509.1c The defending player checks each creature they control to see + * whether it's affected by any requirements (effects that say a creature + * must block, or that it must block if some condition is met). If the + * number of requirements that are being obeyed is fewer than the maximum + * possible number of requirements that could be obeyed without disobeying + * any restrictions, the declaration of blockers is illegal. If a creature + * can't block unless a player pays a cost, that player is not required to + * pay that cost, even if blocking with that creature would increase the + * number of requirements being obeyed. *

*

* Example: A player controls one creature that "blocks if able" and another @@ -1379,7 +1379,7 @@ public class Combat implements Serializable, Copyable { * @param playerId * @param game * @param solveBanding check whether also add creatures banded with - * attackerId + * attackerId */ public void addBlockingGroup(UUID blockerId, UUID attackerId, UUID playerId, Game game, boolean solveBanding) { Permanent blocker = game.getPermanent(blockerId); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index dbdd179cad5..a816624f03d 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1,6 +1,9 @@ package mage.players; import com.google.common.collect.ImmutableMap; +import java.io.Serializable; +import java.util.*; +import java.util.Map.Entry; import mage.ConditionalMana; import mage.MageObject; import mage.MageObjectReference; @@ -65,10 +68,6 @@ import mage.util.GameLog; import mage.util.RandomUtil; import org.apache.log4j.Logger; -import java.io.Serializable; -import java.util.*; -import java.util.Map.Entry; - public abstract class PlayerImpl implements Player, Serializable { private static final Logger logger = Logger.getLogger(PlayerImpl.class); @@ -612,9 +611,9 @@ public abstract class PlayerImpl implements Player, Serializable { && this.hasOpponent(sourceControllerId, game) && game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game) == null && abilities.stream() - .filter(HexproofBaseAbility.class::isInstance) - .map(HexproofBaseAbility.class::cast) - .anyMatch(ability -> ability.checkObject(source, game))) { + .filter(HexproofBaseAbility.class::isInstance) + .map(HexproofBaseAbility.class::cast) + .anyMatch(ability -> ability.checkObject(source, game))) { return false; } @@ -654,7 +653,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(getLogName() + " discards down to " + this.maxHandSize + (this.maxHandSize == 1 - ? " hand card" : " hand cards")); + ? " hand card" : " hand cards")); } 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, card.getId(), source == null - ? null : source.getSourceId(), playerId); + ? null : source.getSourceId(), playerId); gameEvent.setFlag(source != null); // event from effect or from cost (source == null) if (game.replaceEvent(gameEvent, source)) { return false; @@ -1345,6 +1344,9 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean activateAbility(ActivatedAbility ability, Game game) { + if (ability == null) { + return false; + } boolean result; if (ability instanceof PassAbility) { pass(game); @@ -1502,14 +1504,13 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public LinkedHashMap getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) { LinkedHashMap 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(); game.setCheckPlayableState(true); try { - // It may not be possible to activate abilities of stack abilities - if (object instanceof StackAbility) { - return useable; - } - // collect and filter playable activated abilities // GUI: user clicks on card, but it must activate ability from any card's parts (main, left, right) UUID needId1, needId2, needId3; @@ -1810,9 +1811,9 @@ public abstract class PlayerImpl implements Player, Serializable { } private List getPermanentsThatCanBeUntapped(Game game, - List canBeUntapped, - RestrictionUntapNotMoreThanEffect handledEffect, - Map>, Integer> notMoreThanEffectsUsage) { + List canBeUntapped, + RestrictionUntapNotMoreThanEffect handledEffect, + Map>, Integer> notMoreThanEffectsUsage) { List leftForUntap = new ArrayList<>(); // select permanents that can still be untapped for (Permanent permanent : canBeUntapped) { @@ -2521,7 +2522,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean searchLibrary(TargetCardInLibrary target, Ability source, Game game, UUID targetPlayerId, - boolean triggerEvents) { + boolean triggerEvents) { //20091005 - 701.14c Library searchedLibrary = null; String searchInfo = null; @@ -2699,7 +2700,9 @@ public abstract class PlayerImpl implements Player, Serializable { (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), "Heads", "Tails", source, game )); - } else event.setResult(canChooseHeads); + } else { + event.setResult(canChooseHeads); + } game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); } if (event.isWinnable()) { @@ -2721,7 +2724,7 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param game * @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 */ @Override @@ -2758,16 +2761,16 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @param game * @param appliedEffects - * @param numberChaosSides The number of chaos sides the planar die - * currently has (normally 1 but can be 5) + * @param numberChaosSides The number of chaos sides the planar die + * currently has (normally 1 but can be 5) * @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 * or NilRoll */ @Override public PlanarDieRoll rollPlanarDie(Game game, List appliedEffects, int numberChaosSides, - int numberPlanarSides) { + int numberPlanarSides) { int result = RandomUtil.nextInt(9) + 1; PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL; if (numberChaosSides + numberPlanarSides > 9) { @@ -2924,14 +2927,14 @@ public abstract class PlayerImpl implements Player, Serializable { /** * @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 game * @return */ protected boolean canPlay(ActivatedAbility ability, ManaOptions available, MageObject sourceObject, Game game) { 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()) { return false; } @@ -3117,7 +3120,7 @@ public abstract class PlayerImpl implements Player, Serializable { } 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() .filter(landAbility -> landAbility.getAbilityType() == AbilityType.PLAY_LAND) .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 output) { - if (fromZone == null) { + if (fromZone == null || card == null) { return; } @@ -3187,7 +3190,6 @@ public abstract class PlayerImpl implements Player, Serializable { boolean isPlayLand = (ability instanceof PlayLandAbility); // as original controller - // play land restrictions if (isPlayLand && game.getContinuousEffects().preventedByRuleModification( GameEvent.getEvent(GameEvent.EventType.PLAY_LAND, ability.getSourceId(), @@ -3221,7 +3223,6 @@ public abstract class PlayerImpl implements Player, Serializable { || (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard()); // as affected controller - UUID savedControllerId = ability.getControllerId(); ability.setControllerId(this.getId()); try { @@ -3620,7 +3621,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean canPaySacrificeCost(Permanent permanent, UUID sourceId, - UUID controllerId, Game game + UUID controllerId, Game game ) { return sacrificeCostFilter == null || !sacrificeCostFilter.match(permanent, sourceId, controllerId, game); } @@ -3773,8 +3774,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Card card, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { Set cardList = new HashSet<>(); if (card != null) { @@ -3785,22 +3786,22 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCards(Cards cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards.getCards(game), toZone, source, game); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game + Ability source, Game game ) { return moveCards(cards, toZone, source, game, false, false, false, null); } @Override public boolean moveCards(Set cards, Zone toZone, - Ability source, Game game, - boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects + Ability source, Game game, + boolean tapped, boolean faceDown, boolean byOwner, List appliedEffects ) { if (cards.isEmpty()) { return true; @@ -3902,8 +3903,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardsToExile(Card card, Ability source, - Game game, boolean withName, UUID exileId, - String exileZoneName + Game game, boolean withName, UUID exileId, + String exileZoneName ) { Set cards = new HashSet<>(); cards.add(card); @@ -3912,8 +3913,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardsToExile(Set cards, Ability source, - Game game, boolean withName, UUID exileId, - String exileZoneName + Game game, boolean withName, UUID exileId, + String exileZoneName ) { if (cards.isEmpty()) { return true; @@ -3929,14 +3930,14 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, - Game game + Game game ) { return this.moveCardToHandWithInfo(card, sourceId, game, true); } @Override public boolean moveCardToHandWithInfo(Card card, UUID sourceId, - Game game, boolean withName + Game game, boolean withName ) { boolean result = false; Zone fromZone = game.getState().getZone(card.getId()); @@ -3961,7 +3962,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public Set moveCardsToGraveyardWithInfo(Set allCards, Ability source, - Game game, Zone fromZone + Game game, Zone fromZone ) { UUID sourceId = source == null ? null : source.getSourceId(); Set movedCards = new LinkedHashSet<>(); @@ -3969,7 +3970,7 @@ public abstract class PlayerImpl implements Player, Serializable { // identify cards from one owner Cards cards = new CardsImpl(); UUID ownerId = null; - for (Iterator it = allCards.iterator(); it.hasNext(); ) { + for (Iterator it = allCards.iterator(); it.hasNext();) { Card card = it.next(); if (cards.isEmpty()) { ownerId = card.getOwnerId(); @@ -4032,7 +4033,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToGraveyardWithInfo(Card card, UUID sourceId, - Game game, Zone fromZone + Game game, Zone fromZone ) { if (card == null) { return false; @@ -4061,8 +4062,8 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public boolean moveCardToLibraryWithInfo(Card card, UUID sourceId, - Game game, Zone fromZone, - boolean toTop, boolean withName + Game game, Zone fromZone, + boolean toTop, boolean withName ) { if (card == null) { return false; @@ -4127,7 +4128,7 @@ public abstract class PlayerImpl implements Player, Serializable { @Override 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) { return false; } @@ -4150,7 +4151,7 @@ public abstract class PlayerImpl implements Player, Serializable { game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone"); + + ' ' : "") + "to the exile zone"); } result = true; diff --git a/Mage/src/main/java/mage/watchers/common/LostControlWatcher.java b/Mage/src/main/java/mage/watchers/common/LostControlWatcher.java new file mode 100644 index 00000000000..86f8cf26ddb --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/LostControlWatcher.java @@ -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 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)); + } +}