fix [OTJ] Fortune, Loyal Steed — DelayedAbility's zcc was wrong when started from another trigger (#12154)

This commit is contained in:
Susucre 2024-05-04 19:26:11 +02:00 committed by GitHub
parent fa728eafb1
commit d8959f1588
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 312 additions and 73 deletions

View file

@ -1,9 +1,10 @@
package mage.abilities.common;
import mage.MageObjectReference;
import mage.abilities.effects.Effect;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.watchers.common.SaddledMountWatcher;
import java.util.Optional;
@ -15,6 +16,7 @@ public class AttacksWhileSaddledTriggeredAbility extends AttacksTriggeredAbility
public AttacksWhileSaddledTriggeredAbility(Effect effect) {
super(effect);
this.setTriggerPhrase("Whenever {this} attacks while saddled, ");
this.addWatcher(new SaddledMountWatcher());
}
private AttacksWhileSaddledTriggeredAbility(final AttacksWhileSaddledTriggeredAbility ability) {
@ -31,7 +33,7 @@ public class AttacksWhileSaddledTriggeredAbility extends AttacksTriggeredAbility
return super.checkTrigger(event, game)
&& Optional
.ofNullable(getSourcePermanentIfItStillExists(game))
.map(Permanent::isSaddled)
.map(p -> SaddledMountWatcher.hasBeenSaddledThisTurn(new MageObjectReference(p, game), game))
.orElse(false);
}
}

View file

@ -1,9 +1,10 @@
package mage.abilities.condition.common;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.condition.Condition;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.watchers.common.SaddledMountWatcher;
import java.util.Optional;
@ -17,7 +18,7 @@ public enum SaddledCondition implements Condition {
public boolean apply(Game game, Ability source) {
return Optional
.ofNullable(source.getSourcePermanentIfItStillExists(game))
.map(Permanent::isSaddled)
.map(p -> SaddledMountWatcher.hasBeenSaddledThisTurn(new MageObjectReference(p, game), game))
.orElse(false);
}
}

View file

@ -7,11 +7,12 @@ import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.condition.common.SaddledCondition;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.abilities.hint.HintUtils;
import mage.constants.*;
import mage.constants.Outcome;
import mage.constants.TimingRule;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.filter.predicate.permanent.TappedPredicate;
@ -24,11 +25,10 @@ import mage.watchers.common.SaddledMountWatcher;
import java.awt.*;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* @author TheElk801
* @author TheElk801, Susucr
*/
public class SaddleAbility extends SimpleActivatedAbility {
@ -36,7 +36,7 @@ public class SaddleAbility extends SimpleActivatedAbility {
private static final Hint hint = new ConditionHint(SaddledCondition.instance, "This permanent is saddled");
public SaddleAbility(int value) {
super(new SaddleEffect(), new SaddleCost(value));
super(new SaddleEventEffect(), new SaddleCost(value));
this.value = value;
this.addHint(hint);
this.setTiming(TimingRule.SORCERY);
@ -60,42 +60,36 @@ public class SaddleAbility extends SimpleActivatedAbility {
}
}
class SaddleEffect extends ContinuousEffectImpl {
class SaddleEventEffect extends OneShotEffect {
SaddleEffect() {
super(Duration.EndOfTurn, Layer.RulesEffects, SubLayer.NA, Outcome.Benefit);
SaddleEventEffect() {
super(Outcome.Benefit);
}
private SaddleEffect(final SaddleEffect effect) {
private SaddleEventEffect(final SaddleEventEffect effect) {
super(effect);
}
@Override
public SaddleEffect copy() {
return new SaddleEffect(this);
}
@Override
public void init(Ability source, Game game) {
super.init(source, game);
game.fireEvent(GameEvent.getEvent(
GameEvent.EventType.MOUNT_SADDLED,
source.getSourceId(),
source, source.getControllerId()
));
public SaddleEventEffect copy() {
return new SaddleEventEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Optional.ofNullable(source.getSourcePermanentIfItStillExists(game))
.ifPresent(permanent -> permanent.setSaddled(true));
if (source.getSourcePermanentIfItStillExists(game) != null) {
game.fireEvent(GameEvent.getEvent(
GameEvent.EventType.MOUNT_SADDLED,
source.getSourceId(),
source, source.getControllerId()
));
}
return true;
}
}
class SaddleCost extends CostImpl {
private static final FilterControlledCreaturePermanent filter
= new FilterControlledCreaturePermanent("another untapped creature you control");

View file

@ -1,12 +1,13 @@
package mage.abilities.keyword;
import mage.MageObjectReference;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.DelayedTriggeredAbility;
import mage.abilities.costs.Cost;
import mage.abilities.effects.ReplacementEffectImpl;
import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect;
import mage.abilities.effects.common.ExileSourceEffect;
import mage.abilities.effects.common.ExileTargetEffect;
import mage.abilities.effects.common.ReturnSourceFromGraveyardToBattlefieldEffect;
import mage.constants.Duration;
import mage.constants.Outcome;
@ -15,6 +16,8 @@ import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
/**
* @author BetaSteward_at_googlemail.com
@ -60,7 +63,7 @@ public class UnearthAbility extends ActivatedAbilityImpl {
class UnearthDelayedTriggeredAbility extends DelayedTriggeredAbility {
public UnearthDelayedTriggeredAbility() {
super(new ExileSourceEffect());
super(new ExileTargetEffect());
}
protected UnearthDelayedTriggeredAbility(final UnearthDelayedTriggeredAbility ability) {
@ -79,7 +82,19 @@ class UnearthDelayedTriggeredAbility extends DelayedTriggeredAbility {
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return event.getPlayerId().equals(this.controllerId);
if (!event.getPlayerId().equals(this.controllerId)) {
return false;
}
// The delayed trigger source is the card in the graveyard.
// So we need to exile the zcc + 1 permanent
MageObjectReference object = new MageObjectReference(getSourceId(), getSourceObjectZoneChangeCounter() + 1, game);
Permanent permanent = object.getPermanent(game);
if (permanent == null || !permanent.isPhasedIn()) {
// Triggers, but do nothing.
return true;
}
getEffects().setTargetPointer(new FixedTarget(permanent, game));
return true;
}
@Override

View file

@ -1,5 +1,6 @@
package mage.filter.predicate.permanent;
import mage.MageObjectReference;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.game.Game;
@ -17,7 +18,7 @@ public enum SaddledSourceThisTurnPredicate implements ObjectSourcePlayerPredicat
@Override
public boolean apply(ObjectSourcePlayer<Permanent> input, Game game) {
return SaddledMountWatcher.checkIfSaddledThisTurn(
input.getObject(), input.getSource().getSourcePermanentOrLKI(game), game
input.getObject(), new MageObjectReference(input.getSourceId(), input.getSource().getSourceObjectZoneChangeCounter(), game), game
);
}

View file

@ -2162,11 +2162,27 @@ public abstract class GameImpl implements Game {
delayedAbility.setSourceId(source.getSourceId());
delayedAbility.setControllerId(source.getControllerId());
}
// return addDelayedTriggeredAbility(delayedAbility);
DelayedTriggeredAbility newAbility = delayedAbility.copy();
newAbility.newId();
if (source != null) {
newAbility.setSourceObjectZoneChangeCounter(getState().getZoneChangeCounter(source.getSourceId()));
// Relevant ruling:
// 603.7e If an activated or triggered ability creates a delayed triggered ability,
// the source of that delayed triggered ability is the same as the source of that other ability.
// The controller of that delayed triggered ability is the player who controlled that other ability as it resolved.
// 603.7f If a static ability generates a replacement effect which causes a delayed triggered ability to be created,
// the source of that delayed triggered ability is the object with that static ability.
// The controller of that delayed triggered ability is the same as the controller of that object at the time
// the replacement effect was applied.
//
// There are two possibility for the zcc:
// 1/ the source is an Ability with a valid (not 0) zcc, and we must use the same.
int zcc = source.getSourceObjectZoneChangeCounter();
if (zcc == 0) {
// 2/ the source has not a valid zcc (it is most likely a StaticAbility instantiated at beginning of game)
// we use the source objects's zcc
zcc = getState().getZoneChangeCounter(source.getSourceId());
}
newAbility.setSourceObjectZoneChangeCounter(zcc);
newAbility.setSourcePermanentTransformCount(this);
}
newAbility.init(this);

View file

@ -78,10 +78,6 @@ public interface Permanent extends Card, Controllable {
void setSuspected(boolean value, Game game, Ability source);
boolean isSaddled();
void setSaddled(boolean value);
boolean isPrototyped();
void setPrototyped(boolean value);

View file

@ -72,7 +72,6 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean monstrous;
protected boolean renowned;
protected boolean suspected;
protected boolean saddled;
protected boolean manifested = false;
protected boolean morphed = false;
protected boolean disguised = false;
@ -176,7 +175,6 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.monstrous = permanent.monstrous;
this.renowned = permanent.renowned;
this.suspected = permanent.suspected;
this.saddled = permanent.saddled;
this.ringBearerFlag = permanent.ringBearerFlag;
this.classLevel = permanent.classLevel;
this.goadingPlayers.addAll(permanent.goadingPlayers);
@ -239,7 +237,6 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.maxBlockedBy = 0;
this.copy = false;
this.goadingPlayers.clear();
this.saddled = false;
this.loyaltyActivationsAvailable = 1;
this.legendRuleApplies = true;
this.canBeSacrificed = true;
@ -1730,16 +1727,6 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
}
}
@Override
public boolean isSaddled() {
return saddled;
}
@Override
public void setSaddled(boolean saddled) {
this.saddled = saddled;
}
// Used as key for the ring bearer info.
private static final String ringbearerInfoKey = "IS_RINGBEARER";

View file

@ -10,22 +10,30 @@ import mage.watchers.Watcher;
import java.util.*;
/**
* @author TheElk801
* @author TheElk801, Susucr
*/
public class SaddledMountWatcher extends Watcher {
// key: the mount, value: set of creatures which saddled
// key: the mount mor, value: set of creatures which saddled (on Saddle Cost payment)
private final Map<MageObjectReference, Set<MageObjectReference>> saddleMap = new HashMap<>();
// set of mount mor actually saddled (on Saddle Ability resolution)
private final Set<MageObjectReference> saddledSet = new HashSet<>();
public SaddledMountWatcher() {
super(WatcherScope.GAME);
}
@Override
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.SADDLED_MOUNT) {
saddleMap.computeIfAbsent(new MageObjectReference(event.getSourceId(), game), x -> new HashSet<>())
.add(new MageObjectReference(event.getTargetId(), game));
switch (event.getType()) {
case SADDLED_MOUNT:
saddleMap.computeIfAbsent(new MageObjectReference(event.getSourceId(), game), x -> new HashSet<>())
.add(new MageObjectReference(event.getTargetId(), game));
break;
case MOUNT_SADDLED:
saddledSet.add(new MageObjectReference(event.getSourceId(), game));
break;
}
}
@ -33,24 +41,21 @@ public class SaddledMountWatcher extends Watcher {
public void reset() {
super.reset();
saddleMap.clear();
saddledSet.clear();
}
public static boolean checkIfSaddledThisTurn(Permanent saddler, Permanent mount, Game game) {
return game
public static boolean hasBeenSaddledThisTurn(MageObjectReference mountMOR, Game game) {
SaddledMountWatcher watcher = game.getState().getWatcher(SaddledMountWatcher.class);
return watcher != null && watcher.saddledSet.contains(mountMOR);
}
public static boolean checkIfSaddledThisTurn(Permanent saddler, MageObjectReference mountMOR, Game game) {
return hasBeenSaddledThisTurn(mountMOR, game) && game
.getState()
.getWatcher(SaddledMountWatcher.class)
.saddleMap
.getOrDefault(new MageObjectReference(mount, game), Collections.emptySet())
.getOrDefault(mountMOR, Collections.emptySet())
.stream()
.anyMatch(mor -> mor.refersTo(saddler, game));
}
public static int getSaddleCount(Permanent vehicle, Game game) {
return game
.getState()
.getWatcher(SaddledMountWatcher.class)
.saddleMap
.getOrDefault(new MageObjectReference(vehicle, game), Collections.emptySet())
.size();
}
}