Rework AsThough handling to allow choosing/affecting a specific alternate cast (#11114)

* Rework AsThoughEffect

* some cleanup of MageIdentifer

* refactor ActivationStatus

* fix bolas's citadel

* fix a couple of the Alternative Cost being applied too broadly.

* fix Risen Executioneer

* allow cancellation of AsThough choice.

* fix One with the Multiverse

* cleanup cards needing their own MageIdentifier

* last couple of fixes

* apply reviews for cleaner code.

* some more cleanup
This commit is contained in:
Susucre 2023-10-03 00:42:54 +02:00 committed by GitHub
parent ba135abc78
commit 7c454fb24c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 1176 additions and 395 deletions

View file

@ -4,9 +4,21 @@ package mage;
* Used to identify specific actions/events and to be able to assign them to the
* correct watcher or other processing.
*
* @author LevelX2
* @author LevelX2, Susucr
*/
public enum MageIdentifier {
// No special behavior. Cleaner than null as a default.
Default,
// -------------------------------- //
// spell cast watchers //
// -------------------------------- //
//
// All those are used by a watcher to track spells cast using a matching MageIdentifier way.
//
// e.g. [[Johann, Apprentice Sorcerer]]
// "Once each turn, you may cast an instant or sorcery spell from the top of your library."
//
CastFromGraveyardOnceWatcher,
CemeteryIlluminatorWatcher,
GisaAndGeralfWatcher,
@ -19,7 +31,65 @@ public enum MageIdentifier {
WishWatcher,
GlimpseTheCosmosWatcher,
SerraParagonWatcher,
OneWithTheMultiverseWatcher,
OneWithTheMultiverseWatcher("Without paying manacost"),
JohannApprenticeSorcererWatcher,
KaghaShadowArchdruidWatcher
KaghaShadowArchdruidWatcher,
CourtOfLocthwainWatcher("Without paying manacost"),
// ----------------------------//
// alternate casts //
// ----------------------------//
//
// All those are used to link (cost) modification only when cast
// using an AsThough with the matching MageIdentifier.
//
// e.g. [[Bolas's Citadel]]
// """
// You may look at the top card of your library any time.
//
// You may play lands and cast spells from the top of your library.
// If you cast a spell this way, pay life equal to its mana value rather than pay its mana cost.
// """
//
// If there are other ways to cast from the top of the library, then the MageIdentifier being different
// means that the alternate cast won't apply to the other ways to cast.
BolassCitadelAlternateCast,
RisenExectutionerAlternateCast,
DemilichAlternateCast,
DemonicEmbraceAlternateCast,
FalcoSparaPactweaverAlternateCast,
HelbruteAlternateCast,
MaestrosAscendencyAlternateCast,
NashiMoonSagesScionAlternateCast,
RafinnesGuidanceAlternateCast,
RonaSheoldredsFaithfulAlternateCast,
ScourgeOfNelTothAlternateCast,
SqueeDubiousMonarchAlternateCast,
WorldheartPhoenixAlternateCast,
XandersPactAlternateCast;
/**
* Additional text if there is need to differentiate two very similar effects
* from the same source in the UI.
* See [[Court of Lochtwain]] for an example.
* """
* At the beginning of your upkeep, exile the top card of target opponents library.
* You may play that card for as long as it remains exiled, and mana of any type can be spent to cast it.
* If you're the monarch, until end of turn, you may cast a spell from among cards exiled with
* Court of Locthwain without paying its mana cost.
* """
*/
private final String additionalText;
MageIdentifier() {
this("");
}
MageIdentifier(String additionalText) {
this.additionalText = additionalText;
}
public String getAdditionalText() {
return this.additionalText;
}
}

View file

@ -79,7 +79,7 @@ public abstract class AbilityImpl implements Ability {
protected List<Hint> hints = new ArrayList<>();
protected List<CardIcon> icons = new ArrayList<>();
protected Outcome customOutcome = null; // uses for AI decisions instead effects
protected MageIdentifier identifier; // used to identify specific ability (e.g. to match with corresponding watcher)
protected MageIdentifier identifier = MageIdentifier.Default; // used to identify specific ability (e.g. to match with corresponding watcher)
protected String appendToRule = null;
protected int sourcePermanentTransformCount = 0;

View file

@ -7,41 +7,57 @@ import mage.constants.TargetController;
import mage.constants.TimingRule;
import mage.game.Game;
import java.util.UUID;
import java.util.*;
/**
* @author BetaSteward_at_googlemail.com
* @author BetaSteward_at_googlemail.com, Susucr
*/
public interface ActivatedAbility extends Ability {
final class ActivationStatus {
private final boolean canActivate;
private final ApprovingObject approvingObject;
// Expected to not be modified after creation.
private final Set<ApprovingObject> approvingObjects;
public ActivationStatus(boolean canActivate, ApprovingObject approvingObject) {
this.canActivate = canActivate;
this.approvingObject = approvingObject;
// If true, the Activation Status will not check if there is an approvingObject.
private final boolean forcedCanActivate;
public ActivationStatus(ApprovingObject approvingObject) {
this.forcedCanActivate = false;
this.approvingObjects = Collections.singleton(approvingObject);
}
public ActivationStatus(Set<ApprovingObject> approvingObjects) {
this(false, approvingObjects);
}
private ActivationStatus(boolean forcedCanActivate, Set<ApprovingObject> approvingObjects) {
this.forcedCanActivate = forcedCanActivate;
this.approvingObjects = new HashSet<>();
this.approvingObjects.addAll(approvingObjects);
}
public boolean canActivate() {
return canActivate;
}
public ApprovingObject getApprovingObject() {
return approvingObject;
}
public static ActivationStatus getFalse() {
return new ActivationStatus(false, null);
return forcedCanActivate || !approvingObjects.isEmpty();
}
/**
* @param approvingObjectAbility ability that allows to activate/use current ability
* @return the set of all approving objects for that ActivationStatus.
* That Set is readonly in spirit, as there might be different parts
* of the engine retrieving info from it.
*/
public static ActivationStatus getTrue(Ability approvingObjectAbility, Game game) {
ApprovingObject approvingObject = approvingObjectAbility == null ? null : new ApprovingObject(approvingObjectAbility, game);
return new ActivationStatus(true, approvingObject);
public Set<ApprovingObject> getApprovingObjects() {
return approvingObjects;
}
private static final ActivationStatus falseInstance = new ActivationStatus(Collections.emptySet());
public static ActivationStatus getFalse() {
return falseInstance;
}
public static ActivationStatus withoutApprovingObject(boolean forcedCanActivate) {
return new ActivationStatus(forcedCanActivate, Collections.emptySet());
}
}

View file

@ -17,6 +17,7 @@ import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.Set;
import java.util.UUID;
/**
@ -179,15 +180,16 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
// timing check
//20091005 - 602.5d/602.5e
boolean asInstant;
ApprovingObject approvingObject = game.getContinuousEffects()
Set<ApprovingObject> approvingObjects = game
.getContinuousEffects()
.asThough(sourceId,
AsThoughEffectType.ACTIVATE_AS_INSTANT,
this,
controllerId,
game);
asInstant = approvingObject != null;
asInstant |= (timing == TimingRule.INSTANT);
game
);
boolean asInstant = !approvingObjects.isEmpty()
|| (timing == TimingRule.INSTANT);
if (!asInstant && !game.canPlaySorcery(playerId)) {
return ActivationStatus.getFalse();
}
@ -204,7 +206,13 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
// game.inCheckPlayableState() can't be a help here cause some cards checking activating status,
// activatorId must be removed
this.activatorId = playerId;
return new ActivationStatus(true, approvingObject);
if (approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true);
}
else {
return new ActivationStatus(approvingObjects);
}
}
@Override

View file

@ -1,11 +1,13 @@
package mage.abilities;
import mage.ApprovingObject;
import mage.cards.Card;
import mage.constants.AbilityType;
import mage.constants.AsThoughEffectType;
import mage.constants.Zone;
import mage.game.Game;
import java.util.Set;
import java.util.UUID;
/**
@ -33,15 +35,35 @@ public class PlayLandAbility extends ActivatedAbilityImpl {
// no super.canActivate() call
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (!controlsAbility(playerId, game) && null == approvingObject) {
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (!controlsAbility(playerId, game) && approvingObjects.isEmpty()) {
return ActivationStatus.getFalse();
}
//20091005 - 114.2a
return new ActivationStatus(game.isActivePlayer(playerId)
&& game.getPlayer(playerId).canPlayLand()
&& game.canPlaySorcery(playerId),
approvingObject);
if(!game.isActivePlayer(playerId)
|| !game.getPlayer(playerId).canPlayLand()
|| !game.canPlaySorcery(playerId)) {
return ActivationStatus.getFalse();
}
// TODO: this check may not be required, but removing it require more investigation.
// As of now it is only a way for One with the Multiverse to work.
if (!approvingObjects.isEmpty()) {
Card card = game.getCard(sourceId);
Zone zone = game.getState().getZone(sourceId);
if(card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) {
// Regular casting, to be an alternative to the AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE from hand (e.g. One with the Multiverse):
approvingObjects.add(new ApprovingObject(this, game));
}
}
if(approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true);
}
else {
return new ActivationStatus(approvingObjects);
}
}
@Override

View file

@ -1,6 +1,7 @@
package mage.abilities;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.abilities.costs.Cost;
import mage.abilities.costs.VariableCost;
@ -45,6 +46,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
this.spellAbilityType = spellAbilityType;
this.spellAbilityCastMode = spellAbilityCastMode;
this.addManaCost(cost);
this.setIdentifier(MageIdentifier.Default);
setSpellName();
}
@ -97,7 +99,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
}
}
return null != game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game) // check this first to allow Offering in main phase
return !game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game).isEmpty() // check this first to allow Offering in main phase
|| timing == TimingRule.INSTANT
|| object.isInstant(game)
|| object.hasAbility(FlashAbility.getInstance(), game)
@ -116,13 +118,13 @@ public class SpellAbility extends ActivatedAbilityImpl {
}
// play from not own hand
ApprovingObject approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (approvingObject == null && getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) {
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
if (approvingObjects.isEmpty() && getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) {
// allowed to cast adventures from non-hand?
approvingObject = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
approvingObjects = game.getContinuousEffects().asThough(getSourceId(), AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, this, playerId, game);
}
if (approvingObject == null) {
if (approvingObjects.isEmpty()) {
Card card = game.getCard(sourceId);
if (!(card != null && card.isOwnedBy(playerId))) {
return ActivationStatus.getFalse();
@ -141,12 +143,26 @@ public class SpellAbility extends ActivatedAbilityImpl {
}
}
// TODO: this check may not be required, but removing it require more investigation.
// As of now it is only a way for One with the Multiverse to work.
if (!approvingObjects.isEmpty()) {
Card card = game.getCard(sourceId);
Zone zone = game.getState().getZone(sourceId);
if(card != null && card.isOwnedBy(playerId) && Zone.HAND.match(zone)) {
// Regular casting, to be an alternative to the AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE from hand (e.g. One with the Multiverse):
approvingObjects.add(new ApprovingObject(this, game));
}
}
// no mana restrict
// Alternate spell abilities (Flashback, Overload) can't be cast with no mana to pay option
if (getSpellAbilityType() == SpellAbilityType.BASE_ALTERNATE) {
Player player = game.getPlayer(playerId);
if (player != null
&& player.getCastSourceIdWithAlternateMana().contains(getSourceId())) {
&& player.getCastSourceIdWithAlternateMana()
.getOrDefault(getSourceId(), Collections.emptySet())
.contains(MageIdentifier.Default)
) {
return ActivationStatus.getFalse();
}
}
@ -159,13 +175,20 @@ public class SpellAbility extends ActivatedAbilityImpl {
// fused can be called from hand only, so not permitting object allows or other zones checks
// see https://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/251926-snapcaster-mage-and-fuse
if (game.getState().getZone(splitCard.getId()) == Zone.HAND) {
return new ActivationStatus(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)
&& splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId), null);
return ActivationStatus.withoutApprovingObject(splitCard.getLeftHalfCard().getSpellAbility().canChooseTarget(game, playerId)
&& splitCard.getRightHalfCard().getSpellAbility().canChooseTarget(game, playerId));
}
}
return ActivationStatus.getFalse();
} else {
return new ActivationStatus(canChooseTarget(game, playerId), approvingObject);
if(canChooseTarget(game, playerId)) {
if(approvingObjects == null || approvingObjects.isEmpty()) {
return ActivationStatus.withoutApprovingObject(true);
}
else {
return new ActivationStatus(approvingObjects);
}
}
}
}
}

View file

@ -46,7 +46,7 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl {
private final FilterCard filter;
CastFromGraveyardOnceEffect(FilterCard filter, String text) {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit, true);
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
this.filter = filter;
this.staticText = text;
}

View file

@ -1,5 +1,6 @@
package mage.abilities.common;
import mage.ApprovingObject;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.effects.common.PassEffect;
import mage.constants.Zone;
@ -30,7 +31,7 @@ public class PassAbility extends ActivatedAbilityImpl {
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
return ActivationStatus.getTrue(this, game);
return new ActivationStatus(new ApprovingObject(this, game));
}
@Override

View file

@ -37,7 +37,7 @@ public class TapSourceCost extends CostImpl {
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) {
return !permanent.isTapped()
&& (permanent.canTap(game) || null != game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game));
&& (permanent.canTap(game) || !game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game).isEmpty());
}
return false;
}

View file

@ -36,7 +36,7 @@ public class UntapSourceCost extends CostImpl {
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null) {
return permanent.isTapped() && (permanent.canTap(game) || null != game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game));
return permanent.isTapped() && (permanent.canTap(game) || game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.ACTIVATE_HASTE, ability, controllerId, game).isEmpty());
}
return false;
}

View file

@ -41,6 +41,4 @@ public interface AsThoughEffect extends ContinuousEffect {
@Override
AsThoughEffect copy();
boolean isConsumable();
}

View file

@ -1,5 +1,6 @@
package mage.abilities.effects;
import mage.MageIdentifier;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbility;
import mage.cards.Card;
@ -19,23 +20,16 @@ import mage.cards.AdventureCard;
public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements AsThoughEffect {
protected AsThoughEffectType type;
boolean consumable;
public AsThoughEffectImpl(AsThoughEffectType type, Duration duration, Outcome outcome) {
this(type, duration, outcome, false);
}
public AsThoughEffectImpl(AsThoughEffectType type, Duration duration, Outcome outcome, boolean consumable) {
super(duration, outcome);
this.type = type;
this.effectType = EffectType.ASTHOUGH;
this.consumable = consumable;
}
protected AsThoughEffectImpl(final AsThoughEffectImpl effect) {
super(effect);
this.type = effect.type;
this.consumable = effect.consumable;
}
@Override
@ -84,6 +78,10 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
* @return
*/
protected boolean allowCardToPlayWithoutMana(UUID objectId, Ability source, UUID affectedControllerId, Game game) {
return allowCardToPlayWithoutMana(objectId, source, affectedControllerId, MageIdentifier.Default, game);
}
protected boolean allowCardToPlayWithoutMana(UUID objectId, Ability source, UUID affectedControllerId, MageIdentifier identifier, Game game){
Player player = game.getPlayer(affectedControllerId);
Card card = game.getCard(objectId);
if (card == null || player == null) {
@ -92,33 +90,27 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
if (!card.isLand(game)) {
if (card instanceof SplitCard) {
Card leftCard = ((SplitCard) card).getLeftHalfCard();
player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts(), identifier);
Card rightCard = ((SplitCard) card).getRightHalfCard();
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier);
} else if (card instanceof ModalDoubleFacedCard) {
Card leftCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
// some MDFC's are land. IE: sea gate restoration
if (!leftCard.isLand(game)) {
player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(leftCard.getId(), null, leftCard.getSpellAbility().getCosts(), identifier);
}
if (!rightCard.isLand(game)) {
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier);
}
} else if (card instanceof AdventureCard) {
Card creatureCard = card.getMainCard();
Card spellCard = ((AdventureCard) card).getSpellCard();
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts(), identifier);
player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts(), identifier);
}
player.setCastSourceIdWithAlternateMana(objectId, null, card.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(objectId, null, card.getSpellAbility().getCosts(), identifier);
}
return true;
}
@Override
public boolean isConsumable() {
return consumable;
}
}

View file

@ -1,6 +1,7 @@
package mage.abilities.effects;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.MageSingleton;
@ -506,9 +507,10 @@ public class ContinuousEffects implements Serializable {
* @param affectedAbility null if check full object or ability if check only one ability from that object
* @param controllerId
* @param game
* @return sourceId of the permitting effect if any exists otherwise returns null
* @return Set of all the ApprovingObject related to that asThough.
*/
public ApprovingObject asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) {
public Set<ApprovingObject> asThough(UUID objectId, AsThoughEffectType type, Ability affectedAbility, UUID controllerId, Game game) {
Set<ApprovingObject> possibleApprovingObjects = new HashSet<>();
// usage check: effect must apply for specific ability only, not to full object (example: PLAY_FROM_NOT_OWN_HAND_ZONE)
if (type.needAffectedAbility() && affectedAbility == null) {
@ -553,18 +555,13 @@ public class ContinuousEffects implements Serializable {
idToCheck = objectId;
}
Set<ApprovingObject> possibleApprovingObjects = new HashSet<>();
for (AsThoughEffect effect : asThoughEffectsList) {
Set<Ability> abilities = asThoughEffectsMap.get(type).getAbility(effect.getId());
for (Ability ability : abilities) {
if (affectedAbility == null) {
// applies to full object (one effect can be used in multiple abilities)
if (effect.applies(idToCheck, ability, controllerId, game)) {
if (effect.isConsumable() && !game.inCheckPlayableState()) {
possibleApprovingObjects.add(new ApprovingObject(ability, game));
} else {
return new ApprovingObject(ability, game);
}
possibleApprovingObjects.add(new ApprovingObject(ability, game));
}
} else {
// applies to one affected ability
@ -575,46 +572,13 @@ public class ContinuousEffects implements Serializable {
}
if (effect.applies(idToCheck, affectedAbility, ability, game, controllerId)) {
if (effect.isConsumable() && !game.inCheckPlayableState()) {
possibleApprovingObjects.add(new ApprovingObject(ability, game));
} else {
return new ApprovingObject(ability, game);
}
possibleApprovingObjects.add(new ApprovingObject(ability, game));
}
}
}
}
if (possibleApprovingObjects.size() == 1) {
return possibleApprovingObjects.iterator().next();
} else if (possibleApprovingObjects.size() > 1) {
// Select the ability that you use to permit the action
Map<String, String> keyChoices = new HashMap<>();
for (ApprovingObject approvingObject : possibleApprovingObjects) {
MageObject mageObject = game.getObject(approvingObject.getApprovingAbility().getSourceId());
String choiceKey = approvingObject.getApprovingAbility().getId().toString();
String choiceValue;
if (mageObject == null) {
choiceValue = approvingObject.getApprovingAbility().getRule();
} else {
choiceValue = mageObject.getIdName() + ": " + approvingObject.getApprovingAbility().getRule(mageObject.getName());
}
keyChoices.put(choiceKey, choiceValue);
}
Choice choicePermitting = new ChoiceImpl(true);
choicePermitting.setMessage("Choose the permitting object");
choicePermitting.setKeyChoices(keyChoices);
Player player = game.getPlayer(controllerId);
player.choose(Outcome.Detriment, choicePermitting, game);
for (ApprovingObject approvingObject : possibleApprovingObjects) {
if (approvingObject.getApprovingAbility().getId().toString().equals(choicePermitting.getChoiceKey())) {
return approvingObject;
}
}
}
}
return null;
return possibleApprovingObjects;
}
/**

View file

@ -1,18 +1,14 @@
package mage.abilities.effects.common.asthought;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.cards.Card;
import mage.constants.AsThoughEffectType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.constants.*;
import mage.filter.FilterCard;
import mage.game.Game;
import java.util.UUID;
/**
* @author LevelX2
*/
@ -65,7 +61,7 @@ public class PlayFromNotOwnHandZoneAllEffect extends AsThoughEffectImpl {
}
break;
}
return !onlyOwnedCards || card.getOwnerId().equals(source.getControllerId())
return (!onlyOwnedCards || card.getOwnerId().equals(source.getControllerId()))
&& filter.match(card, game)
&& game.getState().getZone(card.getId()).match(fromZone);
}

View file

@ -46,7 +46,7 @@ public class EchoEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId());
if (controller != null
&& source.getSourceObjectIfItStillExists(game) != null) {
if (game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.PAY_0_ECHO, source, source.getControllerId(), game) != null) {
if (!game.getContinuousEffects().asThough(source.getSourceId(), AsThoughEffectType.PAY_0_ECHO, source, source.getControllerId(), game).isEmpty()) {
Cost altCost = new GenericManaCost(0);
if (controller.chooseUse(Outcome.Benefit, "Pay {0} instead of the echo cost?", source, game)) {
altCost.clearPaid();

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.Mana;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost;
@ -55,7 +56,7 @@ public class EmergeAbility extends SpellAbility {
new FilterControlledCreaturePermanent(), this.getControllerId(), this, game)) {
ManaCost costToPay = CardUtil.reduceCost(emergeCost.copy(), creature.getManaValue());
if (costToPay.canPay(this, this, this.getControllerId(), game)) {
return ActivationStatus.getTrue(this, game);
return new ActivationStatus(new ApprovingObject(this, game));
}
}
}

View file

@ -68,7 +68,7 @@ class FlyingEffect extends RestrictionEffect implements MageSingleton {
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
return blocker.getAbilities().containsKey(FlyingAbility.getInstance().getId())
|| blocker.getAbilities().containsKey(ReachAbility.getInstance().getId())
|| (null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, null, blocker.getControllerId(), game)
|| (!game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_DRAGON, null, blocker.getControllerId(), game).isEmpty()
&& attacker.hasSubtype(SubType.DRAGON, game));
}

View file

@ -65,18 +65,18 @@ class LandwalkEffect extends RestrictionEffect {
@Override
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
if (game.getBattlefield().contains(filter, blocker.getControllerId(), source, game, 1)
&& null == game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, null, blocker.getControllerId(), game)) {
&& game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_LANDWALK, null, blocker.getControllerId(), game).isEmpty()) {
switch (filter.getMessage()) {
case "plains":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, null, blocker.getControllerId(), game);
return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_PLAINSWALK, null, blocker.getControllerId(), game).isEmpty();
case "island":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, null, blocker.getControllerId(), game);
return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_ISLANDWALK, null, blocker.getControllerId(), game).isEmpty();
case "swamp":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, null, blocker.getControllerId(), game);
return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SWAMPWALK, null, blocker.getControllerId(), game).isEmpty();
case "mountain":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, null, blocker.getControllerId(), game);
return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_MOUNTAINWALK, null, blocker.getControllerId(), game).isEmpty();
case "forest":
return null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, null, blocker.getControllerId(), game);
return !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_FORESTWALK, null, blocker.getControllerId(), game).isEmpty();
default:
return false;
}

View file

@ -70,7 +70,7 @@ class ShadowEffect extends RestrictionEffect implements MageSingleton {
@Override
public boolean canBeBlocked(Permanent attacker, Permanent blocker, Ability source, Game game, boolean canUseChooseDialogs) {
return blocker.getAbilities().containsKey(ShadowAbility.getInstance().getId())
|| null != game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, null, blocker.getControllerId(), game);
|| !game.getContinuousEffects().asThough(blocker.getId(), AsThoughEffectType.BLOCK_SHADOW, null, blocker.getControllerId(), game).isEmpty();
}
@Override

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.dynamicvalue.common.OpponentsLostLifeCount;
@ -48,7 +49,7 @@ public class SpectacleAbility extends SpellAbility {
public ActivationStatus canActivate(UUID playerId, Game game) {
if (OpponentsLostLifeCount.instance.calculate(game, playerId) > 0
&& super.canActivate(playerId, game).canActivate()) {
return ActivationStatus.getTrue(this, game);
return new ActivationStatus(new ApprovingObject(this, game));
}
return ActivationStatus.getFalse();
}

View file

@ -1,5 +1,6 @@
package mage.abilities.keyword;
import mage.ApprovingObject;
import mage.abilities.SpellAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.cards.Card;
@ -54,7 +55,7 @@ public class SurgeAbility extends SpellAbility {
if (!player.hasOpponent(playerToCheckId, game)) {
if (watcher.getAmountOfSpellsPlayerCastOnCurrentTurn(playerToCheckId) > 0
&& super.canActivate(playerId, game).canActivate()) {
return ActivationStatus.getTrue(this, game);
return new ActivationStatus(new ApprovingObject(this, game));
}
}
}

View file

@ -34,7 +34,7 @@ class BlockTappedPredicate implements Predicate<Permanent> {
@Override
public boolean apply(Permanent input, Game game) {
return !input.isTapped() || null != game.getState().getContinuousEffects().asThough(input.getId(), AsThoughEffectType.BLOCK_TAPPED, null, input.getControllerId(), game);
return !input.isTapped() || !game.getState().getContinuousEffects().asThough(input.getId(), AsThoughEffectType.BLOCK_TAPPED, null, input.getControllerId(), game).isEmpty();
}
@Override

View file

@ -174,8 +174,8 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
Player player = game.getPlayer(defenderAssignsCombatDamage(game) ? defendingPlayerId : attacker.getControllerId());
if ((attacker.getAbilities().containsKey(DamageAsThoughNotBlockedAbility.getInstance().getId()) &&
player.chooseUse(Outcome.Damage, "Have " + attacker.getLogName() + " assign damage as though it weren't blocked?", null, game)) ||
game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED,
null, attacker.getControllerId(), game) != null) {
!game.getContinuousEffects().asThough(attacker.getId(), AsThoughEffectType.DAMAGE_NOT_BLOCKED,
null, attacker.getControllerId(), game).isEmpty()) {
// for handling creatures like Thorn Elemental
blocked = false;
unblockedDamage(first, game);

View file

@ -716,7 +716,7 @@ public class GameEvent implements Serializable {
if (approvingObject == null) {
return false;
}
if (identifier == null) {
if (identifier.equals(MageIdentifier.Default)) {
return false;
}
return identifier.equals(approvingObject.getApprovingAbility().getIdentifier());

View file

@ -1234,13 +1234,13 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
public boolean canBeTargetedBy(MageObject source, UUID sourceControllerId, Game game) {
if (source != null) {
if (abilities.containsKey(ShroudAbility.getInstance().getId())) {
if (null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game)) {
if (game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game).isEmpty()) {
return false;
}
}
if (game.getPlayer(this.getControllerId()).hasOpponent(sourceControllerId, game)
&& null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game)
&& game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game).isEmpty()
&& abilities.stream()
.filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast)
@ -1416,10 +1416,10 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
// battles can never attack
return false;
}
ApprovingObject approvingObject = game.getContinuousEffects().asThough(
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(
this.objectId, AsThoughEffectType.ATTACK_AS_HASTE, null, defenderId, game
);
if (hasSummoningSickness() && approvingObject == null) {
if (hasSummoningSickness() && approvingObjects.isEmpty()) {
return false;
}
//20101001 - 508.1c
@ -1435,7 +1435,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
}
return !abilities.containsKey(DefenderAbility.getInstance().getId())
|| null != game.getContinuousEffects().asThough(this.objectId, AsThoughEffectType.ATTACK, null, this.getControllerId(), game);
|| !game.getContinuousEffects().asThough(this.objectId, AsThoughEffectType.ATTACK, null, this.getControllerId(), game).isEmpty();
}
private boolean canAttackCheckRestrictionEffects(UUID defenderId, Game game) {
@ -1455,7 +1455,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
public boolean canBlock(UUID attackerId, Game game) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game) == null || isBattle(game)) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty() || isBattle(game)) {
return false;
}
Permanent attacker = game.getPermanent(attackerId);
@ -1488,7 +1488,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
public boolean canBlockAny(Game game) {
if (tapped && null == game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game)) {
if (tapped && game.getState().getContinuousEffects().asThough(this.getId(), AsThoughEffectType.BLOCK_TAPPED, null, this.getControllerId(), game).isEmpty()) {
return false;
}

View file

@ -1,9 +1,6 @@
package mage.players;
import mage.ApprovingObject;
import mage.MageItem;
import mage.MageObject;
import mage.Mana;
import mage.*;
import mage.abilities.*;
import mage.abilities.costs.AlternativeSourceCosts;
import mage.abilities.costs.Cost;
@ -1054,13 +1051,28 @@ public interface Player extends MageItem, Copyable<Player> {
* cost
* @param costs alternate other costs you need to pay
*/
void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs);
default void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs) {
setCastSourceIdWithAlternateMana(sourceId, manaCosts, costs, MageIdentifier.Default);
}
Set<UUID> getCastSourceIdWithAlternateMana();
/**
* If the next spell cast has the set sourceId, the spell will be cast
* without mana (null) or the mana set to manaCosts instead of its normal
* mana costs.
*
* @param sourceId the source that can be cast without mana
* @param manaCosts alternate ManaCost, null if it can be cast without mana
* cost
* @param costs alternate other costs you need to pay
* @param identifier if not using the MageIdentifier.Default, only apply the alternate mana when ApprovingSource if of that kind.
*/
void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs, MageIdentifier identifier);
Map<UUID, ManaCosts<ManaCost>> getCastSourceIdManaCosts();
Map<UUID, Set<MageIdentifier>> getCastSourceIdWithAlternateMana();
Map<UUID, Costs<Cost>> getCastSourceIdCosts();
Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> getCastSourceIdManaCosts();
Map<UUID, Map<MageIdentifier, Costs<Cost>>> getCastSourceIdCosts();
void clearCastSourceIdManaCosts();

View file

@ -163,9 +163,12 @@ public abstract class PlayerImpl implements Player, Serializable {
// indicates that the spell with the set sourceId can be cast with an alternate mana costs (can also be no mana costs)
// support multiple cards with alternative mana cost
protected Set<UUID> castSourceIdWithAlternateMana = new HashSet<>();
protected Map<UUID, ManaCosts<ManaCost>> castSourceIdManaCosts = new HashMap<>();
protected Map<UUID, Costs<Cost>> castSourceIdCosts = new HashMap<>();
//
// A card may be able to cast multiple way with multiple methods.
// The specific MageIdentifier should be checked, before checking null as a fallback.
protected Map<UUID, Set<MageIdentifier>> castSourceIdWithAlternateMana = new HashMap<>();
protected Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> castSourceIdManaCosts = new HashMap<>();
protected Map<UUID, Map<MageIdentifier, Costs<Cost>>> castSourceIdCosts = new HashMap<>();
// indicates that the player is in mana payment phase
protected boolean payManaMode = false;
@ -279,13 +282,22 @@ public abstract class PlayerImpl implements Player, Serializable {
this.bufferTimeLeft = player.getBufferTimeLeft();
this.reachedNextTurnAfterLeaving = player.reachedNextTurnAfterLeaving;
this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana());
for (Entry<UUID, ManaCosts<ManaCost>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
for (Entry<UUID, Set<MageIdentifier>> entry : player.getCastSourceIdWithAlternateMana().entrySet()) {
this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue())));
}
for (Entry<UUID, Costs<Cost>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
for (Entry<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>());
for(Entry<MageIdentifier, ManaCosts<ManaCost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
for (Entry<UUID, Map<MageIdentifier, Costs<Cost>>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), new HashMap<>());
for(Entry<MageIdentifier, Costs<Cost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
this.payManaMode = player.payManaMode;
this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null;
for (Designation object : player.designations) {
@ -364,13 +376,20 @@ public abstract class PlayerImpl implements Player, Serializable {
this.reachedNextTurnAfterLeaving = player.hasReachedNextTurnAfterLeaving();
this.clearCastSourceIdManaCosts();
this.castSourceIdWithAlternateMana.clear();
this.castSourceIdWithAlternateMana.addAll(player.getCastSourceIdWithAlternateMana());
for (Entry<UUID, ManaCosts<ManaCost>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
for (Entry<UUID, Set<MageIdentifier>> entry : player.getCastSourceIdWithAlternateMana().entrySet()) {
this.castSourceIdWithAlternateMana.put(entry.getKey(), (entry.getValue() == null ? null : new HashSet<>(entry.getValue())));
}
for (Entry<UUID, Costs<Cost>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), (entry.getValue() == null ? null : entry.getValue().copy()));
for (Entry<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> entry : player.getCastSourceIdManaCosts().entrySet()) {
this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>());
for(Entry<MageIdentifier, ManaCosts<ManaCost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
for (Entry<UUID, Map<MageIdentifier, Costs<Cost>>> entry : player.getCastSourceIdCosts().entrySet()) {
this.castSourceIdCosts.put(entry.getKey(), new HashMap<>());
for(Entry<MageIdentifier, Costs<Cost>> subEntry : entry.getValue().entrySet()) {
this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy());
}
}
this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null;
@ -636,13 +655,13 @@ public abstract class PlayerImpl implements Player, Serializable {
}
if (source != null) {
if (abilities.containsKey(ShroudAbility.getInstance().getId())
&& null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game)) {
&& game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.SHROUD, null, sourceControllerId, game).isEmpty()) {
return false;
}
if (sourceControllerId != null
&& this.hasOpponent(sourceControllerId, game)
&& null == game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game)
&& game.getContinuousEffects().asThough(this.getId(), AsThoughEffectType.HEXPROOF, null, sourceControllerId, game).isEmpty()
&& abilities.stream()
.filter(HexproofBaseAbility.class::isInstance)
.map(HexproofBaseAbility.class::cast)
@ -1080,25 +1099,37 @@ public abstract class PlayerImpl implements Player, Serializable {
}
@Override
public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs) {
public void setCastSourceIdWithAlternateMana(UUID sourceId, ManaCosts<ManaCost> manaCosts, Costs<Cost> costs, MageIdentifier identifier) {
// cost must be copied for data consistence between game simulations
castSourceIdWithAlternateMana.add(sourceId);
castSourceIdManaCosts.put(sourceId, manaCosts != null ? manaCosts.copy() : null);
castSourceIdCosts.put(sourceId, costs != null ? costs.copy() : null);
castSourceIdWithAlternateMana
.computeIfAbsent(sourceId, k -> new HashSet<>())
.add(identifier);
castSourceIdManaCosts
.computeIfAbsent(sourceId, k -> new HashMap<>())
.put(identifier, manaCosts != null ? manaCosts.copy() : null);
castSourceIdCosts
.computeIfAbsent(sourceId, k -> new HashMap<>())
.put(identifier, costs != null ? costs.copy() : null);
if (identifier == null) {
boolean a = true;
}
}
@Override
public Set<UUID> getCastSourceIdWithAlternateMana() {
public Map<UUID, Set<MageIdentifier>> getCastSourceIdWithAlternateMana() {
return castSourceIdWithAlternateMana;
}
@Override
public Map<UUID, Costs<Cost>> getCastSourceIdCosts() {
public Map<UUID, Map<MageIdentifier, Costs<Cost>>> getCastSourceIdCosts() {
return castSourceIdCosts;
}
@Override
public Map<UUID, ManaCosts<ManaCost>> getCastSourceIdManaCosts() {
public Map<UUID, Map<MageIdentifier, ManaCosts<ManaCost>>> getCastSourceIdManaCosts() {
return castSourceIdManaCosts;
}
@ -1187,10 +1218,19 @@ public abstract class PlayerImpl implements Player, Serializable {
// ALTERNATIVE COST from dynamic effects
// some effects set sourceId to cast without paying mana costs or other costs
if (getCastSourceIdWithAlternateMana().contains(ability.getSourceId())) {
MageIdentifier identifier = approvingObject == null
? MageIdentifier.Default
: approvingObject.getApprovingAbility().getIdentifier();
if (!getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) {
// identifier has no alternate cast entry for that sourceId, using Default instead.
identifier = MageIdentifier.Default;
}
if (getCastSourceIdWithAlternateMana().getOrDefault(ability.getSourceId(), Collections.emptySet()).contains(identifier)) {
Ability spellAbility = spell.getSpellAbility();
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId());
Costs<Cost> costs = getCastSourceIdCosts().get(ability.getSourceId());
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(ability.getSourceId()).get(identifier);
Costs<Cost> costs = getCastSourceIdCosts().get(ability.getSourceId()).get(identifier);
if (alternateCosts == null) {
noMana = true;
} else {
@ -1273,21 +1313,30 @@ public abstract class PlayerImpl implements Player, Serializable {
}
}
ApprovingObjectResult approvingResult = chooseApprovingObject(
game,
activationStatus.getApprovingObjects().stream().collect(Collectors.toList()),
false
);
if (approvingResult.status.equals(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE)) {
return false; // canceled choice of approving object.
}
//20091005 - 305.1
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PLAY_LAND,
card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject()))) {
card.getId(), playLandAbility, playerId, approvingResult.approvingObject))) {
// int bookmark = game.bookmarkState();
// land events must return original zone (uses for commander watcher)
Zone cardZoneBefore = game.getState().getZone(card.getId());
GameEvent landEventBefore = GameEvent.getEvent(GameEvent.EventType.PLAY_LAND,
card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject());
card.getId(), playLandAbility, playerId, approvingResult.approvingObject);
landEventBefore.setZone(cardZoneBefore);
game.fireEvent(landEventBefore);
if (moveCards(card, Zone.BATTLEFIELD, playLandAbility, game, false, false, false, null)) {
incrementLandsPlayed();
GameEvent landEventAfter = GameEvent.getEvent(GameEvent.EventType.LAND_PLAYED,
card.getId(), playLandAbility, playerId, activationStatus.getApprovingObject());
card.getId(), playLandAbility, playerId, approvingResult.approvingObject);
landEventAfter.setZone(cardZoneBefore);
game.fireEvent(landEventAfter);
@ -1311,6 +1360,68 @@ public abstract class PlayerImpl implements Player, Serializable {
return true;
}
private enum ApprovingObjectResultStatus {
CHOSEN,
NO_POSSIBLE_CHOICE,
NOT_REQUIRED_NO_CHOICE,
}
private class ApprovingObjectResult {
public final ApprovingObjectResultStatus status;
public final ApprovingObject approvingObject; // not null iff status is CHOSEN
private ApprovingObjectResult(ApprovingObjectResultStatus status, ApprovingObject approvingObject) {
this.status = status;
this.approvingObject = approvingObject;
}
}
private ApprovingObjectResult chooseApprovingObject(Game game, List<ApprovingObject> possibleApprovingObjects, boolean required) {
// Choosing
if (possibleApprovingObjects.isEmpty()) {
return new ApprovingObjectResult(ApprovingObjectResultStatus.NO_POSSIBLE_CHOICE, null);
} else {
// Select the ability that you use to permit the action
Map<String, String> keyChoices = new HashMap<>();
int i = 0;
for (ApprovingObject possibleApprovingObject : possibleApprovingObjects) {
MageObject mageObject = game.getObject(possibleApprovingObject.getApprovingAbility().getSourceId());
String choiceValue = "";
MageIdentifier identifier = possibleApprovingObject.getApprovingAbility().getIdentifier();
if (!identifier.getAdditionalText().isEmpty()) {
choiceValue += identifier.getAdditionalText() + ": ";
}
if (mageObject == null) {
choiceValue += possibleApprovingObject.getApprovingAbility().getRule();
} else {
choiceValue += mageObject.getIdName() + ": ";
String moreDetails = possibleApprovingObject.getApprovingAbility().getRule(mageObject.getName());
choiceValue += moreDetails.isEmpty() ? "Cast normally" : moreDetails;
}
keyChoices.put((i++) + "", choiceValue);
}
int choice = 0;
if (!game.inCheckPlayableState() && keyChoices.size() > 1) {
Choice choicePermitting = new ChoiceImpl(required);
choicePermitting.setMessage("Choose the permitting object");
choicePermitting.setKeyChoices(keyChoices);
if (canRespond()) {
if (choose(Outcome.Neutral, choicePermitting, game)) {
String choiceKey = choicePermitting.getChoiceKey();
if (choiceKey != null) {
choice = Integer.parseInt(choiceKey);
}
} else {
return new ApprovingObjectResult(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE, null);
}
}
}
return new ApprovingObjectResult(ApprovingObjectResultStatus.CHOSEN, possibleApprovingObjects.get(choice));
}
}
protected boolean playManaAbility(ActivatedManaAbilityImpl ability, Game game) {
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.ACTIVATE_ABILITY,
ability.getId(), ability, playerId))) {
@ -1463,7 +1574,16 @@ public abstract class PlayerImpl implements Player, Serializable {
result = playManaAbility((ActivatedManaAbilityImpl) ability.copy(), game);
break;
case SPELL:
result = cast((SpellAbility) ability, game, false, activationStatus.getApprovingObject());
ApprovingObjectResult approvingResult = chooseApprovingObject(
game,
activationStatus.getApprovingObjects().stream().collect(Collectors.toList()),
false
);
if (approvingResult.status.equals(ApprovingObjectResultStatus.NOT_REQUIRED_NO_CHOICE)) {
return false; // chosen to not approve any AsThough.
}
result = cast((SpellAbility) ability, game, false, approvingResult.approvingObject);
break;
default:
result = playAbility(ability.copy(), game);
@ -3452,9 +3572,9 @@ public abstract class PlayerImpl implements Player, Serializable {
}
// ALTERNATIVE COST FROM dynamic effects
if (getCastSourceIdWithAlternateMana().contains(copy.getSourceId())) {
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId());
Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId());
for(MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) {
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier);
Costs<Cost> costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier);
boolean canPutToPlay = true;
if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) {
@ -3499,9 +3619,7 @@ public abstract class PlayerImpl implements Player, Serializable {
}
// Get the ability, if any, which allows for spending many as if it were another color.
// TODO: This needs to be improved to handle multiple approving objects.
// See https://github.com/magefree/mage/issues/8584
ApprovingObject approvingObject = game.getContinuousEffects().asThough(ability.getSourceId(),
Set<ApprovingObject> approvingObjects = game.getContinuousEffects().asThough(ability.getSourceId(),
AsThoughEffectType.SPEND_OTHER_MANA, ability, ability.getControllerId(), game);
for (Mana mana : abilityOptions) {
if (mana.count() == 0) {
@ -3517,7 +3635,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// TODO: Describe this
// Abilities that let us spend mana as if it were any (or other colors/types) must be handled separately
// and can't be incorporated into calculating availableMana since the number of combinations would explode.
if (approvingObject != null && mana.count() <= avail.count()) {
if (!approvingObjects.isEmpty() && mana.count() <= avail.count()) {
// TODO: I think this is wrong for spell that require colorless
return true;
}
@ -3764,7 +3882,7 @@ public abstract class PlayerImpl implements Player, Serializable {
// So make it available all the time
boolean canUse;
if (ability instanceof MorphAbility && object instanceof Card && (game.canPlaySorcery(getId())
|| (null != game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game)))) {
|| (!game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.CAST_AS_INSTANT, playAbility, this.getId(), game).isEmpty()))) {
canUse = canPlayCardByAlternateCost((Card) object, availableMana, playAbility, game);
} else {
canUse = canPlay(playAbility, availableMana, object, game); // canPlay already checks alternative source costs and all conditions
@ -3843,23 +3961,23 @@ public abstract class PlayerImpl implements Player, Serializable {
continue;
}
ApprovingObject approvingObject;
Set<ApprovingObject> approvingObjects;
if ((isPlaySpell || isPlayLand) && (fromZone != Zone.BATTLEFIELD)) {
// play hand from non hand zone (except battlefield - you can't play already played permanents)
approvingObject = game.getContinuousEffects().asThough(object.getId(),
approvingObjects = game.getContinuousEffects().asThough(object.getId(),
AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game);
if (approvingObject == null && isPlaySpell
if (approvingObjects.isEmpty() && isPlaySpell
&& ((SpellAbility) ability).getSpellAbilityType().equals(SpellAbilityType.ADVENTURE_SPELL)) {
approvingObject = game.getContinuousEffects().asThough(object.getId(),
approvingObjects = game.getContinuousEffects().asThough(object.getId(),
AsThoughEffectType.CAST_ADVENTURE_FROM_NOT_OWN_HAND_ZONE, ability, this.getId(), game);
}
} else {
// other abilities from direct zones
approvingObject = null;
approvingObjects = new HashSet<>();
}
boolean canActivateAsHandZone = approvingObject != null
boolean canActivateAsHandZone = !approvingObjects.isEmpty()
|| (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard());
boolean possibleToPlay = canActivateAsHandZone
&& ability.getZone().match(Zone.HAND)
@ -4434,8 +4552,8 @@ public abstract class PlayerImpl implements Player, Serializable {
@Override
public boolean lookAtFaceDownCard(Card card, Game game, int abilitiesToActivate) {
if (null != game.getContinuousEffects().asThough(card.getId(),
AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game)) {
if (!game.getContinuousEffects().asThough(card.getId(),
AsThoughEffectType.LOOK_AT_FACE_DOWN, null, this.getId(), game).isEmpty()) {
// two modes: look at the card or do not look and activate other abilities
String lookMessage = "Look at " + card.getIdName();
String lookYes = "Yes, look at the card";

View file

@ -2,6 +2,7 @@ package mage.util;
import com.google.common.collect.ImmutableList;
import mage.ApprovingObject;
import mage.MageIdentifier;
import mage.MageObject;
import mage.Mana;
import mage.abilities.*;
@ -147,7 +148,7 @@ public final class CardUtil {
ability.addManaCostsToPay(adjustedCost);
}
private static ManaCosts<ManaCost> adjustCost(ManaCosts<ManaCost> manaCosts, int reduceCount) {
public static ManaCosts<ManaCost> adjustCost(ManaCosts<ManaCost> manaCosts, int reduceCount) {
ManaCosts<ManaCost> newCost = new ManaCostsImpl<>();
// nothing to change
@ -1447,8 +1448,8 @@ public final class CardUtil {
Costs<Cost> additionalCostsLeft = leftHalfCard.getSpellAbility().getCosts();
Costs<Cost> additionalCostsRight = rightHalfCard.getSpellAbility().getCosts();
// set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsLeft);
player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsRight);
player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsLeft, MageIdentifier.Default);
player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsRight, MageIdentifier.Default);
}
// allow the card to be cast
game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), Boolean.TRUE);
@ -1465,13 +1466,13 @@ public final class CardUtil {
// get additional cost if any
Costs<Cost> additionalCostsMDFCLeft = leftHalfCard.getSpellAbility().getCosts();
// set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsMDFCLeft);
player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsMDFCLeft, MageIdentifier.Default);
}
if (!rightHalfCard.isLand(game)) {
// get additional cost if any
Costs<Cost> additionalCostsMDFCRight = rightHalfCard.getSpellAbility().getCosts();
// set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsMDFCRight);
player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsMDFCRight, MageIdentifier.Default);
}
}
// allow the card to be cast
@ -1488,8 +1489,8 @@ public final class CardUtil {
Costs<Cost> additionalCostsCreature = creatureCard.getSpellAbility().getCosts();
Costs<Cost> additionalCostsSpellCard = spellCard.getSpellAbility().getCosts();
// set alternative cost and any additional cost
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), manaCost, additionalCostsCreature);
player.setCastSourceIdWithAlternateMana(spellCard.getId(), manaCost, additionalCostsSpellCard);
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), manaCost, additionalCostsCreature, MageIdentifier.Default);
player.setCastSourceIdWithAlternateMana(spellCard.getId(), manaCost, additionalCostsSpellCard, MageIdentifier.Default);
}
// allow the card to be cast
game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), Boolean.TRUE);
@ -1500,7 +1501,7 @@ public final class CardUtil {
if (manaCost != null) {
// get additional cost if any
Costs<Cost> additionalCostsNormalCard = card.getSpellAbility().getCosts();
player.setCastSourceIdWithAlternateMana(card.getMainCard().getId(), manaCost, additionalCostsNormalCard);
player.setCastSourceIdWithAlternateMana(card.getMainCard().getId(), manaCost, additionalCostsNormalCard, MageIdentifier.Default);
}
// cast it