Improved playable abilities and split cards:

* Now human player uses same code for playable abilities search as test framework (old version used different code, so it could not work in one of the modes);
* Split cards - improves playable highlights;
* Split cards - fixed that it doesn't work with dynamic added abilities like flashback (#6327, #6470, #6549);
This commit is contained in:
Oleg Agafonov 2020-06-03 12:44:58 +04:00
parent 47af865bc3
commit b94344341b
8 changed files with 399 additions and 186 deletions

View file

@ -1047,8 +1047,9 @@ public class HumanPlayer extends PlayerImpl {
Zone zone = game.getState().getZone(object.getId());
if (zone != null) {
// look at card or try to cast/activate abilities
LinkedHashMap<UUID, ActivatedAbility> useableAbilities = new LinkedHashMap<>();
Player actingPlayer = null;
LinkedHashMap<UUID, ActivatedAbility> useableAbilities = null;
if (playerId.equals(game.getPriorityPlayerId())) {
actingPlayer = this;
} else if (getPlayersUnderYourControl().contains(game.getPriorityPlayerId())) {
@ -1060,11 +1061,10 @@ public class HumanPlayer extends PlayerImpl {
if (object instanceof Card
&& ((Card) object).isFaceDown(game)
&& lookAtFaceDownCard((Card) object, game, useableAbilities == null ? 0 : useableAbilities.size())) {
&& lookAtFaceDownCard((Card) object, game, useableAbilities.size())) {
result = true;
} else {
if (useableAbilities != null
&& !useableAbilities.isEmpty()) {
if (!useableAbilities.isEmpty()) {
activateAbility(useableAbilities, object, game);
result = true;
}
@ -1347,8 +1347,7 @@ public class HumanPlayer extends PlayerImpl {
Zone zone = game.getState().getZone(object.getId());
if (zone != null) {
LinkedHashMap<UUID, ActivatedManaAbilityImpl> useableAbilities = getUseableManaAbilities(object, zone, game);
if (useableAbilities != null
&& !useableAbilities.isEmpty()) {
if (!useableAbilities.isEmpty()) {
useableAbilities = ManaUtil.tryToAutoPay(unpaid, useableAbilities); // eliminates other abilities if one fits perfectly
currentlyUnpaidMana = unpaid;
activateAbility(useableAbilities, object, game);

View file

@ -914,4 +914,97 @@ public class MorphTest extends CardTestPlayerBase {
Permanent akroma = getPermanent("Akroma, Angel of Fury");
Assert.assertTrue("Akroma has to be red", akroma.getColor(currentGame).isRed());
}
@Test
public void test_LandWithMorph_PlayLand() {
// Morph {2}
addCard(Zone.HAND, playerA, "Zoetic Cavern");
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Zoetic Cavern", true);
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern");
setChoice(playerA, "No"); // no morph (canPay for generic/colored mana returns true all the time, so xmage ask about face down cast)
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
// 1 action must be here ("no" option is restores on failed morph call in playLand)
//assertAllCommandsUsed();
assertChoicesCount(playerA, 1);
assertPermanentCount(playerA, "Zoetic Cavern", 1);
}
@Test
public void test_LandWithMorph_Morph() {
// Morph {2}
addCard(Zone.HAND, playerA, "Zoetic Cavern");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Zoetic Cavern", true);
checkPlayableAbility("morph must be replaced by play ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Morph", false);
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern");
setChoice(playerA, "Yes"); // morph
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Zoetic Cavern", 0);
assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1);
}
@Test
public void test_LandWithMorph_MorphAfterLand() {
removeAllCardsFromHand(playerA);
// Morph {2}
addCard(Zone.HAND, playerA, "Zoetic Cavern");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
//
addCard(Zone.HAND, playerA, "Island", 1);
// play land first
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Island");
// morph ability (play as face down) calls from playLand method, so it visible for play land command
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Zoetic Cavern", true);
checkPlayableAbility("morph must be replaced by play ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Morph", false);
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern");
setChoice(playerA, "Yes"); // morph
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Island", 1);
assertPermanentCount(playerA, "Zoetic Cavern", 0);
assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1);
}
@Test
public void test_LandWithMorph_MorphFromLibrary() {
removeAllCardsFromLibrary(playerA);
// You may play lands and cast spells from the top of your library.
addCard(Zone.BATTLEFIELD, playerA, "Future Sight");
//
// Morph {2}
addCard(Zone.LIBRARY, playerA, "Zoetic Cavern");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3);
checkPlayableAbility("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Zoetic Cavern", true);
checkPlayableAbility("morph must be replaced by play ability", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Morph", false);
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Zoetic Cavern");
setChoice(playerA, "Yes"); // morph
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Zoetic Cavern", 0);
assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1);
}
}

View file

@ -31,9 +31,10 @@ public class DoublingCubeTest extends CardTestPlayerBase {
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{3}, {T}:");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertManaPool(playerA, ManaType.COLORLESS, 4);
assertAllCommandsUsed();
}
}

View file

@ -0,0 +1,108 @@
package org.mage.test.cards.split;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author JayDi85
*/
public class CastSplitCardsWithAsThoughManaTest extends CardTestPlayerBase {
@Test
public void test_AsThoughMana_Simple() {
// {1}{R}
// When Dire Fleet Daredevil enters the battlefield, exile target instant or sorcery card from an opponents graveyard.
// You may cast that card this turn, and you may spend mana as though it were mana of any type to cast that spell.
// If that card would be put into a graveyard this turn, exile it instead.
addCard(Zone.HAND, playerA, "Dire Fleet Daredevil", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
//
addCard(Zone.GRAVEYARD, playerB, "Lightning Bolt", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 1);
// cast fleet
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dire Fleet Daredevil");
addTarget(playerA, "Lightning Bolt");
// cast bolt with blue mana
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertLife(playerB, 20 - 3);
}
@Test
public void test_AsThoughMana_Split_WearTear() {
// {1}{R}
// When Dire Fleet Daredevil enters the battlefield, exile target instant or sorcery card from an opponents graveyard.
// You may cast that card this turn, and you may spend mana as though it were mana of any type to cast that spell.
// If that card would be put into a graveyard this turn, exile it instead.
addCard(Zone.HAND, playerA, "Dire Fleet Daredevil", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
//
// Wear {1}{R} Destroy target artifact.
// Tear {W} Destroy target enchantment.
addCard(Zone.GRAVEYARD, playerB, "Wear // Tear", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.BATTLEFIELD, playerB, "Bident of Thassa", 1); // Legendary Enchantment Artifact
addCard(Zone.BATTLEFIELD, playerB, "Bow of Nylea", 1); // Legendary Enchantment Artifact
// cast fleet
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dire Fleet Daredevil");
addTarget(playerA, "Wear // Tear");
// cast Wear with black mana
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wear", "Bident of Thassa");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerB, "Bident of Thassa", 1);
assertPermanentCount(playerB, "Bow of Nylea", 1);
}
@Test
public void test_AsThoughMana_Split_CatchRelease() {
// {1}{R}
// When Dire Fleet Daredevil enters the battlefield, exile target instant or sorcery card from an opponents graveyard.
// You may cast that card this turn, and you may spend mana as though it were mana of any type to cast that spell.
// If that card would be put into a graveyard this turn, exile it instead.
addCard(Zone.HAND, playerA, "Dire Fleet Daredevil", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
//
// Catch {1}{U}{R} Gain control of target permanent until end of turn. Untap it. It gains haste until end of turn.
// Release {4}{R}{W} Each player sacrifices an artifact, a creature, an enchantment, a land, and a planeswalker.
addCard(Zone.GRAVEYARD, playerB, "Catch // Release", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3);
addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1);
// cast fleet
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dire Fleet Daredevil");
addTarget(playerA, "Catch // Release");
// cast Catch with black mana
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Catch", "Balduvian Bears");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Balduvian Bears", 1);
assertPermanentCount(playerB, "Balduvian Bears", 0);
}
}

View file

@ -0,0 +1,70 @@
package org.mage.test.cards.split;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author JayDi85
*/
public class CastSplitCardsWithFlashbackTest extends CardTestPlayerBase {
@Test
public void test_Flashback_Simple() {
// {1}{U}
// When Snapcaster Mage enters the battlefield, target instant or sorcery card in your graveyard gains flashback until end of turn.
addCard(Zone.HAND, playerA, "Snapcaster Mage", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
//
addCard(Zone.GRAVEYARD, playerA, "Lightning Bolt", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
// add flashback
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snapcaster Mage");
addTarget(playerA, "Lightning Bolt");
// cast as flashback
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback", playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertLife(playerB, 20 - 3);
}
@Test
public void test_Flashback_Split() {
// {1}{U}
// When Snapcaster Mage enters the battlefield, target instant or sorcery card in your graveyard gains flashback until end of turn.
addCard(Zone.HAND, playerA, "Snapcaster Mage", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
//
// Wear {1}{R} Destroy target artifact.
// Tear {W} Destroy target enchantment.
addCard(Zone.GRAVEYARD, playerA, "Wear // Tear", 1);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerB, "Bident of Thassa", 1); // Legendary Enchantment Artifact
addCard(Zone.BATTLEFIELD, playerB, "Bow of Nylea", 1); // Legendary Enchantment Artifact
// add flashback
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snapcaster Mage");
addTarget(playerA, "Wear // Tear");
// cast as flashback
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback {1}{R}", "Bident of Thassa");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerB, "Bident of Thassa", 1);
assertPermanentCount(playerB, "Bow of Nylea", 1);
}
}

View file

@ -963,7 +963,7 @@ public class TestPlayer implements Player {
// you don't need to use stack command all the time, so some cast commands can be skiped to next check
if (game.getStack().isEmpty()) {
this.chooseStrictModeFailed("cast/activate", game,
"Can't find available command - " + action.getAction() + " (use checkPlayableAbility for non castable checks)", true);
"Can't find available command - " + action.getAction() + " (use checkPlayableAbility for \"non available\" checks)", true);
}
} // turn/step
}
@ -1088,7 +1088,7 @@ public class TestPlayer implements Player {
.map(a -> (a.getZone() + " -> "
+ a.getSourceObject(game).getIdName() + " -> "
+ (a.toString().length() > 0
? a.toString().substring(0, Math.min(20, a.toString().length()) - 1)
? a.toString().substring(0, Math.min(20, a.toString().length()))
: a.getClass().getSimpleName())
+ "..."))
.sorted()

View file

@ -110,11 +110,13 @@ public abstract class SplitCard extends CardImpl {
public Abilities<Ability> getAbilities() {
Abilities<Ability> allAbilites = new AbilitiesImpl<>();
for (Ability ability : super.getAbilities()) {
// ignore split abilities TODO: why it here, for GUI's cleanup in card texts? Maybe it can be removed
if (ability instanceof SpellAbility
&& ((SpellAbility) ability).getSpellAbilityType() != SpellAbilityType.SPLIT
&& ((SpellAbility) ability).getSpellAbilityType() != SpellAbilityType.SPLIT_AFTERMATH) {
allAbilites.add(ability);
&& (((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.SPLIT
|| ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.SPLIT_AFTERMATH)) {
continue;
}
allAbilites.add(ability);
}
allAbilites.addAll(leftHalfCard.getAbilities());
allAbilites.addAll(rightHalfCard.getAbilities());

View file

@ -246,7 +246,6 @@ public abstract class PlayerImpl implements Player, Serializable {
this.storedBookmark = player.storedBookmark;
this.topCardRevealed = player.topCardRevealed;
this.playersUnderYourControl.clear();
this.playersUnderYourControl.addAll(player.playersUnderYourControl);
this.usersAllowedToSeeHandCards.addAll(player.usersAllowedToSeeHandCards);
@ -254,7 +253,6 @@ public abstract class PlayerImpl implements Player, Serializable {
this.isGameUnderControl = player.isGameUnderControl;
this.turnController = player.turnController;
this.turnControllers.clear();
this.turnControllers.addAll(player.turnControllers);
this.passed = player.passed;
@ -1190,18 +1188,20 @@ public abstract class PlayerImpl implements Player, Serializable {
return false;
}
ActivatedAbility playLandAbility = null;
boolean found = false;
boolean foundAlternative = false;
for (Ability ability : card.getAbilities()) {
// if cast for noMana no Alternative costs are allowed
if ((ability instanceof AlternativeSourceCosts)
|| (ability instanceof OptionalAdditionalSourceCosts)) {
found = true;
foundAlternative = true;
}
if (ability instanceof PlayLandAbility) {
playLandAbility = (ActivatedAbility) ability;
}
}
if (found) {
// try alternative cast (face down)
if (foundAlternative) {
SpellAbility spellAbility = new SpellAbility(null, "",
game.getState().getZone(card.getId()), SpellAbilityType.FACE_DOWN_CREATURE);
spellAbility.setControllerId(this.getId());
@ -1210,9 +1210,11 @@ public abstract class PlayerImpl implements Player, Serializable {
return true;
}
}
if (playLandAbility == null) {
return false;
}
//20091005 - 114.2a
ActivationStatus activationStatus = playLandAbility.canActivate(this.playerId, game);
if (ignoreTiming) {
@ -1496,110 +1498,29 @@ public abstract class PlayerImpl implements Player, Serializable {
return useable;
}
// Get the usable activated abilities for a *single card object*, that is, either a card or half of a split card.
// Also called on the whole split card but only passing the fuse ability and other whole-split-card shared abilities
// as candidates.
private void getUseableActivatedAbilitiesHalfImpl(MageObject object, Zone zone, Game game, Abilities<Ability> candidateAbilites,
LinkedHashMap<UUID, ActivatedAbility> output) {
boolean canUse = !(object instanceof Permanent) || ((Permanent) object).canUseActivatedAbilities(game);
ManaOptions availableMana = null;
// ManaOptions availableMana = getManaAvailable(game); // can only be activated if mana calculation works flawless otherwise player can't play spells they could play if calculation would work correctly
// availableMana.addMana(manaPool.getMana());
for (Ability ability : candidateAbilites) {
if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) {
if (ability.getZone().match(zone)) {
if (ability instanceof ActivatedAbility) {
if (ability instanceof ActivatedManaAbilityImpl) {
if (((ActivatedAbility) ability).canActivate(playerId, game).canActivate()) {
output.put(ability.getId(), (ActivatedAbility) ability);
}
} else if (canPlay(((ActivatedAbility) ability), availableMana, object, game)) {
output.put(ability.getId(), (ActivatedAbility) ability);
}
} else if (ability instanceof AlternativeSourceCosts) {
if (object.isLand()) {
for (Ability ability2 : object.getAbilities().copy()) {
if (ability2 instanceof PlayLandAbility) {
output.put(ability2.getId(), (ActivatedAbility) ability2);
}
}
}
}
}
}
}
if (zone != Zone.HAND) {
if (Zone.GRAVEYARD == zone && canPlayCardsFromGraveyard()) {
for (ActivatedAbility ability : candidateAbilites.getPlayableAbilities(Zone.HAND)) {
if (canUse
|| ability.getAbilityType() == AbilityType.SPECIAL_ACTION) {
if (ability.getZone().equals(zone) || ability.getZone().equals(Zone.HAND)) {
if (ability.canActivate(playerId, game).canActivate()) {
output.put(ability.getId(), ability);
}
}
}
}
}
if (zone != Zone.BATTLEFIELD) {
for (Ability ability : candidateAbilites) {
if (ability.getZone().equals(zone) || ability.getZone().equals(Zone.HAND)) {
if (game.getContinuousEffects().asThough(object.getId(),
AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE,
null,
this.getId(),
game)
!= null
// if anyone sees an issue with this code, please report it. Worked in my testing. !
|| game.getContinuousEffects().asThough(object.getId(),
AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE,
ability,
this.getId(),
game)
!= null) {
if (canUse
|| ability.getAbilityType() == AbilityType.SPECIAL_ACTION) {
ability.setControllerId(this.getId());
if (ability instanceof ActivatedAbility
&& ability.getZone().match(Zone.HAND)
&& ((ActivatedAbility) ability).canActivate(playerId, game).canActivate()) {
output.put(ability.getId(), (ActivatedAbility) ability);
}
}
}
}
}
}
}
}
@Override
public LinkedHashMap<UUID, ActivatedAbility> getUseableActivatedAbilities(MageObject object, Zone zone, Game game) {
// TODO: replace with getPlayableFromNonHandCardAll (uses for all tests)
LinkedHashMap<UUID, ActivatedAbility> useable = new LinkedHashMap<>();
boolean previousState = game.inCheckPlayableState();
game.setCheckPlayableState(true);
LinkedHashMap<UUID, ActivatedAbility> useable = new LinkedHashMap<>();
if (object instanceof StackAbility) { // It may not be possible to activate abilities of stack abilities
try {
// It may not be possible to activate abilities of stack abilities
if (object instanceof StackAbility) {
return useable;
}
if (object instanceof SplitCard) { // TODO: use of getAbilities(game)
SplitCard splitCard = (SplitCard) object;
getUseableActivatedAbilitiesHalfImpl(splitCard.getLeftHalfCard(),
zone, game, splitCard.getLeftHalfCard().getAbilities(game), useable);
getUseableActivatedAbilitiesHalfImpl(splitCard.getRightHalfCard(),
zone, game, splitCard.getRightHalfCard().getAbilities(game), useable);
getUseableActivatedAbilitiesHalfImpl(splitCard,
zone, game, splitCard.getSharedAbilities(game), useable);
} else if (object instanceof Card) {
getUseableActivatedAbilitiesHalfImpl(object,
zone, game, ((Card) object).getAbilities(game), useable);
} else if (object != null) {
getUseableActivatedAbilitiesHalfImpl(object,
zone, game, object.getAbilities(), useable);
getOtherUseableActivatedAbilities(object, zone, game, useable);
}
// collect and filter playable activated abilities
List<Ability> allPlayable = getPlayable(game, true, zone, false);
for (Ability ability : allPlayable) {
if (ability instanceof ActivatedAbility) {
if (object.hasAbility(ability, game)) {
useable.putIfAbsent(ability.getId(), (ActivatedAbility) ability);
}
}
}
} finally {
game.setCheckPlayableState(previousState);
}
return useable;
}
@ -3184,6 +3105,51 @@ public abstract class PlayerImpl implements Player, Serializable {
return false;
}
protected ActivatedAbility findActivatedAbilityFromPlayable(Card card, ManaOptions manaAvailable, Ability ability, Game game) {
// replace alternative abilities by real play abilities (e.g. morph/facedown static ability by play land)
if (ability instanceof ActivatedManaAbilityImpl) {
// mana ability
if (((ActivatedManaAbilityImpl) ability).canActivate(this.getId(), game).canActivate()) {
return (ActivatedManaAbilityImpl) ability;
}
} else if (ability instanceof AlternativeSourceCosts) {
// alternative cost must be replaced by real play ability
return findActivatedAbilityFromAlternativeSourceCost(card, manaAvailable, ability, game);
} else if (ability instanceof ActivatedAbility) {
// activated ability
if (canPlay((ActivatedAbility) ability, manaAvailable, card, game)) {
return (ActivatedAbility) ability;
}
}
// non playable abilities like static
return null;
}
protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(Card card, ManaOptions manaAvailable, Ability ability, Game game) {
// alternative cost must be replaced by real play ability
if (ability instanceof AlternativeSourceCosts) {
AlternativeSourceCosts altAbility = (AlternativeSourceCosts) ability;
if (card.isLand()) {
// land
// morph ability is static, so it must be replaced with play land ability (playLand search and try to use face down first)
if (canLandPlayAlternateSourceCostsAbility(card, manaAvailable, ability, game)) { // e.g. Land with Morph
Optional<Ability> landAbility = card.getAbilities(game).stream().filter(a -> a instanceof PlayLandAbility).findFirst();
if (landAbility.isPresent()) {
return (ActivatedAbility) landAbility.get();
}
}
} else {
// creature and other
if (altAbility.isAvailable(card.getSpellAbility(), game)) {
return card.getSpellAbility();
}
}
}
return null;
}
protected boolean canLandPlayAlternateSourceCostsAbility(Card sourceObject, ManaOptions available, Ability ability, Game game) {
if (!(sourceObject instanceof Permanent)) {
Ability sourceAbility = sourceObject.getAbilities().stream()
@ -3216,27 +3182,7 @@ public abstract class PlayerImpl implements Player, Serializable {
return false;
}
private void getPlayableFromGraveyardCard(Game game, Card card, Abilities<Ability> candidateAbilities, ManaOptions availableMana, List<Ability> output) {
MageObjectReference permittingObject = game.getContinuousEffects().asThough(card.getId(),
AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, card.getSpellAbility(), this.getId(), game);
for (ActivatedAbility ability : candidateAbilities.getActivatedAbilities(Zone.ALL)) {
boolean possible = false;
if (ability.getZone().match(Zone.GRAVEYARD)) {
possible = true;
} else if (ability.getZone().match(Zone.HAND)
&& (ability instanceof SpellAbility
|| ability instanceof PlayLandAbility)) {
if (permittingObject != null || canPlayCardsFromGraveyard()) {
possible = true;
}
}
if (possible && canPlay(ability, availableMana, card, game)) {
output.add(ability);
}
}
}
private void getPlayableFromNonHandCardAll(Game game, Zone fromZone, Card card, ManaOptions availableMana, List<Ability> output) {
private void getPlayableFromCardAll(Game game, Zone fromZone, Card card, ManaOptions availableMana, List<Ability> output) {
if (fromZone == null) {
return;
}
@ -3244,16 +3190,16 @@ public abstract class PlayerImpl implements Player, Serializable {
// BASIC abilities
if (card instanceof SplitCard) {
SplitCard splitCard = (SplitCard) card;
getPlayableFromNonHandCardSingle(game, fromZone, splitCard.getLeftHalfCard(), splitCard.getLeftHalfCard().getAbilities(), availableMana, output);
getPlayableFromNonHandCardSingle(game, fromZone, splitCard.getRightHalfCard(), splitCard.getRightHalfCard().getAbilities(), availableMana, output);
getPlayableFromNonHandCardSingle(game, fromZone, splitCard, splitCard.getSharedAbilities(game), availableMana, output);
getPlayableFromCardSingle(game, fromZone, splitCard.getLeftHalfCard(), splitCard.getLeftHalfCard().getAbilities(), availableMana, output);
getPlayableFromCardSingle(game, fromZone, splitCard.getRightHalfCard(), splitCard.getRightHalfCard().getAbilities(), availableMana, output);
getPlayableFromCardSingle(game, fromZone, splitCard, splitCard.getSharedAbilities(game), availableMana, output);
} else if (card instanceof AdventureCard) {
// adventure must use different card characteristics for different spells (main or adventure)
AdventureCard adventureCard = (AdventureCard) card;
getPlayableFromNonHandCardSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(), availableMana, output);
getPlayableFromNonHandCardSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output);
getPlayableFromCardSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(), availableMana, output);
getPlayableFromCardSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output);
} else {
getPlayableFromNonHandCardSingle(game, fromZone, card, card.getAbilities(), availableMana, output);
getPlayableFromCardSingle(game, fromZone, card, card.getAbilities(game), availableMana, output);
}
// DYNAMIC ADDED abilities
@ -3278,7 +3224,7 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
private void getPlayableFromNonHandCardSingle(Game game, Zone fromZone, Card card, Abilities<Ability> candidateAbilities, ManaOptions availableMana, List<Ability> output) {
private void getPlayableFromCardSingle(Game game, Zone fromZone, Card card, Abilities<Ability> candidateAbilities, ManaOptions availableMana, List<Ability> output) {
// check "can play" condition as affected controller (BUT play from not own hand zone must be checked as original controller)
for (ActivatedAbility ability : candidateAbilities.getActivatedAbilities(Zone.ALL)) {
boolean isPlaySpell = (ability instanceof SpellAbility);
@ -3339,8 +3285,13 @@ public abstract class PlayerImpl implements Player, Serializable {
possibleToPlay = true;
}
if (possibleToPlay && canPlay(ability, availableMana, card, game)) {
output.add(ability);
if (!possibleToPlay) {
continue;
}
ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game);
if (playAbility != null && !output.contains(playAbility)) {
output.add(playAbility);
}
} finally {
ability.setControllerId(savedControllerId);
@ -3355,8 +3306,13 @@ public abstract class PlayerImpl implements Player, Serializable {
public List<Ability> getPlayable(Game game, boolean hidden, Zone fromZone, boolean hideDuplicatedAbilities) {
List<Ability> playable = new ArrayList<>();
if (shouldSkipGettingPlayable(game)) {
return playable;
}
boolean previousState = game.inCheckPlayableState();
game.setCheckPlayableState(true);
if (!shouldSkipGettingPlayable(game)) {
try {
ManaOptions availableMana = getManaAvailable(game);
availableMana.addMana(manaPool.getMana());
for (ConditionalMana conditionalMana : manaPool.getConditionalMana()) {
@ -3390,26 +3346,9 @@ public abstract class PlayerImpl implements Player, Serializable {
continue;
}
// if have alternative cost
if (ability instanceof ActivatedAbility) {
// normal ability
if (canPlay((ActivatedAbility) ability, availableMana, card, game)) {
playable.add(ability);
}
} else if (ability instanceof AlternativeSourceCosts) {
if (card.isLand()) {
if (canLandPlayAlternateSourceCostsAbility(card, availableMana, ability, game)) { // e.g. Land with Morph
playable.add(ability);
}
} else if (card.isCreature()) { // e.g. makes a card available for play by Morph if the card may not be cast normally
if (!playable.contains(card.getSpellAbility())) {
if (((AlternativeSourceCosts) ability).isAvailable(card.getSpellAbility(), game)) {
playable.add(card.getSpellAbility());
}
}
}
} else {
// unknown type
ActivatedAbility playAbility = findActivatedAbilityFromPlayable(card, availableMana, ability, game);
if (playAbility != null && !playable.contains(playAbility)) {
playable.add(playAbility);
}
}
}
@ -3418,14 +3357,14 @@ public abstract class PlayerImpl implements Player, Serializable {
if (fromAll || fromZone == Zone.GRAVEYARD) {
for (Card card : graveyard.getCards(game)) {
getPlayableFromNonHandCardAll(game, Zone.GRAVEYARD, card, availableMana, playable);
getPlayableFromCardAll(game, Zone.GRAVEYARD, card, availableMana, playable);
}
}
if (fromAll || fromZone == Zone.EXILED) {
for (ExileZone exile : game.getExile().getExileZones()) {
for (Card card : exile.getCards(game)) {
getPlayableFromNonHandCardAll(game, Zone.EXILED, card, availableMana, playable);
getPlayableFromCardAll(game, Zone.EXILED, card, availableMana, playable);
}
}
}
@ -3435,7 +3374,7 @@ public abstract class PlayerImpl implements Player, Serializable {
for (Cards revealedCards : game.getState().getRevealed().values()) {
for (Card card : revealedCards.getCards(game)) {
// revealed cards can be from any zones
getPlayableFromNonHandCardAll(game, game.getState().getZone(card.getId()), card, availableMana, playable);
getPlayableFromCardAll(game, game.getState().getZone(card.getId()), card, availableMana, playable);
}
}
}
@ -3444,7 +3383,7 @@ public abstract class PlayerImpl implements Player, Serializable {
if (fromAll || fromZone == Zone.OUTSIDE) {
for (Cards companionCards : game.getState().getCompanion().values()) {
for (Card card : companionCards.getCards(game)) {
getPlayableFromNonHandCardAll(game, Zone.OUTSIDE, card, availableMana, playable);
getPlayableFromCardAll(game, Zone.OUTSIDE, card, availableMana, playable);
}
}
}
@ -3453,12 +3392,10 @@ public abstract class PlayerImpl implements Player, Serializable {
if (fromAll || fromZone == Zone.LIBRARY) {
for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {
Player player = game.getPlayer(playerInRangeId);
if (player != null) {
if (/*player.isTopCardRevealed() &&*/player.getLibrary().hasCards()) {
if (player != null && player.getLibrary().hasCards()) {
Card card = player.getLibrary().getFromTop(game);
if (card != null) {
getPlayableFromNonHandCardAll(game, Zone.LIBRARY, card, availableMana, playable);
}
getPlayableFromCardAll(game, Zone.LIBRARY, card, availableMana, playable);
}
}
}
@ -3471,20 +3408,22 @@ public abstract class PlayerImpl implements Player, Serializable {
// activated abilities from battlefield objects
if (fromAll || fromZone == Zone.BATTLEFIELD) {
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(playerId)) {
LinkedHashMap<UUID, ActivatedAbility> useableAbilities = getUseableActivatedAbilities(permanent, Zone.BATTLEFIELD, game);
for (ActivatedAbility ability : useableAbilities.values()) {
List<Ability> battlePlayable = new ArrayList<>();
getPlayableFromCardAll(game, Zone.BATTLEFIELD, permanent, availableMana, battlePlayable);
for (Ability ability : battlePlayable) {
if (ability instanceof ActivatedAbility) {
activatedUnique.putIfAbsent(ability.toString(), ability);
activatedAll.add(ability);
}
}
}
}
// activated abilities from stack objects
if (fromAll || fromZone == Zone.STACK) {
for (StackObject stackObject : game.getState().getStack()) {
for (ActivatedAbility ability : stackObject.getAbilities().getActivatedAbilities(Zone.STACK)) {
if (ability != null
&& canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) {
if (canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) {
activatedUnique.put(ability.toString(), ability);
activatedAll.add(ability);
}
@ -3496,8 +3435,7 @@ public abstract class PlayerImpl implements Player, Serializable {
if (fromAll || fromZone == Zone.COMMAND) {
for (CommandObject commandObject : game.getState().getCommand()) {
for (ActivatedAbility ability : commandObject.getAbilities().getActivatedAbilities(Zone.COMMAND)) {
if (ability.isControlledBy(getId())
&& canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) {
if (canPlay(ability, availableMana, game.getObject(ability.getSourceId()), game)) {
activatedUnique.put(ability.toString(), ability);
activatedAll.add(ability);
}
@ -3510,8 +3448,10 @@ public abstract class PlayerImpl implements Player, Serializable {
} else {
playable.addAll(activatedAll);
}
} finally {
game.setCheckPlayableState(previousState);
}
game.setCheckPlayableState(false);
return playable;
}