refactor: removed some usages of short LKI, moved static ability's useable zone logic to basic ability implementation;

This commit is contained in:
Oleg Agafonov 2024-11-16 22:12:14 +04:00
parent 52ebba4cd1
commit 740a9347ae
7 changed files with 32 additions and 30 deletions

View file

@ -114,7 +114,7 @@ class VesuvanShapeshifterEffect extends OneShotEffect {
if (copyFromCreature != null) { if (copyFromCreature != null) {
game.copyPermanent(Duration.Custom, copyFromCreature, copyToCreature.getId(), source, new VesuvanShapeShifterFaceUpCopyApplier()); game.copyPermanent(Duration.Custom, copyFromCreature, copyToCreature.getId(), source, new VesuvanShapeShifterFaceUpCopyApplier());
source.getTargets().clear(); source.getTargets().clear();
game.processAction(); // needed to get effects ready if copy happens in replacment and the copied abilities react of the same event (e.g. turn face up) game.processAction(); // needed to get effects ready if copy happens in replacement and the copied abilities react of the same event (e.g. turn face up)
return true; return true;
} }
} }

View file

@ -13,7 +13,7 @@ import org.mage.test.serverside.base.CardTestCommanderDuelBase;
* *
* @author LevelX2 * @author LevelX2
*/ */
public class CommanderManaReplacmentTest extends CardTestCommanderDuelBase { public class CommanderManaReplacementTest extends CardTestCommanderDuelBase {
@Override @Override
protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException {

View file

@ -357,6 +357,8 @@ public interface Ability extends Controllable, Serializable {
* - for normal abilities and triggers - keep default * - for normal abilities and triggers - keep default
* - for leave battlefield triggers - keep default + set setLeavesTheBattlefieldTrigger(true) * - for leave battlefield triggers - keep default + set setLeavesTheBattlefieldTrigger(true)
* - for dies triggers - override and use TriggeredAbilityImpl.isInUseableZoneDiesTrigger inside + set setLeavesTheBattlefieldTrigger(true) * - for dies triggers - override and use TriggeredAbilityImpl.isInUseableZoneDiesTrigger inside + set setLeavesTheBattlefieldTrigger(true)
*
* @param source can be null for static continues effects checking like rules modification (example: Yixlid Jailer)
*/ */
boolean isInUseableZone(Game game, MageObject source, GameEvent event); boolean isInUseableZone(Game game, MageObject source, GameEvent event);

View file

@ -1179,6 +1179,8 @@ public abstract class AbilityImpl implements Ability {
if (!this.hasSourceObjectAbility(game, source, event)) { if (!this.hasSourceObjectAbility(game, source, event)) {
return false; return false;
} }
// in command zone
if (zone == Zone.COMMAND) { if (zone == Zone.COMMAND) {
if (this.getSourceId() == null) { // commander effects if (this.getSourceId() == null) { // commander effects
return true; return true;
@ -1199,20 +1201,18 @@ public abstract class AbilityImpl implements Ability {
parameterSourceId = getSourceId(); parameterSourceId = getSourceId();
} }
// old code: // on entering permanents - must use static abilities like it already on battlefield
// TODO: delete after dies fix // example: Tatterkite enters without counters from Mikaeus, the Unhallowed
// check against shortLKI for effects that move multiple object at the same time (e.g. destroy all) if (game.getPermanentEntering(parameterSourceId) != null && zone == Zone.BATTLEFIELD) {
if (game.checkShortLivingLKI(getSourceId(), getZone())) { return true;
//return true; // fix 1
} }
// 603.10. // 603.10.
// Normally, objects that exist immediately after an event are checked to see if the event matched // Normally, objects that exist immediately after an event are checked to see if the event matched
// any trigger conditions, and continuous effects that exist at that time are used to determine what the // any trigger conditions, and continuous effects that exist at that time are used to determine what the
// trigger conditions are and what the objects involved in the event look like. // trigger conditions are and what the objects involved in the event look like.
// ... // ...
Zone lookingInZone = game.getState().getZone(parameterSourceId); Zone sourceObjectZone = game.getState().getZone(parameterSourceId);
// 603.10. // 603.10.
// ... // ...
@ -1226,9 +1226,16 @@ public abstract class AbilityImpl implements Ability {
// players can see is put into a hand or library. // players can see is put into a hand or library.
// TODO: research "leaves a graveyard" // TODO: research "leaves a graveyard"
// TODO: research "put into a hand or library" // TODO: research "put into a hand or library"
if (source instanceof Permanent && isTriggerCanFireAfterLeaveBattlefield(event)) { if (isTriggerCanFireAfterLeaveBattlefield(event)) {
// support leaves-the-battlefield abilities // permanents with normal triggers
lookingInZone = Zone.BATTLEFIELD; if (source instanceof Permanent) {
// support leaves-the-battlefield abilities
sourceObjectZone = Zone.BATTLEFIELD;
}
// permanents with continues effects like Yixlid Jailer, see related code "isInUseableZone(game, null"
if (source == null && this instanceof StaticAbility) {
sourceObjectZone = Zone.BATTLEFIELD;
}
} }
// TODO: research use cases and implement shared logic with "looking zone" instead LKI only // TODO: research use cases and implement shared logic with "looking zone" instead LKI only
@ -1239,7 +1246,7 @@ public abstract class AbilityImpl implements Ability {
// 603.10f Abilities that trigger when a player loses the game look back in time. // 603.10f Abilities that trigger when a player loses the game look back in time.
// 603.10g Abilities that trigger when a player planeswalks away from a plane look back in time. // 603.10g Abilities that trigger when a player planeswalks away from a plane look back in time.
return zone.match(lookingInZone); return zone.match(sourceObjectZone);
} }
public static boolean isTriggerCanFireAfterLeaveBattlefield(GameEvent event) { public static boolean isTriggerCanFireAfterLeaveBattlefield(GameEvent event) {
@ -1255,6 +1262,7 @@ public abstract class AbilityImpl implements Ability {
} }
return allEvents.stream().anyMatch(e -> { return allEvents.stream().anyMatch(e -> {
// TODO: need sync code with TriggeredAbilityImpl.isInUseableZone
// TODO: add more events with zone change logic (or make it event's param)? // TODO: add more events with zone change logic (or make it event's param)?
// need research: is it ability's or event's task? // need research: is it ability's or event's task?
// - ability's task: code like ability.setLookBackInTime // - ability's task: code like ability.setLookBackInTime

View file

@ -1,12 +1,8 @@
package mage.abilities; package mage.abilities;
import mage.MageObject;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.constants.AbilityType; import mage.constants.AbilityType;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
/** /**
* *
@ -25,17 +21,6 @@ public abstract class StaticAbility extends AbilityImpl {
} }
} }
@Override
public boolean isInUseableZone(Game game, MageObject source, GameEvent event) {
if (game.checkShortLivingLKI(getSourceId(), zone)) { // TODO: can be deleted? Need research
return true; // maybe this can be a problem if effects removed the ability from the object
}
if (game.getPermanentEntering(getSourceId()) != null && zone == Zone.BATTLEFIELD) {
return true; // abilities of permanents entering battlefield are countes as on battlefield
}
return super.isInUseableZone(game, source, event);
}
protected StaticAbility(final StaticAbility ability) { protected StaticAbility(final StaticAbility ability) {
super(ability); super(ability);
} }

View file

@ -376,11 +376,16 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
* Kozilek card is itself and has the ability. * Kozilek card is itself and has the ability.
*/ */
// process events from other objects
Set<UUID> eventTargets = CardUtil.getEventTargets(event); Set<UUID> eventTargets = CardUtil.getEventTargets(event);
if (!eventTargets.contains(getSourceId())) { if (!eventTargets.contains(getSourceId())) {
return super.isInUseableZone(game, source, event); return super.isInUseableZone(game, source, event);
} }
// process events from own object
// inject process of "look back in time" events
// TODO: need sync code with AbilityImpl.isInUseableZone
switch (event.getType()) { switch (event.getType()) {
case ZONE_CHANGE: case ZONE_CHANGE:
ZoneChangeEvent zce = (ZoneChangeEvent) event; ZoneChangeEvent zce = (ZoneChangeEvent) event;
@ -405,6 +410,8 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
} }
break; break;
} }
// all other events from own object
return super.isInUseableZone(game, source, event); return super.isInUseableZone(game, source, event);
} }

View file

@ -325,7 +325,7 @@ public class ContinuousEffects implements Serializable {
if (effect instanceof PayCostToAttackBlockEffect) { if (effect instanceof PayCostToAttackBlockEffect) {
Set<Ability> abilities = replacementEffects.getAbility(effect.getId()); Set<Ability> abilities = replacementEffects.getAbility(effect.getId());
for (Ability ability : abilities) { for (Ability ability : abilities) {
// for replacment effects of static abilities do not use LKI to check if to apply // for replacement effects of static abilities do not use LKI to check if to apply
if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) { if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) {
if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) { if (effect.getDuration() != Duration.OneUse || !effect.isUsed()) {
if (!game.getScopeRelevant() || effect.hasSelfScope() || !event.getTargetId().equals(ability.getSourceId())) { if (!game.getScopeRelevant() || effect.hasSelfScope() || !event.getTargetId().equals(ability.getSourceId())) {
@ -370,7 +370,7 @@ public class ContinuousEffects implements Serializable {
Set<Ability> abilities = replacementEffects.getAbility(effect.getId()); Set<Ability> abilities = replacementEffects.getAbility(effect.getId());
Set<Ability> applicableAbilities = new HashSet<>(); Set<Ability> applicableAbilities = new HashSet<>();
for (Ability ability : abilities) { for (Ability ability : abilities) {
// for replacment effects of static abilities do not use LKI to check if to apply // for replacement effects of static abilities do not use LKI to check if to apply
if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) { if (ability.getAbilityType() != AbilityType.STATIC || ability.isInUseableZone(game, null, event)) {
if (!effect.isUsed()) { if (!effect.isUsed()) {
if (!game.getScopeRelevant() if (!game.getScopeRelevant()