From cab436e9e5e897544ddf84b96863d31ae1a2698a Mon Sep 17 00:00:00 2001 From: htrajan Date: Thu, 16 Apr 2020 15:53:51 -0700 Subject: [PATCH 1/8] wip --- Mage.Sets/src/mage/cards/f/FlameSpill.java | 4 +- Mage.Sets/src/mage/cards/r/RamThrough.java | 4 +- .../src/mage/cards/s/SuperDuperDeathRay.java | 4 +- .../cards/z/ZilorthaStrengthIncarnate.java | 82 +++++++++++++++++++ .../src/mage/sets/IkoriaLairOfBehemoths.java | 1 + .../iko/ZilorthaStrengthIncarnateTest.java | 23 ++++++ Mage/src/main/java/mage/game/Game.java | 8 ++ Mage/src/main/java/mage/game/GameImpl.java | 38 ++++++++- .../java/mage/game/combat/CombatGroup.java | 47 +++++++---- 9 files changed, 186 insertions(+), 25 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java diff --git a/Mage.Sets/src/mage/cards/f/FlameSpill.java b/Mage.Sets/src/mage/cards/f/FlameSpill.java index fd49a4f9923..50707d477dc 100644 --- a/Mage.Sets/src/mage/cards/f/FlameSpill.java +++ b/Mage.Sets/src/mage/cards/f/FlameSpill.java @@ -15,6 +15,8 @@ import mage.target.common.TargetCreaturePermanent; import java.util.UUID; +import static mage.game.combat.CombatGroup.getLethalDamage; + /** * @author TheElk801 */ @@ -62,7 +64,7 @@ class FlameSpillEffect extends OneShotEffect { if (permanent == null || sourceObject == null) { return false; } - int lethal = Math.max(permanent.getToughness().getValue() - permanent.getDamage(), 0); + int lethal = getLethalDamage(permanent, game); if (sourceObject.getAbilities().containsKey(DeathtouchAbility.getInstance().getId())) { lethal = Math.min(lethal, 1); } diff --git a/Mage.Sets/src/mage/cards/r/RamThrough.java b/Mage.Sets/src/mage/cards/r/RamThrough.java index 0c1fa86c73e..5d808ee476a 100644 --- a/Mage.Sets/src/mage/cards/r/RamThrough.java +++ b/Mage.Sets/src/mage/cards/r/RamThrough.java @@ -18,6 +18,8 @@ import mage.target.common.TargetCreaturePermanent; import java.util.UUID; +import static mage.game.combat.CombatGroup.getLethalDamage; + /** * @author TheElk801 */ @@ -85,7 +87,7 @@ class RamThroughEffect extends OneShotEffect { if (!myPermanent.getAbilities().containsKey(TrampleAbility.getInstance().getId())) { return anotherPermanent.damage(power, myPermanent.getId(), game, false, true) > 0; } - int lethal = Math.max(anotherPermanent.getToughness().getValue() - anotherPermanent.getDamage(), 0); + int lethal = getLethalDamage(anotherPermanent, game); if (myPermanent.getAbilities().containsKey(DeathtouchAbility.getInstance().getId())) { lethal = Math.min(lethal, 1); } diff --git a/Mage.Sets/src/mage/cards/s/SuperDuperDeathRay.java b/Mage.Sets/src/mage/cards/s/SuperDuperDeathRay.java index 7badbea28b1..80994ec3752 100644 --- a/Mage.Sets/src/mage/cards/s/SuperDuperDeathRay.java +++ b/Mage.Sets/src/mage/cards/s/SuperDuperDeathRay.java @@ -17,6 +17,8 @@ import mage.target.common.TargetCreaturePermanent; import java.util.UUID; +import static mage.game.combat.CombatGroup.getLethalDamage; + /** * @author TheElk801 */ @@ -68,7 +70,7 @@ class SuperDuperDeathRayEffect extends OneShotEffect { if (permanent == null || sourceObject == null) { return false; } - int lethal = Math.max(permanent.getToughness().getValue() - permanent.getDamage(), 0); + int lethal = getLethalDamage(permanent, game); if (sourceObject.getAbilities().containsKey(DeathtouchAbility.getInstance().getId())) { lethal = Math.min(lethal, 1); } diff --git a/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java b/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java new file mode 100644 index 00000000000..09ead1ed2e1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java @@ -0,0 +1,82 @@ +package mage.cards.z; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; + +import java.util.UUID; + +/** + * @author htrajan + */ +public final class ZilorthaStrengthIncarnate extends CardImpl { + + public ZilorthaStrengthIncarnate(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}{G}"); + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.DINOSAUR); + this.power = new MageInt(7); + this.toughness = new MageInt(3); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new ZilorthaStrengthIncarnateEffect())); + } + + private ZilorthaStrengthIncarnate(ZilorthaStrengthIncarnate card) { + super(card); + } + + @Override + public Card copy() { + return new ZilorthaStrengthIncarnate(this); + } +} + +class ZilorthaStrengthIncarnateEffect extends ContinuousEffectImpl { + + ZilorthaStrengthIncarnateEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "Lethal damage dealt to creatures you control is determined by their power rather than their toughness"; + } + + private ZilorthaStrengthIncarnateEffect(ZilorthaStrengthIncarnateEffect effect) { + super(effect); + } + + @Override + public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { + // Change the rule + FilterCreaturePermanent filter = StaticFilters.FILTER_PERMANENT_CREATURE.copy(); + filter.add(new ControllerIdPredicate(source.getControllerId())); + game.addUsePowerInsteadOfToughnessForDamageLethalityFilter(source.getSourceId(), filter); + return true; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean hasLayer(Layer layer) { + return layer == Layer.RulesEffects; + } + + @Override + public ContinuousEffect copy() { + return new ZilorthaStrengthIncarnateEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/IkoriaLairOfBehemoths.java b/Mage.Sets/src/mage/sets/IkoriaLairOfBehemoths.java index 4a1e426d634..00fc24c96e9 100644 --- a/Mage.Sets/src/mage/sets/IkoriaLairOfBehemoths.java +++ b/Mage.Sets/src/mage/sets/IkoriaLairOfBehemoths.java @@ -330,6 +330,7 @@ public final class IkoriaLairOfBehemoths extends ExpansionSet { cards.add(new SetCardInfo("Zagoth Mamba", 106, Rarity.UNCOMMON, mage.cards.z.ZagothMamba.class)); cards.add(new SetCardInfo("Zagoth Triome", 259, Rarity.RARE, mage.cards.z.ZagothTriome.class)); cards.add(new SetCardInfo("Zenith Flare", 217, Rarity.UNCOMMON, mage.cards.z.ZenithFlare.class)); + cards.add(new SetCardInfo("Zilortha, Strength Incarnate", 275, Rarity.MYTHIC, mage.cards.z.ZilorthaStrengthIncarnate.class)); cards.add(new SetCardInfo("Zirda, the Dawnwaker", 233, Rarity.RARE, mage.cards.z.ZirdaTheDawnwaker.class)); cards.removeIf(setCardInfo -> mutateNames.contains(setCardInfo.getName())); // remove when mutate is implemented diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java new file mode 100644 index 00000000000..a63703491f6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java @@ -0,0 +1,23 @@ +package org.mage.test.cards.single.iko; + +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { + + @Test + public void testNotPresent_damageResolvesLethalityAsNormal() { + addCard(Zone.BATTLEFIELD, playerA, "Savai Sabertooth", 1); + addCard(Zone.BATTLEFIELD, playerB, "Drannith Healer", 1); + + attack(2, playerA, "Savai Sabertooth"); + block(2, playerB, "Drannith Healer", "Savai Sabertooth"); + + execute(); + + assertGraveyardCount(playerA, "Savai Sabertooth", 1); + assertGraveyardCount(playerB, "Drannith Healer", 1); + } + +} diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 3a3cdd1b81a..d2d20ed707a 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -17,6 +17,9 @@ import mage.cards.decks.Deck; import mage.choices.Choice; import mage.constants.*; import mage.counters.Counters; +import mage.filter.Filter; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicate; import mage.game.combat.Combat; import mage.game.command.Commander; import mage.game.command.Emblem; @@ -485,4 +488,9 @@ public interface Game extends MageItem, Serializable { default Set getCommandersIds(Player player) { return getCommandersIds(player, CommanderCardType.ANY); } + + void addUsePowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter); + + List getActiveUsePowerInsteadOfToughnessForDamageLethalityFilters(); + } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index cd5fb55985e..454aefb61b0 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1,6 +1,7 @@ package mage.game; import mage.MageException; +import mage.MageInt; import mage.MageObject; import mage.abilities.*; import mage.abilities.common.AttachableToRestrictedAbility; @@ -31,6 +32,7 @@ import mage.filter.FilterCard; import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.filter.common.FilterControlledPermanent; +import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.NamePredicate; import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.game.combat.Combat; @@ -69,6 +71,9 @@ import java.io.IOException; import java.io.Serializable; import java.util.*; import java.util.Map.Entry; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; public abstract class GameImpl implements Game, Serializable { @@ -144,6 +149,8 @@ public abstract class GameImpl implements Game, Serializable { // temporary store for income concede commands, don't copy private final LinkedList concedingPlayers = new LinkedList<>(); + private Map usePowerInsteadOfToughnessForDamageLethalityFilters = new HashMap<>(); + public GameImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife) { this.id = UUID.randomUUID(); this.range = range; @@ -1888,6 +1895,7 @@ public abstract class GameImpl implements Game, Serializable { List legendary = new ArrayList<>(); List worldEnchantment = new ArrayList<>(); + List usePowerInsteadOfToughnessForDamageLethalityFilters = getActiveUsePowerInsteadOfToughnessForDamageLethalityFilters(); for (Permanent perm : getBattlefield().getAllActivePermanents()) { if (perm.isCreature()) { //20091005 - 704.5f @@ -1897,10 +1905,19 @@ public abstract class GameImpl implements Game, Serializable { continue; } } //20091005 - 704.5g/704.5h - else if (perm.getToughness().getValue() <= perm.getDamage() || perm.isDeathtouched()) { - if (perm.destroy(null, this, false)) { - somethingHappened = true; - continue; + else { + /* + * for handling Zilortha, Strength Incarnate: + * Any time the game is checking whether damage is lethal or if a creature should be destroyed for having lethal damage marked on it, use the power of your creatures rather than their toughness to check the damage against. This includes being assigned trample damage, damage from Flame Spill, and so on. + */ + boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream() + .anyMatch(filter -> filter.match(perm, this)); + MageInt lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ? perm.getPower() : perm.getToughness(); + if (lethalDamageThreshold.getValue() <= perm.getDamage() || perm.isDeathtouched()) { + if (perm.destroy(null, this, false)) { + somethingHappened = true; + continue; + } } } if (perm.getPairedCard() != null) { @@ -3300,4 +3317,17 @@ public abstract class GameImpl implements Game, Serializable { return player.getCommandersIds(); } + @Override + public void addUsePowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter) { + usePowerInsteadOfToughnessForDamageLethalityFilters.putIfAbsent(source, filter); + } + + @Override + public List getActiveUsePowerInsteadOfToughnessForDamageLethalityFilters() { + return usePowerInsteadOfToughnessForDamageLethalityFilters.isEmpty() ? emptyList() : getBattlefield().getAllActivePermanents().stream() + .map(Card::getId) + .filter(usePowerInsteadOfToughnessForDamageLethalityFilters::containsKey) + .map(usePowerInsteadOfToughnessForDamageLethalityFilters::get) + .collect(Collectors.toList()); + } } diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index be2f139af66..378e4bcc249 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -5,6 +5,7 @@ import java.io.Serializable; import java.util.*; import java.util.stream.Stream; +import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.ControllerAssignCombatDamageToBlockersAbility; import mage.abilities.common.ControllerDivideCombatDamageAbility; @@ -19,12 +20,15 @@ import mage.abilities.keyword.TrampleAbility; import mage.constants.AsThoughEffectType; import mage.constants.Outcome; import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.Copyable; +import static java.util.Collections.emptyList; + /** * * @author BetaSteward_at_googlemail.com @@ -271,12 +275,7 @@ public class CombatGroup implements Serializable, Copyable { if (blocked && canDamage(attacker, first)) { int damage = getDamageValueFromPermanent(attacker, game); if (hasTrample(attacker)) { - int lethalDamage; - if (attacker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId())) { - lethalDamage = 1; - } else { - lethalDamage = Math.max(blocker.getToughness().getValue() - blocker.getDamage(), 0); - } + int lethalDamage = getLethalDamage(blocker, attacker, game); if (lethalDamage >= damage) { blocker.markDamage(damage, attacker.getId(), game, true, true); } else { @@ -325,12 +324,7 @@ public class CombatGroup implements Serializable, Copyable { for (UUID blockerId : new ArrayList<>(blockerOrder)) { // prevent ConcurrentModificationException Permanent blocker = game.getPermanent(blockerId); if (blocker != null) { - int lethalDamage; - if (attacker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId())) { - lethalDamage = 1; - } else { - lethalDamage = Math.max(blocker.getToughness().getValue() - blocker.getDamage(), 0); - } + int lethalDamage = getLethalDamage(blocker, attacker, game); if (lethalDamage >= damage) { if (!oldRuleDamage) { assigned.put(blockerId, damage); @@ -521,12 +515,7 @@ public class CombatGroup implements Serializable, Copyable { for (UUID attackerId : attackerOrder) { Permanent attacker = game.getPermanent(attackerId); if (attacker != null) { - int lethalDamage; - if (blocker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId())) { - lethalDamage = 1; - } else { - lethalDamage = Math.max(attacker.getToughness().getValue() - attacker.getDamage(), 0); - } + int lethalDamage = getLethalDamage(attacker, blocker, game); if (lethalDamage >= damage) { if (!oldRuleDamage) { assigned.put(attackerId, damage); @@ -936,4 +925,26 @@ public class CombatGroup implements Serializable, Copyable { } return false; } + + private static int getLethalDamage(Permanent blocker, Permanent attacker, Game game) { + int lethalDamage; + if (attacker.getAbilities().containsKey(DeathtouchAbility.getInstance().getId())) { + lethalDamage = 1; + } else { + lethalDamage = getLethalDamage(blocker, game); + } + return lethalDamage; + } + + public static int getLethalDamage(Permanent damagedPermanent, Game game) { + List usePowerInsteadOfToughnessForDamageLethalityFilters = game.getActiveUsePowerInsteadOfToughnessForDamageLethalityFilters(); + /* + * for handling Zilortha, Strength Incarnate: + * Any time the game is checking whether damage is lethal or if a creature should be destroyed for having lethal damage marked on it, use the power of your creatures rather than their toughness to check the damage against. This includes being assigned trample damage, damage from Flame Spill, and so on. + */ + boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream() + .anyMatch(filter -> filter.match(damagedPermanent, game)); + MageInt lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality? damagedPermanent.getPower() : damagedPermanent.getToughness(); + return Math.max(lethalDamageThreshold.getValue() - damagedPermanent.getDamage(), 0); + } } From 92d63263161f41fb158bff3f18cb401f1b20cb38 Mon Sep 17 00:00:00 2001 From: htrajan Date: Thu, 16 Apr 2020 17:50:17 -0700 Subject: [PATCH 2/8] finish --- .../cards/z/ZilorthaStrengthIncarnate.java | 2 +- .../iko/ZilorthaStrengthIncarnateTest.java | 146 +++++++++++++++++- Mage/src/main/java/mage/game/Game.java | 4 +- Mage/src/main/java/mage/game/GameImpl.java | 15 +- .../java/mage/game/combat/CombatGroup.java | 26 ++-- 5 files changed, 161 insertions(+), 32 deletions(-) diff --git a/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java b/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java index 09ead1ed2e1..4c5578c65b7 100644 --- a/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java +++ b/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java @@ -61,7 +61,7 @@ class ZilorthaStrengthIncarnateEffect extends ContinuousEffectImpl { // Change the rule FilterCreaturePermanent filter = StaticFilters.FILTER_PERMANENT_CREATURE.copy(); filter.add(new ControllerIdPredicate(source.getControllerId())); - game.addUsePowerInsteadOfToughnessForDamageLethalityFilter(source.getSourceId(), filter); + game.addPowerInsteadOfToughnessForDamageLethalityFilter(source.getSourceId(), filter); return true; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java index a63703491f6..6e207ba17f4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java @@ -1,18 +1,25 @@ package org.mage.test.cards.single.iko; +import mage.constants.PhaseStep; import mage.constants.Zone; +import org.junit.After; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { - @Test - public void testNotPresent_damageResolvesLethalityAsNormal() { - addCard(Zone.BATTLEFIELD, playerA, "Savai Sabertooth", 1); - addCard(Zone.BATTLEFIELD, playerB, "Drannith Healer", 1); + @After + public void after() { + assertAllCommandsUsed(); + } - attack(2, playerA, "Savai Sabertooth"); - block(2, playerB, "Drannith Healer", "Savai Sabertooth"); + @Test + public void testNotPresent_combatDamageResolvesLethalityAsNormal() { + addCard(Zone.BATTLEFIELD, playerA, "Savai Sabertooth"); + addCard(Zone.BATTLEFIELD, playerB, "Drannith Healer"); + + attack(1, playerA, "Savai Sabertooth"); + block(1, playerB, "Drannith Healer", "Savai Sabertooth"); execute(); @@ -20,4 +27,131 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { assertGraveyardCount(playerB, "Drannith Healer", 1); } + @Test + public void testPresent_combatDamageResolvesLethalityUsingPower() { + addCard(Zone.BATTLEFIELD, playerA, "Zilortha, Strength Incarnate"); + addCard(Zone.BATTLEFIELD, playerA, "Savai Sabertooth"); + addCard(Zone.BATTLEFIELD, playerB, "Drannith Healer"); + + attack(1, playerA, "Savai Sabertooth"); + block(1, playerB, "Drannith Healer", "Savai Sabertooth"); + + execute(); + + assertGraveyardCount(playerA, "Savai Sabertooth", 0); + assertGraveyardCount(playerB, "Drannith Healer", 1); + } + + /* + * 2020-04-17 + * A creature with 0 power isn’t destroyed unless it has at least 1 damage marked on it. + */ + @Test + public void testPresent_oneDamageRequiredToDestroyZeroPowerCreature() { + addCard(Zone.BATTLEFIELD, playerA, "Zilortha, Strength Incarnate"); + addCard(Zone.BATTLEFIELD, playerA, "Aegis Turtle"); + addCard(Zone.BATTLEFIELD, playerB, "Aegis Turtle"); + + attack(1, playerA, "Aegis Turtle"); + block(1, playerB, "Aegis Turtle", "Aegis Turtle"); + + execute(); + + assertGraveyardCount(playerA, "Aegis Turtle", 0); + assertGraveyardCount(playerB, "Aegis Turtle", 0); + } + + @Test + public void testNotPresent_flameSpillResolvesAsNormal() { + addCard(Zone.BATTLEFIELD, playerA, "Savai Sabertooth"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3); + addCard(Zone.HAND, playerB, "Flame Spill"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Spill", "Savai Sabertooth"); + + execute(); + + assertGraveyardCount(playerA, "Savai Sabertooth", 1); + assertGraveyardCount(playerB, "Flame Spill", 1); + + assertLife(playerA, 17); + } + + @Test + public void testPresent_flameSpillResolvesUsingPower() { + addCard(Zone.BATTLEFIELD, playerA, "Zilortha, Strength Incarnate"); + addCard(Zone.BATTLEFIELD, playerA, "Savai Sabertooth"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 3); + addCard(Zone.HAND, playerB, "Flame Spill"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Spill", "Savai Sabertooth"); + + execute(); + + assertGraveyardCount(playerA, "Savai Sabertooth", 1); + assertGraveyardCount(playerB, "Flame Spill", 1); + + assertLife(playerA, 19); + } + + /* + * 2020-04-17 + * Because damage remains marked on a creature until the damage is removed as the turn ends, nonlethal damage dealt to a creature you control may become lethal if Zilortha enters or leaves the battlefield during that turn. + */ + @Test + public void testPresent_leavesBattlefield_damageResolvesLethalityUsingPower_thenCheckedAgainstToughness() { + addCard(Zone.BATTLEFIELD, playerA, "Zilortha, Strength Incarnate"); + addCard(Zone.BATTLEFIELD, playerA, "Savai Sabertooth"); + addCard(Zone.BATTLEFIELD, playerB, "Drannith Healer"); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); + addCard(Zone.HAND, playerB, "Murder"); + + attack(1, playerA, "Savai Sabertooth"); + block(1, playerB, "Drannith Healer", "Savai Sabertooth"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Murder", "Zilortha, Strength Incarnate"); + + execute(); + + assertGraveyardCount(playerA, "Zilortha, Strength Incarnate", 1); + assertGraveyardCount(playerA, "Savai Sabertooth", 1); + assertGraveyardCount(playerB, "Drannith Healer", 1); + assertGraveyardCount(playerB, "Murder", 1); + } + + @Test + public void testPresent_ownedByBothPlayers() { + addCard(Zone.BATTLEFIELD, playerA, "Zilortha, Strength Incarnate"); + addCard(Zone.BATTLEFIELD, playerA, "Maned Serval"); + addCard(Zone.BATTLEFIELD, playerB, "Zilortha, Strength Incarnate"); + addCard(Zone.BATTLEFIELD, playerB, "Maned Serval"); + + attack(1, playerA, "Maned Serval"); + block(1, playerB, "Maned Serval", "Maned Serval"); + + execute(); + + assertGraveyardCount(playerA, "Maned Serval", 1); + assertGraveyardCount(playerB, "Maned Serval", 1); + } + + @Test + public void testAbsent_entersBattlefield_damageResolvesLethalityUsingToughness_thenCheckedAgainstPower() { + addCard(Zone.HAND, playerA, "Zilortha, Strength Incarnate"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + addCard(Zone.BATTLEFIELD, playerA, "Maned Serval"); + addCard(Zone.BATTLEFIELD, playerB, "Maned Serval"); + + attack(1, playerA, "Maned Serval"); + block(1, playerB, "Maned Serval", "Maned Serval"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Zilortha, Strength Incarnate"); + + execute(); + + assertGraveyardCount(playerA, "Maned Serval", 1); + assertGraveyardCount(playerB, "Maned Serval", 0); + } } diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index c13da0ceb26..f674c5c3bcf 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -489,8 +489,8 @@ public interface Game extends MageItem, Serializable { return getCommandersIds(player, CommanderCardType.ANY); } - void addUsePowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter); + void addPowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter); - List getActiveUsePowerInsteadOfToughnessForDamageLethalityFilters(); + List getActivePowerInsteadOfToughnessForDamageLethalityFilters(); } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 212d9f1d6f2..efa40d6f72f 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1,7 +1,6 @@ package mage.game; import mage.MageException; -import mage.MageInt; import mage.MageObject; import mage.abilities.*; import mage.abilities.common.AttachableToRestrictedAbility; @@ -1898,7 +1897,7 @@ public abstract class GameImpl implements Game, Serializable { List legendary = new ArrayList<>(); List worldEnchantment = new ArrayList<>(); - List usePowerInsteadOfToughnessForDamageLethalityFilters = getActiveUsePowerInsteadOfToughnessForDamageLethalityFilters(); + List usePowerInsteadOfToughnessForDamageLethalityFilters = getActivePowerInsteadOfToughnessForDamageLethalityFilters(); for (Permanent perm : getBattlefield().getAllActivePermanents()) { if (perm.isCreature()) { //20091005 - 704.5f @@ -1911,12 +1910,14 @@ public abstract class GameImpl implements Game, Serializable { else { /* * for handling Zilortha, Strength Incarnate: - * Any time the game is checking whether damage is lethal or if a creature should be destroyed for having lethal damage marked on it, use the power of your creatures rather than their toughness to check the damage against. This includes being assigned trample damage, damage from Flame Spill, and so on. + * 2020-04-17: Any time the game is checking whether damage is lethal or if a creature should be destroyed for having lethal damage marked on it, use the power of your creatures rather than their toughness to check the damage against. This includes being assigned trample damage, damage from Flame Spill, and so on. */ boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream() .anyMatch(filter -> filter.match(perm, this)); - MageInt lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ? perm.getPower() : perm.getToughness(); - if (lethalDamageThreshold.getValue() <= perm.getDamage() || perm.isDeathtouched()) { + int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ? + // Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isn’t destroyed unless it has at least 1 damage marked on it. + Math.max(perm.getPower().getValue(), 1) : perm.getToughness().getValue(); + if (lethalDamageThreshold <= perm.getDamage() || perm.isDeathtouched()) { if (perm.destroy(null, this, false)) { somethingHappened = true; continue; @@ -3321,12 +3322,12 @@ public abstract class GameImpl implements Game, Serializable { } @Override - public void addUsePowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter) { + public void addPowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter) { usePowerInsteadOfToughnessForDamageLethalityFilters.putIfAbsent(source, filter); } @Override - public List getActiveUsePowerInsteadOfToughnessForDamageLethalityFilters() { + public List getActivePowerInsteadOfToughnessForDamageLethalityFilters() { return usePowerInsteadOfToughnessForDamageLethalityFilters.isEmpty() ? emptyList() : getBattlefield().getAllActivePermanents().stream() .map(Card::getId) .filter(usePowerInsteadOfToughnessForDamageLethalityFilters::containsKey) diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index 378e4bcc249..a190177e20f 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -1,22 +1,11 @@ package mage.game.combat; -import java.io.Serializable; -import java.util.*; -import java.util.stream.Stream; - -import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.ControllerAssignCombatDamageToBlockersAbility; import mage.abilities.common.ControllerDivideCombatDamageAbility; import mage.abilities.common.DamageAsThoughNotBlockedAbility; -import mage.abilities.keyword.BandingAbility; -import mage.abilities.keyword.BandsWithOtherAbility; -import mage.abilities.keyword.CantBlockAloneAbility; -import mage.abilities.keyword.DeathtouchAbility; -import mage.abilities.keyword.DoubleStrikeAbility; -import mage.abilities.keyword.FirstStrikeAbility; -import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.*; import mage.constants.AsThoughEffectType; import mage.constants.Outcome; import mage.filter.StaticFilters; @@ -27,7 +16,9 @@ import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.Copyable; -import static java.util.Collections.emptyList; +import java.io.Serializable; +import java.util.*; +import java.util.stream.Stream; /** * @@ -937,14 +928,17 @@ public class CombatGroup implements Serializable, Copyable { } public static int getLethalDamage(Permanent damagedPermanent, Game game) { - List usePowerInsteadOfToughnessForDamageLethalityFilters = game.getActiveUsePowerInsteadOfToughnessForDamageLethalityFilters(); + List usePowerInsteadOfToughnessForDamageLethalityFilters = game.getActivePowerInsteadOfToughnessForDamageLethalityFilters(); /* * for handling Zilortha, Strength Incarnate: + * 2020-04-17 * Any time the game is checking whether damage is lethal or if a creature should be destroyed for having lethal damage marked on it, use the power of your creatures rather than their toughness to check the damage against. This includes being assigned trample damage, damage from Flame Spill, and so on. */ boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream() .anyMatch(filter -> filter.match(damagedPermanent, game)); - MageInt lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality? damagedPermanent.getPower() : damagedPermanent.getToughness(); - return Math.max(lethalDamageThreshold.getValue() - damagedPermanent.getDamage(), 0); + int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ? + // Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isn’t destroyed unless it has at least 1 damage marked on it. + Math.max(damagedPermanent.getPower().getValue(), 1) : damagedPermanent.getToughness().getValue(); + return Math.max(lethalDamageThreshold - damagedPermanent.getDamage(), 0); } } From ddb14d8d5611ceed45253fbaf2d2265585189e93 Mon Sep 17 00:00:00 2001 From: htrajan Date: Thu, 16 Apr 2020 17:55:24 -0700 Subject: [PATCH 3/8] unused imports --- Mage/src/main/java/mage/game/Game.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index f674c5c3bcf..fc96422d554 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -17,9 +17,7 @@ import mage.cards.decks.Deck; import mage.choices.Choice; import mage.constants.*; import mage.counters.Counters; -import mage.filter.Filter; import mage.filter.common.FilterCreaturePermanent; -import mage.filter.predicate.Predicate; import mage.game.combat.Combat; import mage.game.command.Commander; import mage.game.command.Emblem; From 5eeffc1fcfcbf1021e568ed44c0c1ca00bd86a88 Mon Sep 17 00:00:00 2001 From: htrajan Date: Thu, 16 Apr 2020 17:59:17 -0700 Subject: [PATCH 4/8] unstar import --- Mage/src/main/java/mage/game/combat/CombatGroup.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index a190177e20f..85c83acd1b6 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -5,7 +5,13 @@ import mage.abilities.Ability; import mage.abilities.common.ControllerAssignCombatDamageToBlockersAbility; import mage.abilities.common.ControllerDivideCombatDamageAbility; import mage.abilities.common.DamageAsThoughNotBlockedAbility; -import mage.abilities.keyword.*; +import mage.abilities.keyword.BandingAbility; +import mage.abilities.keyword.BandsWithOtherAbility; +import mage.abilities.keyword.CantBlockAloneAbility; +import mage.abilities.keyword.DeathtouchAbility; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.abilities.keyword.TrampleAbility; import mage.constants.AsThoughEffectType; import mage.constants.Outcome; import mage.filter.StaticFilters; From 7c4d86f583ab28446f62c11ee6375fc59009d927 Mon Sep 17 00:00:00 2001 From: htrajan Date: Thu, 16 Apr 2020 18:11:56 -0700 Subject: [PATCH 5/8] add author tag --- .../test/cards/single/iko/ZilorthaStrengthIncarnateTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java index 6e207ba17f4..58b02a0407f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java @@ -6,6 +6,9 @@ import org.junit.After; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; +/** + * @author htrajan + */ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { @After From 1dc305f3294311bbbf49d51d29771e431282c593 Mon Sep 17 00:00:00 2001 From: htrajan Date: Thu, 16 Apr 2020 19:21:54 -0700 Subject: [PATCH 6/8] -setStrictChooseMode and assertAllCommandsUsed in every test -store filter info in GameState -put instead of putIfAbsent --- .../cards/z/ZilorthaStrengthIncarnate.java | 2 +- .../iko/ZilorthaStrengthIncarnateTest.java | 29 +++++++++++++++---- Mage/src/main/java/mage/game/Game.java | 5 ---- Mage/src/main/java/mage/game/GameImpl.java | 21 +------------- Mage/src/main/java/mage/game/GameState.java | 15 ++++++++++ .../java/mage/game/combat/CombatGroup.java | 2 +- 6 files changed, 42 insertions(+), 32 deletions(-) diff --git a/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java b/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java index 4c5578c65b7..6fc791ed351 100644 --- a/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java +++ b/Mage.Sets/src/mage/cards/z/ZilorthaStrengthIncarnate.java @@ -61,7 +61,7 @@ class ZilorthaStrengthIncarnateEffect extends ContinuousEffectImpl { // Change the rule FilterCreaturePermanent filter = StaticFilters.FILTER_PERMANENT_CREATURE.copy(); filter.add(new ControllerIdPredicate(source.getControllerId())); - game.addPowerInsteadOfToughnessForDamageLethalityFilter(source.getSourceId(), filter); + game.getState().addPowerInsteadOfToughnessForDamageLethalityFilter(source.getSourceId(), filter); return true; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java index 58b02a0407f..21818bf65c1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/iko/ZilorthaStrengthIncarnateTest.java @@ -11,11 +11,6 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { - @After - public void after() { - assertAllCommandsUsed(); - } - @Test public void testNotPresent_combatDamageResolvesLethalityAsNormal() { addCard(Zone.BATTLEFIELD, playerA, "Savai Sabertooth"); @@ -24,7 +19,10 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { attack(1, playerA, "Savai Sabertooth"); block(1, playerB, "Drannith Healer", "Savai Sabertooth"); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Savai Sabertooth", 1); assertGraveyardCount(playerB, "Drannith Healer", 1); @@ -39,7 +37,10 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { attack(1, playerA, "Savai Sabertooth"); block(1, playerB, "Drannith Healer", "Savai Sabertooth"); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Savai Sabertooth", 0); assertGraveyardCount(playerB, "Drannith Healer", 1); @@ -58,7 +59,10 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { attack(1, playerA, "Aegis Turtle"); block(1, playerB, "Aegis Turtle", "Aegis Turtle"); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Aegis Turtle", 0); assertGraveyardCount(playerB, "Aegis Turtle", 0); @@ -72,7 +76,10 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Spill", "Savai Sabertooth"); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Savai Sabertooth", 1); assertGraveyardCount(playerB, "Flame Spill", 1); @@ -89,7 +96,10 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Flame Spill", "Savai Sabertooth"); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Savai Sabertooth", 1); assertGraveyardCount(playerB, "Flame Spill", 1); @@ -114,7 +124,10 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Murder", "Zilortha, Strength Incarnate"); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Zilortha, Strength Incarnate", 1); assertGraveyardCount(playerA, "Savai Sabertooth", 1); @@ -132,7 +145,10 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { attack(1, playerA, "Maned Serval"); block(1, playerB, "Maned Serval", "Maned Serval"); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Maned Serval", 1); assertGraveyardCount(playerB, "Maned Serval", 1); @@ -152,7 +168,10 @@ public class ZilorthaStrengthIncarnateTest extends CardTestPlayerBase { castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Zilortha, Strength Incarnate"); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Maned Serval", 1); assertGraveyardCount(playerB, "Maned Serval", 0); diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index fc96422d554..b41080c668b 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -17,7 +17,6 @@ import mage.cards.decks.Deck; import mage.choices.Choice; import mage.constants.*; import mage.counters.Counters; -import mage.filter.common.FilterCreaturePermanent; import mage.game.combat.Combat; import mage.game.command.Commander; import mage.game.command.Emblem; @@ -487,8 +486,4 @@ public interface Game extends MageItem, Serializable { return getCommandersIds(player, CommanderCardType.ANY); } - void addPowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter); - - List getActivePowerInsteadOfToughnessForDamageLethalityFilters(); - } diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index efa40d6f72f..e367d7f7630 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -70,9 +70,6 @@ import java.io.IOException; import java.io.Serializable; import java.util.*; import java.util.Map.Entry; -import java.util.stream.Collectors; - -import static java.util.Collections.emptyList; public abstract class GameImpl implements Game, Serializable { @@ -149,8 +146,6 @@ public abstract class GameImpl implements Game, Serializable { // temporary store for income concede commands, don't copy private final LinkedList concedingPlayers = new LinkedList<>(); - private Map usePowerInsteadOfToughnessForDamageLethalityFilters = new HashMap<>(); - public GameImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, Mulligan mulligan, int startLife, int startingSize) { this.id = UUID.randomUUID(); this.range = range; @@ -1897,7 +1892,7 @@ public abstract class GameImpl implements Game, Serializable { List legendary = new ArrayList<>(); List worldEnchantment = new ArrayList<>(); - List usePowerInsteadOfToughnessForDamageLethalityFilters = getActivePowerInsteadOfToughnessForDamageLethalityFilters(); + List usePowerInsteadOfToughnessForDamageLethalityFilters = getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters(); for (Permanent perm : getBattlefield().getAllActivePermanents()) { if (perm.isCreature()) { //20091005 - 704.5f @@ -3320,18 +3315,4 @@ public abstract class GameImpl implements Game, Serializable { public Set getCommandersIds(Player player, CommanderCardType commanderCardType) { return player.getCommandersIds(); } - - @Override - public void addPowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter) { - usePowerInsteadOfToughnessForDamageLethalityFilters.putIfAbsent(source, filter); - } - - @Override - public List getActivePowerInsteadOfToughnessForDamageLethalityFilters() { - return usePowerInsteadOfToughnessForDamageLethalityFilters.isEmpty() ? emptyList() : getBattlefield().getAllActivePermanents().stream() - .map(Card::getId) - .filter(usePowerInsteadOfToughnessForDamageLethalityFilters::containsKey) - .map(usePowerInsteadOfToughnessForDamageLethalityFilters::get) - .collect(Collectors.toList()); - } } diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index f9f344c6662..604fe36b7eb 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -10,6 +10,7 @@ import mage.cards.Card; import mage.cards.SplitCard; import mage.constants.Zone; import mage.designations.Designation; +import mage.filter.common.FilterCreaturePermanent; import mage.game.combat.Combat; import mage.game.combat.CombatGroup; import mage.game.command.Command; @@ -38,6 +39,8 @@ import java.io.Serializable; import java.util.*; import java.util.stream.Collectors; +import static java.util.Collections.emptyList; + /** * @author BetaSteward_at_googlemail.com *

@@ -96,6 +99,7 @@ public class GameState implements Serializable, Copyable { private Map zoneChangeCounter = new HashMap<>(); private Map copiedCards = new HashMap<>(); private int permanentOrderNumber; + private Map usePowerInsteadOfToughnessForDamageLethalityFilters = new HashMap<>(); private int applyEffectsCounter; // Upcounting number of each applyEffects execution @@ -1209,4 +1213,15 @@ public class GameState implements Serializable, Copyable { return applyEffectsCounter; } + public void addPowerInsteadOfToughnessForDamageLethalityFilter(UUID source, FilterCreaturePermanent filter) { + usePowerInsteadOfToughnessForDamageLethalityFilters.put(source, filter); + } + + public List getActivePowerInsteadOfToughnessForDamageLethalityFilters() { + return usePowerInsteadOfToughnessForDamageLethalityFilters.isEmpty() ? emptyList() : getBattlefield().getAllActivePermanents().stream() + .map(Card::getId) + .filter(usePowerInsteadOfToughnessForDamageLethalityFilters::containsKey) + .map(usePowerInsteadOfToughnessForDamageLethalityFilters::get) + .collect(Collectors.toList()); + } } diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index 85c83acd1b6..d3d5926822d 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -934,7 +934,7 @@ public class CombatGroup implements Serializable, Copyable { } public static int getLethalDamage(Permanent damagedPermanent, Game game) { - List usePowerInsteadOfToughnessForDamageLethalityFilters = game.getActivePowerInsteadOfToughnessForDamageLethalityFilters(); + List usePowerInsteadOfToughnessForDamageLethalityFilters = game.getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters(); /* * for handling Zilortha, Strength Incarnate: * 2020-04-17 From 5a1b40a42f559d7407bb87e7d02ad598a2f4a441 Mon Sep 17 00:00:00 2001 From: htrajan Date: Thu, 16 Apr 2020 19:54:07 -0700 Subject: [PATCH 7/8] add filter copy to GameState's copy constructor and restore method --- Mage/src/main/java/mage/game/GameState.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 604fe36b7eb..1530fc06456 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -179,6 +179,8 @@ public class GameState implements Serializable, Copyable { this.copiedCards.putAll(state.copiedCards); this.permanentOrderNumber = state.permanentOrderNumber; this.applyEffectsCounter = state.applyEffectsCounter; + state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter) -> + this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy())); } public void restoreForRollBack(GameState state) { @@ -224,6 +226,8 @@ public class GameState implements Serializable, Copyable { this.copiedCards = state.copiedCards; this.permanentOrderNumber = state.permanentOrderNumber; this.applyEffectsCounter = state.applyEffectsCounter; + state.usePowerInsteadOfToughnessForDamageLethalityFilters.forEach((uuid, filter) -> + this.usePowerInsteadOfToughnessForDamageLethalityFilters.put(uuid, filter.copy())); } @Override @@ -1097,6 +1101,7 @@ public class GameState implements Serializable, Copyable { zones.clear(); simultaneousEvents.clear(); copiedCards.clear(); + usePowerInsteadOfToughnessForDamageLethalityFilters.clear(); permanentOrderNumber = 0; } From 51dcaa1725fd2292d02e9f366d96958ee2fbaa3d Mon Sep 17 00:00:00 2001 From: htrajan Date: Thu, 16 Apr 2020 20:14:43 -0700 Subject: [PATCH 8/8] update simulators --- .../java/mage/player/ai/ComputerPlayer.java | 4 +-- .../ai/simulators/CombatGroupSimulator.java | 14 +++++----- .../player/ai/simulators/CombatSimulator.java | 6 ++--- .../ai/simulators/CreatureSimulator.java | 27 ++++++++++++++----- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java index 4070da886fa..d1c17c24666 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/ComputerPlayer.java @@ -2434,7 +2434,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { simulations = new TreeNode<>(combat); addBlockSimulations(blockers, simulations, game); - combat.simulate(); + combat.simulate(game); return getWorstSimulation(simulations); @@ -2452,7 +2452,7 @@ public class ComputerPlayer extends PlayerImpl implements Player { TreeNode child = new TreeNode<>(combat); node.addChild(child); addBlockSimulations(subList, child, game); - combat.simulate(); + combat.simulate(game); } } } diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CombatGroupSimulator.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CombatGroupSimulator.java index 874a4dd1314..ffa8bdee0e6 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CombatGroupSimulator.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CombatGroupSimulator.java @@ -1,5 +1,3 @@ - - package mage.player.ai.simulators; import mage.game.Game; @@ -52,15 +50,15 @@ public class CombatGroupSimulator implements Serializable { return blocker.canBlock(attacker.id, game); } - public void simulateCombat() { + public void simulateCombat(Game game) { unblockedDamage = 0; if (hasFirstOrDoubleStrike()) - assignDamage(true); - assignDamage(false); + assignDamage(true, game); + assignDamage(false, game); } - private void assignDamage(boolean first) { + private void assignDamage(boolean first, Game game) { if (blockers.isEmpty()) { if (canDamage(attacker, first)) unblockedDamage += attacker.power; @@ -69,7 +67,7 @@ public class CombatGroupSimulator implements Serializable { CreatureSimulator blocker = blockers.get(0); if (canDamage(attacker, first)) { if (attacker.hasTrample) { - int lethalDamage = blocker.getLethalDamage(); + int lethalDamage = blocker.getLethalDamage(game); if (attacker.power > lethalDamage) { blocker.damage += lethalDamage; unblockedDamage += attacker.power - lethalDamage; @@ -87,7 +85,7 @@ public class CombatGroupSimulator implements Serializable { int damage = attacker.power; for (CreatureSimulator blocker: blockers) { if (damage > 0 && canDamage(attacker, first)) { - int lethalDamage = blocker.getLethalDamage(); + int lethalDamage = blocker.getLethalDamage(game); if (damage > lethalDamage) { blocker.damage += lethalDamage; damage -= lethalDamage; diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CombatSimulator.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CombatSimulator.java index 79f809bbdad..7157962a522 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CombatSimulator.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CombatSimulator.java @@ -1,5 +1,3 @@ - - package mage.player.ai.simulators; import mage.counters.CounterType; @@ -52,9 +50,9 @@ public class CombatSimulator implements Serializable { attackerId = null; } - public void simulate() { + public void simulate(Game game) { for (CombatGroupSimulator group: groups) { - group.simulateCombat(); + group.simulateCombat(game); } } diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CreatureSimulator.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CreatureSimulator.java index e6ef4aafdd1..1a5e7fe7b85 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CreatureSimulator.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/CreatureSimulator.java @@ -1,14 +1,16 @@ - - package mage.player.ai.simulators; -import java.io.Serializable; -import java.util.UUID; import mage.abilities.keyword.DoubleStrikeAbility; import mage.abilities.keyword.FirstStrikeAbility; import mage.abilities.keyword.TrampleAbility; +import mage.filter.common.FilterCreaturePermanent; +import mage.game.Game; import mage.game.permanent.Permanent; +import java.io.Serializable; +import java.util.List; +import java.util.UUID; + /** * * @author BetaSteward_at_googlemail.com @@ -21,6 +23,7 @@ public class CreatureSimulator implements Serializable { public boolean hasFirstStrike; public boolean hasDoubleStrike; public boolean hasTrample; + public Permanent permanent; public CreatureSimulator(Permanent permanent) { this.id = permanent.getId(); @@ -30,13 +33,25 @@ public class CreatureSimulator implements Serializable { this.hasDoubleStrike = permanent.getAbilities().containsKey(DoubleStrikeAbility.getInstance().getId()); this.hasFirstStrike = permanent.getAbilities().containsKey(FirstStrikeAbility.getInstance().getId()); this.hasTrample = permanent.getAbilities().containsKey(TrampleAbility.getInstance().getId()); + this.permanent = permanent; } public boolean isDead() { return damage >= toughness; } - public int getLethalDamage() { - return toughness - damage; + public int getLethalDamage(Game game) { + List usePowerInsteadOfToughnessForDamageLethalityFilters = game.getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters(); + /* + * for handling Zilortha, Strength Incarnate: + * 2020-04-17 + * Any time the game is checking whether damage is lethal or if a creature should be destroyed for having lethal damage marked on it, use the power of your creatures rather than their toughness to check the damage against. This includes being assigned trample damage, damage from Flame Spill, and so on. + */ + boolean usePowerInsteadOfToughnessForDamageLethality = usePowerInsteadOfToughnessForDamageLethalityFilters.stream() + .anyMatch(filter -> filter.match(permanent, game)); + int lethalDamageThreshold = usePowerInsteadOfToughnessForDamageLethality ? + // Zilortha, Strength Incarnate, 2020-04-17: A creature with 0 power isn’t destroyed unless it has at least 1 damage marked on it. + Math.max(power, 1) : toughness; + return Math.max(lethalDamageThreshold - damage, 0); } }