forked from External/mage
Merge branch 'External-master'
All checks were successful
/ build_release (push) Successful in 27m57s
All checks were successful
/ build_release (push) Successful in 27m57s
This commit is contained in:
commit
d2c32ec53f
435 changed files with 12052 additions and 2085 deletions
|
|
@ -126,7 +126,7 @@ public interface Ability extends Controllable, Serializable {
|
|||
/**
|
||||
* Gets all {@link ManaCosts} associated with this ability. These returned
|
||||
* costs should never be modified as they represent the base costs before
|
||||
* any modifications.
|
||||
* any modifications (only cost adjusters can change it, e.g. set min/max values)
|
||||
*
|
||||
* @return All {@link ManaCosts} that must be paid.
|
||||
*/
|
||||
|
|
@ -151,6 +151,17 @@ public interface Ability extends Controllable, Serializable {
|
|||
|
||||
void addManaCostsToPay(ManaCost manaCost);
|
||||
|
||||
/**
|
||||
* Helper method to setup actual min/max limits of current X costs BEFORE player's X announcement
|
||||
*/
|
||||
void setVariableCostsMinMax(int min, int max);
|
||||
|
||||
/**
|
||||
* Helper method to replace X by direct value BEFORE player's X announcement
|
||||
* If you need additional target for X then use CostAdjuster + EarlyTargetCost (example: Bargaining Table)
|
||||
*/
|
||||
void setVariableCostsValue(int xValue);
|
||||
|
||||
/**
|
||||
* Gets a map of the cost tags (set while casting/activating) of this ability, can be null if no tags have been set yet.
|
||||
* Does NOT return the source permanent's tags.
|
||||
|
|
@ -356,7 +367,7 @@ public interface Ability extends Controllable, Serializable {
|
|||
* - for dies triggers - override and use TriggeredAbilityImpl.isInUseableZoneDiesTrigger inside + set setLeavesTheBattlefieldTrigger(true)
|
||||
*
|
||||
* @param sourceObject can be null for static continues effects checking like rules modification (example: Yixlid Jailer)
|
||||
* @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder)
|
||||
* @param event can be null for state base effects checking like "when you control seven or more" (example: Endrek Sahr, Master Breeder)
|
||||
*/
|
||||
boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event);
|
||||
|
||||
|
|
@ -533,11 +544,25 @@ public interface Ability extends Controllable, Serializable {
|
|||
|
||||
void adjustTargets(Game game);
|
||||
|
||||
/**
|
||||
* Dynamic X and cost modification, see CostAdjuster for more details on usage
|
||||
*/
|
||||
Ability setCostAdjuster(CostAdjuster costAdjuster);
|
||||
|
||||
CostAdjuster getCostAdjuster();
|
||||
/**
|
||||
* Prepare {X} settings for announce
|
||||
*/
|
||||
void adjustX(Game game);
|
||||
|
||||
void adjustCosts(Game game);
|
||||
/**
|
||||
* Prepare costs (generate due game state or announce)
|
||||
*/
|
||||
void adjustCostsPrepare(Game game);
|
||||
|
||||
/**
|
||||
* Apply additional cost modifications logic/effects
|
||||
*/
|
||||
void adjustCostsModify(Game game, CostModificationType costModificationType);
|
||||
|
||||
List<Hint> getHints();
|
||||
|
||||
|
|
|
|||
|
|
@ -291,6 +291,8 @@ public abstract class AbilityImpl implements Ability {
|
|||
|
||||
// fused or spliced spells contain multiple abilities (e.g. fused, left, right)
|
||||
// optional costs and cost modification must be applied only to the first/main ability
|
||||
// TODO: need tests with X announced costs, cost modification effects, CostAdjuster, early cost target, etc
|
||||
// can be bugged due multiple calls (not all code parts below use isMainPartAbility)
|
||||
boolean isMainPartAbility = !CardUtil.isFusedPartAbility(this, game);
|
||||
|
||||
/* 20220908 - 601.2b
|
||||
|
|
@ -326,6 +328,16 @@ public abstract class AbilityImpl implements Ability {
|
|||
return false;
|
||||
}
|
||||
|
||||
// 20241022 - 601.2b
|
||||
// Choose targets for costs that have to be chosen early
|
||||
// Not yet included in 601.2b but this is where it will be
|
||||
handleChooseCostTargets(game, controller);
|
||||
|
||||
// prepare dynamic costs (must be called before any x announce)
|
||||
if (isMainPartAbility) {
|
||||
adjustX(game);
|
||||
}
|
||||
|
||||
// 20121001 - 601.2b
|
||||
// If the spell has a variable cost that will be paid as it's being cast (such as an {X} in
|
||||
// its mana cost; see rule 107.3), the player announces the value of that variable.
|
||||
|
|
@ -402,7 +414,7 @@ public abstract class AbilityImpl implements Ability {
|
|||
// Note: ActivatedAbility does include SpellAbility & PlayLandAbility, but those should be able to be canceled too.
|
||||
boolean canCancel = this instanceof ActivatedAbility && controller.isHuman();
|
||||
if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) {
|
||||
// was canceled during targer selection
|
||||
// was canceled during target selection
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -428,7 +440,7 @@ public abstract class AbilityImpl implements Ability {
|
|||
|
||||
//20101001 - 601.2e
|
||||
if (isMainPartAbility) {
|
||||
adjustCosts(game); // still needed for CostAdjuster objects (to handle some types of dynamic costs)
|
||||
// adjustX already called before any announces
|
||||
game.getContinuousEffects().costModification(this, game);
|
||||
}
|
||||
|
||||
|
|
@ -483,6 +495,7 @@ public abstract class AbilityImpl implements Ability {
|
|||
// A player can't apply two alternative methods of casting or two alternative costs to a single spell.
|
||||
switch (((SpellAbility) this).getSpellAbilityCastMode()) {
|
||||
case FLASHBACK:
|
||||
case HARMONIZE:
|
||||
case MADNESS:
|
||||
case TRANSFORMED:
|
||||
case DISTURB:
|
||||
|
|
@ -726,6 +739,11 @@ public abstract class AbilityImpl implements Ability {
|
|||
((EarlyTargetCost) cost).chooseTarget(game, this, controller);
|
||||
}
|
||||
}
|
||||
for (ManaCost cost : getManaCostsToPay()) {
|
||||
if (cost instanceof EarlyTargetCost && cost.getTargets().isEmpty()) {
|
||||
((EarlyTargetCost) cost).chooseTarget(game, this, controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -764,8 +782,15 @@ public abstract class AbilityImpl implements Ability {
|
|||
if (!variableManaCost.isPaid()) { // should only happen for human players
|
||||
int xValue;
|
||||
if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) {
|
||||
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(),
|
||||
"Announce the value for " + variableManaCost.getText(), game, this);
|
||||
if (variableManaCost.wasAnnounced()) {
|
||||
// announce by rules
|
||||
xValue = variableManaCost.getAmount();
|
||||
} else {
|
||||
// announce by player
|
||||
xValue = controller.announceXMana(variableManaCost.getMinX(), variableManaCost.getMaxX(),
|
||||
"Announce the value for " + variableManaCost.getText(), game, this);
|
||||
}
|
||||
|
||||
int amountMana = xValue * variableManaCost.getXInstancesCount();
|
||||
StringBuilder manaString = threadLocalBuilder.get();
|
||||
if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) {
|
||||
|
|
@ -1072,6 +1097,60 @@ public abstract class AbilityImpl implements Ability {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariableCostsMinMax(int min, int max) {
|
||||
// modify all values (mtg rules allow only one type of X, so min/max must be shared between all X instances)
|
||||
|
||||
// base cost
|
||||
for (ManaCost cost : getManaCosts()) {
|
||||
if (cost instanceof MinMaxVariableCost) {
|
||||
MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost;
|
||||
minMaxCost.setMinX(min);
|
||||
minMaxCost.setMaxX(max);
|
||||
}
|
||||
}
|
||||
|
||||
// prepared cost
|
||||
for (ManaCost cost : getManaCostsToPay()) {
|
||||
if (cost instanceof MinMaxVariableCost) {
|
||||
MinMaxVariableCost minMaxCost = (MinMaxVariableCost) cost;
|
||||
minMaxCost.setMinX(min);
|
||||
minMaxCost.setMaxX(max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariableCostsValue(int xValue) {
|
||||
// only mana cost supported
|
||||
|
||||
// base cost
|
||||
boolean foundBaseCost = false;
|
||||
for (ManaCost cost : getManaCosts()) {
|
||||
if (cost instanceof VariableManaCost) {
|
||||
foundBaseCost = true;
|
||||
((VariableManaCost) cost).setMinX(xValue);
|
||||
((VariableManaCost) cost).setMaxX(xValue);
|
||||
((VariableManaCost) cost).setAmount(xValue, xValue, false);
|
||||
}
|
||||
}
|
||||
|
||||
// prepared cost
|
||||
boolean foundPreparedCost = false;
|
||||
for (ManaCost cost : getManaCostsToPay()) {
|
||||
if (cost instanceof VariableManaCost) {
|
||||
foundPreparedCost = true;
|
||||
((VariableManaCost) cost).setMinX(xValue);
|
||||
((VariableManaCost) cost).setMaxX(xValue);
|
||||
((VariableManaCost) cost).setAmount(xValue, xValue, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundPreparedCost || !foundBaseCost) {
|
||||
throw new IllegalArgumentException("Wrong code usage: auto-announced X values allowed in mana costs only");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEffect(Effect effect) {
|
||||
if (effect != null) {
|
||||
|
|
@ -1676,17 +1755,6 @@ public abstract class AbilityImpl implements Ability {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic cost modification for ability.<br>
|
||||
* Example: if it need stack related info (like real targets) then must
|
||||
* check two states (game.inCheckPlayableState): <br>
|
||||
* 1. In playable state it must check all possible use cases (e.g. allow to
|
||||
* reduce on any available target and modes) <br>
|
||||
* 2. In real cast state it must check current use case (e.g. real selected
|
||||
* targets and modes)
|
||||
*
|
||||
* @param costAdjuster
|
||||
*/
|
||||
@Override
|
||||
public AbilityImpl setCostAdjuster(CostAdjuster costAdjuster) {
|
||||
this.costAdjuster = costAdjuster;
|
||||
|
|
@ -1694,14 +1762,23 @@ public abstract class AbilityImpl implements Ability {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CostAdjuster getCostAdjuster() {
|
||||
return costAdjuster;
|
||||
public void adjustX(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.prepareX(this, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Game game) {
|
||||
public void adjustCostsPrepare(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.adjustCosts(this, game);
|
||||
costAdjuster.prepareCost(this, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustCostsModify(Game game, CostModificationType costModificationType) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.modifyCost(this, game, costModificationType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import mage.MageObject;
|
|||
import mage.abilities.costs.mana.ManaCost;
|
||||
import mage.abilities.costs.mana.VariableManaCost;
|
||||
import mage.abilities.keyword.FlashAbility;
|
||||
import mage.cards.AdventureCardSpell;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.SpellOptionCard;
|
||||
import mage.cards.SplitCard;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
|
|
@ -99,7 +99,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
|
|||
// forced to cast (can be part id or main id)
|
||||
Set<UUID> idsToCheck = new HashSet<>();
|
||||
idsToCheck.add(object.getId());
|
||||
if (object instanceof Card && !(object instanceof AdventureCardSpell)) {
|
||||
if (object instanceof Card && !(object instanceof SpellOptionCard)) {
|
||||
idsToCheck.add(((Card) object).getMainCard().getId());
|
||||
}
|
||||
for (UUID idToCheck : idsToCheck) {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ public class BecomesTargetAnyTriggeredAbility extends TriggeredAbilityImpl {
|
|||
if (permanent == null || !filterTarget.match(permanent, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ public class BecomesTargetAttachedTriggeredAbility extends TriggeredAbilityImpl
|
|||
if (enchantment == null || enchantment.getAttachedTo() == null || !event.getTargetId().equals(enchantment.getAttachedTo())) {
|
||||
return false;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class BecomesTargetControllerTriggeredAbility extends TriggeredAbilityImp
|
|||
return false;
|
||||
}
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !filterStack.match(targetingObject, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public class BecomesTargetSourceTriggeredAbility extends TriggeredAbilityImpl {
|
|||
if (!event.getTargetId().equals(getSourceId())) {
|
||||
return false;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !filter.match(targetingObject, getControllerId(), this, game)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
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.game.stack.Spell;
|
||||
import mage.watchers.common.SpellsCastWatcher;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author balazskristof, Jmlundeen
|
||||
*/
|
||||
public enum CastAnotherSpellThisTurnCondition implements Condition {
|
||||
instance;
|
||||
private final Hint hint = new ConditionHint(
|
||||
this, "You've cast another spell this turn"
|
||||
);
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class);
|
||||
if (watcher == null) {
|
||||
return false;
|
||||
}
|
||||
List<Spell> spells = watcher.getSpellsCastThisTurn(source.getControllerId());
|
||||
return spells != null && spells
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.anyMatch(spell -> !spell.getSourceId().equals(source.getSourceId()) || spell.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter());
|
||||
}
|
||||
|
||||
public Hint getHint() {
|
||||
return hint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "you've cast another spell this turn";
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ package mage.abilities.condition.common;
|
|||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.cards.AdventureCardSpell;
|
||||
import mage.cards.SpellOptionCard;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.ModalDoubleFacedCardHalf;
|
||||
import mage.cards.SplitCardHalf;
|
||||
|
|
@ -20,7 +20,7 @@ public enum IsBeingCastFromHandCondition implements Condition {
|
|||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
MageObject object = game.getObject(source);
|
||||
if (object instanceof SplitCardHalf || object instanceof AdventureCardSpell || object instanceof ModalDoubleFacedCardHalf) {
|
||||
if (object instanceof SplitCardHalf || object instanceof SpellOptionCard || object instanceof ModalDoubleFacedCardHalf) {
|
||||
UUID mainCardId = ((Card) object).getMainCard().getId();
|
||||
object = game.getObject(mainCardId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
|
||||
package mage.abilities.condition.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.game.Game;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public class ModeChoiceSourceCondition implements Condition {
|
||||
|
||||
private final String mode;
|
||||
|
||||
|
||||
public ModeChoiceSourceCondition(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
String chosenMode = (String) game.getState().getValue(source.getSourceId() + "_modeChoice");
|
||||
return chosenMode != null && chosenMode.equals(mode);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package mage.abilities.condition.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.game.Game;
|
||||
import mage.watchers.common.SpellsCastWatcher;
|
||||
|
||||
/**
|
||||
* @author androosss
|
||||
*/
|
||||
public enum YouCastExactOneSpellThisTurnCondition implements Condition {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class);
|
||||
return watcher != null && watcher.getSpellsCastThisTurn(source.getControllerId()).size() == 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,86 @@
|
|||
package mage.abilities.costs;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.CostModificationType;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
* Dynamic costs implementation to control {X} or other costs, can be used in spells and abilities
|
||||
* <p>
|
||||
* Possible use cases:
|
||||
* - define {X} costs like X cards to discard (mana and non-mana values);
|
||||
* - define {X} limits before announce (to help in UX and AI logic);
|
||||
* - define any dynamic costs;
|
||||
* - use as simple cost increase/reduce effect;
|
||||
* <p>
|
||||
* Calls order by game engine:
|
||||
* - ... early cost target selection for EarlyTargetCost ...
|
||||
* - prepareX
|
||||
* - ... x announce ...
|
||||
* - prepareCost
|
||||
* - increaseCost
|
||||
* - reduceCost
|
||||
* - ... normal target selection and payment ...
|
||||
*
|
||||
* @author TheElk801, JayDi85
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CostAdjuster extends Serializable {
|
||||
|
||||
/**
|
||||
* Must check playable and real cast states.
|
||||
* Example: if it need stack related info (like real targets) then must check two states (game.inCheckPlayableState):
|
||||
* 1. In playable state it must check all possible use cases (e.g. allow to reduce on any available target and modes)
|
||||
* 2. In real cast state it must check current use case (e.g. real selected targets and modes)
|
||||
*
|
||||
* @param ability
|
||||
* @param game
|
||||
* Prepare {X} costs settings or define auto-announced mana values
|
||||
* <p>
|
||||
* Usage example:
|
||||
* - define auto-announced mana value {X} by ability.setVariableCostsValue
|
||||
* - define possible {X} settings by ability.setVariableCostsMinMax
|
||||
*/
|
||||
void adjustCosts(Ability ability, Game game);
|
||||
default void prepareX(Ability ability, Game game) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare any dynamic costs
|
||||
* <p>
|
||||
* Usage example:
|
||||
* - add real cost after {X} mana value announce by CardUtil.getSourceCostsTagX
|
||||
* - add dynamic cost from game data
|
||||
*/
|
||||
default void prepareCost(Ability ability, Game game) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple cost reduction effect
|
||||
*/
|
||||
default void reduceCost(Ability ability, Game game) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple cost increase effect
|
||||
*/
|
||||
default void increaseCost(Ability ability, Game game) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation. Override reduceCost or increaseCost instead
|
||||
* TODO: make it private after java 9+ migrate
|
||||
*/
|
||||
default void modifyCost(Ability ability, Game game, CostModificationType costModificationType) {
|
||||
switch (costModificationType) {
|
||||
case REDUCE_COST:
|
||||
reduceCost(ability, game);
|
||||
break;
|
||||
case INCREASE_COST:
|
||||
increaseCost(ability, game);
|
||||
break;
|
||||
case SET_COST:
|
||||
// do nothing
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown mod type: " + costModificationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,20 +6,11 @@ import mage.players.Player;
|
|||
|
||||
/**
|
||||
* @author Grath
|
||||
* Costs which extend this class need to have targets chosen, and those targets must be chosen during 601.2b step.
|
||||
* <p>
|
||||
* Support 601.2b rules for ealry target choice before X announce and other actions
|
||||
*/
|
||||
public abstract class EarlyTargetCost extends CostImpl {
|
||||
public interface EarlyTargetCost {
|
||||
|
||||
protected EarlyTargetCost() {
|
||||
super();
|
||||
}
|
||||
void chooseTarget(Game game, Ability source, Player controller);
|
||||
|
||||
protected EarlyTargetCost(final EarlyTargetCost cost) {
|
||||
super(cost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract EarlyTargetCost copy();
|
||||
|
||||
public abstract void chooseTarget(Game game, Ability source, Player controller);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package mage.abilities.costs;
|
||||
|
||||
/**
|
||||
* @author jayDi85
|
||||
*/
|
||||
public interface MinMaxVariableCost extends VariableCost {
|
||||
|
||||
int getMinX();
|
||||
|
||||
void setMinX(int minX);
|
||||
|
||||
int getMaxX();
|
||||
|
||||
void setMaxX(int maxX);
|
||||
}
|
||||
|
|
@ -10,11 +10,20 @@ import mage.players.Player;
|
|||
import mage.target.common.TargetCardInHand;
|
||||
|
||||
/**
|
||||
* Used to setup discard cost WITHOUT {X} mana cost
|
||||
* <p>
|
||||
* If you have {X} in spell's mana cost then use DiscardXCardsCostAdjuster instead
|
||||
* <p>
|
||||
* Example:
|
||||
* - {2}{U}{R}
|
||||
* - As an additional cost to cast this spell, discard X cards.
|
||||
*
|
||||
* @author LevelX2
|
||||
*/
|
||||
public class DiscardXTargetCost extends VariableCostImpl {
|
||||
|
||||
protected FilterCard filter;
|
||||
protected boolean isRandom = false;
|
||||
|
||||
public DiscardXTargetCost(FilterCard filter) {
|
||||
this(filter, false);
|
||||
|
|
@ -30,6 +39,12 @@ public class DiscardXTargetCost extends VariableCostImpl {
|
|||
protected DiscardXTargetCost(final DiscardXTargetCost cost) {
|
||||
super(cost);
|
||||
this.filter = cost.filter;
|
||||
this.isRandom = cost.isRandom;
|
||||
}
|
||||
|
||||
public DiscardXTargetCost withRandom() {
|
||||
this.isRandom = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -49,6 +64,6 @@ public class DiscardXTargetCost extends VariableCostImpl {
|
|||
@Override
|
||||
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
|
||||
TargetCardInHand target = new TargetCardInHand(xValue, filter);
|
||||
return new DiscardTargetCost(target);
|
||||
return new DiscardTargetCost(target, this.isRandom);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ public class PayLoyaltyCost extends CostImpl {
|
|||
|
||||
int loyaltyCost = amount;
|
||||
|
||||
// apply cost modification
|
||||
// apply dynamic costs and cost modification
|
||||
if (ability instanceof LoyaltyAbility) {
|
||||
LoyaltyAbility copiedAbility = ((LoyaltyAbility) ability).copy();
|
||||
copiedAbility.adjustCosts(game);
|
||||
copiedAbility.adjustX(game);
|
||||
game.getContinuousEffects().costModification(copiedAbility, game);
|
||||
loyaltyCost = 0;
|
||||
for (Cost cost : copiedAbility.getCosts()) {
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@ public class PayVariableLoyaltyCost extends VariableCostImpl {
|
|||
|
||||
int maxValue = permanent.getCounters(game).getCount(CounterType.LOYALTY);
|
||||
|
||||
// apply cost modification
|
||||
// apply dynamic costs and cost modification
|
||||
if (source instanceof LoyaltyAbility) {
|
||||
LoyaltyAbility copiedAbility = ((LoyaltyAbility) source).copy();
|
||||
copiedAbility.adjustCosts(game);
|
||||
copiedAbility.adjustX(game);
|
||||
game.getContinuousEffects().costModification(copiedAbility, game);
|
||||
for (Cost cost : copiedAbility.getCosts()) {
|
||||
if (cost instanceof PayVariableLoyaltyCost) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public enum CommanderManaValueAdjuster implements CostAdjuster {
|
|||
instance;
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Ability ability, Game game) {
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
CardUtil.reduceCost(ability, GreatestCommanderManaValue.instance.calculate(game, ability, null));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package mage.abilities.costs.costadjusters;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.costs.CostAdjuster;
|
||||
import mage.abilities.costs.common.DiscardTargetCost;
|
||||
import mage.abilities.effects.common.InfoEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.Zone;
|
||||
import mage.filter.FilterCard;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
import mage.target.common.TargetCardInHand;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* Used to setup discard cost with {X} mana cost
|
||||
* <p>
|
||||
* If you don't have {X} then use DiscardXTargetCost instead
|
||||
* <p>
|
||||
* Example:
|
||||
* - {X}{1}{B}
|
||||
* - As an additional cost to cast this spell, discard X cards.
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public class DiscardXCardsCostAdjuster implements CostAdjuster {
|
||||
|
||||
private final FilterCard filter;
|
||||
|
||||
private DiscardXCardsCostAdjuster(FilterCard filter) {
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareX(Ability ability, Game game) {
|
||||
Player controller = game.getPlayer(ability.getControllerId());
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minX = 0;
|
||||
int maxX = controller.getHand().getCards(this.filter, ability.getControllerId(), ability, game).size();
|
||||
ability.setVariableCostsMinMax(minX, maxX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareCost(Ability ability, Game game) {
|
||||
int x = CardUtil.getSourceCostsTagX(game, ability, -1);
|
||||
if (x >= 0) {
|
||||
ability.addCost(new DiscardTargetCost(new TargetCardInHand(x, x, this.filter)));
|
||||
}
|
||||
}
|
||||
|
||||
public static void addAdjusterAndMessage(Card card, FilterCard filter) {
|
||||
addAdjusterAndMessage(card, filter, false);
|
||||
}
|
||||
|
||||
public static void addAdjusterAndMessage(Card card, FilterCard filter, boolean isRandom) {
|
||||
if (card.getSpellAbility().getManaCosts().getVariableCosts().isEmpty()) {
|
||||
// how to fix: use DiscardXTargetCost
|
||||
throw new IllegalArgumentException("Wrong code usage: that's cost adjuster must be used with {X} in mana costs only - " + card);
|
||||
}
|
||||
|
||||
Ability ability = new SimpleStaticAbility(
|
||||
Zone.ALL, new InfoEffect("As an additional cost to cast this spell, discard X " + filter.getMessage())
|
||||
);
|
||||
ability.setRuleAtTheTop(true);
|
||||
card.addAbility(ability);
|
||||
|
||||
card.getSpellAbility().setCostAdjuster(new DiscardXCardsCostAdjuster(filter));
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ public enum DomainAdjuster implements CostAdjuster {
|
|||
instance;
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Ability ability, Game game) {
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
CardUtil.reduceCost(ability, DomainValue.REGULAR.calculate(game, ability, null));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,22 +25,29 @@ public class ExileCardsFromHandAdjuster implements CostAdjuster {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Ability ability, Game game) {
|
||||
if (game.inCheckPlayableState()) {
|
||||
return;
|
||||
}
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
Player player = game.getPlayer(ability.getControllerId());
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int cardCount = player.getHand().count(filter, game);
|
||||
int toExile = cardCount > 0 ? player.getAmount(
|
||||
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
|
||||
) : 0;
|
||||
if (toExile > 0) {
|
||||
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
|
||||
CardUtil.reduceCost(ability, 2 * toExile);
|
||||
int reduceCount;
|
||||
if (game.inCheckPlayableState()) {
|
||||
// possible
|
||||
reduceCount = 2 * cardCount;
|
||||
} else {
|
||||
// real - need to choose
|
||||
// TODO: need early target cost instead dialog here
|
||||
int toExile = cardCount == 0 ? 0 : player.getAmount(
|
||||
0, cardCount, "Choose how many " + filter.getMessage() + " to exile", game
|
||||
);
|
||||
reduceCount = 2 * toExile;
|
||||
if (toExile > 0) {
|
||||
ability.addCost(new ExileFromHandCost(new TargetCardInHand(toExile, filter)));
|
||||
}
|
||||
}
|
||||
CardUtil.reduceCost(ability, 2 * reduceCount);
|
||||
}
|
||||
|
||||
public static final void addAdjusterAndMessage(Card card, FilterCard filter) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package mage.abilities.costs.costadjusters;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.costs.CostAdjuster;
|
||||
import mage.cards.Card;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
||||
/**
|
||||
* Used for {X} mana cost that must be replaced by imprinted mana value
|
||||
* <p>
|
||||
* Example:
|
||||
* - Elite Arcanist
|
||||
* - {X}, {T}: Copy the exiled card. ... X is the converted mana cost of the exiled card.
|
||||
*
|
||||
* @author JayDi85
|
||||
*/
|
||||
public enum ImprintedManaValueXCostAdjuster implements CostAdjuster {
|
||||
instance;
|
||||
|
||||
@Override
|
||||
public void prepareX(Ability ability, Game game) {
|
||||
int manaValue = Integer.MAX_VALUE;
|
||||
|
||||
Permanent sourcePermanent = game.getPermanent(ability.getSourceId());
|
||||
if (sourcePermanent != null
|
||||
&& sourcePermanent.getImprinted() != null
|
||||
&& !sourcePermanent.getImprinted().isEmpty()) {
|
||||
Card imprintedInstant = game.getCard(sourcePermanent.getImprinted().get(0));
|
||||
if (imprintedInstant != null) {
|
||||
manaValue = imprintedInstant.getManaValue();
|
||||
}
|
||||
}
|
||||
|
||||
ability.setVariableCostsValue(manaValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,10 +29,8 @@ public enum LegendaryCreatureCostAdjuster implements CostAdjuster {
|
|||
);
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Ability ability, Game game) {
|
||||
int count = game.getBattlefield().count(
|
||||
filter, ability.getControllerId(), ability, game
|
||||
);
|
||||
public void reduceCost(Ability ability, Game game) {
|
||||
int count = game.getBattlefield().count(filter, ability.getControllerId(), ability, game);
|
||||
if (count > 0) {
|
||||
CardUtil.reduceCost(ability, count);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public interface ManaCost extends Cost {
|
|||
ManaOptions getOptions();
|
||||
|
||||
/**
|
||||
* Return all options for paying the mana cost (this) while taking into accoutn if the player can pay life.
|
||||
* Return all options for paying the mana cost (this) while taking into account if the player can pay life.
|
||||
* Used to correctly highlight (or not) spells with Phyrexian mana depending on if the player can pay life costs.
|
||||
* <p>
|
||||
* E.g. Tezzeret's Gambit has a cost of {3}{U/P}.
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public abstract class ManaCostImpl extends CostImpl implements ManaCost {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ManaOptions getOptions() {
|
||||
public final ManaOptions getOptions() {
|
||||
return getOptions(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
|
|||
this.add(new ColoredManaCost(ColoredManaSymbol.lookup(symbol.charAt(0))));
|
||||
} else // check X wasn't added before
|
||||
if (modifierForX == 0) {
|
||||
// count X occurence
|
||||
// count X occurrence
|
||||
for (String s : symbols) {
|
||||
if (s.equals("X")) {
|
||||
modifierForX++;
|
||||
|
|
|
|||
|
|
@ -3,17 +3,19 @@ package mage.abilities.costs.mana;
|
|||
import mage.Mana;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.VariableCost;
|
||||
import mage.abilities.costs.MinMaxVariableCost;
|
||||
import mage.abilities.costs.VariableCostType;
|
||||
import mage.abilities.mana.ManaOptions;
|
||||
import mage.constants.ColoredManaSymbol;
|
||||
import mage.filter.FilterMana;
|
||||
import mage.game.Game;
|
||||
import mage.players.ManaPool;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||
*/
|
||||
public final class VariableManaCost extends ManaCostImpl implements VariableCost {
|
||||
public class VariableManaCost extends ManaCostImpl implements MinMaxVariableCost {
|
||||
|
||||
// variable mana cost usage on 2019-06-20:
|
||||
// 1. as X value in spell/ability cast (announce X, set VariableManaCost as paid and add generic mana to pay instead)
|
||||
|
|
@ -23,7 +25,7 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
|
|||
protected int xInstancesCount; // number of {X} instances in cost like {X} or {X}{X}
|
||||
protected int xValue = 0; // final X value after announce and replace events
|
||||
protected int xPay = 0; // final/total need pay after announce and replace events (example: {X}{X}, X=3, xPay = 6)
|
||||
protected boolean wasAnnounced = false;
|
||||
protected boolean wasAnnounced = false; // X was announced by player or auto-defined by CostAdjuster
|
||||
|
||||
protected FilterMana filter; // mana filter that can be used for that cost
|
||||
protected int minX = 0;
|
||||
|
|
@ -59,6 +61,18 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
|
|||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManaOptions getOptions(boolean canPayLifeCost) {
|
||||
ManaOptions res = new ManaOptions();
|
||||
|
||||
// limit mana options for better performance
|
||||
CardUtil.distributeValues(10, getMinX(), getMaxX()).forEach(value -> {
|
||||
res.add(Mana.GenericMana(value));
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignPayment(Game game, Ability ability, ManaPool pool, Cost costToPay) {
|
||||
// X mana cost always pays as generic mana
|
||||
|
|
@ -86,6 +100,10 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
|
|||
return this.isColorlessPaid(xPay);
|
||||
}
|
||||
|
||||
public boolean wasAnnounced() {
|
||||
return this.wasAnnounced;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VariableManaCost getUnpaid() {
|
||||
return this;
|
||||
|
|
@ -122,18 +140,22 @@ public final class VariableManaCost extends ManaCostImpl implements VariableCost
|
|||
return this.xInstancesCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinX() {
|
||||
return minX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinX(int minX) {
|
||||
this.minX = minX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxX() {
|
||||
return maxX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxX(int maxX) {
|
||||
this.maxX = maxX;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,35 +3,58 @@ package mage.abilities.dynamicvalue.common;
|
|||
import mage.abilities.Ability;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.hint.Hint;
|
||||
import mage.abilities.hint.ValueHint;
|
||||
import mage.filter.FilterCard;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Controllable;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public enum CardsInControllerHandCount implements DynamicValue {
|
||||
instance;
|
||||
|
||||
ANY(StaticFilters.FILTER_CARD_CARDS),
|
||||
CREATURES(StaticFilters.FILTER_CARD_CREATURES),
|
||||
LANDS(StaticFilters.FILTER_CARD_LANDS);
|
||||
|
||||
private final FilterCard filter;
|
||||
private final ValueHint hint;
|
||||
|
||||
CardsInControllerHandCount(FilterCard filter) {
|
||||
this.filter = filter;
|
||||
this.hint = new ValueHint(filter.getMessage() + " in your hand", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculate(Game game, Ability sourceAbility, Effect effect) {
|
||||
if (sourceAbility != null) {
|
||||
Player controller = game.getPlayer(sourceAbility.getControllerId());
|
||||
if (controller != null) {
|
||||
return controller.getHand().size();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return Optional
|
||||
.ofNullable(sourceAbility)
|
||||
.map(Controllable::getControllerId)
|
||||
.map(game::getPlayer)
|
||||
.map(Player::getHand)
|
||||
.map(Set::size)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CardsInControllerHandCount copy() {
|
||||
return CardsInControllerHandCount.instance;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "cards in your hand";
|
||||
return this.filter.getMessage() + " in your hand";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "1";
|
||||
}
|
||||
|
||||
public Hint getHint() {
|
||||
return this.hint;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
package mage.abilities.dynamicvalue.common;
|
||||
|
||||
import mage.MageInt;
|
||||
|
|
@ -6,6 +5,9 @@ import mage.MageObject;
|
|||
import mage.abilities.Ability;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.hint.Hint;
|
||||
import mage.abilities.hint.ValueHint;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
|
||||
|
|
@ -13,16 +15,21 @@ import mage.game.Game;
|
|||
* @author TheElk801
|
||||
*/
|
||||
public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicValue {
|
||||
instance;
|
||||
ALL(StaticFilters.FILTER_CONTROLLED_CREATURES),
|
||||
OTHER(StaticFilters.FILTER_OTHER_CONTROLLED_CREATURES);
|
||||
private final FilterPermanent filter;
|
||||
private final Hint hint;
|
||||
|
||||
GreatestToughnessAmongControlledCreaturesValue(FilterPermanent filter) {
|
||||
this.filter = filter;
|
||||
this.hint = new ValueHint("The greatest toughness among " + filter.getMessage(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculate(Game game, Ability sourceAbility, Effect effect) {
|
||||
return game
|
||||
.getBattlefield()
|
||||
.getActivePermanents(
|
||||
StaticFilters.FILTER_CONTROLLED_CREATURE,
|
||||
sourceAbility.getControllerId(), game
|
||||
)
|
||||
.getActivePermanents(filter, sourceAbility.getControllerId(), game)
|
||||
.stream()
|
||||
.map(MageObject::getToughness)
|
||||
.mapToInt(MageInt::getValue)
|
||||
|
|
@ -32,12 +39,12 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal
|
|||
|
||||
@Override
|
||||
public GreatestToughnessAmongControlledCreaturesValue copy() {
|
||||
return GreatestToughnessAmongControlledCreaturesValue.instance;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "the greatest toughness among creatures you control";
|
||||
return "the greatest toughness among " + filter.getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -45,4 +52,7 @@ public enum GreatestToughnessAmongControlledCreaturesValue implements DynamicVal
|
|||
return "X";
|
||||
}
|
||||
|
||||
public Hint getHint() {
|
||||
return hint;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ import mage.abilities.ActivatedAbility;
|
|||
import mage.cards.Card;
|
||||
import mage.cards.ModalDoubleFacedCard;
|
||||
import mage.cards.SplitCard;
|
||||
import mage.cards.CardWithSpellOption;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import mage.cards.AdventureCard;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
|
|
@ -103,9 +102,9 @@ public abstract class AsThoughEffectImpl extends ContinuousEffectImpl implements
|
|||
if (!rightCard.isLand(game)) {
|
||||
player.setCastSourceIdWithAlternateMana(rightCard.getId(), null, rightCard.getSpellAbility().getCosts(), identifier);
|
||||
}
|
||||
} else if (card instanceof AdventureCard) {
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
Card creatureCard = card.getMainCard();
|
||||
Card spellCard = ((AdventureCard) card).getSpellCard();
|
||||
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
|
||||
player.setCastSourceIdWithAlternateMana(creatureCard.getId(), null, creatureCard.getSpellAbility().getCosts(), identifier);
|
||||
player.setCastSourceIdWithAlternateMana(spellCard.getId(), null, spellCard.getSpellAbility().getCosts(), identifier);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -549,9 +549,9 @@ public class ContinuousEffects implements Serializable {
|
|||
// rules:
|
||||
// 708.4. In every zone except the stack, the characteristics of a split card are those of its two halves combined.
|
||||
idToCheck = ((SplitCardHalf) objectToCheck).getMainCard().getId();
|
||||
} else if (!type.needPlayCardAbility() && objectToCheck instanceof AdventureCardSpell) {
|
||||
// adventure spell uses alternative characteristics for spell/stack, all other cases must use main card
|
||||
idToCheck = ((AdventureCardSpell) objectToCheck).getMainCard().getId();
|
||||
} else if (!type.needPlayCardAbility() && objectToCheck instanceof CardWithSpellOption) {
|
||||
// adventure/omen spell uses alternative characteristics for spell/stack, all other cases must use main card
|
||||
idToCheck = ((CardWithSpellOption) objectToCheck).getMainCard().getId();
|
||||
} else if (!type.needPlayCardAbility() && objectToCheck instanceof ModalDoubleFacedCardHalf) {
|
||||
// each mdf side uses own characteristics to check for playing, all other cases must use main card
|
||||
// rules:
|
||||
|
|
@ -688,6 +688,8 @@ public class ContinuousEffects implements Serializable {
|
|||
* the battlefield for
|
||||
* {@link CostModificationEffect cost modification effects} and applies them
|
||||
* if necessary.
|
||||
* <p>
|
||||
* Warning, don't forget to call ability.adjustX before any cost modifications
|
||||
*
|
||||
* @param abilityToModify
|
||||
* @param game
|
||||
|
|
@ -695,6 +697,10 @@ public class ContinuousEffects implements Serializable {
|
|||
public void costModification(Ability abilityToModify, Game game) {
|
||||
List<CostModificationEffect> costEffects = getApplicableCostModificationEffects(game);
|
||||
|
||||
// add dynamic costs from X and other places
|
||||
abilityToModify.adjustCostsPrepare(game);
|
||||
|
||||
abilityToModify.adjustCostsModify(game, CostModificationType.INCREASE_COST);
|
||||
for (CostModificationEffect effect : costEffects) {
|
||||
if (effect.getModificationType() == CostModificationType.INCREASE_COST) {
|
||||
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());
|
||||
|
|
@ -706,6 +712,7 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
abilityToModify.adjustCostsModify(game, CostModificationType.REDUCE_COST);
|
||||
for (CostModificationEffect effect : costEffects) {
|
||||
if (effect.getModificationType() == CostModificationType.REDUCE_COST) {
|
||||
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());
|
||||
|
|
@ -717,6 +724,7 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
abilityToModify.adjustCostsModify(game, CostModificationType.SET_COST);
|
||||
for (CostModificationEffect effect : costEffects) {
|
||||
if (effect.getModificationType() == CostModificationType.SET_COST) {
|
||||
Set<Ability> abilities = costModificationEffects.getAbility(effect.getId());
|
||||
|
|
|
|||
|
|
@ -1,39 +1,41 @@
|
|||
|
||||
package mage.abilities.effects.common;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.choices.Choice;
|
||||
import mage.choices.ChoiceImpl;
|
||||
import mage.constants.ModeChoice;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author LevelX2
|
||||
*/
|
||||
public class ChooseModeEffect extends OneShotEffect {
|
||||
|
||||
protected final List<String> modes = new ArrayList<>();
|
||||
protected final String choiceMessage;
|
||||
protected final List<ModeChoice> modes = new ArrayList<>();
|
||||
protected final String message;
|
||||
|
||||
public ChooseModeEffect(String choiceMessage, String... modes) {
|
||||
public ChooseModeEffect(ModeChoice... modes) {
|
||||
super(Outcome.Neutral);
|
||||
this.choiceMessage = choiceMessage;
|
||||
this.modes.addAll(Arrays.asList(modes));
|
||||
this.staticText = setText();
|
||||
this.message = makeMessage(this.modes);
|
||||
this.staticText = "choose " + this.message;
|
||||
}
|
||||
|
||||
protected ChooseModeEffect(final ChooseModeEffect effect) {
|
||||
super(effect);
|
||||
this.modes.addAll(effect.modes);
|
||||
this.choiceMessage = effect.choiceMessage;
|
||||
this.message = effect.message;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -48,34 +50,27 @@ public class ChooseModeEffect extends OneShotEffect {
|
|||
if (sourcePermanent == null) {
|
||||
sourcePermanent = game.getPermanentEntering(source.getSourceId());
|
||||
}
|
||||
if (controller != null && sourcePermanent != null) {
|
||||
Choice choice = new ChoiceImpl(true);
|
||||
choice.setMessage(choiceMessage + CardUtil.getSourceLogName(game, source));
|
||||
choice.getChoices().addAll(modes);
|
||||
if (controller.choose(Outcome.Neutral, choice, game)) {
|
||||
if (!game.isSimulation()) {
|
||||
game.informPlayers(sourcePermanent.getLogName() + ": " + controller.getLogName() + " has chosen " + choice.getChoice());
|
||||
}
|
||||
game.getState().setValue(source.getSourceId() + "_modeChoice", choice.getChoice());
|
||||
sourcePermanent.addInfo("_modeChoice", "<font color = 'blue'>Chosen mode: " + choice.getChoice() + "</font>", game);
|
||||
return true;
|
||||
}
|
||||
if (controller == null || sourcePermanent == null) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
Choice choice = new ChoiceImpl(true);
|
||||
choice.setMessage(message + "? (" + CardUtil.getSourceLogName(game, source) + ')');
|
||||
choice.getChoices().addAll(modes.stream().map(ModeChoice::toString).collect(Collectors.toList()));
|
||||
if (!controller.choose(Outcome.Neutral, choice, game)) {
|
||||
return false;
|
||||
}
|
||||
game.informPlayers(sourcePermanent.getLogName() + ": " + controller.getLogName() + " has chosen " + choice.getChoice());
|
||||
game.getState().setValue(source.getSourceId() + "_modeChoice", choice.getChoice());
|
||||
sourcePermanent.addInfo("_modeChoice", "<font color = 'blue'>Chosen mode: " + choice.getChoice() + "</font>", game);
|
||||
return true;
|
||||
}
|
||||
|
||||
private String setText() {
|
||||
StringBuilder sb = new StringBuilder("choose ");
|
||||
int count = 0;
|
||||
for (String choice : modes) {
|
||||
count++;
|
||||
sb.append(choice);
|
||||
if (count + 1 < modes.size()) {
|
||||
sb.append(", ");
|
||||
} else if (count < modes.size()) {
|
||||
sb.append(" or ");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
private static String makeMessage(List<ModeChoice> modeChoices) {
|
||||
return CardUtil.concatWithOr(
|
||||
modeChoices
|
||||
.stream()
|
||||
.map(ModeChoice::toString)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package mage.abilities.effects.common;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.ExileZone;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.game.permanent.token.Token;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class CreateXXTokenExiledEffectManaValueEffect extends OneShotEffect {
|
||||
|
||||
private final Function<Integer, Token> tokenMaker;
|
||||
|
||||
public CreateXXTokenExiledEffectManaValueEffect(Function<Integer, Token> tokenMaker, String description) {
|
||||
super(Outcome.Benefit);
|
||||
this.tokenMaker = tokenMaker;
|
||||
staticText = "the exiled card's owner creates an X/X " + description +
|
||||
"creature token, where X is the mana value of the exiled card";
|
||||
}
|
||||
|
||||
private CreateXXTokenExiledEffectManaValueEffect(final CreateXXTokenExiledEffectManaValueEffect effect) {
|
||||
super(effect);
|
||||
this.tokenMaker = effect.tokenMaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateXXTokenExiledEffectManaValueEffect copy() {
|
||||
return new CreateXXTokenExiledEffectManaValueEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Permanent permanentLeftBattlefield = (Permanent) getValue("permanentLeftBattlefield");
|
||||
ExileZone exile = game.getExile().getExileZone(
|
||||
CardUtil.getExileZoneId(game, source.getSourceId(), permanentLeftBattlefield.getZoneChangeCounter(game))
|
||||
);
|
||||
if (exile == null || exile.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// From ZNR Release Notes:
|
||||
// https://magic.wizards.com/en/articles/archive/feature/zendikar-rising-release-notes-2020-09-10
|
||||
// If Skyclave Apparition's first ability exiled more than one card owned by a single player,
|
||||
// that player creates a token with power and toughness equal to the sum of those cards' converted mana costs.
|
||||
// If the first ability exiled cards owned by more than one player, each of those players creates a token
|
||||
// with power and toughness equal to the sum of the converted mana costs of all cards exiled by the first ability.
|
||||
Set<UUID> owners = new HashSet<>();
|
||||
int totalCMC = exile
|
||||
.getCards(game)
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(card -> {
|
||||
owners.add(card.getOwnerId());
|
||||
return card;
|
||||
})
|
||||
.mapToInt(MageObject::getManaValue)
|
||||
.sum();
|
||||
for (UUID playerId : owners) {
|
||||
tokenMaker.apply(totalCMC).putOntoBattlefield(1, game, source, playerId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import mage.abilities.MageSingleton;
|
|||
import mage.abilities.effects.AsThoughEffectImpl;
|
||||
import mage.abilities.effects.ContinuousEffect;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.cards.AdventureCardSpell;
|
||||
import mage.cards.AdventureSpellCard;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.AsThoughEffectType;
|
||||
import mage.constants.Duration;
|
||||
|
|
@ -51,10 +51,10 @@ public class ExileAdventureSpellEffect extends OneShotEffect implements MageSing
|
|||
Spell spell = game.getStack().getSpell(source.getId());
|
||||
if (spell != null) {
|
||||
Card spellCard = spell.getCard();
|
||||
if (spellCard instanceof AdventureCardSpell) {
|
||||
if (spellCard instanceof AdventureSpellCard) {
|
||||
UUID exileId = adventureExileId(controller.getId(), game);
|
||||
game.getExile().createZone(exileId, "On an Adventure from " + controller.getName());
|
||||
AdventureCardSpell adventureSpellCard = (AdventureCardSpell) spellCard;
|
||||
AdventureSpellCard adventureSpellCard = (AdventureSpellCard) spellCard;
|
||||
Card parentCard = adventureSpellCard.getParentCard();
|
||||
if (controller.moveCardsToExile(parentCard, source, game, true, exileId, "On an Adventure from " + controller.getName())) {
|
||||
ContinuousEffect effect = new AdventureCastFromExileEffect();
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class PermanentsEnterBattlefieldTappedEffect extends ReplacementEffectImp
|
|||
return staticText;
|
||||
}
|
||||
return filter.getMessage()
|
||||
+ " enter tapped"
|
||||
+ (duration == Duration.EndOfTurn ? " this turn" : "");
|
||||
+ " enter" + (filter.getMessage().startsWith("each") ? "s" : "")
|
||||
+ " tapped" + (duration == Duration.EndOfTurn ? " this turn" : "");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import mage.counters.Counters;
|
|||
import mage.game.Game;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
|
|
@ -17,15 +19,17 @@ public class ReturnFromGraveyardToBattlefieldWithCounterTargetEffect extends Ret
|
|||
private final Counters counters;
|
||||
private final String counterText;
|
||||
|
||||
public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter counter) {
|
||||
this(counter, false);
|
||||
public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter... counters) {
|
||||
this(false, counters);
|
||||
}
|
||||
|
||||
public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(Counter counter, boolean additional) {
|
||||
public ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(boolean additional, Counter... counters) {
|
||||
super(false);
|
||||
this.counters = new Counters();
|
||||
this.counters.addCounter(counter);
|
||||
this.counterText = makeText(counter, additional);
|
||||
for (Counter counter : counters) {
|
||||
this.counters.addCounter(counter);
|
||||
}
|
||||
this.counterText = makeText(additional, counters);
|
||||
}
|
||||
|
||||
protected ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(final ReturnFromGraveyardToBattlefieldWithCounterTargetEffect effect) {
|
||||
|
|
@ -47,22 +51,26 @@ public class ReturnFromGraveyardToBattlefieldWithCounterTargetEffect extends Ret
|
|||
return super.apply(game, source);
|
||||
}
|
||||
|
||||
private String makeText(Counter counter, boolean additional) {
|
||||
StringBuilder sb = new StringBuilder(" with ");
|
||||
if (counter.getCount() == 1) {
|
||||
if (additional) {
|
||||
sb.append("an additional ").append(counter.getName());
|
||||
private static String makeText(boolean additional, Counter... counters) {
|
||||
List<String> strings = new ArrayList<>();
|
||||
for (Counter counter : counters) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (counter.getCount() == 1) {
|
||||
if (additional) {
|
||||
sb.append("an additional ").append(counter.getName());
|
||||
} else {
|
||||
sb.append(CardUtil.addArticle(counter.getName()));
|
||||
}
|
||||
sb.append(" counter");
|
||||
} else {
|
||||
sb.append(CardUtil.addArticle(counter.getName()));
|
||||
sb.append(CardUtil.numberToText(counter.getCount()));
|
||||
sb.append(additional ? " additional " : " ");
|
||||
sb.append(counter.getName());
|
||||
sb.append(" counters");
|
||||
}
|
||||
sb.append(" counter");
|
||||
} else {
|
||||
sb.append(CardUtil.numberToText(counter.getCount()));
|
||||
sb.append(additional ? " additional " : " ");
|
||||
sb.append(counter.getName());
|
||||
sb.append(" counters");
|
||||
strings.add(sb.toString());
|
||||
}
|
||||
return sb.toString();
|
||||
return " with " + CardUtil.concatWithAnd(strings);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package mage.abilities.effects.common.continuous;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.effects.ContinuousEffectImpl;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class GainAnchorWordAbilitySourceEffect extends ContinuousEffectImpl {
|
||||
|
||||
private final Ability ability;
|
||||
private final ModeChoice modeChoice;
|
||||
|
||||
public GainAnchorWordAbilitySourceEffect(Effect effect, ModeChoice modeChoice) {
|
||||
this(new SimpleStaticAbility(effect), modeChoice);
|
||||
}
|
||||
|
||||
public GainAnchorWordAbilitySourceEffect(Ability ability, ModeChoice modeChoice) {
|
||||
super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
|
||||
this.staticText = "&bull " + modeChoice + " — " + ability.getRule();
|
||||
this.ability = ability;
|
||||
this.modeChoice = modeChoice;
|
||||
this.ability.setRuleVisible(false);
|
||||
this.generateGainAbilityDependencies(ability, null);
|
||||
}
|
||||
|
||||
private GainAnchorWordAbilitySourceEffect(final GainAnchorWordAbilitySourceEffect effect) {
|
||||
super(effect);
|
||||
this.modeChoice = effect.modeChoice;
|
||||
this.ability = effect.ability;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
|
||||
if (permanent == null || !modeChoice.checkMode(game, source)) {
|
||||
return false;
|
||||
}
|
||||
permanent.addAbility(ability, source.getSourceId(), game);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GainAnchorWordAbilitySourceEffect copy() {
|
||||
return new GainAnchorWordAbilitySourceEffect(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import mage.abilities.effects.RestrictionEffect;
|
|||
import mage.abilities.effects.common.ChooseModeEffect;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.ModeChoice;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
|
@ -19,9 +20,6 @@ import java.util.UUID;
|
|||
*/
|
||||
public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends RestrictionEffect {
|
||||
|
||||
public static final String ALLOW_ATTACKING_LEFT = "Allow attacking left";
|
||||
public static final String ALLOW_ATTACKING_RIGHT = "Allow attacking right";
|
||||
|
||||
public PlayerCanOnlyAttackInDirectionRestrictionEffect(Duration duration, String directionText) {
|
||||
super(duration, Outcome.Neutral);
|
||||
staticText = duration + (duration.toString().isEmpty() ? "" : ", ")
|
||||
|
|
@ -39,10 +37,7 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
|
|||
}
|
||||
|
||||
public static Effect choiceEffect() {
|
||||
return new ChooseModeEffect(
|
||||
"Choose a direction to allow attacking in.",
|
||||
ALLOW_ATTACKING_LEFT, ALLOW_ATTACKING_RIGHT
|
||||
).setText("choose left or right");
|
||||
return new ChooseModeEffect(ModeChoice.LEFT, ModeChoice.RIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -55,10 +50,13 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
|
|||
if (defenderId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String allowedDirection = (String) game.getState().getValue(source.getSourceId() + "_modeChoice");
|
||||
if (allowedDirection == null) {
|
||||
return true; // If no choice was made, the ability has no effect.
|
||||
boolean left;
|
||||
if (ModeChoice.LEFT.checkMode(game, source)) {
|
||||
left = true;
|
||||
} else if (ModeChoice.RIGHT.checkMode(game, source)) {
|
||||
left = false;
|
||||
} else {
|
||||
return false; // If no choice was made, the ability has no effect.
|
||||
}
|
||||
|
||||
Player playerAttacking = game.getPlayer(attacker.getControllerId());
|
||||
|
|
@ -83,18 +81,7 @@ public class PlayerCanOnlyAttackInDirectionRestrictionEffect extends Restriction
|
|||
}
|
||||
|
||||
PlayerList playerList = game.getState().getPlayerList(playerAttacking.getId());
|
||||
if (allowedDirection.equals(ALLOW_ATTACKING_LEFT)
|
||||
&& !playerList.getNext().equals(playerDefending.getId())) {
|
||||
// the defender is not the player to the left
|
||||
return false;
|
||||
}
|
||||
if (allowedDirection.equals(ALLOW_ATTACKING_RIGHT)
|
||||
&& !playerList.getPrevious().equals(playerDefending.getId())) {
|
||||
// the defender is not the player to the right
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return (!left || playerList.getNext().equals(playerDefending.getId()))
|
||||
&& (left || playerList.getPrevious().equals(playerDefending.getId()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect {
|
|||
}
|
||||
TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter);
|
||||
if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) {
|
||||
// TODO: must fizzle discard effect on not full choice
|
||||
// - tests: affected (allow to choose and discard 1 instead 2)
|
||||
// - real game: need to check
|
||||
player.discard(new CardsImpl(target.getTargets()), false, source, game);
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package mage.abilities.effects.common.replacement;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.ReplacementEffectImpl;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.Controllable;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.DefenderAttackedEvent;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.events.NumberOfTriggersEvent;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class AdditionalTriggersAttackingReplacementEffect extends ReplacementEffectImpl {
|
||||
|
||||
private final boolean onlyControlled;
|
||||
|
||||
public AdditionalTriggersAttackingReplacementEffect(boolean onlyControlled) {
|
||||
super(Duration.WhileOnBattlefield, Outcome.Benefit);
|
||||
this.onlyControlled = onlyControlled;
|
||||
staticText = "if a creature " + (onlyControlled ? "you control " : "") + "attacking causes a triggered ability " +
|
||||
"of a permanent you control to trigger, that ability triggers an additional time";
|
||||
}
|
||||
|
||||
private AdditionalTriggersAttackingReplacementEffect(final AdditionalTriggersAttackingReplacementEffect effect) {
|
||||
super(effect);
|
||||
this.onlyControlled = effect.onlyControlled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdditionalTriggersAttackingReplacementEffect copy() {
|
||||
return new AdditionalTriggersAttackingReplacementEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checksEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.NUMBER_OF_TRIGGERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(GameEvent event, Ability source, Game game) {
|
||||
NumberOfTriggersEvent numberOfTriggersEvent = (NumberOfTriggersEvent) event;
|
||||
Permanent sourcePermanent = game.getPermanent(numberOfTriggersEvent.getSourceId());
|
||||
if (sourcePermanent == null || !sourcePermanent.isControlledBy(source.getControllerId())) {
|
||||
return false;
|
||||
}
|
||||
GameEvent sourceEvent = numberOfTriggersEvent.getSourceEvent();
|
||||
if (sourceEvent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (sourceEvent.getType()) {
|
||||
case ATTACKER_DECLARED:
|
||||
return !onlyControlled || source.isControlledBy(sourceEvent.getPlayerId());
|
||||
case DECLARED_ATTACKERS:
|
||||
return !onlyControlled || game
|
||||
.getCombat()
|
||||
.getAttackers()
|
||||
.stream()
|
||||
.map(game::getControllerId)
|
||||
.anyMatch(source::isControlledBy);
|
||||
case DEFENDER_ATTACKED:
|
||||
return !onlyControlled || ((DefenderAttackedEvent) sourceEvent)
|
||||
.getAttackers(game)
|
||||
.stream()
|
||||
.map(Controllable::getControllerId)
|
||||
.anyMatch(source::isControlledBy);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
|
||||
event.setAmount(event.getAmount() + 1);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package mage.abilities.effects.keyword;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.dynamicvalue.common.StaticValue;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.constants.Outcome;
|
||||
import mage.counters.CounterType;
|
||||
|
|
@ -15,13 +17,17 @@ import mage.util.CardUtil;
|
|||
*/
|
||||
public class EndureSourceEffect extends OneShotEffect {
|
||||
|
||||
private final int amount;
|
||||
private final DynamicValue amount;
|
||||
|
||||
public EndureSourceEffect(int amount) {
|
||||
this(amount, "it");
|
||||
}
|
||||
|
||||
public EndureSourceEffect(int amount, String selfText) {
|
||||
this(StaticValue.get(amount), selfText);
|
||||
}
|
||||
|
||||
public EndureSourceEffect(DynamicValue amount, String selfText) {
|
||||
super(Outcome.Benefit);
|
||||
staticText = selfText + " endures " + amount;
|
||||
this.amount = amount;
|
||||
|
|
@ -39,12 +45,23 @@ public class EndureSourceEffect extends OneShotEffect {
|
|||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
if (player == null) {
|
||||
return doEndure(
|
||||
source.getSourcePermanentOrLKI(game),
|
||||
amount.calculate(game, source, this),
|
||||
game, source
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean doEndure(Permanent permanent, int amount, Game game, Ability source) {
|
||||
if (permanent == null || amount < 1) {
|
||||
return false;
|
||||
}
|
||||
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
|
||||
if (permanent != null && player.chooseUse(
|
||||
Player controller = game.getPlayer(permanent.getControllerId());
|
||||
if (controller == null) {
|
||||
return false;
|
||||
}
|
||||
if (permanent.getZoneChangeCounter(game) == game.getState().getZoneChangeCounter(permanent.getId())
|
||||
&& controller.chooseUse(
|
||||
Outcome.BoostCreature, "Put " + CardUtil.numberToText(amount, "a") + " +1/+1 counter" +
|
||||
(amount > 1 ? "s" : "") + " on " + permanent.getName() + " or create " +
|
||||
CardUtil.addArticle("" + amount) + ' ' + amount + '/' + amount + " Spirit token?",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
package mage.abilities.keyword;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.Costs;
|
||||
import mage.abilities.effects.ContinuousEffect;
|
||||
import mage.abilities.effects.ReplacementEffectImpl;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.ModalDoubleFacedCard;
|
||||
import mage.cards.SplitCard;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
import mage.players.Player;
|
||||
import mage.target.targetpointer.FixedTarget;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Base class for Flashback and Harmonize and any future ability which works similarly
|
||||
*
|
||||
* @author TheElk801
|
||||
*/
|
||||
public abstract class CastFromGraveyardAbility extends SpellAbility {
|
||||
|
||||
protected String abilityName;
|
||||
private SpellAbility spellAbilityToResolve;
|
||||
|
||||
protected CastFromGraveyardAbility(Card card, Cost cost, SpellAbilityCastMode spellAbilityCastMode) {
|
||||
super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, spellAbilityCastMode);
|
||||
this.setAdditionalCostsRuleVisible(false);
|
||||
this.name = spellAbilityCastMode + " " + cost.getText();
|
||||
this.addCost(cost);
|
||||
this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT;
|
||||
}
|
||||
|
||||
protected CastFromGraveyardAbility(final CastFromGraveyardAbility ability) {
|
||||
super(ability);
|
||||
this.abilityName = ability.abilityName;
|
||||
this.spellAbilityToResolve = ability.spellAbilityToResolve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActivationStatus canActivate(UUID playerId, Game game) {
|
||||
// flashback ability dynamicly added to all card's parts (split cards)
|
||||
if (!super.canActivate(playerId, game).canActivate()) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
Card card = game.getCard(getSourceId());
|
||||
if (card == null) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
// Card must be in the graveyard zone
|
||||
if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
// Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision)
|
||||
if (card.getManaCost().isEmpty()) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
// CastFromGraveyard can never cast a split card by Fuse, because Fuse only works from hand
|
||||
// https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/
|
||||
if (card instanceof SplitCard) {
|
||||
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
|
||||
return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
}
|
||||
} else if (card instanceof ModalDoubleFacedCard) {
|
||||
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
|
||||
return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
}
|
||||
}
|
||||
return card.getSpellAbility().canActivate(playerId, game);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpellAbility getSpellAbilityToResolve(Game game) {
|
||||
Card card = game.getCard(getSourceId());
|
||||
if (card == null || spellAbilityToResolve != null) {
|
||||
return spellAbilityToResolve;
|
||||
}
|
||||
SpellAbility spellAbilityCopy;
|
||||
if (card instanceof SplitCard) {
|
||||
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy();
|
||||
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy();
|
||||
} else {
|
||||
spellAbilityCopy = null;
|
||||
}
|
||||
} else if (card instanceof ModalDoubleFacedCard) {
|
||||
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy();
|
||||
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
|
||||
} else {
|
||||
spellAbilityCopy = null;
|
||||
}
|
||||
} 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;
|
||||
ContinuousEffect effect = new CastFromGraveyardReplacementEffect();
|
||||
effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId())));
|
||||
game.addEffect(effect, this);
|
||||
return spellAbilityToResolve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Costs<Cost> getCosts() {
|
||||
if (spellAbilityToResolve == null) {
|
||||
return super.getCosts();
|
||||
}
|
||||
return spellAbilityToResolve.getCosts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule(boolean all) {
|
||||
return this.getRule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for split card in PlayerImpl method:
|
||||
* getOtherUseableActivatedAbilities
|
||||
*
|
||||
* @param abilityName
|
||||
*/
|
||||
public CastFromGraveyardAbility setAbilityName(String abilityName) {
|
||||
this.abilityName = abilityName;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class CastFromGraveyardReplacementEffect extends ReplacementEffectImpl {
|
||||
|
||||
public CastFromGraveyardReplacementEffect() {
|
||||
super(Duration.OneUse, Outcome.Exile);
|
||||
}
|
||||
|
||||
protected CastFromGraveyardReplacementEffect(final CastFromGraveyardReplacementEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CastFromGraveyardReplacementEffect copy() {
|
||||
return new CastFromGraveyardReplacementEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller == null) {
|
||||
return false;
|
||||
}
|
||||
Card card = game.getCard(event.getTargetId());
|
||||
if (card == null) {
|
||||
return false;
|
||||
}
|
||||
discard();
|
||||
return controller.moveCards(
|
||||
card, Zone.EXILED, source, game, false,
|
||||
false, false, event.getAppliedEffects()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checksEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.ZONE_CHANGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(GameEvent event, Ability source, Game game) {
|
||||
UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards
|
||||
if (!cardId.equals(event.getTargetId())
|
||||
|| ((ZoneChangeEvent) event).getFromZone() != Zone.STACK
|
||||
|| ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) {
|
||||
return false;
|
||||
}
|
||||
int zcc = game.getState().getZoneChangeCounter(cardId);
|
||||
return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,8 @@
|
|||
package mage.abilities.keyword;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.Costs;
|
||||
import mage.abilities.effects.ContinuousEffect;
|
||||
import mage.abilities.effects.ReplacementEffectImpl;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.ModalDoubleFacedCard;
|
||||
import mage.cards.SplitCard;
|
||||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
import mage.players.Player;
|
||||
import mage.target.targetpointer.FixedTarget;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.constants.SpellAbilityCastMode;
|
||||
|
||||
/**
|
||||
* 702.32. Flashback
|
||||
|
|
@ -33,106 +18,14 @@ import java.util.UUID;
|
|||
*
|
||||
* @author nantuko
|
||||
*/
|
||||
public class FlashbackAbility extends SpellAbility {
|
||||
|
||||
private String abilityName;
|
||||
private SpellAbility spellAbilityToResolve;
|
||||
public class FlashbackAbility extends CastFromGraveyardAbility {
|
||||
|
||||
public FlashbackAbility(Card card, Cost cost) {
|
||||
super(null, "", Zone.GRAVEYARD, SpellAbilityType.BASE_ALTERNATE, SpellAbilityCastMode.FLASHBACK);
|
||||
this.setAdditionalCostsRuleVisible(false);
|
||||
this.name = "Flashback " + cost.getText();
|
||||
this.addCost(cost);
|
||||
this.timing = card.isSorcery() ? TimingRule.SORCERY : TimingRule.INSTANT;
|
||||
super(card, cost, SpellAbilityCastMode.FLASHBACK);
|
||||
}
|
||||
|
||||
protected FlashbackAbility(final FlashbackAbility ability) {
|
||||
super(ability);
|
||||
this.spellAbilityType = ability.spellAbilityType;
|
||||
this.abilityName = ability.abilityName;
|
||||
this.spellAbilityToResolve = ability.spellAbilityToResolve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActivationStatus canActivate(UUID playerId, Game game) {
|
||||
// flashback ability dynamicly added to all card's parts (split cards)
|
||||
if (super.canActivate(playerId, game).canActivate()) {
|
||||
Card card = game.getCard(getSourceId());
|
||||
if (card != null) {
|
||||
// Card must be in the graveyard zone
|
||||
if (game.getState().getZone(card.getId()) != Zone.GRAVEYARD) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
// Cards with no Mana Costs cant't be flashbacked (e.g. Ancestral Vision)
|
||||
if (card.getManaCost().isEmpty()) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
// Flashback can never cast a split card by Fuse, because Fuse only works from hand
|
||||
// https://tappedout.net/mtg-questions/snapcaster-mage-and-flashback-on-a-fuse-card-one-or-both-halves-legal-targets/
|
||||
if (card instanceof SplitCard) {
|
||||
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
|
||||
return ((SplitCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
return ((SplitCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
}
|
||||
} else if (card instanceof ModalDoubleFacedCard) {
|
||||
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
|
||||
return ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
return ((ModalDoubleFacedCard) card).getRightHalfCard().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;
|
||||
if (card instanceof SplitCard) {
|
||||
if (((SplitCard) card).getLeftHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((SplitCard) card).getLeftHalfCard().getSpellAbility().copy();
|
||||
} else if (((SplitCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((SplitCard) card).getRightHalfCard().getSpellAbility().copy();
|
||||
}
|
||||
} else if (card instanceof ModalDoubleFacedCard) {
|
||||
if (((ModalDoubleFacedCard) card).getLeftHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((ModalDoubleFacedCard) card).getLeftHalfCard().getSpellAbility().copy();
|
||||
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().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;
|
||||
ContinuousEffect effect = new FlashbackReplacementEffect();
|
||||
effect.setTargetPointer(new FixedTarget(getSourceId(), game.getState().getZoneChangeCounter(getSourceId())));
|
||||
game.addEffect(effect, this);
|
||||
}
|
||||
}
|
||||
return spellAbilityToResolve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Costs<Cost> getCosts() {
|
||||
if (spellAbilityToResolve == null) {
|
||||
return super.getCosts();
|
||||
}
|
||||
return spellAbilityToResolve.getCosts();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -140,11 +33,6 @@ public class FlashbackAbility extends SpellAbility {
|
|||
return new FlashbackAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule(boolean all) {
|
||||
return this.getRule();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
StringBuilder sbRule = new StringBuilder("Flashback");
|
||||
|
|
@ -170,66 +58,4 @@ public class FlashbackAbility extends SpellAbility {
|
|||
sbRule.append(" <i>(You may cast this card from your graveyard for its flashback cost. Then exile it.)</i>");
|
||||
return sbRule.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for split card in PlayerImpl method:
|
||||
* getOtherUseableActivatedAbilities
|
||||
*
|
||||
* @param abilityName
|
||||
*/
|
||||
public FlashbackAbility setAbilityName(String abilityName) {
|
||||
this.abilityName = abilityName;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FlashbackReplacementEffect extends ReplacementEffectImpl {
|
||||
|
||||
public FlashbackReplacementEffect() {
|
||||
super(Duration.OneUse, Outcome.Exile);
|
||||
staticText = "(If the flashback cost was paid, exile this card instead of putting it anywhere else any time it would leave the stack)";
|
||||
}
|
||||
|
||||
protected FlashbackReplacementEffect(final FlashbackReplacementEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlashbackReplacementEffect copy() {
|
||||
return new FlashbackReplacementEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
if (controller != null) {
|
||||
Card card = game.getCard(event.getTargetId());
|
||||
if (card != null) {
|
||||
discard();
|
||||
return controller.moveCards(
|
||||
card, Zone.EXILED, source, game, false, false, false, event.getAppliedEffects());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checksEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.ZONE_CHANGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(GameEvent event, Ability source, Game game) {
|
||||
UUID cardId = CardUtil.getMainCardId(game, source.getSourceId()); // for split cards
|
||||
if (cardId.equals(event.getTargetId())
|
||||
&& ((ZoneChangeEvent) event).getFromZone() == Zone.STACK
|
||||
&& ((ZoneChangeEvent) event).getToZone() != Zone.EXILED) {
|
||||
|
||||
int zcc = game.getState().getZoneChangeCounter(cardId);
|
||||
return ((FixedTarget) getTargetPointer()).getZoneChangeCounter() + 1 == zcc;
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ public class ForetellAbility extends SpecialAction {
|
|||
game.getState().addOtherAbility(rightHalfCard, ability);
|
||||
}
|
||||
}
|
||||
} else if (card instanceof AdventureCard) {
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
if (foretellCost != null) {
|
||||
Card creatureCard = card.getMainCard();
|
||||
ForetellCostAbility ability = new ForetellCostAbility(foretellCost);
|
||||
|
|
@ -268,7 +268,7 @@ public class ForetellAbility extends SpecialAction {
|
|||
game.getState().addOtherAbility(creatureCard, ability);
|
||||
}
|
||||
if (foretellSplitCost != null) {
|
||||
Card spellCard = ((AdventureCard) card).getSpellCard();
|
||||
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
|
||||
ForetellCostAbility ability = new ForetellCostAbility(foretellSplitCost);
|
||||
ability.setSourceId(spellCard.getId());
|
||||
ability.setControllerId(source.getControllerId());
|
||||
|
|
@ -360,11 +360,11 @@ public class ForetellAbility extends SpecialAction {
|
|||
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
return ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
}
|
||||
} else if (card instanceof AdventureCard) {
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
if (card.getMainCard().getName().equals(abilityName)) {
|
||||
return card.getMainCard().getSpellAbility().canActivate(playerId, game);
|
||||
} else if (((AdventureCard) card).getSpellCard().getName().equals(abilityName)) {
|
||||
return ((AdventureCard) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
|
||||
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) {
|
||||
return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
|
||||
}
|
||||
}
|
||||
return card.getSpellAbility().canActivate(playerId, game);
|
||||
|
|
@ -391,11 +391,11 @@ public class ForetellAbility extends SpecialAction {
|
|||
} else if (((ModalDoubleFacedCard) card).getRightHalfCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((ModalDoubleFacedCard) card).getRightHalfCard().getSpellAbility().copy();
|
||||
}
|
||||
} else if (card instanceof AdventureCard) {
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
if (card.getMainCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = card.getMainCard().getSpellAbility().copy();
|
||||
} else if (((AdventureCard) card).getSpellCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((AdventureCard) card).getSpellCard().getSpellAbility().copy();
|
||||
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(abilityName)) {
|
||||
spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy();
|
||||
}
|
||||
} else {
|
||||
spellAbilityCopy = card.getSpellAbility().copy();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,43 @@
|
|||
package mage.abilities.keyword;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.VariableCostImpl;
|
||||
import mage.abilities.costs.VariableCostType;
|
||||
import mage.abilities.costs.common.TapTargetCost;
|
||||
import mage.abilities.costs.mana.ManaCostsImpl;
|
||||
import mage.abilities.effects.common.cost.CostModificationEffectImpl;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.Zone;
|
||||
import mage.constants.*;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.filter.common.FilterControlledPermanent;
|
||||
import mage.filter.predicate.permanent.PermanentIdPredicate;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
import mage.target.TargetPermanent;
|
||||
import mage.target.common.TargetControlledPermanent;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* TODO: Implement this
|
||||
*
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class HarmonizeAbility extends SpellAbility {
|
||||
public class HarmonizeAbility extends CastFromGraveyardAbility {
|
||||
|
||||
private String abilityName;
|
||||
private SpellAbility spellAbilityToResolve;
|
||||
|
||||
public HarmonizeAbility(Card card, String manaString) {
|
||||
super(new ManaCostsImpl<>(manaString), card.getName(), Zone.GRAVEYARD);
|
||||
super(card, new ManaCostsImpl<>(manaString), SpellAbilityCastMode.HARMONIZE);
|
||||
this.addCost(new HarmonizeCost());
|
||||
this.addSubAbility(new SimpleStaticAbility(Zone.ALL, new HarmonizeCostReductionEffect()).setRuleVisible(false));
|
||||
}
|
||||
|
||||
private HarmonizeAbility(final HarmonizeAbility ability) {
|
||||
|
|
@ -24,4 +48,132 @@ public class HarmonizeAbility extends SpellAbility {
|
|||
public HarmonizeAbility copy() {
|
||||
return new HarmonizeAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return name + " <i>(You may cast this card from your graveyard for its harmonize cost. " +
|
||||
"You may tap a creature you control to reduce that cost by {X}, " +
|
||||
"where X is its power. Then exile this spell.)</i>";
|
||||
}
|
||||
}
|
||||
|
||||
class HarmonizeCostReductionEffect extends CostModificationEffectImpl {
|
||||
|
||||
HarmonizeCostReductionEffect() {
|
||||
super(Duration.WhileOnStack, Outcome.Benefit, CostModificationType.REDUCE_COST);
|
||||
}
|
||||
|
||||
private HarmonizeCostReductionEffect(final HarmonizeCostReductionEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source, Ability abilityToModify) {
|
||||
SpellAbility spellAbility = (SpellAbility) abilityToModify;
|
||||
int power;
|
||||
if (game.inCheckPlayableState()) {
|
||||
power = game
|
||||
.getBattlefield()
|
||||
.getActivePermanents(
|
||||
StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE,
|
||||
source.getControllerId(), source, game
|
||||
).stream()
|
||||
.map(MageObject::getPower)
|
||||
.mapToInt(MageInt::getValue)
|
||||
.max()
|
||||
.orElse(0);
|
||||
} else {
|
||||
power = CardUtil
|
||||
.castStream(spellAbility.getCosts().stream(), HarmonizeCost.class)
|
||||
.map(HarmonizeCost::getChosenCreature)
|
||||
.map(game::getPermanent)
|
||||
.filter(Objects::nonNull)
|
||||
.map(MageObject::getPower)
|
||||
.mapToInt(MageInt::getValue)
|
||||
.map(x -> Math.max(x, 0))
|
||||
.sum();
|
||||
}
|
||||
if (power > 0) {
|
||||
CardUtil.adjustCost(spellAbility, power);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(Ability abilityToModify, Ability source, Game game) {
|
||||
return abilityToModify instanceof SpellAbility
|
||||
&& abilityToModify.getSourceId().equals(source.getSourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HarmonizeCostReductionEffect copy() {
|
||||
return new HarmonizeCostReductionEffect(this);
|
||||
}
|
||||
}
|
||||
|
||||
class HarmonizeCost extends VariableCostImpl {
|
||||
|
||||
private UUID chosenCreature = null;
|
||||
|
||||
HarmonizeCost() {
|
||||
super(VariableCostType.ADDITIONAL, "", "");
|
||||
}
|
||||
|
||||
private HarmonizeCost(final HarmonizeCost cost) {
|
||||
super(cost);
|
||||
this.chosenCreature = cost.chosenCreature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HarmonizeCost copy() {
|
||||
return new HarmonizeCost(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearPaid() {
|
||||
super.clearPaid();
|
||||
chosenCreature = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxValue(Ability source, Game game) {
|
||||
return game.getBattlefield().contains(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1) ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int announceXValue(Ability source, Game game) {
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
if (player == null || !game.getBattlefield().contains(
|
||||
StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE, source, game, 1
|
||||
) || !player.chooseUse(
|
||||
Outcome.Benefit, "Tap an untapped creature you control for harmonize?", source, game
|
||||
)) {
|
||||
return 0;
|
||||
}
|
||||
TargetPermanent target = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_UNTAPPED_CREATURE);
|
||||
target.withNotTarget(true);
|
||||
target.withChooseHint("for harmonize");
|
||||
player.choose(Outcome.PlayForFree, target, source, game);
|
||||
Permanent permanent = game.getPermanent(target.getFirstTarget());
|
||||
if (permanent == null) {
|
||||
return 0;
|
||||
}
|
||||
chosenCreature = permanent.getId();
|
||||
return 1;
|
||||
}
|
||||
|
||||
private FilterControlledPermanent makeFilter() {
|
||||
FilterControlledPermanent filter = new FilterControlledPermanent("tap the chosen creature");
|
||||
filter.add(new PermanentIdPredicate(chosenCreature));
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cost getFixedCostsFromAnnouncedValue(int xValue) {
|
||||
return new TapTargetCost(new TargetControlledPermanent(xValue, xValue, makeFilter(), true));
|
||||
}
|
||||
|
||||
public UUID getChosenCreature() {
|
||||
return chosenCreature;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,11 +268,11 @@ class PlotSpellAbility extends SpellAbility {
|
|||
} else if (((CardWithHalves) mainCard).getRightHalfCard().getName().equals(faceCardName)) {
|
||||
return ((CardWithHalves) mainCard).getRightHalfCard().getSpellAbility().canActivate(playerId, game);
|
||||
}
|
||||
} else if (card instanceof AdventureCard) {
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
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);
|
||||
} else if (((CardWithSpellOption) card).getSpellCard().getName().equals(faceCardName)) {
|
||||
return ((CardWithSpellOption) card).getSpellCard().getSpellAbility().canActivate(playerId, game);
|
||||
}
|
||||
}
|
||||
return card.getSpellAbility().canActivate(playerId, game);
|
||||
|
|
@ -294,11 +294,11 @@ class PlotSpellAbility extends SpellAbility {
|
|||
} else if (((CardWithHalves) card).getRightHalfCard().getName().equals(faceCardName)) {
|
||||
spellAbilityCopy = ((CardWithHalves) card).getRightHalfCard().getSpellAbility().copy();
|
||||
}
|
||||
} else if (card instanceof AdventureCard) {
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
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 if (((CardWithSpellOption) card).getSpellCard().getName().equals(faceCardName)) {
|
||||
spellAbilityCopy = ((CardWithSpellOption) card).getSpellCard().getSpellAbility().copy();
|
||||
}
|
||||
} else {
|
||||
spellAbilityCopy = card.getSpellAbility().copy();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package mage.abilities.keyword;
|
||||
|
||||
import mage.ApprovingObject;
|
||||
import mage.MageIdentifier;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpecialAction;
|
||||
|
|
@ -17,8 +16,11 @@ import mage.abilities.effects.ContinuousEffectImpl;
|
|||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.abilities.effects.common.counter.RemoveCounterSourceEffect;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.CardsImpl;
|
||||
import mage.cards.ModalDoubleFacedCard;
|
||||
import mage.constants.*;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
|
@ -26,8 +28,6 @@ import mage.players.Player;
|
|||
import mage.target.targetpointer.FixedTarget;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -112,7 +112,6 @@ import java.util.UUID;
|
|||
public class SuspendAbility extends SpecialAction {
|
||||
|
||||
private final String ruleText;
|
||||
private boolean gainedTemporary;
|
||||
|
||||
/**
|
||||
* Gives the card the SuspendAbility
|
||||
|
|
@ -132,6 +131,8 @@ public class SuspendAbility extends SpecialAction {
|
|||
this.addEffect(new SuspendExileEffect(suspend));
|
||||
this.usesStack = false;
|
||||
if (suspend == Integer.MAX_VALUE) {
|
||||
// example: Suspend X-{X}{W}{W}. X can't be 0.
|
||||
// TODO: replace by costAdjuster for shared logic
|
||||
VariableManaCost xCosts = new VariableManaCost(VariableCostType.ALTERNATIVE);
|
||||
xCosts.setMinX(1);
|
||||
this.addCost(xCosts);
|
||||
|
|
@ -175,6 +176,11 @@ public class SuspendAbility extends SpecialAction {
|
|||
* or added by Jhoira of the Ghitu
|
||||
*/
|
||||
public static void addSuspendTemporaryToCard(Card card, Ability source, Game game) {
|
||||
if (card instanceof ModalDoubleFacedCard) {
|
||||
// Need to ensure the suspend ability gets put on the left side card
|
||||
// since counters get added to this card.
|
||||
card = ((ModalDoubleFacedCard) card).getLeftHalfCard();
|
||||
}
|
||||
SuspendAbility ability = new SuspendAbility(0, null, card, false);
|
||||
ability.setSourceId(card.getId());
|
||||
ability.setControllerId(card.getOwnerId());
|
||||
|
|
@ -204,7 +210,6 @@ public class SuspendAbility extends SpecialAction {
|
|||
private SuspendAbility(final SuspendAbility ability) {
|
||||
super(ability);
|
||||
this.ruleText = ability.ruleText;
|
||||
this.gainedTemporary = ability.gainedTemporary;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -230,10 +235,6 @@ public class SuspendAbility extends SpecialAction {
|
|||
return ruleText;
|
||||
}
|
||||
|
||||
public boolean isGainedTemporary() {
|
||||
return gainedTemporary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuspendAbility copy() {
|
||||
return new SuspendAbility(this);
|
||||
|
|
@ -343,40 +344,16 @@ class SuspendPlayCardEffect extends OneShotEffect {
|
|||
if (player == null || card == null) {
|
||||
return false;
|
||||
}
|
||||
if (!player.chooseUse(Outcome.Benefit, "Play " + card.getLogName() + " without paying its mana cost?", source, game)) {
|
||||
// ensure we're getting the main card when passing to CardUtil to check all parts of card
|
||||
// MDFC points to left half card
|
||||
card = card.getMainCard();
|
||||
// cast/play the card for free
|
||||
if (!CardUtil.castSpellWithAttributesForFree(player, source, game, new CardsImpl(card),
|
||||
StaticFilters.FILTER_CARD, null, true)) {
|
||||
return true;
|
||||
}
|
||||
// remove temporary suspend ability (used e.g. for Epochrasite)
|
||||
// TODO: isGainedTemporary is not set or use in other places, so it can be deleted?!
|
||||
List<Ability> abilitiesToRemove = new ArrayList<>();
|
||||
for (Ability ability : card.getAbilities(game)) {
|
||||
if (ability instanceof SuspendAbility && (((SuspendAbility) ability).isGainedTemporary())) {
|
||||
abilitiesToRemove.add(ability);
|
||||
}
|
||||
}
|
||||
if (!abilitiesToRemove.isEmpty()) {
|
||||
for (Ability ability : card.getAbilities(game)) {
|
||||
if (ability instanceof SuspendBeginningOfUpkeepInterveningIfTriggeredAbility
|
||||
|| ability instanceof SuspendPlayCardAbility) {
|
||||
abilitiesToRemove.add(ability);
|
||||
}
|
||||
}
|
||||
// remove the abilities from the card
|
||||
// TODO: will not work with Adventure Cards and another auto-generated abilities list
|
||||
// TODO: is it work after blink or return to hand?
|
||||
/*
|
||||
bug example:
|
||||
Epochrasite bug: It comes out of suspend, is cast and enters the battlefield. THEN if it's returned to
|
||||
its owner's hand from battlefield, the bounced Epochrasite can't be cast for the rest of the game.
|
||||
*/
|
||||
card.getAbilities().removeAll(abilitiesToRemove);
|
||||
}
|
||||
// cast the card for free
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE);
|
||||
boolean cardWasCast = player.cast(player.chooseAbilityForCast(card, game, true),
|
||||
game, true, new ApprovingObject(source, game));
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null);
|
||||
if (cardWasCast && (card.isCreature(game))) {
|
||||
// creatures cast from suspend gain haste
|
||||
if ((card.isCreature(game))) {
|
||||
ContinuousEffect effect = new GainHasteEffect();
|
||||
effect.setTargetPointer(new FixedTarget(card.getId(), card.getZoneChangeCounter(game) + 1));
|
||||
game.addEffect(effect, source);
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public class WardAbility extends TriggeredAbilityImpl {
|
|||
if (!getSourceId().equals(event.getTargetId())) {
|
||||
return false;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getId().toString(), event, game);
|
||||
if (targetingObject == null || !game.getOpponents(getControllerId()).contains(targetingObject.getControllerId())) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,98 +1,29 @@
|
|||
package mage.cards;
|
||||
|
||||
import mage.abilities.Abilities;
|
||||
import mage.abilities.AbilitiesImpl;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SpellAbilityType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author phulin
|
||||
*/
|
||||
public abstract class AdventureCard extends CardImpl {
|
||||
|
||||
/* The adventure spell card, i.e. Swift End. */
|
||||
protected AdventureCardSpell spellCard;
|
||||
public abstract class AdventureCard extends CardWithSpellOption {
|
||||
|
||||
public AdventureCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String adventureName, String costsSpell) {
|
||||
super(ownerId, setInfo, types, costs);
|
||||
this.spellCard = new AdventureCardSpellImpl(ownerId, setInfo, adventureName, typesSpell, costsSpell, this);
|
||||
this.spellCard = new AdventureSpellCard(ownerId, setInfo, adventureName, typesSpell, costsSpell, this);
|
||||
}
|
||||
|
||||
public AdventureCard(AdventureCard card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
public void finalizeAdventure() {
|
||||
spellCard.finalizeAdventure();
|
||||
}
|
||||
|
||||
protected AdventureCard(final AdventureCard card) {
|
||||
super(card);
|
||||
this.spellCard = card.getSpellCard().copy();
|
||||
this.spellCard.setParentCard(this);
|
||||
}
|
||||
|
||||
public AdventureCardSpell getSpellCard() {
|
||||
return spellCard;
|
||||
}
|
||||
|
||||
public void setParts(AdventureCardSpell cardSpell) {
|
||||
// for card copy only - set new parts
|
||||
this.spellCard = cardSpell;
|
||||
cardSpell.setParentCard(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignNewId() {
|
||||
super.assignNewId();
|
||||
spellCard.assignNewId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
|
||||
if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
|
||||
Zone currentZone = game.getState().getZone(getId());
|
||||
game.getState().setZone(getSpellCard().getId(), currentZone);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZone(Zone zone, Game game) {
|
||||
super.setZone(zone, game);
|
||||
game.setZone(getSpellCard().getId(), zone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
|
||||
if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
|
||||
Zone currentZone = game.getState().getZone(getId());
|
||||
game.getState().setZone(getSpellCard().getId(), currentZone);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
|
||||
// zone contains only one main card
|
||||
return super.removeFromZone(game, fromZone, source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
|
||||
if (isCopy()) { // same as meld cards
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
return;
|
||||
}
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
getSpellCard().updateZoneChangeCounter(game, event);
|
||||
spellCard.finalizeSpell();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -103,45 +34,4 @@ public abstract class AdventureCard extends CardImpl {
|
|||
this.getSpellCard().getSpellAbility().setControllerId(controllerId);
|
||||
return super.cast(game, fromZone, ability, controllerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Abilities<Ability> getAbilities() {
|
||||
Abilities<Ability> allAbilities = new AbilitiesImpl<>();
|
||||
allAbilities.addAll(spellCard.getAbilities());
|
||||
allAbilities.addAll(super.getAbilities());
|
||||
return allAbilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Abilities<Ability> getInitAbilities() {
|
||||
// must init only parent related abilities, spell card must be init separately
|
||||
return super.getAbilities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Abilities<Ability> getAbilities(Game game) {
|
||||
Abilities<Ability> allAbilities = new AbilitiesImpl<>();
|
||||
allAbilities.addAll(spellCard.getAbilities(game));
|
||||
allAbilities.addAll(super.getAbilities(game));
|
||||
return allAbilities;
|
||||
}
|
||||
|
||||
public Abilities<Ability> getSharedAbilities(Game game) {
|
||||
// abilities without spellcard
|
||||
return super.getAbilities(game);
|
||||
}
|
||||
|
||||
public List<String> getSharedRules(Game game) {
|
||||
// rules without spellcard
|
||||
Abilities<Ability> sourceAbilities = this.getSharedAbilities(game);
|
||||
return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOwnerId(UUID ownerId) {
|
||||
super.setOwnerId(ownerId);
|
||||
abilities.setControllerId(ownerId);
|
||||
spellCard.getAbilities().setControllerId(ownerId);
|
||||
spellCard.setOwnerId(ownerId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
package mage.cards;
|
||||
|
||||
/**
|
||||
* @author phulin
|
||||
*/
|
||||
public interface AdventureCardSpell extends SubCard<AdventureCard> {
|
||||
|
||||
@Override
|
||||
AdventureCardSpell copy();
|
||||
|
||||
void finalizeAdventure();
|
||||
}
|
||||
|
|
@ -19,11 +19,11 @@ import java.util.stream.Collectors;
|
|||
/**
|
||||
* @author phulin
|
||||
*/
|
||||
public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpell {
|
||||
public class AdventureSpellCard extends CardImpl implements SpellOptionCard {
|
||||
|
||||
private AdventureCard adventureCardParent;
|
||||
|
||||
public AdventureCardSpellImpl(UUID ownerId, CardSetInfo setInfo, String adventureName, CardType[] cardTypes, String costs, AdventureCard adventureCardParent) {
|
||||
public AdventureSpellCard(UUID ownerId, CardSetInfo setInfo, String adventureName, CardType[] cardTypes, String costs, AdventureCard adventureCardParent) {
|
||||
super(ownerId, setInfo, cardTypes, costs, SpellAbilityType.ADVENTURE_SPELL);
|
||||
this.subtype.add(SubType.ADVENTURE);
|
||||
|
||||
|
|
@ -35,13 +35,13 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
|
|||
this.adventureCardParent = adventureCardParent;
|
||||
}
|
||||
|
||||
public void finalizeAdventure() {
|
||||
public void finalizeSpell() {
|
||||
if (spellAbility instanceof AdventureCardSpellAbility) {
|
||||
((AdventureCardSpellAbility) spellAbility).finalizeAdventure();
|
||||
}
|
||||
}
|
||||
|
||||
protected AdventureCardSpellImpl(final AdventureCardSpellImpl card) {
|
||||
protected AdventureSpellCard(final AdventureSpellCard card) {
|
||||
super(card);
|
||||
this.adventureCardParent = card.adventureCardParent;
|
||||
}
|
||||
|
|
@ -83,13 +83,13 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
|
|||
}
|
||||
|
||||
@Override
|
||||
public AdventureCardSpellImpl copy() {
|
||||
return new AdventureCardSpellImpl(this);
|
||||
public AdventureSpellCard copy() {
|
||||
return new AdventureSpellCard(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParentCard(AdventureCard card) {
|
||||
this.adventureCardParent = card;
|
||||
public void setParentCard(CardWithSpellOption card) {
|
||||
this.adventureCardParent = (AdventureCard) card;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -102,6 +102,11 @@ public class AdventureCardSpellImpl extends CardImpl implements AdventureCardSpe
|
|||
// id must send to main card (popup card hint in game logs)
|
||||
return getName() + " [" + adventureCardParent.getId().toString().substring(0, 3) + ']';
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSpellType() {
|
||||
return "Adventure";
|
||||
}
|
||||
}
|
||||
|
||||
class AdventureCardSpellAbility extends SpellAbility {
|
||||
|
|
@ -141,8 +146,8 @@ class AdventureCardSpellAbility extends SpellAbility {
|
|||
public ActivationStatus canActivate(UUID playerId, Game game) {
|
||||
ExileZone adventureExileZone = game.getExile().getExileZone(ExileAdventureSpellEffect.adventureExileId(playerId, game));
|
||||
Card spellCard = game.getCard(this.getSourceId());
|
||||
if (spellCard instanceof AdventureCardSpell) {
|
||||
Card card = ((AdventureCardSpell) spellCard).getParentCard();
|
||||
if (spellCard instanceof AdventureSpellCard) {
|
||||
Card card = ((AdventureSpellCard) spellCard).getParentCard();
|
||||
if (adventureExileZone != null && adventureExileZone.contains(card.getId())) {
|
||||
return ActivationStatus.getFalse();
|
||||
}
|
||||
|
|
@ -530,8 +530,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
}
|
||||
}
|
||||
|
||||
if (stackObject == null && (this instanceof AdventureCard)) {
|
||||
stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false);
|
||||
if (stackObject == null && (this instanceof CardWithSpellOption)) {
|
||||
stackObject = game.getStack().getSpell(((CardWithSpellOption) this).getSpellCard().getId(), false);
|
||||
}
|
||||
|
||||
if (stackObject == null) {
|
||||
|
|
|
|||
131
Mage/src/main/java/mage/cards/CardWithSpellOption.java
Normal file
131
Mage/src/main/java/mage/cards/CardWithSpellOption.java
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package mage.cards;
|
||||
|
||||
import mage.abilities.Abilities;
|
||||
import mage.abilities.AbilitiesImpl;
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author phulin, jmlundeen
|
||||
*/
|
||||
public abstract class CardWithSpellOption extends CardImpl {
|
||||
|
||||
/* The adventure/omen spell card, i.e. Swift End. */
|
||||
protected SpellOptionCard spellCard;
|
||||
|
||||
public CardWithSpellOption(UUID ownerId, CardSetInfo setInfo, CardType[] types, String costs) {
|
||||
super(ownerId, setInfo, types, costs);
|
||||
}
|
||||
|
||||
public CardWithSpellOption(CardWithSpellOption card) {
|
||||
super(card);
|
||||
this.spellCard = card.getSpellCard().copy();
|
||||
this.spellCard.setParentCard(this);
|
||||
}
|
||||
|
||||
public SpellOptionCard getSpellCard() {
|
||||
return spellCard;
|
||||
}
|
||||
|
||||
public void setParts(SpellOptionCard cardSpell) {
|
||||
// for card copy only - set new parts
|
||||
this.spellCard = cardSpell;
|
||||
cardSpell.setParentCard(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assignNewId() {
|
||||
super.assignNewId();
|
||||
spellCard.assignNewId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
|
||||
if (super.moveToZone(toZone, source, game, flag, appliedEffects)) {
|
||||
Zone currentZone = game.getState().getZone(getId());
|
||||
game.getState().setZone(getSpellCard().getId(), currentZone);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZone(Zone zone, Game game) {
|
||||
super.setZone(zone, game);
|
||||
game.setZone(getSpellCard().getId(), zone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
|
||||
if (super.moveToExile(exileId, name, source, game, appliedEffects)) {
|
||||
Zone currentZone = game.getState().getZone(getId());
|
||||
game.getState().setZone(getSpellCard().getId(), currentZone);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
|
||||
// zone contains only one main card
|
||||
return super.removeFromZone(game, fromZone, source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) {
|
||||
if (isCopy()) { // same as meld cards
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
return;
|
||||
}
|
||||
super.updateZoneChangeCounter(game, event);
|
||||
getSpellCard().updateZoneChangeCounter(game, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Abilities<Ability> getAbilities() {
|
||||
Abilities<Ability> allAbilities = new AbilitiesImpl<>();
|
||||
allAbilities.addAll(spellCard.getAbilities());
|
||||
allAbilities.addAll(super.getAbilities());
|
||||
return allAbilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Abilities<Ability> getInitAbilities() {
|
||||
// must init only parent related abilities, spell card must be init separately
|
||||
return super.getAbilities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Abilities<Ability> getAbilities(Game game) {
|
||||
Abilities<Ability> allAbilities = new AbilitiesImpl<>();
|
||||
allAbilities.addAll(spellCard.getAbilities(game));
|
||||
allAbilities.addAll(super.getAbilities(game));
|
||||
return allAbilities;
|
||||
}
|
||||
|
||||
public Abilities<Ability> getSharedAbilities(Game game) {
|
||||
// abilities without spellCard
|
||||
return super.getAbilities(game);
|
||||
}
|
||||
|
||||
public List<String> getSharedRules(Game game) {
|
||||
// rules without spellCard
|
||||
Abilities<Ability> sourceAbilities = this.getSharedAbilities(game);
|
||||
return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOwnerId(UUID ownerId) {
|
||||
super.setOwnerId(ownerId);
|
||||
abilities.setControllerId(ownerId);
|
||||
spellCard.getAbilities().setControllerId(ownerId);
|
||||
spellCard.setOwnerId(ownerId);
|
||||
}
|
||||
}
|
||||
34
Mage/src/main/java/mage/cards/OmenCard.java
Normal file
34
Mage/src/main/java/mage/cards/OmenCard.java
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package mage.cards;
|
||||
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SpellAbilityType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class OmenCard extends CardWithSpellOption {
|
||||
|
||||
public OmenCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String omenName, String costsSpell) {
|
||||
super(ownerId, setInfo, types, costs);
|
||||
this.spellCard = new OmenSpellCard(ownerId, setInfo, omenName, typesSpell, costsSpell, this);
|
||||
}
|
||||
|
||||
public OmenCard(OmenCard card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
public void finalizeOmen() {
|
||||
spellCard.finalizeSpell();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) {
|
||||
if (ability.getSpellAbilityType() == SpellAbilityType.OMEN_SPELL) {
|
||||
return this.getSpellCard().cast(game, fromZone, ability, controllerId);
|
||||
}
|
||||
this.getSpellCard().getSpellAbility().setControllerId(controllerId);
|
||||
return super.cast(game, fromZone, ability, controllerId);
|
||||
}
|
||||
}
|
||||
174
Mage/src/main/java/mage/cards/OmenSpellCard.java
Normal file
174
Mage/src/main/java/mage/cards/OmenSpellCard.java
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package mage.cards;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.Modes;
|
||||
import mage.abilities.SpellAbility;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.common.ShuffleIntoLibrarySourceEffect;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SpellAbilityType;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class OmenSpellCard extends CardImpl implements SpellOptionCard {
|
||||
|
||||
private OmenCard omenCardParent;
|
||||
|
||||
public OmenSpellCard(UUID ownerId, CardSetInfo setInfo, String omenName, CardType[] cardTypes, String costs, OmenCard omenCard) {
|
||||
super(ownerId, setInfo, cardTypes, costs, SpellAbilityType.OMEN_SPELL);
|
||||
this.subtype.add(SubType.OMEN);
|
||||
|
||||
OmenCardSpellAbility newSpellAbility = new OmenCardSpellAbility(getSpellAbility(), omenName, cardTypes, costs);
|
||||
this.replaceSpellAbility(newSpellAbility);
|
||||
spellAbility = newSpellAbility;
|
||||
|
||||
this.setName(omenName);
|
||||
this.omenCardParent = omenCard;
|
||||
}
|
||||
|
||||
public void finalizeSpell() {
|
||||
if (spellAbility instanceof OmenCardSpellAbility) {
|
||||
((OmenCardSpellAbility) spellAbility).finalizeOmen();
|
||||
}
|
||||
}
|
||||
|
||||
protected OmenSpellCard(final OmenSpellCard card) {
|
||||
super(card);
|
||||
this.omenCardParent = card.omenCardParent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getOwnerId() {
|
||||
return omenCardParent.getOwnerId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExpansionSetCode() {
|
||||
return omenCardParent.getExpansionSetCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCardNumber() {
|
||||
return omenCardParent.getCardNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToZone(Zone toZone, Ability source, Game game, boolean flag, List<UUID> appliedEffects) {
|
||||
return omenCardParent.moveToZone(toZone, source, game, flag, appliedEffects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToExile(UUID exileId, String name, Ability source, Game game, List<UUID> appliedEffects) {
|
||||
return omenCardParent.moveToExile(exileId, name, source, game, appliedEffects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OmenCard getMainCard() {
|
||||
return omenCardParent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZone(Zone zone, Game game) {
|
||||
game.setZone(omenCardParent.getId(), zone);
|
||||
game.setZone(omenCardParent.getSpellCard().getId(), zone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OmenSpellCard copy() {
|
||||
return new OmenSpellCard(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParentCard(CardWithSpellOption card) {
|
||||
this.omenCardParent = (OmenCard) card;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OmenCard getParentCard() {
|
||||
return this.omenCardParent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdName() {
|
||||
// id must send to main card (popup card hint in game logs)
|
||||
return getName() + " [" + omenCardParent.getId().toString().substring(0, 3) + ']';
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSpellType() {
|
||||
return "Omen";
|
||||
}
|
||||
}
|
||||
|
||||
class OmenCardSpellAbility extends SpellAbility {
|
||||
|
||||
private String nameFull;
|
||||
private boolean finalized = false;
|
||||
|
||||
public OmenCardSpellAbility(final SpellAbility baseSpellAbility, String omenName, CardType[] cardTypes, String costs) {
|
||||
super(baseSpellAbility);
|
||||
this.setName(cardTypes, omenName, costs);
|
||||
this.setCardName(omenName);
|
||||
}
|
||||
|
||||
public void finalizeOmen() {
|
||||
if (finalized) {
|
||||
throw new IllegalStateException("Wrong code usage. "
|
||||
+ "Omen (" + cardName + ") "
|
||||
+ "need to call finalizeOmen() exactly once.");
|
||||
}
|
||||
Effect effect = new ShuffleIntoLibrarySourceEffect();
|
||||
effect.setText("");
|
||||
this.addEffect(effect);
|
||||
this.finalized = true;
|
||||
}
|
||||
|
||||
protected OmenCardSpellAbility(final OmenCardSpellAbility ability) {
|
||||
super(ability);
|
||||
this.nameFull = ability.nameFull;
|
||||
if (!ability.finalized) {
|
||||
throw new IllegalStateException("Wrong code usage. "
|
||||
+ "Omen (" + cardName + ") "
|
||||
+ "need to call finalizeOmen() at the very end of the card's constructor.");
|
||||
}
|
||||
this.finalized = true;
|
||||
}
|
||||
|
||||
public void setName(CardType[] cardTypes, String omenName, String costs) {
|
||||
this.nameFull = "Omen " + Arrays.stream(cardTypes).map(CardType::toString).collect(Collectors.joining(" ")) + " — " + omenName;
|
||||
this.name = this.nameFull + " " + costs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule(boolean all) {
|
||||
return this.getRule();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
StringBuilder sbRule = new StringBuilder();
|
||||
sbRule.append(this.nameFull);
|
||||
sbRule.append(" ");
|
||||
sbRule.append(getManaCosts().getText());
|
||||
sbRule.append(" — ");
|
||||
Modes modes = this.getModes();
|
||||
if (modes.size() <= 1) {
|
||||
sbRule.append(modes.getMode().getEffects().getTextStartingUpperCase(modes.getMode()));
|
||||
} else {
|
||||
sbRule.append(getModes().getText());
|
||||
}
|
||||
sbRule.append(" <i>(Then shuffle this card into its owner's library.)<i>");
|
||||
return sbRule.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OmenCardSpellAbility copy() {
|
||||
return new OmenCardSpellAbility(this);
|
||||
}
|
||||
}
|
||||
18
Mage/src/main/java/mage/cards/SpellOptionCard.java
Normal file
18
Mage/src/main/java/mage/cards/SpellOptionCard.java
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package mage.cards;
|
||||
|
||||
public interface SpellOptionCard extends SubCard<CardWithSpellOption> {
|
||||
|
||||
@Override
|
||||
SpellOptionCard copy();
|
||||
|
||||
/**
|
||||
* Adds the final shared ability to the card. e.g. Adventure exile effect / Omen shuffle effect
|
||||
*/
|
||||
void finalizeSpell();
|
||||
|
||||
/**
|
||||
* Used to get the card type text such as Adventure. Currently only used in {@link mage.game.stack.Spell#getSpellCastText Spell} for logging the spell
|
||||
* being cast as part of the two part card.
|
||||
*/
|
||||
String getSpellType();
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import java.util.List;
|
|||
*/
|
||||
public class MockCard extends CardImpl implements MockableCard {
|
||||
|
||||
public static String ADVENTURE_NAME_SEPARATOR = " // ";
|
||||
public static String CARD_WITH_SPELL_OPTION_NAME_SEPARATOR = " // ";
|
||||
public static String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // ";
|
||||
|
||||
// Needs to be here, as it is normally calculated from the
|
||||
|
|
@ -34,7 +34,7 @@ public class MockCard extends CardImpl implements MockableCard {
|
|||
protected List<String> manaCostLeftStr;
|
||||
protected List<String> manaCostRightStr;
|
||||
protected List<String> manaCostStr;
|
||||
protected String adventureSpellName;
|
||||
protected String spellOptionName; // adventure/omen spell name
|
||||
protected boolean isModalDoubleFacedCard;
|
||||
protected int manaValue;
|
||||
|
||||
|
|
@ -71,8 +71,8 @@ public class MockCard extends CardImpl implements MockableCard {
|
|||
this.secondSideCard = new MockCard(CardRepository.instance.findCardWithPreferredSetAndNumber(card.getSecondSideName(), card.getSetCode(), card.getCardNumber()));
|
||||
}
|
||||
|
||||
if (card.isAdventureCard()) {
|
||||
this.adventureSpellName = card.getAdventureSpellName();
|
||||
if (card.isCardWithSpellOption()) {
|
||||
this.spellOptionName = card.getSpellOptionCardName();
|
||||
}
|
||||
|
||||
if (card.isModalDoubleFacedCard()) {
|
||||
|
|
@ -101,7 +101,7 @@ public class MockCard extends CardImpl implements MockableCard {
|
|||
this.manaCostLeftStr = new ArrayList<>(card.manaCostLeftStr);
|
||||
this.manaCostRightStr = new ArrayList<>(card.manaCostRightStr);
|
||||
this.manaCostStr = new ArrayList<>(card.manaCostStr);
|
||||
this.adventureSpellName = card.adventureSpellName;
|
||||
this.spellOptionName = card.spellOptionName;
|
||||
this.isModalDoubleFacedCard = card.isModalDoubleFacedCard;
|
||||
this.manaValue = card.manaValue;
|
||||
}
|
||||
|
|
@ -155,8 +155,8 @@ public class MockCard extends CardImpl implements MockableCard {
|
|||
return getName();
|
||||
}
|
||||
|
||||
if (adventureSpellName != null) {
|
||||
return getName() + ADVENTURE_NAME_SEPARATOR + adventureSpellName;
|
||||
if (spellOptionName != null) {
|
||||
return getName() + CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + spellOptionName;
|
||||
} else if (isModalDoubleFacedCard) {
|
||||
return getName() + MODAL_DOUBLE_FACES_NAME_SEPARATOR + this.getSecondCardFace().getName();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -106,9 +106,9 @@ public class CardInfo {
|
|||
@DatabaseField
|
||||
protected String secondSideName;
|
||||
@DatabaseField
|
||||
protected boolean adventureCard;
|
||||
protected boolean cardWithSpellOption;
|
||||
@DatabaseField
|
||||
protected String adventureSpellName;
|
||||
protected String spellOptionCardName;
|
||||
@DatabaseField
|
||||
protected boolean modalDoubleFacedCard;
|
||||
@DatabaseField
|
||||
|
|
@ -157,9 +157,9 @@ public class CardInfo {
|
|||
this.secondSideName = secondSide.getName();
|
||||
}
|
||||
|
||||
if (card instanceof AdventureCard) {
|
||||
this.adventureCard = true;
|
||||
this.adventureSpellName = ((AdventureCard) card).getSpellCard().getName();
|
||||
if (card instanceof CardWithSpellOption) {
|
||||
this.cardWithSpellOption = true;
|
||||
this.spellOptionCardName = ((CardWithSpellOption) card).getSpellCard().getName();
|
||||
}
|
||||
|
||||
if (card instanceof ModalDoubleFacedCard) {
|
||||
|
|
@ -189,8 +189,8 @@ public class CardInfo {
|
|||
List<String> manaCostLeft = ((ModalDoubleFacedCard) card).getLeftHalfCard().getManaCostSymbols();
|
||||
List<String> manaCostRight = ((ModalDoubleFacedCard) card).getRightHalfCard().getManaCostSymbols();
|
||||
this.setManaCosts(CardUtil.concatManaSymbols(SPLIT_MANA_SEPARATOR_FULL, manaCostLeft, manaCostRight));
|
||||
} else if (card instanceof AdventureCard) {
|
||||
List<String> manaCostLeft = ((AdventureCard) card).getSpellCard().getManaCostSymbols();
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
List<String> manaCostLeft = ((CardWithSpellOption) card).getSpellCard().getManaCostSymbols();
|
||||
List<String> manaCostRight = card.getManaCostSymbols();
|
||||
this.setManaCosts(CardUtil.concatManaSymbols(SPLIT_MANA_SEPARATOR_FULL, manaCostLeft, manaCostRight));
|
||||
} else {
|
||||
|
|
@ -469,12 +469,16 @@ public class CardInfo {
|
|||
return secondSideName;
|
||||
}
|
||||
|
||||
public boolean isAdventureCard() {
|
||||
return adventureCard;
|
||||
public boolean isCardWithSpellOption() {
|
||||
return cardWithSpellOption;
|
||||
}
|
||||
|
||||
public String getAdventureSpellName() {
|
||||
return adventureSpellName;
|
||||
/**
|
||||
* used for spell card portion of adventure/omen cards
|
||||
* @return name of the spell
|
||||
*/
|
||||
public String getSpellOptionCardName() {
|
||||
return spellOptionCardName;
|
||||
}
|
||||
|
||||
public boolean isModalDoubleFacedCard() {
|
||||
|
|
|
|||
|
|
@ -147,8 +147,8 @@ public enum CardRepository {
|
|||
if (card.getMeldsToCardName() != null && !card.getMeldsToCardName().isEmpty()) {
|
||||
namesList.add(card.getMeldsToCardName());
|
||||
}
|
||||
if (card.getAdventureSpellName() != null && !card.getAdventureSpellName().isEmpty()) {
|
||||
namesList.add(card.getAdventureSpellName());
|
||||
if (card.getSpellOptionCardName() != null && !card.getSpellOptionCardName().isEmpty()) {
|
||||
namesList.add(card.getSpellOptionCardName());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
addNewNames(card, names);
|
||||
|
|
@ -176,7 +176,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
|
||||
qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%'));
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
|
|
@ -193,7 +193,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
|
||||
Where<CardInfo, Object> where = qb.where();
|
||||
where.and(
|
||||
where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'),
|
||||
|
|
@ -214,7 +214,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
|
||||
qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%'));
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
|
|
@ -231,7 +231,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
|
||||
qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%'));
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
|
|
@ -248,7 +248,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
|
||||
qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%'));
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
|
|
@ -265,7 +265,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
|
||||
Where<CardInfo, Object> where = qb.where();
|
||||
where.and(
|
||||
where.not().like("types", '%' + CardType.CREATURE.name() + '%'),
|
||||
|
|
@ -286,7 +286,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "spellOptionCardName");
|
||||
Where<CardInfo, Object> where = qb.where();
|
||||
where.and(
|
||||
where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'),
|
||||
|
|
@ -511,7 +511,7 @@ public enum CardRepository {
|
|||
queryBuilder.where()
|
||||
.eq("flipCardName", new SelectArg(name)).or()
|
||||
.eq("secondSideName", new SelectArg(name)).or()
|
||||
.eq("adventureSpellName", new SelectArg(name)).or()
|
||||
.eq("spellOptionCardName", new SelectArg(name)).or()
|
||||
.eq("modalDoubleFacedSecondSideName", new SelectArg(name));
|
||||
results = cardsDao.query(queryBuilder.prepare());
|
||||
} else {
|
||||
|
|
|
|||
75
Mage/src/main/java/mage/constants/ModeChoice.java
Normal file
75
Mage/src/main/java/mage/constants/ModeChoice.java
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package mage.constants;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public enum ModeChoice {
|
||||
|
||||
KHANS("Khans"),
|
||||
DRAGONS("Dragons"),
|
||||
|
||||
MARDU("Mardu"),
|
||||
TEMUR("Temur"),
|
||||
ABZAN("Abzan"),
|
||||
JESKAI("Jeskai"),
|
||||
SULTAI("Sultai"),
|
||||
|
||||
MIRRAN("Mirran"),
|
||||
PHYREXIAN("Phyrexian "),
|
||||
|
||||
ODD("odd"),
|
||||
EVEN("even"),
|
||||
|
||||
BELIEVE("Believe"),
|
||||
DOUBT("Doubt"),
|
||||
|
||||
NCR("NCR"),
|
||||
LEGION("Legion"),
|
||||
|
||||
BROTHERHOOD("Brotherhood"),
|
||||
ENCLAVE("Enclave"),
|
||||
|
||||
ISLAND("Island"),
|
||||
SWAMP("Swamp"),
|
||||
|
||||
LEFT("left"),
|
||||
RIGHT("right");
|
||||
|
||||
private static class ModeChoiceCondition implements Condition {
|
||||
|
||||
private final ModeChoice modeChoice;
|
||||
|
||||
ModeChoiceCondition(ModeChoice modeChoice) {
|
||||
this.modeChoice = modeChoice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
return modeChoice.checkMode(game, source);
|
||||
}
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final ModeChoiceCondition condition;
|
||||
|
||||
ModeChoice(String name) {
|
||||
this.name = name;
|
||||
this.condition = new ModeChoiceCondition(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public ModeChoiceCondition getCondition() {
|
||||
return condition;
|
||||
}
|
||||
|
||||
public boolean checkMode(Game game, Ability source) {
|
||||
return Objects.equals(game.getState().getValue(source.getSourceId() + "_modeChoice"), name);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ public enum SpellAbilityCastMode {
|
|||
NORMAL("Normal"),
|
||||
MADNESS("Madness"),
|
||||
FLASHBACK("Flashback"),
|
||||
HARMONIZE("Harmonize"),
|
||||
BESTOW("Bestow"),
|
||||
PROTOTYPE("Prototype"),
|
||||
MORPH("Morph", false, true), // and megamorph
|
||||
|
|
@ -91,6 +92,7 @@ public enum SpellAbilityCastMode {
|
|||
case NORMAL:
|
||||
case MADNESS:
|
||||
case FLASHBACK:
|
||||
case HARMONIZE:
|
||||
case DISTURB:
|
||||
case PLOT:
|
||||
case MORE_THAN_MEETS_THE_EYE:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ public enum SpellAbilityType {
|
|||
MODAL_LEFT("LeftModal SpellAbility"),
|
||||
MODAL_RIGHT("RightModal SpellAbility"),
|
||||
SPLICE("Spliced SpellAbility"),
|
||||
ADVENTURE_SPELL("Adventure SpellAbility");
|
||||
ADVENTURE_SPELL("Adventure SpellAbility"),
|
||||
OMEN_SPELL("Omen SpellAbility");
|
||||
|
||||
private final String text;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ public enum SubType {
|
|||
ADVENTURE("Adventure", SubTypeSet.SpellType),
|
||||
ARCANE("Arcane", SubTypeSet.SpellType),
|
||||
LESSON("Lesson", SubTypeSet.SpellType),
|
||||
OMEN("Omen", SubTypeSet.SpellType),
|
||||
TRAP("Trap", SubTypeSet.SpellType),
|
||||
|
||||
// Battle subtypes
|
||||
|
|
@ -274,6 +275,7 @@ public enum SubType {
|
|||
MONGOOSE("Mongoose", SubTypeSet.CreatureType),
|
||||
MONK("Monk", SubTypeSet.CreatureType),
|
||||
MONKEY("Monkey", SubTypeSet.CreatureType),
|
||||
MOOGLE("Moogle", SubTypeSet.CreatureType),
|
||||
MOONFOLK("Moonfolk", SubTypeSet.CreatureType),
|
||||
MOUNT("Mount", SubTypeSet.CreatureType),
|
||||
MOUSE("Mouse", SubTypeSet.CreatureType),
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ public enum CounterType {
|
|||
CURRENCY("currency"),
|
||||
DEATH("death"),
|
||||
DEATHTOUCH("deathtouch"),
|
||||
DECAYED("decayed"),
|
||||
DEFENSE("defense"),
|
||||
DELAY("delay"),
|
||||
DEPLETION("depletion"),
|
||||
|
|
@ -184,6 +185,7 @@ public enum CounterType {
|
|||
PREY("prey"),
|
||||
PUPA("pupa"),
|
||||
RAD("rad"),
|
||||
RALLY("rally"),
|
||||
REACH("reach"),
|
||||
REJECTION("rejection"),
|
||||
REPAIR("repair"),
|
||||
|
|
@ -314,6 +316,8 @@ public enum CounterType {
|
|||
return new BoostCounter(-2, -2, amount);
|
||||
case DEATHTOUCH:
|
||||
return new AbilityCounter(DeathtouchAbility.getInstance(), amount);
|
||||
case DECAYED:
|
||||
return new AbilityCounter(new DecayedAbility(), amount);
|
||||
case DOUBLE_STRIKE:
|
||||
return new AbilityCounter(DoubleStrikeAbility.getInstance(), amount);
|
||||
case EXALTED:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package mage.filter.predicate.card;
|
||||
|
||||
import mage.cards.AdventureCard;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.ModalDoubleFacedCard;
|
||||
import mage.cards.SplitCard;
|
||||
import mage.cards.*;
|
||||
import mage.cards.mock.MockCard;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.SuperType;
|
||||
|
|
@ -55,8 +52,8 @@ public class CardTextPredicate implements Predicate<Card> {
|
|||
fullName = ((MockCard) input).getFullName(true);
|
||||
} else if (input instanceof ModalDoubleFacedCard) {
|
||||
fullName = input.getName() + MockCard.MODAL_DOUBLE_FACES_NAME_SEPARATOR + ((ModalDoubleFacedCard) input).getRightHalfCard().getName();
|
||||
} else if (input instanceof AdventureCard) {
|
||||
fullName = input.getName() + MockCard.ADVENTURE_NAME_SEPARATOR + ((AdventureCard) input).getSpellCard().getName();
|
||||
} else if (input instanceof CardWithSpellOption) {
|
||||
fullName = input.getName() + MockCard.CARD_WITH_SPELL_OPTION_NAME_SEPARATOR + ((CardWithSpellOption) input).getSpellCard().getName();
|
||||
}
|
||||
|
||||
if (fullName.toLowerCase(Locale.ENGLISH).contains(text.toLowerCase(Locale.ENGLISH))) {
|
||||
|
|
@ -107,8 +104,8 @@ public class CardTextPredicate implements Predicate<Card> {
|
|||
}
|
||||
}
|
||||
|
||||
if (input instanceof AdventureCard) {
|
||||
for (String rule : ((AdventureCard) input).getSpellCard().getRules(game)) {
|
||||
if (input instanceof CardWithSpellOption) {
|
||||
for (String rule : ((CardWithSpellOption) input).getSpellCard().getRules(game)) {
|
||||
if (rule.toLowerCase(Locale.ENGLISH).contains(token)) {
|
||||
found = true;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -334,8 +334,8 @@ public abstract class GameImpl implements Game {
|
|||
Card rightCard = ((ModalDoubleFacedCard) card).getRightHalfCard();
|
||||
rightCard.setOwnerId(ownerId);
|
||||
addCardToState(rightCard);
|
||||
} else if (card instanceof AdventureCard) {
|
||||
Card spellCard = ((AdventureCard) card).getSpellCard();
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
|
||||
spellCard.setOwnerId(ownerId);
|
||||
addCardToState(spellCard);
|
||||
} else if (card.isTransformable() && card.getSecondCardFace() != null) {
|
||||
|
|
|
|||
|
|
@ -1639,14 +1639,14 @@ public class GameState implements Serializable, Copyable<GameState> {
|
|||
copiedParts.add(rightCopied);
|
||||
// sync parts
|
||||
((ModalDoubleFacedCard) copiedCard).setParts(leftCopied, rightCopied);
|
||||
} else if (copiedCard instanceof AdventureCard) {
|
||||
} else if (copiedCard instanceof CardWithSpellOption) {
|
||||
// right
|
||||
AdventureCardSpell rightOriginal = ((AdventureCard) copiedCard).getSpellCard();
|
||||
AdventureCardSpell rightCopied = rightOriginal.copy();
|
||||
SpellOptionCard rightOriginal = ((CardWithSpellOption) copiedCard).getSpellCard();
|
||||
SpellOptionCard rightCopied = rightOriginal.copy();
|
||||
prepareCardForCopy(rightOriginal, rightCopied, newController);
|
||||
copiedParts.add(rightCopied);
|
||||
// sync parts
|
||||
((AdventureCard) copiedCard).setParts(rightCopied);
|
||||
((CardWithSpellOption) copiedCard).setParts(rightCopied);
|
||||
}
|
||||
|
||||
// main part prepare (must be called after other parts cause it change ids for all)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package mage.game.permanent.token;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.effects.common.PreventDamageToSourceEffect;
|
||||
import mage.abilities.keyword.VanishingAbility;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Duration;
|
||||
|
||||
/**
|
||||
* @author padfoothelix
|
||||
*/
|
||||
public final class AlienRhinoToken extends TokenImpl {
|
||||
|
||||
public AlienRhinoToken() {
|
||||
super("Alien Rhino Token", "4/4 white Alien Rhino creature token");
|
||||
cardType.add(CardType.CREATURE);
|
||||
color.setWhite(true);
|
||||
subtype.add(SubType.ALIEN);
|
||||
subtype.add(SubType.RHINO);
|
||||
power = new MageInt(4);
|
||||
toughness = new MageInt(4);
|
||||
}
|
||||
|
||||
private AlienRhinoToken(final AlienRhinoToken token) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AlienRhinoToken copy() {
|
||||
return new AlienRhinoToken(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package mage.game.permanent.token;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.costs.mana.GenericManaCost;
|
||||
import mage.abilities.keyword.WardAbility;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Duration;
|
||||
|
||||
/**
|
||||
* @author padfoothelix
|
||||
*/
|
||||
public final class Human11WithWard2Token extends TokenImpl {
|
||||
|
||||
public Human11WithWard2Token() {
|
||||
super("Human Token", "1/1 white Human creature token with ward {2}");
|
||||
cardType.add(CardType.CREATURE);
|
||||
color.setWhite(true);
|
||||
subtype.add(SubType.HUMAN);
|
||||
power = new MageInt(1);
|
||||
toughness = new MageInt(1);
|
||||
this.addAbility(new WardAbility(new GenericManaCost(2)));
|
||||
}
|
||||
|
||||
private Human11WithWard2Token(final Human11WithWard2Token token) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Human11WithWard2Token copy() {
|
||||
return new Human11WithWard2Token(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -209,10 +209,11 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
+ " using " + this.getSpellAbility().getSpellAbilityCastMode();
|
||||
}
|
||||
|
||||
if (card instanceof AdventureCardSpell) {
|
||||
AdventureCard adventureCard = ((AdventureCardSpell) card).getParentCard();
|
||||
return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), adventureCard)
|
||||
+ " as Adventure spell of " + GameLog.getColoredObjectIdName(adventureCard);
|
||||
if (card instanceof SpellOptionCard) {
|
||||
CardWithSpellOption parentCard = ((SpellOptionCard) card).getParentCard();
|
||||
String type = ((SpellOptionCard) card).getSpellType();
|
||||
return GameLog.replaceNameByColoredName(card, getSpellAbility().toString(), parentCard)
|
||||
+ " as " + type + " spell of " + GameLog.getColoredObjectIdName(parentCard);
|
||||
}
|
||||
|
||||
if (card instanceof ModalDoubleFacedCardHalf) {
|
||||
|
|
@ -539,8 +540,8 @@ public class Spell extends StackObjectImpl implements Card {
|
|||
public String getIdName() {
|
||||
String idName;
|
||||
if (card != null) {
|
||||
if (card instanceof AdventureCardSpell) {
|
||||
idName = ((AdventureCardSpell) card).getParentCard().getId().toString().substring(0, 3);
|
||||
if (card instanceof SpellOptionCard) {
|
||||
idName = ((SpellOptionCard) card).getParentCard().getId().toString().substring(0, 3);
|
||||
} else {
|
||||
idName = card.getId().toString().substring(0, 3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,6 +426,16 @@ public class StackAbility extends StackObjectImpl implements Ability {
|
|||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariableCostsMinMax(int min, int max) {
|
||||
ability.setVariableCostsMinMax(min, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVariableCostsValue(int xValue) {
|
||||
ability.setVariableCostsValue(xValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getCostsTagMap() {
|
||||
return ability.getCostsTagMap();
|
||||
|
|
@ -778,14 +788,23 @@ public class StackAbility extends StackObjectImpl implements Ability {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CostAdjuster getCostAdjuster() {
|
||||
return costAdjuster;
|
||||
public void adjustX(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.prepareX(this, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustCosts(Game game) {
|
||||
public void adjustCostsPrepare(Game game) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.adjustCosts(this, game);
|
||||
costAdjuster.prepareCost(this, game);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjustCostsModify(Game game, CostModificationType costModificationType) {
|
||||
if (costAdjuster != null) {
|
||||
costAdjuster.modifyCost(this, game, costModificationType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -743,10 +743,14 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
|
||||
boolean shuffleCardsToLibrary(Card card, Game game, Ability source);
|
||||
|
||||
// set the value for X mana spells and abilities
|
||||
/**
|
||||
* Set the value for X mana spells and abilities
|
||||
*/
|
||||
int announceXMana(int min, int max, String message, Game game, Ability ability);
|
||||
|
||||
// set the value for non mana X costs
|
||||
/**
|
||||
* Set the value for non mana X costs
|
||||
*/
|
||||
int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost);
|
||||
|
||||
// TODO: rework to use pair's list of effect + ability instead string's map
|
||||
|
|
|
|||
|
|
@ -3679,8 +3679,11 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
if (!copy.canActivate(playerId, game).canActivate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// apply dynamic costs and cost modification
|
||||
copy.adjustX(game);
|
||||
if (availableMana != null) {
|
||||
copy.adjustCosts(game);
|
||||
// TODO: need research, why it look at availableMana here - can delete condition?
|
||||
game.getContinuousEffects().costModification(copy, game);
|
||||
}
|
||||
boolean canBeCastRegularly = true;
|
||||
|
|
@ -3891,7 +3894,8 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
copyAbility = ability.copy();
|
||||
copyAbility.clearManaCostsToPay();
|
||||
copyAbility.addManaCostsToPay(manaCosts.copy());
|
||||
copyAbility.adjustCosts(game);
|
||||
// apply dynamic costs and cost modification
|
||||
copyAbility.adjustX(game);
|
||||
game.getContinuousEffects().costModification(copyAbility, game);
|
||||
|
||||
// reduced all cost
|
||||
|
|
@ -3963,12 +3967,9 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
// alternative cost reduce
|
||||
copyAbility = ability.copy();
|
||||
copyAbility.clearManaCostsToPay();
|
||||
// TODO: IDE warning:
|
||||
// Unchecked assignment: 'mage.abilities.costs.mana.ManaCosts' to
|
||||
// 'java.util.Collection<? extends mage.abilities.costs.mana.ManaCost>'.
|
||||
// Reason: 'manaCosts' has raw type, so result of copy is erased
|
||||
copyAbility.addManaCostsToPay(manaCosts.copy());
|
||||
copyAbility.adjustCosts(game);
|
||||
// apply dynamic costs and cost modification
|
||||
copyAbility.adjustX(game);
|
||||
game.getContinuousEffects().costModification(copyAbility, game);
|
||||
|
||||
// reduced all cost
|
||||
|
|
@ -4068,11 +4069,11 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output);
|
||||
getPlayableFromObjectSingle(game, fromZone, mainCard.getRightHalfCard(), mainCard.getRightHalfCard().getAbilities(game), availableMana, output);
|
||||
getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output);
|
||||
} else if (object instanceof AdventureCard) {
|
||||
} else if (object instanceof CardWithSpellOption) {
|
||||
// adventure must use different card characteristics for different spells (main or adventure)
|
||||
AdventureCard adventureCard = (AdventureCard) object;
|
||||
getPlayableFromObjectSingle(game, fromZone, adventureCard.getSpellCard(), adventureCard.getSpellCard().getAbilities(game), availableMana, output);
|
||||
getPlayableFromObjectSingle(game, fromZone, adventureCard, adventureCard.getSharedAbilities(game), availableMana, output);
|
||||
CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object;
|
||||
getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption.getSpellCard(), cardWithSpellOption.getSpellCard().getAbilities(game), availableMana, output);
|
||||
getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption, cardWithSpellOption.getSharedAbilities(game), availableMana, output);
|
||||
} else if (object instanceof Card) {
|
||||
getPlayableFromObjectSingle(game, fromZone, object, ((Card) object).getAbilities(game), availableMana, output);
|
||||
} else if (object instanceof StackObject) {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,13 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
|
|||
|
||||
@Override
|
||||
public void adjustTargets(Ability ability, Game game) {
|
||||
Target newTarget = blueprintTarget.copy();
|
||||
int count = dynamicValue.calculate(game, ability, ability.getEffects().get(0));
|
||||
ability.getTargets().clear();
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Target newTarget = blueprintTarget.copy();
|
||||
newTarget.setMaxNumberOfTargets(count);
|
||||
Filter filter = newTarget.getFilter();
|
||||
if (blueprintTarget.getMinNumberOfTargets() != 0) {
|
||||
|
|
@ -35,7 +40,6 @@ public class TargetsCountAdjuster extends GenericTargetAdjuster {
|
|||
} else {
|
||||
newTarget.withTargetName(filter.getMessage() + " (up to " + count + " targets)");
|
||||
}
|
||||
ability.getTargets().clear();
|
||||
ability.addTarget(newTarget);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,7 @@ public class FixedTargets extends TargetPointerImpl {
|
|||
|
||||
final ArrayList<MageObjectReference> targets = new ArrayList<>();
|
||||
|
||||
public FixedTargets(List<Permanent> objects, Game game) {
|
||||
this(objects
|
||||
.stream()
|
||||
.map(o -> new MageObjectReference(o.getId(), game))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public FixedTargets(Set<Card> objects, Game game) {
|
||||
public FixedTargets(Collection<? extends Card> objects, Game game) {
|
||||
this(objects
|
||||
.stream()
|
||||
.map(o -> new MageObjectReference(o.getId(), game))
|
||||
|
|
@ -50,7 +43,7 @@ public class FixedTargets extends TargetPointerImpl {
|
|||
.collect(Collectors.toList()), game);
|
||||
}
|
||||
|
||||
public FixedTargets(List<MageObjectReference> morList) {
|
||||
public FixedTargets(Collection<MageObjectReference> morList) {
|
||||
super();
|
||||
targets.addAll(morList);
|
||||
this.setInitialized(); // no need dynamic init
|
||||
|
|
|
|||
|
|
@ -1079,6 +1079,53 @@ public final class CardUtil {
|
|||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static Set<UUID> getAllPossibleTargets(Cost cost, Game game, Ability source) {
|
||||
return cost.getTargets()
|
||||
.stream()
|
||||
.map(t -> t.possibleTargets(source.getControllerId(), source, game))
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute values between min and max and make sure that the values will be evenly distributed
|
||||
* Use it to limit possible values list like mana options
|
||||
*/
|
||||
public static List<Integer> distributeValues(int count, int min, int max) {
|
||||
List<Integer> res = new ArrayList<>();
|
||||
if (count <= 0 || min > max) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (min == max) {
|
||||
res.add(min);
|
||||
return res;
|
||||
}
|
||||
|
||||
int range = max - min + 1;
|
||||
|
||||
// low possible amount
|
||||
if (range <= count) {
|
||||
for (int i = 0; i < range; i++) {
|
||||
res.add(min + i);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// big possible amount, so skip some values
|
||||
double step = (double) (max - min) / (count - 1);
|
||||
for (int i = 0; i < count; i++) {
|
||||
res.add(min + (int) Math.round(i * step));
|
||||
}
|
||||
// make sure first and last elements are good
|
||||
res.set(0, min);
|
||||
if (res.size() > 1) {
|
||||
res.set(res.size() - 1, max);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* For finding the spell or ability on the stack for "becomes the target" triggers.
|
||||
*
|
||||
|
|
@ -1086,13 +1133,22 @@ public final class CardUtil {
|
|||
* @param game the Game from checkTrigger() or watch()
|
||||
* @return the StackObject which targeted the source, or null if not found
|
||||
*/
|
||||
public static StackObject getTargetingStackObject(GameEvent event, Game game) {
|
||||
public static StackObject getTargetingStackObject(String checkingReference, GameEvent event, Game game) {
|
||||
// In case of multiple simultaneous triggered abilities from the same source,
|
||||
// need to get the actual one that targeted, see #8026, #8378
|
||||
// Also avoids triggering on cancelled selections, see #8802
|
||||
String stateKey = "targetedMap" + checkingReference;
|
||||
Map<UUID, Set<UUID>> targetMap = (Map<UUID, Set<UUID>>) game.getState().getValue(stateKey);
|
||||
// targetMap: key - targetId; value - Set of stackObject Ids
|
||||
if (targetMap == null) {
|
||||
targetMap = new HashMap<>();
|
||||
} else {
|
||||
targetMap = new HashMap<>(targetMap); // must have new object reference if saved back to game state
|
||||
}
|
||||
Set<UUID> targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
|
||||
for (StackObject stackObject : game.getStack()) {
|
||||
Ability stackAbility = stackObject.getStackAbility();
|
||||
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId())) {
|
||||
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId()) || targetingObjects.contains(stackObject.getId())) {
|
||||
continue;
|
||||
}
|
||||
if (CardUtil.getAllSelectedTargets(stackAbility, game).contains(event.getTargetId())) {
|
||||
|
|
@ -1263,7 +1319,7 @@ public final class CardUtil {
|
|||
Card permCard;
|
||||
if (card instanceof SplitCard) {
|
||||
permCard = card;
|
||||
} else if (card instanceof AdventureCard) {
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
permCard = card;
|
||||
} else if (card instanceof ModalDoubleFacedCard) {
|
||||
permCard = ((ModalDoubleFacedCard) card).getLeftHalfCard();
|
||||
|
|
@ -1451,9 +1507,9 @@ public final class CardUtil {
|
|||
if (cardToCast instanceof CardWithHalves) {
|
||||
cards.add(((CardWithHalves) cardToCast).getLeftHalfCard());
|
||||
cards.add(((CardWithHalves) cardToCast).getRightHalfCard());
|
||||
} else if (cardToCast instanceof AdventureCard) {
|
||||
} else if (cardToCast instanceof CardWithSpellOption) {
|
||||
cards.add(cardToCast);
|
||||
cards.add(((AdventureCard) cardToCast).getSpellCard());
|
||||
cards.add(((CardWithSpellOption) cardToCast).getSpellCard());
|
||||
} else {
|
||||
cards.add(cardToCast);
|
||||
}
|
||||
|
|
@ -1642,9 +1698,9 @@ public final class CardUtil {
|
|||
}
|
||||
|
||||
// handle adventure cards
|
||||
if (card instanceof AdventureCard) {
|
||||
if (card instanceof CardWithSpellOption) {
|
||||
Card creatureCard = card.getMainCard();
|
||||
Card spellCard = ((AdventureCard) card).getSpellCard();
|
||||
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
|
||||
if (manaCost != null) {
|
||||
// get additional cost if any
|
||||
Costs<Cost> additionalCostsCreature = creatureCard.getSpellAbility().getCosts();
|
||||
|
|
@ -1682,9 +1738,9 @@ public final class CardUtil {
|
|||
game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null);
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null);
|
||||
}
|
||||
if (card instanceof AdventureCard) {
|
||||
if (card instanceof CardWithSpellOption) {
|
||||
Card creatureCard = card.getMainCard();
|
||||
Card spellCard = ((AdventureCard) card).getSpellCard();
|
||||
Card spellCard = ((CardWithSpellOption) card).getSpellCard();
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), null);
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + spellCard.getId(), null);
|
||||
}
|
||||
|
|
@ -1899,6 +1955,10 @@ public final class CardUtil {
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
public static int getSourceCostsTagX(Game game, Ability source, int defaultValue) {
|
||||
return getSourceCostsTag(game, source, "X", defaultValue);
|
||||
}
|
||||
|
||||
public static String addCostVerb(String text) {
|
||||
if (costWords.stream().anyMatch(text.toLowerCase(Locale.ENGLISH)::startsWith)) {
|
||||
return text;
|
||||
|
|
@ -2069,8 +2129,8 @@ public final class CardUtil {
|
|||
res.add(mainCard);
|
||||
res.add(mainCard.getLeftHalfCard());
|
||||
res.add(mainCard.getRightHalfCard());
|
||||
} else if (object instanceof AdventureCard || object instanceof AdventureCardSpell) {
|
||||
AdventureCard mainCard = (AdventureCard) ((Card) object).getMainCard();
|
||||
} else if (object instanceof CardWithSpellOption || object instanceof SpellOptionCard) {
|
||||
CardWithSpellOption mainCard = (CardWithSpellOption) ((Card) object).getMainCard();
|
||||
res.add(mainCard);
|
||||
res.add(mainCard.getSpellCard());
|
||||
} else if (object instanceof Spell) {
|
||||
|
|
@ -2176,6 +2236,19 @@ public final class CardUtil {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
public static <T> T getEffectValueFromAbility(Ability ability, String key, Class<T> clazz) {
|
||||
return getEffectValueFromAbility(ability, key, clazz, null);
|
||||
}
|
||||
|
||||
public static <T> T getEffectValueFromAbility(Ability ability, String key, Class<T> clazz, T defaultValue) {
|
||||
return castStream(
|
||||
ability.getAllEffects()
|
||||
.stream()
|
||||
.map(effect -> effect.getValue(key)),
|
||||
clazz
|
||||
).findFirst().orElse(defaultValue);
|
||||
}
|
||||
|
||||
public static <T> Stream<T> castStream(Collection<?> collection, Class<T> clazz) {
|
||||
return castStream(collection.stream(), clazz);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ public class CircularList<E> implements List<E>, Iterable<E>, Serializable {
|
|||
*/
|
||||
@Override
|
||||
public E get(int index) {
|
||||
if (list.size() > this.index) {
|
||||
return list.get(this.index);
|
||||
if (list.size() > index) {
|
||||
return list.get(index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ import mage.abilities.dynamicvalue.DynamicValue;
|
|||
import mage.abilities.dynamicvalue.common.GetXValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.mana.*;
|
||||
import mage.cards.AdventureCard;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.ModalDoubleFacedCard;
|
||||
import mage.cards.SplitCard;
|
||||
import mage.cards.*;
|
||||
import mage.choices.Choice;
|
||||
import mage.constants.ColoredManaSymbol;
|
||||
import mage.constants.ManaType;
|
||||
|
|
@ -644,8 +641,8 @@ public final class ManaUtil {
|
|||
Card secondSide;
|
||||
if (card instanceof SplitCard) {
|
||||
secondSide = ((SplitCard) card).getRightHalfCard();
|
||||
} else if (card instanceof AdventureCard) {
|
||||
secondSide = ((AdventureCard) card).getSpellCard();
|
||||
} else if (card instanceof CardWithSpellOption) {
|
||||
secondSide = ((CardWithSpellOption) card).getSpellCard();
|
||||
} else if (card instanceof ModalDoubleFacedCard) {
|
||||
secondSide = ((ModalDoubleFacedCard) card).getRightHalfCard();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public class NumberOfTimesPermanentTargetedATurnWatcher extends Watcher {
|
|||
if (event.getType() != GameEvent.EventType.TARGETED) {
|
||||
return;
|
||||
}
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(event, game);
|
||||
StackObject targetingObject = CardUtil.getTargetingStackObject(this.getKey(), event, game);
|
||||
if (targetingObject == null || CardUtil.checkTargetedEventAlreadyUsed(this.getKey(), targetingObject, event, game)) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue