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:
xenohedron 2024-05-26 20:01:01 -04:00 committed by GitHub
parent 8dfafad95c
commit 33fe4730ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 489 additions and 56 deletions

View file

@ -1,40 +1,417 @@
package org.mage.test.combat;
import mage.abilities.keyword.DoubleStrikeAbility;
import mage.abilities.keyword.FirstStrikeAbility;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.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
public void firstStrikeAttacker(){
addCard(Zone.BATTLEFIELD, playerA, "Silver Knight", 1);
addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1);
public void firstStrikeAttacker() {
addCard(Zone.BATTLEFIELD, playerA, knight, 1);
addCard(Zone.BATTLEFIELD, playerB, bears, 1);
attack(1, playerA, "Silver Knight");
block(1, playerB, "Grizzly Bears", "Silver Knight");
attack(1, playerA, knight, playerB);
block(1, playerB, bears, knight);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertGraveyardCount(playerB, "Grizzly Bears", 1);
assertGraveyardCount(playerA, "Silver Knight", 0);
assertGraveyardCount(playerB, bears, 1);
assertGraveyardCount(playerA, knight, 0);
}
@Test
public void firstStrikeBlocker(){
addCard(Zone.BATTLEFIELD, playerB, "Silver Knight", 1);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
public void firstStrikeBlocker() {
addCard(Zone.BATTLEFIELD, playerB, knight, 1);
addCard(Zone.BATTLEFIELD, playerA, bears, 1);
attack(1, playerA, "Grizzly Bears");
block(1, playerB, "Silver Knight", "Grizzly Bears");
attack(1, playerA, bears, playerB);
block(1, playerB, knight, bears);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertGraveyardCount(playerA, "Grizzly Bears", 1);
assertGraveyardCount(playerB, "Silver Knight", 0);
assertGraveyardCount(playerA, bears, 1);
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 creatures 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
}
}

View file

