Merge branch 'magefree:master' into case-of-the-pilfered-proof

This commit is contained in:
Matthew Wilson 2024-04-07 14:31:44 +03:00 committed by GitHub
commit 9cf6119c7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
797 changed files with 36049 additions and 3584 deletions

View file

@ -20,12 +20,9 @@ public enum MageIdentifier {
// "Once each turn, you may cast an instant or sorcery spell from the top of your library."
//
CastFromGraveyardOnceWatcher,
CemeteryIlluminatorWatcher,
GisaAndGeralfWatcher,
DanithaNewBenaliasLightWatcher,
OnceEachTurnCastWatcher,
HaukensInsightWatcher,
IntrepidPaleontologistWatcher,
KaradorGhostChieftainWatcher,
KessDissidentMageWatcher,
MuldrothaTheGravetideWatcher,
ShareTheSpoilsWatcher,
@ -33,8 +30,6 @@ public enum MageIdentifier {
GlimpseTheCosmosWatcher,
SerraParagonWatcher,
OneWithTheMultiverseWatcher("Without paying manacost"),
JohannApprenticeSorcererWatcher,
AssembleThePlayersWatcher,
KaghaShadowArchdruidWatcher,
CourtOfLocthwainWatcher("Without paying manacost"),
LaraCroftTombRaiderWatcher,

View file

@ -167,11 +167,19 @@ public interface MageObject extends MageItem, Serializable, Copyable<MageObject>
void setZoneChangeCounter(int value, Game game);
default boolean isHistoric(Game game) {
return getCardType(game).contains(CardType.ARTIFACT)
|| getSuperType(game).contains(SuperType.LEGENDARY)
return isArtifact(game)
|| isLegendary(game)
|| hasSubtype(SubType.SAGA, game);
}
default boolean isOutlaw(Game game) {
return hasSubtype(SubType.ASSASSIN, game)
|| hasSubtype(SubType.MERCENARY, game)
|| hasSubtype(SubType.PIRATE, game)
|| hasSubtype(SubType.ROGUE, game)
|| hasSubtype(SubType.WARLOCK, game);
}
default boolean isCreature() {
return isCreature(null);
}

View file

@ -57,10 +57,12 @@ public class MageObjectReference implements Comparable<MageObjectReference>, Ser
this.zoneChangeCounter = -1;
}
@Deprecated // cause of many bugs, see issue #10479
public MageObjectReference(Ability source) {
this(source, 0);
}
@Deprecated // cause of many bugs, see issue #10479
public MageObjectReference(Ability source, int modifier) {
this.sourceId = source.getSourceId();
this.zoneChangeCounter = source.getSourceObjectZoneChangeCounter() + modifier;

View file

@ -514,6 +514,14 @@ public interface Ability extends Controllable, Serializable {
*/
Ability withFirstModeFlavorWord(String flavorWord);
/**
* Sets cost word for first mode
*
* @param cost
* @return
*/
Ability withFirstModeCost(Cost cost);
/**
* Creates the message about the ability casting/triggering/activating to
* post in the game log before the ability resolves.

View file

@ -52,37 +52,37 @@ public abstract class AbilityImpl implements Ability {
private static final List<Ability> emptyAbilities = new ArrayList<>();
protected UUID id;
protected UUID originalId; // TODO: delete originalId???
private UUID originalId; // TODO: delete originalId???
protected AbilityType abilityType;
protected UUID controllerId;
protected UUID sourceId;
private final ManaCosts<ManaCost> manaCosts;
private final ManaCosts<ManaCost> manaCostsToPay;
private final Costs<Cost> costs;
protected Modes modes; // access to it by GetModes only (it can be overridden by some abilities)
private final Modes modes; // access to it by GetModes only (it can be overridden by some abilities)
protected Zone zone;
protected String name;
protected AbilityWord abilityWord;
protected String flavorWord;
protected boolean usesStack = true;
protected boolean ruleAtTheTop = false;
protected boolean ruleVisible = true;
protected boolean ruleAdditionalCostsVisible = true;
private boolean ruleAtTheTop = false;
private boolean ruleVisible = true;
private boolean ruleAdditionalCostsVisible = true;
protected boolean activated = false;
protected boolean worksFaceDown = false;
protected boolean worksPhasedOut = false;
protected int sourceObjectZoneChangeCounter;
protected List<Watcher> watchers = new ArrayList<>(); // access to it by GetWatchers only (it can be overridden by some abilities)
protected List<Ability> subAbilities = null;
protected boolean canFizzle = true;
protected TargetAdjuster targetAdjuster = null;
protected CostAdjuster costAdjuster = null;
protected List<Hint> hints = new ArrayList<>();
private boolean worksFaceDown = false;
private boolean worksPhasedOut = false;
private int sourceObjectZoneChangeCounter;
private List<Watcher> watchers = new ArrayList<>(); // access to it by GetWatchers only (it can be overridden by some abilities)
private List<Ability> subAbilities = null;
private boolean canFizzle = true; // for Gilded Drake
private TargetAdjuster targetAdjuster = null;
private CostAdjuster costAdjuster = null;
private List<Hint> hints = new ArrayList<>();
protected List<CardIcon> icons = new ArrayList<>();
protected Outcome customOutcome = null; // uses for AI decisions instead effects
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;
private Outcome customOutcome = null; // uses for AI decisions instead effects
private MageIdentifier identifier = MageIdentifier.Default; // used to identify specific ability (e.g. to match with corresponding watcher)
private String appendToRule = null;
private int sourcePermanentTransformCount = 0;
private Map<String, Object> costsTagMap = null;
protected AbilityImpl(AbilityType abilityType, Zone zone) {
@ -311,6 +311,16 @@ public abstract class AbilityImpl implements Ability {
return false;
}
// apply mode costs if they have them
for (UUID modeId : this.getModes().getSelectedModes()) {
Cost cost = this.getModes().get(modeId).getCost();
if (cost instanceof ManaCost) {
this.addManaCostsToPay((ManaCost) cost.copy());
} else if (cost != null) {
this.costs.add(cost.copy());
}
}
// unit tests only: it allows to add targets/choices by two ways:
// 1. From cast/activate command params (process it here)
// 2. From single addTarget/setChoice, it's a preffered method for tests (process it in normal choose dialogs like human player)
@ -429,6 +439,7 @@ public abstract class AbilityImpl implements Ability {
case BESTOW:
case MORPH:
case DISGUISE:
case PLOT:
// from Snapcaster Mage:
// If you cast a spell from a graveyard using its flashback ability, you can't pay other alternative costs
// (such as that of Foil). (2018-12-07)
@ -521,7 +532,7 @@ public abstract class AbilityImpl implements Ability {
String message = controller.getLogName() + " announces a value of " + xValue + " (" + variableCost.getActionText() + ')'
+ CardUtil.getSourceLogName(game, this);
announceString.append(message);
setCostsTag("X",xValue);
setCostsTag("X", xValue);
}
}
return announceString.toString();
@ -626,7 +637,7 @@ public abstract class AbilityImpl implements Ability {
}
addManaCostsToPay(new ManaCostsImpl<>(manaString.toString()));
getManaCostsToPay().setX(xValue * xValueMultiplier, amountMana);
setCostsTag("X",xValue * xValueMultiplier);
setCostsTag("X", xValue * xValueMultiplier);
}
variableManaCost.setPaid();
}
@ -718,8 +729,9 @@ public abstract class AbilityImpl implements Ability {
public Map<String, Object> getCostsTagMap() {
return costsTagMap;
}
public void setCostsTag(String tag, Object value){
if (costsTagMap == null){
public void setCostsTag(String tag, Object value) {
if (costsTagMap == null) {
costsTagMap = new HashMap<>();
}
costsTagMap.put(tag, value);
@ -1139,6 +1151,12 @@ public abstract class AbilityImpl implements Ability {
return this;
}
@Override
public Ability withFirstModeCost(Cost cost) {
this.modes.getMode().withCost(cost);
return this;
}
@Override
public String getGameLogMessage(Game game) {
if (game.isSimulation()) {

View file

@ -238,11 +238,11 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
protected ActivationInfo getActivationInfo(Game game) {
Integer turnNum = (Integer) game.getState()
.getValue(CardUtil.getCardZoneString("activationsTurn" + originalId, sourceId, game));
.getValue(CardUtil.getCardZoneString("activationsTurn" + getOriginalId(), sourceId, game));
Integer activationCount = (Integer) game.getState()
.getValue(CardUtil.getCardZoneString("activationsCount" + originalId, sourceId, game));
.getValue(CardUtil.getCardZoneString("activationsCount" + getOriginalId(), sourceId, game));
Integer totalActivations = (Integer) game.getState()
.getValue(CardUtil.getCardZoneString("totalActivations" + originalId, sourceId, game));
.getValue(CardUtil.getCardZoneString("totalActivations" + getOriginalId(), sourceId, game));
if (turnNum == null || activationCount == null || totalActivations == null) {
return null;
}
@ -251,11 +251,11 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa
protected void setActivationInfo(ActivationInfo activationInfo, Game game) {
game.getState().setValue(CardUtil
.getCardZoneString("activationsTurn" + originalId, sourceId, game), activationInfo.turnNum);
.getCardZoneString("activationsTurn" + getOriginalId(), sourceId, game), activationInfo.turnNum);
game.getState().setValue(CardUtil
.getCardZoneString("activationsCount" + originalId, sourceId, game), activationInfo.activationCounter);
.getCardZoneString("activationsCount" + getOriginalId(), sourceId, game), activationInfo.activationCounter);
game.getState().setValue(CardUtil
.getCardZoneString("totalActivations" + originalId, sourceId, game), activationInfo.totalActivations);
.getCardZoneString("totalActivations" + getOriginalId(), sourceId, game), activationInfo.totalActivations);
}
@Override

View file

@ -1,5 +1,6 @@
package mage.abilities;
import mage.abilities.costs.Cost;
import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects;
import mage.target.Target;
@ -17,6 +18,7 @@ public class Mode implements Serializable {
protected final Targets targets;
protected final Effects effects;
protected String flavorWord;
protected Cost cost = null;
/**
* Optional Tag to distinguish this mode from others.
* In the case of modes that players can only choose once,
@ -39,6 +41,7 @@ public class Mode implements Serializable {
this.effects = mode.effects.copy();
this.flavorWord = mode.flavorWord;
this.modeTag = mode.modeTag;
this.cost = mode.cost != null ? mode.cost.copy() : null;
}
public UUID setRandomId() {
@ -107,4 +110,13 @@ public class Mode implements Serializable {
this.flavorWord = flavorWord;
return this;
}
public Mode withCost(Cost cost) {
this.cost = cost;
return this;
}
public Cost getCost() {
return cost;
}
}

View file

@ -584,7 +584,14 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
sb.append("<br>");
for (Mode mode : this.values()) {
sb.append("&bull ");
if (mode.getCost() != null) {
// for Spree
sb.append("+ ");
sb.append(mode.getCost().getText());
sb.append(" &mdash; ");
} else {
sb.append("&bull ");
}
sb.append(mode.getEffects().getTextStartingUpperCase(mode));
sb.append("<br>");
}

View file

@ -32,7 +32,7 @@ public class SpecialActions extends AbilitiesImpl<SpecialAction> {
LinkedHashMap<UUID, SpecialAction> controlledBy = new LinkedHashMap<>();
for (SpecialAction action : this) {
if (action.isControlledBy(controllerId) && action.isManaAction() == manaAction) {
controlledBy.put(action.id, action);
controlledBy.put(action.getId(), action);
}
}
return controlledBy;

View file

@ -358,6 +358,6 @@ public class SpellAbility extends ActivatedAbilityImpl {
}
public void setId(UUID idToUse) {
this.id = idToUse;
this.id = idToUse; // TODO: research, why is it needed
}
}

View file

@ -75,7 +75,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
return;
}
game.getState().setValue(CardUtil.getCardZoneString(
"lastTurnTriggered" + originalId, sourceId, game
"lastTurnTriggered" + getOriginalId(), sourceId, game
), game.getTurnNum());
}
@ -101,7 +101,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
return true;
}
Integer lastTurnTriggered = (Integer) game.getState().getValue(
CardUtil.getCardZoneString("lastTurnTriggered" + originalId, sourceId, game)
CardUtil.getCardZoneString("lastTurnTriggered" + getOriginalId(), sourceId, game)
);
return lastTurnTriggered == null || lastTurnTriggered != game.getTurnNum();
}
@ -112,7 +112,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
return false;
}
Integer lastTurnUsed = (Integer) game.getState().getValue(
CardUtil.getCardZoneString("lastTurnUsed" + originalId, sourceId, game)
CardUtil.getCardZoneString("lastTurnUsed" + getOriginalId(), sourceId, game)
);
return lastTurnUsed != null && lastTurnUsed == game.getTurnNum();
}
@ -165,7 +165,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
}
if (doOnlyOnceEachTurn) {
game.getState().setValue(CardUtil.getCardZoneString(
"lastTurnUsed" + originalId, sourceId, game
"lastTurnUsed" + getOriginalId(), sourceId, game
), game.getTurnNum());
}
//20091005 - 603.4

View file

@ -0,0 +1,37 @@
package mage.abilities.common;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import java.util.Optional;
/**
* @author TheElk801
*/
public class AttacksWhileSaddledTriggeredAbility extends AttacksTriggeredAbility {
public AttacksWhileSaddledTriggeredAbility(Effect effect) {
super(effect);
this.setTriggerPhrase("Whenever {this} attacks while saddled, ");
}
private AttacksWhileSaddledTriggeredAbility(final AttacksWhileSaddledTriggeredAbility ability) {
super(ability);
}
@Override
public AttacksWhileSaddledTriggeredAbility copy() {
return new AttacksWhileSaddledTriggeredAbility(this);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return super.checkTrigger(event, game)
&& Optional
.ofNullable(getSourcePermanentIfItStillExists(game))
.map(Permanent::isSaddled)
.orElse(false);
}
}

View file

@ -0,0 +1,45 @@
package mage.abilities.common;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
/**
* @author Susucr
*/
public class BecomesPlottedSourceTriggeredAbility extends TriggeredAbilityImpl {
public BecomesPlottedSourceTriggeredAbility(Effect effect, boolean optional) {
super(Zone.EXILED, effect, optional);
setTriggerPhrase("When {this} becomes plotted, ");
replaceRuleText = true;
}
public BecomesPlottedSourceTriggeredAbility(Effect effect) {
this(effect, false);
}
protected BecomesPlottedSourceTriggeredAbility(final BecomesPlottedSourceTriggeredAbility ability) {
super(ability);
}
@Override
public BecomesPlottedSourceTriggeredAbility copy() {
return new BecomesPlottedSourceTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.BECOME_PLOTTED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getTargetId().equals(this.getSourceId())) {
return true;
}
return false;
}
}

View file

@ -40,7 +40,7 @@ public class BecomesTappedOneOrMoreTriggeredAbility extends TriggeredAbilityImpl
public boolean checkTrigger(GameEvent event, Game game) {
TappedBatchEvent batchEvent = (TappedBatchEvent) event;
return batchEvent
.getTargets()
.getTargetIds()
.stream()
.map(game::getPermanent)
.anyMatch(p -> filter.match(p, getControllerId(), this, game));

View file

@ -74,7 +74,7 @@ public class BecomesTargetAnyTriggeredAbility extends TriggeredAbilityImpl {
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
return false;
}
if (CardUtil.checkTargetedEventAlreadyUsed(this.id.toString(), targetingObject, event, game)) {
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
return false;
}
switch (setTargetPointer) {

View file

@ -58,7 +58,7 @@ public class BecomesTargetAttachedTriggeredAbility extends TriggeredAbilityImpl
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
return false;
}
if (CardUtil.checkTargetedEventAlreadyUsed(this.id.toString(), targetingObject, event, game)) {
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
return false;
}
switch (setTargetPointer) {

View file

@ -67,7 +67,7 @@ public class BecomesTargetControllerTriggeredAbility extends TriggeredAbilityImp
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
return false;
}
if (CardUtil.checkTargetedEventAlreadyUsed(this.id.toString(), targetingObject, event, game)) {
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
return false;
}
switch (setTargetPointer) {

View file

@ -61,7 +61,7 @@ public class BecomesTargetSourceTriggeredAbility extends TriggeredAbilityImpl {
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
return false;
}
if (CardUtil.checkTargetedEventAlreadyUsed(this.id.toString(), targetingObject, event, game)) {
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
return false;
}
switch (setTargetPointer) {

View file

@ -3,6 +3,7 @@ package mage.abilities.common;
import mage.MageIdentifier;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.cards.Card;
import mage.constants.*;
@ -10,6 +11,7 @@ import mage.filter.FilterCard;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.watchers.Watcher;
import java.util.HashSet;
@ -18,26 +20,26 @@ import java.util.UUID;
/**
* Once during each of your turns, you may cast... from your graveyard
*
* <p>
* See Lurrus of the Dream Den and Rivaz of the Claw
*
* @author weirddan455
*/
public class CastFromGraveyardOnceStaticAbility extends SimpleStaticAbility {
public class CastFromGraveyardOnceEachTurnAbility extends SimpleStaticAbility {
public CastFromGraveyardOnceStaticAbility(FilterCard filter, String text) {
super(new CastFromGraveyardOnceEffect(filter, text));
public CastFromGraveyardOnceEachTurnAbility(FilterCard filter) {
super(new CastFromGraveyardOnceEffect(filter));
this.addWatcher(new CastFromGraveyardOnceWatcher());
this.setIdentifier(MageIdentifier.CastFromGraveyardOnceWatcher);
}
private CastFromGraveyardOnceStaticAbility(final CastFromGraveyardOnceStaticAbility ability) {
private CastFromGraveyardOnceEachTurnAbility(final CastFromGraveyardOnceEachTurnAbility ability) {
super(ability);
}
@Override
public CastFromGraveyardOnceStaticAbility copy() {
return new CastFromGraveyardOnceStaticAbility(this);
public CastFromGraveyardOnceEachTurnAbility copy() {
return new CastFromGraveyardOnceEachTurnAbility(this);
}
}
@ -45,10 +47,11 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl {
private final FilterCard filter;
CastFromGraveyardOnceEffect(FilterCard filter, String text) {
CastFromGraveyardOnceEffect(FilterCard filter) {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
this.filter = filter;
this.staticText = text;
this.staticText = "Once during each of your turns, you may cast " + filter.getMessage()
+ (filter.getMessage().contains("from your graveyard") ? "" : " from your graveyard");
}
private CastFromGraveyardOnceEffect(final CastFromGraveyardOnceEffect effect) {
@ -68,19 +71,30 @@ class CastFromGraveyardOnceEffect extends AsThoughEffectImpl {
@Override
public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) {
if (source.isControlledBy(affectedControllerId)
&& Zone.GRAVEYARD.equals(game.getState().getZone(objectId))
&& game.isActivePlayer(affectedControllerId)) {
Card card = game.getCard(objectId);
Permanent sourceObject = source.getSourcePermanentIfItStillExists(game);
if (card != null && sourceObject != null
&& card.isOwnedBy(affectedControllerId)
&& card.getSpellAbility() != null
&& card.getSpellAbility().spellCanBeActivatedRegularlyNow(affectedControllerId, game)
&& filter.match(card, affectedControllerId, source, game)) {
CastFromGraveyardOnceWatcher watcher = game.getState().getWatcher(CastFromGraveyardOnceWatcher.class);
return watcher != null && watcher.abilityNotUsed(new MageObjectReference(sourceObject, game));
throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility");
}
@Override
public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) {
Player controller = game.getPlayer(source.getControllerId());
Permanent sourcePermanent = source.getSourcePermanentIfItStillExists(game);
CastFromGraveyardOnceWatcher watcher = game.getState().getWatcher(CastFromGraveyardOnceWatcher.class);
if (controller == null || sourcePermanent == null || watcher == null) {
return false;
}
if (game.isActivePlayer(playerId) // only during your turn
&& source.isControlledBy(playerId) // only you may cast
&& Zone.GRAVEYARD.equals(game.getState().getZone(objectId)) // from graveyard
&& affectedAbility instanceof SpellAbility // characteristics to check
&& watcher.abilityNotUsed(new MageObjectReference(sourcePermanent, game)) // once per turn
) {
SpellAbility spellAbility = (SpellAbility) affectedAbility;
Card cardToCheck = spellAbility.getCharacteristics(game);
if (spellAbility.getManaCosts().isEmpty()) {
return false;
}
return spellAbility.spellCanBeActivatedRegularlyNow(playerId, game)
&& filter.match(cardToCheck, playerId, source, game);
}
return false;
}

View file

@ -0,0 +1,122 @@
package mage.abilities.common;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.Ownerable;
import mage.game.events.GameEvent;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
/**
* @author TheElk801
*/
public class CommittedCrimeTriggeredAbility extends TriggeredAbilityImpl {
public CommittedCrimeTriggeredAbility(Effect effect) {
this(effect, false);
}
public CommittedCrimeTriggeredAbility(Effect effect, boolean optional) {
this(Zone.BATTLEFIELD, effect, optional);
}
public CommittedCrimeTriggeredAbility(Zone zone, Effect effect, boolean optional) {
super(zone, effect, optional);
setTriggerPhrase("Whenever you commit a crime, ");
}
protected CommittedCrimeTriggeredAbility(final CommittedCrimeTriggeredAbility ability) {
super(ability);
}
@Override
public CommittedCrimeTriggeredAbility copy() {
return new CommittedCrimeTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
switch (event.getType()) {
case SPELL_CAST:
case ACTIVATED_ABILITY:
case TRIGGERED_ABILITY:
return true;
default:
return false;
}
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return isControlledBy(getCriminal(event, game));
}
public static UUID getCriminal(GameEvent event, Game game) {
UUID controllerId;
Ability ability;
switch (event.getType()) {
case SPELL_CAST:
Spell spell = game.getSpell(event.getTargetId());
if (spell == null) {
return null;
}
controllerId = spell.getControllerId();
ability = spell.getSpellAbility();
break;
case ACTIVATED_ABILITY:
case TRIGGERED_ABILITY:
StackObject stackObject = game.getStack().getStackObject(event.getTargetId());
if (stackObject == null) {
return null;
}
controllerId = stackObject.getControllerId();
ability = stackObject.getStackAbility();
break;
default:
return null;
}
if (controllerId == null || ability == null) {
return null;
}
Set<UUID> opponents = game.getOpponents(controllerId);
Set<UUID> targets = CardUtil.getAllSelectedTargets(ability, game);
// an opponent
if (targets
.stream()
.anyMatch(opponents::contains)
// an opponent's permanent
|| targets
.stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.map(Controllable::getControllerId)
.anyMatch(opponents::contains)
// an opponent's spell or ability
|| targets
.stream()
.map(game.getStack()::getStackObject)
.filter(Objects::nonNull)
.map(Controllable::getControllerId)
.anyMatch(opponents::contains)
// a card in an opponent's graveyard
|| targets
.stream()
.filter(uuid -> Zone.GRAVEYARD.match(game.getState().getZone(uuid)))
.map(game::getCard)
.filter(Objects::nonNull)
.map(Ownerable::getOwnerId)
.anyMatch(opponents::contains)) {
return controllerId;
}
return null;
}
}

View file

@ -4,7 +4,7 @@ import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedBatchEvent;
import mage.game.events.DamagedBatchAllEvent;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
@ -14,21 +14,17 @@ import mage.game.permanent.Permanent;
*/
public class DealsCombatDamageEquippedTriggeredAbility extends TriggeredAbilityImpl {
private boolean usedThisStep;
public DealsCombatDamageEquippedTriggeredAbility(Effect effect) {
this(effect, false);
}
public DealsCombatDamageEquippedTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
this.usedThisStep = false;
setTriggerPhrase("Whenever equipped creature deals combat damage, ");
}
protected DealsCombatDamageEquippedTriggeredAbility(final DealsCombatDamageEquippedTriggeredAbility ability) {
super(ability);
this.usedThisStep = ability.usedThisStep;
}
@Override
@ -38,23 +34,16 @@ public class DealsCombatDamageEquippedTriggeredAbility extends TriggeredAbilityI
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event instanceof DamagedBatchEvent || event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_PRE;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ALL;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_PRE) {
usedThisStep = false; // clear before damage
return false;
}
if (usedThisStep || !(event instanceof DamagedBatchEvent)) {
return false; // trigger only on DamagedEvent and if not yet triggered this step
}
Permanent sourcePermanent = getSourcePermanentOrLKI(game);
if (sourcePermanent == null || sourcePermanent.getAttachedTo() == null) {
return false;
}
int amount = ((DamagedBatchEvent) event)
int amount = ((DamagedBatchAllEvent) event)
.getEvents()
.stream()
.filter(DamagedEvent::isCombatDamage)
@ -64,11 +53,7 @@ public class DealsCombatDamageEquippedTriggeredAbility extends TriggeredAbilityI
if (amount < 1) {
return false;
}
usedThisStep = true;
this.getEffects().setValue("damage", amount);
// TODO: this value saved will not be correct if both permanent and player damaged by a single creature
// Need to rework engine logic to fire a single DamagedBatchEvent including both permanents and players
// Only Sword of Hours is currently affected.
return true;
}
}

View file

@ -4,7 +4,7 @@ import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedBatchEvent;
import mage.game.events.DamagedBatchAllEvent;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
@ -17,18 +17,14 @@ import mage.game.events.GameEvent;
*/
public class DealsCombatDamageTriggeredAbility extends TriggeredAbilityImpl {
private boolean usedThisStep;
public DealsCombatDamageTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
this.usedThisStep = false;
setTriggerPhrase(getWhen() + "{this} deals combat damage, ");
this.replaceRuleText = true;
}
protected DealsCombatDamageTriggeredAbility(final DealsCombatDamageTriggeredAbility ability) {
super(ability);
this.usedThisStep = ability.usedThisStep;
}
@Override
@ -38,33 +34,22 @@ public class DealsCombatDamageTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event instanceof DamagedBatchEvent || event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_PRE;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ALL;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_PRE) {
usedThisStep = false; // clear before damage
return false;
}
if (usedThisStep || !(event instanceof DamagedBatchEvent)) {
return false; // trigger only on DamagedEvent and if not yet triggered this step
}
int amount = ((DamagedBatchEvent) event)
int amount = ((DamagedBatchAllEvent) event)
.getEvents()
.stream()
.filter(DamagedEvent::isCombatDamage)
.filter(e -> e.getAttackerId().equals(this.sourceId))
.filter(e -> e.getAttackerId().equals(getSourceId()))
.mapToInt(GameEvent::getAmount)
.sum();
if (amount < 1) {
return false;
}
usedThisStep = true;
this.getEffects().setValue("damage", amount);
// TODO: this value saved will not be correct if both permanent and player damaged by a single creature
// Need to rework engine logic to fire a single DamagedBatchEvent including both permanents and players
// Only Aisha of Sparks and Smoke is currently affected.
return true;
}
}

View file

@ -42,7 +42,7 @@ public class DealtDamageAttachedTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_FOR_ONE_PERMANENT;
}
@Override

View file

@ -0,0 +1,62 @@
package mage.abilities.common;
import mage.MageObject;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.Zone;
import mage.filter.common.FilterCreaturePermanent;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeBatchEvent;
import mage.game.events.ZoneChangeEvent;
import java.util.Objects;
/**
* @author Susucr
*/
public class DiesOneOrMoreCreatureTriggeredAbility extends TriggeredAbilityImpl {
private final FilterCreaturePermanent filter;
public DiesOneOrMoreCreatureTriggeredAbility(Effect effect, FilterCreaturePermanent filter) {
super(Zone.BATTLEFIELD, effect, false);
this.filter = filter;
this.setTriggerPhrase("Whenever one or more " + filter.getMessage() + " die, ");
}
private DiesOneOrMoreCreatureTriggeredAbility(final DiesOneOrMoreCreatureTriggeredAbility ability) {
super(ability);
this.filter = ability.filter;
}
@Override
public DiesOneOrMoreCreatureTriggeredAbility copy() {
return new DiesOneOrMoreCreatureTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return ((ZoneChangeBatchEvent) event)
.getEvents()
.stream()
.filter(ZoneChangeEvent::isDiesEvent)
.map(ZoneChangeEvent::getTargetId)
.map(game::getPermanentOrLKIBattlefield)
.filter(Objects::nonNull)
.anyMatch(p -> filter.match(p, getControllerId(), this, game));
}
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
return ((ZoneChangeBatchEvent) event)
.getEvents()
.stream()
.allMatch(e -> TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, e, game));
}
}

View file

@ -36,7 +36,9 @@ public class DrawNthCardTriggeredAbility extends TriggeredAbilityImpl {
super(zone, effect, optional);
this.targetController = targetController;
this.cardNumber = cardNumber;
this.addHint(hint);
if (targetController == TargetController.YOU) {
this.addHint(hint);
}
setTriggerPhrase(generateTriggerPhrase());
}

View file

@ -0,0 +1,47 @@
package mage.abilities.common;
import mage.abilities.StaticAbility;
import mage.abilities.effects.Effect;
import mage.abilities.effects.EntersBattlefieldEffect;
import mage.abilities.effects.common.ChooseColorEffect;
import mage.abilities.effects.common.TapSourceEffect;
import mage.constants.Outcome;
import mage.constants.Zone;
/**
* @author Susucr
*/
public class EntersBattlefieldTappedAsItEntersChooseColorAbility extends StaticAbility {
public EntersBattlefieldTappedAsItEntersChooseColorAbility() {
super(Zone.ALL, new EntersBattlefieldEffect(new TapSourceEffect(true)));
this.addEffect(new ChooseColorEffect(Outcome.Benefit));
}
private EntersBattlefieldTappedAsItEntersChooseColorAbility(final EntersBattlefieldTappedAsItEntersChooseColorAbility ability) {
super(ability);
}
@Override
public EntersBattlefieldTappedAsItEntersChooseColorAbility copy() {
return new EntersBattlefieldTappedAsItEntersChooseColorAbility(this);
}
@Override
public void addEffect(Effect effect) {
if (!getEffects().isEmpty()) {
Effect entersEffect = this.getEffects().get(0);
if (entersEffect instanceof EntersBattlefieldEffect) {
((EntersBattlefieldEffect) entersEffect).addEffect(effect);
return;
}
}
super.addEffect(effect);
}
@Override
public String getRule() {
return "{this} enters the battlefield tapped. As it enters, choose a color.";
}
}

View file

@ -6,7 +6,7 @@ import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.events.DamagedBatchEvent;
import mage.game.events.DamagedBatchForOnePlayerEvent;
import mage.game.events.DamagedEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
@ -60,7 +60,7 @@ public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
DamagedBatchEvent dEvent = (DamagedBatchEvent) event;
DamagedBatchForOnePlayerEvent dEvent = (DamagedBatchForOnePlayerEvent) event;
if (onlyCombat && !dEvent.isCombatDamage()) {
return false;
}
@ -86,7 +86,7 @@ public class OneOrMoreDealDamageTriggeredAbility extends TriggeredAbilityImpl {
this.getAllEffects().setValue("damage", events.stream().mapToInt(DamagedEvent::getAmount).sum());
switch (setTargetPointer) {
case PLAYER:
this.getAllEffects().setTargetPointer(new FixedTarget(event.getPlayerId()));
this.getAllEffects().setTargetPointer(new FixedTarget(event.getTargetId()));
break;
case NONE:
break;

View file

@ -20,7 +20,7 @@ public class PayMoreToCastAsThoughtItHadFlashAbility extends SpellAbility {
super(card.getSpellAbility().getManaCosts().copy(), card.getName() + " as though it had flash", Zone.HAND, SpellAbilityType.BASE_ALTERNATE);
this.costsToAdd = costsToAdd;
this.timing = TimingRule.INSTANT;
this.ruleAtTheTop = true;
this.setRuleAtTheTop(true);
CardUtil.increaseCost(this, costsToAdd);
}

View file

@ -34,7 +34,7 @@ public class TapForManaAllTriggeredAbility extends TriggeredAbilityImpl {
setTriggerPhrase("Whenever " + filter.getMessage() + " for mana, ");
}
public TapForManaAllTriggeredAbility(TapForManaAllTriggeredAbility ability) {
private TapForManaAllTriggeredAbility(TapForManaAllTriggeredAbility ability) {
super(ability);
this.filter = ability.filter.copy();
this.setTargetPointer = ability.setTargetPointer;

View file

@ -28,7 +28,7 @@ public class TapForManaAllTriggeredManaAbility extends TriggeredManaAbility {
setTriggerPhrase("Whenever " + filter.getMessage() + " for mana, ");
}
public TapForManaAllTriggeredManaAbility(TapForManaAllTriggeredManaAbility ability) {
private TapForManaAllTriggeredManaAbility(TapForManaAllTriggeredManaAbility ability) {
super(ability);
this.filter = ability.filter.copy();
this.setTargetPointer = ability.setTargetPointer;

View file

@ -43,7 +43,7 @@ public class AtTheBeginOfNextCleanupDelayedTriggeredAbility extends DelayedTrigg
@Override
public String getRule() {
StringBuilder sb = new StringBuilder();
String text = modes.getText();
String text = getModes().getText();
if (!text.isEmpty()) {
sb.append(Character.toUpperCase(text.charAt(0)));
if (text.endsWith(".")) {

View file

@ -43,7 +43,7 @@ public class AtTheBeginOfNextUpkeepDelayedTriggeredAbility extends DelayedTrigge
@Override
public String getRule() {
StringBuilder sb = new StringBuilder();
String text = modes.getText();
String text = getModes().getText();
if (!text.isEmpty()) {
sb.append(Character.toUpperCase(text.charAt(0)));
if (text.endsWith(".")) {

View file

@ -0,0 +1,32 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.game.Game;
import mage.watchers.common.CommittedCrimeWatcher;
/**
* requires CommittedCrimeWatcher
*
* @author TheElk801
*/
public enum CommittedCrimeCondition implements Condition {
instance;
private static final Hint hint = new ConditionHint(instance, "You committed a crime this turn");
public static Hint getHint() {
return hint;
}
@Override
public boolean apply(Game game, Ability source) {
return CommittedCrimeWatcher.checkCriminality(game, source);
}
@Override
public String toString() {
return "you've committed a crime this turn";
}
}

View file

@ -0,0 +1,35 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.constants.Zone;
import mage.game.Game;
import mage.watchers.common.SpellsCastWatcher;
import java.util.Objects;
/**
* @author Susucr
*/
public enum HaventCastSpellFromHandThisTurnCondition implements Condition {
instance;
public static final Hint hint = new ConditionHint(instance, "No spell cast from hand this turn", null, "Have cast spell from hand this turn", null, true);
@Override
public boolean apply(Game game, Ability source) {
return game.getState()
.getWatcher(SpellsCastWatcher.class)
.getSpellsCastThisTurn(source.getControllerId())
.stream()
.filter(Objects::nonNull)
.noneMatch(spell -> Zone.HAND.equals(spell.getFromZone()));
}
@Override
public String toString() {
return "if you haven't cast a spell from your hand this turn";
}
}

View file

@ -0,0 +1,26 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
import mage.watchers.common.SpellsCastWatcher;
/**
* @author Susucr
*/
public enum HaventCastSpellThisTurnCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
return game.getState()
.getWatcher(SpellsCastWatcher.class)
.getSpellsCastThisTurn(source.getControllerId())
.isEmpty();
}
@Override
public String toString() {
return "if you haven't cast a spell this turn";
}
}

View file

@ -0,0 +1,23 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.util.Optional;
/**
* @author TheElk801
*/
public enum SaddledCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
return Optional
.ofNullable(source.getSourcePermanentIfItStillExists(game))
.map(Permanent::isSaddled)
.orElse(false);
}
}

View file

@ -0,0 +1,41 @@
package mage.abilities.dynamicvalue.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.constants.CommanderCardType;
import mage.constants.Zone;
import mage.game.Game;
/**
* @author PurpleCrowbar
*/
public enum CommanderGreatestManaValue implements DynamicValue {
instance;
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return game.getCommanderCardsFromAnyZones(
game.getPlayer(sourceAbility.getControllerId()), CommanderCardType.ANY, Zone.ALL)
.stream()
.mapToInt(MageObject::getManaValue)
.max()
.orElse(0);
}
@Override
public CommanderGreatestManaValue copy() {
return this;
}
@Override
public String toString() {
return "X";
}
@Override
public String getMessage() {
return "the greatest mana value among your commanders";
}
}

View file

@ -21,7 +21,7 @@ public class CastSourceTriggeredAbility extends TriggeredAbilityImpl {
public CastSourceTriggeredAbility(Effect effect, boolean optional) {
super(Zone.STACK, effect, optional);
this.ruleAtTheTop = true;
this.setRuleAtTheTop(true);
setTriggerPhrase("When you cast this spell, ");
}

View file

@ -1,52 +0,0 @@
package mage.abilities.effects.common;
import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
/**
* @author BetaSteward_at_googlemail.com
*/
public class CastTargetForFreeEffect extends OneShotEffect {
public CastTargetForFreeEffect() {
super(Outcome.Benefit);
}
protected CastTargetForFreeEffect(final CastTargetForFreeEffect effect) {
super(effect);
}
@Override
public CastTargetForFreeEffect copy() {
return new CastTargetForFreeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
Card target = (Card) game.getObject(source.getFirstTarget());
if (controller != null && target != null) {
game.getState().setValue("PlayFromNotOwnHandZone" + target.getId(), Boolean.TRUE);
boolean cardWasCast = controller.cast(controller.chooseAbilityForCast(target, game, true),
game, true, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + target.getId(), null);
return cardWasCast;
}
return false;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return "you may cast " + getTargetPointer().describeTargets(mode.getTargets(), "that card")
+ " without paying its mana cost";
}
}

View file

@ -24,7 +24,7 @@ public class DamageTargetEffect extends OneShotEffect {
protected DynamicValue amount;
protected boolean preventable;
protected String targetDescription;
protected boolean useOnlyTargetPointer;
protected boolean useOnlyTargetPointer; // why do we ignore targetPointer by default??
protected String sourceName = "{this}";
public DamageTargetEffect(int amount) {
@ -44,6 +44,10 @@ public class DamageTargetEffect extends OneShotEffect {
this(StaticValue.get(amount), preventable, targetDescription);
}
public DamageTargetEffect(int amount, boolean preventable, String targetDescription, boolean useOnlyTargetPointer) {
this(StaticValue.get(amount), preventable, targetDescription, useOnlyTargetPointer);
}
public DamageTargetEffect(int amount, boolean preventable, String targetDescription, String whoDealDamageName) {
this(StaticValue.get(amount), preventable, targetDescription);
this.sourceName = whoDealDamageName;

View file

@ -6,14 +6,12 @@ import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbil
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.cards.CardsImpl;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.target.targetpointer.FixedTargets;
import mage.util.CardUtil;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
@ -75,7 +73,10 @@ public class ExileReturnBattlefieldNextEndStepTargetEffect extends OneShotEffect
Effect effect = yourControl
? new ReturnToBattlefieldUnderYourControlTargetEffect(exiledOnly)
: new ReturnToBattlefieldUnderOwnerControlTargetEffect(false, exiledOnly);
effect.setTargetPointer(new FixedTargets(toExile, game));
effect.setTargetPointer(new FixedTargets(toExile
.stream()
.map(Card::getMainCard)
.collect(Collectors.toSet()), game));
game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect), source);
return true;
}

View file

@ -1,7 +1,9 @@
package mage.abilities.effects.common;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.continuous.GainSuspendEffect;
import mage.abilities.keyword.SuspendAbility;
import mage.cards.Card;
import mage.constants.Outcome;
@ -18,16 +20,23 @@ import java.util.UUID;
public class ExileSpellWithTimeCountersEffect extends OneShotEffect {
private final int counters;
private final boolean gainsSuspend;
public ExileSpellWithTimeCountersEffect(int counters) {
super(Outcome.Exile);
this.counters = counters;
this.staticText = "Exile {this} with " + CardUtil.numberToText(this.counters) + " time counters on it";
this (counters, false);
}
public ExileSpellWithTimeCountersEffect(int counters, boolean gainsSuspend) {
super(Outcome.Exile);
this.counters = counters;
this.gainsSuspend = gainsSuspend;
this.staticText = "exile {this} with " + CardUtil.numberToText(this.counters) + " time counters on it"
+ (gainsSuspend ? ". It gains suspend" : "");
}
private ExileSpellWithTimeCountersEffect(final ExileSpellWithTimeCountersEffect effect) {
super(effect);
this.counters = effect.counters;
this.gainsSuspend = effect.gainsSuspend;
}
@Override
@ -38,14 +47,17 @@ public class ExileSpellWithTimeCountersEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
Card card = game.getStack().getSpell(source.getId()).getCard();
Card card = game.getCard(source.getSourceId());
if (controller == null || card == null) {
return true;
return false;
}
UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game);
if (!card.isCopy() && controller.moveCardsToExile(card, source, game, true, exileId, "Suspended cards of " + controller.getName())) {
card.addCounters(CounterType.TIME.createInstance(3), source.getControllerId(), source, game);
game.informPlayers(controller.getLogName() + " exiles " + card.getLogName() + " with " + counters + " time counters on it");
if (gainsSuspend) {
game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source);
}
}
return true;
}

View file

@ -2,6 +2,7 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
@ -14,7 +15,6 @@ public class LoseHalfLifeTargetEffect extends OneShotEffect {
public LoseHalfLifeTargetEffect() {
super(Outcome.Damage);
staticText = "that player loses half their life, rounded up";
}
protected LoseHalfLifeTargetEffect(final LoseHalfLifeTargetEffect effect) {
@ -30,7 +30,7 @@ public class LoseHalfLifeTargetEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(getTargetPointer().getFirst(game, source));
if (player != null) {
Integer amount = (int) Math.ceil(player.getLife() / 2f);
int amount = (int) Math.ceil(player.getLife() / 2f);
if (amount > 0) {
player.loseLife(amount, game, source, false);
return true;
@ -38,4 +38,12 @@ public class LoseHalfLifeTargetEffect extends OneShotEffect {
}
return false;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return getTargetPointer().describeTargets(mode.getTargets(), "that player") + " loses half their life, rounded up";
}
}

View file

@ -0,0 +1,166 @@
package mage.abilities.effects.common;
import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.asthought.YouMaySpendManaAsAnyColorToCastTargetEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.cards.Card;
import mage.constants.CastManaAdjustment;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
/**
* @author xenohedron, Susucr
*/
public class MayCastTargetCardEffect extends OneShotEffect {
private final Duration duration;
private final CastManaAdjustment manaAdjustment;
private final boolean thenExile; // Should the spell be exiled by a replacement effect if cast and it resolves?
/**
* Allows to cast the target card immediately, for its manacost.
*/
public MayCastTargetCardEffect(boolean thenExile) {
this(CastManaAdjustment.NONE, thenExile);
}
/**
* Allows to cast the target card immediately, either for its cost or with a modifier (like for free, or mana as any type).
*/
public MayCastTargetCardEffect(CastManaAdjustment manaAdjustment) {
this(manaAdjustment, false);
}
/**
* Allows to cast the target card immediately, either for its cost or with a modifier (like for free, or mana as any type).
*/
public MayCastTargetCardEffect(CastManaAdjustment manaAdjustment, boolean thenExile) {
this(Duration.OneUse, manaAdjustment, thenExile);
}
/**
* Makes the target card playable for the specified duration as long as it remains in that zone.
*/
public MayCastTargetCardEffect(Duration duration, boolean thenExile) {
this(duration, CastManaAdjustment.NONE, thenExile);
}
protected MayCastTargetCardEffect(Duration duration, CastManaAdjustment manaAdjustment, boolean thenExile) {
super(Outcome.Benefit);
this.duration = duration;
this.manaAdjustment = manaAdjustment;
this.thenExile = thenExile;
// TODO: support the non-yet-supported combinations.
// for now the constructor chains won't allow those.
if (duration != Duration.OneUse && manaAdjustment != CastManaAdjustment.NONE) {
throw new IllegalStateException(
"Wrong code usage, not yet supported "
+ "duration={" + duration.name() + "}, "
+ "manaAdjustment={" + manaAdjustment.name() + "}"
);
}
}
protected MayCastTargetCardEffect(final MayCastTargetCardEffect effect) {
super(effect);
this.duration = effect.duration;
this.manaAdjustment = effect.manaAdjustment;
this.thenExile = effect.thenExile;
}
@Override
public MayCastTargetCardEffect copy() {
return new MayCastTargetCardEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Card card = game.getCard(getTargetPointer().getFirst(game, source));
if (card == null) {
return false;
}
FixedTarget fixedTarget = new FixedTarget(card, game);
if (duration == Duration.OneUse) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null || !controller.chooseUse(outcome, "Cast " + card.getLogName() + '?', source, game)) {
return false;
}
switch (manaAdjustment) {
case NONE:
case WITHOUT_PAYING_MANA_COST:
break;
case AS_THOUGH_ANY_MANA_COLOR:
case AS_THOUGH_ANY_MANA_TYPE:
// TODO: untangle why there is a confusion between the two.
ContinuousEffect effect =
new YouMaySpendManaAsAnyColorToCastTargetEffect(Duration.Custom, controller.getId(), null);
effect.setTargetPointer(fixedTarget.copy());
game.addEffect(effect, source);
break;
default:
throw new IllegalArgumentException("Wrong code usage, manaAdjustment is not yet supported: " + manaAdjustment);
}
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
boolean noMana = manaAdjustment == CastManaAdjustment.WITHOUT_PAYING_MANA_COST;
controller.cast(controller.chooseAbilityForCast(card, game, noMana),
game, noMana, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
} else {
// TODO: support (and add tests!) for the non-NONE manaAdjustment
CardUtil.makeCardPlayable(game, source, card, duration, false);
}
if (thenExile) {
ContinuousEffect effect = new ThatSpellGraveyardExileReplacementEffect(true);
effect.setTargetPointer(fixedTarget.copy());
game.addEffect(effect, source);
}
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
String text = "you may cast " + getTargetPointer().describeTargets(mode.getTargets(), "it");
if (duration == Duration.EndOfTurn) {
text += " this turn";
} else if (!duration.toString().isEmpty()) {
text += duration.toString();
}
switch (manaAdjustment) {
case NONE:
break;
case WITHOUT_PAYING_MANA_COST:
text += " without paying its mana cost";
break;
case AS_THOUGH_ANY_MANA_COLOR:
text += ", and mana of any color can be spent to cast that spell";
break;
case AS_THOUGH_ANY_MANA_TYPE:
text += ", and mana of any type can be spent to cast that spell";
break;
default:
throw new IllegalArgumentException("Wrong code usage, manaAdjustment is not yet supported: " + manaAdjustment);
}
text += ".";
if (thenExile) {
text += " " + ThatSpellGraveyardExileReplacementEffect.RULE_YOUR;
}
return text;
}
}

View file

@ -1,98 +0,0 @@
package mage.abilities.effects.common;
import mage.ApprovingObject;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.replacement.ThatSpellGraveyardExileReplacementEffect;
import mage.cards.Card;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil;
/**
* @author xenohedron
*/
public class MayCastTargetThenExileEffect extends OneShotEffect {
private final Duration duration;
private final boolean noMana;
/**
* Allows to cast the target card immediately, either for its cost or for free.
* If resulting spell would be put into graveyard, exiles it instead.
*/
public MayCastTargetThenExileEffect(boolean noMana) {
super(Outcome.Benefit);
this.duration = Duration.OneUse;
this.noMana = noMana;
}
/**
* Makes the target card playable for the specified duration as long as it remains in that zone.
* If resulting spell would be put into graveyard, exiles it instead.
*/
public MayCastTargetThenExileEffect(Duration duration) {
super(Outcome.Benefit);
this.duration = duration;
this.noMana = false;
}
protected MayCastTargetThenExileEffect(final MayCastTargetThenExileEffect effect) {
super(effect);
this.duration = effect.duration;
this.noMana = effect.noMana;
}
@Override
public MayCastTargetThenExileEffect copy() {
return new MayCastTargetThenExileEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Card card = game.getCard(getTargetPointer().getFirst(game, source));
if (card == null) {
return false;
}
FixedTarget fixedTarget = new FixedTarget(card, game);
if (duration == Duration.OneUse) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null || !controller.chooseUse(outcome, "Cast " + card.getLogName() + '?', source, game)) {
return false;
}
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
controller.cast(controller.chooseAbilityForCast(card, game, noMana),
game, noMana, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
} else {
CardUtil.makeCardPlayable(game, source, card, duration, false);
}
ContinuousEffect effect = new ThatSpellGraveyardExileReplacementEffect(true);
effect.setTargetPointer(fixedTarget);
game.addEffect(effect, source);
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
String text = "you may cast " + getTargetPointer().describeTargets(mode.getTargets(), "it");
if (duration == Duration.EndOfTurn) {
text += " this turn";
} else if (!duration.toString().isEmpty()) {
text += duration.toString();
}
if (noMana) {
text += " without paying its mana cost";
}
return text + ". " + ThatSpellGraveyardExileReplacementEffect.RULE_YOUR;
}
}

View file

@ -0,0 +1,48 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.keyword.PlotAbility;
import mage.cards.Card;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInHand;
public class MayExileCardFromHandPlottedEffect extends OneShotEffect {
private final FilterCard filter;
public MayExileCardFromHandPlottedEffect(FilterCard filter) {
super(Outcome.PutCardInPlay);
this.filter = filter;
this.staticText = "you may exile a " + filter.getMessage() + " from your hand. If you do, it becomes plotted";
}
private MayExileCardFromHandPlottedEffect(final MayExileCardFromHandPlottedEffect effect) {
super(effect);
this.filter = effect.filter;
}
@Override
public MayExileCardFromHandPlottedEffect copy() {
return new MayExileCardFromHandPlottedEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
TargetCardInHand target = new TargetCardInHand(0, 1, filter);
if (player.chooseTarget(outcome, target, source, game)) {
Card card = game.getCard(target.getFirstTarget());
if (card != null) {
PlotAbility.doExileAndPlotCard(card, game, source);
}
}
return true;
}
}

View file

@ -15,6 +15,10 @@ import mage.players.Player;
import mage.target.TargetCard;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author TheElk801
*/
@ -22,6 +26,7 @@ public class MillThenPutInHandEffect extends OneShotEffect {
private final int amount;
private final boolean optional;
private final int maxAmountReturned; // maximum number of cards returned. e.g. 2 for "up to two"
private final FilterCard filter;
private final Effect otherwiseEffect;
private String textFromAmong = "the milled cards"; // for text gen
@ -35,8 +40,8 @@ public class MillThenPutInHandEffect extends OneShotEffect {
}
/**
* @param amount number of cards to mill
* @param filter select a card matching this filter from among the milled cards to put in hand
* @param amount number of cards to mill
* @param filter select a card matching this filter from among the milled cards to put in hand
* @param optional whether the selection is optional (true) or mandatory (false)
*/
public MillThenPutInHandEffect(int amount, FilterCard filter, boolean optional) {
@ -44,8 +49,8 @@ public class MillThenPutInHandEffect extends OneShotEffect {
}
/**
* @param amount number of cards to mill
* @param filter optionally select a card matching this filter from among the milled cards to put in hand
* @param amount number of cards to mill
* @param filter optionally select a card matching this filter from among the milled cards to put in hand
* @param otherwiseEffect applied if no card put into hand
*/
public MillThenPutInHandEffect(int amount, FilterCard filter, Effect otherwiseEffect) {
@ -53,11 +58,16 @@ public class MillThenPutInHandEffect extends OneShotEffect {
this.textFromAmong = "the cards milled this way";
}
protected MillThenPutInHandEffect(int amount, FilterCard filter, Effect otherwiseEffect, boolean optional) {
public MillThenPutInHandEffect(int amount, FilterCard filter, Effect otherwiseEffect, boolean optional) {
this(amount, filter, otherwiseEffect, optional, 1);
}
public MillThenPutInHandEffect(int amount, FilterCard filter, Effect otherwiseEffect, boolean optional, int maxReturnedCard) {
super(Outcome.Benefit);
this.amount = amount;
this.filter = filter;
this.optional = optional;
this.maxAmountReturned = maxReturnedCard;
this.otherwiseEffect = otherwiseEffect;
}
@ -66,6 +76,7 @@ public class MillThenPutInHandEffect extends OneShotEffect {
this.amount = effect.amount;
this.optional = effect.optional;
this.filter = effect.filter;
this.maxAmountReturned = effect.maxAmountReturned;
this.otherwiseEffect = effect.otherwiseEffect;
this.textFromAmong = effect.textFromAmong;
}
@ -85,13 +96,18 @@ public class MillThenPutInHandEffect extends OneShotEffect {
if (cards.isEmpty()) {
return applyOtherwiseEffect(game, source);
}
TargetCard target = new TargetCard(optional ? 0 : 1, 1, Zone.ALL, filter);
TargetCard target = new TargetCard(optional ? 0 : maxAmountReturned, maxAmountReturned, Zone.ALL, filter);
player.choose(Outcome.DrawCard, cards, target, source, game);
Card card = game.getCard(target.getFirstTarget());
if (card == null) {
Set<Card> returned = target
.getTargets()
.stream()
.map(game::getCard)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (returned.isEmpty()) {
return applyOtherwiseEffect(game, source);
}
return player.moveCards(card, Zone.HAND, source, game);
return player.moveCards(returned, Zone.HAND, source, game);
}
private boolean applyOtherwiseEffect(Game game, Ability source) {
@ -115,12 +131,25 @@ public class MillThenPutInHandEffect extends OneShotEffect {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
String text = "mill " + CardUtil.numberToText(amount) + " cards. ";
text += optional ? "You may " : "Then ";
text += "put " + filter.getMessage() + " from among " + textFromAmong + " into your hand";
if (otherwiseEffect != null) {
text += ". If you don't, " + otherwiseEffect.getText(mode);
StringBuilder sb = new StringBuilder("mill ");
sb.append(CardUtil.numberToText(amount));
sb.append(" cards. ");
sb.append(optional ? "You may " : "Then ");
sb.append("put ");
if (maxAmountReturned > 1) {
sb.append(optional ? "up to " : "");
sb.append(CardUtil.numberToText(maxAmountReturned) + " ");
}
return text;
sb.append(filter.getMessage());
sb.append(" from among ");
sb.append(textFromAmong);
sb.append(" into your hand");
if (otherwiseEffect != null) {
sb.append(". If you ");
sb.append(optional ? "don't" : "can't");
sb.append(", ");
sb.append(otherwiseEffect.getText(mode));
}
return sb.toString();
}
}

View file

@ -18,8 +18,8 @@ import java.util.UUID;
*/
public class ReturnToBattlefieldUnderOwnerControlTargetEffect extends OneShotEffect {
private boolean tapped;
protected boolean returnFromExileZoneOnly;
private final boolean tapped;
private final boolean returnFromExileZoneOnly;
/**
* @param returnFromExileZoneOnly see https://github.com/magefree/mage/issues/5151
@ -27,14 +27,10 @@ public class ReturnToBattlefieldUnderOwnerControlTargetEffect extends OneShotEff
* return exiled card - true
*/
public ReturnToBattlefieldUnderOwnerControlTargetEffect(boolean tapped, boolean returnFromExileZoneOnly) {
this(tapped, returnFromExileZoneOnly, "that card");
}
public ReturnToBattlefieldUnderOwnerControlTargetEffect(boolean tapped, boolean returnFromExileZoneOnly, String description) {
super(Outcome.Benefit);
this.tapped = tapped;
this.returnFromExileZoneOnly = returnFromExileZoneOnly;
staticText = "return " + description + " to the battlefield " + (tapped ? "tapped " : "") + "under its owner's control";
staticText = "return that card to the battlefield " + (tapped ? "tapped " : "") + "under its owner's control";
}
protected ReturnToBattlefieldUnderOwnerControlTargetEffect(final ReturnToBattlefieldUnderOwnerControlTargetEffect effect) {

View file

@ -16,6 +16,7 @@ import java.util.UUID;
/**
* Spend mana as any color to cast targeted card. Will not affected after any card movements or blinks.
* Affects to all card's parts
* TODO: AnyType and AnyColor are confused there.
*
* @author JayDi85
*/

View file

@ -15,10 +15,14 @@ public class AddCardSubtypeAllEffect extends ContinuousEffectImpl {
private final FilterPermanent filter;
private final SubType addedSubtype;
/**
* Note: must set text manually
*/
public AddCardSubtypeAllEffect(FilterPermanent filter, SubType addedSubtype, DependencyType dependency) {
super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Benefit);
this.filter = filter;
this.addedSubtype = addedSubtype;
this.staticText = filter.getMessage() + " are " + addedSubtype.getPluralName() + " in addition to their other types";
addDependencyType(dependency);
}

View file

@ -7,6 +7,7 @@ import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.common.TurnFaceUpAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl;
import mage.abilities.costs.mana.ManaCostsImpl;
@ -83,7 +84,22 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
this(createCosts(cost), objectReference, duration, faceDownType);
}
public BecomesFaceDownCreatureEffect(Costs<Cost> turnFaceUpCosts, MageObjectReference objectReference, Duration duration, FaceDownType faceDownType) {
public BecomesFaceDownCreatureEffect(Costs<Cost> cost, MageObjectReference objectReference, Duration duration, FaceDownType faceDownType) {
this(createCosts(cost), objectReference, duration, faceDownType, null);
}
public BecomesFaceDownCreatureEffect(Cost cost, MageObjectReference objectReference, Duration duration, FaceDownType faceDownType, CostAdjuster costAdjuster) {
this(createCosts(cost), objectReference, duration, faceDownType, costAdjuster);
}
/**
* @param turnFaceUpCosts costs for the turn face up ability
* @param objectReference
* @param duration
* @param faceDownType type of face down (morph, disguise, manifest, etc...)
* @param costAdjuster optional costAdjuster for the turn face up ability
*/
public BecomesFaceDownCreatureEffect(Costs<Cost> turnFaceUpCosts, MageObjectReference objectReference, Duration duration, FaceDownType faceDownType, CostAdjuster costAdjuster) {
super(duration, Layer.CopyEffects_1, SubLayer.FaceDownEffects_1b, Outcome.BecomeCreature);
this.objectReference = objectReference;
this.zoneChangeCounter = Integer.MIN_VALUE;
@ -91,7 +107,10 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl {
// add additional face up and information abilities
if (turnFaceUpCosts != null) {
// face up for all
this.additionalAbilities.add(new TurnFaceUpAbility(turnFaceUpCosts, faceDownType == FaceDownType.MEGAMORPHED));
this.additionalAbilities.add(
new TurnFaceUpAbility(turnFaceUpCosts, faceDownType == FaceDownType.MEGAMORPHED)
.setCostAdjuster(costAdjuster)
);
switch (faceDownType) {
case MORPHED:

View file

@ -1,63 +1,32 @@
package mage.abilities.effects.common.continuous;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.cards.Card;
import mage.constants.*;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
/**
* @author TheElk801
*/
public class LookAtTopCardOfLibraryAnyTimeEffect extends ContinuousEffectImpl {
private final TargetController targetLibrary;
public LookAtTopCardOfLibraryAnyTimeEffect() {
this(TargetController.YOU, Duration.WhileOnBattlefield);
this(Duration.WhileOnBattlefield);
}
public LookAtTopCardOfLibraryAnyTimeEffect(TargetController targetLibrary, Duration duration) {
public LookAtTopCardOfLibraryAnyTimeEffect(Duration duration) {
super(duration, Layer.PlayerEffects, SubLayer.NA, Outcome.Benefit);
this.targetLibrary = targetLibrary;
String libInfo;
switch (this.targetLibrary) {
case YOU:
libInfo = "your library";
break;
case OPPONENT:
libInfo = "opponents libraries";
break;
case SOURCE_TARGETS:
libInfo = "target player's library";
break;
default:
throw new IllegalArgumentException("Unknown target library type: " + targetLibrary);
}
StringBuilder sb = new StringBuilder();
String durationString = duration.toString();
if (durationString != null && !durationString.isEmpty()) {
sb.append(durationString);
sb.append(", ");
}
sb.append("you may look at the top card of ");
sb.append(libInfo);
sb.append(" any time");
staticText = sb.toString();
staticText = (duration.toString().isEmpty() ? "" : duration.toString() + ", ") +
"you may look at the top card of your library any time";
}
protected LookAtTopCardOfLibraryAnyTimeEffect(final LookAtTopCardOfLibraryAnyTimeEffect effect) {
super(effect);
this.targetLibrary = effect.targetLibrary;
}
@Override
@ -72,43 +41,11 @@ public class LookAtTopCardOfLibraryAnyTimeEffect extends ContinuousEffectImpl {
if (!canLookAtNextTopLibraryCard(game)) {
return false;
}
MageObject obj = source.getSourceObject(game);
if (obj == null) {
Card topCard = controller.getLibrary().getFromTop(game);
if (topCard == null) {
return false;
}
Set<UUID> needPlayers = new HashSet<>();
switch (this.targetLibrary) {
case YOU: {
needPlayers.add(source.getControllerId());
break;
}
case OPPONENT: {
needPlayers.addAll(game.getOpponents(source.getControllerId()));
break;
}
case SOURCE_TARGETS: {
needPlayers.addAll(CardUtil.getAllSelectedTargets(source, game));
break;
}
}
Set<Card> needCards = new HashSet<>();
needPlayers.stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
.map(player -> player.getLibrary().getFromTop(game))
.filter(Objects::nonNull)
.forEach(needCards::add);
if (needCards.isEmpty()) {
return false;
}
// all fine, can show top card
needCards.forEach(topCard -> {
Player owner = game.getPlayer(topCard.getOwnerId());
controller.lookAtCards(String.format("%s: top card of %s", obj.getName(), owner == null ? "error" : owner.getName()), topCard, game);
});
controller.lookAtCards("Top card of your library", topCard, game);
return true;
}

View file

@ -1,23 +0,0 @@
package mage.abilities.effects.common.continuous;
import mage.constants.Duration;
import mage.constants.TargetController;
/**
* @author JayDi85
*/
public class LookAtTopCardOfLibraryAnyTimeTargetEffect extends LookAtTopCardOfLibraryAnyTimeEffect {
public LookAtTopCardOfLibraryAnyTimeTargetEffect(Duration duration) {
super(TargetController.SOURCE_TARGETS, duration);
}
private LookAtTopCardOfLibraryAnyTimeTargetEffect(final LookAtTopCardOfLibraryAnyTimeTargetEffect effect) {
super(effect);
}
@Override
public LookAtTopCardOfLibraryAnyTimeTargetEffect copy() {
return new LookAtTopCardOfLibraryAnyTimeTargetEffect(this);
}
}

View file

@ -13,6 +13,8 @@ import mage.players.Player;
import mage.util.CardUtil;
import mage.watchers.common.SpellsCastWatcher;
import java.util.UUID;
/**
* @author xenohedron
*/
@ -21,17 +23,24 @@ public class NextSpellCastHasAbilityEffect extends ContinuousEffectImpl {
private int spellsCast;
private final Ability ability;
private final FilterCard filter;
private final TargetController targetController;
public NextSpellCastHasAbilityEffect(Ability ability) {
this(ability, StaticFilters.FILTER_CARD);
}
public NextSpellCastHasAbilityEffect(Ability ability, FilterCard filter) {
this(ability, filter, TargetController.SOURCE_CONTROLLER);
}
public NextSpellCastHasAbilityEffect(Ability ability, FilterCard filter, TargetController targetController) {
super(Duration.EndOfTurn, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.ability = ability;
this.filter = filter;
this.targetController = targetController;
staticText = "the next " + filter.getMessage().replace("card", "spell")
+ " you cast this turn has " + CardUtil.getTextWithFirstCharLowerCase(CardUtil.stripReminderText(ability.getRule()));
+ (targetController == TargetController.SOURCE_CONTROLLER ? " you cast" : " target player casts")
+ " this turn has " + CardUtil.getTextWithFirstCharLowerCase(CardUtil.stripReminderText(ability.getRule()));
}
private NextSpellCastHasAbilityEffect(final NextSpellCastHasAbilityEffect effect) {
@ -39,6 +48,7 @@ public class NextSpellCastHasAbilityEffect extends ContinuousEffectImpl {
this.spellsCast = effect.spellsCast;
this.ability = effect.ability;
this.filter = effect.filter;
this.targetController = effect.targetController;
}
@Override
@ -57,17 +67,28 @@ public class NextSpellCastHasAbilityEffect extends ContinuousEffectImpl {
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
UUID playerId;
switch (targetController){
case SOURCE_TARGETS:
playerId = source.getFirstTarget();
break;
case SOURCE_CONTROLLER:
playerId = source.getControllerId();
break;
default:
throw new UnsupportedOperationException("Value for targetController in NextSpellCastHasAbilityEffect not supported: " + targetController);
}
Player player = game.getPlayer(playerId);
SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class);
if (player == null || watcher == null) {
return false;
}
//check if a spell was cast before
if (watcher.getCount(source.getControllerId()) > spellsCast) {
if (watcher.getCount(playerId) > spellsCast) {
discard(); // only one use
return false;
}
for (Card card : game.getExile().getAllCardsByRange(game, source.getControllerId())) {
for (Card card : game.getExile().getAllCardsByRange(game, playerId)) {
if (filter.match(card, game)) {
game.getState().addOtherAbility(card, ability);
}
@ -94,7 +115,7 @@ public class NextSpellCastHasAbilityEffect extends ContinuousEffectImpl {
.forEach(card -> game.getState().addOtherAbility(card, ability));
for (StackObject stackObject : game.getStack()) {
if (!(stackObject instanceof Spell) || !stackObject.isControlledBy(source.getControllerId())) {
if (!(stackObject instanceof Spell) || !stackObject.isControlledBy(playerId)) {
continue;
}
// TODO: Distinguish "you cast" to exclude copies

View file

@ -0,0 +1,99 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.cards.Card;
import mage.constants.AsThoughEffectType;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import java.util.Locale;
import java.util.UUID;
/**
* @author nantuko, JayDi85, xenohedron
*/
public class PlayFromTopOfLibraryEffect extends AsThoughEffectImpl {
private final FilterCard filter;
private static final FilterCard defaultFilter = new FilterCard("play lands and cast spells");
/**
* You may play lands and cast spells from the top of your library
*/
public PlayFromTopOfLibraryEffect() {
this(defaultFilter);
}
/**
* You may [play lands and/or cast spells, according to filter] from the top of your library
*/
public PlayFromTopOfLibraryEffect(FilterCard filter) {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
this.filter = filter;
this.staticText = "you may " + filter.getMessage() + " from the top of your library";
// verify check: this ability is to allow playing lands or casting spells, not playing a "card"
if (filter.getMessage().toLowerCase(Locale.ENGLISH).contains("card")) {
throw new IllegalArgumentException("Wrong code usage or wrong filter text: PlayTheTopCardEffect");
}
}
protected PlayFromTopOfLibraryEffect(final PlayFromTopOfLibraryEffect effect) {
super(effect);
this.filter = effect.filter;
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public PlayFromTopOfLibraryEffect copy() {
return new PlayFromTopOfLibraryEffect(this);
}
@Override
public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) {
throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility");
}
@Override
public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) {
// can play lands/spells (must check specific part and allows specific part)
Card cardToCheck = game.getCard(objectId); // maybe this should be removed and only check SpellAbility characteristics
if (cardToCheck == null) {
return false;
}
if (affectedAbility instanceof SpellAbility) {
SpellAbility spell = (SpellAbility) affectedAbility;
cardToCheck = spell.getCharacteristics(game);
if (spell.getManaCosts().isEmpty()){
return false;
}
}
// only permits you to cast
if (!playerId.equals(source.getControllerId())) {
return false;
}
Player cardOwner = game.getPlayer(cardToCheck.getOwnerId());
Player controller = game.getPlayer(source.getControllerId());
if (cardOwner == null || controller == null) {
return false;
}
// main card of spell must be on top of your library
Card topCard = controller.getLibrary().getFromTop(game);
if (topCard == null || !topCard.getId().equals(cardToCheck.getMainCard().getId())) {
return false;
}
// spell characteristics must match filter
return filter.match(cardToCheck, playerId, source, game);
}
}

View file

@ -1,170 +0,0 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
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.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
/**
* @author nantuko, JayDi85
*/
public class PlayTheTopCardEffect extends AsThoughEffectImpl {
private final FilterCard filter;
private final TargetController targetLibrary;
// can play card or can play lands/cast spells, see two modes below
private final boolean canPlayCardOnly;
/**
* Support targets, use TargetController.SOURCE_TARGETS
*/
public PlayTheTopCardEffect() {
this(TargetController.YOU);
}
public PlayTheTopCardEffect(TargetController targetLibrary) {
this(targetLibrary, new FilterCard("play lands and cast spells"), false);
}
public PlayTheTopCardEffect(TargetController targetLibrary, FilterCard filter, boolean canPlayCardOnly) {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit);
this.filter = filter;
this.targetLibrary = targetLibrary;
this.canPlayCardOnly = canPlayCardOnly;
String libInfo;
switch (this.targetLibrary) {
case YOU:
libInfo = "your library";
break;
case OPPONENT:
libInfo = "opponents libraries";
break;
case SOURCE_TARGETS:
libInfo = "target player's library";
break;
default:
throw new IllegalArgumentException("Unknown target library type: " + targetLibrary);
}
this.staticText = "you may " + filter.getMessage() + " from the top of " + libInfo;
// verify check: if you see "card" text in the rules then use card mode
// (there aren't any real cards after oracle update, but can be added in the future)
if (this.canPlayCardOnly != filter.getMessage().toLowerCase(Locale.ENGLISH).contains("card")) {
throw new IllegalArgumentException("Wrong usage of card mode settings");
}
}
protected PlayTheTopCardEffect(final PlayTheTopCardEffect effect) {
super(effect);
this.filter = effect.filter;
this.targetLibrary = effect.targetLibrary;
this.canPlayCardOnly = effect.canPlayCardOnly;
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public PlayTheTopCardEffect copy() {
return new PlayTheTopCardEffect(this);
}
@Override
public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) {
throw new IllegalArgumentException("Wrong code usage: can't call applies method on empty affectedAbility");
}
@Override
public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) {
// main card and all parts are checks in different calls.
// two modes:
// * can play cards (must check main card and allows any parts)
// * can play lands/spells (must check specific part and allows specific part)
// current card's part
Card cardToCheck = game.getCard(objectId);
if (cardToCheck == null) {
return false;
}
if (this.canPlayCardOnly) {
// check whole card instead part
cardToCheck = cardToCheck.getMainCard();
} else if (affectedAbility instanceof SpellAbility) {
SpellAbility spell = (SpellAbility) affectedAbility;
cardToCheck = spell.getCharacteristics(game);
if (spell.getManaCosts().isEmpty()){
return false;
}
}
// must be you
if (!playerId.equals(source.getControllerId())) {
return false;
}
Player cardOwner = game.getPlayer(cardToCheck.getOwnerId());
Player controller = game.getPlayer(source.getControllerId());
if (cardOwner == null || controller == null) {
return false;
}
// must be your or opponents library
switch (this.targetLibrary) {
case YOU: {
Card topCard = controller.getLibrary().getFromTop(game);
if (topCard == null || !topCard.getId().equals(cardToCheck.getMainCard().getId())) {
return false;
}
break;
}
case OPPONENT: {
if (!game.getOpponents(controller.getId()).contains(cardOwner.getId())) {
return false;
}
Card topCard = cardOwner.getLibrary().getFromTop(game);
if (topCard == null || !topCard.getId().equals(cardToCheck.getMainCard().getId())) {
return false;
}
break;
}
case SOURCE_TARGETS: {
UUID needCardId = cardToCheck.getMainCard().getId();
if (CardUtil.getAllSelectedTargets(source, game).stream()
.map(game::getPlayer)
.filter(Objects::nonNull)
.noneMatch(player -> {
Card topCard = player.getLibrary().getFromTop(game);
return topCard != null && topCard.getId().equals(needCardId);
})) {
return false;
}
break;
}
default: {
return false;
}
}
// must be correct card
return filter.match(cardToCheck, playerId, source, game);
}
}

View file

@ -1,22 +0,0 @@
package mage.abilities.effects.common.continuous;
import mage.constants.TargetController;
/**
* @author JayDi85
*/
public class PlayTheTopCardTargetEffect extends PlayTheTopCardEffect {
public PlayTheTopCardTargetEffect() {
super(TargetController.SOURCE_TARGETS);
}
protected PlayTheTopCardTargetEffect(final PlayTheTopCardTargetEffect effect) {
super(effect);
}
@Override
public PlayTheTopCardTargetEffect copy() {
return new PlayTheTopCardTargetEffect(this);
}
}

View file

@ -0,0 +1,46 @@
package mage.abilities.effects.common.cost;
import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.filter.FilterCard;
import mage.filter.common.FilterCreatureCard;
import mage.game.Game;
public class FaceDownSpellsCostReductionControllerEffect extends SpellsCostReductionControllerEffect{
private static final FilterCreatureCard standardFilter = new FilterCreatureCard("Face-down creature spells");
/**
* Face-down creature spells you cast cost
* @param amount less to cast
*/
public FaceDownSpellsCostReductionControllerEffect(int amount) {
super(standardFilter, amount);
}
/**
* Face-down spells you cast cost
* @param filter with matching characteristics
* @param amount less to cast
*/
public FaceDownSpellsCostReductionControllerEffect(FilterCard filter, int amount) {
super(filter, amount);
}
protected FaceDownSpellsCostReductionControllerEffect(final FaceDownSpellsCostReductionControllerEffect effect) {
super(effect);
}
@Override
public FaceDownSpellsCostReductionControllerEffect copy() {
return new FaceDownSpellsCostReductionControllerEffect(this);
}
@Override
public boolean applies(Ability abilityToModify, Ability source, Game game) {
if (abilityToModify instanceof SpellAbility && ((SpellAbility) abilityToModify).getSpellAbilityCastMode().isFaceDown()) {
return super.applies(abilityToModify, source, game);
}
return false;
}
}

View file

@ -1,33 +0,0 @@
package mage.abilities.effects.common.cost;
import mage.abilities.Ability;
import mage.abilities.keyword.MorphAbility;
import mage.filter.FilterCard;
import mage.filter.common.FilterCreatureCard;
import mage.game.Game;
public class MorphSpellsCostReductionControllerEffect extends SpellsCostReductionControllerEffect{
private static final FilterCreatureCard standardFilter = new FilterCreatureCard("Face-down creature spells");
public MorphSpellsCostReductionControllerEffect(int amount) {
super(standardFilter, amount);
}
public MorphSpellsCostReductionControllerEffect(FilterCard filter, int amount) {
super(filter, amount);
}
protected MorphSpellsCostReductionControllerEffect(final MorphSpellsCostReductionControllerEffect effect) {
super(effect);
}
@Override
public MorphSpellsCostReductionControllerEffect copy() {
return new MorphSpellsCostReductionControllerEffect(this);
}
@Override
public boolean applies(Ability abilityToModify, Ability source, Game game) {
if (abilityToModify instanceof MorphAbility) {
return super.applies(abilityToModify, source, game);
}
return false;
}
}

View file

@ -47,13 +47,16 @@ public class SpellsCostIncreasingAllEffect extends CostModificationEffectImpl {
private void setText() {
StringBuilder sb = new StringBuilder();
sb.append(filter.getMessage());
String filterMessage = filter.getMessage();
sb.append(filterMessage);
switch (this.targetController) {
case YOU:
sb.append(" you cast");
break;
case OPPONENT:
sb.append(" your opponents cast");
if (!filterMessage.contains("your opponents cast")) {
sb.append(" your opponents cast");
}
break;
case ACTIVE:
sb.append(" the active player casts");

View file

@ -34,12 +34,8 @@ public class SpellsCostReductionControllerEffect extends CostModificationEffectI
this.amount = 0;
this.manaCostsToReduce = manaCostsToReduce;
this.upTo = false;
StringBuilder sb = new StringBuilder();
sb.append(filter.getMessage()).append(" you cast cost ");
sb.append(manaCostsToReduce.getText());
sb.append(" less to cast. This effect reduces only the amount of colored mana you pay.");
this.staticText = sb.toString();
this.staticText = filter.getMessage() + " you cast cost " + manaCostsToReduce.getText() +
" less to cast. This effect reduces only the amount of colored mana you pay.";
}
public SpellsCostReductionControllerEffect(FilterCard filter, int amount) {

View file

@ -44,29 +44,33 @@ public class AddCountersAllEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = game.getObject(source);
if (controller != null && sourceObject != null) {
if (counter != null) {
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source, game)) {
Counter newCounter = counter.copy();
int calculated = amount.calculate(game, source, this); // 0 -- you must use default counter
if (calculated < 0) {
continue;
} else if (calculated == 0) {
// use original counter
} else {
// increase to calculated value
newCounter.remove(newCounter.getCount());
newCounter.add(calculated);
}
permanent.addCounters(newCounter, source.getControllerId(), source, game);
if (!game.isSimulation() && newCounter.getCount() > 0) {
game.informPlayers(sourceObject.getLogName() + ": " + controller.getLogName() + " puts " + newCounter.getCount() + ' ' + newCounter.getName()
+ (newCounter.getCount() == 1 ? " counter" : " counters") + " on " + permanent.getLogName());
}
}
if (controller != null && sourceObject != null && counter != null) {
Counter newCounter = counter.copy();
int calculated = amount.calculate(game, source, this);
if (!(amount instanceof StaticValue) || calculated > 0) {
// If dynamic, or static and set to a > 0 value, we use that instead of the counter's internal amount.
newCounter.remove(newCounter.getCount());
newCounter.add(calculated);
} else {
// StaticValue 0 -- the default counter has the amount, so no adjustment.
}
return true;
if (newCounter.getCount() <= 0) {
return false; // no need to iterate on the permanents, no counters will be put on them
}
boolean result = false;
for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source, game)) {
Counter newCounterForPermanent = newCounter.copy();
permanent.addCounters(newCounterForPermanent, source.getControllerId(), source, game);
game.informPlayers(sourceObject.getLogName() + ": " + controller.getLogName() + " puts "
+ newCounterForPermanent.getCount() + ' ' + newCounterForPermanent.getName()
+ (newCounterForPermanent.getCount() == 1 ? " counter" : " counters") + " on " + permanent.getLogName());
result |= true;
}
return result;
}
return false;
}

View file

@ -56,38 +56,42 @@ public class AddCountersTargetEffect extends OneShotEffect {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = game.getObject(source);
if (controller != null && sourceObject != null && counter != null) {
Counter newCounter = counter.copy();
int calculated = amount.calculate(game, source, this);
if (!(amount instanceof StaticValue) || calculated > 0) {
// If dynamic, or static and set to a > 0 value, we use that instead of the counter's internal amount.
newCounter.remove(newCounter.getCount());
newCounter.add(calculated);
} else {
// StaticValue 0 -- the default counter has the amount, so no adjustment.
}
if (newCounter.getCount() <= 0) {
return false; // no need to iterate on targets, no counters will be put on them
}
int affectedTargets = 0;
for (UUID uuid : getTargetPointer().getTargets(game, source)) {
Counter newCounter = counter.copy();
int calculated = amount.calculate(game, source, this); // 0 -- you must use default couner
if (calculated < 0) {
continue;
} else if (calculated == 0) {
// use original counter
} else {
// increase to calculated value
newCounter.remove(newCounter.getCount());
newCounter.add(calculated);
}
Counter newCounterForTarget = newCounter.copy();
Permanent permanent = game.getPermanent(uuid);
Player player = game.getPlayer(uuid);
Card card = game.getCard(getTargetPointer().getFirst(game, source));
if (permanent != null) {
permanent.addCounters(newCounter, source.getControllerId(), source, game);
permanent.addCounters(newCounterForTarget, source.getControllerId(), source, game);
affectedTargets++;
game.informPlayers(sourceObject.getLogName() + ": " + controller.getLogName() + " puts "
+ newCounter.getCount() + ' ' + newCounter.getName() + " counters on " + permanent.getLogName());
+ newCounterForTarget.getCount() + ' ' + newCounterForTarget.getName() + " counters on " + permanent.getLogName());
} else if (player != null) {
player.addCounters(newCounter, source.getControllerId(), source, game);
player.addCounters(newCounterForTarget, source.getControllerId(), source, game);
affectedTargets++;
game.informPlayers(sourceObject.getLogName() + ": " + controller.getLogName() + " puts "
+ newCounter.getCount() + ' ' + newCounter.getName() + " counters on " + player.getLogName());
+ newCounterForTarget.getCount() + ' ' + newCounterForTarget.getName() + " counters on " + player.getLogName());
} else if (card != null) {
card.addCounters(newCounter, source.getControllerId(), source, game);
card.addCounters(newCounterForTarget, source.getControllerId(), source, game);
affectedTargets++;
game.informPlayers(sourceObject.getLogName() + ": " + controller.getLogName() + " puts "
+ newCounter.getCount() + ' ' + newCounter.getName() + " counters on " + card.getLogName());
+ newCounterForTarget.getCount() + ' ' + newCounterForTarget.getName() + " counters on " + card.getLogName());
}
}
return affectedTargets > 0;

View file

@ -14,9 +14,10 @@ import mage.target.common.TargetCardInLibrary;
*/
public class SearchLibraryPutInGraveyardEffect extends SearchEffect {
public SearchLibraryPutInGraveyardEffect() {
public SearchLibraryPutInGraveyardEffect(boolean textThatCard) {
super(new TargetCardInLibrary(StaticFilters.FILTER_CARD), Outcome.Neutral);
staticText = "search your library for a card, put that card into your graveyard, then shuffle";
staticText = "search your library for a card, put " + (textThatCard ? "that card" : "it")
+ " into your graveyard, then shuffle";
}
protected SearchLibraryPutInGraveyardEffect(final SearchLibraryPutInGraveyardEffect effect) {
@ -41,4 +42,4 @@ public class SearchLibraryPutInGraveyardEffect extends SearchEffect {
return true;
}
}
}

View file

@ -39,7 +39,7 @@ public class BlitzAbility extends SpellAbility {
ability.addEffect(new BlitzAddDelayedTriggeredAbilityEffect());
ability.setRuleVisible(false);
addSubAbility(ability);
this.ruleAdditionalCostsVisible = false;
this.setAdditionalCostsRuleVisible(false);
this.timing = TimingRule.SORCERY;
}

View file

@ -41,9 +41,12 @@ public class CollectEvidenceAbility extends StaticAbility implements OptionalAdd
}
public CollectEvidenceAbility(int amount) {
this(amount, null);
}
public CollectEvidenceAbility(int amount, String extraInfoText) {
super(Zone.STACK, null);
this.additionalCost = makeCost(amount);
this.rule = additionalCost.getName() + ". " + additionalCost.getReminderText();
this.rule = additionalCost.getName() + ". " + (extraInfoText == null ? "" : extraInfoText + ". ") + additionalCost.getReminderText();
this.setRuleAtTheTop(true);
this.addHint(hint);
this.amount = amount;

View file

@ -22,7 +22,7 @@ public class CommanderStormAbility extends TriggeredAbilityImpl {
public CommanderStormAbility() {
super(Zone.STACK, new CommanderStormEffect());
this.ruleAtTheTop = true;
this.setRuleAtTheTop(true);
}
private CommanderStormAbility(final CommanderStormAbility ability) {

View file

@ -153,7 +153,7 @@ public class ConspireAbility extends StaticAbility implements OptionalAdditional
public ConspireAbility setAddedById(UUID addedById) {
this.addedById = addedById;
CardUtil.castStream(
this.subAbilities.stream(),
this.getSubAbilities().stream(),
ConspireTriggeredAbility.class
).forEach(ability -> ability.setAddedById(addedById));
return this;

View file

@ -72,8 +72,9 @@ public class CrewAbility extends SimpleActivatedAbility {
@Override
public String getRule() {
return "Crew " + value + " <i>(Tap any number of creatures you control with total power "
+ value + " or more: This Vehicle becomes an artifact creature until end of turn.)</i>";
return "Crew " + value + (this.maxActivationsPerTurn == 1 ? ". Activate only once each turn." : "") +
" <i>(Tap any number of creatures you control with total power " + value +
" or more: This Vehicle becomes an artifact creature until end of turn.)</i>";
}
}

View file

@ -4,12 +4,14 @@ import mage.abilities.Ability;
import mage.abilities.SpellAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostAdjuster;
import mage.abilities.costs.Costs;
import mage.abilities.costs.CostsImpl;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCost;
import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect;
import mage.cards.Card;
import mage.constants.Duration;
import mage.constants.SpellAbilityCastMode;
import mage.constants.SpellAbilityType;
import mage.constants.TimingRule;
@ -68,6 +70,10 @@ public class DisguiseAbility extends SpellAbility {
protected Costs<Cost> disguiseCosts;
public DisguiseAbility(Card card, Cost disguiseCost) {
this(card, disguiseCost, null);
}
public DisguiseAbility(Card card, Cost disguiseCost, CostAdjuster costAdjuster) {
super(new GenericManaCost(3), card.getName());
this.timing = TimingRule.SORCERY;
this.disguiseCosts = new CostsImpl<>();
@ -77,13 +83,15 @@ public class DisguiseAbility extends SpellAbility {
// face down effect (hidden by default, visible in face down objects)
Ability ability = new SimpleStaticAbility(new BecomesFaceDownCreatureEffect(
this.disguiseCosts, BecomesFaceDownCreatureEffect.FaceDownType.DISGUISED));
this.disguiseCosts, null, Duration.WhileOnBattlefield,
BecomesFaceDownCreatureEffect.FaceDownType.DISGUISED, costAdjuster
));
ability.setWorksFaceDown(true);
ability.setRuleVisible(false);
addSubAbility(ability);
}
private DisguiseAbility(final DisguiseAbility ability) {
protected DisguiseAbility(final DisguiseAbility ability) {
super(ability);
this.disguiseCosts = ability.disguiseCosts; // can't be changed TODO: looks buggy, need research
}

View file

@ -110,7 +110,7 @@ public class EmergeAbility extends SpellAbility {
this.setCostsTag(EMERGE_ACTIVATION_CREATURE_REFERENCE, mor); //Can access with LKI afterwards
return true;
} else {
activated = false;
activated = false; // TODO: research, why
}
}
}

View file

@ -332,17 +332,17 @@ public class ForetellAbility extends SpecialAction {
if (game.getState().getZone(mainCardId) != Zone.EXILED) {
return ActivationStatus.getFalse();
}
Integer foretoldTurn = (Integer) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number");
UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility");
// Card must be Foretold
if (game.getState().getValue(mainCardId.toString() + "Foretell Turn Number") == null
&& game.getState().getValue(mainCardId + "foretellAbility") == null) {
if (foretoldTurn == null || exileId == null) {
return ActivationStatus.getFalse();
}
// Can't be cast if the turn it was Foretold is the same
if ((int) game.getState().getValue(mainCardId.toString() + "Foretell Turn Number") == game.getTurnNum()) {
if (foretoldTurn == game.getTurnNum()) {
return ActivationStatus.getFalse();
}
// Check that the card is actually in the exile zone (ex: Oblivion Ring exiles it after it was Foretold, etc)
UUID exileId = (UUID) game.getState().getValue(mainCardId.toString() + "foretellAbility");
ExileZone exileZone = game.getState().getExile().getExileZone(exileId);
if (exileZone != null
&& exileZone.isEmpty()) {

View file

@ -0,0 +1,325 @@
package mage.abilities.keyword;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.SpecialAction;
import mage.abilities.SpellAbility;
import mage.abilities.costs.Cost;
import mage.abilities.costs.Costs;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.cards.*;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.List;
import java.util.UUID;
/**
* @author Susucr
*/
public class PlotAbility extends SpecialAction {
private final String rule;
public PlotAbility(String plotCost) {
super(Zone.ALL); // Usually, plot only works from hand. However [[Fblthp, Lost on the Range]] allows plotting from library
this.addCost(new ManaCostsImpl<>(plotCost));
this.addEffect(new PlotSourceExileEffect());
this.setTiming(TimingRule.SORCERY);
this.usesStack = false;
this.rule = "Plot " + plotCost;
}
private PlotAbility(final PlotAbility ability) {
super(ability);
this.rule = ability.rule;
}
@Override
public PlotAbility copy() {
return new PlotAbility(this);
}
@Override
public String getRule() {
return rule;
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
// Plot ability uses card's timing restriction
Card card = game.getCard(getSourceId());
if (card == null) {
return ActivationStatus.getFalse();
}
// plot can only be activated from hand or from top of library if allowed to.
Zone zone = game.getState().getZone(getSourceId());
if (zone == Zone.HAND) {
// Allowed from hand
} else if (zone == Zone.LIBRARY) {
// Allowed only if permitted for top card, and only if the card is on top and is nonland
// Note: if another effect changes zones where permitted, or if different card categories are permitted,
// it would be better to refactor this as an unique AsThoughEffect.
// As of now, only Fblthp, Lost on the Range changes permission of plot.
Player player = game.getPlayer(getControllerId());
if (player == null || !player.canPlotFromTopOfLibrary()) {
return ActivationStatus.getFalse();
}
Card topCardLibrary = player.getLibrary().getFromTop(game);
if (topCardLibrary == null || !topCardLibrary.getId().equals(card.getId()) || card.isLand()) {
return ActivationStatus.getFalse();
}
} else {
// Not Allowed from other zones
return ActivationStatus.getFalse();
}
if (!card.getSpellAbility().spellCanBeActivatedRegularlyNow(playerId, game)) {
return ActivationStatus.getFalse();
}
return super.canActivate(playerId, game);
}
static UUID getPlotExileId(UUID playerId, Game game) {
UUID exileId = (UUID) game.getState().getValue("PlotExileId" + playerId.toString());
if (exileId == null) {
exileId = UUID.randomUUID();
game.getState().setValue("PlotExileId" + playerId, exileId);
}
return exileId;
}
static String getPlotTurnKeyForCard(UUID cardId) {
return cardId.toString() + "|" + "Plotted Turn";
}
/**
* To be used in an OneShotEffect's apply.
* 'Plot' the provided card. The card is exiled in it's owner plot zone,
* and may be cast by that player without paying its mana cost at sorcery
* speed on a future turn.
*/
public static boolean doExileAndPlotCard(Card card, Game game, Ability source) {
if (card == null) {
return false;
}
Player owner = game.getPlayer(card.getOwnerId());
if (owner == null) {
return false;
}
UUID exileId = PlotAbility.getPlotExileId(owner.getId(), game);
String exileZoneName = "Plots of " + owner.getName();
Card mainCard = card.getMainCard();
Zone zone = game.getState().getZone(mainCard.getId());
if (mainCard.moveToExile(exileId, exileZoneName, source, game)) {
// Remember on which turn the card was last plotted.
game.getState().setValue(PlotAbility.getPlotTurnKeyForCard(mainCard.getId()), game.getTurnNum());
game.addEffect(new PlotAddSpellAbilityEffect(new MageObjectReference(mainCard, game)), source);
game.informPlayers(
owner.getLogName()
+ " plots " + mainCard.getLogName()
+ " from " + zone.toString().toLowerCase()
+ CardUtil.getSourceLogName(game, source, card.getId())
);
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.BECOME_PLOTTED, mainCard.getId(), source, owner.getId()));
}
return true;
}
}
/**
* Exile the source card in the plot exile zone of its owner
* and allow its owner to cast it at sorcery speed starting
* next turn.
*/
class PlotSourceExileEffect extends OneShotEffect {
PlotSourceExileEffect() {
super(Outcome.Benefit);
}
private PlotSourceExileEffect(final PlotSourceExileEffect effect) {
super(effect);
}
@Override
public PlotSourceExileEffect copy() {
return new PlotSourceExileEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
return PlotAbility.doExileAndPlotCard(game.getCard(source.getSourceId()), game, source);
}
}
class PlotAddSpellAbilityEffect extends ContinuousEffectImpl {
private final MageObjectReference mor;
PlotAddSpellAbilityEffect(MageObjectReference mor) {
super(Duration.EndOfGame, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
this.mor = mor;
staticText = "Plot card";
}
private PlotAddSpellAbilityEffect(final PlotAddSpellAbilityEffect effect) {
super(effect);
this.mor = effect.mor;
}
@Override
public PlotAddSpellAbilityEffect copy() {
return new PlotAddSpellAbilityEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Card card = mor.getCard(game);
if (card == null) {
discard();
return true;
}
Card mainCard = card.getMainCard();
UUID mainCardId = mainCard.getId();
Player player = game.getPlayer(card.getOwnerId());
if (game.getState().getZone(mainCardId) != Zone.EXILED || player == null) {
discard();
return true;
}
List<Card> faces = CardUtil.getCastableComponents(mainCard, null, source, player, game, null, false);
for (Card face : faces) {
// Add the spell ability to each castable face to have the proper name/paramaters.
PlotSpellAbility ability = new PlotSpellAbility(face.getName());
ability.setSourceId(face.getId());
ability.setControllerId(player.getId());
ability.setSpellAbilityType(face.getSpellAbility().getSpellAbilityType());
game.getState().addOtherAbility(face, ability);
}
return true;
}
}
/**
* This is inspired (after a little cleanup) by how {@link ForetellAbility} does it.
*/
class PlotSpellAbility extends SpellAbility {
private String faceCardName; // Same as with Foretell, we identify the proper face with its spell name.
private SpellAbility spellAbilityToResolve;
PlotSpellAbility(String faceCardName) {
super(null, faceCardName, Zone.EXILED, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.PLOT);
this.setRuleVisible(false);
this.setAdditionalCostsRuleVisible(false);
this.faceCardName = faceCardName;
this.addCost(new ManaCostsImpl<>("{0}"));
}
private PlotSpellAbility(final PlotSpellAbility ability) {
super(ability);
this.faceCardName = ability.faceCardName;
this.spellAbilityToResolve = ability.spellAbilityToResolve;
}
@Override
public PlotSpellAbility copy() {
return new PlotSpellAbility(this);
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
if (super.canActivate(playerId, game).canActivate()) {
Card card = game.getCard(getSourceId());
if (card != null) {
Card mainCard = card.getMainCard();
UUID mainCardId = mainCard.getId();
// Card must be in the exile zone
if (game.getState().getZone(mainCardId) != Zone.EXILED) {
return ActivationStatus.getFalse();
}
Integer plottedTurn = (Integer) game.getState().getValue(PlotAbility.getPlotTurnKeyForCard(mainCardId));
// Card must have been plotted
if (plottedTurn == null) {
return ActivationStatus.getFalse();
}
// Can't be cast if the turn it was last Plotted is the same
if (plottedTurn == game.getTurnNum()) {
return ActivationStatus.getFalse();
}
// Only allow the cast at sorcery speed
if (!game.canPlaySorcery(playerId)) {
return ActivationStatus.getFalse();
}
// Check that the proper face can be cast.
// TODO: As with Foretell, this does not look very clean. Is the face card sometimes incorrect on calling canActivate?
if (mainCard instanceof CardWithHalves) {
if (((CardWithHalves) mainCard).getLeftHalfCard().getName().equals(faceCardName)) {
return ((CardWithHalves) mainCard).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
} else if (((CardWithHalves) mainCard).getRightHalfCard().getName().equals(faceCardName)) {
return ((CardWithHalves) mainCard).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
}
} else if (card instanceof AdventureCard) {
if (card.getMainCard().getName().equals(faceCardName)) {
return card.getMainCard().getSpellAbility().canActivate(playerId, game);
} else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
}
}
return card.getSpellAbility().canActivate(playerId, game);
}
}
return ActivationStatus.getFalse();
}
@Override
public SpellAbility getSpellAbilityToResolve(Game game) {
Card card = game.getCard(getSourceId());
if (card != null) {
if (spellAbilityToResolve == null) {
SpellAbility spellAbilityCopy = null;
// TODO: As with Foretell, this does not look very clean. Is the face card sometimes incorrect on calling getSpellAbilityToResolve?
if (card instanceof CardWithHalves) {
if (((CardWithHalves) card).getLeftHalfCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithHalves) card).getLeftHalfCard().getSpellAbility().copy();
} else if (((CardWithHalves) card).getRightHalfCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((CardWithHalves) card).getRightHalfCard().getSpellAbility().copy();
}
} else if (card instanceof AdventureCard) {
if (card.getMainCard().getName().equals(faceCardName)) {
spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
} else if (((AdventureCard) card).getSpellCard().getName().equals(faceCardName)) {
spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
}
} else {
spellAbilityCopy = card.getSpellAbility().copy();
}
if (spellAbilityCopy == null) {
return null;
}
spellAbilityCopy.setId(this.getId());
spellAbilityCopy.clearManaCosts();
spellAbilityCopy.clearManaCostsToPay();
spellAbilityCopy.addCost(this.getCosts().copy());
spellAbilityCopy.addCost(this.getManaCosts().copy());
spellAbilityCopy.setSpellAbilityCastMode(this.getSpellAbilityCastMode());
spellAbilityToResolve = spellAbilityCopy;
}
}
return spellAbilityToResolve;
}
@Override
public Costs<Cost> getCosts() {
if (spellAbilityToResolve == null) {
return super.getCosts();
}
return spellAbilityToResolve.getCosts();
}
}

View file

@ -0,0 +1,179 @@
package mage.abilities.keyword;
import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.condition.common.SaddledCondition;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.abilities.hint.HintUtils;
import mage.constants.*;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.filter.predicate.permanent.TappedPredicate;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.Target;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.watchers.common.SaddledMountWatcher;
import java.awt.*;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* @author TheElk801
*/
public class SaddleAbility extends SimpleActivatedAbility {
private final int value;
private static final Hint hint = new ConditionHint(SaddledCondition.instance, "This permanent is saddled");
public SaddleAbility(int value) {
super(new SaddleEffect(), new SaddleCost(value));
this.value = value;
this.addHint(hint);
this.setTiming(TimingRule.SORCERY);
this.addWatcher(new SaddledMountWatcher());
}
private SaddleAbility(final SaddleAbility ability) {
super(ability);
this.value = ability.value;
}
@Override
public SaddleAbility copy() {
return new SaddleAbility(this);
}
@Override
public String getRule() {
return "Saddle " + value + " <i>(Tap any number of other creatures you control with total power " +
value + " or more: This Mount becomes saddled until end of turn. Saddle only as a sorcery.)</i>";
}
}
class SaddleEffect extends ContinuousEffectImpl {
SaddleEffect() {
super(Duration.EndOfTurn, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit);
}
private SaddleEffect(final SaddleEffect effect) {
super(effect);
}
@Override
public SaddleEffect copy() {
return new SaddleEffect(this);
}
@Override
public void init(Ability source, Game game) {
super.init(source, game);
game.fireEvent(GameEvent.getEvent(
GameEvent.EventType.MOUNT_SADDLED,
source.getSourceId(),
source, source.getControllerId()
));
}
@Override
public boolean apply(Game game, Ability source) {
Optional.ofNullable(source.getSourcePermanentIfItStillExists(game))
.ifPresent(permanent -> permanent.setSaddled(true));
return true;
}
}
class SaddleCost extends CostImpl {
private static final FilterControlledCreaturePermanent filter
= new FilterControlledCreaturePermanent("another untapped creature you control");
static {
filter.add(TappedPredicate.UNTAPPED);
filter.add(AnotherPredicate.instance);
}
private final int value;
SaddleCost(int value) {
this.value = value;
}
private SaddleCost(final SaddleCost cost) {
super(cost);
this.value = cost.value;
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Target target = new TargetControlledCreaturePermanent(0, Integer.MAX_VALUE, filter, true) {
@Override
public String getMessage() {
// shows selected power
int selectedPower = this.targets.keySet().stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.map(MageObject::getPower)
.mapToInt(MageInt::getValue)
.sum();
String extraInfo = "(selected power " + selectedPower + " of " + value + ")";
if (selectedPower >= value) {
extraInfo = HintUtils.prepareText(extraInfo, Color.GREEN);
}
return super.getMessage() + " " + extraInfo;
}
};
// can cancel
if (target.choose(Outcome.Tap, controllerId, source.getSourceId(), source, game)) {
int sumPower = 0;
for (UUID targetId : target.getTargets()) {
GameEvent event = new GameEvent(GameEvent.EventType.SADDLE_MOUNT, targetId, source, controllerId);
if (!game.replaceEvent(event)) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null && permanent.tap(source, game)) {
sumPower += permanent.getPower().getValue();
}
}
}
paid = sumPower >= value;
if (paid) {
for (UUID targetId : target.getTargets()) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.SADDLED_MOUNT, targetId, source, controllerId));
}
}
} else {
return false;
}
return paid;
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
int sumPower = 0;
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, controllerId, game)) {
sumPower += Math.max(permanent.getPower().getValue(), 0);
if (sumPower >= value) {
return true;
}
}
return false;
}
@Override
public SaddleCost copy() {
return new SaddleCost(this);
}
}

View file

@ -0,0 +1,28 @@
package mage.abilities.keyword;
import mage.abilities.StaticAbility;
import mage.cards.Card;
import mage.constants.Zone;
/**
* @author TheElk801
*/
public class SpreeAbility extends StaticAbility {
public SpreeAbility(Card card) {
super(Zone.ALL, null);
this.setRuleVisible(false);
card.getSpellAbility().getModes().setChooseText("Spree <i>(Choose one or more additional costs.)</i>");
card.getSpellAbility().getModes().setMinModes(1);
card.getSpellAbility().getModes().setMaxModes(Integer.MAX_VALUE);
}
private SpreeAbility(final SpreeAbility ability) {
super(ability);
}
@Override
public SpreeAbility copy() {
return new SpreeAbility(this);
}
}

View file

@ -5,7 +5,6 @@ import mage.abilities.SpellAbility;
import mage.abilities.StaticAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.costs.*;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.CreateTokenCopySourceEffect;
import mage.abilities.effects.OneShotEffect;
@ -25,9 +24,6 @@ public class SquadAbility extends StaticAbility implements OptionalAdditionalSou
protected static final String SQUAD_ACTIVATION_VALUE_KEY = "squadActivationCount";
protected static final String SQUAD_REMINDER = "You may pay an additional "
+ "{cost} any number of times as you cast this spell.";
public SquadAbility() {
this(new GenericManaCost(2));
}
public SquadAbility(Cost cost) {
super(Zone.STACK, null);

View file

@ -6,6 +6,7 @@ import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.constants.Outcome;
import mage.constants.Zone;
@ -21,9 +22,15 @@ import org.apache.log4j.Logger;
*/
public class StormAbility extends TriggeredAbilityImpl {
private static final Hint hint = new ValueHint("Spells cast this turn", SpellsCastThisTurnValue.instance);
public static Hint getHint() {
return hint;
}
public StormAbility() {
super(Zone.STACK, new StormEffect());
this.addHint(new ValueHint("Spells cast this turn", SpellsCastThisTurnValue.instance));
this.addHint(hint);
}
private StormAbility(final StormAbility ability) {

View file

@ -81,7 +81,7 @@ public class WardAbility extends TriggeredAbilityImpl {
if (targetingObject == null || !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId())) {
return false;
}
if (CardUtil.checkTargetedEventAlreadyUsed(this.id.toString(), targetingObject, event, game)) {
if (CardUtil.checkTargetedEventAlreadyUsed(this.getId().toString(), targetingObject, event, game)) {
return false;
}
getEffects().setTargetPointer(new FixedTarget(targetingObject.getId()));

View file

@ -605,6 +605,14 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
ability.setRuleVisible(true);
}
}
// The current face down implementation is just setting a boolean, so any trigger checking for a
// permanent property once being turned face up is not seeing the right face up data.
// For instance triggers looking for specific subtypes being turned face up (Detectives in MKM set)
// are broken without that processAction call.
// This is somewhat a band-aid on the special action nature of turning a permanent face up.
// 708.8. As a face-down permanent is turned face up, its copiable values revert to its normal copiable values.
// Any effects that have been applied to the face-down permanent still apply to the face-up permanent.
game.getState().processAction(game);
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.TURNED_FACE_UP, getId(), source, playerId));
return true;
}

View file

@ -32,7 +32,8 @@ public class CardNameUtil {
.replace("ü", "u")
.replace("É", "E")
.replace("ñ", "n")
.replace("®", "");
.replace("®", "")
.replace("", "");
}
private CardNameUtil() {

View file

@ -0,0 +1,29 @@
package mage.constants;
/**
* Groups together the most usual ways a card's payment is adjusted
* by card effects that allow play or cast.
* <p>
* Effects should attempt to support those for all the various ways
* to play/cast cards/spells in Effects
*
* @author Susucr
*/
public enum CastManaAdjustment {
/**
* No adjustment to play/cast
*/
NONE,
/**
* Mana can be used as any mana type to pay for the mana cost
*/
AS_THOUGH_ANY_MANA_TYPE,
/**
* Mana can be used as any mana color to pay for the mana cost
*/
AS_THOUGH_ANY_MANA_COLOR,
/**
* The card is play/cast without paying for its mana cost
*/
WITHOUT_PAYING_MANA_COST,
}

View file

@ -21,7 +21,8 @@ public enum SpellAbilityCastMode {
DISGUISE("Disguise", false, true),
TRANSFORMED("Transformed", true),
DISTURB("Disturb", true),
MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true);
MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true),
PLOT("Plot");
private final String text;
@ -91,6 +92,7 @@ public enum SpellAbilityCastMode {
case MADNESS:
case FLASHBACK:
case DISTURB:
case PLOT:
case MORE_THAN_MEETS_THE_EYE:
// it changes only cost, so keep other characteristics
// TODO: research - why TRANSFORMED here - is it used in this.isTransformed code?!

View file

@ -71,18 +71,19 @@ public enum SubType {
ANGEL("Angel", SubTypeSet.CreatureType),
ANTELOPE("Antelope", SubTypeSet.CreatureType),
ANZELLAN("Anzellan", SubTypeSet.CreatureType, true), // Star Wars
AQUALISH("Aqualish", SubTypeSet.CreatureType, true), // Star Wars
APE("Ape", SubTypeSet.CreatureType),
ARCONA("Arcona", SubTypeSet.CreatureType, true),
AQUALISH("Aqualish", SubTypeSet.CreatureType, true), // Star Wars
ARCHER("Archer", SubTypeSet.CreatureType),
ARCHON("Archon", SubTypeSet.CreatureType),
ARTIFICER("Artificer", SubTypeSet.CreatureType),
ARCONA("Arcona", SubTypeSet.CreatureType, true),
ARMADILLO("Armadillo", SubTypeSet.CreatureType),
ARMY("Army", SubTypeSet.CreatureType),
ARTIFICER("Artificer", SubTypeSet.CreatureType),
ASSASSIN("Assassin", SubTypeSet.CreatureType),
ASSEMBLY_WORKER("Assembly-Worker", SubTypeSet.CreatureType),
ASTARTES("Astartes", SubTypeSet.CreatureType),
ATOG("Atog", SubTypeSet.CreatureType),
ATAT("AT-AT", SubTypeSet.CreatureType, true),
ATOG("Atog", SubTypeSet.CreatureType),
AUROCHS("Aurochs", SubTypeSet.CreatureType),
AUTOBOT("Autobot", SubTypeSet.CreatureType, true), // H17, Grimlock
AVATAR("Avatar", SubTypeSet.CreatureType),
@ -96,6 +97,7 @@ public enum SubType {
BAT("Bat", SubTypeSet.CreatureType),
BEAR("Bear", SubTypeSet.CreatureType),
BEAST("Beast", SubTypeSet.CreatureType),
BEAVER("Beaver", SubTypeSet.CreatureType),
BEEBLE("Beeble", SubTypeSet.CreatureType),
BEHOLDER("Beholder", SubTypeSet.CreatureType),
BERSERKER("Berserker", SubTypeSet.CreatureType),
@ -107,6 +109,7 @@ public enum SubType {
BRINGER("Bringer", SubTypeSet.CreatureType),
BRUSHWAGG("Brushwagg", SubTypeSet.CreatureType),
// C
CTAN("C'tan", SubTypeSet.CreatureType),
CALAMARI("Calamari", SubTypeSet.CreatureType, true), // Star Wars
CAMARID("Camarid", SubTypeSet.CreatureType),
CAMEL("Camel", SubTypeSet.CreatureType),
@ -115,8 +118,8 @@ public enum SubType {
CARRIER("Carrier", SubTypeSet.CreatureType),
CAT("Cat", SubTypeSet.CreatureType),
CENTAUR("Centaur", SubTypeSet.CreatureType),
CEREAN("Cerean", SubTypeSet.CreatureType, true), // Star Wars
CEPHALID("Cephalid", SubTypeSet.CreatureType),
CEREAN("Cerean", SubTypeSet.CreatureType, true), // Star Wars
CHIMERA("Chimera", SubTypeSet.CreatureType),
CHISS("Chiss", SubTypeSet.CreatureType, true),
CITIZEN("Citizen", SubTypeSet.CreatureType),
@ -127,17 +130,17 @@ public enum SubType {
CONSTRUCT("Construct", SubTypeSet.CreatureType),
COW("Cow", SubTypeSet.CreatureType, true), // Unglued
COWARD("Coward", SubTypeSet.CreatureType),
COYOTE("Coyote", SubTypeSet.CreatureType),
CRAB("Crab", SubTypeSet.CreatureType),
CROCODILE("Crocodile", SubTypeSet.CreatureType),
CROLUTE("Crolute", SubTypeSet.CreatureType, true), // Star Wars
CTAN("C'tan", SubTypeSet.CreatureType),
CUSTODES("Custodes", SubTypeSet.CreatureType),
CYBERMAN("Cyberman", SubTypeSet.CreatureType),
CYBORG("Cyborg", SubTypeSet.CreatureType, true), // Star Wars
CYCLOPS("Cyclops", SubTypeSet.CreatureType),
// D
DALEK("Dalek", SubTypeSet.CreatureType),
DATHOMIRIAN("Dathomirian", SubTypeSet.CreatureType, true), // Star Wars,
DATHOMIRIAN("Dathomirian", SubTypeSet.CreatureType, true), // Star Wars
DAUTHI("Dauthi", SubTypeSet.CreatureType),
DEMIGOD("Demigod", SubTypeSet.CreatureType),
DEMON("Demon", SubTypeSet.CreatureType),
@ -151,9 +154,9 @@ public enum SubType {
DRAGON("Dragon", SubTypeSet.CreatureType),
DRAKE("Drake", SubTypeSet.CreatureType),
DREADNOUGHT("Dreadnought", SubTypeSet.CreatureType),
DROID("Droid", SubTypeSet.CreatureType, true), // Star Wars
DRONE("Drone", SubTypeSet.CreatureType),
DRUID("Druid", SubTypeSet.CreatureType),
DROID("Droid", SubTypeSet.CreatureType, true), // Star Wars
DRYAD("Dryad", SubTypeSet.CreatureType),
DWARF("Dwarf", SubTypeSet.CreatureType),
// E
@ -166,9 +169,9 @@ public enum SubType {
ELF("Elf", SubTypeSet.CreatureType),
ELK("Elk", SubTypeSet.CreatureType),
EMPLOYEE("Employee", SubTypeSet.CreatureType),
EYE("Eye", SubTypeSet.CreatureType),
EWOK("Ewok", SubTypeSet.CreatureType, true), // Star Wars
EXPANSION_SYMBOL("Expansion-Symbol", SubTypeSet.CreatureType, true), // Unhinged
EYE("Eye", SubTypeSet.CreatureType),
// F
FAERIE("Faerie", SubTypeSet.CreatureType),
FERRET("Ferret", SubTypeSet.CreatureType),
@ -186,12 +189,12 @@ public enum SubType {
GERM("Germ", SubTypeSet.CreatureType),
GIANT("Giant", SubTypeSet.CreatureType),
GITH("Gith", SubTypeSet.CreatureType),
GNOME("Gnome", SubTypeSet.CreatureType),
GNOLL("Gnoll", SubTypeSet.CreatureType),
GOLEM("Golem", SubTypeSet.CreatureType),
GNOME("Gnome", SubTypeSet.CreatureType),
GOAT("Goat", SubTypeSet.CreatureType),
GOBLIN("Goblin", SubTypeSet.CreatureType),
GOD("God", SubTypeSet.CreatureType),
GOLEM("Golem", SubTypeSet.CreatureType),
GORGON("Gorgon", SubTypeSet.CreatureType),
GRAVEBORN("Graveborn", SubTypeSet.CreatureType),
GREMLIN("Gremlin", SubTypeSet.CreatureType),
@ -226,7 +229,6 @@ public enum SubType {
// J
JACKAL("Jackal", SubTypeSet.CreatureType),
JAWA("Jawa", SubTypeSet.CreatureType, true),
JAYA("Jaya", SubTypeSet.PlaneswalkerType),
JEDI("Jedi", SubTypeSet.CreatureType, true), // Star Wars
JELLYFISH("Jellyfish", SubTypeSet.CreatureType),
JUGGERNAUT("Juggernaut", SubTypeSet.CreatureType),
@ -253,7 +255,6 @@ public enum SubType {
LIZARD("Lizard", SubTypeSet.CreatureType),
LLAMA("Llama", SubTypeSet.CreatureType),
LOBSTER("Lobster", SubTypeSet.CreatureType, true), // Unglued
LUKE("Luke", SubTypeSet.PlaneswalkerType, true), // Star Wars
// M
MANTELLIAN("Mantellian", SubTypeSet.CreatureType, true), // Star Wars
MANTICORE("Manticore", SubTypeSet.CreatureType),
@ -271,6 +272,7 @@ public enum SubType {
MONK("Monk", SubTypeSet.CreatureType),
MONKEY("Monkey", SubTypeSet.CreatureType),
MOONFOLK("Moonfolk", SubTypeSet.CreatureType),
MOUNT("Mount", SubTypeSet.CreatureType),
MOUSE("Mouse", SubTypeSet.CreatureType),
MUTANT("Mutant", SubTypeSet.CreatureType),
MYR("Myr", SubTypeSet.CreatureType),
@ -314,6 +316,8 @@ public enum SubType {
PINCHER("Pincher", SubTypeSet.CreatureType),
PIRATE("Pirate", SubTypeSet.CreatureType),
PLANT("Plant", SubTypeSet.CreatureType),
PORCUPINE("Porcupine", SubTypeSet.CreatureType),
POSSUM("Possum", SubTypeSet.CreatureType),
PRAETOR("Praetor", SubTypeSet.CreatureType),
PRIMARCH("Primarch", SubTypeSet.CreatureType),
PRISM("Prism", SubTypeSet.CreatureType),
@ -378,28 +382,28 @@ public enum SubType {
SPONGE("Sponge", SubTypeSet.CreatureType),
SQUID("Squid", SubTypeSet.CreatureType),
SQUIRREL("Squirrel", SubTypeSet.CreatureType),
SNOKE("Snoke", SubTypeSet.PlaneswalkerType, true), // Star Wars
STARFISH("Starfish", SubTypeSet.CreatureType),
STARSHIP("Starship", SubTypeSet.CreatureType, true), // Star Wars
SULLUSTAN("Sullustan", SubTypeSet.CreatureType, true), // Star Wars
SURRAKAR("Surrakar", SubTypeSet.CreatureType),
SURVIVOR("Survivor", SubTypeSet.CreatureType),
SYNTH("Synth", SubTypeSet.CreatureType),
// T
TENTACLE("Tentacle", SubTypeSet.CreatureType),
TETRAVITE("Tetravite", SubTypeSet.CreatureType),
THALAKOS("Thalakos", SubTypeSet.CreatureType),
THOPTER("Thopter", SubTypeSet.CreatureType),
THRULL("Thrull", SubTypeSet.CreatureType),
TIEFLING("Tiefling", SubTypeSet.CreatureType),
TIME_LORD("Time Lord", SubTypeSet.CreatureType),
TRANDOSHAN("Trandoshan", SubTypeSet.CreatureType, true), // Star Wars
THRULL("Thrull", SubTypeSet.CreatureType),
TREEFOLK("Treefolk", SubTypeSet.CreatureType),
TRILOBITE("Trilobite", SubTypeSet.CreatureType),
TRISKELAVITE("Triskelavite", SubTypeSet.CreatureType),
TROLL("Troll", SubTypeSet.CreatureType),
TROOPER("Trooper", SubTypeSet.CreatureType, true), // Star Wars
TURTLE("Turtle", SubTypeSet.CreatureType),
TUSKEN("Tusken", SubTypeSet.CreatureType, true), // Star Wars
TROOPER("Trooper", SubTypeSet.CreatureType, true), // Star Wars
TRILOBITE("Trilobite", SubTypeSet.CreatureType),
TWILEK("Twi'lek", SubTypeSet.CreatureType, true), // Star Wars
TYRANID("Tyranid", SubTypeSet.CreatureType),
// U
@ -407,6 +411,7 @@ public enum SubType {
UNICORN("Unicorn", SubTypeSet.CreatureType),
// V
VAMPIRE("Vampire", SubTypeSet.CreatureType),
VARMINT("Varmint", SubTypeSet.CreatureType),
VEDALKEN("Vedalken", SubTypeSet.CreatureType),
VIASHINO("Viashino", SubTypeSet.CreatureType),
VILLAIN("Villain", SubTypeSet.CreatureType, true), // Unstable
@ -468,6 +473,7 @@ public enum SubType {
INZERVA("Inzerva", SubTypeSet.PlaneswalkerType),
JACE("Jace", SubTypeSet.PlaneswalkerType),
JARED("Jared", SubTypeSet.PlaneswalkerType),
JAYA("Jaya", SubTypeSet.PlaneswalkerType),
JESKA("Jeska", SubTypeSet.PlaneswalkerType),
KAITO("Kaito", SubTypeSet.PlaneswalkerType),
KARN("Karn", SubTypeSet.PlaneswalkerType),
@ -476,8 +482,9 @@ public enum SubType {
KIORA("Kiora", SubTypeSet.PlaneswalkerType),
KOTH("Koth", SubTypeSet.PlaneswalkerType),
LILIANA("Liliana", SubTypeSet.PlaneswalkerType),
LUKKA("Lukka", SubTypeSet.PlaneswalkerType),
LOLTH("Lolth", SubTypeSet.PlaneswalkerType),
LUKE("Luke", SubTypeSet.PlaneswalkerType, true), // Star Wars
LUKKA("Lukka", SubTypeSet.PlaneswalkerType),
MINSC("Minsc", SubTypeSet.PlaneswalkerType),
MORDENKAINEN("Mordenkainen", SubTypeSet.PlaneswalkerType),
NAHIRI("Nahiri", SubTypeSet.PlaneswalkerType),
@ -497,6 +504,7 @@ public enum SubType {
SERRA("Serra", SubTypeSet.PlaneswalkerType),
SIDIOUS("Sidious", SubTypeSet.PlaneswalkerType, true), // Star Wars
SIVITRI("Sivitri", SubTypeSet.PlaneswalkerType),
SNOKE("Snoke", SubTypeSet.PlaneswalkerType, true), // Star Wars
SORIN("Sorin", SubTypeSet.PlaneswalkerType),
SZAT("Szat", SubTypeSet.PlaneswalkerType),
TAMIYO("Tamiyo", SubTypeSet.PlaneswalkerType),
@ -577,6 +585,11 @@ public enum SubType {
return description;
}
// note: does not account for irregular plurals
public String getPluralName() {
return description.endsWith("y") ? description.substring(0, description.length() - 1) + "ies" : description + 's';
}
@Override
public String toString() {
return description;

View file

@ -123,6 +123,7 @@ public enum CounterType {
LANDMARK("landmark"),
LEVEL("level"),
LIFELINK("lifelink"),
LOOT("loot"),
LORE("lore"),
LUCK("luck"),
LOYALTY("loyalty"),

View file

@ -1060,6 +1060,14 @@ public final class StaticFilters {
FILTER_CREATURE_NON_TOKEN.setLockedFilter(true);
}
public static final FilterCreaturePermanent FILTER_CREATURES_NON_TOKEN = new FilterCreaturePermanent("nontoken creatures");
static {
FILTER_CREATURES_NON_TOKEN.add(TokenPredicate.FALSE);
FILTER_CREATURES_NON_TOKEN.setLockedFilter(true);
}
public static final FilterControlledCreaturePermanent FILTER_A_CONTROLLED_CREATURE_P1P1 = new FilterControlledCreaturePermanent("a creature you control with a +1/+1 counter on it");
static {

View file

@ -0,0 +1,32 @@
package mage.filter.common;
import mage.abilities.keyword.SuspendAbility;
import mage.counters.CounterType;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.AbilityPredicate;
/**
* @author skiwkr
* 702.62b. A card is "suspended" if it's in the exile zone, has suspend, and has a time counter on it.
*/
public class FilterSuspendedCard extends FilterCard {
public FilterSuspendedCard() {
this("suspended card");
}
public FilterSuspendedCard(String name) {
super(name);
this.add(new AbilityPredicate(SuspendAbility.class));
this.add(CounterType.TIME.getPredicate());
}
protected FilterSuspendedCard(final FilterSuspendedCard filter) {
super(filter);
}
@Override
public FilterSuspendedCard copy() {
return new FilterSuspendedCard(this);
}
}

View file

@ -0,0 +1,22 @@
package mage.filter.predicate.mageobject;
import mage.MageObject;
import mage.filter.predicate.Predicate;
import mage.game.Game;
/**
* @author TheElk801
*/
public enum OutlawPredicate implements Predicate<MageObject> {
instance;
@Override
public boolean apply(MageObject input, Game game) {
return input.isOutlaw(game);
}
@Override
public String toString() {
return "Outlaw";
}
}

View file

@ -0,0 +1,61 @@
package mage.filter.predicate.mageobject;
import mage.abilities.Mode;
import mage.filter.FilterPermanent;
import mage.filter.FilterPlayer;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.stack.StackObject;
import mage.players.Player;
import mage.target.Target;
import java.util.UUID;
/**
* @author Susucr
*/
public class TargetsPermanentOrPlayerPredicate implements ObjectSourcePlayerPredicate<StackObject> {
private final FilterPermanent targetFilterPermanent;
private final FilterPlayer targetFilterPlayer;
public TargetsPermanentOrPlayerPredicate(FilterPermanent targetFilterPermanent, FilterPlayer targetFilterPlayer) {
this.targetFilterPermanent = targetFilterPermanent;
this.targetFilterPlayer = targetFilterPlayer;
}
@Override
public boolean apply(ObjectSourcePlayer<StackObject> input, Game game) {
StackObject object = game.getStack().getStackObject(input.getObject().getId());
if (object != null) {
for (UUID modeId : object.getStackAbility().getModes().getSelectedModes()) {
Mode mode = object.getStackAbility().getModes().get(modeId);
for (Target target : mode.getTargets()) {
if (target.isNotTarget()) {
continue;
}
for (UUID targetId : target.getTargets()) {
// Try for permanent
Permanent permanent = game.getPermanent(targetId);
if (targetFilterPermanent.match(permanent, input.getPlayerId(), input.getSource(), game)) {
return true;
}
// Try for player
Player player = game.getPlayer(targetId);
if (targetFilterPlayer.match(player, input.getPlayerId(), input.getSource(), game)) {
return true;
}
}
}
}
}
return false;
}
@Override
public String toString() {
return "that targets a " + targetFilterPermanent.getMessage() + " or " + targetFilterPlayer.getMessage();
}
}

View file

@ -1,6 +1,5 @@
package mage.filter.predicate.mageobject;
import mage.MageObject;
import mage.abilities.Mode;
import mage.filter.FilterPermanent;
import mage.filter.predicate.ObjectSourcePlayer;
@ -15,7 +14,7 @@ import java.util.UUID;
/**
* @author LoneFox
*/
public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate<MageObject> {
public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate<StackObject> {
private final FilterPermanent targetFilter;
@ -24,7 +23,7 @@ public class TargetsPermanentPredicate implements ObjectSourcePlayerPredicate<Ma
}
@Override
public boolean apply(ObjectSourcePlayer<MageObject> input, Game game) {
public boolean apply(ObjectSourcePlayer<StackObject> input, Game game) {
StackObject object = game.getStack().getStackObject(input.getObject().getId());
if (object != null) {
for (UUID modeId : object.getStackAbility().getModes().getSelectedModes()) {

View file

@ -1,8 +1,7 @@
package mage.filter.predicate.mageobject;
import java.util.UUID;
import mage.MageObject;
import mage.abilities.Mode;
import mage.filter.FilterPlayer;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.game.Game;
@ -10,25 +9,34 @@ import mage.game.stack.StackObject;
import mage.players.Player;
import mage.target.Target;
/**
*
* @author jeffwadsworth
*/
public class TargetsPlayerPredicate implements ObjectSourcePlayerPredicate<MageObject> {
import java.util.UUID;
public TargetsPlayerPredicate() {
/**
* @author jeffwadsworth, Susucr
*/
public class TargetsPlayerPredicate implements ObjectSourcePlayerPredicate<StackObject> {
private final FilterPlayer targetFilter;
public TargetsPlayerPredicate(FilterPlayer targetFilter) {
this.targetFilter = targetFilter;
}
@Override
public boolean apply(ObjectSourcePlayer<MageObject> input, Game game) {
public boolean apply(ObjectSourcePlayer<StackObject> input, Game game) {
StackObject object = game.getStack().getStackObject(input.getObject().getId());
if (object != null) {
for (UUID modeId : object.getStackAbility().getModes().getSelectedModes()) {
Mode mode = object.getStackAbility().getModes().get(modeId);
for (Target target : mode.getTargets()) {
if (target.isNotTarget()) {
continue;
}
for (UUID targetId : target.getTargets()) {
Player player = game.getPlayer(targetId);
return player != null;
if (targetFilter.match(player, input.getPlayerId(), input.getSource(), game)) {
return true;
}
}
}
}
@ -38,6 +46,6 @@ public class TargetsPlayerPredicate implements ObjectSourcePlayerPredicate<MageO
@Override
public String toString() {
return "that targets a player";
return "that targets a " + targetFilter.getMessage();
}
}

View file

@ -0,0 +1,23 @@
package mage.filter.predicate.other;
import mage.cards.Card;
import mage.constants.Zone;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.game.Game;
import mage.game.stack.Spell;
public enum SpellCastFromAnywhereOtherThanHand implements ObjectSourcePlayerPredicate<Card> {
instance;
@Override
public boolean apply(ObjectSourcePlayer<Card> input, Game game) {
if (input.getObject() instanceof Spell) {
return !input.getObject().isOwnedBy(input.getPlayerId())
|| !Zone.HAND.match(((Spell) input.getObject()).getFromZone());
} else {
return !input.getObject().isOwnedBy(input.getPlayerId())
|| !Zone.HAND.match(game.getState().getZone(input.getObject().getId()));
}
}
}

View file

@ -0,0 +1,28 @@
package mage.filter.predicate.permanent;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.watchers.common.SaddledMountWatcher;
/**
* requires SaddledMountWatcher
*
* @author TheElk801
*/
public enum SaddledSourceThisTurnPredicate implements ObjectSourcePlayerPredicate<Permanent> {
instance;
@Override
public boolean apply(ObjectSourcePlayer<Permanent> input, Game game) {
return SaddledMountWatcher.checkIfSaddledThisTurn(
input.getObject(), input.getSource().getSourcePermanentOrLKI(game), game
);
}
@Override
public String toString() {
return "saddled {this} this turn";
}
}

View file

@ -810,67 +810,97 @@ public class GameState implements Serializable, Copyable<GameState> {
public void addSimultaneousDamage(DamagedEvent damagedEvent, Game game) {
// Combine multiple damage events in the single event (batch)
// * per damage type (see GameEvent.DAMAGED_BATCH_FOR_PERMANENTS, GameEvent.DAMAGED_BATCH_FOR_PLAYERS)
// * per player (see GameEvent.DAMAGED_BATCH_FOR_ONE_PLAYER)
// * per permanent (see GameEvent.DAMAGED_BATCH_FOR_ONE_PERMANENT)
//
// Warning, one event can be stored in multiple batches,
// example: DAMAGED_BATCH_FOR_PLAYERS + DAMAGED_BATCH_FOR_ONE_PLAYER
// Note: one event can be stored in multiple batches
if (damagedEvent instanceof DamagedPlayerEvent) {
// DAMAGED_BATCH_FOR_PLAYERS + DAMAGED_BATCH_FOR_ONE_PLAYER
addSimultaneousDamageToPlayerBatches((DamagedPlayerEvent) damagedEvent, game);
} else if (damagedEvent instanceof DamagedPermanentEvent) {
// DAMAGED_BATCH_FOR_PERMANENTS + DAMAGED_BATCH_FOR_ONE_PERMANENT
addSimultaneousDamageToPermanentBatches((DamagedPermanentEvent) damagedEvent, game);
}
// DAMAGED_BATCH_FOR_ALL
addSimultaneousDamageToBatchForAll(damagedEvent, game);
}
boolean isPlayerDamage = damagedEvent instanceof DamagedPlayerEvent;
boolean isPermanentDamage = damagedEvent instanceof DamagedPermanentEvent;
// existing batch
boolean isDamageBatchUsed = false;
public void addSimultaneousDamageToPlayerBatches(DamagedPlayerEvent damagedPlayerEvent, Game game) {
// find existing batches first
boolean isTotalBatchUsed = false;
boolean isPlayerBatchUsed = false;
boolean isPermanentBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (isPlayerDamage && event instanceof DamagedBatchForOnePlayerEvent) {
// per player
DamagedBatchForOnePlayerEvent oldPlayerBatch = (DamagedBatchForOnePlayerEvent) event;
if (oldPlayerBatch.getDamageClazz().isInstance(damagedEvent)
&& event.getPlayerId().equals(damagedEvent.getTargetId())) {
oldPlayerBatch.addEvent(damagedEvent);
isPlayerBatchUsed = true;
}
} else if (isPermanentDamage && event instanceof DamagedBatchForOnePermanentEvent) {
// per permanent
DamagedBatchForOnePermanentEvent oldPermanentBatch = (DamagedBatchForOnePermanentEvent) event;
if (oldPermanentBatch.getDamageClazz().isInstance(damagedEvent)
&& CardUtil.getEventTargets(event).contains(damagedEvent.getTargetId())) {
oldPermanentBatch.addEvent(damagedEvent);
isPermanentBatchUsed = true;
}
} else if ((event instanceof DamagedBatchEvent)
&& ((DamagedBatchEvent) event).getDamageClazz().isInstance(damagedEvent)) {
// per damage type
// If the batch event isn't DAMAGED_BATCH_FOR_ONE_PLAYER, the targetIDs need not match,
// since "event" is a generic batch in this case
// (either DAMAGED_BATCH_FOR_PERMANENTS or DAMAGED_BATCH_FOR_PLAYERS)
// Just needs to be a permanent-damaging event for DAMAGED_BATCH_FOR_PERMANENTS,
// or a player-damaging event for DAMAGED_BATCH_FOR_PLAYERS
((DamagedBatchEvent) event).addEvent(damagedEvent);
isDamageBatchUsed = true;
if (event instanceof DamagedBatchForPlayersEvent) {
((DamagedBatchForPlayersEvent) event).addEvent(damagedPlayerEvent);
isTotalBatchUsed = true;
} else if (event instanceof DamagedBatchForOnePlayerEvent
&& damagedPlayerEvent.getTargetId().equals(event.getTargetId())) {
((DamagedBatchForOnePlayerEvent) event).addEvent(damagedPlayerEvent);
isPlayerBatchUsed = true;
}
}
// new batch
if (!isDamageBatchUsed) {
addSimultaneousEvent(DamagedBatchEvent.makeEvent(damagedEvent), game);
// new batches if necessary
if (!isTotalBatchUsed) {
addSimultaneousEvent(new DamagedBatchForPlayersEvent(damagedPlayerEvent), game);
}
if (!isPlayerBatchUsed && isPlayerDamage) {
DamagedBatchEvent event = new DamagedBatchForOnePlayerEvent(damagedEvent);
addSimultaneousEvent(event, game);
}
if (!isPermanentBatchUsed && isPermanentDamage) {
DamagedBatchEvent event = new DamagedBatchForOnePermanentEvent(damagedEvent);
addSimultaneousEvent(event, game);
if (!isPlayerBatchUsed) {
addSimultaneousEvent(new DamagedBatchForOnePlayerEvent(damagedPlayerEvent), game);
}
}
public void addSimultaneousTapped(TappedEvent tappedEvent, Game game) {
public void addSimultaneousDamageToPermanentBatches(DamagedPermanentEvent damagedPermanentEvent, Game game) {
// find existing batches first
boolean isTotalBatchUsed = false;
boolean isSingleBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (event instanceof DamagedBatchForPermanentsEvent) {
((DamagedBatchForPermanentsEvent) event).addEvent(damagedPermanentEvent);
isTotalBatchUsed = true;
} else if (event instanceof DamagedBatchForOnePermanentEvent
&& damagedPermanentEvent.getTargetId().equals(event.getTargetId())) {
((DamagedBatchForOnePermanentEvent) event).addEvent(damagedPermanentEvent);
isSingleBatchUsed = true;
}
}
// new batches if necessary
if (!isTotalBatchUsed) {
addSimultaneousEvent(new DamagedBatchForPermanentsEvent(damagedPermanentEvent), game);
}
if (!isSingleBatchUsed) {
addSimultaneousEvent(new DamagedBatchForOnePermanentEvent(damagedPermanentEvent), game);
}
}
public void addSimultaneousDamageToBatchForAll(DamagedEvent damagedEvent, Game game) {
boolean isBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (event instanceof DamagedBatchAllEvent) {
((DamagedBatchAllEvent) event).addEvent(damagedEvent);
isBatchUsed = true;
}
}
if (!isBatchUsed) {
addSimultaneousEvent(new DamagedBatchAllEvent(damagedEvent), game);
}
}
public void addSimultaneousLifeLossToBatch(LifeLostEvent lifeLossEvent, Game game) {
// Combine multiple life loss events in the single event (batch)
// see GameEvent.LOST_LIFE_BATCH
// existing batch
boolean isLifeLostBatchUsed = false;
for (GameEvent event : simultaneousEvents) {
if (event instanceof LifeLostBatchEvent) {
((LifeLostBatchEvent) event).addEvent(lifeLossEvent);
isLifeLostBatchUsed = true;
}
}
// new batch
if (!isLifeLostBatchUsed) {
addSimultaneousEvent(new LifeLostBatchEvent(lifeLossEvent), game);
}
}
public void addSimultaneousTappedToBatch(TappedEvent tappedEvent, Game game) {
// Combine multiple tapped events in the single event (batch)
boolean isTappedBatchUsed = false;
@ -885,13 +915,11 @@ public class GameState implements Serializable, Copyable<GameState> {
// new batch
if (!isTappedBatchUsed) {
TappedBatchEvent batch = new TappedBatchEvent();
batch.addEvent(tappedEvent);
addSimultaneousEvent(batch, game);
addSimultaneousEvent(new TappedBatchEvent(tappedEvent), game);
}
}
public void addSimultaneousUntapped(UntappedEvent untappedEvent, Game game) {
public void addSimultaneousUntappedToBatch(UntappedEvent untappedEvent, Game game) {
// Combine multiple untapped events in the single event (batch)
boolean isUntappedBatchUsed = false;
@ -906,9 +934,7 @@ public class GameState implements Serializable, Copyable<GameState> {
// new batch
if (!isUntappedBatchUsed) {
UntappedBatchEvent batch = new UntappedBatchEvent();
batch.addEvent(untappedEvent);
addSimultaneousEvent(batch, game);
addSimultaneousEvent(new UntappedBatchEvent(untappedEvent), game);
}
}

View file

@ -334,13 +334,19 @@ public final class ZonesHandler {
isGoodToMove = true;
} else if (event.getToZone().equals(Zone.BATTLEFIELD)) {
// non-permanents can't move to battlefield
// "return to battlefield transformed" abilities uses game state value instead "info.transformed", so check it too
// TODO: possible bug with non permanent on second side like Life // Death, see https://github.com/magefree/mage/issues/11573
// need to check second side here, not status only
// TODO: possible bug with Nightbound, search all usage of getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED and insert additional check Ability.checkCard
boolean wantToPutTransformed = card.isTransformable()
&& Boolean.TRUE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()));
isGoodToMove = card.isPermanent(game) || wantToPutTransformed;
/*
* 712.14a. If a spell or ability puts a transforming double-faced card onto the battlefield "transformed"
* or "converted," it enters the battlefield with its back face up. If a player is instructed to put a card
* that isn't a transforming double-faced card onto the battlefield transformed or converted, that card stays in
* its current zone.
*/
boolean wantToTransform = Boolean.TRUE.equals(game.getState().getValue(TransformAbility.VALUE_KEY_ENTER_TRANSFORMED + card.getId()));
if (wantToTransform) {
isGoodToMove = card.isTransformable() && card.getSecondCardFace().isPermanent(game);
} else {
isGoodToMove = card.isPermanent(game);
}
} else {
// other zones allows to move
isGoodToMove = true;

View file

@ -47,7 +47,7 @@ class JayaBallardCastFromGraveyardEffect extends AsThoughEffectImpl {
JayaBallardCastFromGraveyardEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.Benefit);
staticText = "You may cast instant and sorcery cards from your graveyard";
staticText = "You may cast instant and sorcery spells from your graveyard";
}
JayaBallardCastFromGraveyardEffect(final JayaBallardCastFromGraveyardEffect effect) {
@ -83,7 +83,7 @@ class JayaBallardReplacementEffect extends ReplacementEffectImpl {
public JayaBallardReplacementEffect() {
super(Duration.EndOfGame, Outcome.Exile);
staticText = "If a card cast this way would be put into a graveyard this turn, exile it instead";
staticText = "If a spell cast this way would be put into a graveyard this turn, exile it instead";
}
protected JayaBallardReplacementEffect(final JayaBallardReplacementEffect effect) {

View file

@ -0,0 +1,101 @@
package mage.game.events;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Special events created by game engine to track batches of events that occur simultaneously,
* for triggers that need such information
* @author xenohedron
*/
public abstract class BatchEvent<T extends GameEvent> extends GameEvent {
private final Set<T> events = new HashSet<>();
private final boolean singleTargetId;
/**
* @param eventType specific type of event
* @param singleTargetId if true, all included events must have same target id
* @param firstEvent added to initialize the batch (batch is never empty)
*/
protected BatchEvent(EventType eventType, boolean singleTargetId, T firstEvent) {
super(eventType, (singleTargetId ? firstEvent.getTargetId() : null), null, null);
this.singleTargetId = singleTargetId;
if (firstEvent instanceof BatchEvent) { // sanity check, if you need it then think twice and research carefully
throw new UnsupportedOperationException("Wrong code usage: nesting batch events not supported");
}
this.addEvent(firstEvent);
}
/**
* For alternate event structure logic used by ZoneChangeBatchEvent, list of events starts empty.
*/
protected BatchEvent(EventType eventType) {
super(eventType, null, null, null);
this.singleTargetId = false;
}
public void addEvent(T event) {
if (singleTargetId && !getTargetId().equals(event.getTargetId())) {
throw new IllegalStateException("Wrong code usage. Batch event initiated with single target id, but trying to add event with different target id");
}
this.events.add(event);
}
public Set<T> getEvents() {
return events;
}
public Set<UUID> getTargetIds() {
return events.stream()
.map(GameEvent::getTargetId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
public Set<UUID> getSourceIds() {
return events.stream()
.map(GameEvent::getSourceId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
public Set<UUID> getPlayerIds() {
return events.stream()
.map(GameEvent::getPlayerId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
@Override
public int getAmount() {
return events
.stream()
.mapToInt(GameEvent::getAmount)
.sum();
}
@Override // events can store a diff value, so search it from events list instead
public UUID getTargetId() {
if (singleTargetId) {
return super.getTargetId();
}
throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list or use CardUtil.getEventTargets(event)");
}
@Override // events can store a diff value, so search it from events list instead
@Deprecated // no use case currently supported
public UUID getSourceId() {
throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list");
}
@Override // events can store a diff value, so search it from events list instead
@Deprecated // no use case currently supported
public UUID getPlayerId() {
throw new IllegalStateException("Wrong code usage. Must search value from a getEvents list");
}
}

View file

@ -1,19 +0,0 @@
package mage.game.events;
import java.util.Set;
import java.util.UUID;
/**
* Game event with batch support (batch is an event that can contain multiple events, example: DAMAGED_BATCH_FOR_PLAYERS)
* <p>
* Used by game engine to support event lifecycle for triggers
*
* @author JayDi85
*/
public interface BatchGameEvent<T extends GameEvent> {
Set<T> getEvents();
Set<UUID> getTargets();
}

View file

@ -0,0 +1,11 @@
package mage.game.events;
/**
* @author xenohedron
*/
public class DamagedBatchAllEvent extends BatchEvent<DamagedEvent> {
public DamagedBatchAllEvent(DamagedEvent firstEvent) {
super(EventType.DAMAGED_BATCH_FOR_ALL, false, firstEvent);
}
}

Some files were not shown because too many files have changed in this diff Show more