Attacking you abilities and filters - fixed that planeswalker removes from a combat can cause a game error (NPE error, example: Curse of Hospitality)

This commit is contained in:
Oleg Agafonov 2023-05-09 14:59:44 +04:00
parent 1f4dfd08ce
commit 9d9916280a
9 changed files with 68 additions and 6 deletions

View file

@ -72,6 +72,7 @@ enum AmberGristleOMaulValue implements DynamicValue {
.map(game::getControllerId)
.anyMatch(sourceAbility::isControlledBy))
.map(CombatGroup::getDefenderId)
.filter(Objects::nonNull)
.distinct()
.map(game::getPlayer)
.filter(Objects::nonNull)

View file

@ -16,6 +16,7 @@ import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.target.common.TargetCreaturePermanent;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@ -66,6 +67,7 @@ enum AstralConfrontationValue implements DynamicValue {
.anyMatch(sourceAbility::isControlledBy)
)
.map(CombatGroup::getDefenderId)
.filter(Objects::nonNull)
.distinct()
.filter(opponents::contains)
.mapToInt(x -> 1)

View file

@ -17,6 +17,7 @@ import mage.game.combat.CombatGroup;
import mage.game.events.GameEvent;
import mage.target.targetpointer.FixedTarget;
import java.util.Objects;
import java.util.UUID;
/**
@ -84,6 +85,7 @@ class FiremaneCommandoTriggeredAbility extends TriggeredAbilityImpl {
.getGroups()
.stream()
.map(CombatGroup::getDefenderId)
.filter(Objects::nonNull)
.anyMatch(this.getControllerId()::equals);
this.getEffects().setValue("damage", youWereAttacked ? 0 : 1);
this.getEffects().setTargetPointer(new FixedTarget(event.getPlayerId()));

View file

@ -16,6 +16,7 @@ import mage.constants.Zone;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@ -72,6 +73,7 @@ enum NemesisPhoenixCondition implements Condition {
.map(game::getControllerId)
.anyMatch(source::isControlledBy))
.map(CombatGroup::getDefenderId)
.filter(Objects::nonNull)
.distinct()
.filter(opponents::contains)
.count() >= 2;

View file

@ -55,6 +55,7 @@ enum PackAttackValue implements DynamicValue {
.getGroups()
.stream()
.map(CombatGroup::getDefenderId)
.filter(Objects::nonNull)
.distinct()
.map(game::getPlayer)
.filter(Objects::nonNull)

View file

@ -1,4 +1,3 @@
package org.mage.test.combat;
import mage.constants.PhaseStep;
@ -9,8 +8,7 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
*
* @author LevelX2
* @author LevelX2, JayDi85
*/
public class RemoveFromCombatTest extends CardTestPlayerBase {
@ -21,7 +19,7 @@ public class RemoveFromCombatTest extends CardTestPlayerBase {
* continued attacking and dealt 3 damage to me.
*/
@Test
public void testLeavesCombatIfNoLongerACreature() {
public void test_LeavesCombatIfNoLongerACreature() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
addCard(Zone.HAND, playerA, "Lightning Blast", 1);
@ -51,7 +49,61 @@ public class RemoveFromCombatTest extends CardTestPlayerBase {
assertLife(playerA, 20);
assertLife(playerB, 20);
}
@Test
public void test_Defender_AttackPlayer() {
// Enchant player
// Creatures attacking enchanted player have trample.
addCard(Zone.HAND, playerA, "Curse of Hospitality", 1); // {2}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
//
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
// prepare
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Curse of Hospitality");
addTarget(playerA, playerB);
// attack and get trumple
attack(1, playerA, "Grizzly Bears");
block(1, playerB, "Alpha Myr", "Grizzly Bears");
setChoiceAmount(playerA, 1);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 1); // must get 1 from trumple
}
@Test
public void test_Defender_AttackPlaneswalkerAndRemoveDefender() {
// possible bug: NPE error on defender remove from battle
// Enchant player
// Creatures attacking enchanted player have trample.
addCard(Zone.HAND, playerA, "Curse of Hospitality", 1); // {2}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
//
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Adaptive Snapjaw", 1); // 6/2
addCard(Zone.BATTLEFIELD, playerB, "Jace, Memory Adept", 1); // 4
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
// prepare
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Curse of Hospitality");
addTarget(playerA, playerB);
// attack planeswalker and remove it from battlefield due damage
attack(1, playerA, "Adaptive Snapjaw", "Jace, Memory Adept");
attack(1, playerA, "Grizzly Bears", playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 2);
assertGraveyardCount(playerB, "Jace, Memory Adept", 1);
}
}

View file

@ -2557,6 +2557,7 @@ public abstract class GameImpl implements Game {
.getGroups()
.stream()
.map(CombatGroup::getDefenderId)
.filter(Objects::nonNull)
.noneMatch(perm.getId()::equals)
&& this.getPlayer(perm.getProtectorId()) == null
|| perm.isControlledBy(perm.getProtectorId())) {

View file

@ -1594,6 +1594,7 @@ public class Combat implements Serializable, Copyable<Combat> {
.stream()
.filter(group -> group.getAttackers().contains(attackerId))
.map(CombatGroup::getDefenderId)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}

View file

@ -31,7 +31,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
protected List<UUID> attackerOrder = new ArrayList<>();
protected Map<UUID, UUID> players = new HashMap<>();
protected boolean blocked;
protected UUID defenderId; // planeswalker or player
protected UUID defenderId; // planeswalker or player, can be null after remove from combat (e.g. due damage)
protected UUID defendingPlayerId;
protected boolean defenderIsPermanent;