@ -1386,6 +1386,7 @@ public abstract class GameImpl implements Game {
List<Watcher> newWatchers = new ArrayList<>();
newWatchers.add(new CastSpellLastTurnWatcher());
newWatchers.add(new PlayerLostLifeWatcher());
newWatchers.add(new FirstStrikeWatcher()); // required for combat code
newWatchers.add(new BlockedAttackerWatcher());
newWatchers.add(new PlanarRollWatcher()); // needed for RollDiceTest (planechase code needs improves)
newWatchers.add(new AttackedThisTurnWatcher());

View file

@ -15,6 +15,7 @@ import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.Copyable;
import mage.watchers.common.FirstStrikeWatcher;
import java.io.Serializable;
import java.util.*;
@ -60,10 +61,9 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
public boolean hasFirstOrDoubleStrike(Game game) {
return Stream.concat(attackers.stream(), blockers.stream())
.map(id -> game.getPermanent(id))
.map(game::getPermanent)
.filter(Objects::nonNull)
.anyMatch(this::hasFirstOrDoubleStrike);
.anyMatch(CombatGroup::hasFirstOrDoubleStrike);
}
/**
@ -89,27 +89,27 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
return blockerOrder;
}
private boolean hasFirstOrDoubleStrike(Permanent perm) {
return perm.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()) || perm.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId());
private static boolean hasFirstOrDoubleStrike(Permanent perm) {
return hasFirstStrike(perm) || hasDoubleStrike(perm);
}
private boolean hasFirstStrike(Permanent perm) {
private static boolean hasFirstStrike(Permanent perm) {
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());
}
private boolean hasTrample(Permanent perm) {
private static boolean hasTrample(Permanent perm) {
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());
}
private boolean hasBanding(Permanent perm) {
private static boolean hasBanding(Permanent perm) {
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)
* combat damage step
* Determines if permanent is to deal damage this step based on whether it has first/double strike
* and whether it did during the first combat damage step of this phase.
* Info is stored in FirstStrikeWatcher.
*
* @param perm Permanent to check
* @param first First strike or common combat damage step
* @return
* @param first true for first strike damage step, false for normal damage step
* @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) {
return false;
}
// if now first strike combat damage step
if (first) {
// should have first strike or double strike
return hasFirstOrDoubleStrike(perm);
} // if now not first strike combat
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);
if (hasFirstOrDoubleStrike(perm)) {
FirstStrikeWatcher.recordFirstStrikingCreature(perm.getId(), game);
return true;
}
// can damage otherwise
return true;
return false;
} else { // 702.7c
return hasDoubleStrike(perm) || !FirstStrikeWatcher.wasFirstStrikingCreature(perm.getId(), game);
}
}
private void unblockedDamage(boolean first, Game game) {
for (UUID attackerId : attackers) {
Permanent attacker = game.getPermanent(attackerId);
if (canDamage(attacker, first)) {
if (dealsDamageThisStep(attacker, first, game)) {
//20091005 - 510.1c, 702.17c
if (!blocked || hasTrample(attacker)) {
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));
if (blocker != null && attacker != null) {
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);
if (hasTrample(attacker)) {
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);
}
}
if (canDamage(blocker, first)) {
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
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));
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
Map<UUID, Integer> blockerPower = new HashMap<>();
for (UUID blockerId : blockerOrder) {
Permanent blocker = game.getPermanent(blockerId);
if (canDamage(blocker, first)) {
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
blockerPower.put(blockerId, getDamageValueFromPermanent(blocker, game));
}
@ -375,7 +370,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
} else {
for (UUID blockerId : blockerOrder) {
Permanent blocker = game.getPermanent(blockerId);
if (canDamage(blocker, first)) {
if (dealsDamageThisStep(blocker, first, game)) {
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
attacker.markDamage(getDamageValueFromPermanent(blocker, game), blocker.getId(), null, game, true, true);
}
@ -391,12 +386,12 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
return;
}
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
Map<UUID, Integer> blockerPower = new HashMap<>();
for (UUID blockerId : blockerOrder) {
Permanent blocker = game.getPermanent(blockerId);
if (canDamage(blocker, first)) {
if (dealsDamageThisStep(blocker, first, game)) {
if (checkSoleBlockerAfter(blocker, game)) { // blocking several creatures handled separately
blockerPower.put(blockerId, getDamageValueFromPermanent(blocker, game));
}
@ -440,7 +435,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
if (isAttacking) {
for (UUID blockerId : blockerOrder) {
Permanent blocker = game.getPermanent(blockerId);
if (canDamage(blocker, first)) {
if (dealsDamageThisStep(blocker, first, game)) {
if (!assignsDefendingPlayerAndOrDefendingCreaturesDividedDamage(blocker, blocker.getControllerId(), first, game, false)) {
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 attacker = game.getPermanent(attackers.get(0));
if (blocker != null && attacker != null) {
if (canDamage(blocker, first)) {
if (dealsDamageThisStep(blocker, first, game)) {
int damage = getDamageValueFromPermanent(blocker, game);
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());
int damage = getDamageValueFromPermanent(blocker, game);
if (canDamage(blocker, first)) {
if (dealsDamageThisStep(blocker, first, game)) {
Map<UUID, Integer> assigned = new HashMap<>();
for (UUID attackerId : attackerOrder) {
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
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null && attacker.getBandedCards() != null) {
if (attacker == null) {
continue;
}
if (attacker.getBandedCards() != null) {
for (UUID bandedId : attacker.getBandedCards()) {
Permanent banded = game.getPermanent(bandedId);
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));
// 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)
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)) {
defendingPlayerAndOrDefendingCreaturesDividedDamage(creature, player, first, game, isAttacking);
return true;
@ -968,7 +966,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
return false;
}
private static int getLethalDamage(Permanent blocker, Permanent attacker, Game game) {
return blocker.getLethalDamage(attacker.getId(), game);
private static int getLethalDamage(Permanent damaged, Permanent damaging, Game game) {
return damaged.getLethalDamage(damaging.getId(), game);
}
}

View file

@ -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));
}
}