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

@ -116,6 +116,10 @@ public interface MageObject extends MageItem, Serializable, Copyable<MageObject>
void setStartingLoyalty(int startingLoyalty);
int getStartingDefense();
void setStartingDefense(int startingDefense);
// memory object copy (not mtg)
@Override
MageObject copy();

View file

@ -40,6 +40,7 @@ public abstract class MageObjectImpl implements MageObject {
protected MageInt power;
protected MageInt toughness;
protected int startingLoyalty = -1; // -2 means X, -1 means none, 0 and up is normal
protected int startingDefense = -1; // -2 means X, -1 means none, 0 and up is normal
protected boolean copy;
protected MageObject copyFrom; // copied card INFO (used to call original adjusters)
@ -69,6 +70,7 @@ public abstract class MageObjectImpl implements MageObject {
power = object.power.copy();
toughness = object.toughness.copy();
startingLoyalty = object.startingLoyalty;
startingDefense = object.startingDefense;
abilities = object.abilities.copy();
this.cardType.addAll(object.cardType);
this.subtype.copyFrom(object.subtype);
@ -176,6 +178,16 @@ public abstract class MageObjectImpl implements MageObject {
this.startingLoyalty = startingLoyalty;
}
@Override
public int getStartingDefense() {
return startingDefense;
}
@Override
public void setStartingDefense(int startingDefense) {
this.startingDefense = startingDefense;
}
@Override
public ObjectColor getColor() {
return color;

View file

@ -2,7 +2,6 @@ package mage.abilities;
import mage.MageIdentifier;
import mage.MageObject;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.condition.Condition;
import mage.abilities.costs.*;
import mage.abilities.costs.common.PayLifeCost;
@ -19,7 +18,6 @@ import mage.abilities.hint.Hint;
import mage.abilities.icon.CardIcon;
import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.cards.Card;
import mage.cards.SplitCard;
import mage.constants.*;
import mage.game.Game;
import mage.game.command.Dungeon;
@ -437,7 +435,7 @@ public abstract class AbilityImpl implements Ability {
case FLASHBACK:
case MADNESS:
case DISTURB:
case TRANSFORMED:
// from Snapcaster Mage:
// If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs
// (such as that of Foil). (2018-12-07)

View file

@ -0,0 +1,147 @@
package mage.abilities.common;
import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.StaticAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.TransformAbility;
import mage.cards.Card;
import mage.constants.*;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
/**
* @author TheElk801
*/
public class SiegeAbility extends StaticAbility {
public SiegeAbility() {
super(Zone.ALL, null);
this.addSubAbility(new TransformAbility());
this.addSubAbility(new SiegeDefeatedTriggeredAbility());
}
private SiegeAbility(final SiegeAbility ability) {
super(ability);
}
@Override
public SiegeAbility copy() {
return new SiegeAbility(this);
}
@Override
public String getRule() {
return "<i>(As a Siege enters, choose an opponent to protect it. You and others " +
"can attack it. When it's defeated, exile it, then cast it transformed.)</i>";
}
}
class SiegeDefeatedTriggeredAbility extends TriggeredAbilityImpl {
SiegeDefeatedTriggeredAbility() {
super(Zone.BATTLEFIELD, new SiegeDefeatedEffect());
this.setRuleVisible(false);
}
private SiegeDefeatedTriggeredAbility(final SiegeDefeatedTriggeredAbility ability) {
super(ability);
}
@Override
public SiegeDefeatedTriggeredAbility copy() {
return new SiegeDefeatedTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.COUNTERS_REMOVED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
Permanent permanent = getSourcePermanentOrLKI(game);
return permanent != null
&& permanent.getCounters(game).getCount(CounterType.DEFENSE) == 0
&& event.getTargetId().equals(this.getSourceId())
&& event.getData().equals("defense") && event.getAmount() > 0;
}
@Override
public String getRule() {
return "When the last defense counter is removed from this permanent, exile it, " +
"then you may cast it transformed without paying its mana cost.";
}
}
class SiegeDefeatedEffect extends OneShotEffect {
SiegeDefeatedEffect() {
super(Outcome.Benefit);
}
private SiegeDefeatedEffect(final SiegeDefeatedEffect effect) {
super(effect);
}
@Override
public SiegeDefeatedEffect copy() {
return new SiegeDefeatedEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (player == null || permanent == null) {
return false;
}
Card card = permanent.getMainCard();
player.moveCards(permanent, Zone.EXILED, source, game);
if (card == null || card.getSecondFaceSpellAbility() == null) {
return true;
}
game.getState().setValue("PlayFromNotOwnHandZone" + card.getSecondCardFace().getId(), Boolean.TRUE);
if (player.cast(card.getSecondFaceSpellAbility(), game, true, new ApprovingObject(source, game))) {
game.getState().setValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId(), Boolean.TRUE);
game.addEffect(new SiegeTransformEffect().setTargetPointer(new FixedTarget(card, game)), source);
}
game.getState().setValue("PlayFromNotOwnHandZone" + card.getSecondCardFace().getId(), null);
return true;
}
}
class SiegeTransformEffect extends ContinuousEffectImpl {
public SiegeTransformEffect() {
super(Duration.WhileOnStack, Layer.CopyEffects_1, SubLayer.CopyEffects_1a, Outcome.BecomeCreature);
}
private SiegeTransformEffect(final SiegeTransformEffect effect) {
super(effect);
}
@Override
public SiegeTransformEffect copy() {
return new SiegeTransformEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Spell spell = game.getSpell(getTargetPointer().getFirst(game, source));
if (spell == null || spell.getCard().getSecondCardFace() == null) {
return false;
}
// simulate another side as new card (another code part in spell constructor)
TransformAbility.transformCardSpellDynamic(spell, spell.getCard().getSecondCardFace(), game);
return true;
}
}

View file

@ -134,6 +134,7 @@ public class CopyEffect extends ContinuousEffectImpl {
permanent.getPower().setModifiedBaseValue(copyFromObject.getPower().getModifiedBaseValue());
permanent.getToughness().setModifiedBaseValue(copyFromObject.getToughness().getModifiedBaseValue());
permanent.setStartingLoyalty(copyFromObject.getStartingLoyalty());
permanent.setStartingDefense(copyFromObject.getStartingDefense());
if (copyFromObject instanceof Permanent) {
Permanent targetPermanent = (Permanent) copyFromObject;
permanent.setTransformed(targetPermanent.isTransformed());
@ -199,8 +200,9 @@ public class CopyEffect extends ContinuousEffectImpl {
return applier;
}
public void setApplier(CopyApplier applier) {
public CopyEffect setApplier(CopyApplier applier) {
this.applier = applier;
return this;
}
}

View file

@ -37,7 +37,7 @@ public class DisturbAbility extends SpellAbility {
this.setCardName(card.getSecondCardFace().getName() + " with Disturb");
this.zone = Zone.GRAVEYARD;
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.spellAbilityCastMode = SpellAbilityCastMode.DISTURB;
this.spellAbilityCastMode = SpellAbilityCastMode.TRANSFORMED;
this.manaCost = manaCost;
this.getManaCosts().clear();

View file

@ -24,7 +24,7 @@ public class MoreThanMeetsTheEyeAbility extends SpellAbility {
// getSecondFaceSpellAbility() already verified that second face exists
this.setCardName(card.getSecondCardFace().getName() + " with Disturb");
this.spellAbilityType = SpellAbilityType.BASE_ALTERNATE;
this.spellAbilityCastMode = SpellAbilityCastMode.DISTURB;
this.spellAbilityCastMode = SpellAbilityCastMode.TRANSFORMED;
this.manaCost = manaCost;
this.getManaCosts().clear();

View file

@ -67,6 +67,7 @@ public class TransformAbility extends SimpleStaticAbility {
permanent.getPower().setModifiedBaseValue(sourceCard.getPower().getValue());
permanent.getToughness().setModifiedBaseValue(sourceCard.getToughness().getValue());
permanent.setStartingLoyalty(sourceCard.getStartingLoyalty());
permanent.setStartingDefense(sourceCard.getStartingDefense());
}
public static Card transformCardSpellStatic(Card mainSide, Card otherSide, Game game) {

View file

@ -614,12 +614,17 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
@Override
public final Card getSecondCardFace() {
// init card side on first call
if (secondSideCardClazz == null && secondSideCard == null) {
if (secondSideCardClazz == null) {
return null;
}
if (secondSideCard == null) {
secondSideCard = initSecondSideCard(secondSideCardClazz);
if (secondSideCard != null && secondSideCard.getSpellAbility() != null) {
secondSideCard.getSpellAbility().setSourceId(this.getId());
secondSideCard.getSpellAbility().setSpellAbilityType(SpellAbilityType.BASE_ALTERNATE);
secondSideCard.getSpellAbility().setSpellAbilityCastMode(SpellAbilityCastMode.TRANSFORMED);
}
}
return secondSideCard;

View file

@ -52,6 +52,8 @@ public class CardInfo {
@DatabaseField
protected String startingLoyalty;
@DatabaseField
protected String startingDefense;
@DatabaseField
protected int manaValue;
@DatabaseField(dataType = DataType.ENUM_STRING)
protected Rarity rarity;
@ -226,7 +228,8 @@ public class CardInfo {
}
// Starting loyalty
this.startingLoyalty = CardUtil.convertStartingLoyalty(card.getStartingLoyalty());
this.startingLoyalty = CardUtil.convertLoyaltyOrDefense(card.getStartingLoyalty());
this.startingDefense = CardUtil.convertLoyaltyOrDefense(card.getStartingDefense());
}
public Card getCard() {

View file

@ -13,7 +13,7 @@ public enum SpellAbilityCastMode {
MADNESS("Madness"),
FLASHBACK("Flashback"),
BESTOW("Bestow"),
DISTURB("Disturb");
TRANSFORMED("Transformed");
private final String text;

View file

@ -14,6 +14,10 @@ public enum SubType {
ARCANE("Arcane", SubTypeSet.SpellType),
LESSON("Lesson", SubTypeSet.SpellType),
TRAP("Trap", SubTypeSet.SpellType),
// Battle subtypes
SIEGE("Siege", SubTypeSet.BattleType),
// 205.3i: Lands have their own unique set of subtypes; these subtypes are called land types.
// Of that list, Forest, Island, Mountain, Plains, and Swamp are the basic land types.
FOREST("Forest", SubTypeSet.BasicLandType),
@ -623,6 +627,8 @@ public enum SubType {
return mageObject.isPlaneswalker(game);
case SpellType:
return mageObject.isInstantOrSorcery(game);
case BattleType:
return mageObject.isBattle(game);
}
return false;
}
@ -637,12 +643,10 @@ public enum SubType {
public static Set<SubType> getPlaneswalkerTypes() {
return subTypeSetMap.get(SubTypeSet.PlaneswalkerType);
}
public static Set<SubType> getCreatureTypes() {
return subTypeSetMap.get(SubTypeSet.CreatureType);
}
public static Set<SubType> getBasicLands() {
@ -653,6 +657,10 @@ public enum SubType {
return landTypes;
}
public static Set<SubType> getBattleTypes() {
return subTypeSetMap.get(SubTypeSet.BattleType);
}
public static Set<SubType> getBySubTypeSet(SubTypeSet subTypeSet) {
return subTypeSetMap.get(subTypeSet);
}

View file

@ -1,6 +1,7 @@
package mage.constants;
public enum SubTypeSet {
BattleType,
CreatureType,
SpellType,
BasicLandType(true),

View file

@ -219,6 +219,15 @@ public abstract class Designation implements MageObject {
public void setStartingLoyalty(int startingLoyalty) {
}
@Override
public int getStartingDefense() {
return 0;
}
@Override
public void setStartingDefense(int startingDefense) {
}
@Override
public int getZoneChangeCounter(Game game) {
return 1; // Emblems can't move zones until now so return always 1

View file

@ -230,6 +230,12 @@ public final class StaticFilters {
FILTER_CARD_A_PERMANENT.setLockedFilter(true);
}
public static final FilterPermanentCard FILTER_CARD_PERMANENTS = new FilterPermanentCard("permanent cards");
static {
FILTER_CARD_PERMANENTS.setLockedFilter(true);
}
public static final FilterPermanent FILTER_PERMANENT = new FilterPermanent();
static {

View file

@ -0,0 +1,32 @@
package mage.filter.common;
import mage.constants.CardType;
import mage.filter.predicate.Predicates;
/**
* @author TheElk801
*/
public class FilterAnyTarget extends FilterPermanentOrPlayer {
public FilterAnyTarget() {
this("any target");
}
public FilterAnyTarget(String name) {
super(name);
this.permanentFilter.add(Predicates.or(
CardType.CREATURE.getPredicate(),
CardType.PLANESWALKER.getPredicate(),
CardType.BATTLE.getPredicate()
));
}
public FilterAnyTarget(final FilterAnyTarget filter) {
super(filter);
}
@Override
public FilterAnyTarget copy() {
return new FilterAnyTarget(this);
}
}

View file

@ -1,16 +1,8 @@
package mage.filter.common;
import mage.MageObject;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.Predicates;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* If you add predicate to permanentFilter then it will be applied to planeswalker too
*
@ -19,25 +11,14 @@ import java.util.stream.Collectors;
public class FilterCreaturePlayerOrPlaneswalker extends FilterPermanentOrPlayer {
public FilterCreaturePlayerOrPlaneswalker() {
this("any target");
this("creature, player, or planeswalker");
}
public FilterCreaturePlayerOrPlaneswalker(String name) {
this(name, (SubType) null);
}
public FilterCreaturePlayerOrPlaneswalker(String name, SubType... andCreatureTypes) {
super(name);
List<Predicate<MageObject>> allCreaturePredicates = Arrays.stream(andCreatureTypes)
.filter(Objects::nonNull)
.map(SubType::getPredicate)
.collect(Collectors.toList());
allCreaturePredicates.add(0, CardType.CREATURE.getPredicate());
Predicate<MageObject> planeswalkerPredicate = CardType.PLANESWALKER.getPredicate();
this.permanentFilter.add(Predicates.or(
Predicates.and(allCreaturePredicates),
planeswalkerPredicate
CardType.CREATURE.getPredicate(),
CardType.PLANESWALKER.getPredicate()
));
}

View file

@ -0,0 +1,46 @@
package mage.filter.common;
import mage.constants.CardType;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.other.PlayerIdPredicate;
import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.filter.predicate.permanent.PermanentIdPredicate;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author BetaSteward_at_googlemail.com
*/
public class FilterDefender extends FilterPermanentOrPlayer {
public FilterDefender(Set<UUID> defenders) {
super("player, planeswalker, or battle to attack");
this.permanentFilter.add(Predicates.or(
CardType.PLANESWALKER.getPredicate(),
CardType.BATTLE.getPredicate()
));
this.permanentFilter.add(Predicates.or(
defenders
.stream()
.map(PermanentIdPredicate::new)
.collect(Collectors.toList())
));
this.playerFilter.add(Predicates.or(
defenders
.stream()
.map(PlayerIdPredicate::new)
.collect(Collectors.toList())
));
}
private FilterDefender(final FilterDefender filter) {
super(filter);
}
@Override
public FilterDefender copy() {
return new FilterDefender(this);
}
}

View file

@ -1,78 +0,0 @@
package mage.filter.common;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import mage.filter.FilterImpl;
import mage.filter.FilterPlayer;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.other.PlayerIdPredicate;
import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public class FilterPlaneswalkerOrPlayer extends FilterImpl<Object> {
protected final FilterPlaneswalkerPermanent planeswalkerFilter;
protected final FilterPlayer playerFilter;
public FilterPlaneswalkerOrPlayer(Set<UUID> defenders) {
super("planeswalker or player");
List<Predicate<Permanent>> permanentPredicates = new ArrayList<>();
for (UUID defenderId : defenders) {
permanentPredicates.add(new ControllerIdPredicate(defenderId));
}
planeswalkerFilter = new FilterPlaneswalkerPermanent();
planeswalkerFilter.add(Predicates.or(permanentPredicates));
List<Predicate<Player>> playerPredicates = new ArrayList<>();
for (UUID defenderId : defenders) {
playerPredicates.add(new PlayerIdPredicate(defenderId));
}
playerFilter = new FilterPlayer();
playerFilter.add(Predicates.or(playerPredicates));
}
public FilterPlaneswalkerOrPlayer(final FilterPlaneswalkerOrPlayer filter) {
super(filter);
this.planeswalkerFilter = filter.planeswalkerFilter.copy();
this.playerFilter = filter.playerFilter.copy();
}
public FilterPlaneswalkerPermanent getFilterPermanent() {
return this.planeswalkerFilter;
}
public FilterPlayer getFilterPlayer() {
return this.playerFilter;
}
@Override
public boolean checkObjectClass(Object object) {
return true;
}
@Override
public boolean match(Object o, Game game) {
if (o instanceof Player) {
return playerFilter.match((Player) o, game);
} else if (o instanceof Permanent) {
return planeswalkerFilter.match((Permanent) o, game);
}
return false;
}
@Override
public FilterPlaneswalkerOrPlayer copy() {
return new FilterPlaneswalkerOrPlayer(this);
}
}

View file

@ -13,7 +13,6 @@ public enum ProtectedByOpponentPredicate implements ObjectSourcePlayerPredicate<
@Override
public boolean apply(ObjectSourcePlayer<Permanent> input, Game game) {
// TODO: Implement this
return false;
return game.getOpponents(input.getPlayerId()).contains(input.getObject().getProtectorId());
}
}

View file

@ -19,7 +19,6 @@ public class ProtectorIdPredicate implements Predicate<Permanent> {
@Override
public boolean apply(Permanent input, Game game) {
// TODO: implement this on battles branch
return false;
return input.isProtectedBy(protectorId);
}
}

View file

@ -48,6 +48,7 @@ import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.stack.Spell;
import mage.game.stack.SpellStack;
import mage.game.stack.StackAbility;
import mage.game.stack.StackObject;
import mage.game.turn.Phase;
import mage.game.turn.Step;
@ -2498,6 +2499,32 @@ public abstract class GameImpl implements Game {
somethingHappened = true;
}
}
if (perm.isBattle(this)) {
if (perm
.getCounters(this)
.getCount(CounterType.DEFENSE) == 0
&& this.getStack()
.stream()
.filter(StackAbility.class::isInstance)
.filter(stackObject -> stackObject.getStackAbility() instanceof TriggeredAbilityImpl)
.map(StackObject::getSourceId)
.noneMatch(perm.getId()::equals)
&& this.state
.getTriggered(perm.getControllerId())
.stream()
.filter(TriggeredAbility.class::isInstance)
.map(Ability::getSourceId)
.noneMatch(perm.getId()::equals)) {
if (movePermanentToGraveyardWithInfo(perm)) {
somethingHappened = true;
}
} else if (this.getPlayer(perm.getProtectorId()) == null || perm.isControlledBy(perm.getProtectorId())) {
perm.chooseProtector(this, null);
somethingHappened = true;
}
}
if (perm.isLegendary() && perm.legendRuleApplies()) {
legendary.add(perm);
}

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());

View file

@ -33,16 +33,16 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
protected boolean blocked;
protected UUID defenderId; // planeswalker or player
protected UUID defendingPlayerId;
protected boolean defenderIsPlaneswalker;
protected boolean defenderIsPermanent;
/**
* @param defenderId the player that controls the defending permanents
* @param defenderIsPlaneswalker is the defending permanent a planeswalker
* @param defendingPlayerId regular controller of the defending permanents
* @param defenderId the player that controls the defending permanents
* @param defenderIsPermanent is the defender a permanent
* @param defendingPlayerId regular controller of the defending permanents
*/
public CombatGroup(UUID defenderId, boolean defenderIsPlaneswalker, UUID defendingPlayerId) {
public CombatGroup(UUID defenderId, boolean defenderIsPermanent, UUID defendingPlayerId) {
this.defenderId = defenderId;
this.defenderIsPlaneswalker = defenderIsPlaneswalker;
this.defenderIsPermanent = defenderIsPermanent;
this.defendingPlayerId = defendingPlayerId;
}
@ -55,7 +55,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
this.blocked = group.blocked;
this.defenderId = group.defenderId;
this.defendingPlayerId = group.defendingPlayerId;
this.defenderIsPlaneswalker = group.defenderIsPlaneswalker;
this.defenderIsPermanent = group.defenderIsPermanent;
}
public boolean hasFirstOrDoubleStrike(Game game) {
@ -217,7 +217,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
permanent.applyDamage(game);
}
}
if (defenderIsPlaneswalker) {
if (defenderIsPermanent) {
Permanent permanent = game.getPermanent(defenderId);
if (permanent != null) {
permanent.applyDamage(game);
@ -545,46 +545,44 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
}
}
/**
* Do damage to attacked player or planeswalker
*
* @param attacker
* @param amount
* @param game
* @param damageToDefenderController excess damage to defender's controller (example: trample over planeswalker)
*/
private void defenderDamage(Permanent attacker, int amount, Game game, boolean damageToDefenderController) {
UUID affectedDefenderId = this.defenderId;
if (damageToDefenderController) {
affectedDefenderId = game.getControllerId(this.defenderId);
}
UUID affectedDefenderId = damageToDefenderController ? game.getControllerId(this.defenderId) : this.defenderId;
// on planeswalker
Permanent planeswalker = game.getPermanent(affectedDefenderId);
if (planeswalker != null) {
// apply excess damage from "trample over planeswaslkers" ability (example: Thrasta, Tempest's Roar)
if (hasTrampleOverPlaneswalkers(attacker)) {
int lethalDamage = planeswalker.getLethalDamage(attacker.getId(), game);
if (lethalDamage >= amount) {
// normal damage
planeswalker.markDamage(amount, attacker.getId(), null, game, true, true);
} else {
// damage with excess (additional damage to planeswalker's controller)
planeswalker.markDamage(lethalDamage, attacker.getId(), null, game, true, true);
amount -= lethalDamage;
if (amount > 0) {
defenderDamage(attacker, amount, game, true);
}
}
} else {
// normal damage
planeswalker.markDamage(amount, attacker.getId(), null, game, true, true);
Permanent permanent = game.getPermanent(affectedDefenderId);
if (permanent == null) {// on player
Player defender = game.getPlayer(affectedDefenderId);
if (defender != null) {
defender.damage(amount, attacker.getId(), null, game, true, true);
}
return;
}
// on player
Player defender = game.getPlayer(affectedDefenderId);
if (defender != null) {
defender.damage(amount, attacker.getId(), null, game, true, true);
// apply excess damage from "trample over planeswaslkers" ability (example: Thrasta, Tempest's Roar)
if (permanent.isPlaneswalker(game) && hasTrampleOverPlaneswalkers(attacker)) {
int lethalDamage = permanent.getLethalDamage(attacker.getId(), game);
if (lethalDamage >= amount) {
// normal damage
permanent.markDamage(amount, attacker.getId(), null, game, true, true);
} else {
// damage with excess (additional damage to permanent's controller)
permanent.markDamage(lethalDamage, attacker.getId(), null, game, true, true);
amount -= lethalDamage;
if (amount > 0) {
defenderDamage(attacker, amount, game, true);
}
}
} else {
// normal damage
permanent.markDamage(amount, attacker.getId(), null, game, true, true);
}
}
@ -717,20 +715,12 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
game.informPlayers(sb.toString());
}
public int totalAttackerDamage(Game game) {
int total = 0;
for (UUID attackerId : attackers) {
total += getDamageValueFromPermanent(game.getPermanent(attackerId), game);
}
return total;
public boolean isDefenderIsPermanent() {
return defenderIsPermanent;
}
public boolean isDefenderIsPlaneswalker() {
return defenderIsPlaneswalker;
}
public boolean removeAttackedPlaneswalker(UUID planeswalkerId) {
if (defenderIsPlaneswalker && defenderId.equals(planeswalkerId)) {
public boolean removeAttackedPermanent(UUID permanentId) {
if (defenderIsPermanent && defenderId.equals(permanentId)) {
defenderId = null;
return true;
}
@ -874,36 +864,36 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
}
public boolean changeDefenderPostDeclaration(UUID newDefenderId, Game game) {
if (!defenderId.equals(newDefenderId)) {
for (UUID attackerId : attackers) { // changing defender will remove a banded attacker from its current band
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null && attacker.getBandedCards() != null) {
for (UUID bandedId : attacker.getBandedCards()) {
Permanent banded = game.getPermanent(bandedId);
if (banded != null) {
banded.removeBandedCard(attackerId);
}
if (defenderId.equals(newDefenderId)) {
return false;
}
for (UUID attackerId : attackers) { // changing defender will remove a banded attacker from its current band
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null && attacker.getBandedCards() != null) {
for (UUID bandedId : attacker.getBandedCards()) {
Permanent banded = game.getPermanent(bandedId);
if (banded != null) {
banded.removeBandedCard(attackerId);
}
}
attacker.clearBandedCards();
}
Permanent permanent = game.getPermanent(newDefenderId);
if (permanent != null) {
defenderId = newDefenderId;
defendingPlayerId = permanent.getControllerId();
defenderIsPlaneswalker = true;
return true;
} else {
Player defender = game.getPlayer(newDefenderId);
if (defender != null) {
defenderId = newDefenderId;
defendingPlayerId = newDefenderId;
defenderIsPlaneswalker = false;
return true;
}
}
attacker.clearBandedCards();
}
return false;
Permanent permanent = game.getPermanent(newDefenderId);
if (permanent != null) {
defenderId = newDefenderId;
defendingPlayerId = permanent.isBattle(game) ? permanent.getProtectorId() : permanent.getControllerId();
defenderIsPermanent = true;
return true;
}
Player defender = game.getPlayer(newDefenderId);
if (defender == null) {
return false;
}
defenderId = newDefenderId;
defendingPlayerId = newDefenderId;
defenderIsPermanent = false;
return true;
}
/**

View file

@ -18,7 +18,10 @@ import mage.game.events.ZoneChangeEvent;
import mage.util.GameLog;
import mage.util.SubTypes;
import java.util.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class Commander implements CommandObject {
@ -272,6 +275,15 @@ public class Commander implements CommandObject {
public void setStartingLoyalty(int startingLoyalty) {
}
@Override
public int getStartingDefense() {
return sourceObject.getStartingDefense();
}
@Override
public void setStartingDefense(int startingDefense) {
}
@Override
public UUID getId() {
return sourceObject.getId();

View file

@ -317,6 +317,15 @@ public class Dungeon implements CommandObject {
public void setStartingLoyalty(int startingLoyalty) {
}
@Override
public int getStartingDefense() {
return 0;
}
@Override
public void setStartingDefense(int startingDefense) {
}
@Override
public UUID getId() {
return this.id;

View file

@ -84,7 +84,7 @@ public class Emblem implements CommandObject {
}
if (!availableImageSetCodes.isEmpty()) {
if (expansionSetCodeForImage.equals("") || !availableImageSetCodes.contains(expansionSetCodeForImage)) {
expansionSetCodeForImage = availableImageSetCodes.get(RandomUtil.nextInt(availableImageSetCodes.size()));
expansionSetCodeForImage = availableImageSetCodes.get(RandomUtil.nextInt(availableImageSetCodes.size()));
}
}
}
@ -233,6 +233,15 @@ public class Emblem implements CommandObject {
public void setStartingLoyalty(int startingLoyalty) {
}
@Override
public int getStartingDefense() {
return 0;
}
@Override
public void setStartingDefense(int startingDefense) {
}
@Override
public UUID getId() {
return this.id;

View file

@ -232,6 +232,15 @@ public class Plane implements CommandObject {
public void setStartingLoyalty(int startingLoyalty) {
}
@Override
public int getStartingDefense() {
return 0;
}
@Override
public void setStartingDefense(int startingDefense) {
}
@Override
public UUID getId() {
return this.id;

View file

@ -89,6 +89,14 @@ public interface Permanent extends Card, Controllable {
Set<UUID> getGoadingPlayers();
void chooseProtector(Game game, Ability source);
void setProtectorId(UUID playerId);
UUID getProtectorId();
boolean isProtectedBy(UUID playerId);
void setCardNumber(String cid);
void setExpansionSetCode(String expansionSetCode);
@ -292,6 +300,8 @@ public interface Permanent extends Card, Controllable {
boolean canBlockAny(Game game);
boolean canBeAttacked(UUID attackerId, UUID playerToAttack, Game game);
/**
* Checks by restriction effects if the permanent can use activated
* abilities

View file

@ -62,6 +62,7 @@ public class PermanentCard extends PermanentImpl {
power = card.getPower().copy();
toughness = card.getToughness().copy();
startingLoyalty = card.getStartingLoyalty();
startingDefense = card.getStartingDefense();
copyFromCard(card, game);
// if temporary added abilities to the spell/card exist, you need to add it to the permanent derived from that card
Abilities<Ability> otherAbilities = game.getState().getAllOtherAbilities(card.getId());

View file

@ -21,6 +21,7 @@ import mage.constants.*;
import mage.counters.Counter;
import mage.counters.CounterType;
import mage.counters.Counters;
import mage.filter.FilterOpponent;
import mage.game.Game;
import mage.game.GameState;
import mage.game.ZoneChangeInfo;
@ -33,9 +34,10 @@ import mage.game.permanent.token.SquirrelToken;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.TargetPlayer;
import mage.util.CardUtil;
import mage.util.GameLog;
import mage.util.RandomUtil;
import mage.util.ThreadLocalStringBuilder;
import org.apache.log4j.Logger;
@ -75,6 +77,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected final Set<UUID> goadingPlayers = new HashSet<>();
protected UUID originalControllerId;
protected UUID controllerId;
protected UUID protectorId = null;
protected UUID beforeResetControllerId;
protected int damage;
protected boolean controlledFromStartOfControllerTurn;
@ -174,6 +177,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.loyaltyActivationsAvailable = permanent.loyaltyActivationsAvailable;
this.legendRuleApplies = permanent.legendRuleApplies;
this.transformCount = permanent.transformCount;
this.protectorId = permanent.protectorId;
this.morphed = permanent.morphed;
this.manifested = permanent.manifested;
@ -482,7 +486,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
public void setLoyaltyActivationsAvailable(int setActivations) {
if(this.loyaltyActivationsAvailable < setActivations) {
if (this.loyaltyActivationsAvailable < setActivations) {
this.loyaltyActivationsAvailable = setActivations;
}
}
@ -991,6 +995,15 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
removeCounters(CounterType.LOYALTY.getName(), countersToRemove, source, game);
}
}
if (this.isBattle(game)) {
int defense = getCounters(game).getCount(CounterType.DEFENSE);
int countersToRemove = Math.min(actualDamage, defense);
if (attacker != null && markDamage) {
markDamage(CounterType.DEFENSE.createInstance(countersToRemove), attacker, false);
} else {
removeCounters(CounterType.DEFENSE.getName(), countersToRemove, source, game);
}
}
DamagedEvent damagedEvent = new DamagedPermanentEvent(this.getId(), attackerId, this.getControllerId(), actualDamage, combat);
damagedEvent.setExcess(actualDamage - lethal);
game.fireEvent(damagedEvent);
@ -1134,6 +1147,9 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
if (this.isPlaneswalker(game)) {
lethal = Math.min(lethal, this.getCounters(game).getCount(CounterType.LOYALTY));
}
if (this.isBattle(game)) {
lethal = Math.min(lethal, this.getCounters(game).getCount(CounterType.DEFENSE));
}
return lethal;
}
@ -1196,6 +1212,18 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.addCounters(CounterType.LOYALTY.createInstance(countersToAdd), source, game);
}
}
if (this.isBattle(game)) {
int defense;
if (this.getStartingDefense() == -2) {
defense = source.getManaCostsToPay().getX();
} else {
defense = this.getStartingDefense();
}
if (defense > 0) {
this.addCounters(CounterType.DEFENSE.createInstance(defense), source, game);
}
this.chooseProtector(game, source);
}
if (!fireEvent) {
return false;
}
@ -1381,8 +1409,23 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return canAttackInPrinciple(defenderId, game);
}
@Override
public boolean canBeAttacked(UUID attackerId, UUID playerToAttack, Game game) {
if (isPlaneswalker(game)) {
return isControlledBy(playerToAttack);
}
if (isBattle(game)) {
return isProtectedBy(playerToAttack);
}
return false;
}
@Override
public boolean canAttackInPrinciple(UUID defenderId, Game game) {
if (isBattle(game)) {
// battles can never attack
return false;
}
ApprovingObject approvingObject = game.getContinuousEffects().asThough(
this.objectId, AsThoughEffectType.ATTACK_AS_HASTE, null, defenderId, game
);
@ -1422,7 +1465,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
public boolean canBlock(UUID attackerId, Game game) {
if (tapped && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game)) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game) == null || isBattle(game)) {
return false;
}
Permanent attacker = game.getPermanent(attackerId);
@ -1528,7 +1571,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return game.getCombat().removeFromCombat(objectId, game, withEvent);
} else if (this.isPlaneswalker(game)) {
if (game.getCombat().getDefenders().contains(getId())) {
game.getCombat().removePlaneswalkerFromCombat(objectId, game);
game.getCombat().removeDefendingPermanentFromCombat(objectId, game);
}
}
return false;
@ -1629,6 +1672,40 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return goadingPlayers;
}
@Override
public void chooseProtector(Game game, Ability source) {
Set<UUID> opponents = game.getOpponents(this.getControllerId());
UUID protectorId;
if (opponents.size() > 1) {
TargetPlayer target = new TargetPlayer(new FilterOpponent("protector for " + getName()));
target.setNotTarget(true);
target.setRequired(true);
game.getPlayer(getControllerId()).choose(Outcome.Neutral, target, source, game);
protectorId = target.getFirstTarget();
} else {
protectorId = RandomUtil.randomFromCollection(opponents);
}
String protectorName = game.getPlayer(protectorId).getLogName();
game.informPlayers(protectorName + " has been chosen to protect " + this.getLogName());
this.addInfo("protector", "Protected by " + protectorName, game);
this.setProtectorId(protectorId);
}
@Override
public void setProtectorId(UUID protectorId) {
this.protectorId = protectorId;
}
@Override
public UUID getProtectorId() {
return protectorId;
}
@Override
public boolean isProtectedBy(UUID playerId) {
return protectorId != null && protectorId.equals(playerId);
}
@Override
public void setPairedCard(MageObjectReference pairedCard) {
this.pairedPermanent = pairedCard;

View file

@ -86,6 +86,7 @@ public class PermanentToken extends PermanentImpl {
this.supertype.addAll(token.getSuperType());
this.subtype.copyFrom(token.getSubtype(game));
this.startingLoyalty = token.getStartingLoyalty();
this.startingDefense = token.getStartingDefense();
// workaround for entersTheBattlefield replacement effects
if (this.abilities.containsClass(ChangelingAbility.class)) {
this.subtype.setIsAllCreatureTypes(true);

View file

@ -62,6 +62,7 @@ public class Spell extends StackObjectImpl implements Card {
private boolean resolving = false;
private UUID commandedBy = null; // for Word of Command
private int startingLoyalty;
private int startingDefense;
private ActivationManaAbilityStep currentActivatingManaAbilitiesStep = ActivationManaAbilityStep.BEFORE;
@ -73,7 +74,7 @@ public class Spell extends StackObjectImpl implements Card {
Card affectedCard = card;
// TODO: must be removed after transform cards (one side) migrated to MDF engine (multiple sides)
if (ability.getSpellAbilityCastMode() == SpellAbilityCastMode.DISTURB && affectedCard.getSecondCardFace() != null) {
if (ability.getSpellAbilityCastMode() == SpellAbilityCastMode.TRANSFORMED && affectedCard.getSecondCardFace() != null) {
// simulate another side as new card (another code part in continues effect from disturb ability)
affectedCard = TransformAbility.transformCardSpellStatic(card, card.getSecondCardFace(), game);
}
@ -83,6 +84,7 @@ public class Spell extends StackObjectImpl implements Card {
this.frameColor = affectedCard.getFrameColor(null).copy();
this.frameStyle = affectedCard.getFrameStyle();
this.startingLoyalty = affectedCard.getStartingLoyalty();
this.startingDefense = affectedCard.getStartingDefense();
this.id = ability.getId();
this.zoneChangeCounter = affectedCard.getZoneChangeCounter(game); // sync card's ZCC with spell (copy spell settings)
this.ability = ability;
@ -134,6 +136,7 @@ public class Spell extends StackObjectImpl implements Card {
this.currentActivatingManaAbilitiesStep = spell.currentActivatingManaAbilitiesStep;
this.targetChanged = spell.targetChanged;
this.startingLoyalty = spell.startingLoyalty;
this.startingDefense = spell.startingDefense;
}
public boolean activate(Game game, boolean noMana) {
@ -641,6 +644,16 @@ public class Spell extends StackObjectImpl implements Card {
this.startingLoyalty = startingLoyalty;
}
@Override
public int getStartingDefense() {
return startingDefense;
}
@Override
public void setStartingDefense(int startingDefense) {
this.startingDefense = startingDefense;
}
@Override
public UUID getId() {
return id;

View file

@ -16,7 +16,6 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects;
import mage.abilities.hint.Hint;
import mage.abilities.icon.CardIcon;
import mage.cards.Card;
import mage.cards.FrameStyle;
import mage.constants.*;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
@ -243,6 +242,15 @@ public class StackAbility extends StackObjectImpl implements Ability {
public void setStartingLoyalty(int startingLoyalty) {
}
@Override
public int getStartingDefense() {
return 0;
}
@Override
public void setStartingDefense(int startingDefense) {
}
@Override
public Zone getZone() {
return this.ability.getZone();

View file

@ -1,232 +1,32 @@
package mage.target.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.constants.Zone;
import mage.filter.Filter;
import mage.filter.common.FilterCreaturePlayerOrPlaneswalker;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetImpl;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import mage.filter.common.FilterAnyTarget;
/**
* @author JRHerlehy Created on 4/8/18.
*/
public class TargetAnyTarget extends TargetImpl {
public class TargetAnyTarget extends TargetPermanentOrPlayer {
protected FilterCreaturePlayerOrPlaneswalker filter;
private static final FilterAnyTarget filter = new FilterAnyTarget();
public TargetAnyTarget() {
this(1, 1, new FilterCreaturePlayerOrPlaneswalker());
this(1);
}
public TargetAnyTarget(int numTargets) {
this(numTargets, numTargets, new FilterCreaturePlayerOrPlaneswalker());
this(numTargets, numTargets);
}
public TargetAnyTarget(FilterCreaturePlayerOrPlaneswalker filter) {
this(1, 1, filter);
public TargetAnyTarget(int minNumTargets, int maxNumTargets) {
super(minNumTargets, maxNumTargets, filter, false);
}
public TargetAnyTarget(int numTargets, int maxNumTargets) {
this(numTargets, maxNumTargets, new FilterCreaturePlayerOrPlaneswalker());
}
public TargetAnyTarget(int minNumTargets, int maxNumTargets, FilterCreaturePlayerOrPlaneswalker filter) {
this.minNumberOfTargets = minNumTargets;
this.maxNumberOfTargets = maxNumTargets;
this.zone = Zone.ALL;
this.filter = filter;
this.targetName = filter.getMessage();
}
public TargetAnyTarget(final TargetAnyTarget target) {
protected TargetAnyTarget(final TargetAnyTarget target) {
super(target);
this.filter = target.filter.copy();
}
@Override
public Filter getFilter() {
return this.filter;
}
@Override
public boolean canTarget(UUID id, Game game) {
Permanent permanent = game.getPermanent(id);
if (permanent != null) {
return filter.match(permanent, game);
}
Player player = game.getPlayer(id);
return filter.match(player, game);
}
@Override
public boolean canTarget(UUID id, Ability source, Game game) {
return canTarget(source.getControllerId(), id, source, game);
}
@Override
public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) {
Permanent permanent = game.getPermanent(id);
Player player = game.getPlayer(id);
if (source != null) {
MageObject targetSource = game.getObject(source);
if (permanent != null) {
return permanent.canBeTargetedBy(targetSource, source.getControllerId(), game) && filter.match(permanent, source.getControllerId(), source, game);
}
if (player != null) {
return player.canBeTargetedBy(targetSource, source.getControllerId(), game) && filter.match(player, game);
}
}
if (permanent != null) {
return filter.match(permanent, game);
}
return filter.match(player, game);
}
/**
* Checks if there are enough {@link Permanent} or {@link Player} that can
* be chosen. Should only be used for Ability targets since this checks for
* protection, shroud etc.
*
* @param sourceControllerId - controller of the target event source
* @param source
* @param game
* @return - true if enough valid {@link Permanent} or {@link Player} exist
*/
@Override
public boolean canChoose(UUID sourceControllerId, Ability source, Game game) {
int count = 0;
MageObject targetSource = game.getObject(source);
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null && player.canBeTargetedBy(targetSource, sourceControllerId, game) && filter.match(player, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) {
if (permanent.canBeTargetedBy(targetSource, sourceControllerId, game) && filter.match(permanent, sourceControllerId, source, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
return false;
}
/**
* Checks if there are enough {@link Permanent} or {@link Player} that can
* be selected. Should not be used for Ability targets since this does not
* check for protection, shroud etc.
*
* @param sourceControllerId - controller of the select event
* @param game
* @return - true if enough valid {@link Permanent} or {@link Player} exist
*/
@Override
public boolean canChoose(UUID sourceControllerId, Game game) {
int count = 0;
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (filter.match(player, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) {
if (filter.match(permanent, sourceControllerId, null, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
return false;
}
@Override
public Set<UUID> possibleTargets(UUID sourceControllerId, Ability source, Game game) {
Set<UUID> possibleTargets = new HashSet<>();
MageObject targetSource = game.getObject(source);
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null
&& player.canBeTargetedBy(targetSource, sourceControllerId, game)
&& filter.match(player, sourceControllerId, source, game)) {
possibleTargets.add(playerId);
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) {
if (permanent.canBeTargetedBy(targetSource, sourceControllerId, game)
&& filter.match(permanent, sourceControllerId, source, game)) {
possibleTargets.add(permanent.getId());
}
}
return possibleTargets;
}
@Override
public Set<UUID> possibleTargets(UUID sourceControllerId, Game game) {
Set<UUID> possibleTargets = new HashSet<>();
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (filter.match(player, game)) {
possibleTargets.add(playerId);
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter.getPermanentFilter(), sourceControllerId, game)) {
if (filter.getPermanentFilter().match(permanent, sourceControllerId, null, game)) {
possibleTargets.add(permanent.getId());
}
}
return possibleTargets;
}
@Override
public String getTargetedName(Game game) {
StringBuilder sb = new StringBuilder();
for (UUID targetId : getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
sb.append(permanent.getLogName()).append(' ');
} else {
Player player = game.getPlayer(targetId);
if (player != null) {
sb.append(player.getLogName()).append(' ');
}
}
}
return sb.toString().trim();
}
@Override
public TargetAnyTarget copy() {
return new TargetAnyTarget(this);
}
}

View file

@ -3,15 +3,16 @@ package mage.target.common;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.constants.Zone;
import mage.filter.common.FilterCreaturePlayerOrPlaneswalker;
import mage.filter.common.FilterAnyTarget;
import mage.filter.common.FilterPermanentOrPlayer;
/**
* @author BetaSteward_at_googlemail.com
*/
public class TargetAnyTargetAmount extends TargetPermanentOrPlayerAmount {
private static final FilterCreaturePlayerOrPlaneswalker defaultFilter
= new FilterCreaturePlayerOrPlaneswalker("targets");
private static final FilterPermanentOrPlayer defaultFilter
= new FilterAnyTarget("targets");
public TargetAnyTargetAmount(int amount) {
this(amount, 0);

View file

@ -1,210 +1,25 @@
package mage.target.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.constants.Zone;
import mage.filter.Filter;
import mage.filter.StaticFilters;
import mage.filter.common.FilterPlaneswalkerOrPlayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetImpl;
import mage.filter.common.FilterDefender;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
* @author BetaSteward_at_googlemail.com
*/
public class TargetDefender extends TargetImpl {
public class TargetDefender extends TargetPermanentOrPlayer {
protected final FilterPlaneswalkerOrPlayer filter;
protected final UUID attackerId;
public TargetDefender(Set<UUID> defenders, UUID attackerId) {
this(1, 1, defenders, attackerId);
public TargetDefender(Set<UUID> defenders) {
super(1, 1, new FilterDefender(defenders), true);
}
public TargetDefender(int numTargets, Set<UUID> defenders, UUID attackerId) {
this(numTargets, numTargets, defenders, attackerId);
}
public TargetDefender(int minNumTargets, int maxNumTargets, Set<UUID> defenders, UUID attackerId) {
this.minNumberOfTargets = minNumTargets;
this.maxNumberOfTargets = maxNumTargets;
this.zone = Zone.ALL;
this.filter = new FilterPlaneswalkerOrPlayer(defenders);
this.targetName = filter.getMessage();
this.attackerId = attackerId;
this.notTarget = true;
}
public TargetDefender(final TargetDefender target) {
private TargetDefender(final TargetDefender target) {
super(target);
this.filter = target.filter.copy();
this.attackerId = target.attackerId;
}
@Override
public Filter getFilter() {
return this.filter;
}
@Override
public boolean canChoose(UUID sourceControllerId, Ability source, Game game) {
int count = 0;
MageObject targetSource = game.getObject(source);
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null
&& (notTarget || player.canBeTargetedBy(targetSource, sourceControllerId, game))
&& filter.match(player, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_PLANESWALKER, sourceControllerId, game)) {
if ((notTarget
|| permanent.canBeTargetedBy(targetSource, sourceControllerId, game))
&& filter.match(permanent, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
return false;
}
@Override
public boolean canChoose(UUID sourceControllerId, Game game) {
int count = 0;
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null
&& filter.match(player, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_PLANESWALKER, sourceControllerId, game)) {
if (filter.match(permanent, game)) {
count++;
if (count >= this.minNumberOfTargets) {
return true;
}
}
}
return false;
}
@Override
public Set<UUID> possibleTargets(UUID sourceControllerId, Ability source, Game game) {
if (source == null) {
return possibleTargets(sourceControllerId, game);
}
Set<UUID> possibleTargets = new HashSet<>();
MageObject targetSource = game.getObject(source);
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null
&& (notTarget
|| player.canBeTargetedBy(targetSource, sourceControllerId, game))
&& filter.match(player, game)) {
possibleTargets.add(playerId);
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_PLANESWALKER, sourceControllerId, game)) {
if ((notTarget
|| permanent.canBeTargetedBy(targetSource, sourceControllerId, game))
&& filter.match(permanent, game)) {
possibleTargets.add(permanent.getId());
}
}
return possibleTargets;
}
@Override
public Set<UUID> possibleTargets(UUID sourceControllerId, Game game) {
Set<UUID> possibleTargets = new HashSet<>();
for (UUID playerId : game.getState().getPlayersInRange(sourceControllerId, game)) {
Player player = game.getPlayer(playerId);
if (player != null
&& filter.match(player, game)) {
possibleTargets.add(playerId);
}
}
for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_PLANESWALKER, sourceControllerId, game)) {
if (filter.match(permanent, game)) {
possibleTargets.add(permanent.getId());
}
}
return possibleTargets;
}
@Override
public String getTargetedName(Game game) {
StringBuilder sb = new StringBuilder();
for (UUID targetId : getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
sb.append(permanent.getName()).append(' ');
} else {
Player player = game.getPlayer(targetId);
sb.append(player.getLogName()).append(' ');
}
}
return sb.toString().trim();
}
@Override
public boolean canTarget(UUID id, Game game) {
Player player = game.getPlayer(id);
if (player != null) {
return filter.match(player, game);
}
Permanent permanent = game.getPermanent(id);
return permanent != null
&& filter.match(permanent, game);
}
@Override
public boolean canTarget(UUID id, Ability source, Game game) {
Player player = game.getPlayer(id);
MageObject targetSource = game.getObject(attackerId);
if (player != null) {
return (notTarget
|| player.canBeTargetedBy(targetSource, (source == null ? null : source.getControllerId()), game))
&& filter.match(player, game);
}
Permanent permanent = game.getPermanent(id); // planeswalker
if (permanent != null) {
//Could be targeting due to combat decision to attack a player or planeswalker.
UUID controllerId = null;
if (source != null) {
controllerId = source.getControllerId();
}
return (notTarget
|| permanent.canBeTargetedBy(targetSource, controllerId, game))
&& filter.match(permanent, game);
}
return false;
}
@Override
public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) {
return canTarget(id, source, game);
}
@Override
public TargetDefender copy() {
return new TargetDefender(this);
}
}

View file

@ -1726,14 +1726,14 @@ public final class CardUtil {
return i == null ? 1 : Integer.sum(i, 1);
}
public static String convertStartingLoyalty(int startingLoyalty) {
switch (startingLoyalty) {
public static String convertLoyaltyOrDefense(int value) {
switch (value) {
case -2:
return "X";
case -1:
return "";
default:
return "" + startingLoyalty;
return "" + value;
}
}

View file

@ -96,6 +96,7 @@ public class CopyTokenFunction implements Function<Token, Card> {
target.setPower(sourceObj.getPower().getBaseValue());
target.setToughness(sourceObj.getToughness().getBaseValue());
target.setStartingLoyalty(sourceObj.getStartingLoyalty());
target.setStartingDefense(sourceObj.getStartingDefense());
return target;
}
@ -114,6 +115,7 @@ public class CopyTokenFunction implements Function<Token, Card> {
target.setZoneChangeCounter(spell.getZoneChangeCounter(game), game);
// Copy starting loyalty from spell (Ob Nixilis, the Adversary)
target.setStartingLoyalty(spell.getStartingLoyalty());
target.setStartingDefense(spell.getStartingDefense());
} else {
target.setZoneChangeCounter(source.getZoneChangeCounter(game), game);
}