refactor: remove notTarget targets from abilities (#13651)

Adds OneShotNonTargetEffect and PutCountersTargetCost
Fixes the target timing of spells/abilities, as non-targets should be chosen on resolution.
This commit is contained in:
ssk97 2025-05-31 20:36:15 -07:00 committed by GitHub
parent 8b2a81cb42
commit 3223d99b2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 449 additions and 384 deletions

View file

@ -1749,6 +1749,10 @@ public abstract class AbilityImpl implements Ability {
@Override
public AbilityImpl setTargetAdjuster(TargetAdjuster targetAdjuster) {
if (targetAdjuster == null) {
this.targetAdjuster = null;
return this;
}
if (targetAdjuster instanceof GenericTargetAdjuster && this.getTargets().isEmpty()) {
throw new IllegalStateException("Target adjuster being added but no targets are set!");
}

View file

@ -3,6 +3,7 @@ package mage.abilities.common;
import mage.MageObject;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent;
@ -18,7 +19,7 @@ import mage.target.targetpointer.FixedTarget;
public class DiesCreatureTriggeredAbility extends TriggeredAbilityImpl {
protected FilterPermanent filter;
private boolean setTargetPointer;
private SetTargetPointer setTargetPointer;
public DiesCreatureTriggeredAbility(Effect effect, boolean optional) {
this(effect, optional, false);
@ -33,7 +34,7 @@ public class DiesCreatureTriggeredAbility extends TriggeredAbilityImpl {
if (another) {
filter.add(AnotherPredicate.instance);
}
this.setTargetPointer = setTargetPointer;
this.setTargetPointer = (setTargetPointer ? SetTargetPointer.PERMANENT : SetTargetPointer.NONE);
}
public DiesCreatureTriggeredAbility(Effect effect, boolean optional, FilterPermanent filter) {
@ -44,7 +45,14 @@ public class DiesCreatureTriggeredAbility extends TriggeredAbilityImpl {
this(Zone.BATTLEFIELD, effect, optional, filter, setTargetPointer);
}
public DiesCreatureTriggeredAbility(Effect effect, SetTargetPointer setTargetPointer) {
this(Zone.BATTLEFIELD, effect, false, new FilterCreaturePermanent("a creature"), setTargetPointer);
}
public DiesCreatureTriggeredAbility(Zone zone, Effect effect, boolean optional, FilterPermanent filter, boolean setTargetPointer) {
this(zone, effect, optional, filter, (setTargetPointer ? SetTargetPointer.PERMANENT : SetTargetPointer.NONE));
}
public DiesCreatureTriggeredAbility(Zone zone, Effect effect, boolean optional, FilterPermanent filter, SetTargetPointer setTargetPointer) {
super(zone, effect, optional);
this.filter = filter;
this.setTargetPointer = setTargetPointer;
@ -75,8 +83,17 @@ public class DiesCreatureTriggeredAbility extends TriggeredAbilityImpl {
return false;
}
getEffects().setValue("creatureDied", zEvent.getTarget());
if (setTargetPointer) {
this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game));
switch (setTargetPointer) {
case PLAYER:
this.getAllEffects().setTargetPointer(new FixedTarget(event.getPlayerId(), game));
break;
case PERMANENT:
this.getAllEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game));
break;
case NONE:
break;
default:
throw new IllegalArgumentException("Unsupported SetTargetPointer in DiesCreatureTriggeredAbility");
}
return true;
}

View file

@ -0,0 +1,63 @@
package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.constants.Outcome;
import mage.counters.Counter;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetControlledPermanent;
import java.util.UUID;
/**
* @author notgreat
*/
public class PutCountersTargetCost extends CostImpl {
private final Counter counter;
public PutCountersTargetCost(Counter counter){
this(counter, new TargetControlledCreaturePermanent());
}
public PutCountersTargetCost(Counter counter, TargetControlledPermanent target) {
this.counter = counter.copy();
target.withNotTarget(true);
this.addTarget(target);
this.text = "put " + counter.getDescription() + " on " + target.getDescription();
}
public PutCountersTargetCost(PutCountersTargetCost cost) {
super(cost);
this.counter = cost.counter.copy();
}
public PutCountersTargetCost copy() {
return new PutCountersTargetCost(this);
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
Player player = game.getPlayer(ability.getControllerId());
if (player == null || !this.getTargets().choose(Outcome.Exile, controllerId, source.getSourceId(), source, game)) {
return paid;
}
for (UUID targetId : this.getTargets().get(0).getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent == null) {
return false;
}
paid |= permanent.addCounters(counter, controllerId, ability, game);
}
return paid;
}
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
return this.getTargets().canChoose(controllerId, source, game);
}
}

View file

@ -0,0 +1,80 @@
package mage.abilities.effects;
import mage.abilities.Ability;
import mage.game.Game;
import mage.target.Target;
import mage.target.targetadjustment.TargetAdjuster;
import mage.target.targetpointer.TargetPointer;
/**
* @author notgreat
*/
public class OneShotNonTargetEffect extends OneShotEffect {
OneShotEffect effect;
Target notTarget;
TargetAdjuster adjuster;
public OneShotNonTargetEffect(OneShotEffect effect, Target notTarget) {
this(effect, notTarget, null);
}
public OneShotNonTargetEffect(OneShotEffect effect, Target notTarget, TargetAdjuster adjuster) {
super(effect.outcome);
this.effect = effect;
this.notTarget = notTarget;
this.notTarget.withNotTarget(true);
this.adjuster = adjuster;
if (effect.staticText == null || effect.staticText.equals("")){
throw new IllegalArgumentException("Effect must use static text");
}
this.setText(effect.staticText);
}
private OneShotNonTargetEffect(OneShotNonTargetEffect eff) {
super(eff);
this.effect = eff.effect.copy();
this.notTarget = eff.notTarget.copy();
this.adjuster = eff.adjuster;
}
public OneShotNonTargetEffect copy() {
return new OneShotNonTargetEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
boolean result = false;
Target target = notTarget.copy();
if (source.getTargetAdjuster() != null || !source.getTargets().isEmpty()){
throw new IllegalStateException("source ability already has target but is using OneShotNonTargetEffect");
}
source.addTarget(target);
if (adjuster != null) {
adjuster.clearDefaultTargets();
source.setTargetAdjuster(adjuster);
source.adjustTargets(game);
source.setTargetAdjuster(null);
}
if (source.getTargets().choose(outcome, source.getControllerId(), source.getId(), source, game)) {
result = effect.apply(game, source);
}
source.getTargets().clear();
return result;
}
@Override
public OneShotEffect setTargetPointer(TargetPointer targetPointer) {
if (targetPointer == null) {
return null;
}
effect.setTargetPointer(targetPointer);
return super.setTargetPointer(targetPointer);
}
@Override
public void setValue(String key, Object value) {
effect.setValue(key, value);
super.setValue(key, value);
}
}

View file

@ -71,6 +71,10 @@ public class ConditionalTargetAdjuster implements TargetAdjuster {
blueprintTarget = ability.getTargets().get(0).copy();
}
}
@Override
public void clearDefaultTargets() {
blueprintTarget = null;
}
@Override
public void adjustTargets(Ability ability, Game game) {

View file

@ -14,4 +14,8 @@ public abstract class GenericTargetAdjuster implements TargetAdjuster {
throw new IllegalStateException("Wrong code usage: target adjuster already has blueprint target - " + blueprintTarget);
}
}
@Override
public void clearDefaultTargets() {
blueprintTarget = null;
}
}

View file

@ -20,4 +20,7 @@ public interface TargetAdjuster extends Serializable {
*/
default void addDefaultTargets(Ability ability) {
}
default void clearDefaultTargets() {
}
}