mirror of
https://github.com/magefree/mage.git
synced 2025-12-26 05:22:02 -08:00
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);
This commit is contained in:
parent
4901de12c1
commit
e38a79f231
104 changed files with 2178 additions and 1495 deletions
|
|
@ -1,16 +1,14 @@
|
|||
|
||||
|
||||
package mage;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MageItem extends Serializable {
|
||||
|
||||
UUID getId();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,15 @@ public interface MageObject extends MageItem, Serializable, Copyable<MageObject>
|
|||
|
||||
void setImageNumber(Integer imageNumber);
|
||||
|
||||
/**
|
||||
* Get image file name
|
||||
* - empty for default name from a card
|
||||
* - non-empty for face down objects like Morph (GUI show empty name, but image must show some image)
|
||||
*/
|
||||
String getImageFileName();
|
||||
|
||||
void setImageFileName(String imageFile);
|
||||
|
||||
String getName();
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ public abstract class MageObjectImpl implements MageObject {
|
|||
|
||||
private String expansionSetCode = "";
|
||||
private String cardNumber = "";
|
||||
private String imageFileName = "";
|
||||
private int imageNumber = 0;
|
||||
|
||||
protected List<SuperType> supertype = new ArrayList<>();
|
||||
|
|
@ -77,6 +78,7 @@ public abstract class MageObjectImpl implements MageObject {
|
|||
frameStyle = object.frameStyle;
|
||||
expansionSetCode = object.expansionSetCode;
|
||||
cardNumber = object.cardNumber;
|
||||
imageFileName = object.imageFileName;
|
||||
imageNumber = object.imageNumber;
|
||||
power = object.power.copy();
|
||||
toughness = object.toughness.copy();
|
||||
|
|
@ -266,6 +268,16 @@ public abstract class MageObjectImpl implements MageObject {
|
|||
this.cardNumber = cardNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageFileName() {
|
||||
return imageFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageFileName(String imageFileName) {
|
||||
this.imageFileName = imageFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getImageNumber() {
|
||||
return imageNumber;
|
||||
|
|
|
|||
|
|
@ -35,15 +35,6 @@ import java.util.UUID;
|
|||
*/
|
||||
public interface Ability extends Controllable, Serializable {
|
||||
|
||||
/**
|
||||
* Gets the globally unique id of the ability contained within the game.
|
||||
*
|
||||
* @return A {@link java.util.UUID} which the game will use to store and
|
||||
* retrieve the exact instance of this ability.
|
||||
*/
|
||||
@Override
|
||||
UUID getId();
|
||||
|
||||
/**
|
||||
* Assigns a new {@link java.util.UUID}
|
||||
*
|
||||
|
|
@ -71,14 +62,6 @@ public interface Ability extends Controllable, Serializable {
|
|||
*/
|
||||
AbilityType getAbilityType();
|
||||
|
||||
/**
|
||||
* Gets the id of the player in control of this ability.
|
||||
*
|
||||
* @return The {@link java.util.UUID} of the controlling player.
|
||||
*/
|
||||
@Override
|
||||
UUID getControllerId();
|
||||
|
||||
/**
|
||||
* Sets the id of the controller of this ability.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -428,6 +428,7 @@ public abstract class AbilityImpl implements Ability {
|
|||
case MORE_THAN_MEETS_THE_EYE:
|
||||
case BESTOW:
|
||||
case MORPH:
|
||||
case MEGAMORPH:
|
||||
// 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)
|
||||
|
|
@ -649,6 +650,11 @@ public abstract class AbilityImpl implements Ability {
|
|||
return controllerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return getControllerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setControllerId(UUID controllerId) {
|
||||
this.controllerId = controllerId;
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
if (!approvingObjects.isEmpty()) {
|
||||
Card card = game.getCard(sourceId);
|
||||
Zone zone = game.getState().getZone(sourceId);
|
||||
if(card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) {
|
||||
if (card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) {
|
||||
// Regular casting, to be an alternative to the AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE from hand (e.g. One with the Multiverse):
|
||||
approvingObjects.add(new ApprovingObject(this, game));
|
||||
}
|
||||
|
|
@ -160,8 +160,8 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
Player player = game.getPlayer(playerId);
|
||||
if (player != null
|
||||
&& player.getCastSourceIdWithAlternateMana()
|
||||
.getOrDefault(getSourceId(), Collections.emptySet())
|
||||
.contains(MageIdentifier.Default)
|
||||
.getOrDefault(getSourceId(), Collections.emptySet())
|
||||
.contains(MageIdentifier.Default)
|
||||
) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
|
|
@ -181,11 +181,10 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
}
|
||||
return ActivationStatus.getFalse();
|
||||
} else {
|
||||
if(canChooseTarget(game, playerId)) {
|
||||
if(approvingObjects == null || approvingObjects.isEmpty()) {
|
||||
if (canChooseTarget(game, playerId)) {
|
||||
if (approvingObjects == null || approvingObjects.isEmpty()) {
|
||||
return ActivationStatus.withoutApprovingObject(true);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return new ActivationStatus(approvingObjects);
|
||||
}
|
||||
}
|
||||
|
|
@ -308,22 +307,27 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns a card object with the spell characteristics like color, types,
|
||||
* Returns combined card object with the spell characteristics like color, types,
|
||||
* subtypes etc. E.g. if you cast a Bestow card as enchantment, the
|
||||
* characteristics don't include the creature type.
|
||||
* <p>
|
||||
* Warning, it's not a real card - use it as a blueprint or characteristics searching
|
||||
*
|
||||
* @param game
|
||||
* @return card object with the spell characteristics
|
||||
*/
|
||||
public Card getCharacteristics(Game game) {
|
||||
Card spellCharacteristics = game.getSpell(this.getId());
|
||||
if (spellCharacteristics == null) {
|
||||
// playable check (without put to stack)
|
||||
spellCharacteristics = game.getCard(this.getSourceId());
|
||||
}
|
||||
|
||||
if (spellCharacteristics != null) {
|
||||
if (getSpellAbilityCastMode() != SpellAbilityCastMode.NORMAL) {
|
||||
spellCharacteristics = getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(spellCharacteristics, this);
|
||||
// transform characteristics (morph, transform, bestow, etc)
|
||||
spellCharacteristics = getSpellAbilityCastMode().getTypeModifiedCardObjectCopy(spellCharacteristics, this, game);
|
||||
}
|
||||
spellCharacteristics = spellCharacteristics.copy();
|
||||
}
|
||||
return spellCharacteristics;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl {
|
|||
@Override
|
||||
public void init(Ability source, Game game) {
|
||||
super.init(source, game);
|
||||
|
||||
// save permanents to become face down (one time usage on resolve)
|
||||
for (Permanent perm : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source, game)) {
|
||||
if (!perm.isFaceDown(game) && !perm.isTransformable()) {
|
||||
affectedObjectList.add(new MageObjectReference(perm, game));
|
||||
|
|
@ -66,6 +68,7 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl {
|
|||
public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) {
|
||||
boolean targetExists = false;
|
||||
for (MageObjectReference mor : affectedObjectList) {
|
||||
// TODO: wtf, why it not use a BecomesFaceDownCreatureEffect.makeFaceDownObject and applied by layers?! Looks buggy
|
||||
Permanent permanent = mor.getPermanent(game);
|
||||
if (permanent != null && permanent.isFaceDown(game)) {
|
||||
targetExists = true;
|
||||
|
|
@ -119,7 +122,6 @@ public class BecomesFaceDownCreatureAllEffect extends ContinuousEffectImpl {
|
|||
permanent.getPower().setModifiedBaseValue(2);
|
||||
permanent.getToughness().setModifiedBaseValue(2);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package mage.abilities.effects.common.continuous;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.MageObjectReference;
|
||||
import mage.ObjectColor;
|
||||
import mage.abilities.Ability;
|
||||
|
|
@ -9,27 +10,42 @@ import mage.abilities.costs.Costs;
|
|||
import mage.abilities.costs.CostsImpl;
|
||||
import mage.abilities.effects.ContinuousEffectImpl;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.repository.TokenInfo;
|
||||
import mage.cards.repository.TokenRepository;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.game.permanent.token.EmptyToken;
|
||||
import mage.game.permanent.token.Token;
|
||||
import mage.util.CardUtil;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* This effect lets the card be a 2/2 face-down creature, with no text, no name,
|
||||
* no subtypes, and no mana cost, if it's face down on the battlefield. And it
|
||||
* adds the a TurnFaceUpAbility ability.
|
||||
* <p>
|
||||
* Warning, if a card has multiple face down abilities then keep only one face up cost
|
||||
* Example: Mischievous Quanar
|
||||
* - a. Turn Mischievous Quanar face down - BecomesFaceDownCreatureEffect without turn up cost
|
||||
* - b. Morph - BecomesFaceDownCreatureEffect with turn up cost inside
|
||||
*
|
||||
* @author LevelX2
|
||||
* @author LevelX2, JayDi85
|
||||
*/
|
||||
public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(BecomesFaceDownCreatureEffect.class);
|
||||
|
||||
public enum FaceDownType {
|
||||
MANIFESTED,
|
||||
MANUAL,
|
||||
MEGAMORPHED,
|
||||
MORPHED,
|
||||
MEGAMORPHED,
|
||||
DISGUISED,
|
||||
CLOAKED
|
||||
}
|
||||
|
|
@ -134,51 +150,137 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
|
|||
throw new UnsupportedOperationException("FaceDownType not yet supported: " + faceDownType);
|
||||
}
|
||||
}
|
||||
|
||||
permanent.setName(EmptyNames.FACE_DOWN_CREATURE.toString());
|
||||
permanent.removeAllSuperTypes(game);
|
||||
permanent.removeAllCardTypes(game);
|
||||
permanent.addCardType(game, CardType.CREATURE);
|
||||
permanent.removeAllSubTypes(game);
|
||||
permanent.getColor(game).setColor(ObjectColor.COLORLESS);
|
||||
Card card = game.getCard(permanent.getId());
|
||||
|
||||
List<Ability> abilitiesToRemove = new ArrayList<>();
|
||||
for (Ability ability : permanent.getAbilities()) {
|
||||
|
||||
// keep gained abilities from other sources, removes only own (card text)
|
||||
if (card != null && !card.getAbilities().contains(ability)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 701.33c
|
||||
// If a card with morph is manifested, its controller may turn that card face up using
|
||||
// either the procedure described in rule 702.36e to turn a face-down permanent with morph face up
|
||||
// or the procedure described above to turn a manifested permanent face up.
|
||||
//
|
||||
// so keep all tune face up abilities and other face down compatible
|
||||
if (ability.getWorksFaceDown()) {
|
||||
ability.setRuleVisible(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getRuleVisible() && !ability.getEffects().isEmpty()) {
|
||||
if (ability.getEffects().get(0) instanceof BecomesFaceDownCreatureEffect) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
abilitiesToRemove.add(ability);
|
||||
}
|
||||
permanent.removeAbilities(abilitiesToRemove, source.getSourceId(), game);
|
||||
if (turnFaceUpAbility != null) { // TODO: shouldn't be added by this effect, but separately
|
||||
permanent.addAbility(turnFaceUpAbility, source.getSourceId(), game);
|
||||
}
|
||||
permanent.getPower().setModifiedBaseValue(2);
|
||||
permanent.getToughness().setModifiedBaseValue(2);
|
||||
makeFaceDownObject(game, source.getSourceId(), permanent, faceDownType, this.turnFaceUpAbility);
|
||||
} else if (duration == Duration.Custom && foundPermanent) {
|
||||
discard();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static FaceDownType findFaceDownType(Game game, Permanent permanent) {
|
||||
if (permanent.isMorphed()) {
|
||||
return BecomesFaceDownCreatureEffect.FaceDownType.MORPHED;
|
||||
} else if (permanent.isManifested()) {
|
||||
return BecomesFaceDownCreatureEffect.FaceDownType.MANIFESTED;
|
||||
} else if (permanent.isFaceDown(game)) {
|
||||
return BecomesFaceDownCreatureEffect.FaceDownType.MANUAL;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any object (card, token) to face down (remove/hide all face up information and make it a 2/2 creature)
|
||||
*/
|
||||
public static void makeFaceDownObject(Game game, UUID sourceId, MageObject object, FaceDownType faceDownType, Ability turnFaceUpAbility) {
|
||||
String originalObjectInfo = object.toString();
|
||||
|
||||
// warning, it's a direct changes to the object (without game state, so no game param here)
|
||||
object.setName(EmptyNames.FACE_DOWN_CREATURE.toString());
|
||||
object.removeAllSuperTypes();
|
||||
object.getSubtype().clear();
|
||||
object.removeAllCardTypes();
|
||||
object.addCardType(CardType.CREATURE);
|
||||
object.getColor().setColor(ObjectColor.COLORLESS);
|
||||
|
||||
// remove wrong abilities
|
||||
Card card = game.getCard(object.getId());
|
||||
List<Ability> abilitiesToRemove = new ArrayList<>();
|
||||
for (Ability ability : object.getAbilities()) {
|
||||
|
||||
// keep gained abilities from other sources, removes only own (card text)
|
||||
if (card != null && !card.getAbilities().contains(ability)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 701.33c
|
||||
// If a card with morph is manifested, its controller may turn that card face up using
|
||||
// either the procedure described in rule 702.36e to turn a face-down permanent with morph face up
|
||||
// or the procedure described above to turn a manifested permanent face up.
|
||||
//
|
||||
// so keep all tune face up abilities and other face down compatible
|
||||
if (ability.getWorksFaceDown()) {
|
||||
// only face up abilities hidden by default (see below), so no needs in setRuleVisible
|
||||
//ability.setRuleVisible(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getRuleVisible() && !ability.getEffects().isEmpty()) {
|
||||
if (ability.getEffects().get(0) instanceof BecomesFaceDownCreatureEffect) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
abilitiesToRemove.add(ability);
|
||||
}
|
||||
|
||||
// add face up abilities
|
||||
// TODO: add here all possible face up like morph/disguis, manifest/cloak?
|
||||
if (object instanceof Permanent) {
|
||||
// as permanent
|
||||
Permanent permanentObject = (Permanent) object;
|
||||
permanentObject.removeAbilities(abilitiesToRemove, sourceId, game);
|
||||
if (turnFaceUpAbility != null) {
|
||||
Ability faceUp = turnFaceUpAbility.copy();
|
||||
faceUp.setRuleVisible(true);
|
||||
permanentObject.addAbility(faceUp, sourceId, game);
|
||||
}
|
||||
} else if (object instanceof CardImpl) {
|
||||
// as card
|
||||
CardImpl cardObject = (CardImpl) object;
|
||||
cardObject.getAbilities().removeAll(abilitiesToRemove);
|
||||
if (turnFaceUpAbility != null) {
|
||||
Ability faceUp = turnFaceUpAbility.copy();
|
||||
faceUp.setRuleVisible(true);
|
||||
cardObject.addAbility(faceUp);
|
||||
}
|
||||
}
|
||||
|
||||
object.getPower().setModifiedBaseValue(2);
|
||||
object.getToughness().setModifiedBaseValue(2);
|
||||
|
||||
// image
|
||||
String tokenName;
|
||||
switch (faceDownType) {
|
||||
case MORPHED:
|
||||
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MORPH;
|
||||
break;
|
||||
case MEGAMORPHED:
|
||||
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH;
|
||||
break;
|
||||
case MANIFESTED:
|
||||
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST;
|
||||
break;
|
||||
case CLOAKED:
|
||||
tokenName = "TODO-CLOAKED";
|
||||
break;
|
||||
case DISGUISED:
|
||||
tokenName = "TODO-DISGUISED";
|
||||
break;
|
||||
case MANUAL:
|
||||
tokenName = TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Un-supported face down type for image: " + faceDownType);
|
||||
}
|
||||
|
||||
Token faceDownToken = new EmptyToken();
|
||||
TokenInfo faceDownInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(tokenName, object.getId());
|
||||
if (faceDownInfo != null) {
|
||||
faceDownToken.setExpansionSetCode(faceDownInfo.getSetCode());
|
||||
faceDownToken.setCardNumber("0");
|
||||
faceDownToken.setImageFileName(faceDownInfo.getName());
|
||||
faceDownToken.setImageNumber(faceDownInfo.getImageNumber());
|
||||
} else {
|
||||
logger.error("Can't find face down image for " + tokenName + ": " + originalObjectInfo);
|
||||
// TODO: add default image like backface (warning, missing image info must be visible in card popup)?
|
||||
}
|
||||
|
||||
CardUtil.copySetAndCardNumber(object, faceDownToken);
|
||||
|
||||
// hide rarity info
|
||||
if (object instanceof Card) {
|
||||
((Card) object).setRarity(Rarity.SPECIAL);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ public class BestowAbility extends SpellAbility {
|
|||
public static void becomeCreature(Permanent permanent, Game game) {
|
||||
// permanently changes to the object
|
||||
if (permanent != null) {
|
||||
MageObject basicObject = permanent.getBasicMageObject(game);
|
||||
MageObject basicObject = permanent.getBasicMageObject();
|
||||
if (basicObject != null) {
|
||||
game.checkStateAndTriggered(); // Bug #8157
|
||||
basicObject.getSubtype().remove(SubType.AURA);
|
||||
|
|
@ -164,7 +164,7 @@ class BestowEntersBattlefieldEffect extends ReplacementEffectImpl {
|
|||
}
|
||||
|
||||
// change types permanently
|
||||
MageObject basicObject = bestowPermanent.getBasicMageObject(game);
|
||||
MageObject basicObject = bestowPermanent.getBasicMageObject();
|
||||
if (basicObject != null && !basicObject.getSubtype().contains(SubType.AURA)) {
|
||||
basicObject.addSubType(SubType.AURA);
|
||||
basicObject.removeCardType(CardType.CREATURE);
|
||||
|
|
|
|||
|
|
@ -463,4 +463,9 @@ public class ForetellAbility extends SpecialAction {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
public static boolean isCardInForetell(Card card, Game game) {
|
||||
// searching ForetellCostAbility - it adds for foretelled cards only after exile
|
||||
return card.getAbilities(game).containsClass(ForetellCostAbility.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package mage.abilities.keyword;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.ObjectColor;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
|
|
@ -13,18 +11,17 @@ import mage.abilities.costs.mana.ManaCost;
|
|||
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
|
||||
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect.FaceDownType;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.game.permanent.token.EmptyToken;
|
||||
import mage.game.permanent.token.Token;
|
||||
import mage.util.CardUtil;
|
||||
import mage.constants.SpellAbilityCastMode;
|
||||
import mage.constants.SpellAbilityType;
|
||||
import mage.constants.TimingRule;
|
||||
|
||||
/**
|
||||
* Morph and Megamorph
|
||||
* <p>
|
||||
* 702.36. Morph
|
||||
* <p>
|
||||
* 702.36a Morph is a static ability that functions in any zone from which you
|
||||
* could play the card its on, and the morph effect works any time the card is
|
||||
* could play the card it's on, and the morph effect works any time the card is
|
||||
* face down. "Morph [cost]" means "You may cast this card as a 2/2 face-down
|
||||
* creature, with no text, no name, no subtypes, and no mana cost by paying {3}
|
||||
* rather than paying its mana cost." (See rule 707, "Face-Down Spells and
|
||||
|
|
@ -33,7 +30,7 @@ import mage.util.CardUtil;
|
|||
* 702.36b To cast a card using its morph ability, turn it face down. It becomes
|
||||
* a 2/2 face-down creature card, with no text, no name, no subtypes, and no
|
||||
* mana cost. Any effects or prohibitions that would apply to casting a card
|
||||
* with these characteristics (and not the face-up cards characteristics) are
|
||||
* with these characteristics (and not the face-up card's characteristics) are
|
||||
* applied to casting this card. These values are the copiable values of that
|
||||
* object's characteristics. (See rule 613, "Interaction of Continuous Effects,"
|
||||
* and rule 706, "Copying Objects.") Put it onto the stack (as a face-down spell
|
||||
|
|
@ -48,54 +45,54 @@ import mage.util.CardUtil;
|
|||
* <p>
|
||||
* 702.36d If you have priority, you may turn a face-down permanent you control
|
||||
* face up. This is a special action; it doesn't use the stack (see rule 115).
|
||||
* To do this, show all players what the permanents morph cost would be if it
|
||||
* To do this, show all players what the permanent's morph cost would be if it
|
||||
* were face up, pay that cost, then turn the permanent face up. (If the
|
||||
* permanent wouldn't have a morph cost if it were face up, it cant be turned
|
||||
* permanent wouldn't have a morph cost if it were face up, it can't be turned
|
||||
* face up this way.) The morph effect on it ends, and it regains its normal
|
||||
* characteristics. Any abilities relating to the permanent entering the
|
||||
* battlefield dont trigger when its turned face up and dont have any effect,
|
||||
* battlefield don't trigger when it's turned face up and don't have any effect,
|
||||
* because the permanent has already entered the battlefield.
|
||||
* <p>
|
||||
* 702.36e See rule 707, "Face-Down Spells and Permanents," for more information
|
||||
* on how to cast cards with morph.
|
||||
*
|
||||
* @author LevelX2
|
||||
* @author LevelX2, JayDi85
|
||||
*/
|
||||
public class MorphAbility extends SpellAbility {
|
||||
|
||||
protected static final String ABILITY_KEYWORD = "Morph";
|
||||
protected static final String ABILITY_KEYWORD_MEGA = "Megamorph";
|
||||
protected static final String REMINDER_TEXT = "You may cast this card face down as a "
|
||||
+ "2/2 creature for {3}. Turn it face up any time for its morph cost.";
|
||||
|
||||
protected static final String ABILITY_KEYWORD_MEGA = "Megamorph";
|
||||
protected static final String REMINDER_TEXT_MEGA = "You may cast this card face down "
|
||||
+ "as a 2/2 creature for {3}. Turn it face up any time for its megamorph "
|
||||
+ "cost and put a +1/+1 counter on it.";
|
||||
protected Costs<Cost> morphCosts;
|
||||
// needed to check activation status, if card changes zone after casting it
|
||||
private final boolean megamorph;
|
||||
|
||||
public MorphAbility(Card card, Cost morphCost) {
|
||||
this(card, morphCost, false);
|
||||
}
|
||||
|
||||
public MorphAbility(Card card, Cost morphCost, boolean megamorph) {
|
||||
public MorphAbility(Card card, Cost morphCost, boolean useMegamorph) {
|
||||
super(new GenericManaCost(3), card.getName());
|
||||
this.timing = TimingRule.SORCERY;
|
||||
this.morphCosts = new CostsImpl<>();
|
||||
this.morphCosts.add(morphCost);
|
||||
this.megamorph = megamorph;
|
||||
this.setSpellAbilityCastMode(SpellAbilityCastMode.MORPH);
|
||||
this.setSpellAbilityCastMode(useMegamorph ? SpellAbilityCastMode.MEGAMORPH : SpellAbilityCastMode.MORPH);
|
||||
this.setSpellAbilityType(SpellAbilityType.BASE_ALTERNATE);
|
||||
|
||||
// face down effect (hidden by default, visible in face down objects)
|
||||
Ability ability = new SimpleStaticAbility(new BecomesFaceDownCreatureEffect(
|
||||
morphCosts, (megamorph ? FaceDownType.MEGAMORPHED : FaceDownType.MORPHED)));
|
||||
morphCosts, (useMegamorph ? FaceDownType.MEGAMORPHED : FaceDownType.MORPHED)));
|
||||
ability.setWorksFaceDown(true);
|
||||
ability.setRuleVisible(false);
|
||||
this.timing = TimingRule.SORCERY;
|
||||
addSubAbility(ability);
|
||||
}
|
||||
|
||||
protected MorphAbility(final MorphAbility ability) {
|
||||
super(ability);
|
||||
this.morphCosts = ability.morphCosts; // can't be changed
|
||||
this.megamorph = ability.megamorph;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -110,55 +107,20 @@ public class MorphAbility extends SpellAbility {
|
|||
@Override
|
||||
public String getRule() {
|
||||
boolean isMana = morphCosts.get(0) instanceof ManaCost;
|
||||
String name = megamorph ? ABILITY_KEYWORD_MEGA : ABILITY_KEYWORD;
|
||||
String reminder = " <i>(" + (megamorph ? REMINDER_TEXT_MEGA : REMINDER_TEXT) + ")</i>";
|
||||
return name + (isMana ? " " : "—") +
|
||||
morphCosts.getText() + (isMana ? ' ' : ". ") + reminder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide all info and make it a 2/2 creature
|
||||
*
|
||||
* @param targetObject
|
||||
* @param sourcePermanent source of the face down status
|
||||
* @param game
|
||||
*/
|
||||
public static void setPermanentToFaceDownCreature(MageObject targetObject, Permanent sourcePermanent, Game game) {
|
||||
targetObject.getPower().setModifiedBaseValue(2);
|
||||
targetObject.getToughness().setModifiedBaseValue(2);
|
||||
targetObject.getAbilities().clear();
|
||||
targetObject.getColor(game).setColor(new ObjectColor());
|
||||
targetObject.setName("");
|
||||
targetObject.removeAllCardTypes(game);
|
||||
targetObject.addCardType(game, CardType.CREATURE);
|
||||
targetObject.removeAllSubTypes(game);
|
||||
targetObject.removeAllSuperTypes(game);
|
||||
targetObject.getManaCost().clear();
|
||||
|
||||
Token emptyImage = new EmptyToken();
|
||||
|
||||
// TODO: add morph image here?
|
||||
if (targetObject instanceof Permanent) {
|
||||
// hide image info
|
||||
CardUtil.copySetAndCardNumber(targetObject, emptyImage);
|
||||
// hide rarity info
|
||||
((Permanent) targetObject).setRarity(Rarity.SPECIAL);
|
||||
} else if (targetObject instanceof Token) {
|
||||
CardUtil.copySetAndCardNumber(targetObject, emptyImage);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Wrong code usage: un-supported targetObject in face down method: " + targetObject.getClass().getSimpleName());
|
||||
String text;
|
||||
String reminder;
|
||||
switch (this.getSpellAbilityCastMode()) {
|
||||
case MORPH:
|
||||
text = ABILITY_KEYWORD;
|
||||
reminder = REMINDER_TEXT;
|
||||
break;
|
||||
case MEGAMORPH:
|
||||
text = ABILITY_KEYWORD_MEGA;
|
||||
reminder = REMINDER_TEXT_MEGA;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Un-supported spell ability cast mode for morph: " + this.getSpellAbilityCastMode());
|
||||
}
|
||||
}
|
||||
public static void setCardToFaceDownCreature(Card targetCard) {
|
||||
targetCard.getPower().setModifiedBaseValue(2);
|
||||
targetCard.getToughness().setModifiedBaseValue(2);
|
||||
targetCard.getAbilities().clear();
|
||||
targetCard.getColor().setColor(new ObjectColor());
|
||||
targetCard.setName("");
|
||||
targetCard.removeAllCardTypes();
|
||||
targetCard.addCardType(CardType.CREATURE);
|
||||
targetCard.getSubtype().clear();
|
||||
targetCard.removeAllSuperTypes();
|
||||
targetCard.getManaCost().clear();
|
||||
return text + (isMana ? " " : "—") + morphCosts.getText() + (isMana ? ' ' : ". ") + " <i>(" + reminder + ")</i>";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,27 +80,34 @@ public class ProtectionAbility extends StaticAbility {
|
|||
}
|
||||
|
||||
public boolean canTarget(MageObject source, Game game) {
|
||||
// TODO: need research, protection ability can be bugged with aura and aura permanents, spells (see below)
|
||||
|
||||
// permanent restriction
|
||||
if (filter instanceof FilterPermanent) {
|
||||
if (source instanceof Permanent) {
|
||||
return !filter.match(source, game);
|
||||
return !((FilterPermanent) filter).match((Permanent) source, game);
|
||||
}
|
||||
// TODO: possible bugged, need token too?
|
||||
return true;
|
||||
}
|
||||
|
||||
// card restriction
|
||||
if (filter instanceof FilterCard) {
|
||||
if (source instanceof Permanent) {
|
||||
return !((FilterCard) filter).match((Card) source, ((Permanent) source).getControllerId(), this, game);
|
||||
} else if (source instanceof Card) {
|
||||
return !((FilterCard) filter).match((Card) source, ((Card) source).getOwnerId(), this, game);
|
||||
if (source instanceof Card) {
|
||||
return !((FilterCard) filter).match((Card) source, ((Card) source).getControllerOrOwnerId(), this, game);
|
||||
} else if (source instanceof Token) {
|
||||
// Fake a permanent with the Token info.
|
||||
PermanentToken token = new PermanentToken((Token) source, null, game);
|
||||
return !((FilterCard) filter).match((Card) token, game);
|
||||
// make fake permanent cause it checked before real permanent create
|
||||
// warning, Token don't have controllerId info, so it can be a problem here
|
||||
// TODO: wtf, possible bugged for filters that checking controller/player (if so then use with controllerId param)
|
||||
PermanentToken fakePermanent = new PermanentToken((Token) source, UUID.randomUUID(), game);
|
||||
return !((FilterCard) filter).match(fakePermanent, game);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// spell restriction
|
||||
if (filter instanceof FilterSpell) {
|
||||
// TODO: need research, possible bugged
|
||||
// Problem here is that for the check if a player can play a Spell, the source
|
||||
// object is still a card and not a spell yet.
|
||||
if (source instanceof Spell || game.inCheckPlayableState() && source.isInstantOrSorcery(game)) {
|
||||
|
|
@ -109,16 +116,20 @@ public class ProtectionAbility extends StaticAbility {
|
|||
return true;
|
||||
}
|
||||
|
||||
// unknown restriction
|
||||
if (filter instanceof FilterObject) {
|
||||
return !filter.match(source, game);
|
||||
return !((FilterObject) filter).match(source, game);
|
||||
}
|
||||
|
||||
// player restriction
|
||||
if (filter instanceof FilterPlayer) {
|
||||
Player player = null;
|
||||
if (source instanceof Permanent) {
|
||||
player = game.getPlayer(((Permanent) source).getControllerId());
|
||||
} else if (source instanceof Card) {
|
||||
player = game.getPlayer(((Card) source).getOwnerId());
|
||||
if (source instanceof Card) {
|
||||
player = game.getPlayer(((Card) source).getControllerOrOwnerId());
|
||||
} else if (source instanceof Token) {
|
||||
// TODO: fakePermanent will not work here like above, so try to rework whole logic
|
||||
throw new IllegalArgumentException("Wrong code usage: token can't be checked in player restriction filter");
|
||||
|
||||
}
|
||||
return !((FilterPlayer) filter).match(player, this.getControllerId(), this, game);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import mage.counters.Counters;
|
|||
import mage.filter.FilterMana;
|
||||
import mage.game.Game;
|
||||
import mage.game.GameState;
|
||||
import mage.game.Ownerable;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.util.ManaUtil;
|
||||
import mage.watchers.common.CommanderPlaysCountWatcher;
|
||||
|
|
@ -21,12 +22,12 @@ import mage.watchers.common.CommanderPlaysCountWatcher;
|
|||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface Card extends MageObject {
|
||||
|
||||
UUID getOwnerId();
|
||||
public interface Card extends MageObject, Ownerable {
|
||||
|
||||
Rarity getRarity(); // null for tokens
|
||||
|
||||
void setRarity(Rarity rarity);
|
||||
|
||||
void setOwnerId(UUID ownerId);
|
||||
|
||||
/**
|
||||
|
|
@ -46,7 +47,11 @@ public interface Card extends MageObject {
|
|||
|
||||
List<String> getRules(Game game); // gets card rules + in game modifications
|
||||
|
||||
void checkForCountersToAdd(Permanent permanent, Ability source, Game game);
|
||||
/**
|
||||
* Find ETB counters and apply it to permanent.
|
||||
* Warning, it's one time action, use it before a put to battlefield only.
|
||||
*/
|
||||
void applyEnterWithCounters(Permanent permanent, Ability source, Game game);
|
||||
|
||||
void setFaceDown(boolean value, Game game);
|
||||
|
||||
|
|
@ -143,10 +148,12 @@ public interface Card extends MageObject {
|
|||
List<Mana> getMana();
|
||||
|
||||
/**
|
||||
* @return true if there exists various art images for this card
|
||||
* Set contains multiple cards with same card name but different images. Used for image path generation.
|
||||
*/
|
||||
boolean getUsesVariousArt();
|
||||
|
||||
void setUsesVariousArt(boolean usesVariousArt);
|
||||
|
||||
Counters getCounters(Game game);
|
||||
|
||||
Counters getCounters(GameState state);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import mage.abilities.keyword.FlashbackAbility;
|
|||
import mage.abilities.keyword.ReconfigureAbility;
|
||||
import mage.abilities.keyword.SunburstAbility;
|
||||
import mage.abilities.mana.ActivatedManaAbilityImpl;
|
||||
import mage.cards.mock.MockableCard;
|
||||
import mage.cards.repository.PluginClassloaderRegistery;
|
||||
import mage.constants.*;
|
||||
import mage.counters.Counter;
|
||||
|
|
@ -58,12 +59,15 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
protected CardImpl(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costs) {
|
||||
this(ownerId, setInfo, cardTypes, costs, SpellAbilityType.BASE);
|
||||
}
|
||||
|
||||
protected CardImpl(UUID ownerId, CardSetInfo setInfo, CardType[] cardTypes, String costs, SpellAbilityType spellAbilityType) {
|
||||
this(ownerId, setInfo.getName());
|
||||
|
||||
this.rarity = setInfo.getRarity();
|
||||
this.setExpansionSetCode(setInfo.getExpansionSetCode());
|
||||
this.setCardNumber(setInfo.getCardNumber());
|
||||
this.setImageFileName(""); // use default
|
||||
this.setImageNumber(0);
|
||||
this.cardType.addAll(Arrays.asList(cardTypes));
|
||||
this.manaCost.load(costs);
|
||||
setDefaultColor();
|
||||
|
|
@ -119,12 +123,24 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
ownerId = card.ownerId;
|
||||
rarity = card.rarity;
|
||||
|
||||
// TODO: wtf, do not copy card sides cause it must be re-created each time (see details in getSecondCardFace)
|
||||
// must be reworked to normal copy and workable transform without such magic
|
||||
|
||||
nightCard = card.nightCard;
|
||||
secondSideCardClazz = card.secondSideCardClazz;
|
||||
secondSideCard = null; // will be set on first getSecondCardFace call if card has one
|
||||
nightCard = card.nightCard;
|
||||
if (card.secondSideCard instanceof MockableCard) {
|
||||
// workaround to support gui's mock cards
|
||||
secondSideCard = card.secondSideCard.copy();
|
||||
}
|
||||
|
||||
meldsWithClazz = card.meldsWithClazz;
|
||||
meldsToClazz = card.meldsToClazz;
|
||||
meldsToCard = null; // will be set on first getMeldsToCard call if card has one
|
||||
if (card.meldsToCard instanceof MockableCard) {
|
||||
// workaround to support gui's mock cards
|
||||
meldsToCard = card.meldsToCard.copy();
|
||||
}
|
||||
|
||||
spellAbility = null; // will be set on first getSpellAbility call if card has one
|
||||
flipCard = card.flipCard;
|
||||
|
|
@ -203,6 +219,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
return rarity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRarity(Rarity rarity) {
|
||||
this.rarity = rarity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInfo(String key, String value, Game game) {
|
||||
game.getState().getCardState(objectId).addInfo(key, value);
|
||||
|
|
@ -360,7 +381,12 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
@Override
|
||||
public void setOwnerId(UUID ownerId) {
|
||||
this.ownerId = ownerId;
|
||||
abilities.setControllerId(ownerId);
|
||||
this.abilities.setControllerId(ownerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return getOwnerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -549,7 +575,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void checkForCountersToAdd(Permanent permanent, Ability source, Game game) {
|
||||
public void applyEnterWithCounters(Permanent permanent, Ability source, Game game) {
|
||||
Counters countersToAdd = game.getEnterWithCounters(permanent.getId());
|
||||
if (countersToAdd != null) {
|
||||
for (Counter counter : countersToAdd.values()) {
|
||||
|
|
@ -620,6 +646,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
if (secondSideCard == null) {
|
||||
secondSideCard = initSecondSideCard(secondSideCardClazz);
|
||||
if (secondSideCard != null && secondSideCard.getSpellAbility() != null) {
|
||||
// TODO: wtf, why it set cast mode here?! Transform tests fails without it
|
||||
// must be reworked without that magic, also see CardImpl'constructor for copy code
|
||||
secondSideCard.getSpellAbility().setSourceId(this.getId());
|
||||
secondSideCard.getSpellAbility().setSpellAbilityType(SpellAbilityType.BASE_ALTERNATE);
|
||||
secondSideCard.getSpellAbility().setSpellAbilityCastMode(SpellAbilityCastMode.TRANSFORMED);
|
||||
|
|
@ -693,6 +721,11 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
return usesVariousArt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsesVariousArt(boolean usesVariousArt) {
|
||||
this.usesVariousArt = usesVariousArt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Counters getCounters(Game game) {
|
||||
return getCounters(game.getState());
|
||||
|
|
@ -703,13 +736,6 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
return state.getCardState(this.objectId).getCounters();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The controller if available otherwise the owner.
|
||||
*/
|
||||
protected UUID getControllerOrOwner() {
|
||||
return ownerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addCounters(Counter counter, Ability source, Game game) {
|
||||
return addCounters(counter, source.getControllerId(), source, game);
|
||||
|
|
@ -787,7 +813,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
if (!getCounters(game).removeCounter(name, 1)) {
|
||||
break;
|
||||
}
|
||||
GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, objectId, source, getControllerOrOwner());
|
||||
GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTER_REMOVED, objectId, source, getControllerOrOwnerId());
|
||||
if (source != null
|
||||
&& source.getControllerId() != null) {
|
||||
event.setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counter
|
||||
|
|
@ -796,7 +822,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
game.fireEvent(event);
|
||||
finalAmount++;
|
||||
}
|
||||
GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, objectId, source, getControllerOrOwner());
|
||||
GameEvent event = GameEvent.getEvent(GameEvent.EventType.COUNTERS_REMOVED, objectId, source, getControllerOrOwnerId());
|
||||
if (source != null
|
||||
&& source.getControllerId() != null) {
|
||||
event.setPlayerId(source.getControllerId()); // player who controls the source ability that removed the counter
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import java.util.List;
|
|||
*
|
||||
* @author North
|
||||
*/
|
||||
public class MockCard extends CardImpl {
|
||||
public class MockCard extends CardImpl implements MockableCard {
|
||||
|
||||
static public String ADVENTURE_NAME_SEPARATOR = " // ";
|
||||
static public String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // ";
|
||||
|
|
@ -42,6 +42,8 @@ public class MockCard extends CardImpl {
|
|||
super(null, card.getName());
|
||||
this.setExpansionSetCode(card.getSetCode());
|
||||
this.setCardNumber(card.getCardNumber());
|
||||
this.setImageFileName(""); // use default
|
||||
this.setImageNumber(0);
|
||||
this.power = mageIntFromString(card.getPower());
|
||||
this.toughness = mageIntFromString(card.getToughness());
|
||||
this.rarity = card.getRarity();
|
||||
|
|
@ -157,7 +159,7 @@ public class MockCard extends CardImpl {
|
|||
if (adventureSpellName != null) {
|
||||
return getName() + ADVENTURE_NAME_SEPARATOR + adventureSpellName;
|
||||
} else if (isModalDoubleFacedCard) {
|
||||
return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.secondSideCard.getName();
|
||||
return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.getSecondCardFace().getName();
|
||||
} else {
|
||||
return getName();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import java.util.List;
|
|||
/**
|
||||
* @author North
|
||||
*/
|
||||
public class MockSplitCard extends SplitCard {
|
||||
public class MockSplitCard extends SplitCard implements MockableCard {
|
||||
|
||||
public MockSplitCard(CardInfo card) {
|
||||
super(null, new CardSetInfo(card.getName(), card.getSetCode(), card.getCardNumber(), card.getRarity()),
|
||||
card.getTypes().toArray(new CardType[0]),
|
||||
|
|
|
|||
9
Mage/src/main/java/mage/cards/mock/MockableCard.java
Normal file
9
Mage/src/main/java/mage/cards/mock/MockableCard.java
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package mage.cards.mock;
|
||||
|
||||
/**
|
||||
* Card is mock, e.g. used in GUI only like deck editor
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public interface MockableCard {
|
||||
}
|
||||
|
|
@ -18,6 +18,20 @@ public enum TokenRepository {
|
|||
|
||||
public static final String XMAGE_TOKENS_SET_CODE = "XMAGE";
|
||||
|
||||
// All possible image names. Used for:
|
||||
// - image name from tok/xmage folder
|
||||
// - additional card name for controller like "Morph: face up name"
|
||||
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL = "Face Down";
|
||||
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST = "Manifest";
|
||||
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MORPH = "Morph";
|
||||
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH = "Megamorph";
|
||||
public static final String XMAGE_IMAGE_NAME_FACE_DOWN_FORETELL = "Foretell";
|
||||
public static final String XMAGE_IMAGE_NAME_COPY = "Copy";
|
||||
public static final String XMAGE_IMAGE_NAME_CITY_BLESSING = "City's Blessing";
|
||||
public static final String XMAGE_IMAGE_NAME_DAY = "Day";
|
||||
public static final String XMAGE_IMAGE_NAME_NIGHT = "Night";
|
||||
public static final String XMAGE_IMAGE_NAME_THE_MONARCH = "The Monarch";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(TokenRepository.class);
|
||||
|
||||
private ArrayList<TokenInfo> allTokens = new ArrayList<>();
|
||||
|
|
@ -229,44 +243,60 @@ public enum TokenRepository {
|
|||
// Search by
|
||||
// - https://tagger.scryfall.com/tags/card/assistant-cards
|
||||
// - https://scryfall.com/search?q=otag%3Aassistant-cards&unique=cards&as=grid&order=name
|
||||
// Must add only unique prints
|
||||
// Must add only unique images/prints
|
||||
// TODO: add custom set in download window to download a custom tokens only
|
||||
// TODO: add custom set in card viewer to view a custom tokens only
|
||||
ArrayList<TokenInfo> res = new ArrayList<>();
|
||||
|
||||
// Backface
|
||||
// TODO: can't find backface's api url so use direct link from third party site instead (must be replaced to scryfall someday)
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANUAL, 1, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"));
|
||||
|
||||
// Copy
|
||||
// https://scryfall.com/search?q=include%3Aextras+unique%3Aprints+type%3Atoken+copy&unique=cards&as=grid&order=name
|
||||
res.add(createXmageToken("Copy", 1, "https://api.scryfall.com/cards/tclb/19/en?format=image"));
|
||||
res.add(createXmageToken("Copy", 2, "https://api.scryfall.com/cards/tsnc/1/en?format=image"));
|
||||
res.add(createXmageToken("Copy", 3, "https://api.scryfall.com/cards/tvow/19/en?format=image"));
|
||||
res.add(createXmageToken("Copy", 4, "https://api.scryfall.com/cards/tznr/12/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 1, "https://api.scryfall.com/cards/tclb/19/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 2, "https://api.scryfall.com/cards/tsnc/1/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 3, "https://api.scryfall.com/cards/tvow/19/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 4, "https://api.scryfall.com/cards/tznr/12/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 5, "https://api.scryfall.com/cards/twho/1/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 6, "https://api.scryfall.com/cards/tlci/1/en?format=image"));
|
||||
|
||||
// City's Blessing
|
||||
// https://scryfall.com/search?q=type%3Atoken+include%3Aextras+unique%3Aprints+City%27s+Blessing+&unique=cards&as=grid&order=name
|
||||
res.add(createXmageToken("City's Blessing", 1, "https://api.scryfall.com/cards/f18/2/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_CITY_BLESSING, 1, "https://api.scryfall.com/cards/f18/2/en?format=image"));
|
||||
|
||||
// Day // Night
|
||||
// https://scryfall.com/search?q=include%3Aextras+unique%3Aprints+%22Day+%2F%2F+Night%22&unique=cards&as=grid&order=name
|
||||
res.add(createXmageToken("Day", 1, "https://api.scryfall.com/cards/tvow/21/en?format=image&face=front"));
|
||||
res.add(createXmageToken("Night", 1, "https://api.scryfall.com/cards/tvow/21/en?format=image&face=back"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_DAY, 1, "https://api.scryfall.com/cards/tvow/21/en?format=image&face=front"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_NIGHT, 1, "https://api.scryfall.com/cards/tvow/21/en?format=image&face=back"));
|
||||
|
||||
// Manifest
|
||||
// https://scryfall.com/search?q=Manifest+include%3Aextras+unique%3Aprints&unique=cards&as=grid&order=name
|
||||
res.add(createXmageToken("Manifest", 1, "https://api.scryfall.com/cards/tc19/28/en?format=image"));
|
||||
res.add(createXmageToken("Manifest", 2, "https://api.scryfall.com/cards/tc18/1/en?format=image"));
|
||||
res.add(createXmageToken("Manifest", 3, "https://api.scryfall.com/cards/tfrf/4/en?format=image"));
|
||||
res.add(createXmageToken("Manifest", 4, "https://api.scryfall.com/cards/tncc/3/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, 1, "https://api.scryfall.com/cards/tc19/28/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, 2, "https://api.scryfall.com/cards/tc18/1/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, 3, "https://api.scryfall.com/cards/tfrf/4/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, 4, "https://api.scryfall.com/cards/tncc/3/en?format=image"));
|
||||
|
||||
// Morph
|
||||
// https://scryfall.com/search?q=Morph+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name
|
||||
res.add(createXmageToken("Morph", 1, "https://api.scryfall.com/cards/tktk/11/en?format=image"));
|
||||
res.add(createXmageToken("Morph", 2, "https://api.scryfall.com/cards/ta25/15/en?format=image"));
|
||||
res.add(createXmageToken("Morph", 3, "https://api.scryfall.com/cards/tc19/27/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, 1, "https://api.scryfall.com/cards/tktk/11/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, 2, "https://api.scryfall.com/cards/ta25/15/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MORPH, 3, "https://api.scryfall.com/cards/tc19/27/en?format=image"));
|
||||
|
||||
// Megamorph
|
||||
// warning, mtg don't have megamorph tokens yet so use morph instead (users will see the diff by card name and face up ability text)
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH, 1, "https://api.scryfall.com/cards/tktk/11/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH, 2, "https://api.scryfall.com/cards/ta25/15/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_MEGAMORPH, 3, "https://api.scryfall.com/cards/tc19/27/en?format=image"));
|
||||
|
||||
// Foretell
|
||||
// https://scryfall.com/search?q=Foretell+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_FACE_DOWN_FORETELL, 1, "https://api.scryfall.com/cards/tkhm/23/en?format=image"));
|
||||
|
||||
// The Monarch
|
||||
// https://scryfall.com/search?q=Monarch+unique%3Aprints+otag%3Aassistant-cards&unique=cards&as=grid&order=name
|
||||
res.add(createXmageToken("The Monarch", 1, "https://api.scryfall.com/cards/tonc/22/en?format=image"));
|
||||
res.add(createXmageToken("The Monarch", 2, "https://api.scryfall.com/cards/tcn2/1/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_MONARCH, 1, "https://api.scryfall.com/cards/tonc/22/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_MONARCH, 2, "https://api.scryfall.com/cards/tcn2/1/en?format=image"));
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_MONARCH, 3, "https://api.scryfall.com/cards/tltc/15/en?format=image"));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
@ -307,4 +337,33 @@ public enum TokenRepository {
|
|||
public TokenInfo findPreferredTokenInfoForClass(String className, String preferredSetCode) {
|
||||
return findPreferredTokenInfo(TokenRepository.instance.getByClassName(className), preferredSetCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find random image info by related set code (use for inner tokens like copy, morph, etc)
|
||||
* <p>
|
||||
* Allow to generate "random" image number from an object's UUID (workaround to keep same image after each update)
|
||||
*
|
||||
* @param randomFromId object's UUID for image number generation
|
||||
*/
|
||||
public TokenInfo findPreferredTokenInfoForXmage(String name, UUID randomFromId) {
|
||||
List<TokenInfo> needList = TokenRepository.instance.getByType(TokenType.XMAGE)
|
||||
.stream()
|
||||
.filter(info -> info.getName().equals(name))
|
||||
.collect(Collectors.toList());
|
||||
if (needList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (needList.size() == 1) {
|
||||
return needList.get(0);
|
||||
}
|
||||
|
||||
// workaround to find stable image from object's id (need for face down image generation)
|
||||
if (randomFromId == null) {
|
||||
return RandomUtil.randomFromCollection(needList);
|
||||
} else {
|
||||
// warning, do not use global random here (it can break it with same seed)
|
||||
int itemIndex = new Random(randomFromId.getLeastSignificantBits()).nextInt(needList.size());
|
||||
return needList.get(itemIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
package mage.constants;
|
||||
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
|
||||
import mage.abilities.keyword.BestowAbility;
|
||||
import mage.abilities.keyword.PrototypeAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.abilities.keyword.MorphAbility;
|
||||
import mage.game.Game;
|
||||
import mage.game.stack.Spell;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author LevelX2
|
||||
*/
|
||||
|
|
@ -16,16 +22,24 @@ public enum SpellAbilityCastMode {
|
|||
FLASHBACK("Flashback"),
|
||||
BESTOW("Bestow"),
|
||||
PROTOTYPE("Prototype"),
|
||||
MORPH("Morph"),
|
||||
MORPH("Morph", false, SpellAbilityCastMode.MORPH_ADDITIONAL_RULE),
|
||||
MEGAMORPH("Megamorph", false, SpellAbilityCastMode.MORPH_ADDITIONAL_RULE),
|
||||
TRANSFORMED("Transformed", true),
|
||||
DISTURB("Disturb", true),
|
||||
MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true);
|
||||
|
||||
private static final String MORPH_ADDITIONAL_RULE = "You may cast this card as a 2/2 face-down creature, with no text,"
|
||||
+ " no name, no subtypes, and no mana cost by paying {3} rather than paying its mana cost.";
|
||||
|
||||
private final String text;
|
||||
|
||||
// Should the cast mode use the second face?
|
||||
// should the cast mode use the second face?
|
||||
private final boolean isTransformed;
|
||||
|
||||
// use it to add additional info in stack object cause face down has nothing
|
||||
// TODO: is it possible to use InfoEffect or CardHint instead that?
|
||||
private final List<String> additionalRulesOnStack;
|
||||
|
||||
public boolean isTransformed() {
|
||||
return this.isTransformed;
|
||||
}
|
||||
|
|
@ -35,8 +49,17 @@ public enum SpellAbilityCastMode {
|
|||
}
|
||||
|
||||
SpellAbilityCastMode(String text, boolean isTransformed) {
|
||||
this(text, isTransformed, null);
|
||||
}
|
||||
|
||||
SpellAbilityCastMode(String text, boolean isTransformed, String additionalRulesOnStack) {
|
||||
this.text = text;
|
||||
this.isTransformed = isTransformed;
|
||||
this.additionalRulesOnStack = additionalRulesOnStack == null ? null : Collections.singletonList(additionalRulesOnStack);
|
||||
}
|
||||
|
||||
public List<String> getAdditionalRulesOnStack() {
|
||||
return additionalRulesOnStack;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -44,27 +67,46 @@ public enum SpellAbilityCastMode {
|
|||
return text;
|
||||
}
|
||||
|
||||
public Card getTypeModifiedCardObjectCopy(Card card, SpellAbility spellAbility) {
|
||||
public Card getTypeModifiedCardObjectCopy(Card card, SpellAbility spellAbility, Game game) {
|
||||
Card cardCopy = card.copy();
|
||||
if (this.equals(BESTOW)) {
|
||||
BestowAbility.becomeAura(cardCopy);
|
||||
}
|
||||
if (this.isTransformed) {
|
||||
Card tmp = card.getSecondCardFace();
|
||||
if (tmp != null) {
|
||||
cardCopy = tmp.copy();
|
||||
}
|
||||
}
|
||||
if (this.equals(PROTOTYPE)) {
|
||||
cardCopy = ((PrototypeAbility) spellAbility).prototypeCardSpell(cardCopy);
|
||||
}
|
||||
if (this.equals(MORPH)) {
|
||||
if (cardCopy instanceof Spell) {
|
||||
//Spell doesn't support setName, so make a copy of the card (we're blowing it away anyway)
|
||||
cardCopy = ((Spell) cardCopy).getCard().copy();
|
||||
}
|
||||
MorphAbility.setCardToFaceDownCreature(cardCopy);
|
||||
|
||||
switch (this) {
|
||||
case BESTOW:
|
||||
BestowAbility.becomeAura(cardCopy);
|
||||
break;
|
||||
case PROTOTYPE:
|
||||
cardCopy = ((PrototypeAbility) spellAbility).prototypeCardSpell(cardCopy);
|
||||
break;
|
||||
case MORPH:
|
||||
case MEGAMORPH:
|
||||
if (cardCopy instanceof Spell) {
|
||||
//Spell doesn't support setName, so make a copy of the card (we're blowing it away anyway)
|
||||
// TODO: research - is it possible to apply face down code to spell instead workaround with card
|
||||
cardCopy = ((Spell) cardCopy).getCard().copy();
|
||||
}
|
||||
BecomesFaceDownCreatureEffect.makeFaceDownObject(game, null, cardCopy, BecomesFaceDownCreatureEffect.FaceDownType.MORPHED, null);
|
||||
break;
|
||||
case NORMAL:
|
||||
case MADNESS:
|
||||
case FLASHBACK:
|
||||
case DISTURB:
|
||||
case MORE_THAN_MEETS_THE_EYE:
|
||||
// it changes only cost, so keep other characteristics
|
||||
// TODO: research - why TRANSFORMED here - is it used in this.isTransformed code?!
|
||||
break;
|
||||
case TRANSFORMED:
|
||||
// TODO: research - why TRANSFORMED here - is it used in this.isTransformed code?!
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Un-supported ability cast mode: " + this);
|
||||
}
|
||||
|
||||
return cardCopy;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ import java.util.UUID;
|
|||
/**
|
||||
* @author magenoxx_at_gmail.com
|
||||
*/
|
||||
public interface Controllable {
|
||||
public interface Controllable extends ControllableOrOwnerable {
|
||||
|
||||
UUID getControllerId();
|
||||
|
||||
UUID getId();
|
||||
|
||||
default boolean isControlledBy(UUID controllerID) {
|
||||
if (getControllerId() == null) {
|
||||
return false;
|
||||
|
|
|
|||
16
Mage/src/main/java/mage/game/ControllableOrOwnerable.java
Normal file
16
Mage/src/main/java/mage/game/ControllableOrOwnerable.java
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package mage.game;
|
||||
|
||||
import mage.MageItem;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public interface ControllableOrOwnerable extends MageItem {
|
||||
|
||||
/**
|
||||
* @return the controller if available otherwise the owner
|
||||
*/
|
||||
UUID getControllerOrOwnerId();
|
||||
}
|
||||
|
|
@ -503,6 +503,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
|||
*/
|
||||
void applyEffects();
|
||||
|
||||
@Deprecated // TODO: must research usage and remove it from all non engine code (example: Bestow ability, ProcessActions must be used instead)
|
||||
boolean checkStateAndTriggered();
|
||||
|
||||
void playPriority(UUID activePlayerId, boolean resuming);
|
||||
|
|
@ -589,6 +590,9 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
|||
|
||||
boolean executingRollback();
|
||||
|
||||
/**
|
||||
* Add counters to permanent before ETB. Use it before put real permanent to battlefield.
|
||||
*/
|
||||
void setEnterWithCounters(UUID sourceId, Counters counters);
|
||||
|
||||
Counters getEnterWithCounters(UUID sourceId);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import mage.abilities.effects.Effect;
|
|||
import mage.abilities.effects.PreventionEffectData;
|
||||
import mage.abilities.effects.common.CopyEffect;
|
||||
import mage.abilities.effects.common.InfoEffect;
|
||||
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
|
||||
import mage.abilities.effects.keyword.FinalityCounterEffect;
|
||||
import mage.abilities.effects.keyword.ShieldCounterEffect;
|
||||
import mage.abilities.effects.keyword.StunCounterEffect;
|
||||
|
|
@ -1996,10 +1997,9 @@ public abstract class GameImpl implements Game {
|
|||
|
||||
// workaround to find real copyable characteristics of transformed/facedown/etc permanents
|
||||
|
||||
if (copyFromPermanent.isMorphed()
|
||||
|| copyFromPermanent.isManifested()
|
||||
|| copyFromPermanent.isFaceDown(this)) {
|
||||
MorphAbility.setPermanentToFaceDownCreature(newBluePrint, copyFromPermanent, this);
|
||||
BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(this, copyFromPermanent);
|
||||
if (faceDownType != null) {
|
||||
BecomesFaceDownCreatureEffect.makeFaceDownObject(this, null, newBluePrint, faceDownType, null);
|
||||
}
|
||||
newBluePrint.assignNewId();
|
||||
if (copyFromPermanent.isTransformed()) {
|
||||
|
|
|
|||
10
Mage/src/main/java/mage/game/Ownerable.java
Normal file
10
Mage/src/main/java/mage/game/Ownerable.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package mage.game;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public interface Ownerable extends ControllableOrOwnerable {
|
||||
UUID getOwnerId();
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package mage.game;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.keyword.TransformAbility;
|
||||
import mage.cards.*;
|
||||
import mage.constants.Outcome;
|
||||
|
|
@ -27,7 +26,7 @@ public final class ZonesHandler {
|
|||
|
||||
public static boolean cast(ZoneChangeInfo info, Ability source, Game game) {
|
||||
if (maybeRemoveFromSourceZone(info, game, source)) {
|
||||
placeInDestinationZone(info,0, source, game);
|
||||
placeInDestinationZone(info, 0, source, game);
|
||||
// create a group zone change event if a card is moved to stack for casting (it's always only one card, but some effects check for group events (one or more xxx))
|
||||
Set<Card> cards = new HashSet<>();
|
||||
Set<PermanentToken> tokens = new HashSet<>();
|
||||
|
|
@ -38,12 +37,12 @@ public final class ZonesHandler {
|
|||
cards.add(targetCard);
|
||||
}
|
||||
game.fireEvent(new ZoneChangeGroupEvent(
|
||||
cards,
|
||||
tokens,
|
||||
info.event.getSourceId(),
|
||||
info.event.getSource(),
|
||||
info.event.getPlayerId(),
|
||||
info.event.getFromZone(),
|
||||
cards,
|
||||
tokens,
|
||||
info.event.getSourceId(),
|
||||
info.event.getSource(),
|
||||
info.event.getPlayerId(),
|
||||
info.event.getFromZone(),
|
||||
info.event.getToZone()));
|
||||
// normal movement
|
||||
game.fireEvent(info.event);
|
||||
|
|
@ -325,33 +324,50 @@ public final class ZonesHandler {
|
|||
// Handle all normal cases
|
||||
Card card = getTargetCard(game, event.getTargetId());
|
||||
if (card == null) {
|
||||
// If we can't find the card we can't remove it.
|
||||
// if we can't find the card we can't remove it.
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean success = false;
|
||||
boolean isGoodToMove = false;
|
||||
if (info.faceDown) {
|
||||
card.setFaceDown(true, game);
|
||||
// any card can be moved as face down (doubled faced cards also support face down)
|
||||
isGoodToMove = true;
|
||||
} else if (event.getToZone().equals(Zone.BATTLEFIELD)) {
|
||||
if (!card.isPermanent(game)
|
||||
&& (!card.isTransformable() || Boolean.FALSE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId())))) {
|
||||
// Non permanents (Instants, Sorceries, ... stay in the zone they are if an abilty/effect tries to move it to the battlefield
|
||||
return false;
|
||||
}
|
||||
// non-permanents can't move to battlefield
|
||||
// "return to battlefield transformed" abilities uses game state value instead "info.transformed", so check it too
|
||||
// TODO: possible bug with non permanent on second side like Life // Death, see https://github.com/magefree/mage/issues/11573
|
||||
// need to check second side here, not status only
|
||||
// TODO: possible bug with Nightbound, search all usage of getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED and insert additional check Ability.checkCard
|
||||
boolean wantToPutTransformed = card.isTransformable()
|
||||
&& Boolean.TRUE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()));
|
||||
isGoodToMove = card.isPermanent(game) || wantToPutTransformed;
|
||||
} else {
|
||||
// other zones allows to move
|
||||
isGoodToMove = true;
|
||||
}
|
||||
if (!isGoodToMove) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: is it buggy? Card characteristics are global - if you change face down then it will be
|
||||
// changed in original card too, not in blueprint only
|
||||
card.setFaceDown(info.faceDown, game);
|
||||
|
||||
boolean success = false;
|
||||
if (!game.replaceEvent(event)) {
|
||||
Zone fromZone = event.getFromZone();
|
||||
if (event.getToZone() == Zone.BATTLEFIELD) {
|
||||
// prepare card and permanent
|
||||
// If needed take attributes from the spell (e.g. color of spell was changed)
|
||||
card = takeAttributesFromSpell(card, event, game);
|
||||
// PUT TO BATTLEFIELD AS PERMANENT
|
||||
// prepare card and permanent (card must contain full data, even for face down)
|
||||
// if needed to take attributes from the spell (e.g. color of spell was changed)
|
||||
card = prepareBlueprintCardFromSpell(card, event, game);
|
||||
|
||||
// controlling player can be replaced so use event player now
|
||||
Permanent permanent;
|
||||
if (card instanceof MeldCard) {
|
||||
permanent = new PermanentMeld(card, event.getPlayerId(), game);
|
||||
} else if (card instanceof ModalDoubleFacedCard) {
|
||||
// main mdf card must be processed before that call (e.g. only halfes can be moved to battlefield)
|
||||
// main mdf card must be processed before that call (e.g. only halves can be moved to battlefield)
|
||||
throw new IllegalStateException("Unexpected trying of move mdf card to battlefield instead half");
|
||||
} else if (card instanceof Permanent) {
|
||||
throw new IllegalStateException("Unexpected trying of move permanent to battlefield instead card");
|
||||
|
|
@ -361,11 +377,12 @@ public final class ZonesHandler {
|
|||
|
||||
// put onto battlefield with possible counters
|
||||
game.getPermanentsEntering().put(permanent.getId(), permanent);
|
||||
card.checkForCountersToAdd(permanent, source, game);
|
||||
card.applyEnterWithCounters(permanent, source, game);
|
||||
|
||||
permanent.setTapped(info instanceof ZoneChangeInfo.Battlefield
|
||||
&& ((ZoneChangeInfo.Battlefield) info).tapped);
|
||||
|
||||
|
||||
// if need prototyped version
|
||||
if (Zone.STACK == event.getFromZone()) {
|
||||
Spell spell = game.getStack().getSpell(event.getTargetId());
|
||||
if (spell != null) {
|
||||
|
|
@ -375,29 +392,34 @@ public final class ZonesHandler {
|
|||
|
||||
permanent.setFaceDown(info.faceDown, game);
|
||||
if (info.faceDown) {
|
||||
card.setFaceDown(false, game);
|
||||
// TODO: need research cards with "setFaceDown(false"
|
||||
// TODO: delete after new release and new face down bugs (old code remove face down status from a card for unknown reason), 2024-02-20
|
||||
//card.setFaceDown(false, game);
|
||||
}
|
||||
|
||||
// make sure the controller of all continuous effects of this card are switched to the current controller
|
||||
game.setScopeRelevant(true);
|
||||
game.getContinuousEffects().setController(permanent.getId(), permanent.getControllerId());
|
||||
if (permanent.entersBattlefield(source, game, fromZone, true)
|
||||
&& card.removeFromZone(game, fromZone, source)) {
|
||||
success = true;
|
||||
event.setTarget(permanent);
|
||||
} else {
|
||||
// revert controller to owner if permanent does not enter
|
||||
game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId());
|
||||
game.getPermanentsEntering().remove(permanent.getId());
|
||||
try {
|
||||
game.getContinuousEffects().setController(permanent.getId(), permanent.getControllerId());
|
||||
if (permanent.entersBattlefield(source, game, fromZone, true)
|
||||
&& card.removeFromZone(game, fromZone, source)) {
|
||||
success = true;
|
||||
event.setTarget(permanent);
|
||||
} else {
|
||||
// revert controller to owner if permanent does not enter
|
||||
game.getContinuousEffects().setController(permanent.getId(), permanent.getOwnerId());
|
||||
game.getPermanentsEntering().remove(permanent.getId());
|
||||
}
|
||||
} finally {
|
||||
game.setScopeRelevant(false);
|
||||
}
|
||||
game.setScopeRelevant(false);
|
||||
} else if (event.getTarget() != null) {
|
||||
card.setFaceDown(info.faceDown, game);
|
||||
// PUT PERMANENT TO OTHER ZONE (e.g. remove only)
|
||||
Permanent target = event.getTarget();
|
||||
success = target.removeFromZone(game, fromZone, source)
|
||||
&& game.getPlayer(target.getControllerId()).removeFromBattlefield(target, source, game);
|
||||
} else {
|
||||
card.setFaceDown(info.faceDown, game);
|
||||
// PUT CARD TO OTHER ZONE
|
||||
success = card.removeFromZone(game, fromZone, source);
|
||||
}
|
||||
}
|
||||
|
|
@ -434,17 +456,30 @@ public final class ZonesHandler {
|
|||
return order;
|
||||
}
|
||||
|
||||
private static Card takeAttributesFromSpell(Card card, ZoneChangeEvent event, Game game) {
|
||||
private static Card prepareBlueprintCardFromSpell(Card card, ZoneChangeEvent event, Game game) {
|
||||
card = card.copy();
|
||||
if (Zone.STACK == event.getFromZone()) {
|
||||
// TODO: wtf, why only colors!? Must research and remove colors workaround or add all other data like types too
|
||||
Spell spell = game.getStack().getSpell(event.getTargetId());
|
||||
if (spell != null && !spell.isFaceDown(game)) {
|
||||
// TODO: wtf, why only colors!? Must research and remove colors workaround
|
||||
|
||||
// old version
|
||||
if (false && spell != null && !spell.isFaceDown(game)) {
|
||||
if (!card.getColor(game).equals(spell.getColor(game))) {
|
||||
// the card that is referenced to in the permanent is copied and the spell attributes are set to this copied card
|
||||
card.getColor(game).setColor(spell.getColor(game));
|
||||
}
|
||||
}
|
||||
|
||||
// new version
|
||||
if (true && spell != null && spell.getSpellAbility() != null) {
|
||||
Card characteristics = spell.getSpellAbility().getCharacteristics(game);
|
||||
if (!characteristics.isFaceDown(game)) {
|
||||
if (!card.getColor(game).equals(characteristics.getColor(game))) {
|
||||
// TODO: don't work with prototyped spells (setColor can't set colorless color)
|
||||
card.getColor(game).setColor(characteristics.getColor(game));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ public abstract class CommandObjectImpl implements CommandObject {
|
|||
private UUID id;
|
||||
private String name = "";
|
||||
|
||||
private String expansionSetCode;
|
||||
private String cardNumber;
|
||||
private String expansionSetCode = "";
|
||||
private String cardNumber = "";
|
||||
private String imageFileName = "";
|
||||
private int imageNumber;
|
||||
|
||||
public CommandObjectImpl(String name) {
|
||||
|
|
@ -27,6 +28,7 @@ public abstract class CommandObjectImpl implements CommandObject {
|
|||
this.name = object.name;
|
||||
this.expansionSetCode = object.expansionSetCode;
|
||||
this.cardNumber = object.cardNumber;
|
||||
this.imageFileName = object.imageFileName;
|
||||
this.imageNumber = object.imageNumber;
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +57,16 @@ public abstract class CommandObjectImpl implements CommandObject {
|
|||
this.cardNumber = cardNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageFileName() {
|
||||
return imageFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageFileName(String imageFileName) {
|
||||
this.imageFileName = imageFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getImageNumber() {
|
||||
return imageNumber;
|
||||
|
|
|
|||
|
|
@ -127,6 +127,11 @@ public class Commander extends CommandObjectImpl {
|
|||
return sourceObject.getOwnerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return getControllerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandObject copy() {
|
||||
return new Commander(this);
|
||||
|
|
|
|||
|
|
@ -184,6 +184,11 @@ public class Dungeon extends CommandObjectImpl {
|
|||
this.abilites.setControllerId(controllerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return getControllerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCopy(boolean isCopy, MageObject copyFrom) {
|
||||
this.copy = isCopy;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ public abstract class Emblem extends CommandObjectImpl {
|
|||
if (foundInfo != null) {
|
||||
this.setExpansionSetCode(foundInfo.getSetCode());
|
||||
this.setCardNumber("");
|
||||
this.setImageFileName(""); // use default
|
||||
this.setImageNumber(foundInfo.getImageNumber());
|
||||
} else {
|
||||
// how-to fix: add emblem to the tokens-database
|
||||
|
|
@ -99,6 +100,11 @@ public abstract class Emblem extends CommandObjectImpl {
|
|||
this.abilites.setControllerId(controllerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return getControllerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
abstract public Emblem copy();
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ public abstract class Plane extends CommandObjectImpl {
|
|||
if (foundInfo != null) {
|
||||
this.setExpansionSetCode(foundInfo.getSetCode());
|
||||
this.setCardNumber("");
|
||||
this.setImageFileName(""); // use default
|
||||
this.setImageNumber(foundInfo.getImageNumber());
|
||||
} else {
|
||||
// how-to fix: add plane to the tokens-database
|
||||
|
|
@ -103,6 +104,11 @@ public abstract class Plane extends CommandObjectImpl {
|
|||
this.abilites.setControllerId(controllerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return getControllerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
abstract public Plane copy();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import java.util.stream.Collectors;
|
|||
* mana burn with Yurlok of Scorch Thrash, and anything else players might think of.
|
||||
*/
|
||||
public final class EmblemOfCard extends Emblem {
|
||||
|
||||
private final boolean usesVariousArt;
|
||||
private static final Logger logger = Logger.getLogger(EmblemOfCard.class);
|
||||
|
||||
public static Card lookupCard(
|
||||
String cardName,
|
||||
|
|
@ -75,8 +75,10 @@ public final class EmblemOfCard extends Emblem {
|
|||
return ability;
|
||||
}).collect(Collectors.toList()));
|
||||
this.getAbilities().setSourceId(this.getId());
|
||||
|
||||
this.setExpansionSetCode(card.getExpansionSetCode());
|
||||
this.setCardNumber(card.getCardNumber());
|
||||
this.setImageFileName(card.getImageFileName());
|
||||
this.setImageNumber(card.getImageNumber());
|
||||
this.usesVariousArt = card.getUsesVariousArt();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,4 +103,8 @@ public class ZoneChangeEvent extends GameEvent {
|
|||
return this.source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + ", from " + getFromZone() + " to " + getToZone();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import mage.MageObject;
|
|||
import mage.MageObjectReference;
|
||||
import mage.abilities.Ability;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.Rarity;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Controllable;
|
||||
import mage.game.Game;
|
||||
|
|
@ -113,8 +112,6 @@ public interface Permanent extends Card, Controllable {
|
|||
|
||||
void setExpansionSetCode(String expansionSetCode);
|
||||
|
||||
void setRarity(Rarity rarity);
|
||||
|
||||
void setFlipCard(boolean flipCard);
|
||||
|
||||
void setFlipCardName(String flipCardName);
|
||||
|
|
@ -136,7 +133,7 @@ public interface Permanent extends Card, Controllable {
|
|||
boolean hasProtectionFrom(MageObject source, Game game);
|
||||
|
||||
/**
|
||||
* @param attachment
|
||||
* @param attachment can be any object: card, permanent, token
|
||||
* @param source can be null for default checks like state base
|
||||
* @param game
|
||||
* @param silentMode - use it to ignore warning message for users (e.g. for
|
||||
|
|
@ -194,7 +191,14 @@ public interface Permanent extends Card, Controllable {
|
|||
|
||||
void reset(Game game);
|
||||
|
||||
MageObject getBasicMageObject(Game game);
|
||||
/**
|
||||
* Return original/blueprint/printable object (token or card)
|
||||
* <p>
|
||||
* Original object used on each game cycle for permanent reset and apply all active effects
|
||||
* <p>
|
||||
* Warning, all changes to the original object will be applied forever
|
||||
*/
|
||||
MageObject getBasicMageObject();
|
||||
|
||||
boolean destroy(Ability source, Game game);
|
||||
|
||||
|
|
@ -221,7 +225,7 @@ public interface Permanent extends Card, Controllable {
|
|||
* Add abilities to the permanent, can be used in effects
|
||||
*
|
||||
* @param ability
|
||||
* @param sourceId
|
||||
* @param sourceId can be null
|
||||
* @param game
|
||||
* @return can be null for exists abilities
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,28 +14,30 @@ import mage.cards.SplitCard;
|
|||
import mage.constants.SpellAbilityType;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
import mage.game.permanent.token.Token;
|
||||
|
||||
import javax.annotation.processing.SupportedSourceVersion;
|
||||
import javax.lang.model.SourceVersion;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Static permanent on the battlefield. There are possible multiple permanents per one card,
|
||||
* so be carefull for targets (ids are different) and ZCC (zcc is static for permanent).
|
||||
* so be carefully for targets (ids are different) and ZCC (zcc is static for permanent).
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class PermanentCard extends PermanentImpl {
|
||||
|
||||
// blueprint e.g. a copy of the original card that was cast
|
||||
// (this is not the original card, so it's possible to change some attribute before it enters the battlefield)
|
||||
// TODO: wtf, it modified on getCard/getBasicMageObject/getMainCard() and other places, e.g. on bestow -- must be fixed!
|
||||
protected Card card;
|
||||
|
||||
protected int maxLevelCounters;
|
||||
// A copy of the original card that was cast (this is not the original card, so it's possible to change some attribute to this blueprint to change attributes to the permanent if it enters the battlefield with e.g. a subtype)
|
||||
protected Card card; // TODO: wtf, it modified on getCard and other places, e.g. on bestow -- must be fixed!
|
||||
// the number this permanent instance had
|
||||
protected int zoneChangeCounter;
|
||||
|
||||
public PermanentCard(Card card, UUID controllerId, Game game) {
|
||||
super(card.getId(), card.getOwnerId(), controllerId, card.getName());
|
||||
super(card.getId(), card.getOwnerId(), controllerId, card.getName()); // card id
|
||||
// TODO: wtf, must research - is it possible to have diff ids for same card id?!
|
||||
// ETB with counters depends on card id, not permanent id
|
||||
// TODO: ETB with counters works with tokens?! Must research
|
||||
|
||||
// runtime check: must use real card only inside
|
||||
if (card instanceof PermanentCard) {
|
||||
|
|
@ -124,7 +126,7 @@ public class PermanentCard extends PermanentImpl {
|
|||
this.abilities.setSourceId(objectId);
|
||||
this.cardType.clear();
|
||||
this.cardType.addAll(card.getCardType());
|
||||
this.color = card.getColor(null).copy();
|
||||
this.color = card.getColor(null).copy(); // TODO: need research - why it null
|
||||
this.frameColor = card.getFrameColor(game).copy();
|
||||
this.frameStyle = card.getFrameStyle();
|
||||
this.manaCost = card.getManaCost().copy();
|
||||
|
|
@ -134,10 +136,12 @@ public class PermanentCard extends PermanentImpl {
|
|||
this.subtype.copyFrom(card.getSubtype());
|
||||
this.supertype.clear();
|
||||
this.supertype.addAll(card.getSuperType());
|
||||
this.rarity = card.getRarity();
|
||||
|
||||
this.setExpansionSetCode(card.getExpansionSetCode());
|
||||
this.setCardNumber(card.getCardNumber());
|
||||
this.rarity = card.getRarity();
|
||||
this.setImageFileName(card.getImageFileName());
|
||||
this.setImageNumber(card.getImageNumber());
|
||||
this.usesVariousArt = card.getUsesVariousArt();
|
||||
|
||||
if (card.getSecondCardFace() != null) {
|
||||
|
|
@ -152,7 +156,7 @@ public class PermanentCard extends PermanentImpl {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MageObject getBasicMageObject(Game game) {
|
||||
public MageObject getBasicMageObject() {
|
||||
return card;
|
||||
}
|
||||
|
||||
|
|
@ -214,12 +218,15 @@ public class PermanentCard extends PermanentImpl {
|
|||
|
||||
@Override
|
||||
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
|
||||
// TODO: wtf, permanent must not change ZCC at all, is it buggy here?!
|
||||
card.updateZoneChangeCounter(game, event);
|
||||
zoneChangeCounter = card.getZoneChangeCounter(game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZoneChangeCounter(int value, Game game) {
|
||||
// TODO: wtf, why it sync card only without permanent zcc, is it buggy here?!
|
||||
// TODO: miss zoneChangeCounter = card.getZoneChangeCounter(game); ?
|
||||
card.setZoneChangeCounter(value, game);
|
||||
}
|
||||
|
||||
|
|
@ -227,13 +234,4 @@ public class PermanentCard extends PermanentImpl {
|
|||
public Card getMainCard() {
|
||||
return card.getMainCard();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return card.toString()
|
||||
+ ", " + ((this instanceof Token) ? "T" : "C")
|
||||
+ (this.isCopy() ? ", copy" : "")
|
||||
+ ", " + this.getPower() + "/" + this.getToughness()
|
||||
+ (this.isTapped() ? ", tapped" : "");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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;
|
||||
|
|
@ -30,6 +31,7 @@ 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;
|
||||
|
|
@ -118,6 +120,12 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
|
||||
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();
|
||||
|
|
@ -186,12 +194,20 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = threadLocalBuilder.get();
|
||||
sb.append(this.getName()).append('-').append(this.getExpansionSetCode());
|
||||
if (copy) {
|
||||
sb.append(" [Copy]");
|
||||
}
|
||||
return sb.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
|
||||
|
|
@ -483,7 +499,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected UUID getControllerOrOwner() {
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return controllerId;
|
||||
}
|
||||
|
||||
|
|
@ -1222,18 +1238,24 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
|
|||
@Override
|
||||
public boolean entersBattlefield(Ability source, Game game, Zone fromZone, boolean fireEvent) {
|
||||
controlledFromStartOfControllerTurn = false;
|
||||
if (this.isFaceDown(game)) { // ?? add morphed/manifested here ???
|
||||
|
||||
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
|
||||
MorphAbility.setPermanentToFaceDownCreature(this, this, game);
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ public class PermanentToken extends PermanentImpl {
|
|||
|
||||
// non-modifyable container with token characteristics
|
||||
// this PermanentToken resets to it on each game cycle
|
||||
// TODO: see PermanentCard.card for usage research and fixes
|
||||
protected Token token;
|
||||
|
||||
public PermanentToken(Token token, UUID controllerId, Game game) {
|
||||
super(controllerId, controllerId, token.getName());
|
||||
super(controllerId, controllerId, token.getName()); // random id
|
||||
this.token = token.copy();
|
||||
this.token.getAbilities().newOriginalId(); // neccessary if token has ability like DevourAbility()
|
||||
this.token.getAbilities().setSourceId(objectId);
|
||||
|
|
@ -76,11 +77,6 @@ public class PermanentToken extends PermanentImpl {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s - %s", getExpansionSetCode(), getName());
|
||||
}
|
||||
|
||||
private void copyFromToken(Token token, Game game, boolean reset) {
|
||||
// modify all attributes permanently (without game usage)
|
||||
this.name = token.getName();
|
||||
|
|
@ -119,7 +115,7 @@ public class PermanentToken extends PermanentImpl {
|
|||
}
|
||||
|
||||
@Override
|
||||
public MageObject getBasicMageObject(Game game) {
|
||||
public MageObject getBasicMageObject() {
|
||||
return token;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import mage.game.permanent.PermanentToken;
|
|||
import mage.game.permanent.token.custom.CreatureToken;
|
||||
import mage.players.Player;
|
||||
import mage.target.Target;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.*;
|
||||
|
|
@ -30,6 +31,8 @@ import java.util.*;
|
|||
*/
|
||||
public abstract class TokenImpl extends MageObjectImpl implements Token {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(MageObjectImpl.class);
|
||||
|
||||
protected String description;
|
||||
private final ArrayList<UUID> lastAddedTokenIds = new ArrayList<>();
|
||||
|
||||
|
|
@ -142,6 +145,14 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
|
|||
return putOntoBattlefield(amount, game, source, controllerId, tapped, attacking, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find random token image from a database
|
||||
*
|
||||
* @param token
|
||||
* @param game
|
||||
* @param sourceId
|
||||
* @return
|
||||
*/
|
||||
public static TokenInfo generateTokenInfo(TokenImpl token, Game game, UUID sourceId) {
|
||||
// Choose a token image by priority:
|
||||
// - use source's set code
|
||||
|
|
@ -190,11 +201,10 @@ public abstract class TokenImpl extends MageObjectImpl implements Token {
|
|||
// TODO: return default creature token image
|
||||
}
|
||||
|
||||
// TODO: implement Copy image
|
||||
// TODO: implement Manifest image
|
||||
// TODO: implement Morph image
|
||||
|
||||
// unknown tokens
|
||||
// unknown tokens:
|
||||
// - without official token sets;
|
||||
// - un-implemented token set (must add missing images to tokens database);
|
||||
// - another use cases with unknown tokens
|
||||
return new TokenInfo(TokenType.TOKEN, "Unknown", TokenRepository.XMAGE_TOKENS_SET_CODE, 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
}
|
||||
|
||||
this.card = affectedCard;
|
||||
this.manaCost = this.card.getManaCost().copy();
|
||||
this.manaCost = affectedCard.getManaCost().copy();
|
||||
this.color = affectedCard.getColor(null).copy();
|
||||
this.frameColor = affectedCard.getFrameColor(null).copy();
|
||||
this.frameStyle = affectedCard.getFrameStyle();
|
||||
|
|
@ -100,7 +100,11 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
this.ability = ability;
|
||||
this.ability.setControllerId(controllerId);
|
||||
|
||||
if (ability.getSpellAbilityCastMode() == SpellAbilityCastMode.MORPH){
|
||||
if (ability.getSpellAbilityCastMode() == SpellAbilityCastMode.MORPH
|
||||
|| ability.getSpellAbilityCastMode() == SpellAbilityCastMode.MEGAMORPH){
|
||||
// TODO: need research:
|
||||
// - why it use game param for color and subtype (possible bug?)
|
||||
// - is it possible to use BecomesFaceDownCreatureEffect.makeFaceDownObject or like that?
|
||||
this.faceDown = true;
|
||||
this.getColor(game).setColor(null);
|
||||
game.getState().getCreateMageObjectAttribute(this.getCard(), game).getSubtype().clear();
|
||||
|
|
@ -238,6 +242,16 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
throw new IllegalStateException("Wrong code usage: you can't change card number for the spell");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageFileName() {
|
||||
return card.getImageFileName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageFileName(String imageFile) {
|
||||
throw new IllegalStateException("Wrong code usage: you can't change image file name for the spell");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getImageNumber() {
|
||||
return card.getImageNumber();
|
||||
|
|
@ -509,6 +523,11 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
return this.controllerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return getControllerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return card.getName();
|
||||
|
|
@ -546,6 +565,11 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
return card.getRarity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRarity(Rarity rarity) {
|
||||
throw new IllegalArgumentException("Un-supported operation: " + this, new Throwable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CardType> getCardType(Game game) {
|
||||
if (faceDown) {
|
||||
|
|
@ -933,6 +957,11 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
return card.getUsesVariousArt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsesVariousArt(boolean usesVariousArt) {
|
||||
card.setUsesVariousArt(usesVariousArt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Mana> getMana() {
|
||||
return card.getMana();
|
||||
|
|
@ -1104,8 +1133,8 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void checkForCountersToAdd(Permanent permanent, Ability source, Game game) {
|
||||
card.checkForCountersToAdd(permanent, source, game);
|
||||
public void applyEnterWithCounters(Permanent permanent, Ability source, Game game) {
|
||||
card.applyEnterWithCounters(permanent, source, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -147,6 +147,16 @@ public class StackAbility extends StackObjectImpl implements Ability {
|
|||
throw new IllegalStateException("Wrong code usage: you can't change card number for the stack ability");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageFileName() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageFileName(String imageFile) {
|
||||
throw new IllegalStateException("Wrong code usage: you can't change image file name for the stack ability");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getImageNumber() {
|
||||
return 0;
|
||||
|
|
@ -301,6 +311,11 @@ public class StackAbility extends StackObjectImpl implements Ability {
|
|||
return this.controllerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getControllerOrOwnerId() {
|
||||
return getControllerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Costs<Cost> getCosts() {
|
||||
return emptyCosts;
|
||||
|
|
|
|||
|
|
@ -842,13 +842,13 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
private boolean doDiscard(Card card, Ability source, Game game, boolean payForCost, boolean fireFinalEvent) {
|
||||
//20100716 - 701.7
|
||||
/* 701.7. Discard #
|
||||
701.7a To discard a card, move it from its owners hand to that players graveyard.
|
||||
701.7a To discard a card, move it from its owner's hand to that player's graveyard.
|
||||
701.7b By default, effects that cause a player to discard a card allow the affected
|
||||
player to choose which card to discard. Some effects, however, require a random
|
||||
discard or allow another player to choose which card is discarded.
|
||||
701.7c If a card is discarded, but an effect causes it to be put into a hidden zone
|
||||
instead of into its owners graveyard without being revealed, all values of that
|
||||
cards characteristics are considered to be undefined.
|
||||
instead of into its owner's graveyard without being revealed, all values of that
|
||||
card's characteristics are considered to be undefined.
|
||||
TODO:
|
||||
If a card is discarded this way to pay a cost that specifies a characteristic
|
||||
about the discarded card, that cost payment is illegal; the game returns to
|
||||
|
|
|
|||
|
|
@ -1117,7 +1117,7 @@ public final class CardUtil {
|
|||
|
||||
// put onto battlefield with possible counters without ETB
|
||||
game.getPermanentsEntering().put(permanent.getId(), permanent);
|
||||
permCard.checkForCountersToAdd(permanent, source, game);
|
||||
permCard.applyEnterWithCounters(permanent, source, game);
|
||||
permanent.entersBattlefield(source, game, Zone.OUTSIDE, false);
|
||||
game.addPermanent(permanent, game.getState().getNextPermanentOrderNumber());
|
||||
game.getPermanentsEntering().remove(permanent.getId());
|
||||
|
|
@ -2141,43 +2141,63 @@ public final class CardUtil {
|
|||
|
||||
|
||||
/**
|
||||
* Copy image related data from one object to another (set code, card number, image number)
|
||||
* Copy image related data from one object to another (set code, card number, image number, file name)
|
||||
* Use it in copy/transform effects
|
||||
*/
|
||||
public static void copySetAndCardNumber(MageObject targetObject, MageObject copyFromObject) {
|
||||
String needSetCode;
|
||||
String needCardNumber;
|
||||
String needImageFileName;
|
||||
int needImageNumber;
|
||||
boolean needUsesVariousArt = false;
|
||||
if (copyFromObject instanceof Card) {
|
||||
needUsesVariousArt = ((Card) copyFromObject).getUsesVariousArt();
|
||||
}
|
||||
|
||||
needSetCode = copyFromObject.getExpansionSetCode();
|
||||
needCardNumber = copyFromObject.getCardNumber();
|
||||
needImageFileName = copyFromObject.getImageFileName();
|
||||
needImageNumber = copyFromObject.getImageNumber();
|
||||
|
||||
if (targetObject instanceof Permanent) {
|
||||
copySetAndCardNumber((Permanent) targetObject, needSetCode, needCardNumber, needImageNumber);
|
||||
copySetAndCardNumber((Permanent) targetObject, needSetCode, needCardNumber, needImageFileName, needImageNumber, needUsesVariousArt);
|
||||
} else if (targetObject instanceof Token) {
|
||||
copySetAndCardNumber((Token) targetObject, needSetCode, needCardNumber, needImageNumber);
|
||||
copySetAndCardNumber((Token) targetObject, needSetCode, needCardNumber, needImageFileName, needImageNumber);
|
||||
} else if (targetObject instanceof Card) {
|
||||
copySetAndCardNumber((Card) targetObject, needSetCode, needCardNumber, needImageFileName, needImageNumber, needUsesVariousArt);
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported target object class: " + targetObject.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
private static void copySetAndCardNumber(Permanent targetPermanent, String newSetCode, String newCardNumber, Integer newImageNumber) {
|
||||
private static void copySetAndCardNumber(Permanent targetPermanent, String newSetCode, String newCardNumber, String newImageFileName, Integer newImageNumber, boolean usesVariousArt) {
|
||||
if (targetPermanent instanceof PermanentCard
|
||||
|| targetPermanent instanceof PermanentToken) {
|
||||
targetPermanent.setExpansionSetCode(newSetCode);
|
||||
targetPermanent.setCardNumber(newCardNumber);
|
||||
targetPermanent.setImageFileName(newImageFileName);
|
||||
targetPermanent.setImageNumber(newImageNumber);
|
||||
targetPermanent.setUsesVariousArt(usesVariousArt);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Wrong code usage: un-supported target permanent type: " + targetPermanent.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
private static void copySetAndCardNumber(Token targetToken, String newSetCode, String newCardNumber, Integer newImageNumber) {
|
||||
private static void copySetAndCardNumber(Token targetToken, String newSetCode, String newCardNumber, String newImageFileName, Integer newImageNumber) {
|
||||
targetToken.setExpansionSetCode(newSetCode);
|
||||
targetToken.setCardNumber(newCardNumber);
|
||||
targetToken.setImageFileName(newImageFileName);
|
||||
targetToken.setImageNumber(newImageNumber);
|
||||
}
|
||||
|
||||
private static void copySetAndCardNumber(Card targetCard, String newSetCode, String newCardNumber, String newImageFileName, Integer newImageNumber, boolean usesVariousArt) {
|
||||
targetCard.setExpansionSetCode(newSetCode);
|
||||
targetCard.setCardNumber(newCardNumber);
|
||||
targetCard.setImageFileName(newImageFileName);
|
||||
targetCard.setImageNumber(newImageNumber);
|
||||
targetCard.setUsesVariousArt(usesVariousArt);
|
||||
}
|
||||
|
||||
/**
|
||||
* One single event can be a batch (contain multiple events)
|
||||
*
|
||||
|
|
@ -2193,4 +2213,26 @@ public final class CardUtil {
|
|||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare card name for render in card panels, popups, etc. Can show face down status and real card name instead empty string
|
||||
*
|
||||
* @param imageFileName face down status or another inner image name like Morph, Copy, etc
|
||||
*/
|
||||
public static String getCardNameForGUI(String name, String imageFileName) {
|
||||
if (imageFileName.isEmpty()) {
|
||||
// normal name
|
||||
return name;
|
||||
} else {
|
||||
// face down or inner name
|
||||
return imageFileName + (name.isEmpty() ? "" : ": " + name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GUI related: show real name and day/night button for face down card
|
||||
*/
|
||||
public static boolean canShowAsControlled(Card card, UUID createdForPlayer) {
|
||||
return card.getControllerOrOwnerId().equals(createdForPlayer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -640,7 +640,12 @@ public final class ManaUtil {
|
|||
}
|
||||
|
||||
public static FilterMana getColorIdentity(Token token) {
|
||||
return getColorIdentity(token.getColor(), String.join("", token.getManaCostSymbols()), token.getAbilities().getRules(token.getName()), null);
|
||||
return getColorIdentity(
|
||||
token.getColor(),
|
||||
String.join("", token.getManaCostSymbols()),
|
||||
token.getAbilities().getRules(token.getName()),
|
||||
token.getBackFace() == null ? null : token.getBackFace().getCopySourceCard()
|
||||
);
|
||||
}
|
||||
|
||||
public static int getColorIdentityHash(FilterMana colorIdentity) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package mage.util.functions;
|
|||
import mage.MageObject;
|
||||
import mage.abilities.Abilities;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.keyword.MorphAbility;
|
||||
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
|
||||
import mage.abilities.keyword.PrototypeAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.CardType;
|
||||
|
|
@ -69,10 +69,10 @@ public class CopyTokenFunction {
|
|||
// create token from non-token permanent
|
||||
|
||||
// morph/manifest must hide all info
|
||||
if (((PermanentCard) source).isMorphed()
|
||||
|| ((PermanentCard) source).isManifested()
|
||||
|| source.isFaceDown(game)) {
|
||||
MorphAbility.setPermanentToFaceDownCreature(target, (PermanentCard) source, game);
|
||||
PermanentCard sourcePermanent = (PermanentCard) source;
|
||||
BecomesFaceDownCreatureEffect.FaceDownType faceDownType = BecomesFaceDownCreatureEffect.findFaceDownType(game, sourcePermanent);
|
||||
if (faceDownType != null) {
|
||||
BecomesFaceDownCreatureEffect.makeFaceDownObject(game, null, target, faceDownType, null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
# Use verify test to check it: test_checkMissingTokenData
|
||||
|
||||
# Inner/xmage related tokens stores in TokenRepository (copy, morph, etc)
|
||||
|
||||
# ALL EMBLEMS
|
||||
# Usage hints:
|
||||
# - use simple name for the emblem like Gideon
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue