[UNF] "Name Sticker" Goblin, [WHO] Coward // Killer, [WHO] Thijarian Witness (#11392)

* Add [WHO] Coward // Killer

* Add MTGO version of [UNF] "Name Sticker" Goblin

* Implement [WHO] Thijarian Witness

* Add NameStickerGoblinTest

* Fix Thijarian Witness, add tests (may need additional tests). Also adds a simple toString for MageObjectReference

* Don't spam the java garbage collector, add another test

* Replace non-ASCII characters in card text

* improve MOR toString

* Thijarian Witness fixed better, add AttackingBlockingWatcher in common

* cleanup from xenohedron's review

* Fix test, add warning not to use AttackingBlockingWatcher for static effects

* rename AttackingBlockingWatcher to AttackingBlockingDelayedWatcher to make it more obvious how it should be used, minor documentation changes
Simplify and rename Thijarian Witness Predicate

* add null checks
This commit is contained in:
ssk97 2023-11-18 21:38:21 -08:00 committed by GitHub
parent 50d5b7ce9b
commit 38adbb4ae5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 615 additions and 10 deletions

View file

@ -8,7 +8,7 @@ import mage.cards.CardSetInfo;
import mage.constants.AbilityWord;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetCreaturePermanent;
@ -41,12 +41,6 @@ public final class CleansingBeam extends CardImpl {
class CleansingBeamEffect extends OneShotEffect {
static final FilterPermanent filter = new FilterPermanent("creature");
static {
filter.add(CardType.CREATURE.getPredicate());
}
CleansingBeamEffect() {
super(Outcome.Damage);
staticText = "{this} deals 2 damage to target creature and each other creature that shares a color with it";
@ -61,10 +55,10 @@ class CleansingBeamEffect extends OneShotEffect {
Permanent target = game.getPermanent(targetPointer.getFirst(game, source));
if (target != null) {
ObjectColor color = target.getColor(game);
target.damage(2, source.getSourceId(), source, game);
for (Permanent p : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), game)) {
target.damage(2, source, game);
for (Permanent p : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), game)) {
if (!target.getId().equals(p.getId()) && p.getColor(game).shares(color)) {
p.damage(2, source.getSourceId(), source, game, false, true);
p.damage(2, source, game);
}
}
return true;

View file

@ -0,0 +1,79 @@
package mage.cards.c;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.combat.CantBlockTargetEffect;
import mage.abilities.effects.common.continuous.BecomesCreatureTypeTargetEffect;
import mage.abilities.effects.common.counter.TimeTravelEffect;
import mage.cards.CardSetInfo;
import mage.cards.SplitCard;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
public final class CowardKiller extends SplitCard {
public CowardKiller(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{R}", "{2}{R}{R}", SpellAbilityType.SPLIT);
// Coward
// Target creature can't block this turn and becomes a Coward in addition to its other types until end of turn.
// Time travel.
getLeftHalfCard().getSpellAbility().addEffect(new CantBlockTargetEffect(Duration.EndOfTurn));
getLeftHalfCard().getSpellAbility().addEffect(new BecomesCreatureTypeTargetEffect(Duration.EndOfTurn, SubType.COWARD, false).setText(" and becomes a Coward in addition to its other types until end of turn"));
getLeftHalfCard().getSpellAbility().addTarget(new TargetCreaturePermanent());
getLeftHalfCard().getSpellAbility().addEffect(new TimeTravelEffect().concatBy("<br>"));
// Killer
// Killer deals 3 damage to target creature and each other creature that shares a creature type with it.
getRightHalfCard().getSpellAbility().addTarget(new TargetCreaturePermanent());
getRightHalfCard().getSpellAbility().addEffect(new KillerEffect());
}
private CowardKiller(final CowardKiller card) {
super(card);
}
@Override
public CowardKiller copy() {
return new CowardKiller(this);
}
}
class KillerEffect extends OneShotEffect {
KillerEffect() {
super(Outcome.Damage);
staticText = "{this} deals 3 damage to target creature and each other creature that shares a creature type with it";
}
private KillerEffect(final KillerEffect effect) {
super(effect);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent target = game.getPermanent(targetPointer.getFirst(game, source));
if (target != null) {
target.damage(3, source, game);
for (Permanent p : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, source.getControllerId(), game)) {
if (!target.getId().equals(p.getId()) && p.shareCreatureTypes(game,target)) {
p.damage(3, source, game);
}
}
return true;
}
return false;
}
@Override
public KillerEffect copy() {
return new KillerEffect(this);
}
}

View file

@ -0,0 +1,110 @@
package mage.cards.n;
import mage.MageInt;
import mage.Mana;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.RollDieWithResultTableEffect;
import mage.abilities.effects.mana.BasicManaEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.ComparisonType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.game.events.EntersTheBattlefieldEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import java.util.UUID;
/**
* This is the MTGO variant of the card
* <a href="https://www.mtgo.com/news/mtgo-woe-8292023#unfinity">Original announcement</a>
* <a href="https://scryfall.com/card/unf/107m/name-sticker-goblin">Scryfall link</a>
* @author notgreat
*/
public final class NameStickerGoblin extends CardImpl {
public NameStickerGoblin(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}");
this.subtype.add(SubType.GOBLIN);
this.subtype.add(SubType.GUEST);
this.power = new MageInt(2);
this.toughness = new MageInt(2);
RollDieWithResultTableEffect effect = new RollDieWithResultTableEffect();
// When this creature enters the battlefield from anywhere other than a graveyard or exile, if its
// on the battlefield and you control 9 or fewer creatures named Name Sticker Goblin, roll a 20-sided die.
// 1-6 | Add {R}{R}{R}{R}.
effect.addTableEntry(1, 6, new BasicManaEffect(Mana.RedMana(4)));
// 7-14 | Add {R}{R}{R}{R}{R}.
effect.addTableEntry(7, 14, new BasicManaEffect(Mana.RedMana(5)));
// 15-20 | Add {R}{R}{R}{R}{R}{R}.
effect.addTableEntry(15, 20, new BasicManaEffect(Mana.RedMana(6)));
this.addAbility(new NameStickerGoblinTriggeredAbility(effect));
}
private NameStickerGoblin(final NameStickerGoblin card) {
super(card);
}
@Override
public NameStickerGoblin copy() {
return new NameStickerGoblin(this);
}
}
class NameStickerGoblinTriggeredAbility extends TriggeredAbilityImpl {
private static final FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent();
static{
filter.add(new NamePredicate("\"Name Sticker\" Goblin"));
}
private static final Condition condition = new PermanentsOnTheBattlefieldCondition(filter, ComparisonType.OR_LESS, 9);
NameStickerGoblinTriggeredAbility(Effect effect) {
super(Zone.BATTLEFIELD, effect);
setTriggerPhrase("When this creature enters the battlefield from anywhere other than a graveyard or exile, if it's on the battlefield and you control 9 or fewer creatures named \"Name Sticker\" Goblin, ");
}
private NameStickerGoblinTriggeredAbility(final NameStickerGoblinTriggeredAbility ability) {
super(ability);
}
@Override
public NameStickerGoblinTriggeredAbility copy() {
return new NameStickerGoblinTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
EntersTheBattlefieldEvent zEvent = (EntersTheBattlefieldEvent) event;
if (zEvent == null) {
return false;
}
Permanent permanent = zEvent.getTarget();
if (permanent == null) {
return false;
}
Zone zone = zEvent.getFromZone();
return zone != Zone.GRAVEYARD && zone != Zone.EXILED
&& permanent.getId().equals(getSourceId());
}
@Override
public boolean checkInterveningIfClause(Game game) {
Permanent permanent = getSourcePermanentIfItStillExists(game);
return permanent != null && condition.apply(game, this);
}
}

