Alternative solution to problem of unplayable cards from target adjustment (#12842)

* Alternative solution to problem of unplayable cards from target adjustment

* Review fixes
This commit is contained in:
ssk97 2024-10-19 19:13:39 -07:00 committed by GitHub
parent d293200198
commit f2ff4828b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 117 additions and 34 deletions

View file

@ -40,9 +40,9 @@ public final class BloodchiefsThirst extends CardImpl {
"Destroy target creature or planeswalker with mana value 2 or less. " +
"If this spell was kicked, instead destroy target creature or planeswalker."
));
this.getSpellAbility().addTarget(new TargetPermanent(filter));
this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker());
this.getSpellAbility().setTargetAdjuster(new ConditionalTargetAdjuster(KickedCondition.ONCE,
new TargetCreatureOrPlaneswalker()));
new TargetPermanent(filter), new TargetCreatureOrPlaneswalker()));
}
private BloodchiefsThirst(final BloodchiefsThirst card) {

View file

@ -43,9 +43,9 @@ public final class ExpelTheUnworthy extends CardImpl {
this.getSpellAbility().addEffect(new InfoEffect("Choose target creature with mana value 3 or less. If this spell was kicked, instead choose target creature."));
this.getSpellAbility().addEffect(new ExileTargetEffect().setText("Exile the chosen creature"));
this.getSpellAbility().addEffect(new ExpelTheUnworthyEffect());
this.getSpellAbility().addTarget(new TargetCreaturePermanent(filter));
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
this.getSpellAbility().setTargetAdjuster(new ConditionalTargetAdjuster(KickedCondition.ONCE,
new TargetCreaturePermanent()));
new TargetCreaturePermanent(filter), new TargetCreaturePermanent()));
}
private ExpelTheUnworthy(final ExpelTheUnworthy card) {

View file

@ -8,6 +8,7 @@ import mage.abilities.keyword.KickerAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.target.common.TargetAnyTarget;
import mage.target.common.TargetAnyTargetAmount;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetadjustment.ConditionalTargetAdjuster;
@ -35,9 +36,9 @@ public final class FightWithFire extends CardImpl {
+ "it deals 10 damage divided as you choose among any number of targets instead."
+ "<i> (Those targets can include players and planeswalkers.)</i>"
));
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
this.getSpellAbility().addTarget(new TargetAnyTarget());
this.getSpellAbility().setTargetAdjuster(new ConditionalTargetAdjuster(KickedCondition.ONCE,
new TargetAnyTargetAmount(10)));
new TargetCreaturePermanent(), new TargetAnyTargetAmount(10)));
}
private FightWithFire(final FightWithFire card) {

View file

@ -16,6 +16,7 @@ import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPlayer;
import mage.target.common.TargetCreatureOrPlayer;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetadjustment.ConditionalTargetAdjuster;
@ -42,9 +43,9 @@ public final class GaladrielsDismissal extends CardImpl {
KickedCondition.ONCE, "Target creature phases out. If this spell was kicked, each creature target player controls phases out instead. " +
"<i>(Treat phased-out creatures and anything attached to them as though they don't exist until their controller's next turn.)</i>"
));
this.getSpellAbility().addTarget(new TargetCreaturePermanent());
this.getSpellAbility().addTarget(new TargetCreatureOrPlayer());
this.getSpellAbility().setTargetAdjuster(new ConditionalTargetAdjuster(KickedCondition.ONCE,
new TargetPlayer()));
new TargetCreaturePermanent(), new TargetPlayer()));
}
private GaladrielsDismissal(final GaladrielsDismissal card) {

View file

@ -7,7 +7,10 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.GiftType;
import mage.constants.TargetController;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.predicate.Predicates;
import mage.target.TargetPermanent;
import mage.target.common.TargetOpponentsCreaturePermanent;
import mage.target.targetadjustment.ConditionalTargetAdjuster;
@ -18,6 +21,15 @@ import java.util.UUID;
* @author TheElk801
*/
public final class IntoTheFloodMaw extends CardImpl {
private static final FilterPermanent playableFilter = new FilterPermanent("creature or nonland permanent");
static {
playableFilter.add(TargetController.OPPONENT.getControllerPredicate());
playableFilter.add(Predicates.or(
CardType.CREATURE.getPredicate(),
Predicates.not(CardType.LAND.getPredicate())
));
}
public IntoTheFloodMaw(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{U}");
@ -29,9 +41,9 @@ public final class IntoTheFloodMaw extends CardImpl {
this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()
.setText("return target creature an opponent controls to its owner's hand. If the gift was promise, " +
"instead return target nonland permanent an opponent controls to its owner's hand"));
this.getSpellAbility().addTarget(new TargetOpponentsCreaturePermanent());
this.getSpellAbility().addTarget(new TargetPermanent(playableFilter));
this.getSpellAbility().setTargetAdjuster(new ConditionalTargetAdjuster(GiftWasPromisedCondition.TRUE,
new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_NON_LAND)));
new TargetOpponentsCreaturePermanent(), new TargetPermanent(StaticFilters.FILTER_OPPONENTS_PERMANENT_NON_LAND)));
}
private IntoTheFloodMaw(final IntoTheFloodMaw card) {

View file

@ -27,9 +27,9 @@ public final class LongRiversPull extends CardImpl {
// Counter target creature spell. If the gift was promised, instead counter target spell.
this.getSpellAbility().addEffect(new CounterTargetEffect()
.setText("counter target creature spell. If the gift was promised, instead counter target spell"));
this.getSpellAbility().addTarget(new TargetSpell(StaticFilters.FILTER_SPELL_CREATURE));
this.getSpellAbility().addTarget(new TargetSpell());
this.getSpellAbility().setTargetAdjuster(new ConditionalTargetAdjuster(GiftWasPromisedCondition.TRUE,
new TargetSpell()));
new TargetSpell(StaticFilters.FILTER_SPELL_CREATURE), new TargetSpell()));
}
private LongRiversPull(final LongRiversPull card) {

View file

@ -10,7 +10,6 @@ import mage.constants.CardType;
import mage.filter.StaticFilters;
import mage.target.common.TargetNonlandPermanent;
import mage.target.targetadjustment.ConditionalTargetAdjuster;
import mage.target.targetpointer.EachTargetPointer;
import java.util.UUID;
@ -27,7 +26,6 @@ public final class RushingRiver extends CardImpl {
// Return target nonland permanent to its owner's hand. If Rushing River was kicked, return another target nonland permanent to its owner's hand.
this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()
.setTargetPointer(new EachTargetPointer())
.setText("Return target nonland permanent to its owner's hand. " +
"If this spell was kicked, return another target nonland permanent to its owner's hand"));
this.getSpellAbility().addTarget(new TargetNonlandPermanent());

View file

@ -6,7 +6,9 @@ import mage.abilities.keyword.KickerAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.predicate.Predicates;
import mage.target.TargetPermanent;
import mage.target.common.TargetNonlandPermanent;
import mage.target.targetadjustment.ConditionalTargetAdjuster;
@ -17,6 +19,15 @@ import java.util.UUID;
* @author TheElk801
*/
public final class TearAsunder extends CardImpl {
private static final FilterPermanent playableFilter = new FilterPermanent("artifact, enchantment, or nonland permanent");
static {
playableFilter.add(Predicates.or(
CardType.ARTIFACT.getPredicate(),
CardType.ENCHANTMENT.getPredicate(),
Predicates.not(CardType.LAND.getPredicate())
));
}
public TearAsunder(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}");
@ -26,9 +37,9 @@ public final class TearAsunder extends CardImpl {
// Exile target artifact or enchantment. If this spell was kicked, exile target nonland permanent instead.
this.getSpellAbility().addEffect(new ExileTargetEffect().setText("Exile target artifact or enchantment. If this spell was kicked, exile target nonland permanent instead."));
this.getSpellAbility().addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT));
this.getSpellAbility().addTarget(new TargetPermanent(playableFilter));
this.getSpellAbility().setTargetAdjuster(new ConditionalTargetAdjuster(KickedCondition.ONCE,
new TargetNonlandPermanent()));
new TargetPermanent(StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT), new TargetNonlandPermanent()));
}
private TearAsunder(final TearAsunder card) {

View file

@ -215,4 +215,26 @@ public class GiftTest extends CardTestPlayerBase {
assertHandCount(playerA, 1);
assertHandCount(playerB, 1);
}
//Test Conditional Target Adjuster allowing more generic casts
@Test
public void testLongRiversPull() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.HAND, playerA, "Ponder");
addCard(Zone.HAND, playerA, "Long River's Pull"); // UU, counter noncreature only if gift
setChoice(playerA, true);
setChoice(playerA, playerB.getName());
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ponder");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Long River's Pull");
addTarget(playerA, "Ponder");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.POSTCOMBAT_MAIN);
execute();
assertHandCount(playerA, 0);
assertGraveyardCount(playerA, 2);
assertHandCount(playerB, 1);
}
}

View file

@ -4,48 +4,85 @@ import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
import mage.target.Target;
import mage.target.Targets;
/**
* @author notgreat
*/
public class ConditionalTargetAdjuster implements TargetAdjuster {
private final Condition condition;
private final boolean keepExistingTargets;
private final Targets replacementTargets;
private final boolean keepBlueprintTarget;
private final Target replacementTarget;
private Target blueprintTarget;
/**
* If the condition is true, replace the target
* If the condition is true, replace the ability's target.
* <p>
* Note that if the conditional target can be found when the original can not,
* you must use the two-target constructor and give the ability a separate general target
* that can encompass both targets
*
* @param condition The condition to be checked
* @param replacementTarget The target to use if the condition is true.
*/
public ConditionalTargetAdjuster(Condition condition, Target replacementTarget) {
this(condition, false, replacementTarget);
this(condition, null, false, replacementTarget);
}
/**
* If the condition is true, change the target list with multiple targets at once
* If the condition is true, add another target to the ability
*
* @param condition The condition to be checked
* @param keepExistingTargets if true, don't clear existing targets when adding the new ones
* @param replacementTargets Targets to be added if the condition is true
* @param condition The condition to be checked
* @param keepBlueprintTarget if true, don't remove the original target when adding the new one
* @param replacementTarget The target to use if the condition is true.
*/
public ConditionalTargetAdjuster(Condition condition, boolean keepExistingTargets, Target... replacementTargets) {
public ConditionalTargetAdjuster(Condition condition, boolean keepBlueprintTarget, Target replacementTarget) {
this(condition, null, keepBlueprintTarget, replacementTarget);
}
/**
* If the condition is false, use the blueprint. If the condition is true, use the replacement target.
*
* @param condition The condition to be checked
* @param blueprintTarget The blueprint/original target to use (set to null to use the ability's first target)
* @param replacementTarget The target to use if the condition is true.
*/
public ConditionalTargetAdjuster(Condition condition, Target blueprintTarget, Target replacementTarget) {
this(condition, blueprintTarget, false, replacementTarget);
}
/**
* If the condition is false, use the blueprint. If the condition is true, add or use the replacement target.
*
* @param condition The condition to be checked
* @param blueprintTarget The blueprint/original target to use (set to null to use the ability's first target)
* @param keepBlueprintTarget if true, don't remove the original target when adding the new one
* @param replacementTarget Target to be added if the condition is true
*/
public ConditionalTargetAdjuster(Condition condition, Target blueprintTarget, boolean keepBlueprintTarget, Target replacementTarget) {
this.condition = condition;
this.keepExistingTargets = keepExistingTargets;
this.replacementTargets = new Targets(replacementTargets);
this.keepBlueprintTarget = keepBlueprintTarget;
this.blueprintTarget = blueprintTarget;
this.replacementTarget = replacementTarget;
}
@Override
public void addDefaultTargets(Ability ability) {
if (blueprintTarget == null && !ability.getTargets().isEmpty()) {
blueprintTarget = ability.getTargets().get(0).copy();
}
}
@Override
public void adjustTargets(Ability ability, Game game) {
if (condition.apply(game, ability)) {
if (!keepExistingTargets) {
ability.getTargets().clear();
}
for (Target target : replacementTargets) {
ability.addTarget(target.copy());
ability.getTargets().clear();
boolean result = condition.apply(game, ability);
if (keepBlueprintTarget || !result) {
if (blueprintTarget != null) {
ability.addTarget(blueprintTarget.copy());
}
}
if (result) {
ability.addTarget(replacementTarget.copy());
}
}
}

View file

@ -12,6 +12,7 @@ import java.io.Serializable;
public interface TargetAdjuster extends Serializable {
// Warning: This is not Copyable, do not use changeable data inside (only use static objects like Filter)
// Note: in playability check for cards, targets are not adjusted.
void adjustTargets(Ability ability, Game game);
/**