diff --git a/Mage.Sets/src/mage/cards/c/CleansingBeam.java b/Mage.Sets/src/mage/cards/c/CleansingBeam.java index 4d8cd2f9b06..0693f56b247 100644 --- a/Mage.Sets/src/mage/cards/c/CleansingBeam.java +++ b/Mage.Sets/src/mage/cards/c/CleansingBeam.java @@ -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; diff --git a/Mage.Sets/src/mage/cards/c/CowardKiller.java b/Mage.Sets/src/mage/cards/c/CowardKiller.java new file mode 100644 index 00000000000..e7717532831 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CowardKiller.java @@ -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("
")); + + // 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); + } +} diff --git a/Mage.Sets/src/mage/cards/n/NameStickerGoblin.java b/Mage.Sets/src/mage/cards/n/NameStickerGoblin.java new file mode 100644 index 00000000000..9d8f6968abd --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NameStickerGoblin.java @@ -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 + * Original announcement + * Scryfall link + * @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 it’s + // 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); + } +} diff --git a/Mage.Sets/src/mage/cards/t/ThijarianWitness.java b/Mage.Sets/src/mage/cards/t/ThijarianWitness.java new file mode 100644 index 00000000000..28079712758 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThijarianWitness.java @@ -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 { + 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"; + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index d448dae20df..8c8b624045d 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -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)); diff --git a/Mage.Sets/src/mage/sets/Unfinity.java b/Mage.Sets/src/mage/sets/Unfinity.java index 4fa05a25004..cd97818af28 100644 --- a/Mage.Sets/src/mage/sets/Unfinity.java +++ b/Mage.Sets/src/mage/sets/Unfinity.java @@ -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)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/unf/NameStickerGoblinTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/unf/NameStickerGoblinTest.java new file mode 100644 index 00000000000..3e8383d888d --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/unf/NameStickerGoblinTest.java @@ -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 it’s 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(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/who/ThijarianWitnessTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/who/ThijarianWitnessTest.java new file mode 100644 index 00000000000..3e86680602a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/who/ThijarianWitnessTest.java @@ -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); + } +} diff --git a/Mage/src/main/java/mage/MageObjectReference.java b/Mage/src/main/java/mage/MageObjectReference.java index b0cdc055a78..33583eaa57a 100644 --- a/Mage/src/main/java/mage/MageObjectReference.java +++ b/Mage/src/main/java/mage/MageObjectReference.java @@ -100,6 +100,10 @@ public class MageObjectReference implements Comparable, Ser } } + @Override + public String toString(){ + return "("+zoneChangeCounter+"|"+sourceId.toString().substring(0,3)+")"; + } public UUID getSourceId() { return sourceId; } diff --git a/Mage/src/main/java/mage/watchers/common/AttackingBlockingDelayedWatcher.java b/Mage/src/main/java/mage/watchers/common/AttackingBlockingDelayedWatcher.java new file mode 100644 index 00000000000..e5c2264ffab --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/AttackingBlockingDelayedWatcher.java @@ -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 attackers = new HashSet<>(); + private Set 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); + } +}