View file

@ -0,0 +1,84 @@
package mage.cards.t;
import mage.MageInt;
import mage.abilities.common.DiesCreatureTriggeredAbility;
import mage.abilities.effects.common.ExileTargetEffect;
import mage.abilities.effects.keyword.InvestigateEffect;
import mage.abilities.keyword.FlashAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.watchers.common.AttackingBlockingDelayedWatcher;
import java.util.UUID;
/**
* @author notgreat
*/
public final class ThijarianWitness extends CardImpl {
private static final FilterPermanent filter
= new FilterCreaturePermanent("another creature");
static {
filter.add(AnotherPredicate.instance);
filter.add(WasAttackBlockAlonePredicate.instance);
}
public ThijarianWitness(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}");
this.subtype.add(SubType.ALIEN);
this.subtype.add(SubType.CLERIC);
this.power = new MageInt(0);
this.toughness = new MageInt(4);
// Flash
this.addAbility(FlashAbility.getInstance());
// Bear Witness -- Whenever another creature dies, if it was attacking or blocking alone, exile it and investigate.
DiesCreatureTriggeredAbility ability = new DiesCreatureTriggeredAbility(
new ExileTargetEffect().setText("if it was attacking or blocking alone, exile it"),
false, filter, true
);
ability.addEffect(new InvestigateEffect().concatBy("and"));
ability.withFlavorWord("Bear Witness");
ability.addWatcher(new AttackingBlockingDelayedWatcher());
this.addAbility(ability);
}
private ThijarianWitness(final ThijarianWitness card) {
super(card);
}
@Override
public ThijarianWitness copy() {
return new ThijarianWitness(this);
}
}
enum WasAttackBlockAlonePredicate implements Predicate<Permanent> {
instance;
@Override
public boolean apply(Permanent input, Game game) {
AttackingBlockingDelayedWatcher watcher = AttackingBlockingDelayedWatcher.getWatcher(game);
if (watcher == null) {
return false;
}
return (watcher.checkAttacker(input.getId()) && watcher.countAttackers() == 1)
|| (watcher.checkBlocker(input.getId()) && watcher.countBlockers() == 1);
}
@Override
public String toString() {
return "attacking or blocking alone";
}
}

