mirror of
https://github.com/correl/mage.git
synced 2024-12-27 20:06:31 +00:00
Fix some iterators that try to modify themselves (ConcurrentModificationException, #10460)
* Add test to confirm functionality * Reimplement Whirlwind Denial * Fix Awaken the Sleeper
This commit is contained in:
parent
94dfbdfed4
commit
b340ab3b73
3 changed files with 107 additions and 31 deletions
|
@ -18,6 +18,8 @@ import mage.game.permanent.Permanent;
|
|||
import mage.players.Player;
|
||||
import mage.target.common.TargetCreaturePermanent;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
|
@ -73,12 +75,16 @@ class AwakenTheSleeperEffect extends OneShotEffect {
|
|||
|| !player.chooseUse(outcome, "Destroy all equipment attached to " + permanent.getName() + '?', source, game)) {
|
||||
return false;
|
||||
}
|
||||
List<Permanent> toDestroy = new LinkedList<>();
|
||||
for (UUID attachmentId : permanent.getAttachments()) {
|
||||
Permanent attachment = game.getPermanent(attachmentId);
|
||||
if (attachment != null && attachment.hasSubtype(SubType.EQUIPMENT, game)) {
|
||||
attachment.destroy(source, game);
|
||||
toDestroy.add(attachment);
|
||||
}
|
||||
}
|
||||
for (Permanent equipment : toDestroy) {
|
||||
equipment.destroy(source, game);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,14 +8,16 @@ import mage.cards.CardSetInfo;
|
|||
import mage.constants.CardType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.Game;
|
||||
import mage.game.stack.StackObject;
|
||||
import mage.players.Player;
|
||||
import mage.util.ManaUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
* @author xenohedron
|
||||
*/
|
||||
public final class WhirlwindDenial extends CardImpl {
|
||||
|
||||
|
@ -55,36 +57,46 @@ class WhirlwindDenialEffect extends OneShotEffect {
|
|||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
game.getStack()
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.forEachOrdered(stackObject -> {
|
||||
if (!game.getOpponents(source.getControllerId()).contains(stackObject.getControllerId())) {
|
||||
return;
|
||||
}
|
||||
Player player = game.getPlayer(stackObject.getControllerId());
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
Cost cost = ManaUtil.createManaCost(4, false);
|
||||
if (cost.canPay(source, source, stackObject.getControllerId(), game)
|
||||
&& player.chooseUse(outcome, "Pay {4} to prevent "
|
||||
+ stackObject.getIdName() + " from being countered?", source, game)
|
||||
&& cost.pay(source, game, source, stackObject.getControllerId(), false)) {
|
||||
game.informPlayers("The cost was paid by "
|
||||
+ player.getLogName()
|
||||
+ " to prevent "
|
||||
+ stackObject.getIdName()
|
||||
+ " from being countered.");
|
||||
return;
|
||||
}
|
||||
game.informPlayers("The cost was not paid by "
|
||||
+ player.getLogName()
|
||||
+ " to prevent "
|
||||
List<StackObject> stackObjectsToCounter = new LinkedList<>();
|
||||
Cost cost = ManaUtil.createManaCost(4, false);
|
||||
// As Whirlwind Denial resolves, first the opponent whose turn it is
|
||||
// (or, if it's your turn, the next opponent in turn order) chooses which spells and/or abilities to pay for,
|
||||
// then pays that amount. Then each other opponent in turn order does the same.
|
||||
// Then all spells and abilities that weren't paid for are countered at the same time.
|
||||
// (2020-01-24)
|
||||
for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) {
|
||||
if (playerId.equals(source.getControllerId())) {
|
||||
continue; // only opponents have to pay
|
||||
}
|
||||
Player player = game.getPlayer(playerId);
|
||||
for (StackObject stackObject : game.getStack()) {
|
||||
if (!playerId.equals(stackObject.getControllerId())) {
|
||||
continue; // opponents only choose for their own spells/abilities
|
||||
}
|
||||
if (player == null) { // shouldn't be null, but if somehow so, they can't pay, so counter it
|
||||
stackObjectsToCounter.add(stackObject);
|
||||
continue;
|
||||
}
|
||||
if (cost.canPay(source, source, playerId, game)
|
||||
&& player.chooseUse(outcome, "Pay {4} to prevent "
|
||||
+ stackObject.getIdName() + " from being countered?", source, game)
|
||||
&& cost.pay(source, game, source, stackObject.getControllerId(), false)) {
|
||||
game.informPlayers(player.getLogName()
|
||||
+ " pays the cost to prevent "
|
||||
+ stackObject.getIdName()
|
||||
+ " from being countered.");
|
||||
game.getStack().counter(stackObject.getId(), source, game);
|
||||
});
|
||||
} else {
|
||||
game.informPlayers(stackObject.getIdName()
|
||||
+ " will be countered as "
|
||||
+ player.getLogName()
|
||||
+ " does not pay the cost.");
|
||||
stackObjectsToCounter.add(stackObject); // will be countered all at the end
|
||||
}
|
||||
}
|
||||
}
|
||||
for (StackObject toCounter : stackObjectsToCounter) {
|
||||
game.getStack().counter(toCounter.getId(), source, game);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package org.mage.test.cards.single.thb;
|
||||
|
||||
import mage.constants.PhaseStep;
|
||||
import mage.constants.Zone;
|
||||
import org.junit.Test;
|
||||
import org.mage.test.serverside.base.CardTestPlayerBase;
|
||||
|
||||
/**
|
||||
* @author xenohedron
|
||||
*/
|
||||
|
||||
public class WhirlwindDenialTest extends CardTestPlayerBase {
|
||||
|
||||
private static final String denial = "Whirlwind Denial";
|
||||
private static final String guttersnipe = "Guttersnipe"; // trigger deals 2 damage on cast
|
||||
private static final String tithe = "Blood Tithe"; // deals 3 damage, gain 3 life
|
||||
|
||||
private void baseWhirlwindTest(boolean payTrigger, boolean paySpell) {
|
||||
setStrictChooseMode(true);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Wastes", 3+4+4);
|
||||
addCard(Zone.BATTLEFIELD, playerA, guttersnipe);
|
||||
addCard(Zone.HAND, playerA, tithe); // Player A tries to cast Blood Tithe for 3 damage + 2 damage
|
||||
addCard(Zone.BATTLEFIELD, playerB, "Island", 3);
|
||||
addCard(Zone.HAND, playerB, denial); // Player B denies it
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, tithe);
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, denial);
|
||||
setChoice(playerA, payTrigger);
|
||||
setChoice(playerA, paySpell);
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
execute();
|
||||
|
||||
assertLife(playerB, 20 - (payTrigger ? 2 : 0) - (paySpell ? 3 : 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhirlwindPayAllCosts() {
|
||||
baseWhirlwindTest(true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhirlwindPayTrigger() {
|
||||
baseWhirlwindTest(true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhirlwindPaySpell() {
|
||||
baseWhirlwindTest(false, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhirlwindPayNone() {
|
||||
baseWhirlwindTest(false, false);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue