[FIC] Implement G'raha Tia, Scion Reborn, rework DoIfCostPaid and "do only once" effects (#13660)

* rework effects with DoIfCostPaid and "do this only once each turn"

* [FIC] Implement G'raha Tia, Scion Reborn

* [FIC] Implement Emet Selch of the Third Seat

* rework Emet-Selch

* add test

* add static methods to handle whether ability was used this turn
This commit is contained in:
Evan Kranzler 2025-05-30 21:28:11 -04:00 committed by Failure
parent 71c4be03fb
commit 40d24869a8
11 changed files with 361 additions and 50 deletions

View file

@ -3,7 +3,9 @@ package mage.abilities;
import mage.abilities.condition.Condition;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.util.CardUtil;
import java.util.Optional;
import java.util.UUID;
/**
@ -97,8 +99,6 @@ public interface TriggeredAbility extends Ability {
boolean isOptional();
TriggeredAbility setOptional();
/**
* Allow trigger to fire after source leave the battlefield (example: will use LKI on itself sacrifice)
*/
@ -131,4 +131,34 @@ public interface TriggeredAbility extends Ability {
TriggeredAbility setTriggerPhrase(String triggerPhrase);
String getTriggerPhrase();
static String makeDidThisTurnString(Ability ability, Game game) {
return CardUtil.getCardZoneString("lastTurnUsed" + ability.getOriginalId(), ability.getSourceId(), game);
}
static void setDidThisTurn(Ability ability, Game game) {
game.getState().setValue(makeDidThisTurnString(ability, game), game.getTurnNum());
}
/**
* For abilities which say "Do this only once each turn".
* Most of the time this is handled automatically by calling setDoOnlyOnceEachTurn(true),
* but sometimes the ability will need a way to clear whether it's been used this turn within an effect.
*
* @param ability
* @param game
*/
static void clearDidThisTurn(Ability ability, Game game) {
game.getState().removeValue(makeDidThisTurnString(ability, game));
}
static boolean checkDidThisTurn(Ability ability, Game game) {
return Optional
.ofNullable(makeDidThisTurnString(ability, game))
.map(game.getState()::getValue)
.filter(Integer.class::isInstance)
.map(Integer.class::cast)
.filter(x -> x == game.getTurnNum())
.isPresent();
}
}

View file

@ -156,13 +156,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
@Override
public boolean checkUsedAlready(Game game) {
if (!doOnlyOnceEachTurn) {
return false;
}
Integer lastTurnUsed = (Integer) game.getState().getValue(
CardUtil.getCardZoneString("lastTurnUsed" + getOriginalId(), sourceId, game)
);
return lastTurnUsed != null && lastTurnUsed == game.getTurnNum();
return doOnlyOnceEachTurn && TriggeredAbility.checkDidThisTurn(this, game);
}
@Override
@ -209,7 +203,9 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
@Override
public TriggeredAbility setDoOnlyOnceEachTurn(boolean doOnlyOnce) {
this.doOnlyOnceEachTurn = doOnlyOnce;
setOptional();
if (CardUtil.castStream(this.getAllEffects(), DoIfCostPaid.class).noneMatch(DoIfCostPaid::isOptional)) {
this.optional = true;
}
return this;
}
@ -268,11 +264,9 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
)) {
return false;
}
}
if (doOnlyOnceEachTurn) {
game.getState().setValue(CardUtil.getCardZoneString(
"lastTurnUsed" + getOriginalId(), sourceId, game
), game.getTurnNum());
if (doOnlyOnceEachTurn) {
TriggeredAbility.setDidThisTurn(this, game);
}
}
//20091005 - 603.4
if (!super.resolve(game)) {
@ -387,6 +381,7 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
private static boolean startsWithVerb(String ruleLow) {
return ruleLow.startsWith("attach")
|| ruleLow.startsWith("cast")
|| ruleLow.startsWith("change")
|| ruleLow.startsWith("counter")
|| ruleLow.startsWith("create")
@ -501,20 +496,6 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
return optional;
}
@Override
public TriggeredAbility setOptional() {
this.optional = true;
if (getEffects().stream().anyMatch(
effect -> effect instanceof DoIfCostPaid && ((DoIfCostPaid) effect).isOptional())) {
throw new IllegalArgumentException(
"DoIfCostPaid effect must have only one optional settings, but it have two (trigger + DoIfCostPaid): "
+ this.getClass().getSimpleName());
}
return this;
}
@Override
public TriggeredAbilityImpl setAbilityWord(AbilityWord abilityWord) {
super.setAbilityWord(abilityWord);

View file

@ -41,7 +41,11 @@ public class AttacksWithCreaturesTriggeredAbility extends TriggeredAbilityImpl {
}
public AttacksWithCreaturesTriggeredAbility(Zone zone, Effect effect, int minAttackers, FilterPermanent filter, boolean setTargetPointer) {
super(zone, effect);
this(zone, effect, minAttackers, filter, setTargetPointer, false);
}
public AttacksWithCreaturesTriggeredAbility(Zone zone, Effect effect, int minAttackers, FilterPermanent filter, boolean setTargetPointer, boolean optional) {
super(zone, effect, optional);
this.filter = filter;
this.minAttackers = minAttackers;
this.setTargetPointer = setTargetPointer;

View file

@ -3,6 +3,7 @@ package mage.abilities.effects.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.TriggeredAbility;
import mage.abilities.costs.Cost;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.Effect;
@ -110,6 +111,7 @@ public class DoIfCostPaid extends OneShotEffect {
didPay = true;
game.informPlayers(player.getLogName() + " paid for " + mageObject.getLogName() + " - " + message);
applyEffects(game, source, executingEffects);
TriggeredAbility.setDidThisTurn(source, game);
player.resetStoredBookmark(game); // otherwise you can e.g. undo card drawn with Mentor of the Meek
} else {
// Paying cost was cancels so try to undo payment so far