refactor LandAbility to be created by CardFactory (#5047)

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
This commit is contained in:
Hans Mackowiak
2024-08-23 10:57:33 +02:00
committed by GitHub
parent a52e06fb82
commit 77710cf1b0
24 changed files with 115 additions and 193 deletions

View File

@@ -468,8 +468,7 @@ public class AiController {
} }
} }
} }
return Iterables.any(c.getAllPossibleAbilities(player, true), SpellAbility::isLandAbility);
return player.canPlayLand(c);
}); });
return landList; return landList;
} }
@@ -1376,30 +1375,12 @@ public class AiController {
Card land = chooseBestLandToPlay(landsWannaPlay); Card land = chooseBestLandToPlay(landsWannaPlay);
if ((!player.canLoseLife() || player.cantLoseForZeroOrLessLife() || ComputerUtil.getDamageFromETB(player, land) < player.getLife()) if ((!player.canLoseLife() || player.cantLoseForZeroOrLessLife() || ComputerUtil.getDamageFromETB(player, land) < player.getLife())
&& (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land))) { && (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land))) {
final List<SpellAbility> abilities = Lists.newArrayList(); final List<SpellAbility> abilities = land.getAllPossibleAbilities(player, true);
// skip non Land Abilities
abilities.removeIf(sa -> !sa.isLandAbility());
// TODO extend this logic to evaluate MDFC with both sides land
// this can only happen if its a MDFC land
if (!land.isLand()) {
land.setState(CardStateName.Modal, true);
land.setBackSide(true);
}
LandAbility la = new LandAbility(land, player, null);
la.setCardState(land.getCurrentState());
if (la.canPlay()) {
abilities.add(la);
}
// add mayPlay option
for (CardPlayOption o : land.mayPlay(player)) {
la = new LandAbility(land, player, o);
la.setCardState(land.getCurrentState());
if (la.canPlay()) {
abilities.add(la);
}
}
if (!abilities.isEmpty()) { if (!abilities.isEmpty()) {
// TODO extend this logic to evaluate MDFC with both sides land
return abilities; return abilities;
} }
} }
@@ -1570,7 +1551,7 @@ public class AiController {
Iterables.removeIf(saList, spellAbility -> { //don't include removedAI cards if somehow the AI can play the ability or gain control of unsupported card Iterables.removeIf(saList, spellAbility -> { //don't include removedAI cards if somehow the AI can play the ability or gain control of unsupported card
// TODO allow when experimental profile? // TODO allow when experimental profile?
return spellAbility instanceof LandAbility || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard())); return spellAbility.isLandAbility() || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard()));
}); });
//update LivingEndPlayer //update LivingEndPlayer
useLivingEnd = Iterables.any(player.getZone(ZoneType.Library), CardPredicates.nameEquals("Living End")); useLivingEnd = Iterables.any(player.getZone(ZoneType.Library), CardPredicates.nameEquals("Living End"));

View File

@@ -45,7 +45,7 @@ public class ComputerUtilAbility {
return false; return false;
} }
} }
return player.canPlayLand(c); return player.canPlayLand(c, false, c.getFirstSpellAbility());
}); });
final CardCollection landsNotInHand = new CardCollection(player.getCardsIn(ZoneType.Graveyard)); final CardCollection landsNotInHand = new CardCollection(player.getCardsIn(ZoneType.Graveyard));

View File

