Merge branch 'Card-Forge:master' into master

This commit is contained in:
hovergoat
2024-08-26 21:30:20 -04:00
committed by GitHub
179 changed files with 1965 additions and 511 deletions

View File

@@ -64,7 +64,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
* Keys that should not changed
*/
private static final ImmutableList<String> noChangeKeys = ImmutableList.<String>builder()
.add("TokenScript", "TokenImage", "NewName", "ChooseFromList")
.add("TokenScript", "TokenImage", "NewName" , "DefinedName", "ChooseFromList")
.add("AddAbility").build();
/**
@@ -170,7 +170,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
*
* @return a boolean.
*/
public final boolean isSecondary() {
public boolean isSecondary() {
return getParamOrDefault("Secondary", "False").equals("True");
}

View File

@@ -91,10 +91,10 @@ public final class GameActionUtil {
return alternatives;
}
if (sa.isSpell()) {
if (sa.isSpell() || sa.isLandAbility()) {
boolean lkicheck = false;
Card newHost = ((Spell)sa).getAlternateHost(source);
Card newHost = sa.getAlternateHost(source);
if (newHost != null) {
source = newHost;
lkicheck = true;

View File

@@ -2,7 +2,6 @@ package forge.game;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import com.google.common.collect.Iterables;
@@ -93,11 +92,7 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
if (event.sa.getTargetRestrictions() != null) {
StringBuilder sb = new StringBuilder();
List<TargetChoices> targets = event.sa.getAllTargetChoices();
// Include the TargetChoices from the stack instance, since the real target choices
// are on that object at this point (see SpellAbilityStackInstance constructor).
targets.add(event.si.getTargetChoices());
for (TargetChoices ch : targets) {
for (TargetChoices ch : event.sa.getAllTargetChoices()) {
if (null != ch) {
sb.append(ch);
}

View File

@@ -2953,7 +2953,8 @@ public class AbilityUtils {
}
for (SpellAbility s : list) {
if (s instanceof LandAbility) {
if (s.isLandAbility()) {
s.setActivatingPlayer(controller);
// CR 305.3
if (controller.getGame().getPhaseHandler().isPlayerTurn(controller) && controller.canPlayLand(tgtCard, true, s)) {
sas.add(s);
@@ -2981,9 +2982,7 @@ public class AbilityUtils {
private static void collectSpellsForPlayEffect(final List<SpellAbility> result, final CardState state, final Player controller, final boolean withAltCost) {
if (state.getType().isLand()) {
LandAbility la = new LandAbility(state.getCard(), controller, null);
la.setCardState(state);
result.add(la);
result.add(state.getFirstSpellAbility());
}
final Iterable<SpellAbility> spells = state.getSpellAbilities();
for (SpellAbility sa : spells) {

View File

@@ -13,7 +13,9 @@ import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.CardRarity;
import forge.card.CardRulesPredicates;
import forge.card.CardStateName;
import forge.game.Game;
@@ -46,53 +48,59 @@ public class CopyPermanentEffect extends TokenEffectBase {
final StringBuilder sb = new StringBuilder();
final Player activator = sa.getActivatingPlayer();
final List<Card> tgtCards = getTargetCards(sa);
boolean justOne = tgtCards.size() == 1;
boolean addKWs = sa.hasParam("AddKeywords");
final int numCopies = sa.hasParam("NumCopies") ?
AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCopies"), sa) : 1;
sb.append(activator).append(" creates ").append(Lang.nounWithNumeralExceptOne(numCopies, "token"));
sb.append(numCopies == 1 ? " that's a copy" : " that are copies").append(" of ");
sb.append(Lang.joinHomogenous(tgtCards));
if (addKWs) {
final List<String> keywords = Lists.newArrayList();
keywords.addAll(Arrays.asList(sa.getParam("AddKeywords").split(" & ")));
if (sa.getDescription().contains("except")) {
sb.append(", except ").append(justOne ? "it has " : "they have ");
} else {
sb.append(". ").append(justOne ? "It gains " : "They gain ");
}
sb.append(Lang.joinHomogenous(keywords).toLowerCase());
}
if (sa.hasParam("AddTriggers")) {
final String oDesc = sa.getDescription();
final String trigStg = oDesc.contains("\"") ?
oDesc.substring(oDesc.indexOf("\""),oDesc.lastIndexOf("\"") + 1) :
"[trigger text parsing error]";
if (addKWs) {
sb.append(" and ").append(trigStg);
} else {
sb.append(". ").append(justOne ? "It gains " : "They gain ").append(trigStg);
}
sb.append(activator).append(" creates ");
if (sa.hasParam("DefinedName")) {
sb.append(Lang.nounWithNumeralExceptOne(numCopies, sa.getParam("DefinedName") + " token"));
} else {
sb.append(".");
}
final List<Card> tgtCards = getTargetCards(sa);
boolean justOne = tgtCards.size() == 1;
boolean addKWs = sa.hasParam("AddKeywords");
if (sa.hasParam("AtEOT")) {
String atEOT = sa.getParam("AtEOT");
String verb = "Sacrifice ";
if (atEOT.startsWith("Exile")) {
verb = "Exile ";
sb.append(Lang.nounWithNumeralExceptOne(numCopies, "token"));
sb.append(numCopies == 1 ? " that's a copy" : " that are copies").append(" of ");
sb.append(Lang.joinHomogenous(tgtCards));
if (addKWs) {
final List<String> keywords = Lists.newArrayList();
keywords.addAll(Arrays.asList(sa.getParam("AddKeywords").split(" & ")));
if (sa.getDescription().contains("except")) {
sb.append(", except ").append(justOne ? "it has " : "they have ");
} else {
sb.append(". ").append(justOne ? "It gains " : "They gain ");
}
sb.append(Lang.joinHomogenous(keywords).toLowerCase());
}
sb.append(" ").append(verb).append(justOne ? "it " : "them ").append("at ");
String when = "the beginning of the next end step.";
if (atEOT.endsWith("Combat")) {
when = "end of combat.";
if (sa.hasParam("AddTriggers")) {
final String oDesc = sa.getDescription();
final String trigStg = oDesc.contains("\"") ?
oDesc.substring(oDesc.indexOf("\""),oDesc.lastIndexOf("\"") + 1) :
"[trigger text parsing error]";
if (addKWs) {
sb.append(" and ").append(trigStg);
} else {
sb.append(". ").append(justOne ? "It gains " : "They gain ").append(trigStg);
}
} else {
sb.append(".");
}
if (sa.hasParam("AtEOT")) {
String atEOT = sa.getParam("AtEOT");
String verb = "Sacrifice ";
if (atEOT.startsWith("Exile")) {
verb = "Exile ";
}
sb.append(" ").append(verb).append(justOne ? "it " : "them ").append("at ");
String when = "the beginning of the next end step.";
if (atEOT.endsWith("Combat")) {
when = "end of combat.";
}
sb.append(when);
}
sb.append(when);
}
return sb.toString();
@@ -180,20 +188,21 @@ public class CopyPermanentEffect extends TokenEffectBase {
tgtCards = choice;
System.err.println("Copying random permanent(s): " + tgtCards.toString());
} else if (sa.hasParam("DefinedName")) {
String name = sa.getParam("DefinedName");
if (name.equals("NamedCard")) {
if (!host.getNamedCard().isEmpty()) {
name = host.getNamedCard();
}
}
} else if (sa.hasParam("DefinedName")) {
List<PaperCard> cards = Lists.newArrayList(StaticData.instance().getCommonCards().getUniqueCards());
String name = sa.getParam("DefinedName");
if (name.equals("NamedCard")) {
if (!host.getNamedCard().isEmpty()) {
name = host.getNamedCard();
}
}
Predicate<PaperCard> cpp = Predicates.compose(CardRulesPredicates.name(StringOp.EQUALS, name), PaperCard::getRules);
cards = Lists.newArrayList(Iterables.filter(cards, cpp));
Predicate<PaperCard> cpp = Predicates.compose(CardRulesPredicates.name(StringOp.EQUALS, name), PaperCard::getRules);
cards = Lists.newArrayList(Iterables.filter(cards, cpp));
if (!cards.isEmpty()) {
tgtCards.add(Card.fromPaperCard(cards.get(0), controller));
}
if (!cards.isEmpty()) {
tgtCards.add(Card.fromPaperCard(cards.get(0), controller));
}
} else if (sa.hasParam("Choices")) {
Player chooser = activator;
@@ -272,31 +281,42 @@ public class CopyPermanentEffect extends TokenEffectBase {
}
public static Card getProtoType(final SpellAbility sa, final Card original, final Player newOwner) {
final Card host = sa.getHostCard();
int id = newOwner == null ? 0 : newOwner.getGame().nextCardId();
// need to create a physical card first, i need the original card faces
final Card copy = CardFactory.getCard(original.getPaperCard(), newOwner, id, host.getGame());
final Card copy;
if (sa.hasParam("DefinedName")) {
copy = original;
String name = TextUtil.fastReplace(TextUtil.fastReplace(original.getName(), ",", ""), " ", "_").toLowerCase();
String set = sa.getOriginalHost().getSetCode();
copy.getCurrentState().setRarity(CardRarity.Token);
copy.getCurrentState().setSetCode(set);
copy.getCurrentState().setImageKey(ImageKeys.getTokenKey(name + "_" + set.toLowerCase()));
} else {
final Card host = sa.getHostCard();
copy.setTokenSpawningAbility(sa);
if (original.isTransformable()) {
// 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield,
// the resulting token is a transforming token that has both a front face and a back face.
// The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up.
// This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
copy.setBackSide(original.isBackSide());
if (original.isTransformed()) {
copy.incrementTransformedTimestamp();
int id = newOwner == null ? 0 : newOwner.getGame().nextCardId();
// need to create a physical card first, i need the original card faces
copy = CardFactory.getCard(original.getPaperCard(), newOwner, id, host.getGame());
if (original.isTransformable()) {
// 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield,
// the resulting token is a transforming token that has both a front face and a back face.
// The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up.
// This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
copy.setBackSide(original.isBackSide());
if (original.isTransformed()) {
copy.incrementTransformedTimestamp();
}
}
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
// force update the now set State
if (original.isTransformable()) {
copy.setState(original.isTransformed() ? CardStateName.Transformed : CardStateName.Original, true, true);
} else {
copy.setState(copy.getCurrentStateName(), true, true);
}
}
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
// force update the now set State
if (original.isTransformable()) {
copy.setState(original.isTransformed() ? CardStateName.Transformed : CardStateName.Original, true, true);
} else {
copy.setState(copy.getCurrentStateName(), true, true);
}
copy.setTokenSpawningAbility(sa);
copy.setGamePieceType(GamePieceType.TOKEN);
return copy;

View File

@@ -1,7 +1,5 @@
package forge.game.ability.effects;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
@@ -15,7 +13,7 @@ import forge.game.cost.CostPart;
import forge.game.cost.CostReveal;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
@@ -94,7 +92,7 @@ public class DiscoverEffect extends SpellAbilityEffect {
List<SpellAbility> sas = AbilityUtils.getBasicSpellsFromPlayEffect(found, p);
// filter out land abilities due to MDFC or similar
Iterables.removeIf(sas, Predicates.instanceOf(LandAbility.class));
sas.removeIf(sp -> sp.isLandAbility());
// the spell must also have a mana value equal to or less than the discover number
sas.removeIf(sp -> sp.getPayCosts().getTotalMana().getCMC() > num);

View File

@@ -35,7 +35,7 @@ public class InternalRadiationEffect extends SpellAbilityEffect {
final CardCollectionView milled = game.getAction().mill(new PlayerCollection(p), numRad, ZoneType.Graveyard, sa, moveParams);
table.triggerChangesZoneAll(game, sa);
int n = CardLists.count(milled, Predicates.not(CardPredicates.Presets.LANDS));
if (StaticAbilityGainLifeRadiation.gainLifeRadiation(p)) {
p.gainLife(n, sa.getHostCard(), sa);
} else {
@@ -49,7 +49,7 @@ public class InternalRadiationEffect extends SpellAbilityEffect {
game.getTriggerHandler().runTrigger(TriggerType.LifeLostAll, runParams, false);
}
}
// and remove n rad counter
p.removeRadCounters(n);
}

View File

@@ -39,7 +39,7 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.AlternativeCost;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.zone.Zone;
@@ -343,7 +343,7 @@ public class PlayEffect extends SpellAbilityEffect {
final Zone originZone = tgtCard.getZone();
// lands will be played
if (tgtSA instanceof LandAbility) {
if (tgtSA.isLandAbility()) {
tgtSA.resolve();
amount--;
if (remember) {

View File

@@ -57,7 +57,7 @@ public class PlayLandVariantEffect extends SpellAbilityEffect {
PaperCard ran = Aggregates.random(cards);
random = CardFactory.getCard(ran, activator, game);
cards.remove(ran);
} while (!activator.canPlayLand(random, false));
} while (!activator.canPlayLand(random, false, random.getFirstSpellAbility()));
source.addCloneState(CardFactory.getCloneStates(random, source, sa), game.getNextTimestamp());
source.updateStateForView();

View File

@@ -5524,6 +5524,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return isInstant() || isSorcery() || (isAura() && !isInZone(ZoneType.Battlefield));
}
public final boolean hasPlayableLandFace() { return isLand() || (isModal() && getState(CardStateName.Modal).getType().isLand()); }
public final boolean isLand() { return getType().isLand(); }
public final boolean isBasicLand() { return getType().isBasicLand(); }
public final boolean isSnow() { return getType().isSnow(); }
@@ -7430,7 +7432,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
for (SpellAbility sa : getState(CardStateName.Modal).getSpellAbilities()) {
//add alternative costs as additional spell abilities
// only add Spells there
if (sa.isSpell()) {
if (sa.isSpell() || sa.isLandAbility()) {
abilities.add(sa);
abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false));
}
@@ -7466,106 +7468,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
abilities.removeAll(toRemove);
// Land Abilities below, move them to CardFactory after MayPlayRefactor
if (getLastKnownZone().is(ZoneType.Battlefield)) {
return abilities;
}
if (getState(CardStateName.Original).getType().isLand()) {
LandAbility la = new LandAbility(this, player, null);
la.setCardState(oState);
if (la.canPlay()) {
abilities.add(la);
}
Card source = this;
boolean lkicheck = false;
// if Card is Facedown, need to check if MayPlay still applies
if (isFaceDown()) {
lkicheck = true;
source = CardCopyService.getLKICopy(source);
source.forceTurnFaceUp();
}
if (lkicheck) {
// double freeze tracker, so it doesn't update view
game.getTracker().freeze();
CardCollection preList = new CardCollection(source);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(source), preList);
}
// extra for MayPlay
for (CardPlayOption o : source.mayPlay(player)) {
la = new LandAbility(this, player, o);
la.setCardState(oState);
if (la.canPlay()) {
abilities.add(la);
}
}
// reset static abilities
if (lkicheck) {
game.getAction().checkStaticAbilities(false);
// clear delayed changes, this check should not have updated the view
game.getTracker().clearDelayed();
// need to unfreeze tracker
game.getTracker().unfreeze();
}
}
if (isModal() && hasState(CardStateName.Modal)) {
CardState modal = getState(CardStateName.Modal);
if (modal.getType().isLand()) {
LandAbility la = new LandAbility(this, player, null);
la.setCardState(modal);
Card source = CardCopyService.getLKICopy(this);
boolean lkicheck = true;
// if Card is Facedown, need to check if MayPlay still applies
if (isFaceDown()) {
source.forceTurnFaceUp();
}
// the modal state is not copied with lki, need to copy it extra
if (!source.hasState(CardStateName.Modal)) {
source.addAlternateState(CardStateName.Modal, false);
source.getState(CardStateName.Modal).copyFrom(this.getState(CardStateName.Modal), true);
}
source.setSplitStateToPlayAbility(la);
if (la.canPlay(source)) {
abilities.add(la);
}
if (lkicheck) {
// double freeze tracker, so it doesn't update view
game.getTracker().freeze();
CardCollection preList = new CardCollection(source);
game.getAction().checkStaticAbilities(false, Sets.newHashSet(source), preList);
}
// extra for MayPlay
for (CardPlayOption o : source.mayPlay(player)) {
la = new LandAbility(this, player, o);
la.setCardState(modal);
if (la.canPlay(source)) {
abilities.add(la);
}
}
// reset static abilities
if (lkicheck) {
game.getAction().checkStaticAbilities(false);
// clear delayed changes, this check should not have updated the view
game.getTracker().clearDelayed();
// need to unfreeze tracker
game.getTracker().unfreeze();
}
}
}
return abilities;
}

View File

@@ -413,17 +413,16 @@ public class CardFactory {
// SpellPermanent only for Original State
if (c.getCurrentStateName() == CardStateName.Original || c.getCurrentStateName() == CardStateName.Modal || c.getCurrentStateName().toString().startsWith("Specialize")) {
// this is the "default" spell for permanents like creatures and artifacts
if (c.isPermanent() && !c.isAura() && !c.isLand()) {
if (c.isLand()) {
SpellAbility sa = new LandAbility(c);
sa.setCardState(c.getCurrentState());
c.addSpellAbility(sa);
} else if (c.isPermanent() && !c.isAura()) {
// this is the "default" spell for permanents like creatures and artifacts
SpellAbility sa = new SpellPermanent(c);
// Currently only for Modal, might react different when state is always set
//if (c.getCurrentStateName() == CardStateName.Modal) {
sa.setCardState(c.getCurrentState());
//}
sa.setCardState(c.getCurrentState());
c.addSpellAbility(sa);
}
// TODO add LandAbility there when refactor MayPlay
}
CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities());

View File

@@ -22,7 +22,6 @@ import java.util.Comparator;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.card.CardStateName;
import forge.game.CardTraitBase;
import forge.game.GameEntity;
import forge.game.combat.CombatUtil;
@@ -379,7 +378,7 @@ public final class CardPredicates {
/**
* a Predicate<Card> to get all lands.
*/
public static final Predicate<Card> LANDS = c -> c.isLand() || (!c.isInZone(ZoneType.Battlefield) && c.isModal() && c.getState(CardStateName.Modal).getType().isLand());
public static final Predicate<Card> LANDS = c -> c.isLand();
/**
* a Predicate<Card> to get all mana-producing lands.
*/

View File

@@ -32,7 +32,7 @@ import forge.game.event.*;
import forge.game.player.Player;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -1064,7 +1064,7 @@ public class PhaseHandler implements java.io.Serializable {
final Zone currentZone = saHost.getZone();
// Need to check if Zone did change
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa instanceof LandAbility)) {
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa.isLandAbility())) {
// currently there can be only one Spell put on the Stack at once, or Land Abilities be played
final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost);

View File

@@ -43,7 +43,7 @@ import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.*;
import forge.game.trigger.Trigger;
@@ -1691,9 +1691,9 @@ public class Player extends GameEntity implements Comparable<Player> {
game.fireEvent(new GameEventShuffle(this));
}
public final boolean playLand(final Card land, final boolean ignoreZoneAndTiming) {
public final boolean playLand(final Card land, final boolean ignoreZoneAndTiming, SpellAbility cause) {
// Dakkon Blackblade Avatar will use a similar effect
if (canPlayLand(land, ignoreZoneAndTiming)) {
if (canPlayLand(land, ignoreZoneAndTiming, cause)) {
playLandNoCheck(land, null);
return true;
}
@@ -1706,7 +1706,7 @@ public class Player extends GameEntity implements Comparable<Player> {
land.setController(this, 0);
if (land.isFaceDown()) {
land.turnFaceUp(null);
if (cause instanceof LandAbility) {
if (cause.isLandAbility()) {
land.changeToState(cause.getCardStateName());
}
}
@@ -1730,12 +1730,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return c;
}
public final boolean canPlayLand(final Card land) {
return canPlayLand(land, false);
}
public final boolean canPlayLand(final Card land, final boolean ignoreZoneAndTiming) {
return canPlayLand(land, ignoreZoneAndTiming, null);
}
public final boolean canPlayLand(final Card land, final boolean ignoreZoneAndTiming, SpellAbility landSa) {
if (!ignoreZoneAndTiming) {
// CR 305.3

View File

@@ -21,24 +21,20 @@ import forge.card.CardStateName;
import forge.card.mana.ManaCost;
import forge.game.card.Card;
import forge.game.card.CardCopyService;
import forge.game.card.CardPlayOption;
import forge.game.cost.Cost;
import forge.game.player.Player;
import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
import forge.util.CardTranslation;
import forge.util.Localizer;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
public class LandAbility extends Ability {
public class LandAbility extends AbilityStatic {
public LandAbility(Card sourceCard, Player p, CardPlayOption mayPlay) {
super(sourceCard, new Cost(ManaCost.NO_COST, false));
setActivatingPlayer(p);
setMayPlay(mayPlay);
}
public LandAbility(Card sourceCard) {
this(sourceCard, sourceCard.getController(), null);
super(sourceCard, ManaCost.NO_COST);
getRestrictions().setZone(ZoneType.Hand);
}
public boolean canPlay(Card newHost) {
@@ -46,11 +42,21 @@ public class LandAbility extends Ability {
return p.canPlayLand(newHost, false, this);
}
@Override
public boolean isLandAbility() { return true; }
@Override
public boolean isSecondary() {
return true;
}
@Override
public boolean canPlay() {
Card land = this.getHostCard();
final Player p = this.getActivatingPlayer();
if (p == null || land.isInZone(ZoneType.Battlefield)) {
return false;
}
if (this.getCardState() != null && land.getCurrentStateName() != this.getCardStateName()) {
if (!land.isLKI()) {
land = CardCopyService.getLKICopy(land);
@@ -113,4 +119,41 @@ public class LandAbility extends Ability {
return sb.toString();
}
@Override
public Card getAlternateHost(Card source) {
boolean lkicheck = false;
// need to be done before so it works with Vivien and Zoetic Cavern
if (source.isFaceDown() && source.isInZone(ZoneType.Exile)) {
if (!source.isLKI()) {
source = CardCopyService.getLKICopy(source);
}
source.forceTurnFaceUp();
lkicheck = true;
}
if (getCardState() != null && source.getCurrentStateName() != getCardStateName()) {
if (!source.isLKI()) {
source = CardCopyService.getLKICopy(source);
}
CardStateName stateName = getCardState().getStateName();
if (!source.hasState(stateName)) {
source.addAlternateState(stateName, false);
source.getState(stateName).copyFrom(getHostCard().getState(stateName), true);
}
source.setState(stateName, false);
if (getHostCard().isDoubleFaced()) {
source.setBackSide(getHostCard().getRules().getSplitType().getChangedStateName().equals(stateName));
}
// need to reset CMC
source.setLKICMC(-1);
source.setLKICMC(source.getCMC());
lkicheck = true;
}
return lkicheck ? source : null;
}
}

View File

@@ -153,6 +153,7 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
this.castFaceDown = faceDown;
}
@Override
public Card getAlternateHost(Card source) {
boolean lkicheck = false;

View File

@@ -527,6 +527,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public boolean isSpell() { return false; }
public boolean isAbility() { return true; }
public boolean isActivatedAbility() { return false; }
public boolean isLandAbility() { return false; }
public boolean isTurnFaceUp() {
return isMorphUp() || isDisguiseUp() || isManifestUp() || isCloakUp();
@@ -2185,7 +2186,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
}
else if (incR[0].contains("LandAbility")) {
if (!(root instanceof LandAbility)) {
if (!(root.isLandAbility())) {
return testFailed;
}
}
@@ -2544,7 +2545,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
if (getRestrictions().isInstantSpeed()) {
return true;
}
if ((isSpell() || this instanceof LandAbility) && (isCastFromPlayEffect() || host.isInstant() || host.hasKeyword(Keyword.FLASH))) {
if ((isSpell() || this.isLandAbility()) && (isCastFromPlayEffect() || host.isInstant() || host.hasKeyword(Keyword.FLASH))) {
return true;
}
@@ -2589,6 +2590,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return true;
}
public Card getAlternateHost(Card source) {
return null;
}
public boolean hasOptionalKeywordAmount(KeywordInterface kw) {
return this.optionalKeywordAmount.contains(kw.getKeyword(), Pair.of(kw.getIdx(), kw.getStaticId()));
}