View file

@ -48,6 +48,7 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("Clockwork Droid", 172, Rarity.UNCOMMON, mage.cards.c.ClockworkDroid.class));
cards.add(new SetCardInfo("Command Tower", 263, Rarity.COMMON, mage.cards.c.CommandTower.class));
cards.add(new SetCardInfo("Commander's Sphere", 240, Rarity.COMMON, mage.cards.c.CommandersSphere.class));
cards.add(new SetCardInfo("Coward // Killer", 77, Rarity.RARE, mage.cards.c.CowardKiller.class));
cards.add(new SetCardInfo("Crack in Time", 16, Rarity.RARE, mage.cards.c.CrackInTime.class));
cards.add(new SetCardInfo("Creeping Tar Pit", 267, Rarity.RARE, mage.cards.c.CreepingTarPit.class));
cards.add(new SetCardInfo("Crisis of Conscience", 17, Rarity.RARE, mage.cards.c.CrisisOfConscience.class));
@ -230,6 +231,7 @@ public final class DoctorWho extends ExpansionSet {
cards.add(new SetCardInfo("The Tenth Doctor", 446, Rarity.MYTHIC, mage.cards.t.TheTenthDoctor.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Tenth Doctor", 561, Rarity.MYTHIC, mage.cards.t.TheTenthDoctor.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("The Twelfth Doctor", 164, Rarity.RARE, mage.cards.t.TheTwelfthDoctor.class));
cards.add(new SetCardInfo("Thijarian Witness", 111, Rarity.UNCOMMON, mage.cards.t.ThijarianWitness.class));
cards.add(new SetCardInfo("Think Twice", 220, Rarity.COMMON, mage.cards.t.ThinkTwice.class));
cards.add(new SetCardInfo("Thought Vessel", 255, Rarity.UNCOMMON, mage.cards.t.ThoughtVessel.class));
cards.add(new SetCardInfo("Three Visits", 235, Rarity.UNCOMMON, mage.cards.t.ThreeVisits.class));

View file

@ -20,6 +20,7 @@ public final class Unfinity extends ExpansionSet {
this.hasBasicLands = true;
this.hasBoosters = false; // not likely to be able to drafts at any point
cards.add(new SetCardInfo("\"Name Sticker\" Goblin", "107m", Rarity.COMMON, mage.cards.n.NameStickerGoblin.class));
cards.add(new SetCardInfo("Attempted Murder", 66, Rarity.UNCOMMON, mage.cards.a.AttemptedMurder.class));
cards.add(new SetCardInfo("Blood Crypt", 279, Rarity.RARE, mage.cards.b.BloodCrypt.class));
cards.add(new SetCardInfo("Boing!", 40, Rarity.COMMON, mage.cards.b.Boing.class));

View file

@ -0,0 +1,89 @@
package org.mage.test.cards.single.unf;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Ignore;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author notgreat
*/
public class NameStickerGoblinTest extends CardTestPlayerBase {
/**
* "Name Sticker" Goblin {2}{R}
* When this creature enters the battlefield from anywhere other than a graveyard or exile, if its on the battlefield and you control 9 or fewer creatures named Name Sticker Goblin, roll a 20-sided die.
* 1-6 | Add {R}{R}{R}{R}.
* 7-14 | Add {R}{R}{R}{R}{R}.
* 15-20 | Add {R}{R}{R}{R}{R}{R}.
*/
private final static String nsgoblin = "\"Name Sticker\" Goblin";
@Test
public void testBasicETB() {
setStrictChooseMode(true);
addCard(Zone.HAND, playerA, nsgoblin);
addCard(Zone.BATTLEFIELD, playerA, "Mountain",3);
setDieRollResult(playerA, 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nsgoblin, true);
checkManaPool("Mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 4);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
}
@Test
public void testGraveyardETB() {
setStrictChooseMode(true);
addCard(Zone.GRAVEYARD, playerA, nsgoblin);
addCard(Zone.HAND, playerA, "Unearth");
addCard(Zone.BATTLEFIELD, playerA, "Swamp");
//No dice roll since it's from graveyard
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unearth", nsgoblin, true);
checkManaPool("No Mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 0);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
}
@Test
public void testExileETB() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, nsgoblin);
addCard(Zone.HAND, playerA, "Cloudshift");
addCard(Zone.BATTLEFIELD, playerA, "Plains");
//No dice roll since it's from exile
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cloudshift", nsgoblin, true);
checkManaPool("No mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 0);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
}
@Test
public void testNineETB() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, nsgoblin, 8);
addCard(Zone.HAND, playerA, nsgoblin, 3);
addCard(Zone.BATTLEFIELD, playerA, "Mountain",3);
setDieRollResult(playerA, 15);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nsgoblin, true);
checkManaPool("Mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 6);
//No dice roll since count > 9
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nsgoblin, true);
checkManaPool("No extra mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nsgoblin, true);
checkManaPool("No extra mana", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 0);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
}
}

View file

@ -0,0 +1,172 @@
package org.mage.test.cards.single.who;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author notgreat
*/
public class ThijarianWitnessTest extends CardTestPlayerBase {
/**
* Thijarian Witness {1}{G}
* Creature - Alien Cleric
* Flash
* Bear Witness - Whenever another creature dies, if it was attacking or blocking alone, exile it and investigate.
* 0/4
*/
private static final String witness = "Thijarian Witness";
private static final String clue = "Clue Token";
private static final String tiny = "Raging Goblin"; // 1/1
private static final String tiny2 = "Memnite"; // 1/1
private static final String big = "Alpine Grizzly"; // 4/2
private static final String kill = "Infernal Grasp"; //Also can test with Lightning Bolt
@Test
public void test_AttackingAloneAfterKill() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, witness);
addCard(Zone.BATTLEFIELD, playerA, tiny);
addCard(Zone.BATTLEFIELD, playerA, tiny2);
addCard(Zone.BATTLEFIELD, playerB, "Badlands", 4);
addCard(Zone.HAND, playerB, kill, 2);
attack(1, playerA, tiny);
attack(1, playerA, tiny2);
castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerB, kill, tiny, true);
castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerB, kill, tiny2, true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, clue, 1);
assertExileCount(playerA, 1);
}
@Test
public void test_BlockingAloneAfterKill() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, witness);
addCard(Zone.BATTLEFIELD, playerA, big);
addCard(Zone.BATTLEFIELD, playerA, "Badlands", 4);
addCard(Zone.HAND, playerA, kill);
addCard(Zone.BATTLEFIELD, playerB, tiny);
addCard(Zone.BATTLEFIELD, playerB, tiny2);
attack(1, playerA, big);
block(1, playerB, tiny, big);
block(1, playerB, tiny2, big);
castSpell(1, PhaseStep.DECLARE_BLOCKERS, playerA, kill, tiny2);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, clue, 1);
assertPermanentCount(playerB, tiny, 0);
assertPermanentCount(playerA, big, 1);
assertExileCount(playerB, 1);
}
@Test
public void test_DoubleBlocked() {
//Auto-assign damage
//setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, witness);
addCard(Zone.BATTLEFIELD, playerA, big);
addCard(Zone.BATTLEFIELD, playerB, tiny);
addCard(Zone.BATTLEFIELD, playerB, tiny2);
attack(1, playerA, big);
block(1, playerB, tiny, big);
block(1, playerB, tiny2, big);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, clue, 1);
assertPermanentCount(playerB, tiny, 0);
assertPermanentCount(playerA, big, 0);
assertExileCount(playerA, 1);
assertExileCount(playerB, 0);
}
@Test
public void test_DoubleBlocker() {
//Auto-assign damage
//setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, witness);
addCard(Zone.BATTLEFIELD, playerA, tiny);
addCard(Zone.BATTLEFIELD, playerA, tiny2);
addCard(Zone.BATTLEFIELD, playerB, "Night Market Guard");
attack(1, playerA, tiny);
attack(1, playerA, tiny2);
block(1, playerB, "Night Market Guard", tiny);
block(1, playerB, "Night Market Guard", tiny2);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, clue, 1);
assertExileCount(playerA, 0);
assertExileCount(playerB, 1);
}
@Test
public void test_AttackAndBlock() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, witness);
addCard(Zone.BATTLEFIELD, playerA, tiny);
addCard(Zone.BATTLEFIELD, playerB, tiny);
attack(1, playerA, tiny);
block(1, playerB, tiny, tiny);
setChoice(playerA, ""); //stack triggers
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, clue, 2);
assertExileCount(playerA, 1);
assertExileCount(playerB, 1);
}
@Test
public void test_AttackMakesToken() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, witness);
addCard(Zone.BATTLEFIELD, playerA, "Skyknight Vanguard");
addCard(Zone.HAND, playerB, kill);
addCard(Zone.HAND, playerB, kill);
addCard(Zone.BATTLEFIELD, playerB, "Badlands", 4);
attack(1, playerA, "Skyknight Vanguard");
castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerB, kill, "Skyknight Vanguard");
waitStackResolved(1, PhaseStep.DECLARE_ATTACKERS);
castSpell(1, PhaseStep.DECLARE_ATTACKERS, playerB, kill, "Soldier Token");
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, clue, 2);
assertExileCount(playerA, 1);
}
@Test
public void test_multipleWitness() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, witness);
addCard(Zone.BATTLEFIELD, playerB, witness);
addCard(Zone.BATTLEFIELD, playerA, tiny);
addCard(Zone.BATTLEFIELD, playerB, tiny);
attack(1, playerA, tiny);
block(1, playerB, tiny, tiny);
setChoice(playerA, ""); //stack triggers
setChoice(playerB, "");
setStopAt(1, PhaseStep.END_TURN);
execute();
assertPermanentCount(playerA, clue, 2);
assertPermanentCount(playerB, clue, 2);
assertExileCount(playerA, 1);
assertExileCount(playerB, 1);
}
}

View file

@ -100,6 +100,10 @@ public class MageObjectReference implements Comparable<MageObjectReference>, Ser
}
}
@Override
public String toString(){
return "("+zoneChangeCounter+"|"+sourceId.toString().substring(0,3)+")";
}
public UUID getSourceId() {
return sourceId;
}

View file

@ -0,0 +1,70 @@
package mage.watchers.common;
import mage.constants.WatcherScope;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.watchers.Watcher;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
* Stores attacking/blocking combat information but is only updated
* 1) as damage is dealt (combat damage or otherwise)
* 2) as a spell or ability starts resolving
* 3) Land playing or special action taken (just in case that then causes a static effect to kill)
* Thus the information is available after any involved creatures die
* and all dying creatures can see all other creatures that were in combat at that time
* WARNING: This information is NOT to be used for static effects since the information will always be outdated.
* Use game.getCombat() directly or one of the other combat watchers instead
* @author notgreat
*/
public class AttackingBlockingDelayedWatcher extends Watcher {
private Set<UUID> attackers = new HashSet<>();
private Set<UUID> blockers = new HashSet<>();
public AttackingBlockingDelayedWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
switch (event.getType()) {
case LAND_PLAYED:
case TAKEN_SPECIAL_ACTION:
case RESOLVING_ABILITY:
case DAMAGED_BATCH_FOR_PERMANENTS:
//Note: getAttackers and getBlockers make a new Set, so this is safe to do
attackers = game.getCombat().getAttackers();
blockers = game.getCombat().getBlockers();
}
}
@Override
public void reset() {
super.reset();
attackers.clear();
blockers.clear();
}
public boolean checkAttacker(UUID attacker) {
return attackers.contains(attacker);
}
public boolean checkBlocker(UUID blocker) {
return blockers.contains(blocker);
}
public long countBlockers() {
return blockers.size();
}
public long countAttackers() {
return attackers.size();
}
public static AttackingBlockingDelayedWatcher getWatcher(Game game) {
return game.getState().getWatcher(AttackingBlockingDelayedWatcher.class);
}
}