@@ -810,7 +810,7 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public boolean playChosenSpellAbility(SpellAbility sa) { public boolean playChosenSpellAbility(SpellAbility sa) {
if (sa instanceof LandAbility) { if (sa.isLandAbility()) {
if (sa.canPlay()) { if (sa.canPlay()) {
sa.resolve(); sa.resolve();
} }

View File

@@ -8,7 +8,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.LandAbility;
import forge.game.spellability.Spell; import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -45,7 +45,7 @@ public class DiscoverAi extends SpellAbilityAi {
public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) { public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
Card c = (Card)params.get("Card"); Card c = (Card)params.get("Card");
for (SpellAbility s : AbilityUtils.getBasicSpellsFromPlayEffect(c, ai)) { for (SpellAbility s : AbilityUtils.getBasicSpellsFromPlayEffect(c, ai)) {
if (s instanceof LandAbility) { if (s.isLandAbility()) {
// return false or we get a ClassCastException later if the AI encounters MDFC with land backside // return false or we get a ClassCastException later if the AI encounters MDFC with land backside
return false; return false;
} }

View File

@@ -154,7 +154,7 @@ public class PlayAi extends SpellAbilityAi {
if (!sa.matchesValidParam("ValidSA", s)) { if (!sa.matchesValidParam("ValidSA", s)) {
continue; continue;
} }
if (s instanceof LandAbility) { if (s.isLandAbility()) {
// might want to run some checks here but it's rare anyway // might want to run some checks here but it's rare anyway
return true; return true;
} }

View File

@@ -1,6 +1,6 @@
package forge.ai.simulation; package forge.ai.simulation;
import forge.game.spellability.LandAbility;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@@ -158,9 +158,9 @@ public class GameSimulator {
} }
public Score simulateSpellAbility(SpellAbility origSa, GameStateEvaluator eval, boolean resolve) { public Score simulateSpellAbility(SpellAbility origSa, GameStateEvaluator eval, boolean resolve) {
SpellAbility sa; SpellAbility sa;
if (origSa instanceof LandAbility) { if (origSa.isLandAbility()) {
Card hostCard = (Card) copier.find(origSa.getHostCard()); Card hostCard = (Card) copier.find(origSa.getHostCard());
if (!aiPlayer.playLand(hostCard, false)) { if (!aiPlayer.playLand(hostCard, false, origSa)) {
System.err.println("Simulation: Couldn't play land! " + origSa); System.err.println("Simulation: Couldn't play land! " + origSa);
} }
sa = origSa; sa = origSa;

View File

@@ -260,7 +260,10 @@ public class GameStateEvaluator {
// The value should be more than the value of having a card in hand, so if a land has an // The value should be more than the value of having a card in hand, so if a land has an
// activated ability but not a mana ability, it will still be played. // activated ability but not a mana ability, it will still be played.
for (SpellAbility m: c.getNonManaAbilities()) { for (SpellAbility m: c.getNonManaAbilities()) {
if (!m.getPayCosts().hasTapCost()) { if (m.isLandAbility()) {
// Land Ability has no extra Score
continue;
} if (!m.getPayCosts().hasTapCost()) {
// probably a manland, rate it higher than a rainbow land // probably a manland, rate it higher than a rainbow land
value += 25; value += 25;
} else if (m.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { } else if (m.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {

View File

@@ -24,7 +24,7 @@ import forge.game.card.CardPredicates;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition; import forge.game.spellability.SpellAbilityCondition;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -136,7 +136,7 @@ public class SpellAbilityPicker {
private static boolean isSorcerySpeed(SpellAbility sa, Player player) { private static boolean isSorcerySpeed(SpellAbility sa, Player player) {
// TODO: Can we use the actual rules engine for this instead of trying to do the logic ourselves? // TODO: Can we use the actual rules engine for this instead of trying to do the logic ourselves?
if (sa instanceof LandAbility) { if (sa.isLandAbility()) {
return true; return true;
} }
if (sa.isSpell()) { if (sa.isSpell()) {
@@ -327,16 +327,16 @@ public class SpellAbilityPicker {
} }
private AiPlayDecision canPlayAndPayForSim(final SpellAbility sa) { private AiPlayDecision canPlayAndPayForSim(final SpellAbility sa) {
if (!sa.isLegalAfterStack()) {
return AiPlayDecision.CantPlaySa;
}
if (!sa.checkRestrictions(sa.getHostCard(), player)) { if (!sa.checkRestrictions(sa.getHostCard(), player)) {
return AiPlayDecision.CantPlaySa; return AiPlayDecision.CantPlaySa;
} }
if (sa instanceof LandAbility) { if (sa.isLandAbility()) {
return AiPlayDecision.WillPlay; return AiPlayDecision.WillPlay;
} }
if (!sa.isLegalAfterStack()) {
return AiPlayDecision.CantPlaySa;
}
if (!sa.canPlay()) { if (!sa.canPlay()) {
return AiPlayDecision.CantPlaySa; return AiPlayDecision.CantPlaySa;
} }

View File

@@ -170,7 +170,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
* *
* @return a boolean. * @return a boolean.
*/ */
public final boolean isSecondary() { public boolean isSecondary() {
return getParamOrDefault("Secondary", "False").equals("True"); return getParamOrDefault("Secondary", "False").equals("True");
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,7 +57,7 @@ public class PlayLandVariantEffect extends SpellAbilityEffect {
PaperCard ran = Aggregates.random(cards); PaperCard ran = Aggregates.random(cards);
random = CardFactory.getCard(ran, activator, game); random = CardFactory.getCard(ran, activator, game);
cards.remove(ran); 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.addCloneState(CardFactory.getCloneStates(random, source, sa), game.getNextTimestamp());
source.updateStateForView(); source.updateStateForView();

View File

@@ -7430,7 +7430,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
for (SpellAbility sa : getState(CardStateName.Modal).getSpellAbilities()) { for (SpellAbility sa : getState(CardStateName.Modal).getSpellAbilities()) {
//add alternative costs as additional spell abilities //add alternative costs as additional spell abilities
// only add Spells there // only add Spells there
if (sa.isSpell()) { if (sa.isSpell() || sa.isLandAbility()) {
abilities.add(sa); abilities.add(sa);
abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false)); abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false));
} }
@@ -7466,106 +7466,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
abilities.removeAll(toRemove); 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; return abilities;
} }

View File

@@ -413,17 +413,16 @@ public class CardFactory {
// SpellPermanent only for Original State // SpellPermanent only for Original State
if (c.getCurrentStateName() == CardStateName.Original || c.getCurrentStateName() == CardStateName.Modal || c.getCurrentStateName().toString().startsWith("Specialize")) { 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.isLand()) {
if (c.isPermanent() && !c.isAura() && !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); SpellAbility sa = new SpellPermanent(c);
sa.setCardState(c.getCurrentState());
// Currently only for Modal, might react different when state is always set
//if (c.getCurrentStateName() == CardStateName.Modal) {
sa.setCardState(c.getCurrentState());
//}
c.addSpellAbility(sa); c.addSpellAbility(sa);
} }
// TODO add LandAbility there when refactor MayPlay
} }
CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities()); CardFactoryUtil.addAbilityFactoryAbilities(c, face.getAbilities());

View File

@@ -32,7 +32,7 @@ import forge.game.event.*;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementResult; import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
@@ -1064,7 +1064,7 @@ public class PhaseHandler implements java.io.Serializable {
final Zone currentZone = saHost.getZone(); final Zone currentZone = saHost.getZone();
// Need to check if Zone did change // 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 // 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()); final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost); 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.ReplacementResult;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.*; import forge.game.staticability.*;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
@@ -1691,9 +1691,9 @@ public class Player extends GameEntity implements Comparable<Player> {
game.fireEvent(new GameEventShuffle(this)); 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 // Dakkon Blackblade Avatar will use a similar effect
if (canPlayLand(land, ignoreZoneAndTiming)) { if (canPlayLand(land, ignoreZoneAndTiming, cause)) {
playLandNoCheck(land, null); playLandNoCheck(land, null);
return true; return true;
} }
@@ -1706,7 +1706,7 @@ public class Player extends GameEntity implements Comparable<Player> {
land.setController(this, 0); land.setController(this, 0);
if (land.isFaceDown()) { if (land.isFaceDown()) {
land.turnFaceUp(null); land.turnFaceUp(null);
if (cause instanceof LandAbility) { if (cause.isLandAbility()) {
land.changeToState(cause.getCardStateName()); land.changeToState(cause.getCardStateName());
} }
} }
@@ -1730,12 +1730,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return c; 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) { public final boolean canPlayLand(final Card land, final boolean ignoreZoneAndTiming, SpellAbility landSa) {
if (!ignoreZoneAndTiming) { if (!ignoreZoneAndTiming) {
// CR 305.3 // CR 305.3

View File

@@ -21,24 +21,20 @@ import forge.card.CardStateName;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCopyService; import forge.game.card.CardCopyService;
import forge.game.card.CardPlayOption;
import forge.game.cost.Cost;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
import forge.util.CardTranslation; import forge.util.CardTranslation;
import forge.util.Localizer; import forge.util.Localizer;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; 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) { public LandAbility(Card sourceCard) {
this(sourceCard, sourceCard.getController(), null); super(sourceCard, ManaCost.NO_COST);
getRestrictions().setZone(ZoneType.Hand);
} }
public boolean canPlay(Card newHost) { public boolean canPlay(Card newHost) {
@@ -46,11 +42,21 @@ public class LandAbility extends Ability {
return p.canPlayLand(newHost, false, this); return p.canPlayLand(newHost, false, this);
} }
@Override
public boolean isLandAbility() { return true; }
@Override
public boolean isSecondary() {
return true;
}
@Override @Override
public boolean canPlay() { public boolean canPlay() {
Card land = this.getHostCard(); Card land = this.getHostCard();
final Player p = this.getActivatingPlayer(); final Player p = this.getActivatingPlayer();
if (p == null) {
return false;
}
if (this.getCardState() != null && land.getCurrentStateName() != this.getCardStateName()) { if (this.getCardState() != null && land.getCurrentStateName() != this.getCardStateName()) {
if (!land.isLKI()) { if (!land.isLKI()) {
land = CardCopyService.getLKICopy(land); land = CardCopyService.getLKICopy(land);
@@ -113,4 +119,41 @@ public class LandAbility extends Ability {
return sb.toString(); 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; this.castFaceDown = faceDown;
} }
@Override
public Card getAlternateHost(Card source) { public Card getAlternateHost(Card source) {
boolean lkicheck = false; boolean lkicheck = false;

View File

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

View File

@@ -1,6 +1,6 @@
package forge.ai.simulation; package forge.ai.simulation;
import forge.game.spellability.LandAbility;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -84,7 +84,7 @@ public class SpellAbilityPickerSimulationTest extends SimulationTest {
SpellAbilityPicker picker = new SpellAbilityPicker(game, p); SpellAbilityPicker picker = new SpellAbilityPicker(game, p);
SpellAbility sa = picker.chooseSpellAbilityToPlay(null); SpellAbility sa = picker.chooseSpellAbilityToPlay(null);
AssertJUnit.assertTrue(sa instanceof LandAbility); AssertJUnit.assertTrue(sa.isLandAbility());
AssertJUnit.assertEquals(mountain, sa.getHostCard()); AssertJUnit.assertEquals(mountain, sa.getHostCard());
Plan plan = picker.getPlan(); Plan plan = picker.getPlan();

View File

@@ -25,7 +25,7 @@ import forge.game.card.Card;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerController; import forge.game.player.PlayerController;
import forge.game.player.actions.PassPriorityAction; import forge.game.player.actions.PassPriorityAction;
import forge.game.spellability.LandAbility;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.localinstance.properties.ForgePreferences.FPref; import forge.localinstance.properties.ForgePreferences.FPref;
import forge.model.FModel; import forge.model.FModel;
@@ -169,7 +169,7 @@ public class InputPassPriority extends InputSyncronizedBase {
if (sa.isSpell()) { if (sa.isSpell()) {
return Localizer.getInstance().getMessage("lblCastSpell"); return Localizer.getInstance().getMessage("lblCastSpell");
} }
if (sa instanceof LandAbility) { if (sa.isLandAbility()) {
return Localizer.getInstance().getMessage("lblPlayLand"); return Localizer.getInstance().getMessage("lblPlayLand");
} }
return Localizer.getInstance().getMessage("lblActivateAbility"); return Localizer.getInstance().getMessage("lblActivateAbility");

View File

@@ -33,7 +33,7 @@ import forge.game.mana.ManaRefundService;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerController; import forge.game.player.PlayerController;
import forge.game.player.PlayerView; import forge.game.player.PlayerView;
import forge.game.spellability.LandAbility;
import forge.game.spellability.OptionalCostValue; import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityManaConvert; import forge.game.staticability.StaticAbilityManaConvert;
@@ -70,7 +70,7 @@ public class HumanPlay {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
sa.setActivatingPlayer(p); sa.setActivatingPlayer(p);
if (sa instanceof LandAbility) { if (sa.isLandAbility()) {
if (sa.canPlay()) { if (sa.canPlay()) {
sa.resolve(); sa.resolve();
} }