Merge pull request 'master' (#48) from External/mage:master into master
All checks were successful
/ build_release (push) Successful in 18m43s

Reviewed-on: #48
This commit is contained in:
Failure 2025-11-13 21:47:46 -08:00
commit 4f714203ae
443 changed files with 15065 additions and 2504 deletions

View file

@ -61,6 +61,8 @@ public interface TriggeredAbility extends Ability {
*/
int getRemainingTriggersLimitEachGame(Game game);
TriggeredAbility setOptional(boolean optional);
TriggeredAbility setDoOnlyOnceEachTurn(boolean doOnlyOnce);
/**

View file

@ -209,6 +209,12 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
return this;
}
@Override
public TriggeredAbility setOptional(boolean optional) {
this.optional = optional;
return this;
}
@Override
public TriggeredAbility withRuleTextReplacement(boolean replaceRuleText) {
this.replaceRuleText = replaceRuleText;

View file

@ -29,8 +29,7 @@ public class DealsDamageAttachedTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_PERMANENT
|| event.getType() == GameEvent.EventType.DAMAGED_PLAYER;
return event.getType() == GameEvent.EventType.DAMAGED_BATCH_BY_SOURCE;
}
@Override

View file

@ -14,7 +14,6 @@ import mage.util.CardUtil;
public class ReflexiveTriggeredAbility extends DelayedTriggeredAbility {
private final String text;
private final Condition condition;
public ReflexiveTriggeredAbility(Effect effect, boolean optional) {
this(effect, optional, null);
@ -27,13 +26,14 @@ public class ReflexiveTriggeredAbility extends DelayedTriggeredAbility {
public ReflexiveTriggeredAbility(Effect effect, boolean optional, String text, Condition condition) {
super(effect, Duration.EndOfTurn, true, optional);
this.text = text;
this.condition = condition;
if (condition != null) {
this.withInterveningIf(condition);
}
}
protected ReflexiveTriggeredAbility(final ReflexiveTriggeredAbility ability) {
super(ability);
this.text = ability.text;
this.condition = ability.condition;
}
@Override
@ -55,11 +55,6 @@ public class ReflexiveTriggeredAbility extends DelayedTriggeredAbility {
return CardUtil.getTextWithFirstCharUpperCase(text) + '.';
}
@Override
public boolean checkInterveningIfClause(Game game) {
return condition == null || condition.apply(game, this);
}
@Override
public ReflexiveTriggeredAbility setTriggerPhrase(String triggerPhrase) {
super.setTriggerPhrase(triggerPhrase);

View file

@ -0,0 +1,45 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.constants.SubType;
import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.util.CardUtil;
/**
* @author TheElk801
*/
public enum LessonsInGraveCondition implements Condition {
ONE(1),
THREE(3);
private final int amount;
private static final FilterCard filter = new FilterCard(SubType.LESSON);
private static final Hint hint = new ValueHint("Lesson cards in your graveyard", new CardsInControllerGraveyardCount(filter));
public static Hint getHint() {
return hint;
}
LessonsInGraveCondition(int amount) {
this.amount = amount;
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
return player != null && player.getGraveyard().count(filter, game) >= this.amount;
}
@Override
public String toString() {
if (amount == 1) {
return "there's a Lesson card in your graveyard";
}
return "there are " + CardUtil.numberToText(this.amount) + " or more Lesson cards in your graveyard";
}
}

View file

@ -0,0 +1,38 @@
package mage.abilities.condition.common;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.constants.SuperType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledLandPermanent;
import mage.game.Game;
/**
* @author TheElk801
*/
public enum YouControlABasicLandCondition implements Condition {
instance;
private static final Hint hint = new ConditionHint(instance);
public static Hint getHint() {
return hint;
}
private static final FilterPermanent filter = new FilterControlledLandPermanent();
static {
filter.add(SuperType.BASIC.getPredicate());
}
@Override
public boolean apply(Game game, Ability source) {
return game.getBattlefield().contains(filter, source.getControllerId(), source, game, 1);
}
@Override
public String toString() {
return "you control a basic land";
}
}

View file

@ -0,0 +1,91 @@
package mage.abilities.decorator;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.Effects;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import mage.target.targetpointer.TargetPointer;
import mage.util.CardUtil;
/**
* Adds condition to {@link OneShotEffect}. Acts as decorator.
*
* @author Grath
*/
public class OptionalOneShotEffect extends OneShotEffect {
private final Effects effects = new Effects();
public OptionalOneShotEffect(OneShotEffect effect) {
super(effect != null ? effect.getOutcome() : Outcome.Benefit); // must be first line, can't error for null effect here.
if (effect == null) {
throw new IllegalArgumentException("Wrong code usage: OptionalOneShotEffect should start with an effect to generate Outcome.");
}
this.effects.add(effect);
}
protected OptionalOneShotEffect(final OptionalOneShotEffect effect) {
super(effect);
this.effects.addAll(effect.effects.copy());
}
@Override
public boolean apply(Game game, Ability source) {
// nothing to do - no problem
if (effects.isEmpty()) {
return true;
}
Player player = game.getPlayer(source.getControllerId());
String chooseText = staticText;
if (chooseText == null || chooseText.isEmpty()) {
chooseText = getText(source.getModes().getMode());
chooseText = Character.toUpperCase(chooseText.charAt(0)) + chooseText.substring(1);
}
if (player != null && player.chooseUse(outcome, chooseText, source, game)) {
effects.setTargetPointer(this.getTargetPointer().copy());
effects.forEach(effect -> effect.apply(game, source));
return true;
}
return false;
}
public OptionalOneShotEffect addEffect(OneShotEffect effect) {
this.effects.add(effect);
return this;
}
@Override
public void setValue(String key, Object value) {
super.setValue(key, value);
this.effects.setValue(key, value);
}
@Override
public OptionalOneShotEffect copy() {
return new OptionalOneShotEffect(this);
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return "you may " + CardUtil.getTextWithFirstCharLowerCase(effects.getText(mode));
}
@Override
public OptionalOneShotEffect setTargetPointer(TargetPointer targetPointer) {
effects.setTargetPointer(targetPointer);
super.setTargetPointer(targetPointer);
return this;
}
@Override
public OptionalOneShotEffect withTargetDescription(String target) {
effects.forEach(effect -> effect.withTargetDescription(target));
return this;
}
}

View file

@ -36,14 +36,12 @@ public class IntPlusDynamicValue implements DynamicValue {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(baseValue).append(" plus ");
return sb.append(value.toString()).toString();
return baseValue + " plus " + value.toString();
}
@Override
public String getMessage() {
return value.getMessage();
return baseValue + " plus " + value.getMessage();
}
@Override

View file

@ -48,6 +48,6 @@ public class CountersControllerCount implements DynamicValue {
@Override
public String getMessage() {
return (counterType != null ? counterType.toString() + ' ' : "") + "counter on {this}'s controller";
return "the number of " + counterType.getName() + " counters you have";
}
}

View file

@ -0,0 +1,55 @@
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.constants.SubType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent;
import mage.game.Game;
/**
* @author TheElk801
*/
public enum ShrinesYouControlCount implements DynamicValue {
WHERE_X("X", "the number of Shrines you control"),
FOR_EACH("1", "Shrine you control");
private static final Hint hint = new ValueHint("Shrines you control", WHERE_X);
public static Hint getHint() {
return hint;
}
private static final FilterPermanent filter = new FilterControlledPermanent(SubType.SHRINE);
private final String number;
private final String message;
ShrinesYouControlCount(String number, String message) {
this.number = number;
this.message = message;
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return game.getBattlefield().count(filter, sourceAbility.getControllerId(), sourceAbility, game);
}
@Override
public ShrinesYouControlCount copy() {
return this;
}
@Override
public String getMessage() {
return message;
}
@Override
public String toString() {
return number;
}
}

View file

@ -248,6 +248,10 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
return startingControllerId;
}
protected int getEffectStartingOnTurn() {
return effectStartingOnTurn;
}
@Override
public void setStartingControllerAndTurnNum(Game game, UUID startingController, UUID activePlayerId) {
this.startingControllerId = startingController;

View file

@ -58,8 +58,8 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
private final boolean tapped;
private Permanent savedPermanent = null;
private int startingLoyalty = -1;
private final int tokenPower;
private final int tokenToughness;
private int tokenPower;
private int tokenToughness;
private boolean useLKI = false;
private PermanentModifier permanentModifier = null;
@ -387,6 +387,16 @@ public class CreateTokenCopyTargetEffect extends OneShotEffect {
return this;
}
public CreateTokenCopyTargetEffect setPower(int tokenPower) {
this.tokenPower = tokenPower;
return this;
}
public CreateTokenCopyTargetEffect setToughness(int tokenToughness) {
this.tokenToughness = tokenToughness;
return this;
}
public CreateTokenCopyTargetEffect addAbilityClassesToRemoveFromTokens(Class<? extends Ability> clazz) {
this.abilityClazzesToRemove.add(clazz);
return this;

View file

@ -0,0 +1,83 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
/**
* @author xenohedron
*/
public class DamageTargetAndAllControlledEffect extends OneShotEffect {
private final int firstAmount;
private final int secondAmount;
private final FilterPermanent filter;
/**
* Deals simultaneous damage to the target and to each creature the target controls
*/
public DamageTargetAndAllControlledEffect(int amount) {
this(amount, amount, StaticFilters.FILTER_PERMANENT_CREATURE);
}
/**
* Deals simultaneous damage to the target and to each creature the target controls
*/
public DamageTargetAndAllControlledEffect(int firstAmount, int secondAmount, FilterPermanent filter) {
super(Outcome.Damage);
this.firstAmount = firstAmount;
this.secondAmount = secondAmount;
this.filter = filter;
}
protected DamageTargetAndAllControlledEffect(final DamageTargetAndAllControlledEffect effect) {
super(effect);
this.firstAmount = effect.firstAmount;
this.secondAmount = effect.secondAmount;
this.filter = effect.filter.copy();
}
@Override
public DamageTargetAndAllControlledEffect copy() {
return new DamageTargetAndAllControlledEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent != null) {
permanent.damage(firstAmount, source.getSourceId(), source, game);
} else {
Player player = game.getPlayer(getTargetPointer().getFirst(game, source));
if (player != null) {
player.damage(firstAmount, source.getSourceId(), source, game);
}
}
Player controller = game.getPlayerOrPlaneswalkerController(getTargetPointer().getFirst(game, source));
if (controller != null) {
for (Permanent perm : game.getBattlefield().getAllActivePermanents(filter, controller.getId(), game)) {
perm.damage(secondAmount, source.getSourceId(), source, game);
}
}
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
String description = getTargetPointer().describeTargets(mode.getTargets(), "that player");
return "{this} deals " + firstAmount + "damage to " + description +
" and " + secondAmount + " damage to each " + filter.getMessage() +
" that player" +
(description.contains("planeswalker") ? " or that planeswalker's controller" : "") +
" controls";
}
}

View file

@ -0,0 +1,77 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.UUID;
/**
* @author xenohedron
*/
public class DamageTargetAndSelfEffect extends OneShotEffect {
private final int firstAmount;
private final int secondAmount;
/**
* Deals simultaneous damage to the target and to itself
*/
public DamageTargetAndSelfEffect(int amount) {
this(amount, amount);
}
/**
* Deals simultaneous damage to the target and to itself
*/
public DamageTargetAndSelfEffect(int firstAmount, int secondAmount) {
super(Outcome.Damage);
this.firstAmount = firstAmount;
this.secondAmount = secondAmount;
}
protected DamageTargetAndSelfEffect(final DamageTargetAndSelfEffect effect) {
super(effect);
this.firstAmount = effect.firstAmount;
this.secondAmount = effect.secondAmount;
}
@Override
public DamageTargetAndSelfEffect copy() {
return new DamageTargetAndSelfEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
for (UUID targetId : this.getTargetPointer().getTargets(game, source)) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
permanent.damage(firstAmount, source.getSourceId(), source, game);
} else {
Player player = game.getPlayer(targetId);
if (player != null) {
player.damage(firstAmount, source.getSourceId(), source, game);
}
}
}
Permanent itself = source.getSourcePermanentIfItStillExists(game);
if (itself != null) {
itself.damage(secondAmount, source.getSourceId(), source, game);
}
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return "{this} deals " + firstAmount + "damage to " +
getTargetPointer().describeTargets(mode.getTargets(), "that creature") +
" and " + secondAmount + "damage to itself";
}
}

