refactor: shared logic for diff implementation of isInUseableZone, improved docs and readability;

This commit is contained in:
Oleg Agafonov 2024-11-30 03:26:03 +04:00
parent 57ef74da90
commit d49ff89a81
76 changed files with 285 additions and 273 deletions

View file

@ -348,73 +348,61 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
}
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
/**
* 603.6. Trigger events that involve objects changing zones are called
* zone-change triggers. Many abilities with zone-change triggers
* attempt to do something to that object after it changes zones. During
* resolution, these abilities look for the object in the zone that it
* moved to. If the object is unable to be found in the zone it went to,
* the part of the ability attempting to do something to the object will
* fail to do anything. The ability could be unable to find the object
* because the object never entered the specified zone, because it left
* the zone before the ability resolved, or because it is in a zone that
* is hidden from a player, such as a library or an opponent's hand.
* (This rule applies even if the object leaves the zone and returns
* again before the ability resolves.) The most common zone-change
* triggers are enters-the-battlefield triggers and
* leaves-the-battlefield triggers.
*
* from:
* http://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/537065-ixidron-and-kozilek
* There are two types of triggers that involve the graveyard: dies
* triggers (which are a subset of leave-the-battlefield triggers) and
* put into the graveyard from anywhere triggers.
*
* The former triggers trigger based on the game state prior to the move
* where the Kozilek permanent is face down and has no abilities. The
* latter triggers trigger from the game state after the move where the
* Kozilek card is itself and has the ability.
*/
// workaround for singleton abilities like Flying
UUID affectedSourceId = getRealSourceObjectId(this, sourceObject);
// process events from other objects
Set<UUID> eventTargets = CardUtil.getEventTargets(event);
if (!eventTargets.contains(getSourceId())) {
return super.isInUseableZone(game, source, event);
}
// 603.6
// Trigger events that involve objects changing zones are called "zone-change triggers." Many abilities with
// zone-change triggers attempt to do something to that object after it changes zones. During resolution,
// these abilities look for the object in the zone that it moved to. If the object is unable to be found
// in the zone it went to, the part of the ability attempting to do something to the object will fail to
// do anything. The ability could be unable to find the object because the object never entered the
// specified zone, because it left the zone before the ability resolved, or because it is in a zone that
// is hidden from a player, such as a library or an opponents hand. (This rule applies even if the
// object leaves the zone and returns again before the ability resolves.) The most common zone-change
// triggers are enters-the-battlefield triggers and leaves-the-battlefield triggers.
// process events from own object
// There are possible two different use cases:
// * look in current game state (normal events):
// * look back in time (leaves battlefield, dies, etc);
// inject process of "look back in time" events
// TODO: need sync code with AbilityImpl.isInUseableZone
switch (event.getType()) {
case ZONE_CHANGE:
ZoneChangeEvent zce = (ZoneChangeEvent) event;
if (eventTargets.contains(getSourceId()) && !zce.getToZone().isPublicZone()) {
// If an ability triggers when the object that has it is put into a hidden zone from a graveyard,
// that ability triggers from the graveyard, (such as Golgari Brownscale),
// Yixlid Jailer will prevent that ability from triggering.
if (zce.getFromZone().match(Zone.GRAVEYARD)) {
if (!CardUtil.cardHadAbility(this, game.getLastKnownInformationCard(getSourceId(), zce.getFromZone()), getSourceId(), game)) {
return false;
// TODO: need sync or shared code with AbilityImpl.isInUseableZone
MageObject affectedSourceObject = sourceObject;
if (event == null) {
// state base triggers - use only actual state
} else {
// event triggers - can look back in time for some use cases
switch (event.getType()) {
case ZONE_CHANGE:
ZoneChangeEvent zce = (ZoneChangeEvent) event;
Set<UUID> eventTargets = CardUtil.getEventTargets(event);
if (eventTargets.contains(getSourceId()) && !zce.getToZone().isPublicZone()) {
// TODO: need research and share with AbilityImpl
// If an ability triggers when the object that has it is put into a hidden zone from a graveyard,
// that ability triggers from the graveyard, (such as Golgari Brownscale),
// Yixlid Jailer will prevent that ability from triggering.
if (zce.getFromZone().match(Zone.GRAVEYARD)) {
if (!CardUtil.cardHadAbility(this, game.getLastKnownInformationCard(getSourceId(), zce.getFromZone()), getSourceId(), game)) {
return false;
}
}
}
}
if (isLeavesTheBattlefieldTrigger()) {
source = zce.getTarget();
}
break;
case DESTROYED_PERMANENT:
case EXPLOITED_CREATURE:
if (isLeavesTheBattlefieldTrigger()) {
source = game.getLastKnownInformation(getSourceId(), Zone.BATTLEFIELD);
}
break;
if (isLeavesTheBattlefieldTrigger() && game.checkShortLivingLKI(affectedSourceId, Zone.BATTLEFIELD)) {
affectedSourceObject = game.getLastKnownInformation(affectedSourceId, Zone.BATTLEFIELD);
}
break;
case DESTROYED_PERMANENT:
case EXPLOITED_CREATURE:
if (isLeavesTheBattlefieldTrigger() && game.checkShortLivingLKI(affectedSourceId, Zone.BATTLEFIELD)) {
affectedSourceObject = game.getPermanentOrLKIBattlefield(affectedSourceId);
}
break;
}
}
// all other events from own object
return super.isInUseableZone(game, source, event);
return super.isInUseableZone(game, affectedSourceObject, event);
}
@Override
@ -464,23 +452,26 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
* (Similar logic must be used for any leaves-the-battlefield, but this method assumes to graveyard only.)
* NOTE: If your ability functions from another zone (not battlefield) then must use standard logic, not this.
*/
public static boolean isInUseableZoneDiesTrigger(TriggeredAbility source, GameEvent event, Game game) {
public static boolean isInUseableZoneDiesTrigger(TriggeredAbility sourceAbility, MageObject sourceObject, GameEvent event, Game game) {
// runtime check: wrong trigger settings
if (!source.isLeavesTheBattlefieldTrigger()) {
if (!sourceAbility.isLeavesTheBattlefieldTrigger()) {
throw new IllegalArgumentException("Wrong code usage: all dies triggers must use setLeavesTheBattlefieldTrigger(true) and override isInUseableZone - "
+ source.getSourceObject(game) + " - " + source);
+ sourceAbility.getSourceObject(game) + " - " + sourceAbility);
}
// runtime check: wrong isInUseableZone for batch related triggers
if (event instanceof BatchEvent) {
throw new IllegalArgumentException("Wrong code usage: batch events unsupported here, possible miss of override isInUseableZone - "
+ source.getSourceObject(game) + " - " + source);
+ sourceAbility.getSourceObject(game) + " - " + sourceAbility);
}
// Get the source permanent of the ability
MageObject sourceObject = null;
if (game.getState().getZone(source.getSourceId()) == Zone.BATTLEFIELD) {
sourceObject = game.getPermanent(source.getSourceId());
// workaround for singleton abilities like Flying
UUID affectedSourceId = getRealSourceObjectId(sourceAbility, sourceObject);
// on permanent - can use actual or look back in time
MageObject affectedObject = null;
if (game.getState().getZone(affectedSourceId) == Zone.BATTLEFIELD) {
affectedObject = game.getPermanent(affectedSourceId);
} else {
// The idea: short living LKI must help to find a moment in the inner of resolve
// -
@ -498,26 +489,29 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
// - ! empty stack ! graveyard ! no ! no ! no more to resolve
// --!---------------!-------------!-----!-----------!
// -
if (game.checkShortLivingLKI(source.getSourceId(), Zone.BATTLEFIELD)) {
sourceObject = game.getLastKnownInformation(source.getSourceId(), Zone.BATTLEFIELD);
}
}
if (sourceObject == null) { // source is no permanent
sourceObject = game.getObject(source);
if (sourceObject == null || sourceObject.isPermanent(game)) {
return false; // No source object found => ability is not valid
if (game.checkShortLivingLKI(affectedSourceId, Zone.BATTLEFIELD)) {
affectedObject = game.getLastKnownInformation(affectedSourceId, Zone.BATTLEFIELD);
}
}
if (!source.hasSourceObjectAbility(game, sourceObject, event)) {
if (affectedObject == null) {
affectedObject = game.getObject(sourceAbility);
if (affectedObject == null || affectedObject.isPermanent(game)) {
// if it was a permanent, but now removed then ignore
return false;
}
}
if (!sourceAbility.hasSourceObjectAbility(game, affectedObject, event)) {
return false; // the permanent does currently not have or before it dies the ability so no trigger
}
// check now it is in graveyard (only if it is no token and was the target itself)
if (source.getSourceId().equals(event.getTargetId()) // source is also the target
&& !(sourceObject instanceof PermanentToken) // it's no token
&& sourceObject.getZoneChangeCounter(game) + 1 == game.getState().getZoneChangeCounter(source.getSourceId())) { // It's in the next zone
Zone after = game.getState().getZone(source.getSourceId());
// TODO: need research
if (affectedSourceId.equals(event.getTargetId()) // source is also the target
&& !(affectedObject instanceof PermanentToken) // it's no token
&& affectedObject.getZoneChangeCounter(game) + 1 == game.getState().getZoneChangeCounter(affectedSourceId)) { // It's in the next zone
Zone after = game.getState().getZone(affectedSourceId);
if (!Zone.GRAVEYARD.match(after)) { // Zone is not the graveyard
return false; // Moving to graveyard was replaced so no trigger
}