foul-magics/Mage/src/main/java/mage/game/permanent/PermanentImpl.java
Oleg Agafonov e38a79f231
Face down images and cards rework (#11873)
Face down changes:
* GUI: added visible face down type and real card name for controller/owner (opponent can see it after game ends);
* GUI: added day/night button to view real card for controller/owner (opponent can see it after game ends);
* game: fixed that faced-down card can render symbols, abilities and other hidden data from a real card;
* images: added image support for normal faced-down cards;
* images: added image support for morph and megamorph faced-down cards;
* images: added image support for foretell faced-down cards;

Other changes:
* images: fixed missing tokens from DDD set;
* images: no more client restart to apply newly downloaded images or render settings;
* images: improved backface image quality (use main menu -> symbols to download it);
2024-02-29 01:14:54 +04:00

2065 lines
76 KiB
Java

package mage.game.permanent;
import mage.ApprovingObject;
import mage.MageObject;
import mage.MageObjectReference;
import mage.ObjectColor;
import mage.abilities.Abilities;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.Effect;
import mage.abilities.effects.RequirementEffect;
import mage.abilities.effects.RestrictionEffect;
import mage.abilities.effects.common.RegenerateSourceEffect;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.abilities.hint.HintUtils;
import mage.abilities.keyword.*;
import mage.cards.Card;
import mage.cards.CardImpl;
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;
import mage.game.ZonesHandler;
import mage.game.combat.CombatGroup;
import mage.game.command.CommandObject;
import mage.game.events.*;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.token.SquirrelToken;
import mage.game.permanent.token.Token;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player;
import mage.target.TargetPlayer;
import mage.util.CardUtil;
import mage.util.GameLog;
import mage.util.ThreadLocalStringBuilder;
import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
/**
* @author BetaSteward_at_googlemail.com
*/
public abstract class PermanentImpl extends CardImpl implements Permanent {
private static final Logger logger = Logger.getLogger(PermanentImpl.class);
private static class MarkedDamageInfo implements Serializable {
private final Counter counter;
private final MageObject sourceObject;
private final boolean addCounters;
private MarkedDamageInfo(Counter counter, MageObject sourceObject, boolean addCounters) {
this.counter = counter;
this.sourceObject = sourceObject;
this.addCounters = addCounters;
}
}
private static final ThreadLocalStringBuilder threadLocalBuilder = new ThreadLocalStringBuilder(300);
protected boolean tapped;
protected boolean flipped;
protected boolean transformed;
protected boolean monstrous;
protected boolean renowned;
protected boolean suspected;
protected boolean manifested = false;
protected boolean morphed = false;
protected boolean ringBearerFlag = false;
protected boolean canBeSacrificed = true;
protected int classLevel = 1;
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;
protected int turnsOnBattlefield;
protected boolean phasedIn = true;
protected boolean indirectPhase = false;
protected boolean faceDown;
protected boolean attacking;
protected int blocking;
// number of creatures the permanent can block
protected int maxBlocks = 1;
// minimal number of creatures the creature can be blocked by
protected int minBlockedBy = 1;
// maximal number of creatures the creature can be blocked by 0 = no restriction
protected int maxBlockedBy = 0;
protected boolean deathtouched;
protected boolean solved = false;
protected Map<String, List<UUID>> connectedCards = new HashMap<>();
protected Set<MageObjectReference> dealtDamageByThisTurn;
protected UUID attachedTo;
protected int attachedToZoneChangeCounter;
protected MageObjectReference pairedPermanent;
protected List<UUID> bandedCards = new ArrayList<>();
protected Counters counters;
protected List<MarkedDamageInfo> markedDamage;
protected int markedLifelink;
protected int timesLoyaltyUsed = 0;
protected int loyaltyActivationsAvailable = 1;
protected int transformCount = 0;
protected Map<String, String> info = new LinkedHashMap<>(); // additional info for permanent's rules
protected int createOrder;
protected boolean legendRuleApplies = true;
protected boolean prototyped;
private static final List<UUID> emptyList = Collections.unmodifiableList(new ArrayList<>());
protected PermanentImpl(UUID ownerId, UUID controllerId, String name) {
super(ownerId, name);
// runtime check: need controller (if you catch it in non-game then use random uuid)
if (controllerId == null) {
throw new IllegalArgumentException("Wrong code usage: controllerId can't be null - " + name, new Throwable());
}
this.originalControllerId = controllerId;
this.controllerId = controllerId;
this.counters = new Counters();
}
protected PermanentImpl(UUID id, UUID ownerId, UUID controllerId, String name) {
super(id, ownerId, name);
this.originalControllerId = controllerId;
this.controllerId = controllerId;
this.counters = new Counters();
}
protected PermanentImpl(final PermanentImpl permanent) {
super(permanent);
this.tapped = permanent.tapped;
this.flipped = permanent.flipped;
this.originalControllerId = permanent.originalControllerId;
this.controllerId = permanent.controllerId;
this.damage = permanent.damage;
this.controlledFromStartOfControllerTurn = permanent.controlledFromStartOfControllerTurn;
this.turnsOnBattlefield = permanent.turnsOnBattlefield;
this.phasedIn = permanent.phasedIn;
this.indirectPhase = permanent.indirectPhase;
this.faceDown = permanent.faceDown;
this.attacking = permanent.attacking;
this.blocking = permanent.blocking;
this.maxBlocks = permanent.maxBlocks;
this.deathtouched = permanent.deathtouched;
this.solved = permanent.solved;
this.markedLifelink = permanent.markedLifelink;
this.connectedCards = CardUtil.deepCopyObject(permanent.connectedCards);
this.dealtDamageByThisTurn = CardUtil.deepCopyObject(permanent.dealtDamageByThisTurn);
if (permanent.markedDamage != null) {
markedDamage = new ArrayList<>();
for (MarkedDamageInfo mdi : permanent.markedDamage) {
markedDamage.add(new MarkedDamageInfo(mdi.counter.copy(), mdi.sourceObject, mdi.addCounters));
}
}
this.info.putAll(permanent.info);
this.counters = permanent.counters.copy();
this.attachedTo = permanent.attachedTo;
this.attachedToZoneChangeCounter = permanent.attachedToZoneChangeCounter;
this.minBlockedBy = permanent.minBlockedBy;
this.maxBlockedBy = permanent.maxBlockedBy;
this.transformed = permanent.transformed;
this.monstrous = permanent.monstrous;
this.renowned = permanent.renowned;
this.suspected = permanent.suspected;
this.ringBearerFlag = permanent.ringBearerFlag;
this.classLevel = permanent.classLevel;
this.goadingPlayers.addAll(permanent.goadingPlayers);
this.pairedPermanent = permanent.pairedPermanent;
this.bandedCards.addAll(permanent.bandedCards);
this.timesLoyaltyUsed = permanent.timesLoyaltyUsed;
this.loyaltyActivationsAvailable = permanent.loyaltyActivationsAvailable;
this.legendRuleApplies = permanent.legendRuleApplies;
this.transformCount = permanent.transformCount;
this.protectorId = permanent.protectorId;
this.morphed = permanent.morphed;
this.manifested = permanent.manifested;
this.createOrder = permanent.createOrder;
this.prototyped = permanent.prototyped;
this.canBeSacrificed = permanent.canBeSacrificed;
}
@Override
public String toString() {
String name = getName().isEmpty()
? "face down" + " [" + getId().toString().substring(0, 3) + "]"
: getIdName();
String imageInfo = getExpansionSetCode()
+ ":" + getCardNumber()
+ ":" + getImageFileName()
+ ":" + getImageNumber();
return name
+ ", " + (getBasicMageObject() instanceof Token ? "T" : "C")
+ ", " + getBasicMageObject().getClass().getSimpleName()
+ ", " + imageInfo
+ ", " + this.getPower() + "/" + this.getToughness()
+ (this.isCopy() ? ", copy" : "")
+ (this.isTapped() ? ", tapped" : "");
}
@Override
public void setControllerId(UUID controllerId) {
this.controllerId = controllerId;
abilities.setControllerId(controllerId);
}
@Override
public void setOriginalControllerId(UUID originalControllerId) {
this.originalControllerId = originalControllerId;
}
/**
* Called before each applyEffects or if after a permanent was copied for
* the copied object
*
* @param game
*/
@Override
public void reset(Game game) {
this.resetControl();
this.maxBlocks = 1;
this.minBlockedBy = 1;
this.maxBlockedBy = 0;
this.copy = false;
this.goadingPlayers.clear();
this.loyaltyActivationsAvailable = 1;
this.legendRuleApplies = true;
this.canBeSacrificed = true;
}
@Override
public String getName() {
if (name.isEmpty()) {
if (faceDown) {
return EmptyNames.FACE_DOWN_CREATURE.toString();
} else {
return "";
}
} else {
return name;
}
}
@Override
public String getValue(GameState state) {
StringBuilder sb = threadLocalBuilder.get();
sb.append(controllerId).append(getName()).append(tapped).append(damage);
sb.append(subtype).append(supertype).append(power.getValue()).append(toughness.getValue());
sb.append(abilities.getValue());
for (Counter counter : getCounters(state).values()) {
sb.append(counter.getName()).append(counter.getCount());
}
return sb.toString();
}
@Override
public void addInfo(String key, String value, Game game) {
if (info == null) {
info = new HashMap<>();
}
if (value == null || value.isEmpty()) {
info.remove(key);
} else {
info.put(key, value);
}
}
/**
* @param game can be null, e.g. for cards viewer
* @return
*/
@Override
final public List<String> getRules(Game game) {
try {
List<String> rules = super.getRules(game);
// add additional data for GUI
// info
if (info != null) {
rules.addAll(info.values());
}
if (game == null || game.getPhase() == null) {
// dynamic hints for started game only
return rules;
}
// ability hints already collected in super call
// warning, if you add new icon type for restriction or requirement then don't forget
// to add it for card icon too (search CardIconType.OTHER_HAS_RESTRICTIONS)
// restrict hints
List<String> restrictHints = new ArrayList<>();
if (HintUtils.RESTRICT_HINTS_ENABLE) {
// restrict
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRestrictionEffects(this, game).entrySet()) {
for (Ability ability : entry.getValue()) {
if (!entry.getKey().canAttack(game, false) || !entry.getKey().canAttack(this, null, ability, game, false)) {
restrictHints.add(HintUtils.prepareText("Can't attack" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_RESTRICT));
}
if (!entry.getKey().canBlock(null, this, ability, game, false)) {
restrictHints.add(HintUtils.prepareText("Can't block" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_RESTRICT));
}
if (!entry.getKey().canBeUntapped(this, ability, game, false)) {
restrictHints.add(HintUtils.prepareText("Can't untapped" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_RESTRICT));
}
if (!entry.getKey().canUseActivatedAbilities(this, ability, game, false)) {
restrictHints.add(HintUtils.prepareText("Can't use activated abilities" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_RESTRICT));
}
if (!entry.getKey().canTransform(game, false)) {
restrictHints.add(HintUtils.prepareText("Can't transform" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_RESTRICT));
}
}
}
// requirement hints
for (Map.Entry<RequirementEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRequirementEffects(this, false, game).entrySet()) {
for (Ability ability : entry.getValue()) {
if (entry.getKey().mustAttack(game)) {
restrictHints.add(HintUtils.prepareText("Must attack" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_REQUIRE));
}
if (entry.getKey().mustBlock(game)) {
restrictHints.add(HintUtils.prepareText("Must block" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_REQUIRE));
}
if (entry.getKey().mustBlockAny(game)) {
restrictHints.add(HintUtils.prepareText("Must block any" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_REQUIRE));
}
if (entry.getKey().mustBlockAllAttackers(game)) {
restrictHints.add(HintUtils.prepareText("Must block all attackers" + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_REQUIRE));
}
MageObject object = game.getObject(entry.getKey().mustAttackDefender(ability, game));
if (object != null) {
restrictHints.add(HintUtils.prepareText("Must attack defender " + object.getLogName() + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_REQUIRE));
}
Player player = game.getPlayer(entry.getKey().mustAttackDefender(ability, game));
if (player != null) {
restrictHints.add(HintUtils.prepareText(
"Must attack defender " + player.getLogName() + addSourceObjectName(game, ability),
null, HintUtils.HINT_ICON_REQUIRE));
}
object = game.getObject(entry.getKey().mustBlockAttacker(ability, game));
if (object != null) {
restrictHints.add(HintUtils.prepareText("Must block attacker " + object.getLogName() + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_REQUIRE));
}
object = game.getObject(entry.getKey().mustBlockAttackerIfElseUnblocked(ability, game));
if (object != null) {
restrictHints.add(HintUtils.prepareText("Must block attacker if able " + object.getLogName() + addSourceObjectName(game, ability), null, HintUtils.HINT_ICON_REQUIRE));
}
}
}
// goaded hints
for (UUID playerId : getGoadingPlayers()) {
Player player = game.getPlayer(playerId);
if (player != null) {
restrictHints.add(HintUtils.prepareText("Goaded by " + player.getLogName() + " (must attack)", null, HintUtils.HINT_ICON_REQUIRE));
}
}
restrictHints.sort(String::compareTo);
}
// total hints
if (!restrictHints.isEmpty()) {
if (rules.stream().noneMatch(s -> s.contains(HintUtils.HINT_START_MARK))) {
rules.add(HintUtils.HINT_START_MARK);
}
HintUtils.appendHints(rules, restrictHints);
}
return rules;
} catch (Exception e) {
return CardUtil.RULES_ERROR_INFO;
}
}
private String addSourceObjectName(Game game, Ability ability) {
if (ability != null) {
MageObject object = game.getObject(ability.getSourceId());
if (object != null) {
return " (" + object.getIdName() + ")";
}
}
return "";
}
@Override
public Abilities<Ability> getAbilities() {
return super.getAbilities();
}
@Override
public Abilities<Ability> getAbilities(Game game) {
return super.getAbilities(game);
}
/**
* Add an ability to the permanent. When copying from an existing source
* you should use the fromExistingObject variant of this function to prevent double-copying subabilities
*
* @param ability The ability to be added
* @param sourceId id of the source doing the added (for the effect created to add it)
* @param game
* @return The newly added ability copy
*/
@Override
public Ability addAbility(Ability ability, UUID sourceId, Game game) {
return addAbility(ability, sourceId, game, false);
}
/**
* @param ability The ability to be added
* @param sourceId id of the source doing the added (for the effect created to add it)
* @param game
* @param fromExistingObject if copying abilities from an existing source then must ignore sub-abilities because they're already on the source object
* Otherwise sub-abilities will be added twice to the resulting object
* @return The newly added ability copy
*/
@Override
public Ability addAbility(Ability ability, UUID sourceId, Game game, boolean fromExistingObject) {
// singleton abilities -- only one instance
// other abilities -- any amount of instances
if (!abilities.containsKey(ability.getId())) {
Ability copyAbility = ability.copy();
copyAbility.newId(); // needed so that source can get an ability multiple times (e.g. Raging Ravine)
copyAbility.setControllerId(controllerId);
copyAbility.setSourceId(objectId);
// triggered abilities must be added to the state().triggers
// still as long as the prev. permanent is known to the LKI (e.g. Showstopper) so gained dies triggered ability will trigger
if (game != null) {
// game is null in cards viewer window (MageBook)
game.getState().addAbility(copyAbility, sourceId, this);
}
abilities.add(copyAbility);
if (!fromExistingObject) {
abilities.addAll(copyAbility.getSubAbilities());
}
return copyAbility;
}
return null;
}
@Override
public void removeAllAbilities(UUID sourceId, Game game) {
// TODO: what about triggered abilities? See addAbility above -- triggers adds to GameState
abilities.clear();
}
@Override
public void removeAbility(Ability abilityToRemove, UUID sourceId, Game game) {
if (abilityToRemove == null) {
return;
}
// 112.10b Effects that remove an ability remove all instances of it.
List<Ability> toRemove = new ArrayList<>();
abilities.forEach(a -> {
if (a.isSameInstance(abilityToRemove)) {
toRemove.add(a);
}
});
// TODO: what about triggered abilities? See addAbility above -- triggers adds to GameState
toRemove.forEach(r -> abilities.remove(r));
}
@Override
public void removeAbilities(List<Ability> abilitiesToRemove, UUID sourceId, Game game) {
if (abilitiesToRemove == null) {
return;
}
abilitiesToRemove.forEach(a -> removeAbility(a, sourceId, game));
}
@Override
public Counters getCounters(Game game) {
return counters;
}
@Override
public Counters getCounters(GameState state) {
return counters;
}
@Override
public UUID getControllerOrOwnerId() {
return controllerId;
}
@Override
public int getTurnsOnBattlefield() {
return turnsOnBattlefield;
}
@Override
public void beginningOfTurn(Game game) {
if (game.isActivePlayer(this.controllerId)) {
this.controlledFromStartOfControllerTurn = true;
}
}
@Override
public void endOfTurn(Game game) {
this.damage = 0;
this.timesLoyaltyUsed = 0;
this.turnsOnBattlefield++;
this.deathtouched = false;
dealtDamageByThisTurn = null;
for (Ability ability : this.abilities) {
ability.reset(game);
}
}
@Override
public void incrementLoyaltyActivationsAvailable() {
this.incrementLoyaltyActivationsAvailable(Integer.MAX_VALUE);
}
@Override
public void incrementLoyaltyActivationsAvailable(int max) {
if (this.loyaltyActivationsAvailable < max) {
this.loyaltyActivationsAvailable++;
}
}
@Override
public void setLoyaltyActivationsAvailable(int setActivations) {
if (this.loyaltyActivationsAvailable < setActivations) {
this.loyaltyActivationsAvailable = setActivations;
}
}
@Override
public void addLoyaltyUsed() {
this.timesLoyaltyUsed++;
}
@Override
public boolean canLoyaltyBeUsed(Game game) {
Player controller = game.getPlayer(controllerId);
if (controller != null) {
return loyaltyActivationsAvailable > timesLoyaltyUsed;
}
return false;
}
@Override
public void setLegendRuleApplies(boolean legendRuleApplies) {
this.legendRuleApplies = legendRuleApplies;
}
@Override
public boolean legendRuleApplies() {
return this.legendRuleApplies;
}
@Override
public boolean isTapped() {
return tapped;
}
@Override
public void setTapped(boolean tapped) {
this.tapped = tapped;
}
@Override
public boolean canTap(Game game) {
return !isCreature(game) || !hasSummoningSickness();
}
@Override
public boolean untap(Game game) {
//20091005 - 701.15b
if (tapped && !replaceEvent(EventType.UNTAP, game)) {
this.tapped = false;
UntappedEvent event = new UntappedEvent(
objectId, this.controllerId,
// Since triggers are not checked until the next step,
// we use the event flag to know if untapping was done during the untap step
game.getTurnStepType() == PhaseStep.UNTAP
);
game.fireEvent(event);
game.getState().addSimultaneousUntapped(event, game);
return true;
}
return false;
}
@Override
public boolean tap(Ability source, Game game) {
return tap(false, source, game);
}
@Override
public boolean tap(boolean forCombat, Ability source, Game game) {
//20091005 - 701.15a
if (!tapped && !replaceEvent(EventType.TAP, game)) {
this.tapped = true;
TappedEvent event = new TappedEvent(objectId, source, source == null ? null : source.getControllerId(), forCombat);
game.fireEvent(event);
game.getState().addSimultaneousTapped(event, game);
return true;
}
return false;
}
@Override
public void setFaceDown(boolean value, Game game) {
this.faceDown = value;
}
@Override
public boolean isFaceDown(Game game) {
return faceDown;
}
@Override
public boolean isFlipped() {
return flipped;
}
@Override
public boolean flip(Game game) {
if (!flipped && !replaceEvent(EventType.FLIP, game)) {
this.flipped = true;
fireEvent(EventType.FLIPPED, game);
return true;
}
return false;
}
@Override
public boolean transform(Ability source, Game game) {
return this.transform(source, game, false);
}
private boolean checkDayNightBound() {
return this.getAbilities().containsClass(DayboundAbility.class)
|| this.getAbilities().containsClass(NightboundAbility.class);
}
private boolean checkTransformRestrictionEffects(Game game) {
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRestrictionEffects(this, game).entrySet()) {
if (!entry.getKey().canTransform(game, true)) {
return false;
}
}
return true;
}
public MageObject getOtherFace() {
return transformed ? this.getMainCard() : this.getMainCard().getSecondCardFace();
}
@Override
public boolean transform(Ability source, Game game, boolean ignoreDayNight) {
if (!this.isTransformable()
|| (!ignoreDayNight && this.checkDayNightBound())
|| this.getOtherFace().isInstantOrSorcery()
|| !this.checkTransformRestrictionEffects(game)
|| (source != null && !source.checkTransformCount(this, game))) {
return false;
}
if (this.transformed) {
Card orgCard = this.getMainCard();
this.getPower().setModifiedBaseValue(orgCard.getPower().getValue());
this.getToughness().setModifiedBaseValue(orgCard.getToughness().getValue());
}
game.informPlayers(this.getLogName() + " transforms into " + this.getOtherFace().getLogName()
+ CardUtil.getSourceLogName(game, source, this.getId()));
this.setTransformed(!this.transformed);
this.transformCount++;
game.applyEffects();
this.replaceEvent(EventType.TRANSFORMING, game);
game.addSimultaneousEvent(GameEvent.getEvent(EventType.TRANSFORMED, this.getId(), this.getControllerId()));
return true;
}
@Override
public int getTransformCount() {
return transformCount;
}
@Override
public boolean isPhasedIn() {
return phasedIn;
}
@Override
public boolean isPhasedOutIndirectly() {
return !phasedIn && indirectPhase;
}
@Override
public boolean phaseIn(Game game) {
return phaseIn(game, true);
}
@Override
public boolean phaseIn(Game game, boolean onlyDirect) {
if (!phasedIn && !replaceEvent(EventType.PHASE_IN, game) && (!onlyDirect || !indirectPhase)) {
this.phasedIn = true;
this.indirectPhase = false;
game.informPlayers(getLogName() + " phased in");
for (UUID attachedId : this.getAttachments()) {
Permanent attachedPerm = game.getPermanent(attachedId);
if (attachedPerm != null) {
attachedPerm.phaseIn(game, false);
}
}
game.addSimultaneousEvent(GameEvent.getEvent(EventType.PHASED_IN, this.objectId, null, this.controllerId));
return true;
}
return false;
}
@Override
public boolean phaseOut(Game game) {
return phaseOut(game, false);
}
@Override
public boolean phaseOut(Game game, boolean indirectPhase) {
if (phasedIn && !replaceEvent(EventType.PHASE_OUT, game)) {
for (UUID attachedId : this.getAttachments()) {
Permanent attachedPerm = game.getPermanent(attachedId);
if (attachedPerm != null) {
attachedPerm.phaseOut(game, true);
}
}
this.removeFromCombat(game);
this.phasedIn = false;
this.indirectPhase = indirectPhase;
game.informPlayers(getLogName() + " phased out");
fireEvent(EventType.PHASED_OUT, game);
return true;
}
return false;
}
public void removeSummoningSickness() {
this.controlledFromStartOfControllerTurn = true;
}
@Override
public boolean wasControlledFromStartOfControllerTurn() {
return this.controlledFromStartOfControllerTurn;
}
@Override
public boolean hasSummoningSickness() {
return !(this.controlledFromStartOfControllerTurn || this.abilities.containsKey(HasteAbility.getInstance().getId()));
}
@Override
public boolean isAttacking() {
return attacking;
}
@Override
public boolean isBlocked(Game game) {
for (CombatGroup combatGroup : game.getCombat().getGroups()) {
if (combatGroup.getBlocked() && combatGroup.getAttackers().contains(this.getId())) {
return true;
}
}
return false;
}
@Override
public int getBlocking() {
return blocking;
}
@Override
public int getMaxBlocks() {
return maxBlocks;
}
@Override
public int getMinBlockedBy() {
return minBlockedBy;
}
@Override
public int getMaxBlockedBy() {
return maxBlockedBy;
}
@Override
public UUID getControllerId() {
return this.controllerId;
}
@Override
public void resetControl() {
this.beforeResetControllerId = this.controllerId;
this.controllerId = this.originalControllerId;
}
@Override
public boolean changeControllerId(UUID newControllerId, Game game, Ability source) {
Player newController = game.getPlayer(newControllerId);
if (newController == null || !newController.isInGame()) {
return false;
}
// For each control change compared to last controller send a GAIN_CONTROL replace event to be able to prevent the gain control (e.g. Guardian Beast)
if (beforeResetControllerId != newControllerId) {
GameEvent gainControlEvent = GameEvent.getEvent(GameEvent.EventType.GAIN_CONTROL, this.getId(), null, newControllerId);
if (game.replaceEvent(gainControlEvent)) {
return false;
}
}
GameEvent loseControlEvent = GameEvent.getEvent(GameEvent.EventType.LOSE_CONTROL, this.getId(), null, newControllerId);
if (game.replaceEvent(loseControlEvent)) {
return false;
}
// must change abilities controller too
this.controllerId = newControllerId;
this.getAbilities().setControllerId(newControllerId);
return true;
}
// Losing control of a ring bearer clear its status.
public void removeUncontrolledRingBearer(Game game) {
if (isRingBearer()) {
Player controller = beforeResetControllerId == null ? null : game.getPlayer(beforeResetControllerId);
String controllerName = controller == null ? "" : controller.getLogName();
game.informPlayers(controllerName + " has lost control of " + getLogName() + ". It is no longer a Ring-bearer.");
this.setRingBearer(game, false);
}
}
@Override
public boolean checkControlChanged(Game game) {
if (!controllerId.equals(beforeResetControllerId)) {
this.removeFromCombat(game);
this.controlledFromStartOfControllerTurn = false;
this.removeUncontrolledRingBearer(game);
this.getAbilities(game).setControllerId(controllerId);
game.getContinuousEffects().setController(objectId, controllerId);
// the controller of triggered abilities is always set/checked before the abilities triggers so not needed here
game.fireEvent(new GameEvent(GameEvent.EventType.LOST_CONTROL, objectId, null, beforeResetControllerId));
game.fireEvent(new GameEvent(GameEvent.EventType.GAINED_CONTROL, objectId, null, controllerId));
return true;
} else if (isCopy()) {// Because the previous copied abilities can be from another controller - change controller in any case for abilities
this.getAbilities(game).setControllerId(controllerId);
game.getContinuousEffects().setController(objectId, controllerId);
}
return false;
}
@Override
public UUID getAttachedTo() {
return attachedTo;
}
@Override
public int getAttachedToZoneChangeCounter() {
return attachedToZoneChangeCounter;
}
@Override
public void addConnectedCard(String key, UUID connectedCard) {
if (this.connectedCards.containsKey(key)) {
this.connectedCards.get(key).add(connectedCard);
} else {
List<UUID> list = new ArrayList<>();
list.add(connectedCard);
this.connectedCards.put(key, list);
}
}
@Override
public List<UUID> getConnectedCards(String key) {
return this.connectedCards.getOrDefault(key, emptyList);
}
@Override
public void clearConnectedCards(String key) {
if (this.connectedCards.containsKey(key)) {
this.connectedCards.get(key).clear();
}
}
@Override
public void unattach(Game game) {
this.attachedTo = null;
this.addInfo("attachedTo", null, game);
}
@Override
public void attachTo(UUID attachToObjectId, Ability source, Game game) {
// 701.3a - Permanents can be attached to an object or player
if (this.attachedTo != null && !Objects.equals(this.attachedTo, attachToObjectId)) {
Permanent attachedToUntilNowObject = game.getPermanent(this.attachedTo);
if (attachedToUntilNowObject != null) {
attachedToUntilNowObject.removeAttachment(this.objectId, source, game);
} else {
Card attachedToUntilNowCard = game.getCard(this.attachedTo);
if (attachedToUntilNowCard != null) {
attachedToUntilNowCard.removeAttachment(this.objectId, source, game);
} else {
Player attachedToUntilNowPlayer = game.getPlayer(this.attachedTo);
if (attachedToUntilNowPlayer != null) {
attachedToUntilNowPlayer.removeAttachment(this, source, game);
}
}
}
}
this.attachedTo = attachToObjectId;
this.attachedToZoneChangeCounter = game.getState().getZoneChangeCounter(attachToObjectId);
for (Ability ability : this.getAbilities()) {
for (Effect value : ability.getEffects(game, EffectType.CONTINUOUS)) {
ContinuousEffect effect = (ContinuousEffect) value;
game.getContinuousEffects().setOrder(effect);
// It's important to update the timestamp of the copied effect in ContinuousEffects because it does the action
for (ContinuousEffect conEffect : game.getContinuousEffects().getLayeredEffects(game)) {
if (conEffect.getId().equals(effect.getId())) {
game.getContinuousEffects().setOrder(conEffect);
}
}
}
}
// Reset "attached to" tooltip before potentially adding it
this.addInfo("attachedTo", null, game);
if (this.attachedTo != null) {
Permanent attachedToPerm = game.getPermanent(this.getAttachedTo());
// If what this permanent is attached to isn't also a permanent, such as a
// player or card in graveyard, it is important to mention what it is attached
// to in the tooltip. The rules let you attach a permanent to any kind of object
// or player, although currently the only objects it's possible to attach a
// permanent to are another permanent or a card. But to help future-proof this,
// we'll generalise to objects.
if (attachedToPerm == null) {
MageObject attachedToObject = game.getObject(this.getAttachedTo());
if (attachedToObject != null) {
this.addInfo("attachedTo",
CardUtil.addToolTipMarkTags("Attached to: " + attachedToObject.getIdName()), game);
} else {
Player attachedToPlayer = game.getPlayer(this.getAttachedTo());
if (attachedToPlayer != null) {
this.addInfo("attachedTo",
CardUtil.addToolTipMarkTags("Attached to: " + attachedToPlayer.getName()), game);
}
}
}
}
}
@Override
public boolean isDeathtouched() {
return deathtouched;
}
@Override
public int getDamage() {
return this.damage;
}
@Override
public int damage(int damage, Ability source, Game game) {
return damage(damage, source.getSourceId(), source, game);
}
@Override
public int damage(int damage, UUID attackerId, Ability source, Game game) {
return doDamage(damage, attackerId, source, game, true, false, false, null);
}
@Override
public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combat, boolean preventable) {
return doDamage(damage, attackerId, source, game, preventable, combat, false, null);
}
@Override
public int damage(int damage, UUID attackerId, Ability source, Game game, boolean combat, boolean preventable, List<UUID> appliedEffects) {
return doDamage(damage, attackerId, source, game, preventable, combat, false, appliedEffects);
}
/**
* @param damageAmount
* @param attackerId id of the permanent or player who make damage (source.getSourceId() in most cases)
* @param source
* @param game
* @param preventable
* @param combat
* @param markDamage If true, damage will be dealt later in applyDamage
* method, uses only in inner markDamage.
* @return
*/
private int doDamage(int damageAmount, UUID attackerId, Ability source, Game game, boolean preventable, boolean combat, boolean markDamage, List<UUID> appliedEffects) {
if (damageAmount < 1) {
return 0;
}
DamageEvent event = new DamagePermanentEvent(objectId, attackerId, controllerId, damageAmount, preventable, combat);
event.setAppliedEffects(appliedEffects);
if (game.replaceEvent(event)) {
return 0;
}
int actualDamageDone = checkProtectionAbilities(event, attackerId, source, game);
if (actualDamageDone < 1) {
return 0;
}
int lethal = getLethalDamage(attackerId, game);
MageObject attacker = game.getObject(attackerId);
if (this.isCreature(game)) {
if (checkWither(event, attacker, game)) {
if (markDamage) {
// mark damage only
markDamage(CounterType.M1M1.createInstance(actualDamageDone), attacker, true);
} else {
Ability damageSourceAbility = null;
if (attacker instanceof Permanent) {
damageSourceAbility = ((Permanent) attacker).getSpellAbility();
}
// deal damage immediately
addCounters(CounterType.M1M1.createInstance(actualDamageDone), game.getControllerId(attackerId), damageSourceAbility, game);
}
} else {
this.damage = CardUtil.overflowInc(this.damage, actualDamageDone);
}
}
if (this.isPlaneswalker(game)) {
int loyalty = getCounters(game).getCount(CounterType.LOYALTY);
int countersToRemove = Math.min(actualDamageDone, loyalty);
if (attacker != null && markDamage) {
markDamage(CounterType.LOYALTY.createInstance(countersToRemove), attacker, false);
} else {
removeCounters(CounterType.LOYALTY.getName(), countersToRemove, source, game);
}
}
if (this.isBattle(game)) {
int defense = getCounters(game).getCount(CounterType.DEFENSE);
int countersToRemove = Math.min(actualDamageDone, 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(), actualDamageDone, combat);
damagedEvent.setExcess(actualDamageDone - lethal);
game.fireEvent(damagedEvent);
game.getState().addSimultaneousDamage(damagedEvent, game);
UUID sourceControllerId = null;
Abilities sourceAbilities = null;
attacker = game.getPermanentOrLKIBattlefield(attackerId);
if (attacker == null) {
StackObject stackObject = game.getStack().getStackObject(attackerId);
if (stackObject != null) {
attacker = stackObject.getStackAbility().getSourceObject(game);
} else {
attacker = game.getObject(attackerId);
}
if (attacker instanceof Spell) {
sourceAbilities = ((Spell) attacker).getAbilities(game);
sourceControllerId = ((Spell) attacker).getControllerId();
} else if (attacker instanceof Card) {
sourceAbilities = ((Card) attacker).getAbilities(game);
sourceControllerId = ((Card) attacker).getOwnerId();
} else if (attacker instanceof CommandObject) {
sourceControllerId = ((CommandObject) attacker).getControllerId();
sourceAbilities = attacker.getAbilities();
} else {
attacker = null;
}
} else {
sourceAbilities = ((Permanent) attacker).getAbilities(game);
sourceControllerId = ((Permanent) attacker).getControllerId();
}
if (attacker != null && sourceAbilities != null) {
if (sourceAbilities.containsKey(LifelinkAbility.getInstance().getId())) {
if (markDamage) {
game.getPermanent(attackerId).markLifelink(actualDamageDone);
} else {
Player player = game.getPlayer(sourceControllerId);
player.gainLife(actualDamageDone, game, source);
}
}
if (sourceAbilities.containsKey(DeathtouchAbility.getInstance().getId())) {
deathtouched = true;
}
if (dealtDamageByThisTurn == null) {
dealtDamageByThisTurn = new HashSet<>();
}
// Unstable ability - Earl of Squirrel
if (sourceAbilities.containsKey(SquirrellinkAbility.getInstance().getId())) {
Player player = game.getPlayer(sourceControllerId);
new SquirrelToken().putOntoBattlefield(actualDamageDone, game, source, player.getId());
}
dealtDamageByThisTurn.add(new MageObjectReference(attacker, game));
}
if (attacker == null) {
game.informPlayers(getLogName() + " gets " + actualDamageDone + " damage");
} else {
game.informPlayers(attacker.getLogName() + " deals " + actualDamageDone + " damage to " + getLogName());
}
return actualDamageDone;
}
private static boolean checkWither(DamageEvent event, MageObject attacker, Game game) {
if (event.isAsThoughWither() || event.isAsThoughInfect()) {
return true;
}
if (attacker == null) {
return false;
}
Abilities abilities;
if (attacker instanceof Card) {
abilities = ((Card) attacker).getAbilities(game);
} else {
abilities = attacker.getAbilities();
}
return abilities.containsKey(InfectAbility.getInstance().getId())
|| abilities.containsKey(WitherAbility.getInstance().getId());
}
@Override
public void markLifelink(int damage) {
markedLifelink += damage;
}
@Override
public int markDamage(int damageAmount, UUID attackerId, Ability source, Game game, boolean preventable, boolean combat) {
return doDamage(damageAmount, attackerId, source, game, preventable, combat, true, null);
}
@Override
public int applyDamage(Game game) {
if (markedLifelink > 0) {
Player player = game.getPlayer(this.getControllerId());
player.gainLife(markedLifelink, game, null);
markedLifelink = 0;
}
if (markedDamage == null) {
return 0;
}
for (MarkedDamageInfo mdi : markedDamage) {
Ability source = null;
if (mdi.sourceObject instanceof PermanentToken) {
/* Tokens don't have a spellAbility. We must make a phony one as the source so the events in addCounters
* can trace the source back to an object/controller.
*/
source = new SpellAbility(null, ((PermanentToken) mdi.sourceObject).name);
source.setSourceId(((PermanentToken) mdi.sourceObject).objectId);
} else if (mdi.sourceObject instanceof Permanent) {
source = ((Permanent) mdi.sourceObject).getSpellAbility();
}
if (mdi.addCounters) {
addCounters(mdi.counter, game.getControllerId(mdi.sourceObject.getId()), source, game);
} else {
removeCounters(mdi.counter, source, game);
}
}
markedDamage.clear();
return 0;
}
@Override
public int getLethalDamage(UUID attackerId, Game game) {
int lethal = Integer.MAX_VALUE;
if (this.isCreature(game)) {
if (game.getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters().stream().anyMatch(f -> f.match(this, game))) {
lethal = power.getValue();
} else {
lethal = toughness.getValue();
}
lethal = Math.max(lethal - this.damage, 0);
Card attacker = game.getPermanent(attackerId);
if (attacker == null) {
attacker = game.getCard(attackerId);
}
if (attacker != null && attacker.getAbilities(game).containsKey(DeathtouchAbility.getInstance().getId())) {
lethal = Math.min(1, lethal);
}
}
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;
}
@Override
public void removeAllDamage(Game game) {
damage = 0;
deathtouched = false;
}
private int checkProtectionAbilities(GameEvent event, UUID attackerId, Ability source, Game game) {
MageObject attacker = game.getObject(attackerId);
if (attacker != null && hasProtectionFrom(attacker, game)) {
GameEvent preventEvent = new PreventDamageEvent(this.getId(), attackerId, source, this.getControllerId(), event.getAmount(), ((DamageEvent) event).isCombatDamage());
if (!game.replaceEvent(preventEvent)) {
int preventedDamage = event.getAmount();
event.setAmount(0);
game.fireEvent(new PreventedDamageEvent(this.getId(), attackerId, source, this.getControllerId(), preventedDamage));
return 0;
}
}
return event.getAmount();
}
private void markDamage(Counter counter, MageObject source, boolean addCounters) {
if (markedDamage == null) {
markedDamage = new ArrayList<>();
}
markedDamage.add(new MarkedDamageInfo(counter, source, addCounters));
}
@Override
public boolean entersBattlefield(Ability source, Game game, Zone fromZone, boolean fireEvent) {
controlledFromStartOfControllerTurn = false;
BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(game, this);
if (faceDownType != null) {
// remove some attributes here, because first apply effects comes later otherwise abilities (e.g. color related) will unintended trigger
BecomesFaceDownCreatureEffect.makeFaceDownObject(game, null, this, faceDownType, null);
}
// own etb event
if (game.replaceEvent(new EntersTheBattlefieldEvent(this, source, getControllerId(), fromZone, EnterEventType.SELF))) {
return false;
}
// normal etb event
EntersTheBattlefieldEvent event = new EntersTheBattlefieldEvent(this, source, getControllerId(), fromZone);
if (game.replaceEvent(event)) {
return false;
}
if (this.isPlaneswalker(game)) {
int loyalty;
if (this.getStartingLoyalty() == -2) {
loyalty = source.getManaCostsToPay().getX();
} else {
loyalty = this.getStartingLoyalty();
}
int countersToAdd;
if (this.hasAbility(CompleatedAbility.getInstance(), game)) {
countersToAdd = loyalty - 2 * source.getManaCostsToPay().getPhyrexianPaid();
} else {
countersToAdd = loyalty;
}
if (countersToAdd > 0) {
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;
}
game.addSimultaneousEvent(event);
return true;
}
@Override
public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) {
if (source != null) {
if (abilities.containsKey(ShroudAbility.getInstance().getId())) {
if (game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game).isEmpty()) {
return false;
}
}
if (game.getPlayer(this.getControllerId()).hasOpponent(sourceControllerId, game)
&& game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game).isEmpty()
&& abilities.stream()
.filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast)
.anyMatch(ability -> ability.checkObject(source, game))) {
return false;
}
if (hasProtectionFrom(source, game)) {
return false;
}
// example: Fiendslayer Paladin tried to target with Ultimate Price
return !game.getContinuousEffects().preventedByRuleModification(
new TargetEvent(this, source.getId(), sourceControllerId),
null,
game,
true
);
}
return true;
}
@Override
public boolean hasProtectionFrom(MageObject source, Game game) {
for (ProtectionAbility ability : this.getAbilities(game).getProtectionAbilities()) {
if (!ability.canTarget(source, game)) {
return true;
}
}
return false;
}
@Override
public boolean cantBeAttachedBy(MageObject attachment, Ability source, Game game, boolean silentMode) {
for (ProtectionAbility ability : this.getAbilities(game).getProtectionAbilities()) {
if ((!attachment.hasSubtype(SubType.AURA, game) || ability.removesAuras())
&& (!attachment.hasSubtype(SubType.EQUIPMENT, game) || ability.removesEquipment())
&& !attachment.getId().equals(ability.getAuraIdNotToBeRemoved())
&& !ability.canTarget(attachment, game)) {
return !ability.getDoesntRemoveControlled() || isControlledBy(game.getControllerId(attachment.getId()));
}
}
return game.getContinuousEffects().preventedByRuleModification(new StayAttachedEvent(this.getId(), attachment.getId(), source), null, game, silentMode);
}
@Override
public boolean destroy(Ability source, Game game) {
return destroy(source, game, false);
}
@Override
public boolean destroy(Ability source, Game game, boolean noRegen) {
// Only permanents on the battlefield can be destroyed
if (!game.getState().getZone(getId()).equals(Zone.BATTLEFIELD)) {
return false;
}
//20091005 - 701.6
if (abilities.containsKey(IndestructibleAbility.getInstance().getId())) {
return false;
}
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DESTROY_PERMANENT, objectId, source, controllerId, noRegen ? 1 : 0))) {
// this means destroy was successful, if object movement to graveyard will be replaced (e.g. commander to command zone) it's still
// handled as successful destroying (but not as successful "dies this way" for destroying).
if (moveToZone(Zone.GRAVEYARD, source, game, false)) {
String logName;
Card card = game.getCard(this.getId());
if (card != null) {
logName = card.getLogName();
} else {
logName = this.getLogName();
}
if (this.isCreature(game)) {
game.informPlayers(logName + " died" + CardUtil.getSourceLogName(game, " by ", source, "", ""));
} else {
game.informPlayers(logName + " was destroyed" + CardUtil.getSourceLogName(game, " by ", source, "", ""));
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DESTROYED_PERMANENT, objectId, source, controllerId));
}
return true;
}
return false;
}
@Override
public boolean sacrifice(Ability source, Game game) {
//20091005 - 701.13
if (isPhasedIn() && canBeSacrificed && !game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.SACRIFICE_PERMANENT, objectId, source, controllerId))) {
// Commander replacement effect or Rest in Peace (exile instead of graveyard) in play does not prevent successful sacrifice
// so the return value of the moveToZone is not taken into account here
moveToZone(Zone.GRAVEYARD, source, game, false);
Player player = game.getPlayer(getControllerId());
if (player != null) {
game.informPlayers(player.getLogName() + " sacrificed " + this.getLogName() + CardUtil.getSourceLogName(game, source));
}
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.SACRIFICED_PERMANENT, objectId, source, controllerId));
return true;
}
return false;
}
@Override
public boolean regenerate(Ability source, Game game) {
//20110930 - 701.12
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REGENERATE, objectId, source, controllerId))) {
this.tap(source, game);
this.removeFromCombat(game);
this.removeAllDamage(game);
// remove one regen shield
RegenerateSourceEffect.decRegenerationShieldsAmount(game, this.getId());
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.REGENERATED, objectId, source, controllerId));
return true;
}
return false;
}
@Override
public void addPower(int power) {
this.power.increaseBoostedValue(power);
}
@Override
public void addToughness(int toughness) {
this.toughness.increaseBoostedValue(toughness);
}
/**
* Simple event without source
*
* @param eventType
* @param game
*/
protected void fireEvent(EventType eventType, Game game) {
game.fireEvent(GameEvent.getEvent(eventType, this.objectId, null, this.controllerId));
}
/**
* Simple event without source
*
* @param eventType
* @param game
* @return
*/
protected boolean replaceEvent(EventType eventType, Game game) {
return game.replaceEvent(GameEvent.getEvent(eventType, this.objectId, null, this.controllerId));
}
@Override
public boolean canAttack(UUID defenderId, Game game) {
if (tapped) {
return false;
}
return canAttackInPrinciple(defenderId, game);
}
@Override
public boolean canBeAttacked(UUID attackerId, UUID defendingPlayerId, Game game) {
if (isPlaneswalker(game)) {
return isControlledBy(defendingPlayerId);
}
if (isBattle(game)) {
return isProtectedBy(defendingPlayerId);
}
return false;
}
@Override
public boolean canAttackInPrinciple(UUID defenderId, Game game) {
if (isBattle(game)) {
// battles can never attack
return false;
}
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(
this.objectId, AsThoughEffectType.ATTACK_AS_HASTE, null, defenderId, game
);
if (hasSummoningSickness() && approvingObjects.isEmpty()) {
return false;
}
//20101001 - 508.1c
if (defenderId == null) {
if (game.getCombat()
.getDefenders()
.stream()
.noneMatch(defenderToCheckId -> canAttackCheckRestrictionEffects(defenderToCheckId, game))) {
return false;
}
} else if (!canAttackCheckRestrictionEffects(defenderId, game)) {
return false;
}
return !abilities.containsKey(DefenderAbility.getInstance().getId())
|| !game.getContinuousEffects().asThough(this.objectId, AsThoughEffectType.ATTACK, null, this.getControllerId(), game).isEmpty();
}
private boolean canAttackCheckRestrictionEffects(UUID defenderId, Game game) {
//20101001 - 508.1c
for (Map.Entry<RestrictionEffect, Set<Ability>> effectEntry : game.getContinuousEffects().getApplicableRestrictionEffects(this, game).entrySet()) {
if (!effectEntry.getKey().canAttack(game, true)) {
return false;
}
for (Ability ability : effectEntry.getValue()) {
if (!effectEntry.getKey().canAttack(this, defenderId, ability, game, true)) {
return false;
}
}
}
return true;
}
@Override
public boolean canBlock(UUID attackerId, Game game) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty() || isBattle(game) || isSuspected()) {
return false;
}
Permanent attacker = game.getPermanent(attackerId);
if (attacker == null) {
return false;
}
// controller of attacking permanent must be an opponent
if (!game.getPlayer(this.getControllerId()).hasOpponent(attacker.getControllerId(), game)) {
return false;
}
//20101001 - 509.1b
// check blocker restrictions
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRestrictionEffects(this, game).entrySet()) {
for (Ability ability : entry.getValue()) {
if (!entry.getKey().canBlock(attacker, this, ability, game, true)) {
return false;
}
}
}
// check also attacker's restriction effects
for (Map.Entry<RestrictionEffect, Set<Ability>> restrictionEntry : game.getContinuousEffects().getApplicableRestrictionEffects(attacker, game).entrySet()) {
for (Ability ability : restrictionEntry.getValue()) {
if (!restrictionEntry.getKey().canBeBlocked(attacker, this, ability, game, true)) {
return false;
}
}
}
return !attacker.hasProtectionFrom(this, game);
}
@Override
public boolean canBlockAny(Game game) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty()) {
return false;
}
//20101001 - 509.1b
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRestrictionEffects(this, game).entrySet()) {
RestrictionEffect effect = entry.getKey();
for (Ability ability : entry.getValue()) {
if (!effect.canBlock(null, this, ability, game, true)) {
return false;
}
}
}
return true;
}
/**
* Checks by restriction effects if the permanent can use activated
* abilities
*
* @param game
* @return true - permanent can use activated abilities
*/
@Override
public boolean canUseActivatedAbilities(Game game) {
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : game.getContinuousEffects().getApplicableRestrictionEffects(this, game).entrySet()) {
RestrictionEffect effect = entry.getKey();
for (Ability ability : entry.getValue()) {
if (!effect.canUseActivatedAbilities(this, ability, game, true)) {
return false;
}
}
}
return true;
}
@Override
public void setAttacking(boolean attacking) {
this.attacking = attacking;
}
@Override
public void setBlocking(int blocking) {
this.blocking = blocking;
}
@Override
public void setMaxBlocks(int maxBlocks) {
this.maxBlocks = maxBlocks;
}
@Override
public void setMinBlockedBy(int minBlockedBy) {
this.minBlockedBy = minBlockedBy;
}
@Override
public void setMaxBlockedBy(int maxBlockedBy) {
this.maxBlockedBy = maxBlockedBy;
}
@Override
public boolean removeFromCombat(Game game) {
return removeFromCombat(game, true);
}
@Override
public boolean removeFromCombat(Game game, boolean withEvent) {
if (this.isAttacking() || this.blocking > 0) {
return game.getCombat().removeFromCombat(objectId, game, withEvent);
} else if (this.isPlaneswalker(game) && game.getCombat().getDefenders().contains(getId())) {
game.getCombat().removeDefendingPermanentFromCombat(objectId, game);
}
return false;
}
@Override
public boolean imprint(UUID imprintedCard, Game game) {
if (!game.getExile().containsId(imprintedCard, game)) {
return false;
}
if (connectedCards.containsKey("imprint")) {
if (!this.connectedCards.get("imprint").contains(imprintedCard)) {
this.connectedCards.get("imprint").add(imprintedCard);
}
} else {
List<UUID> imprinted = new ArrayList<>();
imprinted.add(imprintedCard);
this.connectedCards.put("imprint", imprinted);
}
return true;
}
@Override
public boolean clearImprinted(Game game) {
this.connectedCards.put("imprint", new ArrayList<>());
return true;
}
@Override
public List<UUID> getImprinted() {
return this.connectedCards.getOrDefault("imprint", emptyList);
}
@Override
public Set<MageObjectReference> getDealtDamageByThisTurn() {
if (dealtDamageByThisTurn == null) {
return new HashSet<>();
}
return dealtDamageByThisTurn;
}
@Override
public boolean isTransformed() {
return this.transformed;
}
@Override
public void setTransformed(boolean value) {
this.transformed = value;
}
@Override
public boolean isMonstrous() {
return this.monstrous;
}
@Override
public boolean isPrototyped() {
return this.prototyped;
}
@Override
public void setMonstrous(boolean value) {
this.monstrous = value;
}
@Override
public boolean isRenowned() {
return this.renowned;
}
@Override
public void setRenowned(boolean value) {
this.renowned = value;
}
@Override
public boolean isSuspected() {
return suspected;
}
private static final String suspectedInfoKey = "IS_SUSPECTED";
@Override
public void setSuspected(boolean value, Game game, Ability source) {
if (!value || !game.replaceEvent(GameEvent.getEvent(
EventType.BECOME_SUSPECTED, getId(),
source, source.getControllerId()
))) {
this.suspected = value;
}
if (this.suspected) {
addInfo(suspectedInfoKey, CardUtil.addToolTipMarkTags("Suspected (has menace and can't block)"), game);
} else {
addInfo(suspectedInfoKey, null, game);
}
}
// Used as key for the ring bearer info.
private static final String ringbearerInfoKey = "IS_RINGBEARER";
@Override
public void setRingBearer(Game game, boolean value) {
if (value == this.ringBearerFlag) {
return;
}
if (value) {
// The player may have another Ringbearer. We need to clear it if so.
//
// 701.52a Certain spells and abilities have the text “the Ring tempts you.”
// Each time the Ring tempts you, choose a creature you control.
// That creature becomes your Ring-bearer until another creature
// becomes your Ring-bearer or another player gains control of it.
Player player = game.getPlayer(getControllerId());
String playername = "";
if (player != null) {
playername = player.getLogName();
Permanent existingRingbearer = player.getRingBearer(game);
if (existingRingbearer != null && existingRingbearer.getId() != this.getId()) {
existingRingbearer.setRingBearer(game, false);
}
}
addInfo(ringbearerInfoKey, CardUtil.addToolTipMarkTags("Is " + playername + "'s Ring-bearer"), game);
} else {
addInfo(ringbearerInfoKey, null, game);
}
this.ringBearerFlag = value;
}
@Override
public int getClassLevel() {
return classLevel;
}
@Override
public boolean setClassLevel(int classLevel) {
// can level up to next (+1) level only
if (this.classLevel == classLevel - 1) {
this.classLevel = classLevel;
return true;
}
return false;
}
@Override
public void addGoadingPlayer(UUID playerId) {
this.goadingPlayers.add(playerId);
}
@Override
public Set<UUID> getGoadingPlayers() {
return goadingPlayers;
}
@Override
public void chooseProtector(Game game, Ability source) {
Set<UUID> opponents = game.getOpponents(this.getControllerId());
Player controller = game.getPlayer(this.getControllerId());
if (controller == null) {
return;
}
Player newProtector;
if (opponents.size() > 1) {
TargetPlayer target = new TargetPlayer(new FilterOpponent("protector for " + getName()));
target.withNotTarget(true);
target.setRequired(true);
controller.choose(Outcome.Neutral, target, source, game);
newProtector = game.getPlayer(target.getFirstTarget());
} else if (opponents.size() == 1) {
newProtector = game.getPlayer(opponents.iterator().next());
} else {
newProtector = null;
}
if (newProtector != null) {
String protectorName = newProtector.getLogName();
game.informPlayers(protectorName + " has been chosen to protect " + this.getLogName());
this.addInfo("protector", "Protected by " + protectorName, game);
this.setProtectorId(newProtector.getId());
} else {
game.informPlayers(controller.getLogName() + " remains in protect of " + this.getLogName() + " as there are no opponents for new protector");
}
}
@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 setCanBeSacrificed(boolean canBeSacrificed) {
this.canBeSacrificed = canBeSacrificed;
}
@Override
public boolean canBeSacrificed() {
return canBeSacrificed;
}
@Override
public void setPairedCard(MageObjectReference pairedCard) {
this.pairedPermanent = pairedCard;
if (pairedCard == null) {
// remove existing soulbond info text
this.addInfo("soulbond", null, null);
}
}
@Override
public MageObjectReference getPairedCard() {
return pairedPermanent;
}
@Override
public void clearPairedCard() {
this.pairedPermanent = null;
}
@Override
public void addBandedCard(UUID bandedCard) {
if (!this.bandedCards.contains(bandedCard)) {
this.bandedCards.add(bandedCard);
}
}
@Override
public void removeBandedCard(UUID bandedCard) {
this.bandedCards.remove(bandedCard);
}
@Override
public List<UUID> getBandedCards() {
return bandedCards;
}
@Override
public void clearBandedCards() {
this.bandedCards.clear();
}
@Override
public String getLogName() {
if (name.isEmpty()) {
if (faceDown) {
return GameLog.getNeutralColoredText("face down creature");
} else {
return GameLog.getNeutralColoredText("a creature without name");
}
}
return GameLog.getColoredObjectIdName(this);
}
@Override
public boolean isManifested() {
return manifested;
}
@Override
public void setManifested(boolean value) {
manifested = value;
}
@Override
public boolean isMorphed() {
return morphed;
}
@Override
public void setMorphed(boolean value) {
morphed = value;
}
@Override
public void setRarity(Rarity rarity) {
this.rarity = rarity;
}
@Override
public void setFlipCard(boolean flipCard) {
this.flipCard = flipCard;
}
@Override
public void setFlipCardName(String flipCardName) {
this.flipCardName = flipCardName;
}
@Override
public void setSecondCardFace(Card card) {
this.secondSideCard = card;
}
public void setPrototyped(boolean prototyped) {
this.prototyped = prototyped;
}
@Override
public boolean isRingBearer() {
return ringBearerFlag;
}
@Override
public boolean isSolved() {
return solved;
}
@Override
public boolean solve(Game game, Ability source) {
if (this.solved) {
return false;
}
GameEvent event = new GameEvent(GameEvent.EventType.SOLVE_CASE, getId(),
source, source.getControllerId());
if (game.replaceEvent(event)) {
return false;
}
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
game.informPlayers(controller.getLogName() + " solved " + this.getLogName() +
CardUtil.getSourceLogName(game, source));
}
this.solved = true;
game.fireEvent(new GameEvent(EventType.CASE_SOLVED, getId(), source,
source.getControllerId()));
return true;
}
@Override
public boolean fight(Permanent fightTarget, Ability source, Game game) {
return this.fight(fightTarget, source, game, true);
}
@Override
public boolean fight(Permanent fightTarget, Ability source, Game game, boolean batchTrigger) {
// double fight events for each creature
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.FIGHTED_PERMANENT, fightTarget.getId(), source, source.getControllerId()));
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.FIGHTED_PERMANENT, getId(), source, source.getControllerId()));
damage(fightTarget.getPower().getValue(), fightTarget.getId(), source, game);
fightTarget.damage(getPower().getValue(), getId(), source, game);
if (batchTrigger) {
Set<MageObjectReference> morSet = new HashSet<>();
morSet.add(new MageObjectReference(this, game));
morSet.add(new MageObjectReference(fightTarget, game));
String data = UUID.randomUUID().toString();
game.getState().setValue("batchFight_" + data, morSet);
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.BATCH_FIGHT, getId(), source, source.getControllerId(), data, 0));
}
return true;
}
@Override
public int getCreateOrder() {
return createOrder;
}
@Override
public void setCreateOrder(int createOrder) {
this.createOrder = createOrder;
}
@Override
public ObjectColor getColor() {
return color;
}
@Override
public ObjectColor getColor(Game game) {
return color;
}
//20180810 - 701.3d
//If an object leaves the zone it's in, all attached permanents become unattached
//note that this code doesn't actually detach anything, and is a bit of a bandaid
public void detachAllAttachments(Game game) {
for (UUID attachmentId : getAttachments()) {
Permanent attachment = game.getPermanent(attachmentId);
Card attachmentCard = game.getCard(attachmentId);
if (attachment != null && attachmentCard != null) {
//make bestow cards and licids into creatures
//aura test to stop bludgeon brawl shenanigans from using this code
//consider adding code to handle that case?
if (attachment.hasSubtype(SubType.AURA, game) && attachmentCard.isCreature(game)) {
BestowAbility.becomeCreature(attachment, game);
}
}
}
}
@Override
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
Zone fromZone = game.getState().getZone(objectId);
Player controller = game.getPlayer(controllerId);
if (controller != null) {
ZoneChangeEvent event = new ZoneChangeEvent(this, source, controllerId, fromZone, toZone, appliedEffects);
ZoneChangeInfo zoneChangeInfo;
if (toZone == Zone.LIBRARY) {
zoneChangeInfo = new ZoneChangeInfo.Library(event, flag /* put on top */);
} else {
zoneChangeInfo = new ZoneChangeInfo(event);
}
boolean successfullyMoved = ZonesHandler.moveCard(zoneChangeInfo, game, source);
//20180810 - 701.3d
if (successfullyMoved) {
detachAllAttachments(game);
}
return successfullyMoved;
}
return false;
}
@Override
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
Zone fromZone = game.getState().getZone(objectId);
ZoneChangeEvent event = new ZoneChangeEvent(this, source, ownerId, fromZone, Zone.EXILED, appliedEffects);
ZoneChangeInfo.Exile zcInfo = new ZoneChangeInfo.Exile(event, exileId, name);
boolean successfullyMoved = ZonesHandler.moveCard(zcInfo, game, source);
//20180810 - 701.3d
if (successfullyMoved) {
detachAllAttachments(game);
}
return successfullyMoved;
}
}