View file

@ -0,0 +1,69 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.UUID;
/**
* @author xenohedron
*/
public class DamageTargetAndTargetControllerEffect extends OneShotEffect {
private final int firstAmount;
private final int secondAmount;
/**
* Deals simultaneous damage to the target and to the controller of the target
*/
public DamageTargetAndTargetControllerEffect(int firstAmount, int secondAmount) {
super(Outcome.Damage);
this.firstAmount = firstAmount;
this.secondAmount = secondAmount;
}
protected DamageTargetAndTargetControllerEffect(final DamageTargetAndTargetControllerEffect effect) {
super(effect);
this.firstAmount = effect.firstAmount;
this.secondAmount = effect.secondAmount;
}
@Override
public DamageTargetAndTargetControllerEffect copy() {
return new DamageTargetAndTargetControllerEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
for (UUID targetId : this.getTargetPointer().getTargets(game, source)) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
permanent.damage(firstAmount, source.getSourceId(), source, game);
}
Permanent lki = game.getPermanentOrLKIBattlefield(targetId);
if (lki != null) {
Player player = game.getPlayer(lki.getControllerId());
if (player != null) {
player.damage(secondAmount, source.getSourceId(), source, game);
}
}
}
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
String description = getTargetPointer().describeTargets(mode.getTargets(), "that creature");
return "{this} deals " + firstAmount + "damage to " + description +
" and " + secondAmount + "damage to that " +
(description.contains(" or ") ? "permanent's" : "creature's") + " controller";
}
}

