mirror of
https://github.com/magefree/mage.git
synced 2026-01-09 12:22:10 -08:00
Fix first strike damage logic (#12297)
* add tests for first strike rules * fix first strike damage logic per 702.7c * add more test cases * update logic to not check actual damage dealt * add another test case * adjust naming and docs
This commit is contained in:
parent
8dfafad95c
commit
33fe4730ae
4 changed files with 489 additions and 56 deletions
|
|
@ -1,40 +1,417 @@
|
||||||
package org.mage.test.combat;
|
package org.mage.test.combat;
|
||||||
|
|
||||||
|
import mage.abilities.keyword.DoubleStrikeAbility;
|
||||||
|
import mage.abilities.keyword.FirstStrikeAbility;
|
||||||
import mage.constants.PhaseStep;
|
import mage.constants.PhaseStep;
|
||||||
import mage.constants.Zone;
|
import mage.constants.Zone;
|
||||||
|
import mage.counters.CounterType;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||||
|
|
||||||
public class FirstStrikeTest extends CardTestPlayerBase {
|
public class FirstStrikeTest extends CardTestPlayerBase {
|
||||||
|
|
||||||
|
/*
|
||||||
|
702.7b. If at least one attacking or blocking creature has first strike or double strike (see rule 702.4)
|
||||||
|
as the combat damage step begins, the only creatures that assign combat damage in that step
|
||||||
|
are those with first strike or double strike. After that step, instead of proceeding to the end of combat step,
|
||||||
|
the phase gets a second combat damage step. The only creatures that assign combat damage in that step
|
||||||
|
are the remaining attackers and blockers that had neither first strike nor double strike
|
||||||
|
as the first combat damage step began, as well as the remaining attackers and blockers
|
||||||
|
that currently have double strike. After that step, the phase proceeds to the end of combat step.
|
||||||
|
|
||||||
|
702.7c. Giving first strike to a creature without it after combat damage has already been dealt
|
||||||
|
in the first combat damage step won't preclude that creature from assigning combat damage
|
||||||
|
in the second combat damage step. Removing first strike from a creature after it has already
|
||||||
|
dealt combat damage in the first combat damage step won't allow it to also assign combat damage
|
||||||
|
in the second combat damage step (unless the creature has double strike).
|
||||||
|
|
||||||
|
702.4c. Removing double strike from a creature during the first combat damage step will stop it from
|
||||||
|
assigning combat damage in the second combat damage step.
|
||||||
|
|
||||||
|
702.4d. Giving double strike to a creature with first strike after it has already dealt combat damage
|
||||||
|
in the first combat damage step will allow the creature to assign combat damage in the second combat damage step.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static final String knight = "White Knight"; // 2/2 first strike, protection from black
|
||||||
|
private static final String bears = "Grizzly Bears"; // 2/2
|
||||||
|
private static final String piker = "Goblin Piker"; // 2/1
|
||||||
|
private static final String ghoul = "Warpath Ghoul"; // 3/2
|
||||||
|
private static final String centaur = "Centaur Courser"; // 3/3
|
||||||
|
private static final String sentry = "Dragon's Eye Sentry"; // 1/3, defender, first strike
|
||||||
|
private static final String bodyguard = "Anaba Bodyguard"; // 2/3 first strike
|
||||||
|
private static final String blademaster = "Markov Blademaster"; // 1/1 double strike, +1/+1 counter when deals damage
|
||||||
|
private static final String wolverine = "Spelleater Wolverine"; // 3/2, double strike as long as 3+ instants/sorceries in your graveyard
|
||||||
|
private static final String fury = "Kindled Fury"; // R: target gets +1/+0 and first strike
|
||||||
|
private static final String runemark = "Mardu Runemark"; // 2R Aura
|
||||||
|
// Enchanted creature gets +2/+2.
|
||||||
|
// Enchanted creature has first strike as long as you control a white or black permanent.
|
||||||
|
private static final String urchin = "Bile Urchin"; // 1/1
|
||||||
|
// Sacrifice Bile Urchin: Target player loses 1 life
|
||||||
|
private static final String cleave = "Double Cleave"; // 1{R/W}: target gains double strike
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void firstStrikeAttacker(){
|
public void firstStrikeAttacker() {
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Silver Knight", 1);
|
addCard(Zone.BATTLEFIELD, playerA, knight, 1);
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1);
|
addCard(Zone.BATTLEFIELD, playerB, bears, 1);
|
||||||
|
|
||||||
attack(1, playerA, "Silver Knight");
|
attack(1, playerA, knight, playerB);
|
||||||
block(1, playerB, "Grizzly Bears", "Silver Knight");
|
block(1, playerB, bears, knight);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
assertGraveyardCount(playerB, "Grizzly Bears", 1);
|
assertGraveyardCount(playerB, bears, 1);
|
||||||
assertGraveyardCount(playerA, "Silver Knight", 0);
|
assertGraveyardCount(playerA, knight, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void firstStrikeBlocker(){
|
public void firstStrikeBlocker() {
|
||||||
addCard(Zone.BATTLEFIELD, playerB, "Silver Knight", 1);
|
addCard(Zone.BATTLEFIELD, playerB, knight, 1);
|
||||||
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
|
addCard(Zone.BATTLEFIELD, playerA, bears, 1);
|
||||||
|
|
||||||
attack(1, playerA, "Grizzly Bears");
|
attack(1, playerA, bears, playerB);
|
||||||
block(1, playerB, "Silver Knight", "Grizzly Bears");
|
block(1, playerB, knight, bears);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
execute();
|
execute();
|
||||||
|
|
||||||
assertGraveyardCount(playerA, "Grizzly Bears", 1);
|
assertGraveyardCount(playerA, bears, 1);
|
||||||
assertGraveyardCount(playerB, "Silver Knight", 0);
|
assertGraveyardCount(playerB, knight, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeBoth() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, bodyguard);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, sentry);
|
||||||
|
|
||||||
|
attack(1, playerA, bodyguard, playerB);
|
||||||
|
block(1, playerB, sentry, bodyguard);
|
||||||
|
|
||||||
|
checkDamage("after first strike", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, bodyguard, 1);
|
||||||
|
checkDamage("after first strike", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, sentry, 2);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertPermanentCount(playerA, bodyguard, 1);
|
||||||
|
assertPermanentCount(playerB, sentry, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doubleStrikePump() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, blademaster); // 1/1
|
||||||
|
// Double strike; Whenever Markov Blademaster deals combat damage to a player, put a +1/+1 counter on it.
|
||||||
|
|
||||||
|
attack(1, playerA, blademaster, playerB);
|
||||||
|
|
||||||
|
checkLife("after first strike damage", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 19);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 17);
|
||||||
|
assertPowerToughness(playerA, blademaster, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeGainedAttacker() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, piker, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, bears, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
|
||||||
|
addCard(Zone.HAND, playerA, fury);
|
||||||
|
|
||||||
|
attack(1, playerA, piker, playerB);
|
||||||
|
block(1, playerB, bears, piker);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.DECLARE_BLOCKERS, playerA, fury, piker);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerB, bears, 1);
|
||||||
|
assertGraveyardCount(playerA, piker, 0);
|
||||||
|
assertAbility(playerA, piker, FirstStrikeAbility.getInstance(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeGainedBlocker() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, piker, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, bears, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Mountain");
|
||||||
|
addCard(Zone.HAND, playerB, fury);
|
||||||
|
|
||||||
|
attack(1, playerA, bears, playerB);
|
||||||
|
block(1, playerB, piker, bears);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.DECLARE_BLOCKERS, playerB, fury, piker);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerA, bears, 1);
|
||||||
|
assertGraveyardCount(playerB, piker, 0);
|
||||||
|
assertAbility(playerB, piker, FirstStrikeAbility.getInstance(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeGainedBothDie() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, knight, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, bears, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, "Mountain");
|
||||||
|
addCard(Zone.HAND, playerB, fury);
|
||||||
|
|
||||||
|
attack(1, playerA, knight, playerB);
|
||||||
|
block(1, playerB, bears, knight);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.DECLARE_BLOCKERS, playerB, fury, bears);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertGraveyardCount(playerB, bears, 1);
|
||||||
|
assertGraveyardCount(playerA, knight, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeGainedMidCombat() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, knight, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, ghoul, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
|
||||||
|
addCard(Zone.HAND, playerA, fury);
|
||||||
|
|
||||||
|
attack(1, playerA, knight, playerB);
|
||||||
|
attack(1, playerA, ghoul, playerB);
|
||||||
|
|
||||||
|
checkLife("after first strike", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 18);
|
||||||
|
castSpell(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, fury, ghoul);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeLostMidCombat() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, centaur, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, urchin, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
|
||||||
|
addCard(Zone.HAND, playerA, runemark);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, runemark, centaur);
|
||||||
|
|
||||||
|
checkAbility("first strike gained", 1, PhaseStep.BEGIN_COMBAT, playerA, centaur, FirstStrikeAbility.class, true);
|
||||||
|
|
||||||
|
attack(1, playerA, centaur, playerB);
|
||||||
|
|
||||||
|
checkLife("after first strike", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 15);
|
||||||
|
activateAbility(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, "Sacrifice ");
|
||||||
|
addTarget(playerA, playerA);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerA, 19);
|
||||||
|
assertLife(playerB, 15);
|
||||||
|
assertAbility(playerA, centaur, FirstStrikeAbility.getInstance(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doubleStrikeGainedMidCombat() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, knight, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
|
||||||
|
addCard(Zone.HAND, playerA, cleave);
|
||||||
|
|
||||||
|
attack(1, playerA, knight, playerB);
|
||||||
|
|
||||||
|
checkLife("after first strike", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 18);
|
||||||
|
castSpell(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, cleave, knight);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 16);
|
||||||
|
assertAbility(playerA, knight, DoubleStrikeAbility.getInstance(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doubleStrikeLostMidCombat() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, wolverine, 1);
|
||||||
|
addCard(Zone.GRAVEYARD, playerA, "Lightning Bolt");
|
||||||
|
addCard(Zone.GRAVEYARD, playerA, "Divination");
|
||||||
|
addCard(Zone.GRAVEYARD, playerA, "Prey Upon");
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Heap Doll"); // sac to exile target from graveyard
|
||||||
|
|
||||||
|
checkAbility("has double strike", 1, PhaseStep.BEGIN_COMBAT, playerA, wolverine, DoubleStrikeAbility.class, true);
|
||||||
|
|
||||||
|
attack(1, playerA, wolverine, playerB);
|
||||||
|
|
||||||
|
checkLife("after first strike", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 17);
|
||||||
|
activateAbility(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, "Sacrifice ");
|
||||||
|
addTarget(playerA, "Divination");
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 17);
|
||||||
|
assertAbility(playerA, wolverine, DoubleStrikeAbility.getInstance(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeLostDoubleStrikeGained() {
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, centaur, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, urchin, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5);
|
||||||
|
addCard(Zone.HAND, playerA, runemark);
|
||||||
|
addCard(Zone.HAND, playerA, cleave);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, runemark, centaur);
|
||||||
|
|
||||||
|
checkAbility("first strike gained", 1, PhaseStep.BEGIN_COMBAT, playerA, centaur, FirstStrikeAbility.class, true);
|
||||||
|
checkAbility("no double strike", 1, PhaseStep.BEGIN_COMBAT, playerA, centaur, DoubleStrikeAbility.class, false);
|
||||||
|
|
||||||
|
attack(1, playerA, centaur, playerB);
|
||||||
|
|
||||||
|
checkLife("after first strike", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 15);
|
||||||
|
activateAbility(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, "Sacrifice ");
|
||||||
|
addTarget(playerA, playerA);
|
||||||
|
waitStackResolved(1, PhaseStep.FIRST_COMBAT_DAMAGE);
|
||||||
|
castSpell(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, cleave, centaur);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerA, 19);
|
||||||
|
assertLife(playerB, 10);
|
||||||
|
assertAbility(playerA, centaur, FirstStrikeAbility.getInstance(), false);
|
||||||
|
assertAbility(playerA, centaur, DoubleStrikeAbility.getInstance(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void damageDealtInBetween() {
|
||||||
|
// To check that the first strike damage watcher doesn't count noncombat damage
|
||||||
|
|
||||||
|
String fall = "Fall of the Hammer"; // 1R Instant
|
||||||
|
// Target creature you control deals damage equal to its power to another target creature.
|
||||||
|
String hatchling = "Kraken Hatchling"; // 0/4
|
||||||
|
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, knight, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, ghoul, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerB, hatchling);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
|
||||||
|
addCard(Zone.HAND, playerA, fall);
|
||||||
|
|
||||||
|
attack(1, playerA, knight, playerB);
|
||||||
|
attack(1, playerA, ghoul, playerB);
|
||||||
|
|
||||||
|
checkLife("after first strike", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 18);
|
||||||
|
castSpell(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, fall);
|
||||||
|
addTarget(playerA, ghoul);
|
||||||
|
addTarget(playerA, hatchling);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 15);
|
||||||
|
assertDamageReceived(playerB, hatchling, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeDamagePrevented() {
|
||||||
|
String prevention = "Dazzling Reflection"; // 1W Instant
|
||||||
|
// You gain life equal to target creature’s power. The next time that creature would deal damage this turn, prevent that damage.
|
||||||
|
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, knight, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, ghoul, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
|
||||||
|
addCard(Zone.HAND, playerA, prevention);
|
||||||
|
|
||||||
|
attack(1, playerA, knight, playerB);
|
||||||
|
attack(1, playerA, ghoul, playerB);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.DECLARE_BLOCKERS, playerA, prevention, knight);
|
||||||
|
|
||||||
|
checkLife("life gained", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, 22);
|
||||||
|
checkLife("damage prevented", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 20);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 17);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstStrikeZeroDamage() {
|
||||||
|
String rograkh = "Rograkh, Son of Rohgahh"; // 0/1 first strike
|
||||||
|
String battlegrowth = "Battlegrowth"; // G: put a +1/+1 counter on target creature
|
||||||
|
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, rograkh, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Forest");
|
||||||
|
addCard(Zone.HAND, playerA, battlegrowth);
|
||||||
|
|
||||||
|
attack(1, playerA, rograkh, playerB);
|
||||||
|
|
||||||
|
castSpell(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, battlegrowth, rograkh);
|
||||||
|
// no combat damage should be dealt here
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
assertLife(playerB, 20);
|
||||||
|
assertPowerToughness(playerA, rograkh, 1, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ninjutsuTwice() {
|
||||||
|
/* If a creature in combat has first strike or double strike,
|
||||||
|
* you can activate the ninjutsu ability during the first-strike combat damage step.
|
||||||
|
* The Ninja will deal combat damage during the regular combat damage step, even if it has first strike.
|
||||||
|
*/
|
||||||
|
|
||||||
|
String moonblade = "Moonblade Shinobi"; // 3/2, Ninjutsu 2U
|
||||||
|
// Whenever Moonblade Shinobi deals combat damage to a player, create a 1/1 blue Illusion creature token with flying.
|
||||||
|
String ambusher = "Mukotai Ambusher"; // 3/2 Lifelink; Ninjutsu 1B
|
||||||
|
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, moonblade, 1);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 5);
|
||||||
|
addCard(Zone.HAND, playerA, ambusher);
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Celebrity Fencer");
|
||||||
|
// Whenever another creature enters the battlefield under your control, put a +1/+1 counter on Celebrity Fencer.
|
||||||
|
addCard(Zone.BATTLEFIELD, playerA, "Knighthood"); // Creatures you control have first strike
|
||||||
|
|
||||||
|
attack(1, playerA, moonblade, playerB);
|
||||||
|
|
||||||
|
checkLife("first strike damage dealt", 1, PhaseStep.FIRST_COMBAT_DAMAGE, playerB, 17);
|
||||||
|
activateAbility(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, "Ninjutsu {1}{B}");
|
||||||
|
setChoice(playerA, moonblade);
|
||||||
|
waitStackResolved(1, PhaseStep.FIRST_COMBAT_DAMAGE);
|
||||||
|
activateAbility(1, PhaseStep.FIRST_COMBAT_DAMAGE, playerA, "Ninjutsu {2}{U}");
|
||||||
|
setChoice(playerA, ambusher);
|
||||||
|
|
||||||
|
setStrictChooseMode(true);
|
||||||
|
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
|
||||||
|
execute();
|
||||||
|
|
||||||
|
// the moonblade deals 3 damage each step because it is a different object after zone change
|
||||||
|
assertLife(playerA, 20);
|
||||||
|
assertLife(playerB, 14);
|
||||||
|
assertPermanentCount(playerA, "Illusion Token", 2);
|
||||||
|
assertCounterCount(playerA, "Celebrity Fencer", CounterType.P1P1, 4); // two tokens and both ninjutsu
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1386,6 +1386,7 @@ public abstract class GameImpl implements Game {
|
||||||
List<Watcher> newWatchers = new ArrayList<>();
|
List<Watcher> newWatchers = new ArrayList<>();
|
||||||
newWatchers.add(new CastSpellLastTurnWatcher());
|
newWatchers.add(new CastSpellLastTurnWatcher());
|
||||||
newWatchers.add(new PlayerLostLifeWatcher());
|
newWatchers.add(new PlayerLostLifeWatcher());
|
||||||
|
newWatchers.add(new FirstStrikeWatcher()); // required for combat code
|
||||||
newWatchers.add(new BlockedAttackerWatcher());
|
newWatchers.add(new BlockedAttackerWatcher());
|
||||||
newWatchers.add(new PlanarRollWatcher()); // needed for RollDiceTest (planechase code needs improves)
|
newWatchers.add(new PlanarRollWatcher()); // needed for RollDiceTest (planechase code needs improves)
|
||||||
newWatchers.add(new AttackedThisTurnWatcher());
|
newWatchers.add(new AttackedThisTurnWatcher());
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import mage.game.events.GameEvent;
|
||||||
import mage.game.permanent.Permanent;
|
import mage.game.permanent.Permanent;
|
||||||
import mage.players.Player;
|
import mage.players.Player;
|
||||||
import mage.util.Copyable;
|
import mage.util.Copyable;
|
||||||
|
import mage.watchers.common.FirstStrikeWatcher;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
@ -60,10 +61,9 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
|
|
||||||
public boolean hasFirstOrDoubleStrike(Game game) {
|
public boolean hasFirstOrDoubleStrike(Game game) {
|
||||||
return Stream.concat(attackers.stream(), blockers.stream())
|
return Stream.concat(attackers.stream(), blockers.stream())
|
||||||
.map(id -> game.getPermanent(id))
|
.map(game::getPermanent)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.anyMatch(this::hasFirstOrDoubleStrike);
|
.anyMatch(CombatGroup::hasFirstOrDoubleStrike);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -89,27 +89,27 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
return blockerOrder;
|
return blockerOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasFirstOrDoubleStrike(Permanent perm) {
|
private static boolean hasFirstOrDoubleStrike(Permanent perm) {
|
||||||
return perm.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()) || perm.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId());
|
return hasFirstStrike(perm) || hasDoubleStrike(perm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasFirstStrike(Permanent perm) {
|
private static boolean hasFirstStrike(Permanent perm) {
|
||||||
return perm.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId());
|
return perm.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasDoubleStrike(Permanent perm) {
|
private static boolean hasDoubleStrike(Permanent perm) {
|
||||||
return perm.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId());
|
return perm.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasTrample(Permanent perm) {
|
private static boolean hasTrample(Permanent perm) {
|
||||||
return perm.getAbilities().containsKey(TrampleAbility.getInstance().getId());
|
return perm.getAbilities().containsKey(TrampleAbility.getInstance().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasTrampleOverPlaneswalkers(Permanent perm) {
|
private static boolean hasTrampleOverPlaneswalkers(Permanent perm) {
|
||||||
return perm.getAbilities().containsKey(TrampleOverPlaneswalkersAbility.getInstance().getId());
|
return perm.getAbilities().containsKey(TrampleOverPlaneswalkersAbility.getInstance().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasBanding(Permanent perm) {
|
private static boolean hasBanding(Permanent perm) {
|
||||||
return perm.getAbilities().containsKey(BandingAbility.getInstance().getId());
|
return perm.getAbilities().containsKey(BandingAbility.getInstance().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,38 +229,33 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if permanent can damage in current (First Strike or not)
|
* Determines if permanent is to deal damage this step based on whether it has first/double strike
|
||||||
* combat damage step
|
* and whether it did during the first combat damage step of this phase.
|
||||||
|
* Info is stored in FirstStrikeWatcher.
|
||||||
*
|
*
|
||||||
* @param perm Permanent to check
|
* @param perm Permanent to check
|
||||||
* @param first First strike or common combat damage step
|
* @param first true for first strike damage step, false for normal damage step
|
||||||
* @return
|
* @return true if permanent should deal damage this step
|
||||||
*/
|
*/
|
||||||
private boolean canDamage(Permanent perm, boolean first) {
|
private boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) {
|
||||||
if (perm == null) {
|
if (perm == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// if now first strike combat damage step
|
|
||||||
if (first) {
|
if (first) {
|
||||||
// should have first strike or double strike
|
if (hasFirstOrDoubleStrike(perm)) {
|
||||||
return hasFirstOrDoubleStrike(perm);
|
FirstStrikeWatcher.recordFirstStrikingCreature(perm.getId(), game);
|
||||||
} // if now not first strike combat
|
return true;
|
||||||
else {
|
|
||||||
if (hasFirstStrike(perm)) {
|
|
||||||
// if it has first strike in non FS combat damage step
|
|
||||||
// then it can damage only if it has ALSO double strike
|
|
||||||
// Fixes Issue 200
|
|
||||||
return hasDoubleStrike(perm);
|
|
||||||
}
|
}
|
||||||
// can damage otherwise
|
return false;
|
||||||
return true;
|
} else { // 702.7c
|
||||||
|
return hasDoubleStrike(perm) || !FirstStrikeWatcher.wasFirstStrikingCreature(perm.getId(), game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unblockedDamage(boolean first, Game game) {
|
private void unblockedDamage(boolean first, Game game) {
|
||||||
for (UUID attackerId : attackers) {
|
for (UUID attackerId : attackers) {
|
||||||
Permanent attacker = game.getPermanent(attackerId);
|
Permanent attacker = game.getPermanent(attackerId);
|
||||||
if (canDamage(attacker, first)) {
|
if (dealsDamageThisStep(attacker, first, game)) {
|
||||||
//20091005 - 510.1c, 702.17c
|
//20091005 - 510.1c, 702.17c
|
||||||
if (!blocked || hasTrample(attacker)) {
|
if (!blocked || hasTrample(attacker)) {
|
||||||
defenderDamage(attacker, getDamageValueFromPermanent(attacker, game), game, false);
|
defenderDamage(attacker, getDamageValueFromPermanent(attacker, game), game, false);
|
||||||
|
|
@ -274,7 +269,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
Permanent attacker = game.getPermanent(attackers.get(0));
|
Permanent attacker = game.getPermanent(attackers.get(0));
|
||||||
if (blocker != null && attacker != null) {
|
if (blocker != null && attacker != null) {
|
||||||
int blockerDamage = getDamageValueFromPermanent(blocker, game); // must be set before attacker damage marking because of effects like Test of Faith
|
int blockerDamage = getDamageValueFromPermanent(blocker, game); // must be set before attacker damage marking because of effects like Test of Faith
|
||||||
if (blocked && canDamage(attacker, first)) {
|
if (blocked && dealsDamageThisStep(attacker, first, game)) {
|
||||||
int damage = getDamageValueFromPermanent(attacker, game);
|
int damage = getDamageValueFromPermanent(attacker, game);
|
||||||
if (hasTrample(attacker)) {
|
if (hasTrample(attacker)) {
|
||||||
int lethalDamage = getLethalDamage(blocker, attacker, game);
|
int lethalDamage = getLethalDamage(blocker, attacker, game);
|
||||||
|
|
@ -292,7 +287,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
blocker.markDamage(damage, attacker.getId(), null, game, true, true);
|
blocker.markDamage(damage, attacker.getId(), null, game, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (canDamage(blocker, first)) {
|
if (dealsDamageThisStep(blocker, first, game)) {
|
||||||
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
|
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
|
||||||
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
|
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
|
||||||
attacker.markDamage(blockerDamage, blocker.getId(), null, game, true, true);
|
attacker.markDamage(blockerDamage, blocker.getId(), null, game, true, true);
|
||||||
|
|
@ -309,12 +304,12 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
}
|
}
|
||||||
boolean oldRuleDamage = (Objects.equals(player.getId(), defendingPlayerId));
|
boolean oldRuleDamage = (Objects.equals(player.getId(), defendingPlayerId));
|
||||||
int damage = getDamageValueFromPermanent(attacker, game);
|
int damage = getDamageValueFromPermanent(attacker, game);
|
||||||
if (canDamage(attacker, first)) {
|
if (dealsDamageThisStep(attacker, first, game)) {
|
||||||
// must be set before attacker damage marking because of effects like Test of Faith
|
// must be set before attacker damage marking because of effects like Test of Faith
|
||||||
Map<UUID, Integer> blockerPower = new HashMap<>();
|
Map<UUID, Integer> blockerPower = new HashMap<>();
|
||||||
for (UUID blockerId : blockerOrder) {
|
for (UUID blockerId : blockerOrder) {
|
||||||
Permanent blocker = game.getPermanent(blockerId);
|
Permanent blocker = game.getPermanent(blockerId);
|
||||||
if (canDamage(blocker, first)) {
|
if (dealsDamageThisStep(blocker, first, game)) {
|
||||||
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
|
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
|
||||||
blockerPower.put(blockerId, getDamageValueFromPermanent(blocker, game));
|
blockerPower.put(blockerId, getDamageValueFromPermanent(blocker, game));
|
||||||
}
|
}
|
||||||
|
|
@ -375,7 +370,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
} else {
|
} else {
|
||||||
for (UUID blockerId : blockerOrder) {
|
for (UUID blockerId : blockerOrder) {
|
||||||
Permanent blocker = game.getPermanent(blockerId);
|
Permanent blocker = game.getPermanent(blockerId);
|
||||||
if (canDamage(blocker, first)) {
|
if (dealsDamageThisStep(blocker, first, game)) {
|
||||||
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
|
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
|
||||||
attacker.markDamage(getDamageValueFromPermanent(blocker, game), blocker.getId(), null, game, true, true);
|
attacker.markDamage(getDamageValueFromPermanent(blocker, game), blocker.getId(), null, game, true, true);
|
||||||
}
|
}
|
||||||
|
|
@ -391,12 +386,12 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int damage = getDamageValueFromPermanent(attacker, game);
|
int damage = getDamageValueFromPermanent(attacker, game);
|
||||||
if (canDamage(attacker, first)) {
|
if (dealsDamageThisStep(attacker, first, game)) {
|
||||||
// must be set before attacker damage marking because of effects like Test of Faith
|
// must be set before attacker damage marking because of effects like Test of Faith
|
||||||
Map<UUID, Integer> blockerPower = new HashMap<>();
|
Map<UUID, Integer> blockerPower = new HashMap<>();
|
||||||
for (UUID blockerId : blockerOrder) {
|
for (UUID blockerId : blockerOrder) {
|
||||||
Permanent blocker = game.getPermanent(blockerId);
|
Permanent blocker = game.getPermanent(blockerId);
|
||||||
if (canDamage(blocker, first)) {
|
if (dealsDamageThisStep(blocker, first, game)) {
|
||||||
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
|
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
|
||||||
blockerPower.put(blockerId, getDamageValueFromPermanent(blocker, game));
|
blockerPower.put(blockerId, getDamageValueFromPermanent(blocker, game));
|
||||||
}
|
}
|
||||||
|
|
@ -440,7 +435,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
if (isAttacking) {
|
if (isAttacking) {
|
||||||
for (UUID blockerId : blockerOrder) {
|
for (UUID blockerId : blockerOrder) {
|
||||||
Permanent blocker = game.getPermanent(blockerId);
|
Permanent blocker = game.getPermanent(blockerId);
|
||||||
if (canDamage(blocker, first)) {
|
if (dealsDamageThisStep(blocker, first, game)) {
|
||||||
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
|
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
|
||||||
attacker.markDamage(getDamageValueFromPermanent(blocker, game), blocker.getId(), null, game, true, true);
|
attacker.markDamage(getDamageValueFromPermanent(blocker, game), blocker.getId(), null, game, true, true);
|
||||||
}
|
}
|
||||||
|
|
@ -488,7 +483,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
Permanent blocker = game.getPermanent(blockers.get(0));
|
Permanent blocker = game.getPermanent(blockers.get(0));
|
||||||
Permanent attacker = game.getPermanent(attackers.get(0));
|
Permanent attacker = game.getPermanent(attackers.get(0));
|
||||||
if (blocker != null && attacker != null) {
|
if (blocker != null && attacker != null) {
|
||||||
if (canDamage(blocker, first)) {
|
if (dealsDamageThisStep(blocker, first, game)) {
|
||||||
int damage = getDamageValueFromPermanent(blocker, game);
|
int damage = getDamageValueFromPermanent(blocker, game);
|
||||||
attacker.markDamage(damage, blocker.getId(), null, game, true, true);
|
attacker.markDamage(damage, blocker.getId(), null, game, true, true);
|
||||||
}
|
}
|
||||||
|
|
@ -514,7 +509,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
Player player = game.getPlayer(oldRuleDamage ? game.getCombat().getAttackingPlayerId() : blocker.getControllerId());
|
Player player = game.getPlayer(oldRuleDamage ? game.getCombat().getAttackingPlayerId() : blocker.getControllerId());
|
||||||
int damage = getDamageValueFromPermanent(blocker, game);
|
int damage = getDamageValueFromPermanent(blocker, game);
|
||||||
|
|
||||||
if (canDamage(blocker, first)) {
|
if (dealsDamageThisStep(blocker, first, game)) {
|
||||||
Map<UUID, Integer> assigned = new HashMap<>();
|
Map<UUID, Integer> assigned = new HashMap<>();
|
||||||
for (UUID attackerId : attackerOrder) {
|
for (UUID attackerId : attackerOrder) {
|
||||||
Permanent attacker = game.getPermanent(attackerId);
|
Permanent attacker = game.getPermanent(attackerId);
|
||||||
|
|
@ -881,7 +876,10 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
}
|
}
|
||||||
for (UUID attackerId : attackers) { // changing defender will remove a banded attacker from its current band
|
for (UUID attackerId : attackers) { // changing defender will remove a banded attacker from its current band
|
||||||
Permanent attacker = game.getPermanent(attackerId);
|
Permanent attacker = game.getPermanent(attackerId);
|
||||||
if (attacker != null && attacker.getBandedCards() != null) {
|
if (attacker == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (attacker.getBandedCards() != null) {
|
||||||
for (UUID bandedId : attacker.getBandedCards()) {
|
for (UUID bandedId : attacker.getBandedCards()) {
|
||||||
Permanent banded = game.getPermanent(bandedId);
|
Permanent banded = game.getPermanent(bandedId);
|
||||||
if (banded != null) {
|
if (banded != null) {
|
||||||
|
|
@ -958,7 +956,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : (!isAttacking && attackerAssignsCombatDamage(game) ? game.getCombat().getAttackingPlayerId() : playerId));
|
Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : (!isAttacking && attackerAssignsCombatDamage(game) ? game.getCombat().getAttackingPlayerId() : playerId));
|
||||||
// 10/4/2004 If it is blocked but then all of its blockers are removed before combat damage is assigned, then it won't be able to deal combat damage and you won't be able to use its ability.
|
// 10/4/2004 If it is blocked but then all of its blockers are removed before combat damage is assigned, then it won't be able to deal combat damage and you won't be able to use its ability.
|
||||||
// (same principle should apply if it's blocking and its blocked attacker is removed from combat)
|
// (same principle should apply if it's blocking and its blocked attacker is removed from combat)
|
||||||
if (!((blocked && blockers.isEmpty() && isAttacking) || (attackers.isEmpty() && !isAttacking)) && canDamage(creature, first)) {
|
if (!((blocked && blockers.isEmpty() && isAttacking) || (attackers.isEmpty() && !isAttacking)) && dealsDamageThisStep(creature, first, game)) {
|
||||||
if (player.chooseUse(Outcome.Damage, "Have " + creature.getLogName() + " assign its combat damage divided among defending player and/or any number of defending creatures?", null, game)) {
|
if (player.chooseUse(Outcome.Damage, "Have " + creature.getLogName() + " assign its combat damage divided among defending player and/or any number of defending creatures?", null, game)) {
|
||||||
defendingPlayerAndOrDefendingCreaturesDividedDamage(creature, player, first, game, isAttacking);
|
defendingPlayerAndOrDefendingCreaturesDividedDamage(creature, player, first, game, isAttacking);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -968,7 +966,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getLethalDamage(Permanent blocker, Permanent attacker, Game game) {
|
private static int getLethalDamage(Permanent damaged, Permanent damaging, Game game) {
|
||||||
return blocker.getLethalDamage(attacker.getId(), game);
|
return damaged.getLethalDamage(damaging.getId(), game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package mage.watchers.common;
|
||||||
|
|
||||||
|
import mage.MageObjectReference;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author xenohedron
|
||||||
|
*/
|
||||||
|
public class FirstStrikeWatcher extends Watcher {
|
||||||
|
|
||||||
|
// creatures that had first strike or double strike for the first strike combat damage step of this combat phase
|
||||||
|
// (note, due to 0 power or prevention, they may not necessarily have dealt damage)
|
||||||
|
private final Set<MageObjectReference> firstStrikingCreatures;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Game default watcher, required for combat code
|
||||||
|
*/
|
||||||
|
public FirstStrikeWatcher() {
|
||||||
|
super(WatcherScope.GAME);
|
||||||
|
this.firstStrikingCreatures = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void watch(GameEvent event, Game game) {
|
||||||
|
if (event.getType() == GameEvent.EventType.COMBAT_PHASE_POST) {
|
||||||
|
firstStrikingCreatures.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
super.reset();
|
||||||
|
firstStrikingCreatures.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void recordFirstStrikingCreature(UUID creatureId, Game game) {
|
||||||
|
game.getState()
|
||||||
|
.getWatcher(FirstStrikeWatcher.class)
|
||||||
|
.firstStrikingCreatures
|
||||||
|
.add(new MageObjectReference(creatureId, game));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean wasFirstStrikingCreature(UUID creatureId, Game game) {
|
||||||
|
return game.getState()
|
||||||
|
.getWatcher(FirstStrikeWatcher.class)
|
||||||
|
.firstStrikingCreatures
|
||||||
|
.contains(new MageObjectReference(creatureId, game));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue