mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 18:50:06 -08:00
* Fixed provisional game freezing bugs of the show playable cards feature #457 (caused by DelverAbility, OfferingAbility, Rooftop Storm, Omniscience, Aluren).
This commit is contained in:
parent
ad9b73c34c
commit
142e95fe42
20 changed files with 106 additions and 34 deletions
|
|
@ -114,7 +114,7 @@ class CantBeBlockedUnlessAllEffect extends RestrictionEffect {
|
|||
// check if all creatures of defender are able to block this permanent
|
||||
// permanent.canBlock() can't be used because causing recursive call
|
||||
for (Permanent permanent: game.getBattlefield().getAllActivePermanents(filter, blocker.getControllerId(), game)) {
|
||||
if (permanent.isTapped() && !game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, blocker.getControllerId(), game)) {
|
||||
if (permanent.isTapped() && !game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, source, blocker.getControllerId(), game)) {
|
||||
return false;
|
||||
}
|
||||
// check blocker restrictions
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import mage.players.Player;
|
|||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.ActivatedAbility;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -100,9 +101,10 @@ class RooftopStormCostReductionEffect extends CostModificationEffectImpl {
|
|||
Card sourceCard = game.getCard(spell.getSourceId());
|
||||
if (sourceCard != null && sourceCard.hasSubtype("Zombie")) {
|
||||
Player player = game.getPlayer(spell.getControllerId());
|
||||
if (player != null && player.chooseUse(Outcome.Benefit, "Pay {0} rather than pay the mana cost for Zombie creature", game)) {
|
||||
if (player != null &&
|
||||
(CardUtil.isCheckPlayableMode(spell) || player.chooseUse(Outcome.Benefit, "Pay {0} rather than pay the mana cost for Zombie creature", game))) {
|
||||
spell.getManaCostsToPay().clear();
|
||||
spell.getManaCostsToPay().addAll(new ManaCostsImpl("{0}"));
|
||||
spell.getManaCostsToPay().addAll(new ManaCostsImpl<>("{0}"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import mage.game.stack.StackObject;
|
|||
import mage.players.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -99,7 +100,8 @@ class OmniscienceEffect extends CostModificationEffectImpl {
|
|||
&& !sourceCard.getCardType().contains(CardType.LAND)) {
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
String message = "Cast " + sourceCard.getName() + " without paying its mana costs?";
|
||||
if (player != null && player.chooseUse(outcome, message, game)) {
|
||||
if (player != null &&
|
||||
(CardUtil.isCheckPlayableMode(abilityToModify) || player.chooseUse(outcome, message, game))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import mage.game.Game;
|
|||
import mage.game.stack.Spell;
|
||||
import mage.game.stack.StackObject;
|
||||
import mage.players.Player;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -113,7 +114,8 @@ class AlurenEffect extends CostModificationEffectImpl {
|
|||
if (sourceCard != null && sourceCard.getCardType().contains(CardType.CREATURE) && sourceCard.getManaCost().convertedManaCost() <= 3) {
|
||||
Player player = game.getPlayer(stackObject.getControllerId());
|
||||
String message = "Cast " + sourceCard.getName() + " without paying its mana costs?";
|
||||
if (player != null && player.chooseUse(outcome, message, game)) {
|
||||
if (player != null &&
|
||||
(CardUtil.isCheckPlayableMode(abilityToModify) || player.chooseUse(outcome, message, game))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,4 +38,20 @@ import mage.game.Game;
|
|||
public interface ActivatedAbility extends Ability {
|
||||
|
||||
boolean canActivate(UUID playerId, Game game);
|
||||
|
||||
/**
|
||||
* Creates a fresh copy of this activated ability.
|
||||
*
|
||||
* @return A new copy of this ability.
|
||||
*/
|
||||
@Override
|
||||
ActivatedAbility copy();
|
||||
|
||||
/**
|
||||
* Set a flag to know, that the ability is only created adn used to check
|
||||
* what's playbable for the player.
|
||||
*/
|
||||
void setCheckPlayableMode();
|
||||
|
||||
boolean isCheckPlayableMode();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,9 +53,11 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
|
|||
protected TimingRule timing = TimingRule.INSTANT;
|
||||
protected TargetController mayActivate = TargetController.YOU;
|
||||
protected UUID activatorId;
|
||||
protected boolean checkPlayableMode;
|
||||
|
||||
protected ActivatedAbilityImpl(AbilityType abilityType, Zone zone) {
|
||||
super(abilityType, zone);
|
||||
this.checkPlayableMode = false;
|
||||
}
|
||||
|
||||
public ActivatedAbilityImpl(ActivatedAbilityImpl ability) {
|
||||
|
|
@ -63,6 +65,7 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
|
|||
timing = ability.timing;
|
||||
mayActivate = ability.mayActivate;
|
||||
activatorId = ability.activatorId;
|
||||
checkPlayableMode = ability.checkPlayableMode;
|
||||
}
|
||||
|
||||
public ActivatedAbilityImpl(Zone zone) {
|
||||
|
|
@ -217,4 +220,14 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
|
|||
this.timing = timing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCheckPlayableMode() {
|
||||
checkPlayableMode = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCheckPlayableMode() {
|
||||
return checkPlayableMode;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
|
||||
@Override
|
||||
public boolean canActivate(UUID playerId, Game game) {
|
||||
if (this.spellCanBeActivatedRegularlyNow(playerId, game) ||
|
||||
game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST, playerId, game)) {
|
||||
if (game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST, this, playerId, game) // check this first to allow Offering in main phase
|
||||
|| this.spellCanBeActivatedRegularlyNow(playerId, game)) {
|
||||
if (spellAbilityType.equals(SpellAbilityType.SPLIT)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public class AlternativeCost2Impl <T extends AlternativeCost2Impl<T>> extends Co
|
|||
if (reminderText != null) {
|
||||
this.reminderText = new StringBuilder("<i>").append(reminderText).append("</i>").toString();
|
||||
}
|
||||
this.add((Cost) cost);
|
||||
this.add(cost);
|
||||
}
|
||||
|
||||
public AlternativeCost2Impl(final AlternativeCost2Impl cost) {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@
|
|||
package mage.abilities.costs;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.StaticAbility;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import mage.game.Game;
|
|||
*/
|
||||
public interface AsThoughEffect extends ContinuousEffect {
|
||||
|
||||
boolean applies(UUID sourceId, Ability affectedAbility, Ability source, Game game);
|
||||
boolean applies(UUID sourceId, Ability source, Game game);
|
||||
AsThoughEffectType getAsThoughEffectType();
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,13 @@
|
|||
|
||||
package mage.abilities.effects;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.AsThoughEffectType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.EffectType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.Game;
|
||||
|
||||
/**
|
||||
*
|
||||
|
|
@ -52,6 +55,11 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
|
|||
this.type = effect.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(UUID sourceId, Ability affectedAbility, Ability source, Game game) {
|
||||
return applies(sourceId, source, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsThoughEffectType getAsThoughEffectType() {
|
||||
return type;
|
||||
|
|
|
|||
|
|
@ -420,20 +420,32 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
|
||||
public boolean asThough(UUID objectId, AsThoughEffectType type, UUID controllerId, Game game) {
|
||||
return asThough(objectId, type, null, controllerId, game);
|
||||
}
|
||||
|
||||
public boolean asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) {
|
||||
List<AsThoughEffect> asThoughEffectsList = getApplicableAsThoughEffects(type, game);
|
||||
for (AsThoughEffect effect: asThoughEffectsList) {
|
||||
HashSet<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId());
|
||||
for (Ability ability : abilities) {
|
||||
if (controllerId.equals(ability.getControllerId())) {
|
||||
if (effect.applies(objectId, ability, game)) {
|
||||
return true;
|
||||
if (affectedAbility == null) {
|
||||
if (effect.applies(objectId, ability, game)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (effect.applies(objectId, affectedAbility, ability, game)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filters out asThough effects that are not active.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -32,13 +32,10 @@ import mage.constants.Duration;
|
|||
import mage.constants.EffectType;
|
||||
import mage.constants.Outcome;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.effects.ContinuousEffectImpl;
|
||||
import mage.abilities.effects.CostModificationEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.CostModificationType;
|
||||
import mage.game.Game;
|
||||
import mage.game.stack.Spell;
|
||||
|
||||
/**
|
||||
* Simple implementation of a {@link CostModificationEffect} offering simplified
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ import mage.util.CardUtil;
|
|||
if (!target.canChoose(sourceId, controllerId, game)) {
|
||||
return;
|
||||
}
|
||||
if (player.chooseUse(Outcome.Detriment, "Delve cards from your graveyard?", game)) {
|
||||
if (!CardUtil.isCheckPlayableMode(ability) && player.chooseUse(Outcome.Detriment, "Delve cards from your graveyard?", game)) {
|
||||
player.chooseTarget(Outcome.Detriment, target, ability, game);
|
||||
if (target.getTargets().size() > 0) {
|
||||
int adjCost = 0;
|
||||
|
|
@ -94,11 +94,11 @@ import mage.util.CardUtil;
|
|||
if (card == null) {
|
||||
continue;
|
||||
}
|
||||
card.moveToExile(null, null, this.getSourceId(), game);
|
||||
player.moveCardToExileWithInfo(card, null, "", getSourceId(), game, Zone.GRAVEYARD);
|
||||
++adjCost;
|
||||
}
|
||||
game.informPlayers(new StringBuilder(player.getName()).append(" delved ")
|
||||
.append(adjCost).append(" creature").append(adjCost != 1?"s":"").append(" from his or her graveyard").toString());
|
||||
game.informPlayers(new StringBuilder("Delve: ").append(player.getName()).append(" exiled ")
|
||||
.append(adjCost).append(" card").append(adjCost != 1?"s":"").append(" from his or her graveyard").toString());
|
||||
CardUtil.adjustCost((SpellAbility)ability, adjCost);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,11 @@ class OfferingAsThoughEffect extends AsThoughEffectImpl {
|
|||
|
||||
@Override
|
||||
public boolean applies(UUID sourceId, Ability source, Game game) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(UUID sourceId, Ability affectedAbility, Ability source, Game game) {
|
||||
if (sourceId.equals(source.getSourceId())) {
|
||||
Card card = game.getCard(sourceId);
|
||||
if (!card.getOwnerId().equals(source.getControllerId())) {
|
||||
|
|
@ -139,10 +144,7 @@ class OfferingAsThoughEffect extends AsThoughEffectImpl {
|
|||
Object alreadyConfirmed = game.getState().getValue("offering_ok_" + card.getId());
|
||||
game.getState().setValue("offering_" + card.getId(), null);
|
||||
game.getState().setValue("offering_ok_" + card.getId(), null);
|
||||
if (alreadyConfirmed != null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return alreadyConfirmed != null;
|
||||
} else {
|
||||
// first call -> remove previous Ids
|
||||
game.getState().setValue("offering_Id_" + card.getId(), null);
|
||||
|
|
@ -153,7 +155,8 @@ class OfferingAsThoughEffect extends AsThoughEffectImpl {
|
|||
FilterControlledCreaturePermanent filter = ((OfferingAbility) source).getFilter();
|
||||
Card spellToCast = game.getCard(source.getSourceId());
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
if (player != null && player.chooseUse(Outcome.Benefit, "Offer a " + filter.getMessage() + " to cast " + spellToCast.getName() + "?", game)) {
|
||||
if (player != null && !CardUtil.isCheckPlayableMode(affectedAbility) &&
|
||||
player.chooseUse(Outcome.Benefit, "Offer a " + filter.getMessage() + " to cast " + spellToCast.getName() + "?", game)) {
|
||||
Target target = new TargetControlledCreaturePermanent(1,1,filter,true);
|
||||
player.chooseTarget(Outcome.Sacrifice, target, source, game);
|
||||
if (!target.isChosen()) {
|
||||
|
|
@ -185,9 +188,9 @@ class OfferingAsThoughEffect extends AsThoughEffectImpl {
|
|||
|
||||
class OfferingCostReductionEffect extends CostModificationEffectImpl {
|
||||
|
||||
private UUID spellAbilityId;
|
||||
private UUID activationId;
|
||||
private ManaCosts<ManaCost> manaCostsToReduce;
|
||||
private final UUID spellAbilityId;
|
||||
private final UUID activationId;
|
||||
private final ManaCosts<ManaCost> manaCostsToReduce;
|
||||
|
||||
OfferingCostReductionEffect (UUID spellAbilityId, ManaCosts<ManaCost> manaCostsToReduce, UUID activationId) {
|
||||
super(Duration.OneUse, Outcome.Benefit, CostModificationType.REDUCE_COST);
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class ShadowEffect extends RestrictionEffect implements MageSingleton {
|
|||
@Override
|
||||
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game) {
|
||||
if (blocker.getAbilities().containsKey(ShadowAbility.getInstance().getId())
|
||||
|| game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, blocker.getControllerId(), game)) {
|
||||
|| game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, source, blocker.getControllerId(), game)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ public class SuspendAbility extends ActivatedAbilityImpl {
|
|||
MageObject object = game.getObject(sourceId);
|
||||
return (object.getCardType().contains(CardType.INSTANT) ||
|
||||
object.hasAbility(FlashAbility.getInstance().getId(), game) ||
|
||||
game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST, playerId, game) ||
|
||||
game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST, this, playerId, game) ||
|
||||
game.canPlaySorcery(playerId));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ public class ManaPool implements Serializable {
|
|||
|
||||
// check if any mana can be spend to cast the mana cost of an ability
|
||||
private boolean spendAnyMana(Ability ability, Game game) {
|
||||
return game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_ANY_MANA, ability.getControllerId(), game);
|
||||
return game.getContinuousEffects().asThough(ability.getSourceId(), AsThoughEffectType.SPEND_ANY_MANA, ability, ability.getControllerId(), game);
|
||||
}
|
||||
|
||||
public int get(ManaType manaType) {
|
||||
|
|
|
|||
|
|
@ -1834,8 +1834,12 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
|
||||
protected boolean canPlay(ActivatedAbility ability, ManaOptions available, Game game) {
|
||||
if (!(ability instanceof ManaAbility) && ability.canActivate(playerId, game)) {
|
||||
Ability copy = ability.copy();
|
||||
if (!(ability instanceof ManaAbility)) {
|
||||
ActivatedAbility copy = ability.copy();
|
||||
copy.setCheckPlayableMode(); // prevents from endless loops for asking player to use effects by checking this mode
|
||||
if (!copy.canActivate(playerId, game)) {
|
||||
return false;
|
||||
}
|
||||
game.getContinuousEffects().costModification(copy, game);
|
||||
|
||||
Card card = game.getCard(ability.getSourceId());
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import java.util.UUID;
|
|||
|
||||
import mage.Mana;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.ActivatedAbility;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.costs.AlternativeCost;
|
||||
import mage.abilities.costs.AlternativeCostImpl;
|
||||
|
|
@ -490,4 +491,17 @@ public class CardUtil {
|
|||
}
|
||||
return uniqueString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the ability is used to check which cards
|
||||
* are playable on hand. (Issue #457)
|
||||
* @param ability - ability to check
|
||||
* @return
|
||||
*/
|
||||
public static boolean isCheckPlayableMode(Ability ability) {
|
||||
if (ability instanceof ActivatedAbility) {
|
||||
return ((ActivatedAbility) ability).isCheckPlayableMode();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue