forked from External/mage
Refactor implementation of spell copies for cards like Twinning Staff as well as refactor handling of target changing (WIP) (#7662)
* refactored createCopyOnStack to be void * added new interface for modifying copied spellsspells * update implementation of Fork to use new applier * reworked epic effect * add applier to spell copy code * updated implementation of Beamsplitter Mage * updated cards which copy for each possible target * added support for additional copies having targets changed * fixed/ignored failing tests * updated target changing to prevent unnecessary choosing * added test for Twinning Staff * updated implementation of spell copy applier * added new method for choosing order of copies on stack * fixed test failures * [TSR] various text fixes * fixed a test failure * [SLD] fixed Rick, Steadfast Leader only counting Human creatures * updated test framework to handle skips without affecting starting player choice * fixed another test failure * updated copy messaging for consistency * added copy messaging to stack abilities
This commit is contained in:
parent
b51915f6e8
commit
9c56a98dc9
47 changed files with 972 additions and 1092 deletions
|
|
@ -13,10 +13,14 @@ import mage.abilities.keyword.BestowAbility;
|
|||
import mage.abilities.keyword.MorphAbility;
|
||||
import mage.abilities.text.TextPart;
|
||||
import mage.cards.*;
|
||||
import mage.choices.Choice;
|
||||
import mage.choices.ChoiceImpl;
|
||||
import mage.constants.*;
|
||||
import mage.counters.Counter;
|
||||
import mage.counters.Counters;
|
||||
import mage.filter.FilterMana;
|
||||
import mage.filter.predicate.Predicate;
|
||||
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
|
||||
import mage.game.Game;
|
||||
import mage.game.GameState;
|
||||
import mage.game.MageObjectAttribute;
|
||||
|
|
@ -32,9 +36,11 @@ import mage.util.CardUtil;
|
|||
import mage.util.GameLog;
|
||||
import mage.util.ManaUtil;
|
||||
import mage.util.SubTypes;
|
||||
import mage.util.functions.SpellCopyApplier;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
|
|
@ -1057,27 +1063,128 @@ public class Spell extends StackObjImpl implements Card {
|
|||
}
|
||||
|
||||
@Override
|
||||
public StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) {
|
||||
return createCopyOnStack(game, source, newControllerId, chooseNewTargets, 1);
|
||||
public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) {
|
||||
createCopyOnStack(game, source, newControllerId, chooseNewTargets, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount) {
|
||||
Spell spellCopy = null;
|
||||
public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount) {
|
||||
createCopyOnStack(game, source, newControllerId, chooseNewTargets, amount, null);
|
||||
}
|
||||
|
||||
private static final class PredicateIterator implements Iterator<MageObjectReferencePredicate> {
|
||||
private final SpellCopyApplier applier;
|
||||
private final Player player;
|
||||
private final int amount;
|
||||
private final Game game;
|
||||
private Map<String, MageObjectReferencePredicate> predicateMap = null;
|
||||
private int anyCount = 0;
|
||||
private int setCount = 0;
|
||||
private Iterator<MageObjectReferencePredicate> iterator = null;
|
||||
private Choice choice = null;
|
||||
|
||||
private PredicateIterator(Game game, UUID newControllerId, int amount, SpellCopyApplier applier) {
|
||||
this.applier = applier;
|
||||
this.player = game.getPlayer(newControllerId);
|
||||
this.amount = amount;
|
||||
this.game = game;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void makeMap() {
|
||||
if (predicateMap != null) {
|
||||
return;
|
||||
}
|
||||
predicateMap = new HashMap<>();
|
||||
|
||||
for (int i = 0; i < amount; i++) {
|
||||
MageObjectReferencePredicate predicate = applier.getNextPredicate();
|
||||
if (predicate == null) {
|
||||
anyCount++;
|
||||
String message = "Any target";
|
||||
if (anyCount > 1) {
|
||||
message += " (" + anyCount + ")";
|
||||
}
|
||||
predicateMap.put(message, predicate);
|
||||
continue;
|
||||
}
|
||||
setCount++;
|
||||
predicateMap.put(predicate.getName(game), predicate);
|
||||
}
|
||||
if ((setCount == 1 && anyCount == 0) || setCount == 0) {
|
||||
iterator = predicateMap.values().stream().collect(Collectors.toList()).iterator();
|
||||
}
|
||||
}
|
||||
|
||||
private void makeChoice() {
|
||||
if (choice != null) {
|
||||
return;
|
||||
}
|
||||
choice = new ChoiceImpl(false);
|
||||
choice.setMessage("Choose the order of copies to go on the stack");
|
||||
choice.setSubMessage("Press cancel to put the rest in any order");
|
||||
choice.setChoices(new HashSet<>(predicateMap.keySet()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MageObjectReferencePredicate next() {
|
||||
if (player == null || applier == null) {
|
||||
return null;
|
||||
}
|
||||
makeMap();
|
||||
if (iterator != null) {
|
||||
return iterator.hasNext() ? iterator.next() : null;
|
||||
}
|
||||
makeChoice();
|
||||
if (choice.getChoices().size() < 2) {
|
||||
iterator = choice.getChoices().stream().map(predicateMap::get).iterator();
|
||||
return next();
|
||||
}
|
||||
choice.clearChoice();
|
||||
player.choose(Outcome.AIDontUseIt, choice, game);
|
||||
String chosen = choice.getChoice();
|
||||
if (chosen == null) {
|
||||
iterator = choice.getChoices().stream().map(predicateMap::get).iterator();
|
||||
return next();
|
||||
}
|
||||
choice.getChoices().remove(chosen);
|
||||
return predicateMap.get(chosen);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount, SpellCopyApplier applier) {
|
||||
GameEvent gameEvent = new CopyStackObjectEvent(source, this, newControllerId, amount);
|
||||
if (game.replaceEvent(gameEvent)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
Iterator<MageObjectReferencePredicate> predicates = new PredicateIterator(game, newControllerId, gameEvent.getAmount(), applier);
|
||||
for (int i = 0; i < gameEvent.getAmount(); i++) {
|
||||
spellCopy = this.copySpell(game, source, newControllerId);
|
||||
Spell spellCopy = this.copySpell(game, source, newControllerId);
|
||||
if (applier != null) {
|
||||
applier.modifySpell(spellCopy, game);
|
||||
}
|
||||
spellCopy.setZone(Zone.STACK, game); // required for targeting ex: Nivmagus Elemental
|
||||
game.getStack().push(spellCopy);
|
||||
if (chooseNewTargets) {
|
||||
Predicate predicate = predicates.next();
|
||||
if (predicate != null) {
|
||||
spellCopy.chooseNewTargets(game, newControllerId, true, false, predicate);
|
||||
} else if (chooseNewTargets || applier != null) { // if applier is non-null but predicate is null then it's extra
|
||||
spellCopy.chooseNewTargets(game, newControllerId);
|
||||
}
|
||||
game.fireEvent(new CopiedStackObjectEvent(this, spellCopy, newControllerId));
|
||||
}
|
||||
return spellCopy;
|
||||
Player player = game.getPlayer(newControllerId);
|
||||
if (player != null) {
|
||||
game.informPlayers(
|
||||
player.getName() + " created " + CardUtil.numberToText(gameEvent.getAmount(), "a")
|
||||
+ " cop" + (gameEvent.getAmount() == 1 ? "y" : "ies") + " of " + getIdName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package mage.game.stack;
|
||||
|
||||
import mage.*;
|
||||
import mage.MageIdentifier;
|
||||
import mage.MageInt;
|
||||
import mage.MageObject;
|
||||
import mage.ObjectColor;
|
||||
import mage.abilities.*;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.CostAdjuster;
|
||||
|
|
@ -27,8 +30,10 @@ import mage.players.Player;
|
|||
import mage.target.Target;
|
||||
import mage.target.Targets;
|
||||
import mage.target.targetadjustment.TargetAdjuster;
|
||||
import mage.util.CardUtil;
|
||||
import mage.util.GameLog;
|
||||
import mage.util.SubTypes;
|
||||
import mage.util.functions.SpellCopyApplier;
|
||||
import mage.watchers.Watcher;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -593,15 +598,20 @@ public class StackAbility extends StackObjImpl implements Ability {
|
|||
}
|
||||
|
||||
@Override
|
||||
public StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) {
|
||||
return createCopyOnStack(game, source, newControllerId, chooseNewTargets, 1);
|
||||
public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) {
|
||||
createCopyOnStack(game, source, newControllerId, chooseNewTargets, 1);
|
||||
}
|
||||
|
||||
public StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount) {
|
||||
@Override
|
||||
public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount) {
|
||||
createCopyOnStack(game, source, newControllerId, chooseNewTargets, amount, null);
|
||||
}
|
||||
|
||||
public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount, SpellCopyApplier applier) {
|
||||
StackAbility newStackAbility = null;
|
||||
GameEvent gameEvent = new CopyStackObjectEvent(source, this, newControllerId, amount);
|
||||
if (game.replaceEvent(gameEvent)) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < gameEvent.getAmount(); i++) {
|
||||
Ability newAbility = this.copy();
|
||||
|
|
@ -618,7 +628,13 @@ public class StackAbility extends StackObjImpl implements Ability {
|
|||
}
|
||||
game.fireEvent(new CopiedStackObjectEvent(this, newStackAbility, newControllerId));
|
||||
}
|
||||
return newStackAbility;
|
||||
Player player = game.getPlayer(newControllerId);
|
||||
if (player != null) {
|
||||
game.informPlayers(
|
||||
player.getName() + " created " + CardUtil.numberToText(gameEvent.getAmount(), "a")
|
||||
+ " cop" + (gameEvent.getAmount() == 1 ? "y" : "ies") + " of " + getIdName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import mage.abilities.Ability;
|
|||
import mage.abilities.Mode;
|
||||
import mage.abilities.dynamicvalue.common.StaticValue;
|
||||
import mage.constants.Outcome;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.filter.predicate.Predicate;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
import mage.target.Target;
|
||||
import mage.target.TargetAmount;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -94,12 +95,12 @@ public abstract class StackObjImpl implements StackObject {
|
|||
* targetId
|
||||
* @param onlyOneTarget - 114.6b one target must be changed to another
|
||||
* target
|
||||
* @param filterNewTarget restriction for the new target, if null nothing is
|
||||
* @param extraPredicate restriction for the new target, if null nothing is
|
||||
* cheched
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean chooseNewTargets(Game game, UUID targetControllerId, boolean forceChange, boolean onlyOneTarget, FilterPermanent filterNewTarget) {
|
||||
public boolean chooseNewTargets(Game game, UUID targetControllerId, boolean forceChange, boolean onlyOneTarget, Predicate extraPredicate) {
|
||||
Player targetController = game.getPlayer(targetControllerId);
|
||||
if (targetController != null) {
|
||||
StringBuilder oldTargetDescription = new StringBuilder();
|
||||
|
|
@ -118,7 +119,7 @@ public abstract class StackObjImpl implements StackObject {
|
|||
ability.getModes().setActiveMode(mode);
|
||||
oldTargetDescription.append(ability.getTargetDescription(mode.getTargets(), game));
|
||||
for (Target target : mode.getTargets()) {
|
||||
Target newTarget = chooseNewTarget(targetController, ability, mode, target, forceChange, filterNewTarget, game);
|
||||
Target newTarget = chooseNewTarget(targetController, ability, mode, target, forceChange, extraPredicate, game);
|
||||
// clear the old target and copy all targets from new target
|
||||
target.clearChosen();
|
||||
for (UUID targetId : newTarget.getTargets()) {
|
||||
|
|
@ -149,8 +150,13 @@ public abstract class StackObjImpl implements StackObject {
|
|||
* @param game
|
||||
* @return
|
||||
*/
|
||||
private Target chooseNewTarget(Player targetController, Ability ability, Mode mode, Target target, boolean forceChange, FilterPermanent filterNewTarget, Game game) {
|
||||
private Target chooseNewTarget(Player targetController, Ability ability, Mode mode, Target target, boolean forceChange, Predicate predicate, Game game) {
|
||||
Target newTarget = target.copy();
|
||||
if (predicate != null) {
|
||||
newTarget.getFilter().add(predicate);
|
||||
// If adding a predicate, there will only be one choice and therefore target can be automatic
|
||||
newTarget.setRandom(true);
|
||||
}
|
||||
newTarget.setEventReporting(false);
|
||||
if (!targetController.getId().equals(getControllerId())) {
|
||||
newTarget.setTargetController(targetController.getId()); // target controller for the change is different from spell controller
|
||||
|
|
@ -181,15 +187,6 @@ public abstract class StackObjImpl implements StackObject {
|
|||
|
||||
newTarget.chooseTarget(outcome, getControllerId(), ability, game);
|
||||
|
||||
// check target restriction TODO: add multiple target checks
|
||||
if (newTarget.getFirstTarget() != null && filterNewTarget != null) {
|
||||
Permanent newTargetPermanent = game.getPermanent(newTarget.getFirstTarget());
|
||||
if (newTargetPermanent == null || !filterNewTarget.match(newTargetPermanent, game)) {
|
||||
game.informPlayer(targetController, "Target does not fullfil the target requirements (" + filterNewTarget.getMessage() + ')');
|
||||
newTarget.clearChosen();
|
||||
}
|
||||
}
|
||||
|
||||
// workaround to stop infinite AI choose (remove after chooseTarget can be called with extra filter to disable some ids)
|
||||
if (iteration > 10) {
|
||||
break;
|
||||
|
|
@ -200,6 +197,11 @@ public abstract class StackObjImpl implements StackObject {
|
|||
} else {
|
||||
// build a target definition with exactly one possible target to select that replaces old target
|
||||
Target tempTarget = target.copy();
|
||||
if (predicate != null) {
|
||||
tempTarget.getFilter().add(predicate);
|
||||
// If adding a predicate, there will only be one choice and therefore target can be automatic
|
||||
tempTarget.setRandom(true);
|
||||
}
|
||||
tempTarget.setEventReporting(false);
|
||||
if (target instanceof TargetAmount) {
|
||||
((TargetAmount) tempTarget).setAmountDefinition(StaticValue.get(target.getTargetAmount(targetId)));
|
||||
|
|
@ -242,12 +244,6 @@ public abstract class StackObjImpl implements StackObject {
|
|||
// keep the old
|
||||
newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, true);
|
||||
}
|
||||
} else if (newTarget.getFirstTarget() != null && filterNewTarget != null) {
|
||||
Permanent newTargetPermanent = game.getPermanent(newTarget.getFirstTarget());
|
||||
if (newTargetPermanent == null || !filterNewTarget.match(newTargetPermanent, game)) {
|
||||
game.informPlayer(targetController, "This target does not fullfil the target requirements (" + filterNewTarget.getMessage() + ')');
|
||||
again = true;
|
||||
}
|
||||
} else {
|
||||
// valid target was selected, add it to the new target definition
|
||||
newTarget.addTarget(tempTarget.getFirstTarget(), target.getTargetAmount(targetId), ability, game, true);
|
||||
|
|
@ -263,6 +259,30 @@ public abstract class StackObjImpl implements StackObject {
|
|||
return newTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTarget(Game game, UUID targetId) {
|
||||
Abilities<Ability> objectAbilities = new AbilitiesImpl<>();
|
||||
if (this instanceof Spell) {
|
||||
objectAbilities.addAll(((Spell) this).getSpellAbilities());
|
||||
} else {
|
||||
objectAbilities.add(getStackAbility());
|
||||
}
|
||||
for (Ability ability : objectAbilities) {
|
||||
if (ability.getModes()
|
||||
.getSelectedModes()
|
||||
.stream()
|
||||
.map(ability.getModes()::get)
|
||||
.filter(Objects::nonNull)
|
||||
.map(Mode::getTargets)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(t -> !t.isNotTarget())
|
||||
.anyMatch(t -> t.canTarget(ability.getControllerId(), targetId, ability, game))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getNamesOftargets(UUID targetId, Game game) {
|
||||
MageObject object = game.getObject(targetId);
|
||||
String name = null;
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import mage.MageObject;
|
|||
import mage.abilities.Ability;
|
||||
import mage.constants.Zone;
|
||||
import mage.constants.ZoneDetail;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.filter.predicate.Predicate;
|
||||
import mage.game.Controllable;
|
||||
import mage.game.Game;
|
||||
import mage.util.functions.SpellCopyApplier;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -17,7 +18,6 @@ public interface StackObject extends MageObject, Controllable {
|
|||
UUID getSourceId();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param source null for fizzled events (sourceId will be null)
|
||||
* @param game
|
||||
*/
|
||||
|
|
@ -27,11 +27,15 @@ public interface StackObject extends MageObject, Controllable {
|
|||
|
||||
Ability getStackAbility();
|
||||
|
||||
boolean chooseNewTargets(Game game, UUID playerId, boolean forceChange, boolean onlyOneTarget, FilterPermanent filterNewTarget);
|
||||
boolean chooseNewTargets(Game game, UUID playerId, boolean forceChange, boolean onlyOneTarget, Predicate extraPredicate);
|
||||
|
||||
StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets);
|
||||
boolean canTarget(Game game, UUID targetId);
|
||||
|
||||
StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount);
|
||||
void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets);
|
||||
|
||||
void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount);
|
||||
|
||||
void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount, SpellCopyApplier applier);
|
||||
|
||||
boolean isTargetChanged();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue