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:
Evan Kranzler 2021-03-12 12:47:49 -05:00 committed by GitHub
parent b51915f6e8
commit 9c56a98dc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 972 additions and 1092 deletions

View file

@ -1,7 +1,10 @@
package mage.cards.b; package mage.cards.b;
import mage.MageInt; import mage.MageInt;
import mage.MageObject;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.AbilityImpl;
import mage.abilities.Mode; import mage.abilities.Mode;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
@ -12,20 +15,21 @@ import mage.constants.Outcome;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.FilterPermanent; import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledPermanent; import mage.filter.StaticFilters;
import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.CopiedStackObjectEvent;
import mage.game.events.CopyStackObjectEvent;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetPermanent; import mage.target.TargetPermanent;
import mage.target.targetpointer.FixedTarget; import mage.util.functions.SpellCopyApplier;
import java.util.UUID; import java.util.*;
/** /**
* @author TheElk801 * @author TheElk801
@ -56,11 +60,11 @@ public final class BeamsplitterMage extends CardImpl {
class BeamsplitterMageTriggeredAbility extends TriggeredAbilityImpl { class BeamsplitterMageTriggeredAbility extends TriggeredAbilityImpl {
public BeamsplitterMageTriggeredAbility() { BeamsplitterMageTriggeredAbility() {
super(Zone.BATTLEFIELD, new BeamsplitterMageEffect(), false); super(Zone.BATTLEFIELD, new BeamsplitterMageEffect(), false);
} }
public BeamsplitterMageTriggeredAbility(final BeamsplitterMageTriggeredAbility ability) { private BeamsplitterMageTriggeredAbility(final BeamsplitterMageTriggeredAbility ability) {
super(ability); super(ability);
} }
@ -76,46 +80,60 @@ class BeamsplitterMageTriggeredAbility extends TriggeredAbilityImpl {
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
if (event.getPlayerId().equals(this.getControllerId())) { if (!isControlledBy(event.getPlayerId())) {
Spell spell = game.getStack().getSpell(event.getTargetId()); return false;
if (!isControlledInstantOrSorcery(spell)) {
return false;
}
boolean targetsSource = false;
for (Ability ability : spell.getSpellAbilities()) {
for (UUID modeId : ability.getModes().getSelectedModes()) {
Mode mode = ability.getModes().get(modeId);
for (Target target : mode.getTargets()) {
if (!target.isNotTarget()) {
for (UUID targetId : target.getTargets()) {
if (targetId.equals(getSourceId())) {
targetsSource = true;
} else {
return false;
}
}
}
}
}
}
if (targetsSource) {
this.getEffects().get(0).setTargetPointer(new FixedTarget(spell.getId()));
return true;
}
} }
return false; Spell spell = game.getSpellOrLKIStack(event.getTargetId());
if (spell == null || !spell.isInstantOrSorcery()) {
return false;
}
boolean targetsSource = false;
if (spell.getSpellAbilities()
.stream()
.map(AbilityImpl::getModes)
.flatMap(m -> m.getSelectedModes().stream().map(m::get))
.filter(Objects::nonNull)
.map(Mode::getTargets)
.flatMap(Collection::stream)
.filter(t -> !t.isNotTarget())
.map(Target::getTargets)
.flatMap(Collection::stream)
.anyMatch(uuid -> !getSourceId().equals(uuid) && uuid != null)) {
return false;
}
this.getEffects().setValue("spellCast", spell);
return true;
} }
private boolean isControlledInstantOrSorcery(Spell spell) { @Override
return spell != null public boolean checkInterveningIfClause(Game game) {
&& (spell.isControlledBy(this.getControllerId())) Spell spell = (Spell) getEffects().get(0).getValue("spellCast");
&& (spell.isInstant() || spell.isSorcery()); if (spell == null) {
return false;
}
return game.getBattlefield().getActivePermanents(
StaticFilters.FILTER_CONTROLLED_CREATURE,
getControllerId(), getSourceId(), game
).stream()
.filter(Objects::nonNull)
.filter(MageObject::isCreature)
.filter(p -> checkNotSource(p, game))
.anyMatch(p -> spell.canTarget(game, p.getId()));
}
private boolean checkNotSource(Permanent permanent, Game game) {
// workaround for zcc not being set before first intervening if check
if (this.getSourceObjectZoneChangeCounter() == 0) {
return !permanent.getId().equals(this.getSourceId());
}
return !permanent.getId().equals(this.getSourceId())
|| permanent.getZoneChangeCounter(game) != this.getSourceObjectZoneChangeCounter();
} }
@Override @Override
public String getRule() { public String getRule() {
return "Whenever you cast an instant or sorcery spell that targets " return "Whenever you cast an instant or sorcery spell that targets "
+ "only {this}, if you control one or more creatures " + "only {this}, if you control one or more other creatures "
+ "that spell could target, choose one of those creatures. " + "that spell could target, choose one of those creatures. "
+ "Copy that spell. The copy targets the chosen creature."; + "Copy that spell. The copy targets the chosen creature.";
} }
@ -123,11 +141,11 @@ class BeamsplitterMageTriggeredAbility extends TriggeredAbilityImpl {
class BeamsplitterMageEffect extends OneShotEffect { class BeamsplitterMageEffect extends OneShotEffect {
public BeamsplitterMageEffect() { BeamsplitterMageEffect() {
super(Outcome.Detriment); super(Outcome.Detriment);
} }
public BeamsplitterMageEffect(final BeamsplitterMageEffect effect) { private BeamsplitterMageEffect(final BeamsplitterMageEffect effect) {
super(effect); super(effect);
} }
@ -138,99 +156,62 @@ class BeamsplitterMageEffect extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Spell spell = game.getSpellOrLKIStack(this.getTargetPointer().getFirst(game, source)); Player player = game.getPlayer(source.getControllerId());
Player controller = game.getPlayer(source.getControllerId()); Spell spell = (Spell) getValue("spellCast");
if (spell != null && controller != null) { if (player == null || spell == null) {
// search the target that targets source return false;
Target usedTarget = null;
setUsedTarget:
for (Ability ability : spell.getSpellAbilities()) {
for (UUID modeId : ability.getModes().getSelectedModes()) {
Mode mode = ability.getModes().get(modeId);
for (Target target : mode.getTargets()) {
if (!target.isNotTarget() && target.getFirstTarget().equals(source.getSourceId())) {
usedTarget = target.copy();
usedTarget.clearChosen();
break setUsedTarget;
}
}
}
}
if (usedTarget == null) {
return false;
}
FilterPermanent filter = new BeamsplitterMageFilter(usedTarget, source.getSourceId(), source.getControllerId());
Target target1 = new TargetPermanent(filter);
target1.setNotTarget(true);
if (controller.choose(outcome, target1, source.getSourceId(), game)) {
Permanent creature = game.getPermanent(target1.getFirstTarget());
if (creature == null) {
return false;
}
// TODO: add support if multiple copies? See Twinning Staff
GameEvent gameEvent = new CopyStackObjectEvent(source, spell, source.getControllerId(), 1);
if (game.replaceEvent(gameEvent)) {
return false;
}
Spell copy = spell.copySpell(game, source, source.getControllerId());
copy.setZone(Zone.STACK, game);
game.getStack().push(copy);
setTarget:
for (UUID modeId : copy.getSpellAbility().getModes().getSelectedModes()) {
Mode mode = copy.getSpellAbility().getModes().get(modeId);
for (Target target : mode.getTargets()) {
if (target.getClass().equals(usedTarget.getClass())) {
target.clearChosen(); // For targets with Max > 1 we need to clear before the text is comapred
if (target.getMessage().equals(usedTarget.getMessage())) {
target.addTarget(creature.getId(), copy.getSpellAbility(), game, false);
break setTarget;
}
}
}
}
game.fireEvent(new CopiedStackObjectEvent(spell, copy, source.getControllerId()));
String activateMessage = copy.getActivatedMessage(game);
if (activateMessage.startsWith(" casts ")) {
activateMessage = activateMessage.substring(6);
}
if (!game.isSimulation()) {
game.informPlayers(controller.getLogName() + activateMessage);
}
}
return true;
} }
return false; FilterPermanent filter = new FilterControlledCreaturePermanent("a creature you control that can be targeted by " + spell.getName());
filter.add(AnotherPredicate.instance);
filter.add(new BeamsplitterMagePredicate(spell));
TargetPermanent target = new TargetPermanent(filter);
target.setNotTarget(true);
player.choose(outcome, target, source.getSourceId(), game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
if (permanent == null) {
return false;
}
spell.createCopyOnStack(
game, source, player.getId(), false,
1, new BeamsplitterMageApplier(permanent, game)
);
return true;
} }
} }
class BeamsplitterMageFilter extends FilterControlledPermanent { class BeamsplitterMagePredicate implements Predicate<Permanent> {
private final Target target; private final Spell spell;
private final UUID notId;
public BeamsplitterMageFilter(Target target, UUID notId, UUID controllerId) { BeamsplitterMagePredicate(Spell spell) {
super("creature this spell could target"); this.spell = spell;
this.target = target;
this.notId = notId;
this.add(new ControllerIdPredicate(controllerId));
}
public BeamsplitterMageFilter(final BeamsplitterMageFilter filter) {
super(filter);
this.target = filter.target;
this.notId = filter.notId;
} }
@Override @Override
public BeamsplitterMageFilter copy() { public boolean apply(Permanent input, Game game) {
return new BeamsplitterMageFilter(this); return spell != null && spell.canTarget(game, input.getId());
} }
}
@Override
public boolean match(Permanent permanent, UUID sourceId, UUID playerId, Game game) { class BeamsplitterMageApplier implements SpellCopyApplier {
return super.match(permanent, game)
&& permanent.isCreature() private final Iterator<MageObjectReferencePredicate> predicate;
&& !permanent.getId().equals(notId)
&& target.canTarget(permanent.getId(), game); BeamsplitterMageApplier(Permanent permanent, Game game) {
this.predicate = Arrays.asList(new MageObjectReferencePredicate(
new MageObjectReference(permanent, game)
)).iterator();
}
@Override
public void modifySpell(Spell spell, Game game) {
}
@Override
public MageObjectReferencePredicate getNextPredicate() {
if (predicate.hasNext()) {
return predicate.next();
}
return null;
} }
} }

View file

@ -79,7 +79,6 @@ class ChainLightningEffect extends OneShotEffect {
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null) { if (spell != null) {
spell.createCopyOnStack(game, source, affectedPlayer.getId(), true); spell.createCopyOnStack(game, source, affectedPlayer.getId(), true);
game.informPlayers(affectedPlayer.getLogName() + " copies " + spell.getName() + '.');
} }
} }
} }

View file

@ -81,7 +81,6 @@ class ChainOfAcidEffect extends OneShotEffect {
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null) { if (spell != null) {
spell.createCopyOnStack(game, source, affectedPlayer.getId(), true); spell.createCopyOnStack(game, source, affectedPlayer.getId(), true);
game.informPlayers(affectedPlayer.getLogName() + " copies " + spell.getName() + '.');
} }
} }
return true; return true;

View file

@ -79,7 +79,6 @@ class ChainOfPlasmaEffect extends OneShotEffect {
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null) { if (spell != null) {
spell.createCopyOnStack(game, source, affectedPlayer.getId(), true); spell.createCopyOnStack(game, source, affectedPlayer.getId(), true);
game.informPlayers(affectedPlayer.getLogName() + " copies " + spell.getName() + '.');
} }
} }
} }

View file

@ -1,7 +1,6 @@
package mage.cards.c; package mage.cards.c;
import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.Mode;
@ -17,13 +16,13 @@ import mage.filter.common.FilterControlledLandPermanent;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.target.common.TargetControlledPermanent; import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import java.util.UUID;
/** /**
*
* @author TheElk801 * @author TheElk801
*/ */
public final class ChainOfSilence extends CardImpl { public final class ChainOfSilence extends CardImpl {
@ -46,6 +45,7 @@ public final class ChainOfSilence extends CardImpl {
return new ChainOfSilence(this); return new ChainOfSilence(this);
} }
} }
class ChainOfSilenceEffect extends OneShotEffect { class ChainOfSilenceEffect extends OneShotEffect {
public ChainOfSilenceEffect() { public ChainOfSilenceEffect() {
@ -80,14 +80,7 @@ class ChainOfSilenceEffect extends OneShotEffect {
if (player.chooseUse(outcome, "Copy the spell?", source, game)) { if (player.chooseUse(outcome, "Copy the spell?", source, game)) {
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null) { if (spell != null) {
StackObject newStackObject = spell.createCopyOnStack(game, source, player.getId(), true); spell.createCopyOnStack(game, source, player.getId(), true);
if (newStackObject instanceof Spell) {
String activateMessage = ((Spell) newStackObject).getActivatedMessage(game);
if (activateMessage.startsWith(" casts ")) {
activateMessage = activateMessage.substring(6);
}
game.informPlayers(player.getLogName() + ' ' + activateMessage);
}
} }
} }
} }

View file

@ -1,7 +1,6 @@
package mage.cards.c; package mage.cards.c;
import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.Mode;
@ -15,19 +14,19 @@ import mage.filter.common.FilterControlledLandPermanent;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.target.common.TargetControlledPermanent; import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetNonlandPermanent; import mage.target.common.TargetNonlandPermanent;
import java.util.UUID;
/** /**
*
* @author Plopman * @author Plopman
*/ */
public final class ChainOfVapor extends CardImpl { public final class ChainOfVapor extends CardImpl {
public ChainOfVapor(UUID ownerId, CardSetInfo setInfo) { public ChainOfVapor(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{U}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}");
// Return target nonland permanent to its owner's hand. Then that permanent's controller may sacrifice a land. If the player does, they may copy this spell and may choose a new target for that copy. // Return target nonland permanent to its owner's hand. Then that permanent's controller may sacrifice a land. If the player does, they may copy this spell and may choose a new target for that copy.
this.getSpellAbility().addEffect(new ChainOfVaporEffect()); this.getSpellAbility().addEffect(new ChainOfVaporEffect());
@ -77,14 +76,7 @@ class ChainOfVaporEffect extends OneShotEffect {
if (player.chooseUse(outcome, "Copy the spell?", source, game)) { if (player.chooseUse(outcome, "Copy the spell?", source, game)) {
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null) { if (spell != null) {
StackObject newStackObject = spell.createCopyOnStack(game, source, player.getId(), true); spell.createCopyOnStack(game, source, player.getId(), true);
if (newStackObject instanceof Spell) {
String activateMessage = ((Spell) newStackObject).getActivatedMessage(game);
if (activateMessage.startsWith(" casts ")) {
activateMessage = activateMessage.substring(6);
}
game.informPlayers(player.getLogName() + ' ' + activateMessage);
}
} }
} }
} }

View file

@ -1,7 +1,6 @@
package mage.cards.c; package mage.cards.c;
import java.util.UUID;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.Mode;
@ -17,13 +16,13 @@ import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/** /**
*
* @author TheElk801 * @author TheElk801
*/ */
public final class ChainStasis extends CardImpl { public final class ChainStasis extends CardImpl {
@ -74,7 +73,7 @@ class ChainStasisEffect extends OneShotEffect {
effect.setTargetPointer(new FixedTarget(source.getFirstTarget())); effect.setTargetPointer(new FixedTarget(source.getFirstTarget()));
effect.apply(game, source); effect.apply(game, source);
Player player = game.getPlayer(permanent.getControllerId()); Player player = game.getPlayer(permanent.getControllerId());
if(player == null){ if (player == null) {
return false; return false;
} }
Cost cost = new ManaCostsImpl("{2}{U}"); Cost cost = new ManaCostsImpl("{2}{U}");
@ -82,14 +81,7 @@ class ChainStasisEffect extends OneShotEffect {
if (player.chooseUse(outcome, "Copy the spell?", source, game)) { if (player.chooseUse(outcome, "Copy the spell?", source, game)) {
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null) { if (spell != null) {
StackObject newStackObject = spell.createCopyOnStack(game, source, player.getId(), true); spell.createCopyOnStack(game, source, player.getId(), true);
if (newStackObject instanceof Spell) {
String activateMessage = ((Spell) newStackObject).getActivatedMessage(game);
if (activateMessage.startsWith(" casts ")) {
activateMessage = activateMessage.substring(6);
}
game.informPlayers(player.getLogName() + ' ' + activateMessage);
}
} }
} }
} }

View file

@ -103,7 +103,6 @@ class ChandrasRegulatorEffect extends OneShotEffect {
return false; return false;
} }
ability.createCopyOnStack(game, source, source.getControllerId(), true); ability.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getIdName() + ": " + controller.getLogName() + " copied activated ability");
return true; return true;
} }
} }

View file

@ -154,7 +154,6 @@ class FinaleOfPromiseEffect extends OneShotEffect {
Spell spell = game.getStack().getSpell(card.getId()); Spell spell = game.getStack().getSpell(card.getId());
if (spell != null) { if (spell != null) {
spell.createCopyOnStack(game, source, controller.getId(), true, 2); spell.createCopyOnStack(game, source, controller.getId(), true, 2);
game.informPlayers(controller.getLogName() + " copies " + spell.getName() + " twice.");
} }
} }
} }

View file

@ -1,37 +1,32 @@
package mage.cards.f; package mage.cards.f;
import mage.ObjectColor;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.CopiedStackObjectEvent;
import mage.game.events.CopyStackObjectEvent;
import mage.game.events.GameEvent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player;
import mage.target.TargetSpell; import mage.target.TargetSpell;
import mage.util.functions.SpellCopyApplier;
import java.util.UUID; import java.util.UUID;
/** /**
* @author jeffwadsworth * @author TheElk801
*/ */
public final class Fork extends CardImpl { public final class Fork extends CardImpl {
public Fork(UUID ownerId, CardSetInfo setInfo) { public Fork(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}{R}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{R}{R}");
// Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy. // Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy.
this.getSpellAbility().addEffect(new ForkEffect()); this.getSpellAbility().addEffect(new ForkEffect());
this.getSpellAbility().addTarget(new TargetSpell(StaticFilters.FILTER_SPELL_INSTANT_OR_SORCERY)); this.getSpellAbility().addTarget(new TargetSpell(StaticFilters.FILTER_SPELL_INSTANT_OR_SORCERY));
} }
private Fork(final Fork card) { private Fork(final Fork card) {
@ -46,45 +41,45 @@ public final class Fork extends CardImpl {
class ForkEffect extends OneShotEffect { class ForkEffect extends OneShotEffect {
public ForkEffect() { ForkEffect() {
super(Outcome.Copy); super(Outcome.Copy);
staticText = "Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy"; staticText = "copy target instant or sorcery spell, except that the copy is red. " +
"You may choose new targets for the copy";
} }
public ForkEffect(final ForkEffect effect) { private ForkEffect(final ForkEffect effect) {
super(effect); super(effect);
} }
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Spell spell = game.getSpell(source.getFirstTarget());
Spell spell = game.getStack().getSpell(targetPointer.getFirst(game, source)); if (spell == null) {
if (spell != null && controller != null) { return false;
// TODO: add support if multiple copies? See Twinning Staff
GameEvent gameEvent = new CopyStackObjectEvent(source, spell, source.getControllerId(), 1);
if (game.replaceEvent(gameEvent)) {
return false;
}
Spell copy = spell.copySpell(game, source, source.getControllerId());
copy.getColor(game).setRed(true);
copy.setZone(Zone.STACK, game);
game.getStack().push(copy);
copy.chooseNewTargets(game, controller.getId());
game.fireEvent(new CopiedStackObjectEvent(spell, copy, source.getControllerId()));
return true;
} }
return false; spell.createCopyOnStack(
game, source, source.getControllerId(),
true, 1, ForkApplier.instance
);
return true;
} }
@Override @Override
public ForkEffect copy() { public ForkEffect copy() {
return new ForkEffect(this); return new ForkEffect(this);
} }
}
enum ForkApplier implements SpellCopyApplier {
instance;
@Override @Override
public String getText(Mode mode) { public void modifySpell(Spell spell, Game game) {
StringBuilder sb = new StringBuilder(); spell.getColor(game).setColor(ObjectColor.RED);
sb.append("Copy target ").append(mode.getTargets().get(0).getTargetName()).append(", except that the copy is red. You may choose new targets for the copy"); }
return sb.toString();
@Override
public MageObjectReferencePredicate getNextPredicate() {
return null;
} }
} }

View file

@ -111,7 +111,6 @@ class CopyActivatedAbilityEffect extends OneShotEffect {
Permanent sourcePermanent = game.getPermanent(source.getSourceId()); Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (ability != null && controller != null && sourcePermanent != null) { if (ability != null && controller != null && sourcePermanent != null) {
ability.createCopyOnStack(game, source, source.getControllerId(), true); ability.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getName() + ": " + controller.getLogName() + " copied activated ability");
return true; return true;
} }
return false; return false;

View file

@ -12,7 +12,6 @@ import mage.filter.FilterSpell;
import mage.filter.predicate.Predicates; import mage.filter.predicate.Predicates;
import mage.game.Game; import mage.game.Game;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetSpell; import mage.target.TargetSpell;
@ -81,10 +80,7 @@ class IncreasingVengeanceEffect extends OneShotEffect {
if (sourceSpell != null && sourceSpell.getFromZone() == Zone.GRAVEYARD) { if (sourceSpell != null && sourceSpell.getFromZone() == Zone.GRAVEYARD) {
copies++; copies++;
} }
StackObject stackObjectCopy = spell.createCopyOnStack(game, source, source.getControllerId(), true, copies); spell.createCopyOnStack(game, source, source.getControllerId(), true, copies);
if (stackObjectCopy instanceof Spell) {
game.informPlayers(controller.getLogName() + ((Spell) stackObjectCopy).getActivatedMessage(game));
}
return true; return true;
} }

View file

@ -1,35 +1,37 @@
package mage.cards.i; package mage.cards.i;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.abilities.effects.common.CopySpellForEachItCouldTargetEffect; import mage.abilities.effects.common.CopySpellForEachItCouldTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.FilterInPlay; import mage.filter.StaticFilters;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.util.TargetAddress; import mage.util.TargetAddress;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/** /**
* @author duncant * @author duncant
*/ */
public final class InkTreaderNephilim extends CardImpl { public final class InkTreaderNephilim extends CardImpl {
public InkTreaderNephilim(UUID ownerId, CardSetInfo setInfo) { public InkTreaderNephilim(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{R}{G}{W}{U}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{G}{W}{U}");
this.subtype.add(SubType.NEPHILIM); this.subtype.add(SubType.NEPHILIM);
this.power = new MageInt(3); this.power = new MageInt(3);
@ -55,7 +57,7 @@ class InkTreaderNephilimTriggeredAbility extends TriggeredAbilityImpl {
super(Zone.BATTLEFIELD, new InkTreaderNephilimEffect(), false); super(Zone.BATTLEFIELD, new InkTreaderNephilimEffect(), false);
} }
InkTreaderNephilimTriggeredAbility(final InkTreaderNephilimTriggeredAbility ability) { private InkTreaderNephilimTriggeredAbility(final InkTreaderNephilimTriggeredAbility ability) {
super(ability); super(ability);
} }
@ -71,12 +73,9 @@ class InkTreaderNephilimTriggeredAbility extends TriggeredAbilityImpl {
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
Spell spell = game.getStack().getSpell(event.getTargetId()); Spell spell = game.getSpell(event.getTargetId());
if (spell != null if (spell != null && spell.isInstantOrSorcery()) {
&& (spell.isInstant() || spell.isSorcery())) { getEffects().setValue("triggeringSpell", spell);
for (Effect effect : getEffects()) {
effect.setValue("triggeringSpell", spell);
}
return true; return true;
} }
return false; return false;
@ -85,63 +84,74 @@ class InkTreaderNephilimTriggeredAbility extends TriggeredAbilityImpl {
@Override @Override
public boolean checkInterveningIfClause(Game game) { public boolean checkInterveningIfClause(Game game) {
Spell spell = (Spell) getEffects().get(0).getValue("triggeringSpell"); Spell spell = (Spell) getEffects().get(0).getValue("triggeringSpell");
if (spell != null) { if (spell == null) {
boolean allTargetsInkTreaderNephilim = true; return false;
boolean atLeastOneTargetsInkTreaderNephilim = false; }
for (TargetAddress addr : TargetAddress.walk(spell)) { boolean flag = false;
Target targetInstance = addr.getTarget(spell); for (TargetAddress addr : TargetAddress.walk(spell)) {
for (UUID target : targetInstance.getTargets()) { Target targetInstance = addr.getTarget(spell);
allTargetsInkTreaderNephilim &= target.equals(sourceId); for (UUID target : targetInstance.getTargets()) {
atLeastOneTargetsInkTreaderNephilim |= target.equals(sourceId); if (target == null) {
continue;
} }
} Permanent permanent = game.getPermanent(target);
if (allTargetsInkTreaderNephilim && atLeastOneTargetsInkTreaderNephilim) { if (permanent == null || !permanent.getId().equals(getSourceId())) {
return true; return false;
}
if (getSourceObjectZoneChangeCounter() != 0
&& getSourceObjectZoneChangeCounter() != permanent.getZoneChangeCounter(game)) {
return false;
}
flag = true;
} }
} }
return false; return flag;
} }
@Override @Override
public String getRule() { public String getRule() {
return "Whenever a player casts an instant or sorcery spell, if that spell targets only {this}, copy the spell for each other creature that spell could target. Each copy targets a different one of those creatures."; return "Whenever a player casts an instant or sorcery spell, " +
"if that spell targets only {this}, copy the spell for each other creature that spell could target. " +
"Each copy targets a different one of those creatures.";
} }
} }
class InkTreaderNephilimEffect extends CopySpellForEachItCouldTargetEffect<Permanent> { class InkTreaderNephilimEffect extends CopySpellForEachItCouldTargetEffect {
public InkTreaderNephilimEffect() { InkTreaderNephilimEffect() {
this(new FilterCreaturePermanent()); super();
} }
public InkTreaderNephilimEffect(InkTreaderNephilimEffect effect) { private InkTreaderNephilimEffect(InkTreaderNephilimEffect effect) {
super(effect); super(effect);
} }
private InkTreaderNephilimEffect(FilterInPlay<Permanent> filter) {
super(filter);
}
@Override @Override
protected Player getPlayer(Game game, Ability source) { protected Player getPlayer(Game game, Ability source) {
return game.getPlayer(source.getControllerId()); return game.getPlayer(source.getControllerId());
} }
@Override
protected List<MageObjectReferencePredicate> getPossibleTargets(Spell spell, Player player, Ability source, Game game) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
return game.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_PERMANENT_CREATURE,
player.getId(), source.getSourceId(), game
).stream()
.filter(Objects::nonNull)
.filter(p -> !p.equals(permanent))
.filter(p -> spell.canTarget(game, p.getId()))
.map(p -> new MageObjectReference(p, game))
.map(MageObjectReferencePredicate::new)
.collect(Collectors.toList());
}
@Override @Override
protected Spell getSpell(Game game, Ability source) { protected Spell getSpell(Game game, Ability source) {
return (Spell) getValue("triggeringSpell"); return (Spell) getValue("triggeringSpell");
} }
@Override
protected boolean changeTarget(Target target, Game game, Ability source) {
return true;
}
@Override
protected void modifyCopy(Spell copy, Game game, Ability source) {
copy.setControllerId(source.getControllerId());
}
@Override @Override
public InkTreaderNephilimEffect copy() { public InkTreaderNephilimEffect copy() {
return new InkTreaderNephilimEffect(this); return new InkTreaderNephilimEffect(this);

View file

@ -119,7 +119,6 @@ class KurkeshOnakkeAncientEffect extends OneShotEffect {
Permanent sourcePermanent = game.getPermanent(source.getSourceId()); Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (ability != null && controller != null) { if (ability != null && controller != null) {
ability.createCopyOnStack(game, source, source.getControllerId(), true); ability.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getName() + ": " + controller.getLogName() + " copied activated ability");
return true; return true;
} }
return false; return false;

View file

@ -109,7 +109,6 @@ class LithoformEngineEffect extends OneShotEffect {
Permanent sourcePermanent = game.getPermanent(source.getSourceId()); Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (controller != null && sourcePermanent != null) { if (controller != null && sourcePermanent != null) {
stackAbility.createCopyOnStack(game, source, source.getControllerId(), true); stackAbility.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getIdName() + ": " + controller.getLogName() + " copied ability");
return true; return true;
} }
} }

View file

@ -80,14 +80,7 @@ class CopySourceSpellEffect extends OneShotEffect {
if (controller != null) { if (controller != null) {
Spell spell = game.getStack().getSpell(source.getSourceId()); Spell spell = game.getStack().getSpell(source.getSourceId());
if (spell != null) { if (spell != null) {
StackObject stackObjectCopy = spell.createCopyOnStack(game, source, source.getControllerId(), true); spell.createCopyOnStack(game, source, source.getControllerId(), true);
if (stackObjectCopy instanceof Spell) {
String activateMessage = ((Spell) stackObjectCopy).getActivatedMessage(game);
if (activateMessage.startsWith(" casts ")) {
activateMessage = activateMessage.substring(6);
}
game.informPlayers(controller.getLogName() + " copies " + activateMessage);
}
return true; return true;
} }
} }

View file

@ -1,7 +1,6 @@
package mage.cards.m; package mage.cards.m;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.common.SimpleActivatedAbility;
@ -11,25 +10,24 @@ import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetSpell; import mage.target.TargetSpell;
import java.util.UUID;
/** /**
*
* @author LevelX2 * @author LevelX2
*/ */
public final class MeletisCharlatan extends CardImpl { public final class MeletisCharlatan extends CardImpl {
public MeletisCharlatan(UUID ownerId, CardSetInfo setInfo) { public MeletisCharlatan(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{2}{U}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}");
this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.HUMAN);
this.subtype.add(SubType.WIZARD); this.subtype.add(SubType.WIZARD);
@ -69,15 +67,7 @@ class MeletisCharlatanCopyTargetSpellEffect extends OneShotEffect {
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Spell spell = game.getStack().getSpell(targetPointer.getFirst(game, source)); Spell spell = game.getStack().getSpell(targetPointer.getFirst(game, source));
if (spell != null) { if (spell != null) {
StackObject newStackObject = spell.createCopyOnStack(game, source, spell.getControllerId(), true); spell.createCopyOnStack(game, source, spell.getControllerId(), true);
Player player = game.getPlayer(spell.getControllerId());
if (player != null && newStackObject instanceof Spell) {
String activateMessage = ((Spell) newStackObject).getActivatedMessage(game);
if (activateMessage.startsWith(" casts ")) {
activateMessage = activateMessage.substring(6);
}
game.informPlayers(player.getLogName() + " copies " + activateMessage);
}
return true; return true;
} }
return false; return false;

View file

@ -1,8 +1,7 @@
package mage.cards.m; package mage.cards.m;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.common.CopySpellForEachItCouldTargetEffect; import mage.abilities.effects.common.CopySpellForEachItCouldTargetEffect;
@ -12,25 +11,28 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.FilterInPlay; import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.util.TargetAddress; import mage.util.TargetAddress;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/** /**
*
* @author LevelX2 * @author LevelX2
*/ */
public final class MirrorwingDragon extends CardImpl { public final class MirrorwingDragon extends CardImpl {
public MirrorwingDragon(UUID ownerId, CardSetInfo setInfo) { public MirrorwingDragon(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{R}{R}"); super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}{R}");
this.subtype.add(SubType.DRAGON); this.subtype.add(SubType.DRAGON);
this.power = new MageInt(4); this.power = new MageInt(4);
this.toughness = new MageInt(5); this.toughness = new MageInt(5);
@ -59,7 +61,7 @@ class MirrorwingDragonCopyTriggeredAbility extends TriggeredAbilityImpl {
super(Zone.BATTLEFIELD, new MirrorwingDragonCopySpellEffect(), false); super(Zone.BATTLEFIELD, new MirrorwingDragonCopySpellEffect(), false);
} }
MirrorwingDragonCopyTriggeredAbility(final MirrorwingDragonCopyTriggeredAbility ability) { private MirrorwingDragonCopyTriggeredAbility(final MirrorwingDragonCopyTriggeredAbility ability) {
super(ability); super(ability);
} }
@ -80,26 +82,34 @@ class MirrorwingDragonCopyTriggeredAbility extends TriggeredAbilityImpl {
} }
private boolean checkSpell(Spell spell, Game game) { private boolean checkSpell(Spell spell, Game game) {
if (spell != null if (spell == null || !spell.isInstantOrSorcery()) {
&& (spell.isInstant() || spell.isSorcery())) { return false;
boolean noTargets = true; }
for (TargetAddress addr : TargetAddress.walk(spell)) { boolean noTargets = true;
noTargets = false; for (TargetAddress addr : TargetAddress.walk(spell)) {
Target targetInstance = addr.getTarget(spell); if (addr == null) {
for (UUID target : targetInstance.getTargets()) { continue;
Permanent permanent = game.getPermanent(target); }
if (permanent == null || !permanent.getId().equals(getSourceId())) { noTargets = false;
return false; Target targetInstance = addr.getTarget(spell);
} if (targetInstance == null) {
continue;
}
for (UUID target : targetInstance.getTargets()) {
if (target == null) {
continue;
}
Permanent permanent = game.getPermanent(target);
if (permanent == null || !permanent.getId().equals(getSourceId())) {
return false;
} }
} }
if (noTargets) {
return false;
}
getEffects().get(0).setValue("triggeringSpell", spell);
return true;
} }
return false; if (noTargets) {
return false;
}
getEffects().setValue("triggeringSpell", spell);
return true;
} }
@Override @Override
@ -110,28 +120,40 @@ class MirrorwingDragonCopyTriggeredAbility extends TriggeredAbilityImpl {
} }
} }
class MirrorwingDragonCopySpellEffect extends CopySpellForEachItCouldTargetEffect<Permanent> { class MirrorwingDragonCopySpellEffect extends CopySpellForEachItCouldTargetEffect {
public MirrorwingDragonCopySpellEffect() { MirrorwingDragonCopySpellEffect() {
this(new FilterControlledCreaturePermanent()); super();
this.staticText = "that player copies that spell for each other creature they control that the spell could target. Each copy targets a different one of those creatures."; this.staticText = "that player copies that spell for each other creature they control that the spell could target. Each copy targets a different one of those creatures.";
} }
public MirrorwingDragonCopySpellEffect(MirrorwingDragonCopySpellEffect effect) { private MirrorwingDragonCopySpellEffect(MirrorwingDragonCopySpellEffect effect) {
super(effect); super(effect);
} }
private MirrorwingDragonCopySpellEffect(FilterInPlay<Permanent> filter) {
super(filter);
}
@Override @Override
protected Player getPlayer(Game game, Ability source) { protected Player getPlayer(Game game, Ability source) {
Spell spell = getSpell(game, source); Spell spell = getSpell(game, source);
if (spell != null) { if (spell == null) {
return game.getPlayer(spell.getControllerId()); return null;
} }
return null; return game.getPlayer(spell.getControllerId());
}
@Override
protected List<MageObjectReferencePredicate> getPossibleTargets(Spell spell, Player player, Ability source, Game game) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
return game.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_CONTROLLED_CREATURE,
player.getId(), source.getSourceId(), game
).stream()
.filter(Objects::nonNull)
.filter(p -> !p.equals(permanent))
.filter(p -> spell.canTarget(game, p.getId()))
.map(p -> new MageObjectReference(p, game))
.map(MageObjectReferencePredicate::new)
.collect(Collectors.toList());
} }
@Override @Override
@ -139,26 +161,6 @@ class MirrorwingDragonCopySpellEffect extends CopySpellForEachItCouldTargetEffec
return (Spell) getValue("triggeringSpell"); return (Spell) getValue("triggeringSpell");
} }
@Override
protected boolean changeTarget(Target target, Game game, Ability source) {
return true;
}
@Override
protected void modifyCopy(Spell copy, Game game, Ability source) {
Spell spell = getSpell(game, source);
copy.setControllerId(spell.getControllerId());
}
@Override
protected boolean okUUIDToCopyFor(UUID potentialTarget, Game game, Ability source, Spell spell) {
Permanent permanent = game.getPermanent(potentialTarget);
if (permanent == null || !permanent.isControlledBy(spell.getControllerId())) {
return false;
}
return true;
}
@Override @Override
public MirrorwingDragonCopySpellEffect copy() { public MirrorwingDragonCopySpellEffect copy() {
return new MirrorwingDragonCopySpellEffect(this); return new MirrorwingDragonCopySpellEffect(this);

View file

@ -1,8 +1,7 @@
package mage.cards.p; package mage.cards.p;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
@ -13,11 +12,10 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.filter.FilterInPlay; import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.permanent.token.GolemToken; import mage.game.permanent.token.GolemToken;
import mage.game.stack.Spell; import mage.game.stack.Spell;
@ -25,13 +23,18 @@ import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.util.TargetAddress; import mage.util.TargetAddress;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/** /**
* @author duncant * @author duncant
*/ */
public final class PrecursorGolem extends CardImpl { public final class PrecursorGolem extends CardImpl {
public PrecursorGolem(UUID ownerId, CardSetInfo setInfo) { public PrecursorGolem(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.ARTIFACT,CardType.CREATURE},"{5}"); super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{5}");
this.subtype.add(SubType.GOLEM); this.subtype.add(SubType.GOLEM);
this.power = new MageInt(3); this.power = new MageInt(3);
@ -60,7 +63,7 @@ class PrecursorGolemCopyTriggeredAbility extends TriggeredAbilityImpl {
super(Zone.BATTLEFIELD, new PrecursorGolemCopySpellEffect(), false); super(Zone.BATTLEFIELD, new PrecursorGolemCopySpellEffect(), false);
} }
PrecursorGolemCopyTriggeredAbility(final PrecursorGolemCopyTriggeredAbility ability) { private PrecursorGolemCopyTriggeredAbility(final PrecursorGolemCopyTriggeredAbility ability) {
super(ability); super(ability);
} }
@ -77,62 +80,77 @@ class PrecursorGolemCopyTriggeredAbility extends TriggeredAbilityImpl {
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
Spell spell = game.getStack().getSpell(event.getTargetId()); Spell spell = game.getStack().getSpell(event.getTargetId());
return checkSpell(spell, game); if (spell == null || !spell.isInstantOrSorcery()) {
} return false;
}
private boolean checkSpell(Spell spell, Game game) { UUID targetGolem = null;
if (spell != null for (TargetAddress addr : TargetAddress.walk(spell)) {
&& (spell.isInstant() || spell.isSorcery())) { Target targetInstance = addr.getTarget(spell);
UUID targetGolem = null; for (UUID target : targetInstance.getTargets()) {
for (TargetAddress addr : TargetAddress.walk(spell)) { Permanent permanent = game.getPermanent(target);
Target targetInstance = addr.getTarget(spell); if (permanent == null || !permanent.hasSubtype(SubType.GOLEM, game)) {
for (UUID target : targetInstance.getTargets()) { return false;
Permanent permanent = game.getPermanent(target); }
if (permanent == null || !permanent.hasSubtype(SubType.GOLEM, game)) { if (targetGolem == null) {
targetGolem = target;
} else // If a spell has multiple targets, but it's targeting the same Golem with all of them, Precursor Golem's last ability will trigger
{
if (!targetGolem.equals(target)) {
return false; return false;
} }
if (targetGolem == null) {
targetGolem = target;
} else // If a spell has multiple targets, but it's targeting the same Golem with all of them, Precursor Golem's last ability will trigger
{
if (!targetGolem.equals(target)) {
return false;
}
}
} }
} }
if (targetGolem != null) {
getEffects().get(0).setValue("triggeringSpell", spell);
getEffects().get(0).setValue("targetedGolem", targetGolem);
return true;
}
} }
return false; if (targetGolem == null) {
return false;
}
getEffects().setValue("triggeringSpell", spell);
getEffects().setValue("targetedGolem", targetGolem);
return true;
} }
@Override @Override
public String getRule() { public String getRule() {
return "Whenever a player casts an instant or sorcery spell that targets only a single Golem, that player copies that spell for each other Golem that spell could target. Each copy targets a different one of those Golems."; return "Whenever a player casts an instant or sorcery spell that targets only a single Golem, " +
"that player copies that spell for each other Golem that spell could target. " +
"Each copy targets a different one of those Golems.";
} }
} }
class PrecursorGolemCopySpellEffect extends CopySpellForEachItCouldTargetEffect<Permanent> { class PrecursorGolemCopySpellEffect extends CopySpellForEachItCouldTargetEffect {
public PrecursorGolemCopySpellEffect() { private static final FilterPermanent filter = new FilterPermanent(SubType.GOLEM, "");
this(new FilterCreaturePermanent(SubType.GOLEM, "Golem"));
PrecursorGolemCopySpellEffect() {
super();
} }
public PrecursorGolemCopySpellEffect(PrecursorGolemCopySpellEffect effect) { private PrecursorGolemCopySpellEffect(PrecursorGolemCopySpellEffect effect) {
super(effect); super(effect);
} }
private PrecursorGolemCopySpellEffect(FilterInPlay<Permanent> filter) {
super(filter);
}
@Override @Override
protected Player getPlayer(Game game, Ability source) { protected Player getPlayer(Game game, Ability source) {
return game.getPlayer(source.getControllerId()); Spell spell = getSpell(game, source);
if (spell == null) {
return null;
}
return game.getPlayer(spell.getControllerId());
}
@Override
protected List<MageObjectReferencePredicate> getPossibleTargets(Spell spell, Player player, Ability source, Game game) {
Permanent permanent = (Permanent) getValue("targetedGolem");
return game.getBattlefield()
.getActivePermanents(
filter, player.getId(), source.getSourceId(), game
).stream()
.filter(Objects::nonNull)
.filter(p -> !p.equals(permanent))
.filter(p -> spell.canTarget(game, p.getId()))
.map(p -> new MageObjectReference(p, game))
.map(MageObjectReferencePredicate::new)
.collect(Collectors.toList());
} }
@Override @Override
@ -140,15 +158,6 @@ class PrecursorGolemCopySpellEffect extends CopySpellForEachItCouldTargetEffect<
return (Spell) getValue("triggeringSpell"); return (Spell) getValue("triggeringSpell");
} }
@Override
protected boolean changeTarget(Target target, Game game, Ability source) {
return true;
}
@Override
protected void modifyCopy(Spell copy, Game game, Ability source) {
}
@Override @Override
public PrecursorGolemCopySpellEffect copy() { public PrecursorGolemCopySpellEffect copy() {
return new PrecursorGolemCopySpellEffect(this); return new PrecursorGolemCopySpellEffect(this);

View file

@ -1,7 +1,6 @@
package mage.cards.p; package mage.cards.p;
import java.util.UUID;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.Mode;
import mage.abilities.condition.common.SpellMasteryCondition; import mage.abilities.condition.common.SpellMasteryCondition;
@ -21,8 +20,9 @@ import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetSpell; import mage.target.TargetSpell;
import java.util.UUID;
/** /**
*
* @author LevelX2 * @author LevelX2
*/ */
public final class PsychicRebuttal extends CardImpl { public final class PsychicRebuttal extends CardImpl {
@ -35,7 +35,7 @@ public final class PsychicRebuttal extends CardImpl {
} }
public PsychicRebuttal(UUID ownerId, CardSetInfo setInfo) { public PsychicRebuttal(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{1}{U}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{U}");
// Counter target instant or sorcery spell that targets you. // Counter target instant or sorcery spell that targets you.
this.getSpellAbility().addEffect(new PsychicRebuttalEffect()); this.getSpellAbility().addEffect(new PsychicRebuttalEffect());
@ -82,15 +82,7 @@ class PsychicRebuttalEffect extends OneShotEffect {
if (SpellMasteryCondition.instance.apply(game, source) if (SpellMasteryCondition.instance.apply(game, source)
&& controller.chooseUse(Outcome.PlayForFree, "Copy " + spell.getName() + " (you may choose new targets for the copy)?", source, game)) { && controller.chooseUse(Outcome.PlayForFree, "Copy " + spell.getName() + " (you may choose new targets for the copy)?", source, game)) {
spell.createCopyOnStack(game, source, source.getControllerId(), true);
StackObject newStackObject = spell.createCopyOnStack(game, source, source.getControllerId(), true);
if (newStackObject instanceof Spell) {
String activateMessage = ((Spell) newStackObject).getActivatedMessage(game);
if (activateMessage.startsWith(" casts ")) {
activateMessage = activateMessage.substring(6);
}
game.informPlayers(controller.getLogName() + activateMessage);
}
} }
return true; return true;

View file

@ -1,43 +1,43 @@
package mage.cards.r; package mage.cards.r;
import java.util.UUID; import mage.MageObjectReference;
import mage.MageItem;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.AbilityImpl;
import mage.abilities.effects.common.CopySpellForEachItCouldTargetEffect; import mage.abilities.effects.common.CopySpellForEachItCouldTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.filter.FilterInPlay;
import mage.filter.FilterSpell; import mage.filter.FilterSpell;
import mage.filter.StaticFilters;
import mage.filter.common.FilterInstantOrSorcerySpell; import mage.filter.common.FilterInstantOrSorcerySpell;
import mage.filter.common.FilterPermanentOrPlayer;
import mage.filter.predicate.ObjectPlayer; import mage.filter.predicate.ObjectPlayer;
import mage.filter.predicate.ObjectPlayerPredicate; import mage.filter.predicate.ObjectPlayerPredicate;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetSpell; import mage.target.TargetSpell;
import mage.util.TargetAddress; import mage.util.TargetAddress;
import java.util.*;
/** /**
* @author duncant * @author duncant
*/ */
public final class Radiate extends CardImpl { public final class Radiate extends CardImpl {
protected static final FilterSpell filter = new FilterInstantOrSorcerySpell(); protected static final FilterSpell filter = new FilterInstantOrSorcerySpell(
"instant or sorcery spell that targets only a single permanent or player"
);
static { static {
filter.add(new SpellWithOnlySingleTargetPredicate()); filter.add(SpellWithOnlySingleTargetPredicate.instance);
filter.add(new SpellWithOnlyPermanentOrPlayerTargetsPredicate()); filter.add(SpellWithOnlyPermanentOrPlayerTargetsPredicate.instance);
filter.setMessage("instant or sorcery spell that targets only a single permanent or player");
} }
public Radiate(UUID ownerId, CardSetInfo setInfo) { public Radiate(UUID ownerId, CardSetInfo setInfo) {
super(ownerId,setInfo,new CardType[]{CardType.INSTANT},"{3}{R}{R}"); super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{R}{R}");
// Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players. // Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players.
this.getSpellAbility().addEffect(new RadiateEffect()); this.getSpellAbility().addEffect(new RadiateEffect());
@ -54,7 +54,8 @@ public final class Radiate extends CardImpl {
} }
} }
class SpellWithOnlySingleTargetPredicate implements ObjectPlayerPredicate<ObjectPlayer<Spell>> { enum SpellWithOnlySingleTargetPredicate implements ObjectPlayerPredicate<ObjectPlayer<Spell>> {
instance;
@Override @Override
public boolean apply(ObjectPlayer<Spell> input, Game game) { public boolean apply(ObjectPlayer<Spell> input, Game game) {
@ -77,7 +78,8 @@ class SpellWithOnlySingleTargetPredicate implements ObjectPlayerPredicate<Object
} }
} }
class SpellWithOnlyPermanentOrPlayerTargetsPredicate implements ObjectPlayerPredicate<ObjectPlayer<Spell>> { enum SpellWithOnlyPermanentOrPlayerTargetsPredicate implements ObjectPlayerPredicate<ObjectPlayer<Spell>> {
instance;
@Override @Override
public boolean apply(ObjectPlayer<Spell> input, Game game) { public boolean apply(ObjectPlayer<Spell> input, Game game) {
@ -98,47 +100,61 @@ class SpellWithOnlyPermanentOrPlayerTargetsPredicate implements ObjectPlayerPred
} }
} }
class RadiateEffect extends CopySpellForEachItCouldTargetEffect<MageItem> { class RadiateEffect extends CopySpellForEachItCouldTargetEffect {
public RadiateEffect() { RadiateEffect() {
this(new FilterPermanentOrPlayer()); super();
staticText = "Choose target instant or sorcery spell that targets only a single permanent or player. " +
"Copy that spell for each other permanent or player the spell could target. " +
"Each copy targets a different one of those permanents and players.";
} }
public RadiateEffect(RadiateEffect effect) { private RadiateEffect(RadiateEffect effect) {
super(effect); super(effect);
} }
private RadiateEffect(FilterInPlay<MageItem> filter) {
super(filter);
}
@Override
public String getText(Mode mode) {
return "Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell for each other permanent or player the spell could target. Each copy targets a different one of those permanents and players.";
}
@Override @Override
protected Player getPlayer(Game game, Ability source) { protected Player getPlayer(Game game, Ability source) {
return game.getPlayer(source.getControllerId()); return game.getPlayer(source.getControllerId());
} }
@Override
protected List<MageObjectReferencePredicate> getPossibleTargets(Spell spell, Player player, Ability source, Game game) {
List<MageObjectReferencePredicate> predicates = new ArrayList<>();
UUID targeted = spell
.getSpellAbilities()
.stream()
.map(AbilityImpl::getTargets)
.flatMap(Collection::stream)
.map(Target::getTargets)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.findAny()
.orElse(null);
game.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_PERMANENT, player.getId(), source.getSourceId(), game
).stream()
.filter(Objects::nonNull)
.filter(p -> !p.equals(game.getPermanent(targeted)))
.filter(p -> spell.canTarget(game, p.getId()))
.map(p -> new MageObjectReference(p, game))
.map(MageObjectReferencePredicate::new)
.forEach(predicates::add);
game.getState()
.getPlayersInRange(source.getControllerId(), game)
.stream()
.filter(uuid -> !uuid.equals(targeted))
.filter(uuid -> spell.canTarget(game, uuid))
.map(MageObjectReference::new)
.map(MageObjectReferencePredicate::new)
.forEach(predicates::add);
return predicates;
}
@Override @Override
protected Spell getSpell(Game game, Ability source) { protected Spell getSpell(Game game, Ability source) {
StackObject ret = game.getStack().getStackObject(targetPointer.getFirst(game, source)); return game.getSpell(source.getFirstTarget());
if (ret instanceof Spell) {
return (Spell) ret;
}
return null;
}
@Override
protected boolean changeTarget(Target target, Game game, Ability source) {
return true;
}
@Override
protected void modifyCopy(Spell copy, Game game, Ability source) {
copy.setControllerId(source.getControllerId());
} }
@Override @Override

View file

@ -132,7 +132,6 @@ class RepeatedReverberationEffect extends OneShotEffect {
return false; return false;
} }
stackAbility.createCopyOnStack(game, source, source.getControllerId(), true, 2); stackAbility.createCopyOnStack(game, source, source.getControllerId(), true, 2);
game.informPlayers(sourcePermanent.getIdName() + ": " + controller.getLogName() + " copied loyalty ability twice");
return true; return true;
} }

View file

@ -109,7 +109,6 @@ class RingsOfBrighthearthEffect extends OneShotEffect {
Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(source.getSourceId()); Permanent sourcePermanent = game.getPermanentOrLKIBattlefield(source.getSourceId());
if (ability != null && controller != null && sourcePermanent != null) { if (ability != null && controller != null && sourcePermanent != null) {
ability.createCopyOnStack(game, source, source.getControllerId(), true); ability.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getIdName() + ": " + controller.getLogName() + " copied activated ability");
return true; return true;
} }
return false; return false;

View file

@ -82,7 +82,6 @@ class StringOfDisappearancesEffect extends OneShotEffect {
return true; return true;
} }
spell.createCopyOnStack(game, source, affectedPlayer.getId(), true); spell.createCopyOnStack(game, source, affectedPlayer.getId(), true);
game.informPlayers(affectedPlayer.getLogName() + " copies " + spell.getName() + '.');
return true; return true;
} }
} }

View file

@ -63,7 +63,6 @@ class StrionicResonatorEffect extends OneShotEffect {
Permanent sourcePermanent = game.getPermanent(source.getSourceId()); Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (controller != null && sourcePermanent != null) { if (controller != null && sourcePermanent != null) {
stackAbility.createCopyOnStack(game, source, source.getControllerId(), true); stackAbility.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getIdName() + ": " + controller.getLogName() + " copied triggered ability");
return true; return true;
} }
} }

View file

@ -85,7 +85,6 @@ class TawnosUrzasApprenticeEffect extends OneShotEffect {
Permanent sourcePermanent = game.getPermanent(source.getSourceId()); Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (controller != null && sourcePermanent != null) { if (controller != null && sourcePermanent != null) {
stackAbility.createCopyOnStack(game, source, source.getControllerId(), true); stackAbility.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getIdName() + ": " + controller.getLogName() + " copied an ability");
return true; return true;
} }
} }

View file

@ -174,7 +174,6 @@ class UnboundFlourishingCopyEffect extends OneShotEffect {
if (needObject instanceof StackAbility) { if (needObject instanceof StackAbility) {
StackAbility stackAbility = (StackAbility) needObject; StackAbility stackAbility = (StackAbility) needObject;
stackAbility.createCopyOnStack(game, source, source.getControllerId(), true); stackAbility.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getName() + ": " + controller.getLogName() + " copied activated ability");
return true; return true;
} }
@ -182,7 +181,6 @@ class UnboundFlourishingCopyEffect extends OneShotEffect {
if (needObject instanceof Spell) { if (needObject instanceof Spell) {
Spell spell = (Spell) needObject; Spell spell = (Spell) needObject;
spell.createCopyOnStack(game, source, source.getControllerId(), true); spell.createCopyOnStack(game, source, source.getControllerId(), true);
game.informPlayers(sourcePermanent.getName() + ": " + controller.getLogName() + " copied casted spell");
return true; return true;
} }
} }

View file

@ -1,26 +1,32 @@
package mage.cards.z; package mage.cards.z;
import java.util.UUID;
import mage.MageInt; import mage.MageInt;
import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.common.CopySpellForEachItCouldTargetEffect; import mage.abilities.effects.common.CopySpellForEachItCouldTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.*; import mage.constants.CardType;
import mage.filter.FilterInPlay; import mage.constants.SubType;
import mage.filter.common.FilterControlledCreaturePermanent; import mage.constants.SuperType;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.util.TargetAddress; import mage.util.TargetAddress;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/** /**
*
* @author LevelX2 * @author LevelX2
*/ */
public final class ZadaHedronGrinder extends CardImpl { public final class ZadaHedronGrinder extends CardImpl {
@ -36,7 +42,6 @@ public final class ZadaHedronGrinder extends CardImpl {
// copy that spell for each other creature you control that the spell could target. // copy that spell for each other creature you control that the spell could target.
// Each copy targets a different one of those creatures. // Each copy targets a different one of those creatures.
this.addAbility(new ZadaHedronGrinderTriggeredAbility()); this.addAbility(new ZadaHedronGrinderTriggeredAbility());
} }
private ZadaHedronGrinder(final ZadaHedronGrinder card) { private ZadaHedronGrinder(final ZadaHedronGrinder card) {
@ -55,7 +60,7 @@ class ZadaHedronGrinderTriggeredAbility extends TriggeredAbilityImpl {
super(Zone.BATTLEFIELD, new ZadaHedronGrinderCopySpellEffect(), false); super(Zone.BATTLEFIELD, new ZadaHedronGrinderCopySpellEffect(), false);
} }
ZadaHedronGrinderTriggeredAbility(final ZadaHedronGrinderTriggeredAbility ability) { private ZadaHedronGrinderTriggeredAbility(final ZadaHedronGrinderTriggeredAbility ability) {
super(ability); super(ability);
} }
@ -71,40 +76,38 @@ class ZadaHedronGrinderTriggeredAbility extends TriggeredAbilityImpl {
@Override @Override
public boolean checkTrigger(GameEvent event, Game game) { public boolean checkTrigger(GameEvent event, Game game) {
Spell spell = game.getStack().getSpell(event.getTargetId()); if (!isControlledBy(event.getPlayerId())) {
return checkSpell(spell, game) return false;
&& event.getPlayerId().equals(controllerId); }
} Spell spell = game.getSpell(event.getTargetId());
if (spell == null || !spell.isInstantOrSorcery()) {
private boolean checkSpell(Spell spell, Game game) { return false;
if (spell != null }
&& (spell.isInstant() boolean noTargets = true;
|| spell.isSorcery())) { for (TargetAddress addr : TargetAddress.walk(spell)) {
boolean noTargets = true; if (addr == null) {
for (TargetAddress addr : TargetAddress.walk(spell)) { continue;
if (addr != null) { }
noTargets = false; noTargets = false;
Target targetInstance = addr.getTarget(spell); Target targetInstance = addr.getTarget(spell);
if (targetInstance != null) { if (targetInstance == null) {
for (UUID target : targetInstance.getTargets()) { continue;
if (target != null) { }
Permanent permanent = game.getPermanent(target); for (UUID target : targetInstance.getTargets()) {
if (permanent == null if (target == null) {
|| !permanent.getId().equals(getSourceId())) { continue;
return false; }
} Permanent permanent = game.getPermanent(target);
} if (permanent == null || !permanent.getId().equals(getSourceId())) {
} return false;
}
} }
} }
if (noTargets) {
return false;
}
getEffects().get(0).setValue("triggeringSpell", spell);
return true;
} }
return false; if (noTargets) {
return false;
}
getEffects().setValue("triggeringSpell", spell);
return true;
} }
@Override @Override
@ -115,29 +118,35 @@ class ZadaHedronGrinderTriggeredAbility extends TriggeredAbilityImpl {
} }
} }
class ZadaHedronGrinderCopySpellEffect extends CopySpellForEachItCouldTargetEffect<Permanent> { class ZadaHedronGrinderCopySpellEffect extends CopySpellForEachItCouldTargetEffect {
public ZadaHedronGrinderCopySpellEffect() { ZadaHedronGrinderCopySpellEffect() {
this(new FilterControlledCreaturePermanent()); super();
this.staticText = "copy that spell for each other creature you control "
+ "that the spell could target. Each copy targets a different one of those creatures.";
} }
public ZadaHedronGrinderCopySpellEffect(ZadaHedronGrinderCopySpellEffect effect) { private ZadaHedronGrinderCopySpellEffect(final ZadaHedronGrinderCopySpellEffect effect) {
super(effect); super(effect);
} }
private ZadaHedronGrinderCopySpellEffect(FilterInPlay<Permanent> filter) {
super(filter);
}
@Override @Override
protected Player getPlayer(Game game, Ability source) { protected Player getPlayer(Game game, Ability source) {
Spell spell = getSpell(game, source); return game.getPlayer(source.getControllerId());
if (spell != null) { }
return game.getPlayer(spell.getControllerId());
} @Override
return null; protected List<MageObjectReferencePredicate> getPossibleTargets(Spell spell, Player player, Ability source, Game game) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
return game.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_CONTROLLED_CREATURE,
player.getId(), source.getSourceId(), game
).stream()
.filter(Objects::nonNull)
.filter(p -> !p.equals(permanent))
.filter(p -> spell.canTarget(game, p.getId()))
.map(p -> new MageObjectReference(p, game))
.map(MageObjectReferencePredicate::new)
.collect(Collectors.toList());
} }
@Override @Override
@ -145,27 +154,6 @@ class ZadaHedronGrinderCopySpellEffect extends CopySpellForEachItCouldTargetEffe
return (Spell) getValue("triggeringSpell"); return (Spell) getValue("triggeringSpell");
} }
@Override
protected boolean changeTarget(Target target, Game game, Ability source) {
return true;
}
@Override
protected void modifyCopy(Spell copy, Game game, Ability source) {
Spell spell = getSpell(game, source);
copy.setControllerId(spell.getControllerId());
}
@Override
protected boolean okUUIDToCopyFor(UUID potentialTarget, Game game, Ability source, Spell spell) {
Permanent permanent = game.getPermanent(potentialTarget);
if (permanent == null
|| !permanent.isControlledBy(spell.getControllerId())) {
return false;
}
return true;
}
@Override @Override
public ZadaHedronGrinderCopySpellEffect copy() { public ZadaHedronGrinderCopySpellEffect copy() {
return new ZadaHedronGrinderCopySpellEffect(this); return new ZadaHedronGrinderCopySpellEffect(this);

View file

@ -0,0 +1,47 @@
package org.mage.test.cards.abilities.keywords;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author TheElk801
*/
public class EpicTest extends CardTestPlayerBase {
@Test
public void testEndlessSwarm() {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 8);
addCard(Zone.HAND, playerA, "Forest", 3);
addCard(Zone.HAND, playerA, "Endless Swarm");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Endless Swarm");
setStrictChooseMode(true);
setStopAt(7, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Snake", 3 + 3 + 4 + 5);
assertPermanentCount(playerA, "Forest", 8);
}
@Test
public void testEndlessSwarmCopied() {
addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 10);
addCard(Zone.HAND, playerA, "Forest", 3);
addCard(Zone.HAND, playerA, "Endless Swarm");
addCard(Zone.HAND, playerA, "Twincast");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Endless Swarm");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Twincast", "Endless Swarm");
setStopAt(7, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Snake", 2 * (3 + 3 + 4 + 5));
assertPermanentCount(playerA, "Tropical Island", 10);
}
}

View file

@ -0,0 +1,91 @@
package org.mage.test.cards.single.c20;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author TheElk801
*/
public class TwinningStaffTest extends CardTestPlayerBase {
private static final String staff = "Twinning Staff";
private static final String zada = "Zada, Hedron Grinder";
private static final String bear = "Grizzly Bears";
private static final String lion = "Silvercoat Lion";
private static final String growth = "Giant Growth";
private static final String disfigure = "Disfigure";
private static final String fork = "Fork";
private static final String elite = "Scalebane's Elite";
@Test
public void testWithZada() {
addCard(Zone.BATTLEFIELD, playerA, "Forest");
addCard(Zone.BATTLEFIELD, playerA, staff);
addCard(Zone.BATTLEFIELD, playerA, zada);
addCard(Zone.BATTLEFIELD, playerA, bear);
addCard(Zone.BATTLEFIELD, playerA, lion);
addCard(Zone.HAND, playerA, growth);
setChoice(playerA, TestPlayer.CHOICE_SKIP); // skip stack order
setChoice(playerA, "Yes");
addTarget(playerA, bear);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, growth, zada);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPowerToughness(playerA, zada, 3 + 3, 3 + 3);
assertPowerToughness(playerA, bear, 2 + 3 + 3, 2 + 3 + 3);
assertPowerToughness(playerA, lion, 2 + 3, 2 + 3);
}
@Test
public void testWithZadaTwice() {
addCard(Zone.BATTLEFIELD, playerA, "Forest");
addCard(Zone.BATTLEFIELD, playerA, staff, 2);
addCard(Zone.BATTLEFIELD, playerA, zada);
addCard(Zone.BATTLEFIELD, playerA, bear);
addCard(Zone.BATTLEFIELD, playerA, lion);
addCard(Zone.HAND, playerA, growth);
addTarget(playerA, bear, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, growth, zada);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPowerToughness(playerA, zada, 3 + 3, 3 + 3);
assertPowerToughness(playerA, bear, 2 + 3 + 3 + 3, 2 + 3 + 3 + 3);
assertPowerToughness(playerA, lion, 2 + 3, 2 + 3);
}
@Test
public void testFork() {
addCard(Zone.BATTLEFIELD, playerA, "Badlands", 3);
addCard(Zone.BATTLEFIELD, playerA, staff);
addCard(Zone.BATTLEFIELD, playerA, bear);
addCard(Zone.BATTLEFIELD, playerA, elite); // protection from black
addCard(Zone.HAND, playerA, disfigure);
addCard(Zone.HAND, playerA, fork);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, disfigure, bear);
setChoice(playerA, "Yes", 2);
addTarget(playerA, elite, 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, fork, disfigure, disfigure);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerA, disfigure, 1);
assertGraveyardCount(playerA, fork, 1);
assertGraveyardCount(playerA, bear, 1);
assertGraveyardCount(playerA, elite, 1);
}
}

View file

@ -0,0 +1,58 @@
package org.mage.test.cards.single.grn;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author TheElk801
*/
public class BeamsplitterMageTest extends CardTestPlayerBase {
private static final String bsm = "Beamsplitter Mage";
private static final String lion = "Silvercoat Lion";
private static final String duelist = "Deft Duelist";
private static final String bolt = "Lightning Bolt";
private static final String seeds = "Seeds of Strength";
@Test
public void testLightningBolt() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
addCard(Zone.BATTLEFIELD, playerA, lion);
addCard(Zone.BATTLEFIELD, playerA, duelist);
addCard(Zone.BATTLEFIELD, playerA, bsm);
addCard(Zone.HAND, playerA, bolt);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, bsm);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, bsm, 0);
assertPermanentCount(playerA, lion, 0);
assertPermanentCount(playerA, duelist, 1);
assertGraveyardCount(playerA, bsm, 1);
assertGraveyardCount(playerA, lion, 1);
assertGraveyardCount(playerA, duelist, 0);
assertGraveyardCount(playerA, bolt, 1);
}
@Test
public void testSeedsOfStrength() {
addCard(Zone.BATTLEFIELD, playerA, "Savannah", 2);
addCard(Zone.BATTLEFIELD, playerA, lion);
addCard(Zone.BATTLEFIELD, playerA, duelist);
addCard(Zone.BATTLEFIELD, playerA, bsm);
addCard(Zone.HAND, playerA, seeds);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, seeds, bsm);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertAllCommandsUsed();
assertPowerToughness(playerA, bsm, 5, 5);
assertPowerToughness(playerA, lion, 5, 5);
}
}

View file

@ -26,13 +26,13 @@ public class RadiateTest extends CardTestPlayerBaseWithAIHelps {
addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 2); addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 2);
// cast bolt and copy spell for each another target // cast bolt and copy spell for each another target
setChoice(playerA, TestPlayer.CHOICE_SKIP); // skip stack order
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Radiate", "Lightning Bolt", "Lightning Bolt"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Radiate", "Lightning Bolt", "Lightning Bolt");
checkStackSize("before radiate", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); checkStackSize("before radiate", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
// must have: 2x for corsairs, 2x for bears, 1x for A // must have: 2x for corsairs, 2x for bears, 1x for A
checkStackSize("after radiate", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1 + 5); checkStackSize("after radiate", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1 + 5);
addTarget(playerA, TestPlayer.TARGET_SKIP);
setStrictChooseMode(true); setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN); setStopAt(1, PhaseStep.END_TURN);
@ -47,7 +47,7 @@ public class RadiateTest extends CardTestPlayerBaseWithAIHelps {
@Test @Test
public void test_Play_AI() { public void test_Play_AI() {
// possible bug: game freeze or Target wasn't handled... TargetWithAdditionalFilter // This test has trouble now but the manual version works
// Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell // Choose target instant or sorcery spell that targets only a single permanent or player. Copy that spell
// for each other permanent or player the spell could target. Each copy targets a different one of those // for each other permanent or player the spell could target. Each copy targets a different one of those

View file

@ -4,6 +4,7 @@ import mage.abilities.keyword.TrampleAbility;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import org.junit.Test; import org.junit.Test;
import org.mage.test.player.TestPlayer;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
/** /**
@ -58,8 +59,8 @@ public class ZadaHedronGrinderTest extends CardTestPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
// cast // cast
setChoice(playerA, TestPlayer.CHOICE_SKIP); // skip stack order
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arbor Armament", "Zada, Hedron Grinder"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Arbor Armament", "Zada, Hedron Grinder");
addTarget(playerA, "Balduvian Bears^Silvercoat Lion");
setStrictChooseMode(true); setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT); setStopAt(1, PhaseStep.BEGIN_COMBAT);

View file

@ -1,6 +1,5 @@
package org.mage.test.multiplayer; package org.mage.test.multiplayer;
import java.io.FileNotFoundException;
import mage.constants.MultiplayerAttackOption; import mage.constants.MultiplayerAttackOption;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.RangeOfInfluence; import mage.constants.RangeOfInfluence;
@ -15,6 +14,8 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestMultiPlayerBase; import org.mage.test.serverside.base.CardTestMultiPlayerBase;
import java.io.FileNotFoundException;
/** /**
* @author LevelX2 * @author LevelX2
*/ */
@ -346,7 +347,7 @@ public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase {
/** /**
* Captive Audience doesn't work correctly in multiplayer #5593 * Captive Audience doesn't work correctly in multiplayer #5593
* * <p>
* Currently, if the controller of Captive Audience leaves the game, Captive * Currently, if the controller of Captive Audience leaves the game, Captive
* Audience returns to its owner instead of being exiled. * Audience returns to its owner instead of being exiled.
*/ */
@ -364,8 +365,6 @@ public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase {
addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1);
addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox", 1); addCard(Zone.BATTLEFIELD, playerA, "Pillarfield Ox", 1);
setChoice(playerA, "PlayerA"); // Starting Player
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Captive Audience"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Captive Audience");
setChoice(playerA, "PlayerD"); setChoice(playerA, "PlayerD");

View file

@ -1963,6 +1963,13 @@ public class TestPlayer implements Player {
public boolean choose(Outcome outcome, Choice choice, Game game) { public boolean choose(Outcome outcome, Choice choice, Game game) {
assertAliasSupportInChoices(false); assertAliasSupportInChoices(false);
if (!choices.isEmpty()) { if (!choices.isEmpty()) {
// skip choices
if (choices.get(0).equals(CHOICE_SKIP)) {
choices.remove(0);
return true;
}
if (choice.setChoiceByAnswers(choices, true)) { if (choice.setChoiceByAnswers(choices, true)) {
return true; return true;
} }
@ -2007,6 +2014,11 @@ public class TestPlayer implements Player {
abilityControllerId = target.getAbilityController(); abilityControllerId = target.getAbilityController();
} }
// ignore player select
if (target.getMessage().equals("Select a starting player")) {
return computerPlayer.choose(outcome, target, sourceId, game, options);
}
assertAliasSupportInChoices(true); assertAliasSupportInChoices(true);
if (!choices.isEmpty()) { if (!choices.isEmpty()) {
@ -2208,15 +2220,12 @@ public class TestPlayer implements Player {
*/ */
} }
// ignore player select this.chooseStrictModeFailed("choice", game, getInfo(game.getObject(sourceId)) + ";\n" + getInfo(target));
if (!target.getMessage().equals("Select a starting player")) {
this.chooseStrictModeFailed("choice", game, getInfo(game.getObject(sourceId)) + ";\n" + getInfo(target));
}
return computerPlayer.choose(outcome, target, sourceId, game, options); return computerPlayer.choose(outcome, target, sourceId, game, options);
} }
private void checkTargetDefinitionMarksSupport(Target needTarget, String targetDefinition, String canSupportChars) { private void checkTargetDefinitionMarksSupport(Target needTarget, String targetDefinition, String canSupportChars) {
// fail on wrong chars in definition // fail on wrong chars in definition ` ` ` ` ` ` ` ``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````
// ^ - multiple targets // ^ - multiple targets
// [] - special option like [no copy] // [] - special option like [no copy]
// = - target type like targetPlayer=PlayerA // = - target type like targetPlayer=PlayerA

View file

@ -1,58 +1,47 @@
package mage.abilities.effects.common; package mage.abilities.effects.common;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.filter.FilterPermanent;
import mage.game.Game; import mage.game.Game;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
*
*/ */
public class ChooseNewTargetsTargetEffect extends OneShotEffect { public class ChooseNewTargetsTargetEffect extends OneShotEffect {
private boolean forceChange; private final boolean forceChange;
private boolean onlyOneTarget; private final boolean onlyOneTarget;
private FilterPermanent filterNewTarget;
public ChooseNewTargetsTargetEffect() { public ChooseNewTargetsTargetEffect() {
this(false, false); this(false, false);
} }
public ChooseNewTargetsTargetEffect(boolean forceChange, boolean onlyOneTarget) {
this(forceChange, onlyOneTarget, null);
}
/** /**
* * @param forceChange forces the user to choose another target (only targets
* @param forceChange forces the user to choose another target (only targets * with maxtargets = 1 supported)
* with maxtargets = 1 supported) * @param onlyOneTarget only one target can be selected for the change
* @param onlyOneTarget only one target can be selected for the change
* @param filterNewTarget restriction to the new target * @param filterNewTarget restriction to the new target
*/ */
public ChooseNewTargetsTargetEffect(boolean forceChange, boolean onlyOneTarget, FilterPermanent filterNewTarget) { public ChooseNewTargetsTargetEffect(boolean forceChange, boolean onlyOneTarget) {
super(Outcome.Benefit); super(Outcome.Benefit);
this.forceChange = forceChange; this.forceChange = forceChange;
this.onlyOneTarget = onlyOneTarget; this.onlyOneTarget = onlyOneTarget;
this.filterNewTarget = filterNewTarget;
} }
public ChooseNewTargetsTargetEffect(final ChooseNewTargetsTargetEffect effect) { public ChooseNewTargetsTargetEffect(final ChooseNewTargetsTargetEffect effect) {
super(effect); super(effect);
this.forceChange = effect.forceChange; this.forceChange = effect.forceChange;
this.onlyOneTarget = effect.onlyOneTarget; this.onlyOneTarget = effect.onlyOneTarget;
this.filterNewTarget = effect.filterNewTarget;
} }
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
StackObject stackObject = game.getStack().getStackObject(source.getFirstTarget()); StackObject stackObject = game.getStack().getStackObject(source.getFirstTarget());
if (stackObject != null) { if (stackObject != null) {
return stackObject.chooseNewTargets(game, source.getControllerId(), forceChange, onlyOneTarget, filterNewTarget); return stackObject.chooseNewTargets(game, source.getControllerId(), forceChange, onlyOneTarget, null);
} }
return false; return false;
} }

View file

@ -1,463 +1,69 @@
package mage.abilities.effects.common; package mage.abilities.effects.common;
import mage.MageItem;
import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone; import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.filter.FilterImpl;
import mage.filter.FilterInPlay;
import mage.filter.predicate.other.FromSetPredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.CopiedStackObjectEvent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.util.functions.SpellCopyApplier;
import mage.target.TargetImpl;
import mage.util.TargetAddress;
import java.util.*; import java.util.Iterator;
import java.util.List;
/** /**
* @param <T>
* @author duncant * @author duncant
*/ */
public abstract class CopySpellForEachItCouldTargetEffect<T extends MageItem> extends OneShotEffect { public abstract class CopySpellForEachItCouldTargetEffect extends OneShotEffect {
protected final FilterInPlay<T> filter; private static final class CopyApplier implements SpellCopyApplier {
public CopySpellForEachItCouldTargetEffect(FilterInPlay<T> filter) { private final Iterator<MageObjectReferencePredicate> iterator;
super(Outcome.Copy);
this.staticText = "copy the spell for each other " + filter.getMessage() private CopyApplier(List<MageObjectReferencePredicate> predicates) {
+ " that spell could target. Each copy targets a different one"; this.iterator = predicates.iterator();
this.filter = filter; }
@Override
public void modifySpell(Spell spell, Game game) {
}
@Override
public MageObjectReferencePredicate getNextPredicate() {
if (iterator.hasNext()) {
return iterator.next();
}
return null;
}
} }
public CopySpellForEachItCouldTargetEffect(final CopySpellForEachItCouldTargetEffect effect) { protected CopySpellForEachItCouldTargetEffect() {
super(Outcome.Copy);
}
protected CopySpellForEachItCouldTargetEffect(final CopySpellForEachItCouldTargetEffect effect) {
super(effect); super(effect);
this.filter = effect.filter;
} }
protected abstract Spell getSpell(Game game, Ability source); protected abstract Spell getSpell(Game game, Ability source);
protected abstract Player getPlayer(Game game, Ability source); protected abstract Player getPlayer(Game game, Ability source);
protected abstract boolean changeTarget(Target target, Game game, Ability source); protected abstract List<MageObjectReferencePredicate> getPossibleTargets(Spell spell, Player player, Ability source, Game game);
protected abstract void modifyCopy(Spell copy, Game game, Ability source);
protected void modifyCopy(Spell copy, T newTarget, Game game, Ability source) {
modifyCopy(copy, game, source);
}
protected boolean okUUIDToCopyFor(UUID potentialTarget, Game game, Ability source, Spell spell) {
return true;
}
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player actingPlayer = getPlayer(game, source); Player actingPlayer = getPlayer(game, source);
if (actingPlayer == null) {
return false;
}
Spell spell = getSpell(game, source); Spell spell = getSpell(game, source);
if (spell != null) { if (actingPlayer == null || spell == null) {
Set<TargetAddress> targetsToBeChanged = new HashSet<>();
boolean madeACopy = false;
for (TargetAddress addr : TargetAddress.walk(spell)) {
Target targetInstance = addr.getTarget(spell);
if (targetInstance.getNumberOfTargets() > 1) {
throw new UnsupportedOperationException("Changing Target instances "
+ "with multiple targets is unsupported");
}
if (changeTarget(targetInstance, game, source)) {
targetsToBeChanged.add(addr);
}
}
if (targetsToBeChanged.size() < 1) {
return false;
}
// TODO: add support if multiple copies? See Twinning Staff
// generate copies for each possible target, but do not put it to stack (must choose targets in custom order later)
Spell copy = spell.copySpell(game, source, source.getControllerId());
modifyCopy(copy, game, source);
Target sampleTarget = targetsToBeChanged.iterator().next().getTarget(copy);
sampleTarget.setNotTarget(true);
Map<UUID, Map<UUID, Spell>> playerTargetCopyMap = new HashMap<>();
for (UUID objId : sampleTarget.possibleTargets(actingPlayer.getId(), game)) {
MageItem obj = game.getObject(objId);
if (obj == null) {
obj = game.getPlayer(objId);
}
if (obj != null) {
// TODO: add support if multiple copies? See Twinning Staff
copy = spell.copySpell(game, source, source.getControllerId());
try {
modifyCopy(copy, (T) obj, game, source);
if (!filter.match((T) obj, source.getSourceId(), actingPlayer.getId(), game)) {
continue;
}
} catch (ClassCastException e) {
continue;
}
boolean legal = true;
for (TargetAddress addr : targetsToBeChanged) {
// potential target must be legal for all targets that we're about to change
Target targetInstance = addr.getTarget(copy);
legal &= targetInstance.canTarget(actingPlayer.getId(), objId, addr.getSpellAbility(copy), game);
if (!legal) {
break;
}
// potential target must not be the thing that was targeted initially
targetInstance = addr.getTarget(spell);
legal &= !targetInstance.getTargets().contains(objId);
if (!legal) {
break;
}
}
legal &= okUUIDToCopyFor(objId, game, source, spell);
if (legal) {
for (TargetAddress addr : targetsToBeChanged) {
Target targetInstance = addr.getTarget(copy);
targetInstance.clearChosen();
targetInstance.add(objId, game);
}
if (!playerTargetCopyMap.containsKey(copy.getControllerId())) {
playerTargetCopyMap.put(copy.getControllerId(), new HashMap<>());
}
playerTargetCopyMap.get(copy.getControllerId()).put(objId, copy);
}
}
}
// allows controller of the copies to choose spells order on stack (by using targeting GUI)
for (Player player : game.getPlayers().values()) {
if (playerTargetCopyMap.containsKey(player.getId())) {
Map<UUID, Spell> targetCopyMap = playerTargetCopyMap.get(player.getId());
if (targetCopyMap != null) {
while (!targetCopyMap.isEmpty()) {
// all checks must be make for new copied spell, not original (controller can be changed)
Spell spellSample = targetCopyMap.values().stream().findFirst().get();
FilterInPlay<T> setFilter = filter.copy();
setFilter.add(new FromSetPredicate(targetCopyMap.keySet())); // allows only unselected targets
Target target = new TargetWithAdditionalFilter(sampleTarget, setFilter);
target.setNotTarget(false); // it is targeted, not chosen
target.setMinNumberOfTargets(0); // if not selected then it uses first target (see below), same for AI
target.setMaxNumberOfTargets(1);
target.setTargetName(filter.getMessage() + " that " + spellSample.getLogName()
+ " could target (" + targetCopyMap.size() + " remaining)");
if (targetCopyMap.size() > 1
&& target.canChoose(spellSample.getId(), player.getId(), game)) {
// The original "source" is not applicable here due to the spell being a copy. ie: Zada, Hedron Grinder
Outcome outcome = spellSample.getSpellAbility().getAllEffects().getOutcome(spellSample.getSpellAbility());
player.chooseTarget(outcome, target, spellSample.getSpellAbility(), game); // not source, but the spell that is copied
}
Collection<UUID> chosenIds = target.getTargets();
if (chosenIds.isEmpty()) {
// uses first target on cancel/non-selected
chosenIds = targetCopyMap.keySet();
}
List<UUID> toDelete = new ArrayList<>();
for (UUID chosenId : chosenIds) {
Spell chosenCopy = targetCopyMap.get(chosenId);
if (chosenCopy != null) {
// COPY DONE, can put to stack
chosenCopy.setZone(Zone.STACK, game);
game.getStack().push(chosenCopy);
game.fireEvent(new CopiedStackObjectEvent(spell, chosenCopy, source.getControllerId()));
toDelete.add(chosenId);
madeACopy = true;
}
}
targetCopyMap.keySet().removeAll((toDelete));
}
}
}
}
return madeACopy;
}
return false;
}
}
class CompoundFilter<T extends MageItem> extends FilterImpl<T> implements FilterInPlay<T> {
protected final FilterInPlay<T> filterA;
protected final FilterInPlay<T> filterB;
public CompoundFilter(String name) {
super(name);
this.filterA = null;
this.filterB = null;
}
public CompoundFilter(FilterInPlay<T> filterA, FilterInPlay<T> filterB, String name) {
super(name);
this.filterA = filterA;
this.filterB = filterB;
}
@Override
public boolean checkObjectClass(Object object) {
return true; // already checked in the filter classes itself
}
@Override
public boolean match(T obj, Game game) {
return (filterA == null
|| !filterA.match(obj, game))
&& (filterB == null
|| !filterB.match(obj, game));
}
@Override
public boolean match(T obj, UUID sourceId, UUID playerId, Game game) {
return (filterA == null
|| !filterA.match(obj, sourceId, playerId, game))
&& (filterB == null
|| !filterB.match(obj, sourceId, playerId, game));
}
@Override
public CompoundFilter copy() {
return new CompoundFilter(filterA == null ? null : filterA.copy(),
filterB == null ? null : filterB.copy(),
message);
}
}
class TargetWithAdditionalFilter<T extends MageItem> extends TargetImpl {
protected final FilterInPlay<T> additionalFilter;
protected final Target originalTarget;
public TargetWithAdditionalFilter(final TargetWithAdditionalFilter target) {
this(target.originalTarget, target.additionalFilter, false);
}
public TargetWithAdditionalFilter(Target originalTarget, FilterInPlay<T> additionalFilter) {
this(originalTarget, additionalFilter, false);
}
public TargetWithAdditionalFilter(Target originalTarget, FilterInPlay<T> additionalFilter, boolean notTarget) {
this.originalTarget = originalTarget.copy();
this.originalTarget.clearChosen();
this.targetName = originalTarget.getFilter().getMessage();
this.notTarget = notTarget;
this.additionalFilter = additionalFilter;
}
@Override
public Target getOriginalTarget() {
return originalTarget;
}
@Override
public int getNumberOfTargets() {
return originalTarget.getNumberOfTargets();
}
@Override
public int getMinNumberOfTargets() {
return originalTarget.getMinNumberOfTargets();
}
@Override
public void setMinNumberOfTargets(int minNumberOfTargets) {
originalTarget.setMinNumberOfTargets(minNumberOfTargets);
}
@Override
public int getMaxNumberOfTargets() {
return originalTarget.getMaxNumberOfTargets();
}
@Override
public void setMaxNumberOfTargets(int maxNumberOfTargets) {
originalTarget.setMaxNumberOfTargets(maxNumberOfTargets);
}
@Override
public Zone getZone() {
return originalTarget.getZone();
}
@Override
public boolean canTarget(UUID id, Game game) {
MageItem obj = game.getObject(id);
if (obj == null) {
obj = game.getPlayer(id);
}
try {
return obj != null
&& originalTarget.canTarget(id, game)
&& additionalFilter.match((T) obj, game);
} catch (ClassCastException e) {
return false; return false;
} }
} List<MageObjectReferencePredicate> predicates = getPossibleTargets(spell, actingPlayer, source, game);
spell.createCopyOnStack(
@Override game, source, actingPlayer.getId(), false,
public boolean canTarget(UUID id, Ability source, Game game) { predicates.size(), new CopyApplier(predicates)
MageItem obj = game.getObject(id); );
if (obj == null) { return true;
obj = game.getPlayer(id);
}
try {
return obj != null
&& originalTarget.canTarget(id, source, game)
&& additionalFilter.match((T) obj, source.getSourceId(), source.getControllerId(), game);
} catch (ClassCastException e) {
return false;
}
}
@Override
public boolean canTarget(UUID controllerId, UUID id, Ability source, Game game) {
MageItem obj = game.getObject(id);
if (obj == null) {
obj = game.getPlayer(id);
}
try {
return obj != null
&& originalTarget.canTarget(controllerId, id, source, game)
&& additionalFilter.match((T) obj, source.getSourceId(), controllerId, game);
} catch (ClassCastException e) {
return false;
}
}
@Override
public FilterInPlay<T> getFilter() {
return new CompoundFilter((FilterInPlay<T>) originalTarget.getFilter(),
additionalFilter, originalTarget.getFilter().getMessage());
}
@Override
public boolean canChoose(UUID sourceId, UUID sourceControllerId, Game game) {
int remainingTargets = getNumberOfTargets() - targets.size();
if (remainingTargets <= 0) {
return true;
}
int count = 0;
for (UUID objId : originalTarget.possibleTargets(sourceId, sourceControllerId, game)) {
MageItem obj = game.getObject(objId);
if (obj == null) {
obj = game.getPlayer(objId);
}
try {
if (!targets.containsKey(objId)
&& obj != null
&& additionalFilter.match((T) obj, sourceId, sourceControllerId, game)) {
count++;
if (count >= remainingTargets) {
return true;
}
}
} catch (ClassCastException e) {
}
}
return false;
}
@Override
public boolean canChoose(UUID sourceControllerId, Game game) {
int remainingTargets = getNumberOfTargets() - targets.size();
if (remainingTargets <= 0) {
return true;
}
int count = 0;
for (UUID objId : originalTarget.possibleTargets(sourceControllerId, game)) {
MageItem obj = game.getObject(objId);
if (obj == null) {
obj = game.getPlayer(objId);
}
try {
if (!targets.containsKey(objId)
&& obj != null
&& additionalFilter.match((T) obj, game)) {
count++;
if (count >= remainingTargets) {
return true;
}
}
} catch (ClassCastException e) {
}
}
return false;
}
@Override
public Set<UUID> possibleTargets(UUID sourceId, UUID sourceControllerId, Game game) {
Set<UUID> ret = new HashSet<>();
for (UUID id : originalTarget.possibleTargets(sourceId, sourceControllerId, game)) {
MageItem obj = game.getObject(id);
if (obj == null) {
obj = game.getPlayer(id);
}
try {
if (obj != null
&& additionalFilter.match((T) obj, sourceId, sourceControllerId, game)) {
ret.add(id);
}
} catch (ClassCastException e) {
}
}
return ret;
}
@Override
public Set<UUID> possibleTargets(UUID sourceControllerId, Game game) {
Set<UUID> ret = new HashSet<>();
for (UUID id : originalTarget.possibleTargets(sourceControllerId, game)) {
MageItem obj = game.getObject(id);
if (obj == null) {
obj = game.getPlayer(id);
}
try {
if (obj != null
&& additionalFilter.match((T) obj, game)) {
ret.add(id);
}
} catch (ClassCastException e) {
}
}
return ret;
}
@Override
public TargetWithAdditionalFilter copy() {
return new TargetWithAdditionalFilter(this);
}
@Override
public String getTargetedName(Game game) {
StringBuilder sb = new StringBuilder();
for (UUID targetId : getTargets()) {
MageObject object = game.getObject(targetId);
if (object != null) {
sb.append(object.getLogName()).append(' ');
} else {
Player player = game.getPlayer(targetId);
if (player != null) {
sb.append(player.getLogName()).append(' ');
}
}
}
return sb.toString().trim();
} }
} }

View file

@ -65,17 +65,7 @@ public class CopyTargetSpellEffect extends OneShotEffect {
spell = (Spell) game.getLastKnownInformation(targetPointer.getFirst(game, source), Zone.STACK); spell = (Spell) game.getLastKnownInformation(targetPointer.getFirst(game, source), Zone.STACK);
} }
if (spell != null) { if (spell != null) {
StackObject newStackObject = spell.createCopyOnStack(game, source, useController ? spell.getControllerId() : source.getControllerId(), chooseTargets); spell.createCopyOnStack(game, source, useController ? spell.getControllerId() : source.getControllerId(), chooseTargets);
Player player = game.getPlayer(source.getControllerId());
if (player != null && newStackObject instanceof Spell) {
String activateMessage = ((Spell) newStackObject).getActivatedMessage(game);
if (activateMessage.startsWith(" casts ")) {
activateMessage = activateMessage.substring(6);
}
if (!game.isSimulation()) {
game.informPlayers(player.getLogName() + activateMessage);
}
}
return true; return true;
} }
return false; return false;

View file

@ -1,26 +1,17 @@
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package mage.abilities.effects.common; package mage.abilities.effects.common;
import mage.MageObject; import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.common.delayed.AtTheBeginOfYourNextUpkeepDelayedTriggeredAbility; import mage.abilities.common.delayed.AtTheBeginOfYourNextUpkeepDelayedTriggeredAbility;
import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.abilities.effects.ContinuousRuleModifyingEffectImpl;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import java.util.Objects;
/** /**
* @author jeffwadsworth * @author jeffwadsworth
* <p> * <p>
@ -35,42 +26,36 @@ import java.util.Objects;
*/ */
public class EpicEffect extends OneShotEffect { public class EpicEffect extends OneShotEffect {
static final String rule = "<br>Epic <i>(For the rest of the game, you can't cast spells. At the beginning of each of your upkeeps for the rest of the game, copy this spell except for its epic ability. If the spell has targets, you may choose new targets for the copy)"; private static final String rule = "Epic <i>(For the rest of the game, you can't cast spells. " +
"At the beginning of each of your upkeeps for the rest of the game, copy this spell " +
"except for its epic ability. If the spell has targets, you may choose new targets for the copy)";
public EpicEffect() { public EpicEffect() {
super(Outcome.Benefit); super(Outcome.Benefit);
staticText = rule; staticText = "<br>" + rule;
} }
public EpicEffect(final EpicEffect effect) { private EpicEffect(final EpicEffect effect) {
super(effect); super(effect);
} }
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId()); Player controller = game.getPlayer(source.getControllerId());
if (controller != null) { if (controller == null) {
StackObject stackObject = game.getStack().getStackObject(source.getId()); return false;
Spell spell = (Spell) stackObject;
if (spell == null) {
return false;
}
spell = spell.copySpell(game, source, source.getControllerId()); // it's a fake copy, real copy with events in EpicPushEffect
// Remove Epic effect from the spell
Effect epicEffect = null;
for (Effect effect : spell.getSpellAbility().getEffects()) {
if (effect instanceof EpicEffect) {
epicEffect = effect;
break;
}
}
spell.getSpellAbility().getEffects().remove(epicEffect);
DelayedTriggeredAbility ability = new AtTheBeginOfYourNextUpkeepDelayedTriggeredAbility(new EpicPushEffect(spell, rule), Duration.EndOfGame, false);
game.addDelayedTriggeredAbility(ability, source);
game.addEffect(new EpicReplacementEffect(), source);
return true;
} }
return false; Spell spell = (Spell) source.getSourceObject(game);
if (spell == null) {
return false;
}
spell = spell.copy();
spell.getSpellAbility().getEffects().removeIf(EpicEffect.class::isInstance);
game.addDelayedTriggeredAbility(new AtTheBeginOfYourNextUpkeepDelayedTriggeredAbility(
new EpicPushEffect(spell, rule), Duration.EndOfGame, false
), source);
game.addEffect(new EpicReplacementEffect(), source);
return true;
} }
@Override @Override
@ -81,12 +66,12 @@ public class EpicEffect extends OneShotEffect {
class EpicReplacementEffect extends ContinuousRuleModifyingEffectImpl { class EpicReplacementEffect extends ContinuousRuleModifyingEffectImpl {
public EpicReplacementEffect() { EpicReplacementEffect() {
super(Duration.EndOfGame, Outcome.Neutral); super(Duration.EndOfGame, Outcome.Neutral);
staticText = "For the rest of the game, you can't cast spells"; staticText = "For the rest of the game, you can't cast spells";
} }
public EpicReplacementEffect(final EpicReplacementEffect effect) { private EpicReplacementEffect(final EpicReplacementEffect effect) {
super(effect); super(effect);
} }
@ -116,25 +101,22 @@ class EpicReplacementEffect extends ContinuousRuleModifyingEffectImpl {
@Override @Override
public boolean applies(GameEvent event, Ability source, Game game) { public boolean applies(GameEvent event, Ability source, Game game) {
if (Objects.equals(source.getControllerId(), event.getPlayerId())) { return source.isControlledBy(event.getPlayerId())
MageObject object = game.getObject(event.getSourceId()); && game.getObject(event.getSourceId()) != null;
return object != null;
}
return false;
} }
} }
class EpicPushEffect extends OneShotEffect { class EpicPushEffect extends OneShotEffect {
final private Spell spell; private final Spell spell;
public EpicPushEffect(Spell spell, String ruleText) { EpicPushEffect(Spell spell, String ruleText) {
super(Outcome.Copy); super(Outcome.Copy);
this.spell = spell; this.spell = spell;
staticText = ruleText; staticText = ruleText;
} }
public EpicPushEffect(final EpicPushEffect effect) { private EpicPushEffect(final EpicPushEffect effect) {
super(effect); super(effect);
this.spell = effect.spell; this.spell = effect.spell;
} }
@ -145,7 +127,6 @@ class EpicPushEffect extends OneShotEffect {
spell.createCopyOnStack(game, source, source.getControllerId(), true); spell.createCopyOnStack(game, source, source.getControllerId(), true);
return true; return true;
} }
return false; return false;
} }

View file

@ -22,7 +22,6 @@ import mage.filter.predicate.permanent.TappedPredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.stack.Spell; import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import mage.players.Player; import mage.players.Player;
import mage.target.common.TargetControlledPermanent; import mage.target.common.TargetControlledPermanent;
@ -269,10 +268,7 @@ class ConspireEffect extends OneShotEffect {
if (controller != null && conspiredSpell != null) { if (controller != null && conspiredSpell != null) {
Card card = game.getCard(conspiredSpell.getSourceId()); Card card = game.getCard(conspiredSpell.getSourceId());
if (card != null) { if (card != null) {
StackObject newStackObject = conspiredSpell.createCopyOnStack(game, source, source.getControllerId(), true); conspiredSpell.createCopyOnStack(game, source, source.getControllerId(), true);
if (newStackObject instanceof Spell && !game.isSimulation()) {
game.informPlayers(controller.getLogName() + ((Spell) newStackObject).getActivatedMessage(game));
}
return true; return true;
} }
} }

View file

@ -96,10 +96,10 @@ public class ReplicateAbility extends StaticAbility implements OptionalAdditiona
// canPay checks only single mana available, not total mana usage // canPay checks only single mana available, not total mana usage
if (additionalCost.canPay(ability, this, ability.getControllerId(), game) if (additionalCost.canPay(ability, this, ability.getControllerId(), game)
&& player.chooseUse(/*Outcome.Benefit*/Outcome.AIDontUseIt, && player.chooseUse(/*Outcome.Benefit*/Outcome.AIDontUseIt,
new StringBuilder("Pay ").append(times).append( new StringBuilder("Pay ").append(times).append(
additionalCost.getText(false)).append(" ?").toString(), ability, game)) { additionalCost.getText(false)).append(" ?").toString(), ability, game)) {
additionalCost.activate(); additionalCost.activate();
for (Iterator it = ((Costs) additionalCost).iterator(); it.hasNext();) { for (Iterator it = ((Costs) additionalCost).iterator(); it.hasNext(); ) {
Cost cost = (Cost) it.next(); Cost cost = (Cost) it.next();
if (cost instanceof ManaCostsImpl) { if (cost instanceof ManaCostsImpl) {
ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy());
@ -225,8 +225,7 @@ class ReplicateCopyEffect extends OneShotEffect {
} }
} }
// create the copies // create the copies
StackObject newStackObject = spell.createCopyOnStack(game, source, spell.createCopyOnStack(game, source, source.getControllerId(), true, replicateCount);
source.getControllerId(), true, replicateCount);
return true; return true;
} }

View file

@ -1,16 +1,17 @@
package mage.filter.predicate.mageobject; package mage.filter.predicate.mageobject;
import mage.MageItem;
import mage.MageObject; import mage.MageObject;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.filter.predicate.Predicate; import mage.filter.predicate.Predicate;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
/** /**
*
* @author TheElk801 * @author TheElk801
*/ */
public class MageObjectReferencePredicate implements Predicate<MageObject> { public class MageObjectReferencePredicate implements Predicate<MageItem> {
private final MageObjectReference mor; private final MageObjectReference mor;
@ -19,8 +20,23 @@ public class MageObjectReferencePredicate implements Predicate<MageObject> {
} }
@Override @Override
public boolean apply(MageObject input, Game game) { public boolean apply(MageItem input, Game game) {
return mor.refersTo(input, game); if (input instanceof Player) {
return mor.getSourceId().equals(input.getId());
}
return input instanceof MageObject && mor.refersTo((MageObject) input, game);
}
public String getName(Game game) {
Permanent permanent = mor.getPermanent(game);
if (permanent != null) {
return permanent.getIdName();
}
Player player = game.getPlayer(mor.getSourceId());
if (player != null) {
return player.getName();
}
return null;
} }
@Override @Override

View file

@ -13,10 +13,14 @@ import mage.abilities.keyword.BestowAbility;
import mage.abilities.keyword.MorphAbility; import mage.abilities.keyword.MorphAbility;
import mage.abilities.text.TextPart; import mage.abilities.text.TextPart;
import mage.cards.*; import mage.cards.*;
import mage.choices.Choice;
import mage.choices.ChoiceImpl;
import mage.constants.*; import mage.constants.*;
import mage.counters.Counter; import mage.counters.Counter;
import mage.counters.Counters; import mage.counters.Counters;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game; import mage.game.Game;
import mage.game.GameState; import mage.game.GameState;
import mage.game.MageObjectAttribute; import mage.game.MageObjectAttribute;
@ -32,9 +36,11 @@ import mage.util.CardUtil;
import mage.util.GameLog; import mage.util.GameLog;
import mage.util.ManaUtil; import mage.util.ManaUtil;
import mage.util.SubTypes; import mage.util.SubTypes;
import mage.util.functions.SpellCopyApplier;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -1057,27 +1063,128 @@ public class Spell extends StackObjImpl implements Card {
} }
@Override @Override
public StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) { public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) {
return createCopyOnStack(game, source, newControllerId, chooseNewTargets, 1); createCopyOnStack(game, source, newControllerId, chooseNewTargets, 1);
} }
@Override @Override
public StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount) { public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets, int amount) {
Spell spellCopy = null; 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); GameEvent gameEvent = new CopyStackObjectEvent(source, this, newControllerId, amount);
if (game.replaceEvent(gameEvent)) { 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++) { 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 spellCopy.setZone(Zone.STACK, game); // required for targeting ex: Nivmagus Elemental
game.getStack().push(spellCopy); 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); spellCopy.chooseNewTargets(game, newControllerId);
} }
game.fireEvent(new CopiedStackObjectEvent(this, spellCopy, 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 @Override

View file

@ -1,6 +1,9 @@
package mage.game.stack; package mage.game.stack;
import mage.*; import mage.MageIdentifier;
import mage.MageInt;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.*; import mage.abilities.*;
import mage.abilities.costs.Cost; import mage.abilities.costs.Cost;
import mage.abilities.costs.CostAdjuster; import mage.abilities.costs.CostAdjuster;
@ -27,8 +30,10 @@ import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.Targets; import mage.target.Targets;
import mage.target.targetadjustment.TargetAdjuster; import mage.target.targetadjustment.TargetAdjuster;
import mage.util.CardUtil;
import mage.util.GameLog; import mage.util.GameLog;
import mage.util.SubTypes; import mage.util.SubTypes;
import mage.util.functions.SpellCopyApplier;
import mage.watchers.Watcher; import mage.watchers.Watcher;
import java.util.ArrayList; import java.util.ArrayList;
@ -593,15 +598,20 @@ public class StackAbility extends StackObjImpl implements Ability {
} }
@Override @Override
public StackObject createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) { public void createCopyOnStack(Game game, Ability source, UUID newControllerId, boolean chooseNewTargets) {
return createCopyOnStack(game, source, newControllerId, chooseNewTargets, 1); 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; StackAbility newStackAbility = null;
GameEvent gameEvent = new CopyStackObjectEvent(source, this, newControllerId, amount); GameEvent gameEvent = new CopyStackObjectEvent(source, this, newControllerId, amount);
if (game.replaceEvent(gameEvent)) { if (game.replaceEvent(gameEvent)) {
return null; return;
} }
for (int i = 0; i < gameEvent.getAmount(); i++) { for (int i = 0; i < gameEvent.getAmount(); i++) {
Ability newAbility = this.copy(); Ability newAbility = this.copy();
@ -618,7 +628,13 @@ public class StackAbility extends StackObjImpl implements Ability {
} }
game.fireEvent(new CopiedStackObjectEvent(this, newStackAbility, newControllerId)); 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 @Override

View file

@ -7,13 +7,14 @@ import mage.abilities.Ability;
import mage.abilities.Mode; import mage.abilities.Mode;
import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.dynamicvalue.common.StaticValue;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.filter.FilterPermanent; import mage.filter.predicate.Predicate;
import mage.game.Game; import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetAmount; import mage.target.TargetAmount;
import java.util.Collection;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -94,12 +95,12 @@ public abstract class StackObjImpl implements StackObject {
* targetId * targetId
* @param onlyOneTarget - 114.6b one target must be changed to another * @param onlyOneTarget - 114.6b one target must be changed to another
* target * target
* @param filterNewTarget restriction for the new target, if null nothing is * @param extraPredicate restriction for the new target, if null nothing is
* cheched * cheched
* @return * @return
*/ */
@Override @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); Player targetController = game.getPlayer(targetControllerId);
if (targetController != null) { if (targetController != null) {
StringBuilder oldTargetDescription = new StringBuilder(); StringBuilder oldTargetDescription = new StringBuilder();
@ -118,7 +119,7 @@ public abstract class StackObjImpl implements StackObject {
ability.getModes().setActiveMode(mode); ability.getModes().setActiveMode(mode);
oldTargetDescription.append(ability.getTargetDescription(mode.getTargets(), game)); oldTargetDescription.append(ability.getTargetDescription(mode.getTargets(), game));
for (Target target : mode.getTargets()) { 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 // clear the old target and copy all targets from new target
target.clearChosen(); target.clearChosen();
for (UUID targetId : newTarget.getTargets()) { for (UUID targetId : newTarget.getTargets()) {
@ -149,8 +150,13 @@ public abstract class StackObjImpl implements StackObject {
* @param game * @param game
* @return * @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(); 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); newTarget.setEventReporting(false);
if (!targetController.getId().equals(getControllerId())) { if (!targetController.getId().equals(getControllerId())) {
newTarget.setTargetController(targetController.getId()); // target controller for the change is different from spell controller 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); 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) // workaround to stop infinite AI choose (remove after chooseTarget can be called with extra filter to disable some ids)
if (iteration > 10) { if (iteration > 10) {
break; break;
@ -200,6 +197,11 @@ public abstract class StackObjImpl implements StackObject {
} else { } else {
// build a target definition with exactly one possible target to select that replaces old target // build a target definition with exactly one possible target to select that replaces old target
Target tempTarget = target.copy(); 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); tempTarget.setEventReporting(false);
if (target instanceof TargetAmount) { if (target instanceof TargetAmount) {
((TargetAmount) tempTarget).setAmountDefinition(StaticValue.get(target.getTargetAmount(targetId))); ((TargetAmount) tempTarget).setAmountDefinition(StaticValue.get(target.getTargetAmount(targetId)));
@ -242,12 +244,6 @@ public abstract class StackObjImpl implements StackObject {
// keep the old // keep the old
newTarget.addTarget(targetId, target.getTargetAmount(targetId), ability, game, true); 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 { } else {
// valid target was selected, add it to the new target definition // valid target was selected, add it to the new target definition
newTarget.addTarget(tempTarget.getFirstTarget(), target.getTargetAmount(targetId), ability, game, true); newTarget.addTarget(tempTarget.getFirstTarget(), target.getTargetAmount(targetId), ability, game, true);
@ -263,6 +259,30 @@ public abstract class StackObjImpl implements StackObject {
return newTarget; 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) { private String getNamesOftargets(UUID targetId, Game game) {
MageObject object = game.getObject(targetId); MageObject object = game.getObject(targetId);
String name = null; String name = null;

View file

@ -4,9 +4,10 @@ import mage.MageObject;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.constants.Zone; import mage.constants.Zone;
import mage.constants.ZoneDetail; import mage.constants.ZoneDetail;
import mage.filter.FilterPermanent; import mage.filter.predicate.Predicate;
import mage.game.Controllable; import mage.game.Controllable;
import mage.game.Game; import mage.game.Game;
import mage.util.functions.SpellCopyApplier;
import java.util.UUID; import java.util.UUID;
@ -17,7 +18,6 @@ public interface StackObject extends MageObject, Controllable {
UUID getSourceId(); UUID getSourceId();
/** /**
*
* @param source null for fizzled events (sourceId will be null) * @param source null for fizzled events (sourceId will be null)
* @param game * @param game
*/ */
@ -27,11 +27,15 @@ public interface StackObject extends MageObject, Controllable {
Ability getStackAbility(); 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(); boolean isTargetChanged();

View file

@ -0,0 +1,17 @@
package mage.util.functions;
import mage.filter.predicate.mageobject.MageObjectReferencePredicate;
import mage.game.Game;
import mage.game.stack.Spell;
import java.io.Serializable;
/**
* @author TheElk801
*/
public interface SpellCopyApplier extends Serializable {
void modifySpell(Spell spell, Game game);
MageObjectReferencePredicate getNextPredicate();
}