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

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

View file

@ -1,4 +1,3 @@
package mage.cards.a;
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);
}

View file

@ -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;
/**
*
@ -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);
}

View file

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

View file

@ -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
@ -36,6 +36,7 @@ public final class MasterThief extends CardImpl {
false);
ability.addTarget(new TargetArtifactPermanent());
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
package mage.cards.t;
import java.util.Objects;
import java.util.UUID;
import mage.MageInt;
import mage.abilities.Ability;
@ -13,33 +13,39 @@ 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
* 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) {
@ -49,8 +55,9 @@ public final class TheWretched extends CardImpl {
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,7 +90,9 @@ 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;
}
@ -92,7 +101,9 @@ class TheWretchedEffect extends OneShotEffect {
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);

View file

@ -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;
/**
*
@ -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);
}

View file

@ -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;
/**
*
@ -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) {

View file

@ -1,4 +1,3 @@
package mage.cards.w;
import java.util.UUID;
@ -15,12 +14,13 @@ 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;
/**
*
@ -48,6 +48,7 @@ public final class WillowSatyr extends CardImpl {
"Gain control of target legendary creature for as long as you control {this} and {this} remains tapped");
Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, effect, new TapSourceCost());
ability.addTarget(new TargetCreaturePermanent(filter));
ability.addWatcher(new LostControlWatcher());
this.addAbility(ability);
}

View file

@ -14,6 +14,8 @@ 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;
@ -34,6 +36,39 @@ public class GoadTest extends CardTestMultiPlayerBase {
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
@ -54,28 +89,32 @@ public class GoadTest extends CardTestMultiPlayerBase {
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
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);
attack(3, playerC, "Silvercoat Lion", playerA);
attack(3, playerC, "Silvercoat Lion", playerB);
attack(3, playerC, "Silvercoat Lion", playerD);
setStopAt(4, PhaseStep.BEGIN_COMBAT);
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);
assertLife(playerB, 38);
assertLife(playerA, 38);
assertLife(playerD, 38);
}
}

View file

@ -26,7 +26,7 @@ public class TheWretchedTest extends CardTestPlayerBase {
attack(3, playerA, "The Wretched");
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);
@ -60,12 +60,11 @@ public class TheWretchedTest extends CardTestPlayerBase {
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
@ -83,6 +82,7 @@ public class TheWretchedTest extends CardTestPlayerBase {
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);
}
@ -97,7 +97,6 @@ public class TheWretchedTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerB, "Wall of Pine Needles"); // a 3/3 with regeneration
addCard(Zone.BATTLEFIELD, playerB, "Wall of 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");

View file

@ -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,7 +31,7 @@ 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);
@ -40,10 +39,11 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
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);
@ -67,7 +67,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
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
@ -103,7 +102,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
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");
@ -135,10 +134,10 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
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,6 +150,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
Assert.assertTrue(lazav.isLegendary());
}
/**
* Tests old copy is discarded after reanmiation of Lazav
*/
@ -161,7 +161,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
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);
@ -173,7 +173,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
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");
@ -194,7 +194,6 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
Assert.assertFalse(lazav.getSubtype(currentGame).contains(SubType.GRIFFIN)); // no Griffin type
Assert.assertFalse("Lazav, Dimir Mastermind must have flying", lazav.getAbilities().contains(FlyingAbility.getInstance()));
}
/**
@ -204,7 +203,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
public void testCopyCreatureExiled() {
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);
@ -216,7 +215,7 @@ public class LazavDimirMastermindTest extends CardTestPlayerBase {
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");

View file

@ -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.*;
/**

View file

@ -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!");
}
}

View file

@ -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

View file

@ -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
return effectStartingOnTurn < game.getTurnNum()
&& game.isActivePlayer(startingControllerId);
} else {
return yourTurnNumPlayed == 0
&& 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);

View file

@ -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<ContinuousEffect> 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<ContinuousEffect> getLayeredEffects(Game game, String timestampGroupName) {
@ -229,8 +217,10 @@ public class ContinuousEffects implements Serializable {
* "actual" meaning it becomes turned on that is defined by
* Ability.#isInUseableZone(Game, boolean) method in
* #getLayeredEffects(Game).
* <p>
* It must be called with different timestamp group name (otherwise sort order will be changed for add/remove effects, see Urborg and Bloodmoon test)
*
* It must be called with different timestamp group name (otherwise sort
* order will be changed for add/remove effects, see Urborg and Bloodmoon
* test)
*
* @param layerEffects
*/
@ -1283,7 +1273,8 @@ public class ContinuousEffects implements Serializable {
}
private void setControllerForEffect(ContinuousEffectsList<?> effects, UUID sourceId, UUID controllerId) {
for (Effect effect : effects) {
for (ContinuousEffect effect : effects) {
if (!effect.getDuration().isFixedController()) {
Set<Ability> abilities = effects.getAbility(effect.getId());
for (Ability ability : abilities) {
if (ability.getSourceId() != null) {
@ -1294,7 +1285,7 @@ public class ContinuousEffects implements Serializable {
logger.fatal("Continuous effect for ability with no sourceId Ability: " + ability);
}
}
}
}
}

View file

@ -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 <T>
* @author BetaSteward_at_googlemail.com
@ -86,15 +85,6 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
}
}
public void incYourTurnNumPlayed(Game game) {
for (Iterator<T> i = this.iterator(); i.hasNext(); ) {
T entry = i.next();
if (game.isActivePlayer(entry.getStartingController())) {
entry.incYourTurnNumPlayed();
}
}
}
private boolean isInactive(T effect, Game game) {
// ends all inactive effects -- calls on player leave or apply new effect
if (game.getState().isGameOver()) {
@ -111,7 +101,6 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList
order whos still in the game.
*/
// objects removes doing in player.leave() call... effects removes is here
Set<Ability> set = effectAbilityMap.get(effect.getId());
if (set == null) {
logger.debug("No abilities for effect found: " + effect.toString());

View file

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

View file

@ -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 doesnt meet any of the above exceptions and can attack, it must attack a player other than
the controller of the spell or ability that goaded it if able. It the creature cant attack any of those
the controller of the spell or ability that goaded it if able. If the creature cant attack any of those
players but could otherwise attack, it must attack an opposing planeswalker (controlled by any opponent)
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());

View file

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

View file

@ -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;
@ -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 isnt 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 isnt 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;
}

View file

@ -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
* <p>
@ -179,8 +177,8 @@ public class GameState implements Serializable, Copyable<GameState> {
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<GameState> {
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<GameState> {
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<GameState> {
// 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.

View file

@ -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
*/
@ -412,7 +411,8 @@ public class Combat implements Serializable, Copyable<Combat> {
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(", ");
@ -744,15 +744,15 @@ public class Combat implements Serializable, Copyable<Combat> {
}
/**
* 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.
* <p>
* <p>
* Example: A player controls one creature that "blocks if able" and another

View file

@ -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);
@ -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<UUID, ActivatedAbility> getPlayableActivatedAbilities(MageObject object, Zone zone, Game game) {
LinkedHashMap<UUID, ActivatedAbility> useable = new LinkedHashMap<>();
// It may not be possible to activate abilities of stack abilities
if (object instanceof StackAbility || object == null) {
return useable;
}
boolean previousState = game.inCheckPlayableState();
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;
@ -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()) {
@ -2931,7 +2934,7 @@ public abstract class PlayerImpl implements Player, Serializable {
*/
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<ActivatedAbility> 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 {

View file

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