TargetAmount refactors (#13128)

* Add minimum and maximum target counts as parameters for TargetAmount and its subclasses; update/add several rules comments (and one actual text) for clarity; remove unused imports

* Get amount+description from target instead of parameters for DistributeCountersEffect and DamageMultiEffect; additions to TargetImpl.getDescription to accommodate

* Create separate method to check if "any number" phrasing should be used, override it in TargetAmount

* Check instanceof TargetAmount before casting

* Add new constructors to chain off of for TargetCreaturePermanentAmount & TargetCreatureOrPlaneswalkerAmount

* Fix text for Storm the Seedcore

* Use Integer.MAX_VALUE instead of 0 to represent no maximum targets

* Add comment about getUseAnyNumber()

* Use amount-only constructors in some TargetAmount subclasses, add clarifying documentation

* Fix a few calls

* Require more specific filters
This commit is contained in:
Cameron Merkel 2024-12-17 18:23:18 -06:00 committed by GitHub
parent 960c26a291
commit 73b63d14ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
123 changed files with 444 additions and 365 deletions

View file

@ -4,13 +4,13 @@ import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetAmount;
import java.util.*;
@ -19,32 +19,21 @@ import java.util.*;
*/
public class DamageMultiEffect extends OneShotEffect {
protected DynamicValue amount;
private String sourceName = "{this}";
private final Set<MageObjectReference> damagedSet = new HashSet<>();
public DamageMultiEffect(int amount) {
this(StaticValue.get(amount));
}
public DamageMultiEffect(int amount, String whoDealDamageName) {
this(StaticValue.get(amount), whoDealDamageName);
}
public DamageMultiEffect(DynamicValue amount, String whoDealDamageName) {
this(amount);
this.sourceName = whoDealDamageName;
}
public DamageMultiEffect(DynamicValue amount) {
public DamageMultiEffect() {
super(Outcome.Damage);
this.amount = amount;
}
public DamageMultiEffect(String whoDealDamageName) {
super(Outcome.Damage);
this.sourceName = whoDealDamageName;
}
protected DamageMultiEffect(final DamageMultiEffect effect) {
super(effect);
this.damagedSet.addAll(effect.damagedSet);
this.amount = effect.amount;
this.sourceName = effect.sourceName;
}
@ -84,16 +73,16 @@ public class DamageMultiEffect extends OneShotEffect {
StringBuilder sb = new StringBuilder(sourceName);
sb.append(" deals ");
String amountString = amount.toString();
sb.append(amountString);
sb.append(" damage divided as you choose among ");
sb.append(amountString.equals("2") ? "one or two " : amountString.equals("3") ? "one, two, or three " : "any number of ");
String targetName = mode.getTargets().get(0).getTargetName();
if (!targetName.contains("target")) {
sb.append("target ");
Target target = mode.getTargets().get(0);
if (!(target instanceof TargetAmount)) {
throw new IllegalStateException("Must use TargetAmount");
}
sb.append(targetName);
TargetAmount targetAmount = (TargetAmount) target;
DynamicValue amount = targetAmount.getAmount();
sb.append(amount.toString());
sb.append(" damage divided as you choose among ");
sb.append(targetAmount.getDescription());
return sb.toString();
}

View file

@ -12,6 +12,7 @@ import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.Target;
import mage.target.TargetAmount;
import mage.util.CardUtil;
import java.util.UUID;
@ -22,34 +23,24 @@ import java.util.UUID;
public class DistributeCountersEffect extends OneShotEffect {
private final CounterType counterType;
private final DynamicValue amount;
private boolean removeAtEndOfTurn = false;
private final String targetDescription;
/**
* Distribute +1/+1 counters among targets
*/
public DistributeCountersEffect(int amount, String targetDescription) {
this(CounterType.P1P1, StaticValue.get(amount), targetDescription);
public DistributeCountersEffect() {
this(CounterType.P1P1);
}
public DistributeCountersEffect(CounterType counterType, int amount, String targetDescription) {
this(counterType, StaticValue.get(amount), targetDescription);
}
public DistributeCountersEffect(CounterType counterType, DynamicValue amount, String targetDescription) {
public DistributeCountersEffect(CounterType counterType) {
super(Outcome.BoostCreature);
this.counterType = counterType;
this.amount = amount;
this.targetDescription = targetDescription;
}
protected DistributeCountersEffect(final DistributeCountersEffect effect) {
super(effect);
this.counterType = effect.counterType;
this.amount = effect.amount;
this.removeAtEndOfTurn = effect.removeAtEndOfTurn;
this.targetDescription = effect.targetDescription;
}
@Override
@ -90,9 +81,16 @@ public class DistributeCountersEffect extends OneShotEffect {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
Target target = mode.getTargets().get(0);
if (!(target instanceof TargetAmount)) {
throw new IllegalStateException("Must use TargetAmount");
}
TargetAmount targetAmount = (TargetAmount) target;
DynamicValue amount = targetAmount.getAmount();
String name = counterType.getName();
String number = (amount instanceof StaticValue) ? CardUtil.numberToText(((StaticValue) amount).getValue()) : amount.toString();
String text = "distribute " + number + ' ' + name + " counters among " + targetDescription;
String text = "distribute " + number + ' ' + name + " counters among " + targetAmount.getDescription();
if (removeAtEndOfTurn) {
text += ". For each " + name + " counter you put on a creature this way, remove a "
+ name + " counter from that creature at the beginning of the next cleanup step.";

View file

@ -2,6 +2,7 @@ package mage.target;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
@ -18,8 +19,10 @@ public abstract class TargetAmount extends TargetImpl {
DynamicValue amount;
int remainingAmount;
protected TargetAmount(DynamicValue amount) {
protected TargetAmount(DynamicValue amount, int minNumberOfTargets, int maxNumberOfTargets) {
this.amount = amount;
setMinNumberOfTargets(minNumberOfTargets);
setMaxNumberOfTargets(maxNumberOfTargets);
}
protected TargetAmount(final TargetAmount target) {
@ -65,6 +68,10 @@ public abstract class TargetAmount extends TargetImpl {
amountWasSet = true;
}
public DynamicValue getAmount() {
return amount;
}
public int getAmountTotal(Game game, Ability source) {
return amount.calculate(game, source, null);
}
@ -176,4 +183,24 @@ public abstract class TargetAmount extends TargetImpl {
remainingAmount -= (amount - this.getTargetAmount(targetId));
this.setTargetAmount(targetId, amount, game);
}
@Override
protected boolean getUseAnyNumber() {
int min = getMinNumberOfTargets();
int max = getMaxNumberOfTargets();
if (min != 0) {
return false;
}
if (max == Integer.MAX_VALUE) {
return true;
}
// For a TargetAmount with a min of 0:
// A max that equals the amount, when the amount is a StaticValue,
// usually represents "any number of target __s", since you can't target more than the amount.
//
// 601.2d. If the spell requires the player to divide or distribute an effect
// (such as damage or counters) among one or more targets, the player announces the division.
// Each of these targets must receive at least one of whatever is being divided.
return amount instanceof StaticValue && max == ((StaticValue) amount).getValue();
}
}

View file

@ -109,17 +109,22 @@ public abstract class TargetImpl implements Target {
sb.append(" or more ");
} else if (!targetName.startsWith("X ") && (min != 1 || max != 1)) {
targetName = targetName.replace("another", "other"); //If non-singular, use "other" instead of "another"
if (min < max && max != Integer.MAX_VALUE) {
if (min == 1 && max == 2) {
sb.append("one or ");
} else if (min == 1 && max == 3) {
sb.append("one, two, or ");
} else {
sb.append("up to ");
if (getUseAnyNumber()) {
sb.append(("any number of "));
} else {
if (min < max && max != Integer.MAX_VALUE) {
if (min == 1 && max == 2) {
sb.append("one or ");
} else if (min == 1 && max == 3) {
sb.append("one, two, or ");
} else {
sb.append("up to ");
}
}
sb.append(CardUtil.numberToText(max));
sb.append(' ');
}
sb.append(CardUtil.numberToText(max));
sb.append(' ');
}
boolean addTargetWord = false;
if (!isNotTarget()) {
@ -127,7 +132,8 @@ public abstract class TargetImpl implements Target {
if (targetName.contains("target ")) {
addTargetWord = false;
} else if (targetName.endsWith("any target")
|| targetName.endsWith("any other target")) {
|| targetName.endsWith("any other target")
|| targetName.endsWith("targets")) {
addTargetWord = false;
}
// endsWith needs to be specific.
@ -144,6 +150,15 @@ public abstract class TargetImpl implements Target {
return sb.toString();
}
/**
* Used for generating text description. Needed so that subclasses may override.
*/
protected boolean getUseAnyNumber() {
int min = getMinNumberOfTargets();
int max = getMaxNumberOfTargets();
return min == 0 && max == Integer.MAX_VALUE;
}
@Override
public String getMessage(Game game) {
// UI choose message

View file

@ -14,25 +14,45 @@ public class TargetAnyTargetAmount extends TargetPermanentOrPlayerAmount {
private static final FilterPermanentOrPlayer defaultFilter
= new FilterAnyTarget("targets");
/**
* <b>IMPORTANT</b>: Use more specific constructor if {@code amount} is not always the same number!<br>
* {@code minNumberOfTargets} defaults to zero for {@code amount} > 3, otherwise to one, in line with typical templating.<br>
* {@code maxNumberOfTargets} defaults to {@code amount}.
*
* @see TargetAnyTargetAmount#TargetAnyTargetAmount(int, int, int)
*/
public TargetAnyTargetAmount(int amount) {
this(amount, 0);
this(amount, amount > 3 ? 0 : 1, amount);
}
public TargetAnyTargetAmount(int amount, int maxNumberOfTargets) {
// 107.1c If a rule or ability instructs a player to choose any number, that player may choose
// any positive number or zero, unless something (such as damage or counters) is being divided
// or distributed among any number of players and/or objects. In that case, a nonzero number
// of players and/or objects must be chosen if possible.
this(StaticValue.get(amount), maxNumberOfTargets);
this.minNumberOfTargets = 1;
/**
* @param amount Amount of stuff (e.g. damage) to distribute.
* @param minNumberOfTargets Minimum number of targets.
* @param maxNumberOfTargets Maximum number of targets. Should be set to {@code amount} if no lower max is needed.
*/
public TargetAnyTargetAmount(int amount, int minNumberOfTargets, int maxNumberOfTargets) {
this(StaticValue.get(amount), minNumberOfTargets, maxNumberOfTargets);
}
/**
* {@code minNumberOfTargets} defaults to zero.<br>
* {@code maxNumberOfTargets} defaults to Integer.MAX_VALUE.
*
* @see TargetAnyTargetAmount#TargetAnyTargetAmount(DynamicValue, int, int)
*/
public TargetAnyTargetAmount(DynamicValue amount) {
this(amount, 0);
this(amount, 0, Integer.MAX_VALUE);
}
public TargetAnyTargetAmount(DynamicValue amount, int maxNumberOfTargets) {
super(amount, maxNumberOfTargets);
/**
* @param amount Amount of stuff (e.g. damage) to distribute.
* @param minNumberOfTargets Minimum number of targets.
* @param maxNumberOfTargets Maximum number of targets. Since {@code amount} is dynamic,
* should be set to Integer.MAX_VALUE if no lower max is needed.
* (Game will always prevent choosing more than {@code amount} targets.)
*/
public TargetAnyTargetAmount(DynamicValue amount, int minNumberOfTargets, int maxNumberOfTargets) {
super(amount, minNumberOfTargets, maxNumberOfTargets);
this.zone = Zone.ALL;
this.filter = defaultFilter;
this.targetName = filter.getMessage();

View file

@ -1,6 +1,6 @@
package mage.target.common;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledCreatureOrPlaneswalkerPermanent;
import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent;
/**
@ -11,12 +11,62 @@ public class TargetCreatureOrPlaneswalkerAmount extends TargetPermanentAmount {
private static final FilterCreatureOrPlaneswalkerPermanent defaultFilter
= new FilterCreatureOrPlaneswalkerPermanent("target creatures and/or planeswalkers");
/**
* <b>IMPORTANT</b>: Use more specific constructor if {@code amount} is not always the same number!<br>
* {@code minNumberOfTargets} defaults to zero for {@code amount} > 3, otherwise to one, in line with typical templating.<br>
* {@code maxNumberOfTargets} defaults to {@code amount}.<br>
* {@code filter} defaults to all creature and planeswalker permanents. ("target creatures and/or planeswalkers")
*
* @see TargetCreatureOrPlaneswalkerAmount#TargetCreatureOrPlaneswalkerAmount(int, int, int, FilterCreatureOrPlaneswalkerPermanent)
*/
public TargetCreatureOrPlaneswalkerAmount(int amount) {
super(amount, defaultFilter);
this(amount, defaultFilter);
}
public TargetCreatureOrPlaneswalkerAmount(int amount, FilterPermanent filter) {
super(amount, filter);
/**
* <b>IMPORTANT</b>: Use more specific constructor if {@code amount} is not always the same number!<br>
* {@code minNumberOfTargets} defaults to zero for {@code amount} > 3, otherwise to one, in line with typical templating.<br>
* {@code maxNumberOfTargets} defaults to {@code amount}.
*
* @see TargetCreatureOrPlaneswalkerAmount#TargetCreatureOrPlaneswalkerAmount(int, int, int, FilterCreatureOrPlaneswalkerPermanent)
*/
public TargetCreatureOrPlaneswalkerAmount(int amount, FilterCreatureOrPlaneswalkerPermanent filter) {
this(amount, amount > 3 ? 0 : 1, amount, filter);
}
/**
* {@code filter} defaults to all creature and planeswalker permanents. ("target creatures and/or planeswalkers")
*
* @see TargetCreatureOrPlaneswalkerAmount#TargetCreatureOrPlaneswalkerAmount(int, int, int, FilterCreatureOrPlaneswalkerPermanent)
*/
public TargetCreatureOrPlaneswalkerAmount(int amount, int minNumberOfTargets, int maxNumberOfTargets) {
this(amount, minNumberOfTargets, maxNumberOfTargets, defaultFilter);
}
/**
* @param amount Amount of stuff (e.g. damage, counters) to distribute.
* @param minNumberOfTargets Minimum number of targets.
* @param maxNumberOfTargets Maximum number of targets. If no lower max is needed, set to {@code amount}.
* @param filter Filter for creatures and/or planeswalkers that something will be distributed amongst.
*/
public TargetCreatureOrPlaneswalkerAmount(int amount, int minNumberOfTargets, int maxNumberOfTargets, FilterCreatureOrPlaneswalkerPermanent filter) {
super(amount, minNumberOfTargets, maxNumberOfTargets, filter);
}
/**
* <b>IMPORTANT</b>: Use more specific constructor if {@code amount} is not always the same number!
*
* @see TargetCreatureOrPlaneswalkerAmount#TargetCreatureOrPlaneswalkerAmount(int, FilterCreatureOrPlaneswalkerPermanent)
*/
public TargetCreatureOrPlaneswalkerAmount(int amount, FilterControlledCreatureOrPlaneswalkerPermanent filter) {
this(amount, amount > 3 ? 0 : 1, amount, filter);
}
/**
* @see TargetCreatureOrPlaneswalkerAmount#TargetCreatureOrPlaneswalkerAmount(int, int, int, FilterCreatureOrPlaneswalkerPermanent)
*/
public TargetCreatureOrPlaneswalkerAmount(int amount, int minNumberOfTargets, int maxNumberOfTargets, FilterControlledCreatureOrPlaneswalkerPermanent filter) {
super(amount, minNumberOfTargets, maxNumberOfTargets, filter);
}
private TargetCreatureOrPlaneswalkerAmount(final TargetCreatureOrPlaneswalkerAmount target) {

View file

@ -1,7 +1,7 @@
package mage.target.common;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.common.FilterCreaturePermanent;
/**
@ -11,20 +11,88 @@ public class TargetCreaturePermanentAmount extends TargetPermanentAmount {
private static final FilterCreaturePermanent defaultFilter = new FilterCreaturePermanent("target creatures");
/**
* <b>IMPORTANT</b>: Use more specific constructor if {@code amount} is not always the same number!<br>
* {@code minNumberOfTargets} defaults to zero for {@code amount} > 3, otherwise to one, in line with typical templating.<br>
* {@code maxNumberOfTargets} defaults to {@code amount}.<br>
* {@code filter} defaults to all creature permanents. ("target creatures")
*
* @see TargetCreaturePermanentAmount#TargetCreaturePermanentAmount(int, int, int, FilterCreaturePermanent)
*/
public TargetCreaturePermanentAmount(int amount) {
super(amount, defaultFilter);
this(amount, defaultFilter);
}
/**
* <b>IMPORTANT</b>: Use more specific constructor if {@code amount} is not always the same number!<br>
* {@code minNumberOfTargets} defaults to zero for {@code amount} > 3, otherwise to one, in line with typical templating.<br>
* {@code maxNumberOfTargets} defaults to {@code amount}.
*
* @see TargetCreaturePermanentAmount#TargetCreaturePermanentAmount(int, int, int, FilterCreaturePermanent)
*/
public TargetCreaturePermanentAmount(int amount, FilterCreaturePermanent filter) {
this(amount, amount > 3 ? 0 : 1, amount, filter);
}
/**
* {@code filter} defaults to all creature permanents. ("target creatures")
*
* @see TargetCreaturePermanentAmount#TargetCreaturePermanentAmount(int, int, int, FilterCreaturePermanent)
*/
public TargetCreaturePermanentAmount(int amount, int minNumberOfTargets, int maxNumberOfTargets) {
this(amount, minNumberOfTargets, maxNumberOfTargets, defaultFilter);
}
/**
* @param amount Amount of stuff (e.g. damage, counters) to distribute.
* @param minNumberOfTargets Minimum number of targets.
* @param maxNumberOfTargets Maximum number of targets. If no lower max is needed, set to {@code amount}.
* @param filter Filter for creatures that something will be distributed amongst.
*/
public TargetCreaturePermanentAmount(int amount, int minNumberOfTargets, int maxNumberOfTargets, FilterCreaturePermanent filter) {
super(amount, minNumberOfTargets, maxNumberOfTargets, filter);
}
/**
* {@code filter} defaults to all creature permanents. ("target creatures")
*
* @see TargetCreaturePermanentAmount#TargetCreaturePermanentAmount(DynamicValue, FilterCreaturePermanent)
*/
public TargetCreaturePermanentAmount(DynamicValue amount) {
this(amount, defaultFilter);
}
public TargetCreaturePermanentAmount(int amount, FilterPermanent filter) {
super(amount, filter);
/**
* Minimum number of targets will be zero, and max will be set to Integer.MAX_VALUE.
*
* @param amount Amount of stuff (e.g. damage, counters) to distribute.
* @param filter Filter for creatures that something will be distributed amongst.
*/
public TargetCreaturePermanentAmount(DynamicValue amount, FilterCreaturePermanent filter) {
super(amount, 0, filter);
}
public TargetCreaturePermanentAmount(DynamicValue amount, FilterPermanent filter) {
super(amount, filter);
/**
* <b>IMPORTANT</b>: Use more specific constructor if {@code amount} is not always the same number!
*
* @see TargetCreaturePermanentAmount#TargetCreaturePermanentAmount(int, FilterCreaturePermanent)
*/
public TargetCreaturePermanentAmount(int amount, FilterControlledCreaturePermanent filter) {
this(amount, amount > 3 ? 0 : 1, amount, filter);
}
/**
* @see TargetCreaturePermanentAmount#TargetCreaturePermanentAmount(int, int, int, FilterCreaturePermanent)
*/
public TargetCreaturePermanentAmount(int amount, int minNumberOfTargets, int maxNumberOfTargets, FilterControlledCreaturePermanent filter) {
super(amount, minNumberOfTargets, maxNumberOfTargets, filter);
}
/**
* @see TargetCreaturePermanentAmount#TargetCreaturePermanentAmount(DynamicValue, FilterCreaturePermanent)
*/
public TargetCreaturePermanentAmount(DynamicValue amount, FilterControlledCreaturePermanent filter) {
super(amount, 0, filter);
}
private TargetCreaturePermanentAmount(final TargetCreaturePermanentAmount target) {

View file

@ -5,7 +5,6 @@ import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.constants.Zone;
import mage.filter.Filter;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
@ -23,16 +22,41 @@ public class TargetPermanentAmount extends TargetAmount {
protected final FilterPermanent filter;
public TargetPermanentAmount(int amount, FilterPermanent filter) {
// 107.1c If a rule or ability instructs a player to choose any number, that player may choose
// any positive number or zero, unless something (such as damage or counters) is being divided
// or distributed among any number of players and/or objects. In that case, a nonzero number
// of players and/or objects must be chosen if possible.
this(StaticValue.get(amount), filter);
/**
* {@code maxNumberOfTargets} defaults to {@code amount}.
*
* @see TargetPermanentAmount#TargetPermanentAmount(DynamicValue, int, int, FilterPermanent)
*/
public TargetPermanentAmount(int amount, int minNumberOfTargets, FilterPermanent filter) {
this(amount, minNumberOfTargets, amount, filter);
}
public TargetPermanentAmount(DynamicValue amount, FilterPermanent filter) {
super(amount);
/**
* {@code maxNumberOfTargets} defaults to Integer.MAX_VALUE.
*
* @see TargetPermanentAmount#TargetPermanentAmount(DynamicValue, int, int, FilterPermanent)
*/
public TargetPermanentAmount(DynamicValue amount, int minNumberOfTargets, FilterPermanent filter) {
this(amount, minNumberOfTargets, Integer.MAX_VALUE, filter);
}
/**
* @see TargetPermanentAmount#TargetPermanentAmount(DynamicValue, int, int, FilterPermanent)
*/
public TargetPermanentAmount(int amount, int minNumberOfTargets, int maxNumberOfTargets, FilterPermanent filter) {
this(StaticValue.get(amount), minNumberOfTargets, maxNumberOfTargets, filter);
}
/**
* @param amount Amount of stuff (e.g. damage, counters) to distribute.
* @param minNumberOfTargets Minimum number of targets.
* @param maxNumberOfTargets Maximum number of targets. If no lower max is needed:
* set to {@code amount} if amount is static; otherwise, set to Integer.MAX_VALUE.
* (Game will always prevent distributing among more than {@code amount} permanents.)
* @param filter Filter for permanents that something will be distributed amongst.
*/
public TargetPermanentAmount(DynamicValue amount, int minNumberOfTargets, int maxNumberOfTargets, FilterPermanent filter) {
super(amount, minNumberOfTargets, maxNumberOfTargets);
this.zone = Zone.ALL;
this.filter = filter;
this.targetName = filter.getMessage();
@ -49,7 +73,7 @@ public class TargetPermanentAmount extends TargetAmount {
}
@Override
public Filter getFilter() {
public FilterPermanent getFilter() {
return this.filter;
}

View file

@ -22,9 +22,8 @@ public abstract class TargetPermanentOrPlayerAmount extends TargetAmount {
protected FilterPermanentOrPlayer filter;
TargetPermanentOrPlayerAmount(DynamicValue amount, int maxNumberOfTargets) {
super(amount);
this.maxNumberOfTargets = maxNumberOfTargets;
TargetPermanentOrPlayerAmount(DynamicValue amount, int minNumberOfTargets, int maxNumberOfTargets) {
super(amount, minNumberOfTargets, maxNumberOfTargets);
}
TargetPermanentOrPlayerAmount(final TargetPermanentOrPlayerAmount target) {