Merge branch 'master' into wish

This commit is contained in:
Oleg Agafonov 2021-07-20 09:52:18 +04:00
commit 96ca260109
213 changed files with 3962 additions and 528 deletions

View file

@ -552,8 +552,21 @@ public interface Ability extends Controllable, Serializable {
Ability addHint(Hint hint);
/**
* For abilities with static icons
*
* @return
*/
List<CardIcon> getIcons();
/**
* For abilities with dynamic icons
*
* @param game can be null for static calls like copies
* @return
*/
List<CardIcon> getIcons(Game game);
Ability addIcon(CardIcon cardIcon);
Ability addCustomOutcome(Outcome customOutcome);

View file

@ -1342,7 +1342,12 @@ public abstract class AbilityImpl implements Ability {
}
@Override
public List<CardIcon> getIcons() {
final public List<CardIcon> getIcons() {
return getIcons(null);
}
@Override
public List<CardIcon> getIcons(Game game) {
return this.icons;
}

View file

@ -22,7 +22,6 @@ public class ConstellationAbility extends TriggeredAbilityImpl {
public ConstellationAbility(Effect effect) {
this(effect, false);
setAbilityWord(AbilityWord.CONSTELLATION);
}
public ConstellationAbility(Effect effect, boolean optional) {
@ -32,6 +31,7 @@ public class ConstellationAbility extends TriggeredAbilityImpl {
public ConstellationAbility(Effect effect, boolean optional, boolean thisOr) {
super(Zone.BATTLEFIELD, effect, optional);
this.thisOr = thisOr;
setAbilityWord(AbilityWord.CONSTELLATION);
}
public ConstellationAbility(final ConstellationAbility ability) {

View file

@ -37,7 +37,8 @@ public class StriveAbility extends SimpleStaticAbility {
@Override
public String getRule() {
return new StringBuilder("this spell costs ").append(striveCost).append(" more to cast for each target beyond the first.").toString();
return abilityWord.formatWord() + "This spell costs "
+ striveCost + " more to cast for each target beyond the first.";
}
}

View file

@ -118,24 +118,22 @@ public class BeginningOfEndStepTriggeredAbility extends TriggeredAbilityImpl {
}
private String generateConditionString() {
if (interveningIfClauseCondition != null) {
if (interveningIfClauseCondition.toString().startsWith("if")) {
//Fixes punctuation on multiple sentence if-then construction
// see -- Colfenor's Urn
if (interveningIfClauseCondition.toString().endsWith(".")) {
return interveningIfClauseCondition.toString() + " ";
}
return interveningIfClauseCondition.toString() + ", ";
} else {
return "if {this} is " + interveningIfClauseCondition.toString() + ", ";
if (interveningIfClauseCondition == null) {
switch (getZone()) {
case GRAVEYARD:
return "if {this} is in your graveyard, ";
}
return "";
}
switch (getZone()) {
case GRAVEYARD:
return "if {this} is in your graveyard, ";
String clauseText = interveningIfClauseCondition.toString();
if (clauseText.startsWith("if")) {
//Fixes punctuation on multiple sentence if-then construction
// see -- Colfenor's Urn
if (clauseText.endsWith(".")) {
return clauseText + " ";
}
return clauseText + ", ";
}
return "";
return "if " + clauseText + ", ";
}
}

View file

@ -10,7 +10,6 @@ import mage.game.Game;
import mage.game.events.GameEvent;
/**
*
* @author LevelX2
*/
public class CastOnlyIfConditionIsTrueEffect extends ContinuousRuleModifyingEffectImpl {
@ -52,7 +51,7 @@ public class CastOnlyIfConditionIsTrueEffect extends ContinuousRuleModifyingEffe
private String setText() {
StringBuilder sb = new StringBuilder("cast this spell only ");
if (condition != null) {
sb.append(' ').append(condition.toString());
sb.append(condition);
}
return sb.toString();
}

View file

@ -1,12 +1,15 @@
package mage.abilities.common;
import java.util.UUID;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.AbilityWord;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.DamagedEvent;
import mage.game.events.DamagedPermanentBatchEvent;
import mage.game.events.GameEvent;
/**
@ -15,7 +18,6 @@ import mage.game.events.GameEvent;
public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl {
private final boolean useValue;
private boolean usedForCombatDamageStep;
public DealtDamageToSourceTriggeredAbility(Effect effect, boolean optional) {
this(effect, optional, false);
@ -28,7 +30,6 @@ public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl {
public DealtDamageToSourceTriggeredAbility(Effect effect, boolean optional, boolean enrage, boolean useValue) {
super(Zone.BATTLEFIELD, effect, optional);
this.useValue = useValue;
this.usedForCombatDamageStep = false;
if (enrage) {
this.setAbilityWord(AbilityWord.ENRAGE);
}
@ -37,7 +38,6 @@ public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl {
public DealtDamageToSourceTriggeredAbility(final DealtDamageToSourceTriggeredAbility ability) {
super(ability);
this.useValue = ability.useValue;
this.usedForCombatDamageStep = ability.usedForCombatDamageStep;
}
@Override
@ -47,30 +47,34 @@ public class DealtDamageToSourceTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT || event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_POST;
return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT_BATCH;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.DAMAGED_PERMANENT && event.getTargetId().equals(getSourceId())) {
if (useValue) {
// TODO: this ability should only trigger once for multiple creatures dealing combat damage.
// If the damaged creature uses the amount (e.g. Boros Reckoner), this will still trigger separately instead of all at once
getEffects().setValue("damage", event.getAmount());
return true;
} else {
if (((DamagedEvent) event).isCombatDamage()) {
if (!usedForCombatDamageStep) {
usedForCombatDamageStep = true;
return true;
}
} else {
return true;
}
if (event == null || game == null || this.getSourceId() == null) {
return false;
}
int damage = 0;
DamagedPermanentBatchEvent dEvent = (DamagedPermanentBatchEvent) event;
for (DamagedEvent damagedEvent : dEvent.getEvents()) {
UUID targetID = damagedEvent.getTargetId();
if (targetID == null) {
continue;
}
if (targetID == this.getSourceId()) {
damage += damagedEvent.getAmount();
}
}
if (event.getType() == GameEvent.EventType.COMBAT_DAMAGE_STEP_POST) {
usedForCombatDamageStep = false;
if (damage > 0) {
if (this.useValue) {
this.getEffects().setValue("damage", damage);
}
return true;
}
return false;
}

View file

@ -14,7 +14,6 @@ import mage.game.events.ZoneChangeEvent;
import mage.target.targetpointer.FixedTarget;
/**
*
* @author LevelX2
*/
public class PutCardIntoGraveFromAnywhereAllTriggeredAbility extends TriggeredAbilityImpl {
@ -42,7 +41,8 @@ public class PutCardIntoGraveFromAnywhereAllTriggeredAbility extends TriggeredAb
this.filter.add(targetController.getOwnerPredicate());
StringBuilder sb = new StringBuilder("Whenever ");
sb.append(filter.getMessage());
sb.append(" is put into ");
sb.append(filter.getMessage().startsWith("one or more") ? " are" : "is");
sb.append(" put into ");
switch (targetController) {
case OPPONENT:
sb.append("an opponent's");
@ -103,6 +103,6 @@ public class PutCardIntoGraveFromAnywhereAllTriggeredAbility extends TriggeredAb
@Override
public String getTriggerPhrase() {
return ruleText ;
return ruleText;
}
}

View file

@ -34,7 +34,7 @@ public enum EquippedSourceCondition implements Condition {
@Override
public String toString() {
return "equipped";
return "{this} is equipped";
}
}

View file

@ -7,7 +7,9 @@ import mage.game.Game;
import mage.watchers.common.ManaSpentToCastWatcher;
public enum ManacostVariableValue implements DynamicValue {
REGULAR, ETB;
REGULAR, // if you need X on cast/activate (in stack)
ETB; // if you need X after ETB (in battlefield)
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
@ -15,7 +17,10 @@ public enum ManacostVariableValue implements DynamicValue {
return sourceAbility.getManaCostsToPay().getX();
}
ManaSpentToCastWatcher watcher = game.getState().getWatcher(ManaSpentToCastWatcher.class);
return watcher != null ? watcher.getAndResetLastXValue(sourceAbility.getSourceId()) : sourceAbility.getManaCostsToPay().getX();
if (watcher != null) {
return watcher.getAndResetLastXValue(sourceAbility);
}
return 0;
}
@Override

View file

@ -77,4 +77,9 @@ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect {
}
}
@Override
public void setValue(String key, Object value) {
ability.getEffects().setValue(key, value);
super.setValue(key, value);
}
}

View file

@ -49,6 +49,7 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
private boolean isntLegendary = false;
private int startingLoyalty = -1;
private final List<Ability> additionalAbilities = new ArrayList();
private Permanent savedPermanent = null;
public CreateTokenCopyTargetEffect(boolean useLKI) {
this();
@ -133,7 +134,9 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
targetId = getTargetPointer().getFirst(game, source);
}
Permanent permanent;
if (useLKI) {
if (savedPermanent != null) {
permanent = savedPermanent;
} else if (useLKI) {
permanent = getTargetPointer().getFirstTargetPermanentOrLKI(game, source);
} else {
permanent = game.getPermanentOrLKIBattlefield(targetId);
@ -319,4 +322,8 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
public void addAdditionalAbilities(Ability... abilities) {
Arrays.stream(abilities).forEach(this.additionalAbilities::add);
}
public void setSavedPermanent(Permanent savedPermanent) {
this.savedPermanent = savedPermanent;
}
}

View file

@ -3,7 +3,6 @@ package mage.abilities.effects.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect;
import mage.cards.Card;
@ -21,21 +20,28 @@ public class ExileTopXMayPlayUntilEndOfTurnEffect extends OneShotEffect {
private final int amount;
private final boolean showHint;
private final Duration duration;
public ExileTopXMayPlayUntilEndOfTurnEffect(int amount) {
this(amount, false);
}
public ExileTopXMayPlayUntilEndOfTurnEffect(int amount, boolean showHint) {
this(amount, showHint, Duration.EndOfTurn);
}
public ExileTopXMayPlayUntilEndOfTurnEffect(int amount, boolean showHint, Duration duration) {
super(Outcome.Benefit);
this.amount = amount;
this.showHint = showHint;
this.duration = duration;
}
private ExileTopXMayPlayUntilEndOfTurnEffect(final ExileTopXMayPlayUntilEndOfTurnEffect effect) {
super(effect);
this.amount = effect.amount;
this.showHint = effect.showHint;
this.duration = effect.duration;
}
@Override
@ -47,21 +53,21 @@ public class ExileTopXMayPlayUntilEndOfTurnEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = game.getObject(source.getSourceId());
if (controller != null && sourceObject != null) {
Set<Card> cards = controller.getLibrary().getTopCards(game, amount);
if (!cards.isEmpty()) {
controller.moveCardsToExile(cards, source, game, true, source.getSourceId(), sourceObject.getIdName());
// remove cards that could not be moved to exile
cards.removeIf(card -> !Zone.EXILED.equals(game.getState().getZone(card.getId())));
if (!cards.isEmpty()) {
ContinuousEffect effect = new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, Duration.EndOfTurn);
effect.setTargetPointer(new FixedTargets(cards, game));
game.addEffect(effect, source);
}
}
if (controller == null || sourceObject == null) {
return false;
}
Set<Card> cards = controller.getLibrary().getTopCards(game, amount);
if (cards.isEmpty()) {
return true;
}
return false;
controller.moveCardsToExile(cards, source, game, true, source.getSourceId(), sourceObject.getIdName());
// remove cards that could not be moved to exile
cards.removeIf(card -> !Zone.EXILED.equals(game.getState().getZone(card.getId())));
if (!cards.isEmpty()) {
game.addEffect(new PlayFromNotOwnHandZoneTargetEffect(Zone.EXILED, duration)
.setTargetPointer(new FixedTargets(cards, game)), source);
}
return true;
}
@Override
@ -71,11 +77,15 @@ public class ExileTopXMayPlayUntilEndOfTurnEffect extends OneShotEffect {
}
StringBuilder sb = new StringBuilder();
if (amount == 1) {
sb.append("exile the top card of your library. You may play that card this turn");
sb.append("exile the top card of your library. ");
sb.append(CardUtil.getTextWithFirstCharUpperCase(duration.toString()));
sb.append(", you may play that card");
} else {
sb.append("exile the top ");
sb.append(CardUtil.numberToText(amount));
sb.append(" cards of your library. Until end of turn, you may play cards exiled this way");
sb.append(" cards of your library. ");
sb.append(CardUtil.getTextWithFirstCharUpperCase(duration.toString()));
sb.append(", you may play cards exiled this way");
}
if (showHint) {
sb.append(". <i>(You still pay its costs. You can play a land this way only if you have an available land play remaining.)</i>");

View file

@ -10,7 +10,6 @@ import mage.game.permanent.Permanent;
/**
* @author LevelX2
*/
public class CantBeBlockedByCreaturesAllEffect extends RestrictionEffect {
private final FilterCreaturePermanent filterBlockedBy;
@ -20,8 +19,9 @@ public class CantBeBlockedByCreaturesAllEffect extends RestrictionEffect {
super(duration);
this.filterCreatures = filterCreatures;
this.filterBlockedBy = filterBlockedBy;
staticText = new StringBuilder(filterCreatures.getMessage()).append(" can't be blocked ")
.append(filterBlockedBy.getMessage().startsWith("except by") ? "" : "by ").append(filterBlockedBy.getMessage()).toString();
staticText = filterCreatures.getMessage() + " can't be blocked "
+ (filterBlockedBy.getMessage().startsWith("except by") ? "" : "by ")
+ filterBlockedBy.getMessage();
}
public CantBeBlockedByCreaturesAllEffect(final CantBeBlockedByCreaturesAllEffect effect) {

View file

@ -182,10 +182,13 @@ public class BecomesCreatureTargetEffect extends ContinuousEffectImpl {
sb.append(token.getDescription());
sb.append(' ').append(duration.toString());
if (addStillALandText) {
if (!sb.toString().endsWith("\" ")) {
sb.append(". ");
}
if (target.getMaxNumberOfTargets() > 1) {
sb.append(". They're still lands");
sb.append("They're still lands");
} else {
sb.append(". It's still a land");
sb.append("It's still a land");
}
}
return sb.toString().replace(" .", ".");

View file

@ -19,7 +19,7 @@ import mage.target.Target;
import mage.target.Targets;
import mage.target.targetpointer.FixedTarget;
import java.util.Arrays;
import java.util.Collections;
/**
* @author TheElk801
@ -28,8 +28,8 @@ public class GainAbilityWithAttachmentEffect extends ContinuousEffectImpl {
private final Effects effects = new Effects();
private final Targets targets = new Targets();
private final Costs costs = new CostsImpl();
private final UseAttachedCost useAttachedCost;
private final Costs<Cost> costs = new CostsImpl<>();
protected final UseAttachedCost useAttachedCost;
public GainAbilityWithAttachmentEffect(String rule, Effect effect, Target target, UseAttachedCost attachedCost, Cost... costs) {
this(rule, new Effects(effect), new Targets(target), attachedCost, costs);
@ -40,12 +40,12 @@ public class GainAbilityWithAttachmentEffect extends ContinuousEffectImpl {
this.staticText = rule;
this.effects.addAll(effects);
this.targets.addAll(targets);
this.costs.addAll(Arrays.asList(costs));
Collections.addAll(this.costs, costs);
this.useAttachedCost = attachedCost;
this.generateGainAbilityDependencies(makeAbility(this.effects, this.targets, this.costs), null);
this.generateGainAbilityDependencies(makeAbility(null, null), null);
}
public GainAbilityWithAttachmentEffect(final GainAbilityWithAttachmentEffect effect) {
protected GainAbilityWithAttachmentEffect(final GainAbilityWithAttachmentEffect effect) {
super(effect);
this.effects.addAll(effect.effects);
this.targets.addAll(effect.targets);
@ -87,14 +87,13 @@ public class GainAbilityWithAttachmentEffect extends ContinuousEffectImpl {
if (permanent == null) {
return true;
}
Ability ability = makeAbility(this.effects, this.targets, this.costs);
Ability ability = makeAbility(game, source);
ability.getEffects().setValue("attachedPermanent", game.getPermanent(source.getSourceId()));
ability.addCost(useAttachedCost.copy().setMageObjectReference(source, game));
permanent.addAbility(ability, source.getSourceId(), game);
return true;
}
private static Ability makeAbility(Effects effects, Targets targets, Cost... costs) {
protected Ability makeAbility(Game game, Ability source) {
Ability ability = new SimpleActivatedAbility(null, null);
for (Effect effect : effects) {
if (effect == null) {
@ -108,12 +107,15 @@ public class GainAbilityWithAttachmentEffect extends ContinuousEffectImpl {
}
ability.addTarget(target);
}
for (Cost cost : costs) {
for (Cost cost : this.costs) {
if (cost == null) {
continue;
}
ability.addCost(cost.copy());
}
if (source != null && game != null) {
ability.addCost(useAttachedCost.copy().setMageObjectReference(source, game));
}
return ability;
}
}

View file

@ -62,7 +62,7 @@ public class SpellCostReductionForEachSourceEffect extends CostModificationEffec
if (reduceManaCosts != null) {
// color reduce
ManaCosts<ManaCost> needReduceMana = new ManaCostsImpl<>();
for (int i = 0; i <= needReduceAmount; i++) {
for (int i = 0; i < needReduceAmount; i++) {
needReduceMana.add(reduceManaCosts.copy());
}
CardUtil.adjustCost((SpellAbility) abilityToModify, needReduceMana, false);

View file

@ -35,7 +35,7 @@ public class SearchLibraryGraveyardPutInHandEffect extends OneShotEffect {
this.filter = filter;
this.forceToSearchBoth = forceToSearchBoth;
staticText = (youMay ? "you may " : "") + "search your library and" + (forceToSearchBoth ? "" : "/or") + " graveyard for a card named " + filter.getMessage()
+ ", reveal it, and put it into your hand. " + (forceToSearchBoth ? "Then shuffle" : "If you search your library this way, shuffle");
+ ", reveal it, and put it into your hand. " + (forceToSearchBoth ? "Then shuffle" : "If you search your library this way, shuffle it");
}
public SearchLibraryGraveyardPutInHandEffect(final SearchLibraryGraveyardPutInHandEffect effect) {

View file

@ -1,6 +1,8 @@
package mage.abilities.icon;
/**
* For GUI: different icons category can go to different position/panels on the card
*
* @author JayDi85
*/
public enum CardIconCategory {

View file

@ -1,9 +1,11 @@
package mage.abilities.icon;
import java.io.Serializable;
/**
* @author JayDi85
*/
public class CardIconImpl implements CardIcon {
public class CardIconImpl implements CardIcon, Serializable {
private final CardIconType cardIconType;
private final String text;

View file

@ -29,6 +29,10 @@ public enum CardIconType {
ABILITY_INFECT("prepared/flask.svg", CardIconCategory.ABILITY, 100),
ABILITY_INDESTRUCTIBLE("prepared/ankh.svg", CardIconCategory.ABILITY, 100),
ABILITY_VIGILANCE("prepared/eye.svg", CardIconCategory.ABILITY, 100),
ABILITY_CLASS_LEVEL("prepared/hexagon-fill.svg", CardIconCategory.ABILITY, 100),
//
OTHER_FACEDOWN("prepared/reply-fill.svg", CardIconCategory.ABILITY, 100),
OTHER_COST_X("prepared/square-fill.svg", CardIconCategory.ABILITY, 100),
//
SYSTEM_COMBINED("prepared/square-fill.svg", CardIconCategory.SYSTEM, 1000), // inner usage, must use last order
SYSTEM_DEBUG("prepared/link.svg", CardIconCategory.SYSTEM, 1000); // used for test render dialog

View file

@ -0,0 +1,31 @@
package mage.abilities.icon.other;
import mage.abilities.icon.CardIcon;
import mage.abilities.icon.CardIconType;
/**
* @author JayDi85
*/
public enum FaceDownCardIcon implements CardIcon {
instance;
@Override
public CardIconType getIconType() {
return CardIconType.OTHER_FACEDOWN;
}
@Override
public String getText() {
return "";
}
@Override
public String getHint() {
return "Card is face down";
}
@Override
public CardIcon copy() {
return instance;
}
}

View file

@ -0,0 +1,16 @@
package mage.abilities.icon.other;
import mage.abilities.icon.CardIconImpl;
import mage.abilities.icon.CardIconType;
/**
* Showing x cost value
*
* @author JayDi85
*/
public class VariableCostCardIcon extends CardIconImpl {
public VariableCostCardIcon(int costX) {
super(CardIconType.OTHER_COST_X, "Announced X = " + costX, "x=" + costX);
}
}

View file

@ -61,6 +61,7 @@ class SetClassLevelEffect extends OneShotEffect {
SetClassLevelEffect(int level) {
super(Outcome.Benefit);
this.level = level;
staticText = "level up to " + level;
}
private SetClassLevelEffect(final SetClassLevelEffect effect) {
@ -76,9 +77,17 @@ class SetClassLevelEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null || !permanent.setClassLevel(level)) {
if (permanent == null) {
return false;
}
int oldLevel = permanent.getClassLevel();
if (!permanent.setClassLevel(level)) {
return false;
}
game.informPlayers(permanent.getLogName() + " levelled up from " + oldLevel + " to " + permanent.getClassLevel());
game.fireEvent(GameEvent.getEvent(
GameEvent.EventType.GAINS_CLASS_LEVEL, source.getSourceId(),
source, source.getControllerId(), level

View file

@ -2,7 +2,15 @@ package mage.abilities.keyword;
import mage.abilities.StaticAbility;
import mage.abilities.hint.common.ClassLevelHint;
import mage.abilities.icon.CardIcon;
import mage.abilities.icon.CardIconImpl;
import mage.abilities.icon.CardIconType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.util.ArrayList;
import java.util.List;
/**
* @author TheElk801
@ -27,4 +35,27 @@ public class ClassReminderAbility extends StaticAbility {
public String getRule() {
return "<i>(Gain the next level as a sorcery to add its ability.)</i>";
}
@Override
public List<CardIcon> getIcons(Game game) {
if (game == null) {
return this.icons;
}
// dynamic GUI icon with current level
List<CardIcon> res = new ArrayList<>();
Permanent permanent = this.getSourcePermanentOrLKI(game);
if (permanent == null) {
return res;
}
CardIcon levelIcon = new CardIconImpl(
CardIconType.ABILITY_CLASS_LEVEL,
"Current class level: " + permanent.getClassLevel(),
String.valueOf(permanent.getClassLevel())
);
res.add(levelIcon);
return res;
}
}

View file

@ -28,7 +28,7 @@ public class EquipAbility extends ActivatedAbilityImpl {
public EquipAbility(Outcome outcome, Cost cost, Target target) {
super(Zone.BATTLEFIELD, new EquipEffect(outcome), cost);
this.addTarget(target);
this.timing = TimingRule.SORCERY;
this.timing = TimingRule.SORCERY;
}
public EquipAbility(final EquipAbility ability) {
@ -50,19 +50,23 @@ public class EquipAbility extends ActivatedAbilityImpl {
String targetText = getTargets().get(0) != null ? getTargets().get(0).getFilter().getMessage() : "creature";
String reminderText = " <i>(" + manaCosts.getText() + ": Attach to target " + targetText + ". Equip only as a sorcery. This card enters the battlefield unattached and stays on the battlefield if the creature leaves.)</i>";
StringBuilder sb = new StringBuilder("Equip ");
StringBuilder sb = new StringBuilder("Equip");
if (!targetText.equals("creature you control")) {
sb.append(targetText);
sb.append(' ').append(targetText);
}
String costText = costs.getText();
if (costText != null && !costText.isEmpty()) {
sb.append("&mdash;").append(costText).append('.');
} else {
sb.append(' ');
}
sb.append(costs.getText());
sb.append(manaCosts.getText());
if (costReduceText != null && !costReduceText.isEmpty()) {
sb.append(' ');
sb.append(". ");
sb.append(costReduceText);
}
if (maxActivationsPerTurn == 1) {
sb.append(" Activate only once each turn.");
sb.append(". Activate only once each turn.");
}
sb.append(reminderText);
return sb.toString();

View file

@ -31,7 +31,7 @@ public class IntimidateAbility extends EvasionAbility implements MageSingleton {
@Override
public String getRule() {
return "Intimidate";
return "intimidate";
}
@Override

View file

@ -1,12 +1,9 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.hint.common.MonstrousHint;
import mage.constants.Outcome;
import mage.constants.Zone;
@ -19,41 +16,40 @@ import mage.util.CardUtil;
/**
* Monstrosity
*
* <p>
* 701.28. Monstrosity
*
* <p>
* 701.28a Monstrosity N means If this permanent isn't monstrous, put N +1/+1 counters on it
* and it becomes monstrous. Monstrous is a condition of that permanent that can be
* referred to by other abilities.
*
* and it becomes monstrous. Monstrous is a condition of that permanent that can be
* referred to by other abilities.
* <p>
* 701.28b If a permanent's ability instructs a player to monstrosity X, other abilities of
* that permanent may also refer to X. The value of X in those abilities is equal to
* the value of X as that permanent became monstrous.
*
* that permanent may also refer to X. The value of X in those abilities is equal to
* the value of X as that permanent became monstrous.
* <p>
* * Once a creature becomes monstrous, it can't become monstrous again. If the creature
* is already monstrous when the monstrosity ability resolves, nothing happens.
*
* is already monstrous when the monstrosity ability resolves, nothing happens.
* <p>
* * Monstrous isn't an ability that a creature has. It's just something true about that
* creature. If the creature stops being a creature or loses its abilities, it will
* continue to be monstrous.
*
* creature. If the creature stops being a creature or loses its abilities, it will
* continue to be monstrous.
* <p>
* * An ability that triggers when a creature becomes monstrous won't trigger if that creature
* isn't on the battlefield when its monstrosity ability resolves.
* isn't on the battlefield when its monstrosity ability resolves.
*
* @author LevelX2
*/
public class MonstrosityAbility extends ActivatedAbilityImpl {
private int monstrosityValue;
private final int monstrosityValue;
/**
*
* @param manaString
* @param monstrosityValue use Integer.MAX_VALUE for monstrosity X.
*/
public MonstrosityAbility(String manaString, int monstrosityValue) {
super(Zone.BATTLEFIELD, new BecomeMonstrousSourceEffect(monstrosityValue),new ManaCostsImpl(manaString));
super(Zone.BATTLEFIELD, new BecomeMonstrousSourceEffect(monstrosityValue), new ManaCostsImpl<>(manaString));
this.monstrosityValue = monstrosityValue;
this.addHint(MonstrousHint.instance);
@ -72,7 +68,6 @@ public class MonstrosityAbility extends ActivatedAbilityImpl {
public int getMonstrosityValue() {
return monstrosityValue;
}
}
@ -94,28 +89,33 @@ class BecomeMonstrousSourceEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(source.getSourceId());
if (permanent != null && !permanent.isMonstrous() && source instanceof MonstrosityAbility) {
int monstrosityValue = ((MonstrosityAbility) source).getMonstrosityValue();
// handle monstrosity = X
if (monstrosityValue == Integer.MAX_VALUE) {
monstrosityValue = source.getManaCostsToPay().getX();
}
new AddCountersSourceEffect(CounterType.P1P1.createInstance(monstrosityValue)).apply(game, source);
permanent.setMonstrous(true);
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.BECOMES_MONSTROUS, source.getSourceId(), source, source.getControllerId(), monstrosityValue));
return true;
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null || permanent.isMonstrous()) {
return false;
}
return false;
int monstrosityValue = ((MonstrosityAbility) source).getMonstrosityValue();
// handle monstrosity = X
if (monstrosityValue == Integer.MAX_VALUE) {
monstrosityValue = source.getManaCostsToPay().getX();
}
permanent.addCounters(
CounterType.P1P1.createInstance(monstrosityValue),
source.getControllerId(), source, game
);
permanent.setMonstrous(true);
game.fireEvent(GameEvent.getEvent(
GameEvent.EventType.BECOMES_MONSTROUS, source.getSourceId(),
source, source.getControllerId(), monstrosityValue
));
return true;
}
private String setText(int monstrosityValue) {
StringBuilder sb = new StringBuilder("Monstrosity ");
sb.append(monstrosityValue == Integer.MAX_VALUE ? "X":monstrosityValue)
sb.append(monstrosityValue == Integer.MAX_VALUE ? "X" : monstrosityValue)
.append(". <i>(If this creature isn't monstrous, put ")
.append(monstrosityValue == Integer.MAX_VALUE ? "X":CardUtil.numberToText(monstrosityValue))
.append(monstrosityValue == Integer.MAX_VALUE ? "X" : CardUtil.numberToText(monstrosityValue))
.append(" +1/+1 counters on it and it becomes monstrous.)</i>").toString();
return sb.toString();
}
}

View file

@ -467,7 +467,8 @@ public enum SubType {
XENAGOS("Xenagos", SubTypeSet.PlaneswalkerType),
YANGGU("Yanggu", SubTypeSet.PlaneswalkerType),
YANLING("Yanling", SubTypeSet.PlaneswalkerType),
YODA("Yoda", SubTypeSet.PlaneswalkerType, true); // Star Wars
YODA("Yoda", SubTypeSet.PlaneswalkerType, true), // Star Wars,
ZARIEL("Zariel", SubTypeSet.PlaneswalkerType);
public static class SubTypePredicate implements Predicate<MageObject> {

View file

@ -542,15 +542,71 @@ public interface Game extends MageItem, Serializable {
* @param commanderCardType commander or signature spell
* @return
*/
default Set<Card> getCommanderCardsFromAnyZones(Player player, CommanderCardType commanderCardType) {
// from command zone
Set<Card> res = getCommanderCardsFromCommandZone(player, commanderCardType);
// from battlefield
this.getCommandersIds(player, commanderCardType, true).stream()
.map(this::getPermanent)
default Set<Card> getCommanderCardsFromAnyZones(Player player, CommanderCardType commanderCardType, Zone... searchZones) {
Set<Zone> needZones = Arrays.stream(searchZones).collect(Collectors.toSet());
if (needZones.isEmpty()) {
throw new IllegalArgumentException("Empty zones list in searching commanders");
}
Set<UUID> needCommandersIds = this.getCommandersIds(player, commanderCardType, true);
Set<Card> needCommandersCards = needCommandersIds.stream()
.map(this::getCard)
.filter(Objects::nonNull)
.forEach(res::add);
.collect(Collectors.toSet());
Set<Card> res = new HashSet<>();
// hand
if (needZones.contains(Zone.ALL) || needZones.contains(Zone.HAND)) {
needCommandersCards.stream()
.filter(card -> Zone.HAND.equals(this.getState().getZone(card.getId())))
.forEach(res::add);
}
// graveyard
if (needZones.contains(Zone.ALL) || needZones.contains(Zone.GRAVEYARD)) {
needCommandersCards.stream()
.filter(card -> Zone.GRAVEYARD.equals(this.getState().getZone(card.getId())))
.forEach(res::add);
}
// library
if (needZones.contains(Zone.ALL) || needZones.contains(Zone.LIBRARY)) {
needCommandersCards.stream()
.filter(card -> Zone.LIBRARY.equals(this.getState().getZone(card.getId())))
.forEach(res::add);
}
// battlefield (need permanent card)
if (needZones.contains(Zone.ALL) || needZones.contains(Zone.BATTLEFIELD)) {
needCommandersIds.stream()
.map(this::getPermanent)
.filter(Objects::nonNull)
.forEach(res::add);
}
// stack
if (needZones.contains(Zone.ALL) || needZones.contains(Zone.STACK)) {
needCommandersCards.stream()
.filter(card -> Zone.STACK.equals(this.getState().getZone(card.getId())))
.forEach(res::add);
}
// exiled
if (needZones.contains(Zone.ALL) || needZones.contains(Zone.EXILED)) {
needCommandersCards.stream()
.filter(card -> Zone.EXILED.equals(this.getState().getZone(card.getId())))
.forEach(res::add);
}
// command
if (needZones.contains(Zone.ALL) || needZones.contains(Zone.COMMAND)) {
res.addAll(getCommanderCardsFromCommandZone(player, commanderCardType));
}
// outside must be ignored (example: second side of MDFC commander after cast)
if (needZones.contains(Zone.OUTSIDE)) {
throw new IllegalArgumentException("Outside zone doesn't supported in searching commanders");
}
return res;
}

View file

@ -3185,8 +3185,8 @@ public abstract class GameImpl implements Game, Serializable {
@Override
public void cheat(UUID ownerId, List<Card> library, List<Card> hand, List<PermanentCard> battlefield, List<Card> graveyard, List<Card> command) {
// fake test ability for triggers and events
Ability fakeSourceAbility = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards"));
fakeSourceAbility.setControllerId(ownerId);
Ability fakeSourceAbilityTemplate = new SimpleStaticAbility(Zone.OUTSIDE, new InfoEffect("adding testing cards"));
fakeSourceAbilityTemplate.setControllerId(ownerId);
Player player = getPlayer(ownerId);
if (player != null) {
@ -3221,6 +3221,8 @@ public abstract class GameImpl implements Game, Serializable {
}
for (PermanentCard permanentCard : battlefield) {
Ability fakeSourceAbility = fakeSourceAbilityTemplate.copy();
fakeSourceAbility.setSourceId(permanentCard.getId());
CardUtil.putCardOntoBattlefieldWithEffects(fakeSourceAbility, this, permanentCard, player);
}

View file

@ -178,6 +178,7 @@ public class Combat implements Serializable, Copyable<Combat> {
numberCreaturesDefenderAttackedBy.clear();
creaturesForcedToAttack.clear();
maxAttackers = Integer.MIN_VALUE;
attackersTappedByAttack.clear();
}
public String getValue() {

View file

@ -0,0 +1,60 @@
package mage.game.command.emblems;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.common.AdditionalCombatPhaseEffect;
import mage.abilities.effects.common.UntapTargetEffect;
import mage.constants.TurnPhase;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.command.Emblem;
import mage.game.events.GameEvent;
import mage.target.common.TargetControlledCreaturePermanent;
/**
* @author TheElk801
*/
public final class ZarielArchdukeOfAvernusEmblem extends Emblem {
// 6: You get an emblem with "At the end of the first combat phase on your turn, untap target creature you control. After this phase, there is an additional combat phase."
public ZarielArchdukeOfAvernusEmblem() {
this.setName("Emblem Zariel");
this.setExpansionSetCodeForImage("AFR");
this.getAbilities().add(new ZarielArchdukeOfAvernusEmblemAbility());
}
}
class ZarielArchdukeOfAvernusEmblemAbility extends TriggeredAbilityImpl {
ZarielArchdukeOfAvernusEmblemAbility() {
super(Zone.COMMAND, new UntapTargetEffect());
this.addEffect(new AdditionalCombatPhaseEffect());
this.addTarget(new TargetControlledCreaturePermanent());
}
private ZarielArchdukeOfAvernusEmblemAbility(final ZarielArchdukeOfAvernusEmblemAbility ability) {
super(ability);
}
@Override
public ZarielArchdukeOfAvernusEmblemAbility copy() {
return new ZarielArchdukeOfAvernusEmblemAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.END_COMBAT_STEP_PRE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return game.isActivePlayer(getControllerId())
&& game.getTurn().getPhase(TurnPhase.COMBAT).getCount() == 0;
}
@Override
public String getRule() {
return "At the end of the first combat phase on your turn, untap target creature you control. " +
"After this phase, there is an additional combat phase.";
}
}

View file

@ -75,6 +75,12 @@ public interface Permanent extends Card, Controllable {
int getClassLevel();
/**
* Level up to next level.
*
* @param classLevel
* @return false on wrong settings (e.g. level up to multiple levels)
*/
boolean setClassLevel(int classLevel);
void setCardNumber(String cid);

View file

@ -220,4 +220,9 @@ public class PermanentCard extends PermanentImpl {
public Card getMainCard() {
return card.getMainCard();
}
@Override
public String toString() {
return card.toString();
}
}

View file

@ -1528,6 +1528,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
@Override
public boolean setClassLevel(int classLevel) {
// can level up to next (+1) level only
if (this.classLevel == classLevel - 1) {
this.classLevel = classLevel;
return true;

View file

@ -21,7 +21,7 @@ public final class EldraziHorrorToken extends TokenImpl {
}
public EldraziHorrorToken() {
super("Eldrazi Horror", "3/2 colorless Eldrazi Horror creature");
super("Eldrazi Horror", "3/2 colorless Eldrazi Horror creature token");
cardType.add(CardType.CREATURE);
subtype.add(SubType.ELDRAZI);
subtype.add(SubType.HORROR);

View file

@ -12,7 +12,7 @@ import mage.MageInt;
public final class GrovetenderDruidsPlantToken extends TokenImpl {
public GrovetenderDruidsPlantToken() {
super("Plant", "1/1 green Plant creature");
super("Plant", "1/1 green Plant creature token");
cardType.add(CardType.CREATURE);
color.setGreen(true);
subtype.add(SubType.PLANT);

View file

@ -692,6 +692,11 @@ public class StackAbility extends StackObjectImpl implements Ability {
return this.ability.getIcons();
}
@Override
public List<CardIcon> getIcons(Game game) {
return this.ability.getIcons(game);
}
@Override
public Ability addIcon(CardIcon cardIcon) {
throw new IllegalArgumentException("Stack ability is not supports icon adding");

View file

@ -81,7 +81,11 @@ public class CopyTokenFunction implements Function<Token, Card> {
for (Ability ability0 : sourceObj.getAbilities()) {
Ability ability = ability0.copy();
ability.newOriginalId(); // The token is independant from the copy from object so it need a new original Id, otherwise there are problems to check for created continuous effects to check if the source (the Token) has still this ability
// The token is independant from the copy from object so it need a new original Id,
// otherwise there are problems to check for created continuous effects to check if
// the source (the Token) has still this ability
ability.newOriginalId();
target.addAbility(ability);
}

View file

@ -1,6 +1,7 @@
package mage.watchers.common;
import mage.Mana;
import mage.abilities.Ability;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.game.Game;
@ -51,8 +52,14 @@ public class ManaSpentToCastWatcher extends Watcher {
return manaMap.getOrDefault(sourceId, null);
}
public int getAndResetLastXValue(UUID sourceId) {
return xValueMap.getOrDefault(sourceId, 0);
public int getAndResetLastXValue(Ability source) {
if (xValueMap.containsKey(source.getSourceId())) {
// cast normal way
return xValueMap.get(source.getSourceId());
} else {
// put to battlefield without cast (example: copied spell must keep announced X)
return source.getManaCostsToPay().getX();
}
}
@Override