Ready for Review: Implementing Battles (#10156)

* add types and subtypes

* add startingDefense attribute

* [MOM] Implement Invasion of Ravnica / Guildpact Paragon

* fix two small errors

* refactor various instances of "any target"

* fully implement defense counters

* battles can now be attacked

* [MOM] Implement Invasion of Dominaria / Serra Faithkeeper

* [MOM] Implement Invasion of Innistrad / Deluge of the Dead

* [MOM] Implement Invasion of Kaladesh / Aetherwing, Golden-Scale Flagship

* [MOM] Implement Invasion of Kamigawa / Rooftop Saboteurs

* [MOM] Implement Invasion of Karsus / Refraction Elemental

* [MOM] Implement Invasion of Tolvada / The Broken Sky

* simplify battle info ability

* fix verify failure

* some more fixes for attacking battles

* [MOM] Implement Invasion of Kaldheim / Pyre of the World Tree

* [MOM] Implement Invasion of Lorwyn / Winnowing Forces

* [MOM] Implement Invasion of Moag / Bloomwielder Dryads

* [MOM] Implement Invasion of Shandalar / Leyline Surge

* [MOM] Implement Invasion of Belenon / Belenon War Anthem

* [MOM] Implement Invasion of Pyrulea / Gargantuan Slabhorn

* [MOM] Implement Invasion of Vryn / Overloaded Mage-Ring

* [MOM] Implement Marshal of Zhalfir

* [MOM] Implement Sunfall

* implement protectors for sieges

* partially implement siege defeated trigger

* fix verify failure

* some updates to blocking

* [MOM] Implement Invasion of Mercadia / Kyren Flamewright

* [MOM] Implement Invasion of Theros / Ephara, Ever-Sheltering

* [MOM] Implement Invasion of Ulgrotha / Grandmother Ravi Sengir

* [MOM] Implement Invasion of Xerex / Vertex Paladin

* add initial battle test

* fix verify failure

* [MOM] Implement Invasion of Amonkhet / Lazotep Convert

* [MOM] update spoiler

* update how protectors are chosen

* update text

* battles can't block

* add control change test

* rename battle test for duel

* add multiplayer test

* [MOM] Implement Invasion of Alara / Awaken the Maelstrom

* [MOM] Implement Invasion of Eldraine

* [MOM] Implement Invasion of Ergamon / Truga Cliffhanger

* [MOM] Implement Invasion of Ixalan / Belligerent Regisaur

* battles now cast transformed (this is super hacky but we need to refactor TDFCs anyway)

* add TODO

* add ignore for randomly failing test

* a few small fixes

* add defense to MtgJsonCard (unused like loyalty)

* implement ProtectorIdPredicate

* small fixes
This commit is contained in:
Evan Kranzler 2023-04-13 20:03:16 -04:00 committed by GitHub
parent edf1cff8a8
commit 947351932b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
129 changed files with 4057 additions and 1087 deletions

View file

@ -11,7 +11,9 @@ import mage.abilities.keyword.VigilanceAbility;
import mage.abilities.keyword.special.JohanVigilanceAbility;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.common.FilterBattlePermanent;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.common.FilterCreatureForCombatBlock;
import mage.filter.common.FilterCreaturePermanent;
@ -21,6 +23,7 @@ import mage.filter.predicate.mageobject.AbilityPredicate;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.filter.predicate.permanent.AttackingSameNotBandedPredicate;
import mage.filter.predicate.permanent.PermanentIdPredicate;
import mage.filter.predicate.permanent.ProtectedByOpponentPredicate;
import mage.game.Game;
import mage.game.events.*;
import mage.game.permanent.Permanent;
@ -45,6 +48,12 @@ public class Combat implements Serializable, Copyable<Combat> {
private static final Logger logger = Logger.getLogger(Combat.class);
private static FilterCreatureForCombatBlock filterBlockers = new FilterCreatureForCombatBlock();
private static final FilterPermanent filterBattles = new FilterBattlePermanent();
static {
filterBattles.add(ProtectedByOpponentPredicate.instance);
}
// There are effects that let creatures assigns combat damage equal to its toughness rather than its power
private boolean useToughnessForDamage;
private final List<FilterCreaturePermanent> useToughnessForDamageFilters = new ArrayList<>();
@ -213,10 +222,12 @@ public class Combat implements Serializable, Copyable<Combat> {
if (playerToAttack != null) {
possibleDefenders = new HashSet<>();
for (UUID objectId : defenders) {
Permanent planeswalker = game.getPermanent(objectId);
if (planeswalker != null && planeswalker.isControlledBy(playerToAttack)) {
if (playerToAttack.equals(objectId)) {
possibleDefenders.add(objectId);
} else if (playerToAttack.equals(objectId)) {
continue;
}
Permanent permanent = game.getPermanent(objectId);
if (permanent != null && permanent.canBeAttacked(creatureId, playerToAttack, game)) {
possibleDefenders.add(objectId);
}
}
@ -231,8 +242,7 @@ public class Combat implements Serializable, Copyable<Combat> {
addAttackerToCombat(creatureId, possibleDefenders.iterator().next(), game);
return true;
} else {
TargetDefender target = new TargetDefender(possibleDefenders, creatureId);
target.setNotTarget(true);
TargetDefender target = new TargetDefender(possibleDefenders);
target.setRequired(true);
player.chooseTarget(Outcome.Damage, target, null, game);
if (target.getFirstTarget() != null) {
@ -357,8 +367,8 @@ public class Combat implements Serializable, Copyable<Combat> {
if (game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, attackingPlayerId, attackingPlayerId))
|| (!canBand && !canBandWithOther)
|| !player.chooseUse(Outcome.Benefit,
(isBanded ? "Band " + attacker.getLogName()
+ " with another " : "Form a band with " + attacker.getLogName() + " and an ")
(isBanded ? "Band " + attacker.getLogName()
+ " with another " : "Form a band with " + attacker.getLogName() + " and an ")
+ "attacking creature?", null, game)) {
break;
}
@ -511,9 +521,9 @@ public class Combat implements Serializable, Copyable<Combat> {
player.declareAttacker(creature.getId(), defendersToChooseFrom.iterator().next(), game, false);
continue;
}
TargetDefender target = new TargetDefender(defendersToChooseFrom, creature.getId());
TargetDefender target = new TargetDefender(defendersToChooseFrom);
target.setRequired(true);
target.setTargetName("planeswalker or player for " + creature.getLogName() + " to attack (must attack effect)");
target.setTargetName("permanent or player for " + creature.getLogName() + " to attack (must attack effect)");
if (player.chooseTarget(Outcome.Damage, target, null, game)) {
player.declareAttacker(creature.getId(), target.getFirstTarget(), game, false);
}
@ -595,7 +605,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* Handle the blocker selection process
*
* @param blockController player that controls how to block, if null the
* defender is the controller
* defender is the controller
* @param game
*/
public void selectBlockers(Player blockController, Ability source, Game game) {
@ -605,34 +615,35 @@ public class Combat implements Serializable, Copyable<Combat> {
Player controller;
for (UUID defenderId : getPlayerDefenders(game)) {
Player defender = game.getPlayer(defenderId);
if (defender != null) {
boolean choose = true;
if (blockController == null) {
controller = defender;
} else {
controller = blockController;
if (defender == null) {
continue;
}
boolean choose = true;
if (blockController == null) {
controller = defender;
} else {
controller = blockController;
}
while (choose) {
controller.selectBlockers(source, game, defenderId);
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
return;
}
while (choose) {
controller.selectBlockers(source, game, defenderId);
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
return;
}
if (!game.getCombat().checkBlockRestrictions(defender, game)) {
if (controller.isHuman()) { // only human player can decide to do the block in another way
continue;
}
}
choose = !game.getCombat().checkBlockRequirementsAfter(defender, controller, game);
if (!choose) {
choose = !game.getCombat().checkBlockRestrictionsAfter(defender, controller, game);
if (!game.getCombat().checkBlockRestrictions(defender, game)) {
if (controller.isHuman()) { // only human player can decide to do the block in another way
continue;
}
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defenderId, defenderId));
choose = !game.getCombat().checkBlockRequirementsAfter(defender, controller, game);
if (!choose) {
choose = !game.getCombat().checkBlockRestrictionsAfter(defender, controller, game);
}
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defenderId, defenderId));
// add info about attacker blocked by blocker to the game log
if (!game.isSimulation()) {
game.getCombat().logBlockerInfo(defender, game);
}
// add info about attacker blocked by blocker to the game log
if (!game.isSimulation()) {
game.getCombat().logBlockerInfo(defender, game);
}
}
// tool to catch the bug about flyers blocked by non flyers or intimidate blocked by creatures with other colors
@ -1259,6 +1270,9 @@ public class Combat implements Serializable, Copyable<Combat> {
for (UUID playerId : getAttackablePlayers(game)) {
addDefender(playerId, game);
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filterBattles, attackingPlayerId, game)) {
defenders.add(permanent.getId());
}
}
public List<UUID> getAttackablePlayers(Game game) {
@ -1351,7 +1365,17 @@ public class Combat implements Serializable, Copyable<Combat> {
if (attacker == null) {
return false;
}
CombatGroup newGroup = new CombatGroup(defenderId, defender != null, defender != null ? defender.getControllerId() : defenderId);
UUID defendingPlayerId;
if (defender == null) {
defendingPlayerId = defenderId;
} else if (defender.isPlaneswalker(game)) {
defendingPlayerId = defender.getControllerId();
} else if (defender.isBattle(game)) {
defendingPlayerId = defender.getProtectorId();
} else {
defendingPlayerId = null;
}
CombatGroup newGroup = new CombatGroup(defenderId, defender != null, defendingPlayerId);
newGroup.attackers.add(attackerId);
attacker.setAttacking(true);
groups.add(newGroup);
@ -1413,7 +1437,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* @param playerId
* @param game
* @param solveBanding check whether also add creatures banded with
* attackerId
* attackerId
*/
public void addBlockingGroup(UUID blockerId, UUID attackerId, UUID playerId, Game game, boolean solveBanding) {
Permanent blocker = game.getPermanent(blockerId);
@ -1453,11 +1477,11 @@ public class Combat implements Serializable, Copyable<Combat> {
}
}
public boolean removePlaneswalkerFromCombat(UUID planeswalkerId, Game game) {
public boolean removeDefendingPermanentFromCombat(UUID permanentId, Game game) {
boolean result = false;
for (CombatGroup group : groups) {
if (group.getDefenderId() != null && group.getDefenderId().equals(planeswalkerId)) {
group.removeAttackedPlaneswalker(planeswalkerId);
if (group.getDefenderId() != null && group.getDefenderId().equals(permanentId)) {
group.removeAttackedPermanent(permanentId);
result = true;
}
}
@ -1465,31 +1489,32 @@ public class Combat implements Serializable, Copyable<Combat> {
}
public boolean removeFromCombat(UUID creatureId, Game game, boolean withEvent) {
boolean result = false;
Permanent creature = game.getPermanent(creatureId);
if (creature != null) {
if (withEvent) {
creature.setAttacking(false);
creature.setBlocking(0);
}
for (CombatGroup group : groups) {
for (UUID attackerId : group.attackers) {
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null) {
attacker.removeBandedCard(creatureId);
}
if (creature == null) {
return false;
}
boolean result = false;
if (withEvent) {
creature.setAttacking(false);
creature.setBlocking(0);
}
for (CombatGroup group : groups) {
for (UUID attackerId : group.attackers) {
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null) {
attacker.removeBandedCard(creatureId);
}
result |= group.remove(creatureId);
}
for (CombatGroup blockingGroup : getBlockingGroups()) {
result |= blockingGroup.remove(creatureId);
}
creature.clearBandedCards();
blockingGroups.remove(creatureId);
if (result && withEvent) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.REMOVED_FROM_COMBAT, creatureId, null, null));
game.informPlayers(creature.getLogName() + " removed from combat");
}
result |= group.remove(creatureId);
}
for (CombatGroup blockingGroup : getBlockingGroups()) {
result |= blockingGroup.remove(creatureId);
}
creature.clearBandedCards();
blockingGroups.remove(creatureId);
if (result && withEvent) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.REMOVED_FROM_COMBAT, creatureId, null, null));
game.informPlayers(creature.getLogName() + " removed from combat");
}
return result;
}
@ -1540,15 +1565,6 @@ public class Combat implements Serializable, Copyable<Combat> {
return null;
}
// public int totalUnblockedDamage(Game game) {
// int total = 0;
// for (CombatGroup group : groups) {
// if (group.getBlockers().isEmpty()) {
// total += group.totalAttackerDamage(game);
// }
// }
// return total;
// }
public boolean attacksAlone() {
return (groups.size() == 1 && groups.get(0).getAttackers().size() == 1);
}
@ -1557,24 +1573,9 @@ public class Combat implements Serializable, Copyable<Combat> {
return groups.isEmpty() || getAttackers().isEmpty();
}
public boolean isAttacked(UUID defenderId, Game game) {
for (CombatGroup group : groups) {
if (group.getDefenderId().equals(defenderId)) {
return true;
}
if (group.defenderIsPlaneswalker) {
Permanent permanent = game.getPermanent(group.getDefenderId());
if (permanent.isControlledBy(defenderId)) {
return true;
}
}
}
return false;
}
public boolean isPlaneswalkerAttacked(UUID defenderId, Game game) {
for (CombatGroup group : groups) {
if (group.defenderIsPlaneswalker) {
if (group.isDefenderIsPermanent()) {
Permanent permanent = game.getPermanent(group.getDefenderId());
if (permanent.isControlledBy(defenderId)) {
return true;
@ -1589,14 +1590,12 @@ public class Combat implements Serializable, Copyable<Combat> {
* @return uuid of defending player or planeswalker
*/
public UUID getDefenderId(UUID attackerId) {
UUID defenderId = null;
for (CombatGroup group : groups) {
if (group.getAttackers().contains(attackerId)) {
defenderId = group.getDefenderId();
break;
}
}
return defenderId;
return groups
.stream()
.filter(group -> group.getAttackers().contains(attackerId))
.map(CombatGroup::getDefenderId)
.findFirst()
.orElse(null);
}
/**
@ -1608,40 +1607,32 @@ public class Combat implements Serializable, Copyable<Combat> {
* @return
*/
public UUID getDefendingPlayerId(UUID attackingCreatureId, Game game) {
UUID defenderId = null;
for (CombatGroup group : groups) {
if (group.getAttackers().contains(attackingCreatureId)) {
defenderId = group.getDefenderId();
if (group.defenderIsPlaneswalker) {
Permanent permanent = game.getPermanentOrLKIBattlefield(defenderId);
if (permanent != null) {
defenderId = permanent.getControllerId();
} else {
defenderId = null;
}
}
break;
}
}
return defenderId;
return groups
.stream()
.filter(group -> group.getAttackers().contains(attackingCreatureId))
.map(CombatGroup::getDefendingPlayerId)
.findFirst()
.orElse(null);
}
public Set<UUID> getPlayerDefenders(Game game) {
return getPlayerDefenders(game, true);
}
public Set<UUID> getPlayerDefenders(Game game, boolean includePlaneswalkers) {
public Set<UUID> getPlayerDefenders(Game game, boolean includePermanents) {
Set<UUID> playerDefenders = new HashSet<>();
for (CombatGroup group : groups) {
if (group.defenderIsPlaneswalker && !includePlaneswalkers) {
if (group.isDefenderIsPermanent() && !includePermanents) {
continue;
}
if (group.defenderIsPlaneswalker) {
if (group.isDefenderIsPermanent()) {
Permanent permanent = game.getPermanent(group.getDefenderId());
if (permanent != null) {
playerDefenders.add(permanent.getControllerId());
} else {
if (permanent == null) {
playerDefenders.add(group.getDefendingPlayerId());
} else if (permanent.isPlaneswalker(game)) {
playerDefenders.add(permanent.getControllerId());
} else if (permanent.isBattle(game)) {
playerDefenders.add(permanent.getProtectorId());
}
} else {
playerDefenders.add(group.getDefenderId());