View file

@ -0,0 +1,72 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.UUID;
/**
* @author xenohedron
*/
public class DamageTargetAndTargetEffect extends OneShotEffect {
private final int firstAmount;
private final int secondAmount;
/**
* Deals simultaneous damage to two targets. Must set target tag 1 and 2
*/
public DamageTargetAndTargetEffect(int firstAmount, int secondAmount) {
super(Outcome.Damage);
this.firstAmount = firstAmount;
this.secondAmount = secondAmount;
}
protected DamageTargetAndTargetEffect(final DamageTargetAndTargetEffect effect) {
super(effect);
this.firstAmount = effect.firstAmount;
this.secondAmount = effect.secondAmount;
}
@Override
public DamageTargetAndTargetEffect copy() {
return new DamageTargetAndTargetEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
source.getTargets().getTargetsByTag(1).forEach(uuid -> damageTarget(uuid, firstAmount, source, game));
source.getTargets().getTargetsByTag(2).forEach(uuid -> damageTarget(uuid, secondAmount, source, game));
return true;
}
private void damageTarget(UUID targetId, int amount, Ability source, Game game) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
permanent.damage(amount, source.getSourceId(), source, game) ;
} else {
Player player = game.getPlayer(targetId);
if (player != null) {
player.damage(amount, source.getSourceId(), source, game);
}
}
}
@Override
public String getText(Mode mode) {
// verify check that target tags are properly setup
if (mode.getTargets().getByTag(1) == null || mode.getTargets().getByTag(2) == null) {
throw new IllegalArgumentException("Wrong code usage: need to add tags to targets");
}
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return "{this} deals " + firstAmount + "damage to " + mode.getTargets().getByTag(1).getDescription() +
" and " + secondAmount + "damage to " + mode.getTargets().getByTag(2).getDescription();
}
}

View file

@ -0,0 +1,77 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.UUID;
/**
* @author xenohedron
*/
public class DamageTargetAndYouEffect extends OneShotEffect {
private final int firstAmount;
private final int secondAmount;
/**
* Deals simultaneous damage to the target and the controller of the source
*/
public DamageTargetAndYouEffect(int amount) {
this(amount, amount);
}
/**
* Deals simultaneous damage to the target and the controller of the source
*/
public DamageTargetAndYouEffect(int firstAmount, int secondAmount) {
super(Outcome.Damage);
this.firstAmount = firstAmount;
this.secondAmount = secondAmount;
}
protected DamageTargetAndYouEffect(final DamageTargetAndYouEffect effect) {
super(effect);
this.firstAmount = effect.firstAmount;
this.secondAmount = effect.secondAmount;
}
@Override
public DamageTargetAndYouEffect copy() {
return new DamageTargetAndYouEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
for (UUID targetId : this.getTargetPointer().getTargets(game, source)) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
permanent.damage(firstAmount, source.getSourceId(), source, game);
} else {
Player player = game.getPlayer(targetId);
if (player != null) {
player.damage(firstAmount, source.getSourceId(), source, game);
}
}
}
Player controller = game.getPlayer(source.getControllerId());
if (controller != null) {
controller.damage(secondAmount, source.getSourceId(), source, game);
}
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return "{this} deals " + firstAmount + "damage to " +
getTargetPointer().describeTargets(mode.getTargets(), "that creature") +
" and " + secondAmount + "damage to you";
}
}

View file

