mirror of
https://github.com/correl/mage.git
synced 2025-03-07 20:53:18 -10:00
* Fix to handle returning effects correct if multiple objects return at the same time (e.g. two creatures with evolve return from exile because two Banisher Priests die by damage to all effect). (not complete finished yet, because Undying test does not run without error).
This commit is contained in:
parent
3caacdfec8
commit
9364616517
9 changed files with 142 additions and 29 deletions
|
@ -79,6 +79,8 @@ public class BanisherPriest extends CardImpl<BanisherPriest> {
|
|||
// Implemented as triggered effect that doesn't uses the stack (implementation with watcher does not work correctly because if the returned creature
|
||||
// has a DiesTriggeredAll ability it triggers for the dying Banish Priest, what shouldn't happen)
|
||||
this.addAbility(new BanisherPriestReturnExiledAbility());
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -145,4 +145,83 @@ public class EvolveTest extends CardTestPlayerBase {
|
|||
assertPowerToughness(playerA, "Experiment One", 3, 3);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleCreaturesComeIntoPlay() {
|
||||
|
||||
// Cloudfin Raptor gets one +1/+1 because itself and other creatur return from exile
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Judge's Familiar", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Cloudfin Raptor", 1);
|
||||
addCard(Zone.HAND, playerA, "Mizzium Mortars", 1);
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Plains", 6);
|
||||
addCard(Zone.HAND, playerB, "Banisher Priest", 2);
|
||||
|
||||
|
||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Banisher Priest");
|
||||
addTarget(playerB, "Cloudfin Raptor");
|
||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Banisher Priest");
|
||||
addTarget(playerB, "Judge's Familiar");
|
||||
|
||||
castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Mizzium Mortars with overload");
|
||||
|
||||
setStopAt(3, PhaseStep.END_TURN);
|
||||
execute();
|
||||
|
||||
assertLife(playerA, 20);
|
||||
assertLife(playerB, 20);
|
||||
|
||||
assertPermanentCount(playerB, "Banisher Priest", 0);
|
||||
|
||||
assertGraveyardCount(playerB, 2);
|
||||
assertGraveyardCount(playerA, 1);
|
||||
|
||||
assertPermanentCount(playerA, "Cloudfin Raptor", 1);
|
||||
assertPermanentCount(playerA, "Judge's Familiar", 1);
|
||||
|
||||
assertPowerToughness(playerA, "Cloudfin Raptor", 1, 2);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleCreaturesComeIntoPlaySuddenDisappearance() {
|
||||
|
||||
// Sudden Disappearance
|
||||
// Sorcery {5}{W}
|
||||
// Exile all nonland permanents target player controls. Return the exiled cards
|
||||
// to the battlefield under their owner's control at the beginning of the next end step.
|
||||
|
||||
// Battering Krasis (2/1) and Crocanura (1/3) get both a +1/+1 counter each other because they come into play at the same time
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Battering Krasis", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Crocanura", 1);
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Plains", 6);
|
||||
addCard(Zone.HAND, playerB, "Sudden Disappearance", 2);
|
||||
|
||||
|
||||
castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Sudden Disappearance", playerA);
|
||||
|
||||
setStopAt(3, PhaseStep.PRECOMBAT_MAIN);
|
||||
execute();
|
||||
|
||||
assertLife(playerA, 20);
|
||||
assertLife(playerB, 20);
|
||||
|
||||
assertGraveyardCount(playerB, 1);
|
||||
assertGraveyardCount(playerA, 0);
|
||||
|
||||
assertPermanentCount(playerA, "Battering Krasis", 1);
|
||||
assertPermanentCount(playerA, "Crocanura", 1);
|
||||
|
||||
assertPowerToughness(playerA, "Battering Krasis", 3, 2);
|
||||
assertPowerToughness(playerA, "Crocanura", 2, 4);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ public abstract class CardTestPlayerBase extends CardTestPlayerAPIImpl {
|
|||
public CardTestPlayerBase() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TestPlayer createNewPlayer(String playerName) {
|
||||
return createPlayer(playerName);
|
||||
}
|
||||
|
|
|
@ -178,8 +178,6 @@ public abstract class AbilityImpl<T extends AbilityImpl<T>> implements Ability {
|
|||
if (effect.applyEffectsAfter()) {
|
||||
game.applyEffects();
|
||||
}
|
||||
// effects like entersBattlefield have to trigger simultanously so objects see each other
|
||||
game.getState().handleSimultaneousEvent(game);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -28,17 +28,17 @@
|
|||
|
||||
package mage.abilities.keyword;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.Zone;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.TriggeredAbilityImpl;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.target.targetpointer.FixedTarget;
|
||||
|
||||
/**
|
||||
* FAQ 2013/01/11
|
||||
|
@ -52,6 +52,34 @@ import mage.game.permanent.Permanent;
|
|||
*
|
||||
* 702.98b If a creature has multiple instances of evolve, each triggers separately
|
||||
*
|
||||
* Rulings
|
||||
*
|
||||
* When comparing the stats of the two creatures, you always compare power to power and toughness to toughness.
|
||||
* Whenever a creature enters the battlefield under your control, check its power and toughness against
|
||||
* the power and toughness of the creature with evolve. If neither stat of the new creature is greater,
|
||||
* evolve won't trigger at all. For example, if you control a 2/3 creature with evolve and a 2/2 creature
|
||||
* enters the battlefield under your control, you won't have the opportunity to cast a spell like Giant Growth
|
||||
* to make the 2/2 creature large enough to cause evolve to trigger.
|
||||
* If evolve triggers, the stat comparison will happen again when the ability tries to resolve. If
|
||||
* neither stat of the new creature is greater, the ability will do nothing. If the creature that
|
||||
* entered the battlefield leaves the battlefield before evolve tries to resolve, use its last known
|
||||
* power and toughness to compare the stats.
|
||||
* If a creature enters the battlefield with +1/+1 counters on it, consider those counters when determining
|
||||
* if evolve will trigger. For example, a 1/1 creature that enters the battlefield with two +1/+1 counters
|
||||
* on it will cause the evolve ability of a 2/2 creature to trigger.
|
||||
* If multiple creatures enter the battlefield at the same time, evolve may trigger multiple times, although the stat
|
||||
* comparison will take place each time one of those abilities tries to resolve. For example, if you control a 2/2
|
||||
* creature with evolve and two 3/3 creatures enter the battlefield, evolve will trigger twice. The first ability
|
||||
* will resolve and put a +1/+1 counter on the creature with evolve. When the second ability tries to resolve,
|
||||
* neither the power nor the toughness of the new creature is greater than that of the creature with evolve,
|
||||
* so that ability does nothing.
|
||||
* When comparing the stats as the evolve ability resolves, it's possible that the stat that's greater changes
|
||||
* from power to toughness or vice versa. If this happens, the ability will still resolve and you'll put a +1/+1
|
||||
* counter on the creature with evolve. For example, if you control a 2/2 creature with evolve and a 1/3 creature
|
||||
* enters the battlefield under your control, it toughness is greater so evolve will trigger. In response, the 1/3
|
||||
* creature gets +2/-2. When the evolve trigger tries to resolve, its power is greater. You'll put a +1/+1
|
||||
* counter on the creature with evolve.
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
|
||||
|
@ -68,15 +96,17 @@ public class EvolveAbility extends TriggeredAbilityImpl<EvolveAbility> {
|
|||
|
||||
@Override
|
||||
public boolean checkTrigger(GameEvent event, Game game) {
|
||||
if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD && !event.getTargetId().equals(this.getSourceId())) {
|
||||
Permanent triggeringCreature = game.getPermanent(event.getTargetId());
|
||||
if (triggeringCreature != null
|
||||
&& triggeringCreature.getCardType().contains(CardType.CREATURE)
|
||||
&& triggeringCreature.getControllerId().equals(this.controllerId)) {
|
||||
Permanent sourceCreature = game.getPermanent(sourceId);
|
||||
if (sourceCreature != null && isPowerOrThoughnessGreater(sourceCreature, triggeringCreature)) {
|
||||
this.getEffects().get(0).setValue("triggeringCreature", event.getTargetId());
|
||||
return true;
|
||||
if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD) {
|
||||
if (!event.getTargetId().equals(this.getSourceId())) {
|
||||
Permanent triggeringCreature = game.getPermanent(event.getTargetId());
|
||||
if (triggeringCreature != null
|
||||
&& triggeringCreature.getCardType().contains(CardType.CREATURE)
|
||||
&& triggeringCreature.getControllerId().equals(this.controllerId)) {
|
||||
Permanent sourceCreature = game.getPermanent(sourceId);
|
||||
if (sourceCreature != null && isPowerOrThoughnessGreater(sourceCreature, triggeringCreature)) {
|
||||
this.getEffects().get(0).setTargetPointer(new FixedTarget(event.getTargetId()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,10 +123,7 @@ public class EvolveAbility extends TriggeredAbilityImpl<EvolveAbility> {
|
|||
if (newCreature.getPower().getValue() > sourceCreature.getPower().getValue()) {
|
||||
return true;
|
||||
}
|
||||
if (newCreature.getToughness().getValue() > sourceCreature.getToughness().getValue()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return newCreature.getToughness().getValue() > sourceCreature.getToughness().getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -127,11 +154,7 @@ class EvolveEffect extends OneShotEffect<EvolveEffect> {
|
|||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
UUID triggeringCreatureId = (UUID) getValue("triggeringCreature");
|
||||
Permanent triggeringCreature = game.getPermanent(triggeringCreatureId);
|
||||
if (triggeringCreature == null) {
|
||||
triggeringCreature = (Permanent) game.getLastKnownInformation(triggeringCreatureId, Zone.BATTLEFIELD);
|
||||
}
|
||||
Permanent triggeringCreature = game.getPermanentOrLKIBattlefield(getTargetPointer().getFirst(game, source));
|
||||
if (triggeringCreature != null) {
|
||||
Permanent sourceCreature = game.getPermanent(source.getSourceId());
|
||||
if (sourceCreature != null && EvolveAbility.isPowerOrThoughnessGreater(sourceCreature, triggeringCreature)) {
|
||||
|
|
|
@ -376,7 +376,7 @@ public abstract class CardImpl<T extends CardImpl<T>> extends MageObjectImpl<T>
|
|||
}
|
||||
setControllerId(ownerId);
|
||||
game.setZone(objectId, event.getToZone());
|
||||
game.fireEvent(event);
|
||||
game.addSimultaneousEvent(event);
|
||||
return game.getState().getZone(objectId) == toZone;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -986,7 +986,10 @@ public abstract class GameImpl<T extends GameImpl<T>> implements Game, Serializa
|
|||
while (!player.isPassed() && player.isInGame() && !isPaused() && !gameOver(null)) {
|
||||
if (!resuming) {
|
||||
if (checkStateAndTriggered()) {
|
||||
applyEffects();
|
||||
do {
|
||||
state.handleSimultaneousEvent(this);
|
||||
applyEffects();
|
||||
} while (state.hasSimultaneousEvents());
|
||||
}
|
||||
//resetLKI();
|
||||
applyEffects();
|
||||
|
@ -1057,6 +1060,7 @@ public abstract class GameImpl<T extends GameImpl<T>> implements Game, Serializa
|
|||
} finally {
|
||||
if (top != null) {
|
||||
state.getStack().remove(top);
|
||||
state.handleSimultaneousEvent(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -494,12 +494,18 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
|
||||
public void handleSimultaneousEvent(Game game) {
|
||||
if (!simultaneousEvents.isEmpty()) {
|
||||
for (GameEvent event:simultaneousEvents) {
|
||||
// it can happen, that the events add new simultaneous events, so copy the list before
|
||||
List<GameEvent> eventsToHandle = new ArrayList<>();
|
||||
eventsToHandle.addAll(simultaneousEvents);
|
||||
simultaneousEvents.clear();
|
||||
for (GameEvent event:eventsToHandle) {
|
||||
this.handleEvent(event, game);
|
||||
}
|
||||
simultaneousEvents.clear();
|
||||
}
|
||||
}
|
||||
public boolean hasSimultaneousEvents() {
|
||||
return !simultaneousEvents.isEmpty();
|
||||
}
|
||||
|
||||
public void handleEvent(GameEvent event, Game game) {
|
||||
watchers.watch(event, game);
|
||||
|
|
|
@ -161,7 +161,7 @@ public class PermanentCard extends PermanentImpl<PermanentCard> {
|
|||
break;
|
||||
}
|
||||
game.setZone(objectId, event.getToZone());
|
||||
game.fireEvent(event);
|
||||
game.addSimultaneousEvent(event);
|
||||
if (event.getFromZone().equals(Zone.BATTLEFIELD)) {
|
||||
game.resetForSourceId(getId());
|
||||
game.applyEffects(); // LevelX2: needed to execute isInactive for of custom duration copy effect if source returns directly (e.g. cloudshifted clone)
|
||||
|
|
Loading…
Add table
Reference in a new issue