mirror of
https://github.com/magefree/mage.git
synced 2025-12-21 02:52:02 -08:00
move findTargetingStackObject from CardUtil to Game, so saved data can be cleared with short living lki add test cases
This commit is contained in:
parent
030e8ae5d3
commit
24f030fa71
13 changed files with 181 additions and 73 deletions
|
|
@ -23,7 +23,6 @@ import mage.game.stack.Spell;
|
||||||
import mage.game.stack.StackObject;
|
import mage.game.stack.StackObject;
|
||||||
import mage.players.Player;
|
import mage.players.Player;
|
||||||
import mage.target.Target;
|
import mage.target.Target;
|
||||||
import mage.util.CardUtil;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -90,7 +89,7 @@ class AgrusKosEternalSoldierTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
if (!event.getTargetId().equals(getSourceId())) {
|
if (!event.getTargetId().equals(getSourceId())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getId().toString(), event);
|
||||||
if (targetingObject == null || targetingObject instanceof Spell) {
|
if (targetingObject == null || targetingObject instanceof Spell) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
package mage.cards.p;
|
package mage.cards.p;
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
import mage.MageInt;
|
import mage.MageInt;
|
||||||
import mage.abilities.TriggeredAbility;
|
import mage.abilities.TriggeredAbility;
|
||||||
import mage.abilities.TriggeredAbilityImpl;
|
import mage.abilities.TriggeredAbilityImpl;
|
||||||
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
|
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
|
||||||
import mage.constants.*;
|
|
||||||
import mage.abilities.keyword.OffspringAbility;
|
import mage.abilities.keyword.OffspringAbility;
|
||||||
import mage.abilities.keyword.TrampleAbility;
|
import mage.abilities.keyword.TrampleAbility;
|
||||||
import mage.cards.CardImpl;
|
import mage.cards.CardImpl;
|
||||||
import mage.cards.CardSetInfo;
|
import mage.cards.CardSetInfo;
|
||||||
|
import mage.constants.CardType;
|
||||||
|
import mage.constants.SubType;
|
||||||
|
import mage.constants.Zone;
|
||||||
import mage.counters.CounterType;
|
import mage.counters.CounterType;
|
||||||
import mage.filter.FilterPermanent;
|
|
||||||
import mage.filter.FilterStackObject;
|
|
||||||
import mage.filter.StaticFilters;
|
import mage.filter.StaticFilters;
|
||||||
import mage.filter.common.FilterControlledCreaturePermanent;
|
import mage.filter.common.FilterControlledCreaturePermanent;
|
||||||
import mage.filter.common.FilterControlledPermanent;
|
import mage.filter.common.FilterControlledPermanent;
|
||||||
|
|
@ -23,7 +22,8 @@ import mage.game.events.GameEvent;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import mage.game.stack.StackObject;
|
import mage.game.stack.StackObject;
|
||||||
import mage.target.TargetPermanent;
|
import mage.target.TargetPermanent;
|
||||||
import mage.util.CardUtil;
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
@ -63,9 +63,6 @@ public final class PawpatchRecruit extends CardImpl {
|
||||||
|
|
||||||
class PawpatchRecruitTriggeredAbility extends TriggeredAbilityImpl {
|
class PawpatchRecruitTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
|
|
||||||
private final FilterPermanent filterTarget = StaticFilters.FILTER_CONTROLLED_A_CREATURE;
|
|
||||||
private final FilterStackObject filterStack = StaticFilters.FILTER_SPELL_OR_ABILITY_OPPONENTS;
|
|
||||||
|
|
||||||
public PawpatchRecruitTriggeredAbility() {
|
public PawpatchRecruitTriggeredAbility() {
|
||||||
super(Zone.BATTLEFIELD, new AddCountersTargetEffect(CounterType.P1P1.createInstance()), false);
|
super(Zone.BATTLEFIELD, new AddCountersTargetEffect(CounterType.P1P1.createInstance()), false);
|
||||||
}
|
}
|
||||||
|
|
@ -93,11 +90,11 @@ class PawpatchRecruitTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
@Override
|
@Override
|
||||||
public boolean checkTrigger(GameEvent event, Game game) {
|
public boolean checkTrigger(GameEvent event, Game game) {
|
||||||
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId());
|
Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId());
|
||||||
if (permanent == null || !filterTarget.match(permanent, getControllerId(), this, game)) {
|
if (permanent == null || !StaticFilters.FILTER_CONTROLLED_A_CREATURE.match(permanent, getControllerId(), this, game)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getId().toString(), event);
|
||||||
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
if (targetingObject == null || !StaticFilters.FILTER_SPELL_OR_ABILITY_OPPONENTS.match(targetingObject, getControllerId(), this, game)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.getTargets().clear();
|
this.getTargets().clear();
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import mage.game.events.GameEvent;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import mage.game.stack.Spell;
|
import mage.game.stack.Spell;
|
||||||
import mage.game.stack.StackObject;
|
import mage.game.stack.StackObject;
|
||||||
import mage.util.CardUtil;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
@ -90,7 +89,7 @@ class SurrakElusiveHunterTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
if (!checkTargeted(event.getTargetId(), game)) {
|
if (!checkTargeted(event.getTargetId(), game)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getId().toString(), event);
|
||||||
return targetingObject != null && game.getOpponents(getControllerId()).contains(targetingObject.getControllerId());
|
return targetingObject != null && game.getOpponents(getControllerId()).contains(targetingObject.getControllerId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package org.mage.test.cards.abilities.keywords;
|
package org.mage.test.cards.abilities.keywords;
|
||||||
|
|
||||||
|
import mage.abilities.keyword.FlyingAbility;
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -80,4 +81,115 @@ public class WardTest extends CardTestPlayerBase {
|
||||||
assertPermanentCount(playerB, "Roaming Throne", 1);
|
assertPermanentCount(playerB, "Roaming Throne", 1);
|
||||||
assertDamageReceived(playerB, "Roaming Throne", 0);
|
assertDamageReceived(playerB, "Roaming Throne", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reported bug: #13523 - Ward not properly triggering when re-casting Aura
|
||||||
|
|
||||||
|
private static final String creature = "Owlin Shieldmage"; // 3/3 Flying, Ward - Pay 3 life
|
||||||
|
private static final String aura = "Kenrith's Transformation"; // 1G enchanted creature loses all abilities and is 3/3 Elk (also draw card on ETB)
|
||||||
|
private static final String spell = "Beast Within"; // 2G destroy permanent, controller gets 3/3 Beast
|
||||||
|
private static final String regrowth = "Regrowth"; // 1G return target card from graveyard to hand
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wardRecastAuraNoPay() {
|
||||||
|
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, creature);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 9);
|
||||||
|
addCard(Zone.HAND, playerA, aura);
|
||||||
|
addCard(Zone.HAND, playerA, spell);
|
||||||
|
addCard(Zone.HAND, playerA, regrowth);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aura, creature);
|
||||||
|
setChoice(playerA, false); // don't pay for ward
|
||||||
|
|
||||||
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, regrowth, aura);
|
||||||
|
|
||||||
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aura, creature);
|
||||||
|
setChoice(playerA, false); // don't pay for ward
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerA, 20);
|
||||||
|
assertGraveyardCount(playerA, aura, 1);
|
||||||
|
assertGraveyardCount(playerA, regrowth, 1);
|
||||||
|
assertGraveyardCount(playerA, 2);
|
||||||
|
assertHandCount(playerA, spell, 1);
|
||||||
|
assertHandCount(playerA, 1);
|
||||||
|
assertAbility(playerB, creature, FlyingAbility.getInstance(), true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wardRecastAuraPaySecond() {
|
||||||
|
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, creature);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 9);
|
||||||
|
addCard(Zone.HAND, playerA, aura);
|
||||||
|
addCard(Zone.HAND, playerA, spell);
|
||||||
|
addCard(Zone.HAND, playerA, regrowth);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aura, creature);
|
||||||
|
setChoice(playerA, false); // don't pay for ward
|
||||||
|
|
||||||
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, regrowth, aura);
|
||||||
|
|
||||||
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aura, creature);
|
||||||
|
setChoice(playerA, true); // pay ward
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerA, 17);
|
||||||
|
assertGraveyardCount(playerA, aura, 0);
|
||||||
|
assertGraveyardCount(playerA, regrowth, 1);
|
||||||
|
assertGraveyardCount(playerA, 1);
|
||||||
|
assertHandCount(playerA, spell, 1);
|
||||||
|
assertHandCount(playerA, 2); // one draw from aura entering
|
||||||
|
assertAttachedTo(playerB, aura, creature, true);
|
||||||
|
assertAbility(playerB, creature, FlyingAbility.getInstance(), false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wardRecastAuraPayBoth() {
|
||||||
|
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, creature);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Forest", 9);
|
||||||
|
addCard(Zone.HAND, playerA, aura);
|
||||||
|
addCard(Zone.HAND, playerA, spell);
|
||||||
|
addCard(Zone.HAND, playerA, regrowth);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aura, creature);
|
||||||
|
setChoice(playerA, true); // pay for ward
|
||||||
|
|
||||||
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, spell, aura); // destroy aura
|
||||||
|
|
||||||
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, regrowth, aura);
|
||||||
|
|
||||||
|
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aura, creature);
|
||||||
|
setChoice(playerA, true); // pay ward
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.END_TURN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerA, 14);
|
||||||
|
assertGraveyardCount(playerA, spell, 1);
|
||||||
|
assertGraveyardCount(playerA, regrowth, 1);
|
||||||
|
assertGraveyardCount(playerA, 2);
|
||||||
|
assertHandCount(playerA, 2); // two draws from aura entering
|
||||||
|
assertAttachedTo(playerB, aura, creature, true);
|
||||||
|
assertAbility(playerB, creature, FlyingAbility.getInstance(), false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import mage.game.events.GameEvent;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import mage.game.stack.StackObject;
|
import mage.game.stack.StackObject;
|
||||||
import mage.target.targetpointer.FixedTarget;
|
import mage.target.targetpointer.FixedTarget;
|
||||||
import mage.util.CardUtil;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author xenohedron
|
* @author xenohedron
|
||||||
|
|
@ -70,7 +69,7 @@ public class BecomesTargetAnyTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
if (permanent == null || !filterTarget.match(permanent, getControllerId(), this, game)) {
|
if (permanent == null || !filterTarget.match(permanent, getControllerId(), this, game)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getId().toString(), event);
|
||||||
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,9 @@ import mage.filter.FilterStackObject;
|
||||||
import mage.filter.StaticFilters;
|
import mage.filter.StaticFilters;
|
||||||
import mage.game.Game;
|
import mage.game.Game;
|
||||||
import mage.game.events.GameEvent;
|
import mage.game.events.GameEvent;
|
||||||
import mage.game.stack.StackObject;
|
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
|
import mage.game.stack.StackObject;
|
||||||
import mage.target.targetpointer.FixedTarget;
|
import mage.target.targetpointer.FixedTarget;
|
||||||
import mage.util.CardUtil;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author LoneFox
|
* @author LoneFox
|
||||||
|
|
@ -54,7 +53,7 @@ public class BecomesTargetAttachedTriggeredAbility extends TriggeredAbilityImpl
|
||||||
if (enchantment == null || enchantment.getAttachedTo() == null || !event.getTargetId().equals(enchantment.getAttachedTo())) {
|
if (enchantment == null || enchantment.getAttachedTo() == null || !event.getTargetId().equals(enchantment.getAttachedTo())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getId().toString(), event);
|
||||||
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
|
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,14 @@ package mage.abilities.common;
|
||||||
import mage.abilities.TriggeredAbilityImpl;
|
import mage.abilities.TriggeredAbilityImpl;
|
||||||
import mage.abilities.effects.Effect;
|
import mage.abilities.effects.Effect;
|
||||||
import mage.constants.SetTargetPointer;
|
import mage.constants.SetTargetPointer;
|
||||||
|
import mage.constants.Zone;
|
||||||
import mage.filter.FilterPermanent;
|
import mage.filter.FilterPermanent;
|
||||||
import mage.filter.FilterStackObject;
|
import mage.filter.FilterStackObject;
|
||||||
import mage.game.events.GameEvent;
|
|
||||||
import mage.constants.Zone;
|
|
||||||
import mage.game.Game;
|
import mage.game.Game;
|
||||||
|
import mage.game.events.GameEvent;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import mage.game.stack.StackObject;
|
import mage.game.stack.StackObject;
|
||||||
import mage.target.targetpointer.FixedTarget;
|
import mage.target.targetpointer.FixedTarget;
|
||||||
import mage.util.CardUtil;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author xenohedron
|
* @author xenohedron
|
||||||
|
|
@ -63,7 +62,7 @@ public class BecomesTargetControllerTriggeredAbility extends TriggeredAbilityImp
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getId().toString(), event);
|
||||||
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import mage.game.Game;
|
||||||
import mage.game.events.GameEvent;
|
import mage.game.events.GameEvent;
|
||||||
import mage.game.stack.StackObject;
|
import mage.game.stack.StackObject;
|
||||||
import mage.target.targetpointer.FixedTarget;
|
import mage.target.targetpointer.FixedTarget;
|
||||||
import mage.util.CardUtil;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author North
|
* @author North
|
||||||
|
|
@ -57,7 +56,7 @@ public class BecomesTargetSourceTriggeredAbility extends TriggeredAbilityImpl {
|
||||||
if (!event.getTargetId().equals(getSourceId())) {
|
if (!event.getTargetId().equals(getSourceId())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getId().toString(), event);
|
||||||
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
|
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ public class WardAbility extends TriggeredAbilityImpl {
|
||||||
if (!getSourceId().equals(event.getTargetId())) {
|
if (!getSourceId().equals(event.getTargetId())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getId().toString(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getId().toString(), event);
|
||||||
if (targetingObject == null || !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId())) {
|
if (targetingObject == null || !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import mage.game.permanent.Battlefield;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import mage.game.stack.Spell;
|
import mage.game.stack.Spell;
|
||||||
import mage.game.stack.SpellStack;
|
import mage.game.stack.SpellStack;
|
||||||
|
import mage.game.stack.StackObject;
|
||||||
import mage.game.turn.Phase;
|
import mage.game.turn.Phase;
|
||||||
import mage.game.turn.Step;
|
import mage.game.turn.Step;
|
||||||
import mage.game.turn.Turn;
|
import mage.game.turn.Turn;
|
||||||
|
|
@ -310,6 +311,19 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
||||||
|
|
||||||
void resetShortLivingLKI();
|
void resetShortLivingLKI();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For finding the spell or ability on the stack for "becomes the target" triggers.
|
||||||
|
* Also ensures that spells/abilities that target the same object twice only trigger each "becomes the target" ability once.
|
||||||
|
* If this is the first attempt at triggering for a given ability targeting a given object,
|
||||||
|
* this method temporarily records that in the game state for later checks by this same method,
|
||||||
|
* to not return the same object again (until short living LKI is cleared)
|
||||||
|
*
|
||||||
|
* @param checkingReference must be unique for each usage (this.getId().toString() of the TriggeredAbility, or this.getKey() of the watcher)
|
||||||
|
* @param event the GameEvent.EventType.TARGETED from checkTrigger() or watch()
|
||||||
|
* @return the StackObject which targeted the source, or null if already used or not found
|
||||||
|
*/
|
||||||
|
StackObject findTargetingStackObject(String checkingReference, GameEvent event);
|
||||||
|
|
||||||
void setLosingPlayer(Player player);
|
void setLosingPlayer(Player player);
|
||||||
|
|
||||||
Player getLosingPlayer();
|
Player getLosingPlayer();
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,8 @@ public abstract class GameImpl implements Game {
|
||||||
protected Map<UUID, Map<Integer, MageObject>> lkiExtended = new HashMap<>();
|
protected Map<UUID, Map<Integer, MageObject>> lkiExtended = new HashMap<>();
|
||||||
// Used to check if an object was moved by the current effect in resolution (so Wrath like effect can be handled correctly)
|
// Used to check if an object was moved by the current effect in resolution (so Wrath like effect can be handled correctly)
|
||||||
protected Map<Zone, Set<UUID>> lkiShortLiving = new EnumMap<>(Zone.class);
|
protected Map<Zone, Set<UUID>> lkiShortLiving = new EnumMap<>(Zone.class);
|
||||||
|
// For checking "becomes the target" triggers accurately. Cleared on short living LKI reset
|
||||||
|
protected Map<String, Map<UUID, Set<UUID>>> targetedMap = new HashMap<>();
|
||||||
|
|
||||||
// Permanents entering the Battlefield while handling replacement effects before they are added to the battlefield
|
// Permanents entering the Battlefield while handling replacement effects before they are added to the battlefield
|
||||||
protected Map<UUID, Permanent> permanentsEntering = new HashMap<>();
|
protected Map<UUID, Permanent> permanentsEntering = new HashMap<>();
|
||||||
|
|
@ -202,6 +204,7 @@ public abstract class GameImpl implements Game {
|
||||||
this.lkiCardState = CardUtil.deepCopyObject(game.lkiCardState);
|
this.lkiCardState = CardUtil.deepCopyObject(game.lkiCardState);
|
||||||
this.lkiExtended = CardUtil.deepCopyObject(game.lkiExtended);
|
this.lkiExtended = CardUtil.deepCopyObject(game.lkiExtended);
|
||||||
this.lkiShortLiving = CardUtil.deepCopyObject(game.lkiShortLiving);
|
this.lkiShortLiving = CardUtil.deepCopyObject(game.lkiShortLiving);
|
||||||
|
this.targetedMap = CardUtil.deepCopyObject(game.targetedMap);
|
||||||
|
|
||||||
this.permanentsEntering = CardUtil.deepCopyObject(game.permanentsEntering);
|
this.permanentsEntering = CardUtil.deepCopyObject(game.permanentsEntering);
|
||||||
this.enterWithCounters = CardUtil.deepCopyObject(game.enterWithCounters);
|
this.enterWithCounters = CardUtil.deepCopyObject(game.enterWithCounters);
|
||||||
|
|
@ -3687,6 +3690,39 @@ public abstract class GameImpl implements Game {
|
||||||
@Override
|
@Override
|
||||||
public void resetShortLivingLKI() {
|
public void resetShortLivingLKI() {
|
||||||
lkiShortLiving.clear();
|
lkiShortLiving.clear();
|
||||||
|
targetedMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StackObject findTargetingStackObject(String checkingReference, GameEvent event) {
|
||||||
|
// In case of multiple simultaneous triggered abilities from the same source,
|
||||||
|
// need to get the actual one that targeted, see #8026, #8378, rulings for Battle Mammoth
|
||||||
|
// In case of copied triggered abilities, need to trigger on each independently, see #13498
|
||||||
|
// Also avoids triggering on cancelled selections, see #8802
|
||||||
|
Map<UUID, Set<UUID>> targetMap = targetedMap.getOrDefault(checkingReference, null);
|
||||||
|
// targetMap: key - targetId; value - Set of stackObject Ids
|
||||||
|
if (targetMap == null) {
|
||||||
|
targetMap = new HashMap<>();
|
||||||
|
} else {
|
||||||
|
targetMap = new HashMap<>(targetMap); // must have new object reference if saved back
|
||||||
|
}
|
||||||
|
Set<UUID> targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
|
||||||
|
for (StackObject stackObject : getStack()) {
|
||||||
|
Ability stackAbility = stackObject.getStackAbility();
|
||||||
|
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (CardUtil.getAllSelectedTargets(stackAbility, this).contains(event.getTargetId())) {
|
||||||
|
if (!targetingObjects.add(stackObject.getId())) {
|
||||||
|
continue; // The trigger/watcher already recorded that target of the stack object, check for another
|
||||||
|
}
|
||||||
|
// Otherwise, store this combination of trigger/watcher + target + stack object
|
||||||
|
targetMap.put(event.getTargetId(), targetingObjects);
|
||||||
|
targetedMap.put(checkingReference, targetMap);
|
||||||
|
return stackObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ import mage.game.permanent.PermanentMeld;
|
||||||
import mage.game.permanent.PermanentToken;
|
import mage.game.permanent.PermanentToken;
|
||||||
import mage.game.permanent.token.Token;
|
import mage.game.permanent.token.Token;
|
||||||
import mage.game.stack.Spell;
|
import mage.game.stack.Spell;
|
||||||
import mage.game.stack.StackObject;
|
|
||||||
import mage.players.Player;
|
import mage.players.Player;
|
||||||
import mage.players.PlayerList;
|
import mage.players.PlayerList;
|
||||||
import mage.target.Target;
|
import mage.target.Target;
|
||||||
|
|
@ -1133,49 +1132,6 @@ public final class CardUtil {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For finding the spell or ability on the stack for "becomes the target" triggers.
|
|
||||||
* Also ensures that spells/abilities that target the same object twice only trigger each "becomes the target" ability once.
|
|
||||||
* If this is the first attempt at triggering for a given ability targeting a given object,
|
|
||||||
* this method records that in the game state for later checks by this same method, to not return the same object again.
|
|
||||||
*
|
|
||||||
* @param checkingReference must be unique for each usage (this.getId().toString() of the TriggeredAbility, or this.getKey() of the watcher)
|
|
||||||
* @param event the GameEvent.EventType.TARGETED from checkTrigger() or watch()
|
|
||||||
* @param game the Game from checkTrigger() or watch()
|
|
||||||
* @return the StackObject which targeted the source, or null if already used or not found
|
|
||||||
*/
|
|
||||||
public static StackObject findTargetingStackObject(String checkingReference, GameEvent event, Game game) {
|
|
||||||
// In case of multiple simultaneous triggered abilities from the same source,
|
|
||||||
// need to get the actual one that targeted, see #8026, #8378, rulings for Battle Mammoth
|
|
||||||
// In case of copied triggered abilities, need to trigger on each independently, see #13498
|
|
||||||
// Also avoids triggering on cancelled selections, see #8802
|
|
||||||
String stateKey = "targetedMap" + checkingReference;
|
|
||||||
Map<UUID, Set<UUID>> targetMap = (Map<UUID, Set<UUID>>) game.getState().getValue(stateKey);
|
|
||||||
// targetMap: key - targetId; value - Set of stackObject Ids
|
|
||||||
if (targetMap == null) {
|
|
||||||
targetMap = new HashMap<>();
|
|
||||||
} else {
|
|
||||||
targetMap = new HashMap<>(targetMap); // must have new object reference if saved back to game state
|
|
||||||
}
|
|
||||||
Set<UUID> targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
|
|
||||||
for (StackObject stackObject : game.getStack()) {
|
|
||||||
Ability stackAbility = stackObject.getStackAbility();
|
|
||||||
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (CardUtil.getAllSelectedTargets(stackAbility, game).contains(event.getTargetId())) {
|
|
||||||
if (!targetingObjects.add(stackObject.getId())) {
|
|
||||||
continue; // The trigger/watcher already recorded that target of the stack object, check for another
|
|
||||||
}
|
|
||||||
// Otherwise, store this combination of trigger/watcher + target + stack object
|
|
||||||
targetMap.put(event.getTargetId(), targetingObjects);
|
|
||||||
game.getState().setValue(stateKey, targetMap);
|
|
||||||
return stackObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For overriding `canTarget()` with usages such as "any number of target cards with total mana value X or less".
|
* For overriding `canTarget()` with usages such as "any number of target cards with total mana value X or less".
|
||||||
* Call this after super.canTarget() returns true.
|
* Call this after super.canTarget() returns true.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import mage.game.Game;
|
||||||
import mage.game.events.GameEvent;
|
import mage.game.events.GameEvent;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import mage.game.stack.StackObject;
|
import mage.game.stack.StackObject;
|
||||||
import mage.util.CardUtil;
|
|
||||||
import mage.watchers.Watcher;
|
import mage.watchers.Watcher;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -29,7 +28,7 @@ public class NumberOfTimesPermanentTargetedATurnWatcher extends Watcher {
|
||||||
if (event.getType() != GameEvent.EventType.TARGETED) {
|
if (event.getType() != GameEvent.EventType.TARGETED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
StackObject targetingObject = CardUtil.findTargetingStackObject(this.getKey(), event, game);
|
StackObject targetingObject = game.findTargetingStackObject(this.getKey(), event);
|
||||||
if (targetingObject == null) {
|
if (targetingObject == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue