mirror of
https://github.com/magefree/mage.git
synced 2026-01-09 12:22:10 -08:00
Merge branch 'magefree:master' into case-of-the-pilfered-proof
This commit is contained in:
commit
9cf6119c7e
797 changed files with 36049 additions and 3584 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(" — ");
|
||||
} else {
|
||||
sb.append("&bull ");
|
||||
}
|
||||
sb.append(mode.getEffects().getTextStartingUpperCase(mode));
|
||||
sb.append("<br>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(".")) {
|
||||
|
|
|
|||
|
|
@ -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(".")) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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, ");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
325
Mage/src/main/java/mage/abilities/keyword/PlotAbility.java
Normal file
325
Mage/src/main/java/mage/abilities/keyword/PlotAbility.java
Normal 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();
|
||||
}
|
||||
}
|
||||
179
Mage/src/main/java/mage/abilities/keyword/SaddleAbility.java
Normal file
179
Mage/src/main/java/mage/abilities/keyword/SaddleAbility.java
Normal 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);
|
||||
}
|
||||
}
|
||||
28
Mage/src/main/java/mage/abilities/keyword/SpreeAbility.java
Normal file
28
Mage/src/main/java/mage/abilities/keyword/SpreeAbility.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ public class CardNameUtil {
|
|||
.replace("ü", "u")
|
||||
.replace("É", "E")
|
||||
.replace("ñ", "n")
|
||||
.replace("®", "");
|
||||
.replace("®", "")
|
||||
.replace("—", "");
|
||||
}
|
||||
|
||||
private CardNameUtil() {
|
||||
|
|
|
|||
29
Mage/src/main/java/mage/constants/CastManaAdjustment.java
Normal file
29
Mage/src/main/java/mage/constants/CastManaAdjustment.java
Normal 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,
|
||||
}
|
||||
|
|
@ -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?!
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ public enum CounterType {
|
|||
LANDMARK("landmark"),
|
||||
LEVEL("level"),
|
||||
LIFELINK("lifelink"),
|
||||
LOOT("loot"),
|
||||
LORE("lore"),
|
||||
LUCK("luck"),
|
||||
LOYALTY("loyalty"),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
101
Mage/src/main/java/mage/game/events/BatchEvent.java
Normal file
101
Mage/src/main/java/mage/game/events/BatchEvent.java
Normal 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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue