refactor: find targeting stack object (#13534)

* refactor: simplify finding targeting stack object
related to #11185, 8e1805c

* clarify docs

---------

Co-authored-by: xenohedron <12538125+xenohedron@users.noreply.github.com>
This commit is contained in:
xenohedron 2025-04-14 11:19:57 -04:00 committed by GitHub
parent 0f0026c375
commit 74d2265c12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 27 additions and 73 deletions

View file

@ -1128,14 +1128,19 @@ public final class CardUtil {
/**
* For finding the spell or ability on the stack for "becomes the target" triggers.
*
* Also ensures that spells/abilities that target the same object twice only trigger each "becomes the target" ability once.
* If this is the first attempt at triggering for a given ability targeting a given object,
* this method records that in the game state for later checks by this same method, to not return the same object again.
*
* @param checkingReference must be unique for each usage (this.getId().toString() of the TriggeredAbility, or this.getKey() of the watcher)
* @param event the GameEvent.EventType.TARGETED from checkTrigger() or watch()
* @param game the Game from checkTrigger() or watch()
* @return the StackObject which targeted the source, or null if not found
* @return the StackObject which targeted the source, or null if already used or not found
*/
public static StackObject getTargetingStackObject(String checkingReference, GameEvent event, Game game) {
public static StackObject findTargetingStackObject(String checkingReference, GameEvent event, Game game) {
// In case of multiple simultaneous triggered abilities from the same source,
// need to get the actual one that targeted, see #8026, #8378
// need to get the actual one that targeted, see #8026, #8378, rulings for Battle Mammoth
// In case of copied triggered abilities, need to trigger on each independently, see #13498
// Also avoids triggering on cancelled selections, see #8802
String stateKey = "targetedMap" + checkingReference;
Map<UUID, Set<UUID>> targetMap = (Map<UUID, Set<UUID>>) game.getState().getValue(stateKey);
@ -1148,50 +1153,22 @@ public final class CardUtil {
Set<UUID> targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
for (StackObject stackObject : game.getStack()) {
Ability stackAbility = stackObject.getStackAbility();
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId()) || targetingObjects.contains(stackObject.getId())) {
if (stackAbility == null || !stackAbility.getSourceId().equals(event.getSourceId())) {
continue;
}
if (CardUtil.getAllSelectedTargets(stackAbility, game).contains(event.getTargetId())) {
if (!targetingObjects.add(stackObject.getId())) {
continue; // The trigger/watcher already recorded that target of the stack object, check for another
}
// Otherwise, store this combination of trigger/watcher + target + stack object
targetMap.put(event.getTargetId(), targetingObjects);
game.getState().setValue(stateKey, targetMap);
return stackObject;
}
}
return null;
}
/**
* For ensuring that spells/abilities that target the same object twice only trigger each "becomes the target" ability once.
* If this is the first attempt at triggering for a given ability targeting a given object,
* this method records that in the game state for later checks by this same method.
*
* @param checkingReference must be unique for each usage (this.id.toString() of the TriggeredAbility, or this.getKey() of the watcher)
* @param targetingObject from getTargetingStackObject
* @param event the GameEvent.EventType.TARGETED from checkTrigger() or watch()
* @param game the Game from checkTrigger() or watch()
* @return true if already triggered/watched, false if this is the first/only trigger/watch
*/
public static boolean checkTargetedEventAlreadyUsed(String checkingReference, StackObject targetingObject, GameEvent event, Game game) {
String stateKey = "targetedMap" + checkingReference;
// If a spell or ability an opponent controls targets a single permanent you control more than once,
// Battle Mammoth's triggered ability will trigger only once.
// However, if a spell or ability an opponent controls targets multiple permanents you control,
// Battle Mammoth's triggered ability will trigger once for each of those permanents. (2021-02-05)
Map<UUID, Set<UUID>> targetMap = (Map<UUID, Set<UUID>>) game.getState().getValue(stateKey);
// targetMap: key - targetId; value - Set of stackObject Ids
if (targetMap == null) {
targetMap = new HashMap<>();
} else {
targetMap = new HashMap<>(targetMap); // must have new object reference if saved back to game state
}
Set<UUID> targetingObjects = targetMap.computeIfAbsent(event.getTargetId(), k -> new HashSet<>());
if (!targetingObjects.add(targetingObject.getId())) {
return true; // The trigger/watcher already recorded that target of the stack object
}
// Otherwise, store this combination of trigger/watcher + target + stack object
targetMap.put(event.getTargetId(), targetingObjects);
game.getState().setValue(stateKey, targetMap);
return false;
}
/**
* For overriding `canTarget()` with usages such as "any number of target cards with total mana value X or less".
* Call this after super.canTarget() returns true.