@ -4,7 +4,6 @@ import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
@ -24,7 +23,6 @@ public class DamageTargetEffect extends OneShotEffect {
protected DynamicValue amount;
protected boolean preventable;
protected String targetDescription;
protected boolean useOnlyTargetPointer; // TODO: investigate why do we ignore targetPointer by default??
protected String sourceName = "{this}";
public DamageTargetEffect(int amount) {
@ -44,10 +42,6 @@ public class DamageTargetEffect extends OneShotEffect {
this(StaticValue.get(amount), preventable, targetDescription);
}
public DamageTargetEffect(int amount, boolean preventable, String targetDescription, boolean useOnlyTargetPointer) {
this(StaticValue.get(amount), preventable, targetDescription, useOnlyTargetPointer);
}
public DamageTargetEffect(int amount, boolean preventable, String targetDescription, String whoDealDamageName) {
this(StaticValue.get(amount), preventable, targetDescription);
this.sourceName = whoDealDamageName;
@ -67,15 +61,10 @@ public class DamageTargetEffect extends OneShotEffect {
}
public DamageTargetEffect(DynamicValue amount, boolean preventable, String targetDescription) {
this(amount, preventable, targetDescription, false);
}
public DamageTargetEffect(DynamicValue amount, boolean preventable, String targetDescription, boolean useOnlyTargetPointer) {
super(Outcome.Damage);
this.amount = amount;
this.preventable = preventable;
this.targetDescription = targetDescription;
this.useOnlyTargetPointer = useOnlyTargetPointer;
}
public int getAmount() {
@ -95,21 +84,15 @@ public class DamageTargetEffect extends OneShotEffect {
this.amount = effect.amount.copy();
this.preventable = effect.preventable;
this.targetDescription = effect.targetDescription;
this.useOnlyTargetPointer = effect.useOnlyTargetPointer;
this.sourceName = effect.sourceName;
}
@Override
public DamageTargetEffect withTargetDescription(String targetDescription) {
this.targetDescription = targetDescription;
return this;
}
// TODO: this should most likely be refactored to not be needed and always use target pointer.
public Effect setUseOnlyTargetPointer(boolean useOnlyTargetPointer) {
this.useOnlyTargetPointer = useOnlyTargetPointer;
return this;
}
@Override
public DamageTargetEffect copy() {
return new DamageTargetEffect(this);
@ -117,21 +100,6 @@ public class DamageTargetEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
if (!useOnlyTargetPointer && source.getTargets().size() > 1) {
for (Target target : source.getTargets()) {
for (UUID targetId : target.getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
permanent.damage(amount.calculate(game, source, this), source.getSourceId(), source, game, false, preventable);
}
Player player = game.getPlayer(targetId);
if (player != null) {
player.damage(amount.calculate(game, source, this), source.getSourceId(), source, game, false, preventable);
}
}
}
return true;
}
for (UUID targetId : this.getTargetPointer().getTargets(game, source)) {
Permanent permanent = game.getPermanent(targetId);
if (permanent != null) {
@ -154,7 +122,7 @@ public class DamageTargetEffect extends OneShotEffect {
StringBuilder sb = new StringBuilder();
String message = amount.getMessage();
sb.append(this.sourceName).append(" deals ");
if (message.isEmpty() || !message.equals("1")) {
if (!message.equals("1")) {
sb.append(amount);
}
if (!sb.toString().endsWith(" ")) {

View file

@ -54,6 +54,7 @@ public class GainLifeEffect extends OneShotEffect {
@Override
public String getText(Mode mode) {
// TODO: this text generation probably needs reworking
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}

View file

@ -6,7 +6,6 @@ import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.Target;
import java.util.ArrayList;
import java.util.List;
@ -42,37 +41,26 @@ public class TargetsDamageTargetsEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
if (source.getTargets().size() < 2) {
return false;
}
Target damageTarget = source.getTargets().getByTag(1);
Target additionalDamageTarget = source.getTargets().getByTag(2);
Target destTarget = source.getTargets().getByTag(3);
List<Permanent> damagingPermanents = new ArrayList<>();
List<Permanent> receivingPermanents = new ArrayList<>();
for (UUID id : damageTarget.getTargets()) {
for (UUID id : source.getTargets().getTargetsByTag(1)) { // dealing damage
Permanent permanent = game.getPermanent(id);
if (permanent != null) {
damagingPermanents.add(permanent);
}
}
if (additionalDamageTarget != null) {
for (UUID id : additionalDamageTarget.getTargets()) {
Permanent permanent = game.getPermanent(id);
if (permanent != null) {
damagingPermanents.add(permanent);
}
for (UUID id : source.getTargets().getTargetsByTag(2)) { // additional dealing damage, if applicable
Permanent permanent = game.getPermanent(id);
if (permanent != null) {
damagingPermanents.add(permanent);
}
}
for (UUID id : destTarget.getTargets()) {
for (UUID id : source.getTargets().getTargetsByTag(3)) { // receiving damage
Permanent permanent = game.getPermanent(id);
if (permanent != null) {
receivingPermanents.add(permanent);
}
}
if (receivingPermanents.isEmpty() || damagingPermanents.isEmpty()) {
return false;
}
@ -86,6 +74,10 @@ public class TargetsDamageTargetsEffect extends OneShotEffect {
@Override
public String getText(Mode mode) {
// verify check that target tags are properly setup
if (mode.getTargets().getByTag(1) == null || mode.getTargets().getByTag(3) == null) {
throw new IllegalArgumentException("Wrong code usage: need to add tags to targets");
}
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
@ -102,4 +94,4 @@ public class TargetsDamageTargetsEffect extends OneShotEffect {
sb.append(mode.getTargets().getByTag(3).getDescription());
return sb.toString();
}
}
}

View file

@ -0,0 +1,42 @@
package mage.abilities.effects.common.combat;
import mage.abilities.Ability;
import mage.abilities.effects.RestrictionEffect;
import mage.constants.Duration;
import mage.filter.common.FilterBlockingCreature;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author LevelX2
*/
public class CantBlockAloneSourceEffect extends RestrictionEffect {
private static final FilterBlockingCreature filter = new FilterBlockingCreature("Blocking creatures");
public CantBlockAloneSourceEffect() {
super(Duration.WhileOnBattlefield);
staticText = "{this} can't block alone";
}
protected CantBlockAloneSourceEffect(final CantBlockAloneSourceEffect effect) {
super(effect);
}
@Override
public CantBlockAloneSourceEffect copy() {
return new CantBlockAloneSourceEffect(this);
}
@Override
public boolean canBlockCheckAfter(Ability source, Game game, boolean canUseChooseDialogs) {
return false;
}
@Override
public boolean applies(Permanent permanent, Ability source, Game game) {
if (permanent.getId().equals(source.getSourceId())) {
return game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source, game).size() <= 1;
}
return false;
}
}

View file

@ -1,5 +1,6 @@
package mage.abilities.effects.common.continuous;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.mana.*;
@ -8,6 +9,8 @@ import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import java.util.Iterator;
/**
* @author TheElk801
*/
@ -21,9 +24,9 @@ public class BecomesAllBasicsControlledEffect extends ContinuousEffectImpl {
new GreenManaAbility()
};
public BecomesAllBasicsControlledEffect() {
super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Detriment);
this.staticText = "Lands you control are every basic land type in addition to their other types";
public BecomesAllBasicsControlledEffect(Duration duration) {
super(duration, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Detriment);
this.staticText = "lands you control are every basic land type in addition to their other types";
dependendToTypes.add(DependencyType.BecomeNonbasicLand);
dependencyTypes.add(DependencyType.BecomeMountain);
dependencyTypes.add(DependencyType.BecomeForest);
@ -42,29 +45,55 @@ public class BecomesAllBasicsControlledEffect extends ContinuousEffectImpl {
}
@Override
public boolean apply(Game game, Ability source) {
for (Permanent permanent : game.getBattlefield().getActivePermanents(
StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND, source.getControllerId(), game)) {
permanent.addSubType(game,
SubType.PLAINS,
SubType.ISLAND,
SubType.SWAMP,
SubType.MOUNTAIN,
SubType.FOREST);
// Optimization: Remove basic mana abilities since they are redundant with AnyColorManaAbility
// and keeping them will only produce too many combinations inside ManaOptions
for (Ability basicManaAbility : basicManaAbilities) {
if (permanent.getAbilities(game).containsRule(basicManaAbility)) {
permanent.removeAbility(basicManaAbility, source.getSourceId(), game);
}
public void init(Ability source, Game game) {
super.init(source, game);
if (getAffectedObjectsSet()) {
for (Permanent permanent : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND, source.getControllerId(), game)) {
affectedObjectList.add(new MageObjectReference(permanent, game));
}
// Add the {T}: Add one mana of any color ability
// This is functionally equivalent to having five "{T}: Add {COLOR}" for each COLOR in {W}{U}{B}{R}{G}
AnyColorManaAbility ability = new AnyColorManaAbility();
if (!permanent.getAbilities(game).containsRule(ability)) {
permanent.addAbility(ability, source.getSourceId(), game);
}
}
@Override
public boolean apply(Game game, Ability source) {
if (!getAffectedObjectsSet()) {
for (Permanent permanent : game.getBattlefield().getActivePermanents(
StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND, source.getControllerId(), game
)) {
removeTypes(permanent, game, source);
}
return true;
}
for (Iterator<MageObjectReference> it = affectedObjectList.iterator(); it.hasNext(); ) {
Permanent permanent = it.next().getPermanent(game);
if (permanent != null) {
removeTypes(permanent, game, source);
} else {
it.remove();
}
}
return true;
}
private static void removeTypes(Permanent permanent, Game game, Ability source) {
permanent.addSubType(game,
SubType.PLAINS,
SubType.ISLAND,
SubType.SWAMP,
SubType.MOUNTAIN,
SubType.FOREST);
// Optimization: Remove basic mana abilities since they are redundant with AnyColorManaAbility
// and keeping them will only produce too many combinations inside ManaOptions
for (Ability basicManaAbility : basicManaAbilities) {
if (permanent.getAbilities(game).containsRule(basicManaAbility)) {
permanent.removeAbility(basicManaAbility, source.getSourceId(), game);
}
}
// Add the {T}: Add one mana of any color ability
// This is functionally equivalent to having five "{T}: Add {COLOR}" for each COLOR in {W}{U}{B}{R}{G}
AnyColorManaAbility ability = new AnyColorManaAbility();
if (!permanent.getAbilities(game).containsRule(ability)) {
permanent.addAbility(ability, source.getSourceId(), game);
}
}
}

View file

@ -0,0 +1,45 @@
package mage.abilities.effects.common.continuous;
import mage.abilities.Ability;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.mana.RedManaAbility;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author LevelX2
*/
public class NonbasicLandsAreMountainsEffect extends ContinuousEffectImpl {
public NonbasicLandsAreMountainsEffect() {
super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Detriment);
this.staticText = "nonbasic lands are Mountains";
this.dependencyTypes.add(DependencyType.BecomeMountain);
this.dependendToTypes.add(DependencyType.BecomeNonbasicLand);
}
private NonbasicLandsAreMountainsEffect(final NonbasicLandsAreMountainsEffect effect) {
super(effect);
}
@Override
public NonbasicLandsAreMountainsEffect copy() {
return new NonbasicLandsAreMountainsEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
for (Permanent land : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_LANDS_NONBASIC, source.getControllerId(), game)) {
// 305.7 Note that this doesn't remove any abilities that were granted to the land by other effects
// So the ability removing has to be done before Layer 6
// Lands have their mana ability intrinsically, so that is added in layer 4
land.removeAllSubTypes(game, SubTypeSet.NonBasicLandType);
land.addSubType(game, SubType.MOUNTAIN);
land.removeAllAbilities(source.getSourceId(), game);
land.addAbility(new RedManaAbility(), source.getSourceId(), game);
}
return true;
}
}

View file

@ -17,11 +17,11 @@ import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ -47,18 +47,21 @@ public class AirbendTargetEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
Set<Permanent> permanents = this
if (player == null) {
return false;
}
Set<Card> objects = this
.getTargetPointer()
.getTargets(game, source)
.stream()
.map(game::getPermanent)
.map(uuid -> Optional.ofNullable((Card) game.getPermanent(uuid)).orElseGet(() -> game.getSpell(uuid)))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (player == null || permanents.isEmpty()) {
if (objects.isEmpty()) {
return false;
}
player.moveCards(permanents, Zone.EXILED, source, game);
Cards cards = new CardsImpl(permanents);
player.moveCards(objects, Zone.EXILED, source, game);
Cards cards = new CardsImpl(objects);
cards.retainZone(Zone.EXILED, game);
for (Card card : cards.getCards(game)) {
game.addEffect(new AirbendingCastEffect(card, game), source);

View file

@ -4,6 +4,8 @@ import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.Mode;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ReturnToBattlefieldUnderOwnerControlTargetEffect;
import mage.abilities.effects.common.continuous.BecomesCreatureTargetEffect;
@ -25,9 +27,13 @@ import mage.util.CardUtil;
*/
public class EarthbendTargetEffect extends OneShotEffect {
private final int amount;
private final DynamicValue amount;
public EarthbendTargetEffect(int amount) {
this(StaticValue.get(amount));
}
public EarthbendTargetEffect(DynamicValue amount) {
super(Outcome.Benefit);
this.amount = amount;
}
@ -48,18 +54,23 @@ public class EarthbendTargetEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
int value = amount.calculate(game, source, this);
doEarthBend(permanent, value, game, source);
return true;
}
public static void doEarthBend(Permanent permanent, int value, Game game, Ability source) {
game.addEffect(new BecomesCreatureTargetEffect(
new CreatureToken(0, 0)
.withAbility(HasteAbility.getInstance()),
false, true, Duration.Custom
), source);
permanent.addCounters(CounterType.P1P1.createInstance(amount), source, game);
permanent.addCounters(CounterType.P1P1.createInstance(value), source, game);
game.addDelayedTriggeredAbility(new EarthbendingDelayedTriggeredAbility(permanent, game), source);
game.fireEvent(GameEvent.getEvent(
GameEvent.EventType.EARTHBENDED, permanent.getId(),
source, source.getControllerId(), amount
source, source.getControllerId(), value
));
return true;
}
@Override
@ -67,10 +78,21 @@ public class EarthbendTargetEffect extends OneShotEffect {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return "earthbend " + amount + ". <i>(Target land you control becomes a 0/0 creature " +
"with haste that's still a land. Put " + CardUtil.numberToText(amount, "a") +
" +1/+1 counter" + (amount > 1 ? "s" : "") + " on it. " +
"When it dies or is exiled, return it to the battlefield tapped.)</i>";
StringBuilder sb = new StringBuilder("earthbend ");
sb.append(amount);
if (!(amount instanceof StaticValue)) {
sb.append(", where X is ");
sb.append(amount.getMessage());
}
sb.append(". <i>(Target land you control becomes a 0/0 creature with haste that's still a land. Put ");
String value = amount instanceof StaticValue
? CardUtil.numberToText(((StaticValue) amount).getValue(), "a")
: amount.toString();
sb.append(value);
sb.append(" +1/+1 counter");
sb.append(("a".equals(value) ? "" : "s"));
sb.append(" on it. When it dies or is exiled, return it to the battlefield tapped.)</i>");
return sb.toString();
}
}

View file

@ -1,26 +0,0 @@
package mage.abilities.keyword;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.combat.CantAttackAloneSourceEffect;
import mage.constants.Zone;
/**
* @author magenoxx_at_googlemail.com
*/
public class CantAttackAloneAbility extends SimpleStaticAbility {
public CantAttackAloneAbility() {
super(Zone.BATTLEFIELD, new CantAttackAloneSourceEffect());
}
private CantAttackAloneAbility(CantAttackAloneAbility ability) {
super(ability);
}
@Override
public CantAttackAloneAbility copy() {
return new CantAttackAloneAbility(this);
}
}

View file

@ -0,0 +1,29 @@
package mage.abilities.keyword;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.combat.CantAttackAloneSourceEffect;
import mage.abilities.effects.common.combat.CantBlockAloneSourceEffect;
import mage.constants.Zone;
/**
* @author notgreat
*/
public class CantAttackOrBlockAloneAbility extends SimpleStaticAbility {
public CantAttackOrBlockAloneAbility() {
super(Zone.BATTLEFIELD, new CantAttackAloneSourceEffect().setText("{this} can't attack or block alone"));
this.addEffect(new CantBlockAloneSourceEffect().setText(""));
}
private CantAttackOrBlockAloneAbility(CantAttackOrBlockAloneAbility ability) {
super(ability);
}
@Override
public CantAttackOrBlockAloneAbility copy() {
return new CantAttackOrBlockAloneAbility(this);
}
}

View file

@ -1,40 +0,0 @@
package mage.abilities.keyword;
import mage.constants.Zone;
import mage.abilities.MageSingleton;
import mage.abilities.StaticAbility;
import java.io.ObjectStreamException;
/**
* @author magenoxx_at_googlemail.com
*/
public class CantBlockAloneAbility extends StaticAbility implements MageSingleton {
private static final CantBlockAloneAbility instance = new CantBlockAloneAbility();
private Object readResolve() throws ObjectStreamException {
return instance;
}
public static CantBlockAloneAbility getInstance() {
return instance;
}
private CantBlockAloneAbility() {
super(Zone.BATTLEFIELD, null);
}
@Override
public String getRule() {
return "{this} can't block alone.";
}
@Override
public CantBlockAloneAbility copy() {
return instance;
}
}

View file

@ -1,23 +1,22 @@
package mage.abilities.keyword;
import mage.MageIdentifier;
import mage.abilities.PlayLandAbility;
import mage.cards.Card;
import mage.constants.Zone;
import mage.abilities.Ability;
import mage.abilities.StaticAbility;
import mage.abilities.effects.AsThoughEffect;
import mage.abilities.effects.AsThoughEffectImpl;
import mage.constants.*;
import mage.game.Game;
import java.util.Set;
import java.util.UUID;
public class MayhemLandAbility extends PlayLandAbility {
public class MayhemLandAbility extends StaticAbility {
private final String rule;
public MayhemLandAbility(Card card) {
super(card.getName());
this.zone = Zone.GRAVEYARD;
public MayhemLandAbility() {
super(AbilityType.STATIC, Zone.GRAVEYARD);
this.newId();
this.name += " with Mayhem";
this.addEffect(new MayhemPlayEffect());
this.addWatcher(new MayhemWatcher());
this.setRuleAtTheTop(true);
this.rule = "Mayhem " +
@ -30,24 +29,6 @@ public class MayhemLandAbility extends PlayLandAbility {
this.rule = ability.rule;
}
@Override
public ActivationStatus canActivate(UUID playerId, Game game) {
if (!Zone.GRAVEYARD.match(game.getState().getZone(getSourceId()))
|| !MayhemWatcher.checkCard(getSourceId(), game)) {
return ActivationStatus.getFalse();
}
return super.canActivate(playerId, game);
}
@Override
public boolean activate(Game game, Set<MageIdentifier> allowedIdentifiers, boolean noMana) {
if (!super.activate(game, allowedIdentifiers, noMana)) {
return false;
}
this.setCostsTag(MayhemAbility.MAYHEM_ACTIVATION_VALUE_KEY, null);
return true;
}
@Override
public MayhemLandAbility copy() {
return new MayhemLandAbility(this);
@ -57,4 +38,31 @@ public class MayhemLandAbility extends PlayLandAbility {
public String getRule() {
return rule;
}
}
}
class MayhemPlayEffect extends AsThoughEffectImpl {
public MayhemPlayEffect() {
super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileInGraveyard, Outcome.Neutral);
}
public MayhemPlayEffect(final MayhemPlayEffect effect) {
super(effect);
}
@Override
public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) {
return Zone.GRAVEYARD.match(game.getState().getZone(sourceId))
&& MayhemWatcher.checkCard(sourceId, game);
}
@Override
public boolean apply(Game game, Ability source) {
return true;
}
@Override
public AsThoughEffect copy() {
return new MayhemPlayEffect(this);
}
}

View file

@ -272,6 +272,8 @@ public enum TokenRepository {
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 12, "https://api.scryfall.com/cards/tpip/1/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 13, "https://api.scryfall.com/cards/teoc/1/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 14, "https://api.scryfall.com/cards/tspm/1/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 15, "https://api.scryfall.com/cards/ttla/1/en?format=image"));
res.add(createXmageToken(XMAGE_IMAGE_NAME_COPY, 16, "https://api.scryfall.com/cards/ttla/2/en?format=image"));
// City's Blessing
// https://scryfall.com/search?q=type%3Atoken+include%3Aextras+unique%3Aprints+City%27s+Blessing+&unique=cards&as=grid&order=name

View file

@ -42,6 +42,7 @@ public enum AffinityType {
BIRDS(new FilterControlledPermanent(SubType.BIRD, "Birds")),
CITIZENS(new FilterControlledPermanent(SubType.CITIZEN, "Citizens")),
SLIVERS(new FilterControlledPermanent(SubType.SLIVER, "Slivers")),
ALLIES(new FilterControlledPermanent(SubType.ALLY, "Allies"), "Ally"),
TOWNS(new FilterControlledPermanent(SubType.TOWN, "Towns")),
GATES(new FilterControlledPermanent(SubType.GATE, "Gates"), GatesYouControlHint.instance),
SNOW_LANDS(AffinityFilters.SNOW_LANDS),

View file

@ -6,7 +6,11 @@ import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.util.CardUtil;
import java.util.Set;
/**
* @author awjackson
@ -17,6 +21,7 @@ public enum PutCards {
GRAVEYARD(Outcome.Discard, Zone.GRAVEYARD, "into your graveyard"),
BATTLEFIELD(Outcome.PutCardInPlay, Zone.BATTLEFIELD, "onto the battlefield"),
BATTLEFIELD_TAPPED(Outcome.PutCardInPlay, Zone.BATTLEFIELD, "onto the battlefield tapped"),
BATTLEFIELD_TAPPED_ATTACKING(Outcome.PutCardInPlay, Zone.BATTLEFIELD, "onto the battlefield tapped and attacking"),
BATTLEFIELD_TRANSFORMED(Outcome.PutCardInPlay, Zone.BATTLEFIELD, "onto the battlefield transformed"),
EXILED(Outcome.Exile, Zone.EXILED, "into exile"), // may need special case code to generate correct text
TOP_OR_BOTTOM(Outcome.Benefit, Zone.LIBRARY, "on the top or bottom of your library"),
@ -75,6 +80,15 @@ public enum PutCards {
return player.putCardsOnBottomOfLibrary(new CardsImpl(card), game, source, false);
case BATTLEFIELD_TAPPED:
return player.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null);
case BATTLEFIELD_TAPPED_ATTACKING:
if (player.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null)) {
Permanent permanent = CardUtil.getPermanentFromCardPutToBattlefield(card, game);
if (permanent != null) {
game.getCombat().addAttackingCreature(permanent.getId(), game);
}
return true;
}
return false;
case SHUFFLE:
return player.shuffleCardsToLibrary(card, game, source);
case BATTLEFIELD_TRANSFORMED:
@ -101,6 +115,18 @@ public enum PutCards {
return player.putCardsOnBottomOfLibrary(cards, game, source, false);
case BATTLEFIELD_TAPPED:
return player.moveCards(cards.getCards(game), Zone.BATTLEFIELD, source, game, true, false, false, null);
case BATTLEFIELD_TAPPED_ATTACKING:
Set<Card> cardSet = cards.getCards(game);
if (player.moveCards(cardSet, Zone.BATTLEFIELD, source, game, true, false, false, null)) {
for (Card card : cardSet) {
Permanent permanent = CardUtil.getPermanentFromCardPutToBattlefield(card, game);
if (permanent != null) {
game.getCombat().addAttackingCreature(permanent.getId(), game);
}
}
return true;
}
return false;
case SHUFFLE:
return player.shuffleCardsToLibrary(cards, game, source);
case BATTLEFIELD_TRANSFORMED:

View file

@ -333,6 +333,7 @@ public enum SubType {
PINCHER("Pincher", SubTypeSet.CreatureType),
PIRATE("Pirate", SubTypeSet.CreatureType),
PLANT("Plant", SubTypeSet.CreatureType),
PLATYPUS("Platypus", SubTypeSet.CreatureType),
PORCUPINE("Porcupine", SubTypeSet.CreatureType),
POSSUM("Possum", SubTypeSet.CreatureType),
PRAETOR("Praetor", SubTypeSet.CreatureType),

View file

@ -46,6 +46,7 @@ public enum CounterType {
COLLECTION("collection"),
COMPONENT("component"),
CONTESTED("contested"),
CONQUEROR("conqueror"),
CORPSE("corpse"),
CORRUPTION("corruption"),
CREDIT("credit"),

View file

@ -601,6 +601,13 @@ public final class StaticFilters {
FILTER_OPPONENTS_PERMANENT_ARTIFACT_OR_CREATURE.setLockedFilter(true);
}
public static final FilterPermanent FILTER_ANOTHER_PERMANENT = new FilterPermanent("another permanent");
static {
FILTER_ANOTHER_PERMANENT.add(AnotherPredicate.instance);
FILTER_ANOTHER_PERMANENT.setLockedFilter(true);
}
public static final FilterCreaturePermanent FILTER_ANOTHER_CREATURE = new FilterCreaturePermanent("another creature");
static {
@ -902,6 +909,13 @@ public final class StaticFilters {
FILTER_CONTROLLED_SAMURAI_OR_WARRIOR.setLockedFilter(true);
}
public static final FilterControlledPermanent FILTER_ANOTHER_CONTROLLED_SHRINE = new FilterControlledPermanent(SubType.SHRINE, "another Shrine you control");
static {
FILTER_ANOTHER_CONTROLLED_SHRINE.add(AnotherPredicate.instance);
FILTER_ANOTHER_CONTROLLED_SHRINE.setLockedFilter(true);
}
public static final FilterPlaneswalkerPermanent FILTER_PERMANENT_PLANESWALKER = new FilterPlaneswalkerPermanent();
static {

View file

@ -679,29 +679,6 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
possibleBlockers.put(attacker.getId(), goodBlockers);
}
// effects: can't block alone
// too much blockers
if (blockersCount == 1) {
List<UUID> toBeRemoved = new ArrayList<>();
for (UUID blockerId : getBlockers()) {
Permanent blocker = game.getPermanent(blockerId);
if (blocker != null && blocker.getAbilities().containsKey(CantBlockAloneAbility.getInstance().getId())) {
blockWasLegal = false;
if (!game.isSimulation()) {
game.informPlayers(blocker.getLogName() + " can't block alone. Removing it from combat.");
}
toBeRemoved.add(blockerId);
}
}
for (UUID blockerId : toBeRemoved) {
game.getCombat().removeBlocker(blockerId, game);
}
if (blockers.isEmpty()) {
this.blocked = false;
}
}
for (UUID uuid : attackers) {
Permanent attacker = game.getPermanent(uuid);
if (attacker != null && this.blocked) {

View file

@ -1,8 +1,7 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.keyword.CantAttackAloneAbility;
import mage.abilities.keyword.CantBlockAloneAbility;
import mage.abilities.keyword.CantAttackOrBlockAloneAbility;
import mage.constants.CardType;
import mage.constants.SubType;
@ -19,8 +18,7 @@ public final class BeastieToken extends TokenImpl {
power = new MageInt(4);
toughness = new MageInt(4);
this.addAbility(new CantAttackAloneAbility());
this.addAbility(CantBlockAloneAbility.getInstance());
this.addAbility(new CantAttackOrBlockAloneAbility());
}
private BeastieToken(final BeastieToken token) {

View file

@ -1,13 +1,10 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.DealsCombatDamageTriggeredAbility;
import mage.abilities.effects.common.SacrificeSourceEffect;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
public final class VrondissRageOfAncientsToken extends TokenImpl {
@ -21,7 +18,7 @@ public final class VrondissRageOfAncientsToken extends TokenImpl {
power = new MageInt(5);
toughness = new MageInt(4);
this.addAbility(new VrondissRageOfAncientsTokenTriggeredAbility());
this.addAbility(new DealsCombatDamageTriggeredAbility(new SacrificeSourceEffect(), false));
}
private VrondissRageOfAncientsToken(final VrondissRageOfAncientsToken token) {
@ -32,35 +29,3 @@ public final class VrondissRageOfAncientsToken extends TokenImpl {
return new VrondissRageOfAncientsToken(this);
}
}
class VrondissRageOfAncientsTokenTriggeredAbility extends TriggeredAbilityImpl {
public VrondissRageOfAncientsTokenTriggeredAbility() {
super(Zone.BATTLEFIELD, new SacrificeSourceEffect(), false);
}
protected VrondissRageOfAncientsTokenTriggeredAbility(final VrondissRageOfAncientsTokenTriggeredAbility ability) {
super(ability);
}
@Override
public VrondissRageOfAncientsTokenTriggeredAbility copy() {
return new VrondissRageOfAncientsTokenTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_PLAYER
|| event.getType() == GameEvent.EventType.DAMAGED_PERMANENT;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return event.getSourceId().equals(this.getSourceId());
}
@Override
public String getRule() {
return "When this creature deals damage, sacrifice it.";
}
}

View file

@ -42,6 +42,7 @@ public class ManaPool implements Serializable {
// empty mana pool effects
private final Set<ManaType> doNotEmptyManaTypes = new HashSet<>(); // keep some colors
private boolean manaBecomesBlack = false; // replace all pool by black
private boolean manaBecomesRed = false; // replace all pool by red
private boolean manaBecomesColorless = false; // replace all pool by colorless
private static final class ConditionalManaInfo {
@ -76,6 +77,7 @@ public class ManaPool implements Serializable {
}
this.doNotEmptyManaTypes.addAll(pool.doNotEmptyManaTypes);
this.manaBecomesBlack = pool.manaBecomesBlack;
this.manaBecomesRed = pool.manaBecomesRed;
this.manaBecomesColorless = pool.manaBecomesColorless;
}
@ -236,6 +238,7 @@ public class ManaPool implements Serializable {
public void clearEmptyManaPoolRules() {
doNotEmptyManaTypes.clear();
this.manaBecomesBlack = false;
this.manaBecomesRed = false;
this.manaBecomesColorless = false;
}
@ -247,6 +250,10 @@ public class ManaPool implements Serializable {
this.manaBecomesBlack = manaBecomesBlack;
}
public void setManaBecomesRed(boolean manaBecomesRed) {
this.manaBecomesRed = manaBecomesRed;
}
public void setManaBecomesColorless(boolean manaBecomesColorless) {
this.manaBecomesColorless = manaBecomesColorless;
}
@ -267,6 +274,9 @@ public class ManaPool implements Serializable {
if (manaBecomesBlack) {
continue;
}
if (manaBecomesRed) {
continue;
}
if (manaBecomesColorless) {
continue;
}
@ -315,12 +325,20 @@ public class ManaPool implements Serializable {
return 0;
}
}
// TODO: This should be reimplemented as replacement effects instead, so you can choose which applies.
if (manaBecomesBlack) {
int amount = toEmpty.get(manaType);
toEmpty.clear(manaType);
toEmpty.add(ManaType.BLACK, amount);
return 0;
}
if (manaBecomesRed) {
int amount = toEmpty.get(manaType);
toEmpty.clear(manaType);
toEmpty.add(ManaType.RED, amount);
return 0;
}
if (manaBecomesColorless) {
int amount = toEmpty.get(manaType);
toEmpty.clear(manaType);

View file

@ -1536,7 +1536,7 @@ public final class CardUtil {
} else {
chosenAbility = player.chooseAbilityForCast(cardToCast, game, true);
}
boolean result = false;
boolean result;
if (chosenAbility instanceof SpellAbility) {
result = player.cast(
(SpellAbility) chosenAbility,
@ -1545,6 +1545,8 @@ public final class CardUtil {
} else if (playLand && chosenAbility instanceof PlayLandAbility) {
Card land = game.getCard(chosenAbility.getSourceId());
result = player.playLand(land, game, true);
} else {
result = false;
}
partsToCast.forEach(card -> game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null));
if (result && spellCastTracker != null) {

View file

@ -1515,10 +1515,13 @@
# SLD
|Generate|TOK:SLD|Angel|||AngelToken|
|Generate|TOK:SLD|Blood|||BloodToken|
|Generate|TOK:SLD|Cat|1||GreenCatToken|
|Generate|TOK:SLD|Cat|2||CatToken2|
|Generate|TOK:SLD|Cat|3||CatToken2|
|Generate|TOK:SLD|Clue|||ClueArtifactToken|
|Generate|TOK:SLD|Cordyceps Infected|1||CordycepsInfectedToken|
|Generate|TOK:SLD|Cordyceps Infected|2||CordycepsInfectedToken|
|Generate|TOK:SLD|Dog|||WhiteDogToken|
|Generate|TOK:SLD|Egg|||AtlaPalaniToken|
|Generate|TOK:SLD|Faerie Rogue|1||FaerieRogueToken|
@ -2881,6 +2884,34 @@
|Generate|TOK:SPM|Spider|||Spider21Token|
|Generate|TOK:SPM|Treasure|||TreasureToken|
#TLA
|Generate|TOK:TLA|Ally|1||AllyToken|
|Generate|TOK:TLA|Ally|2||AllyToken|
|Generate|TOK:TLA|Ally|3||AllyToken|
|Generate|TOK:TLA|Ally|4||AllyToken|
|Generate|TOK:TLA|Ally|5||AllyToken|
|Generate|TOK:TLA|Ballistic Boulder|||BallisticBoulder|
|Generate|TOK:TLA|Bear|||BearsCompanionBearToken|
|Generate|TOK:TLA|Clue|1||ClueArtifactToken|
|Generate|TOK:TLA|Clue|2||ClueArtifactToken|
|Generate|TOK:TLA|Clue|3||ClueArtifactToken|
|Generate|TOK:TLA|Clue|4||ClueArtifactToken|
|Generate|TOK:TLA|Clue|5||ClueArtifactToken|
|Generate|TOK:TLA|Food|1||FoodToken|
|Generate|TOK:TLA|Food|2||FoodToken|
|Generate|TOK:TLA|Food|3||FoodToken|
|Generate|TOK:TLA|Monk|||MonkRedToken|
|Generate|TOK:TLA|Soldier|||SoldierFirebendingToken|
|Generate|TOK:TLA|Spirit|||SpiritWorldToken|
|Generate|TOK:TLA|Treasure|||TreasureToken|
#TLE
|Generate|TOK:TLE|Marit Lage|||MaritLageToken|
|Generate|TOK:TLE|Soldier|||SoldierRedToken|
#TMT
|Generate|TOK:TMT|Mutagen|||MutagenToken|
# JVC
|Generate|TOK:JVC|Elemental Shaman|||ElementalShamanToken|