forked from External/mage
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:
parent
edf1cff8a8
commit
947351932b
129 changed files with 4057 additions and 1087 deletions
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue