diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 91c4f25e68c..df67f6bf4c6 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -164,8 +164,8 @@ public class GameAction { } if (!found) { c.clearControllers(); - if (c.removeChangedState()) { - c.updateStateForView(); + if (cause != null) { + unanimateOnAbortedChange(cause, c); } return c; } @@ -365,7 +365,18 @@ public class GameAction { copied.getOwner().removeInboundToken(copied); if (repres == ReplacementResult.Prevented) { - if (game.getStack().isResolving(c) && !zoneTo.is(ZoneType.Graveyard)) { + c.clearEtbCounters(); + c.clearControllers(); + if (cause != null) { + unanimateOnAbortedChange(cause, c); + if (cause.hasParam("Transformed") || cause.hasParam("FaceDown")) { + c.setBackSide(false); + c.changeToState(CardStateName.Original); + } + unattachCardLeavingBattlefield(c); + } + + if (c.isInZone(ZoneType.Stack) && !zoneTo.is(ZoneType.Graveyard)) { return moveToGraveyard(c, cause, params); } @@ -373,10 +384,8 @@ public class GameAction { copied.clearDelved(); copied.clearConvoked(); copied.clearExploited(); - } - - // was replaced with another Zone Change - if (toBattlefield && !c.isInPlay()) { + } else if (toBattlefield && !c.isInPlay()) { + // was replaced with another Zone Change if (c.removeChangedState()) { c.updateStateForView(); } @@ -2560,4 +2569,17 @@ public class GameAction { } return false; } + + private static void unanimateOnAbortedChange(final SpellAbility cause, final Card c) { + if (cause.hasParam("AnimateSubAbility")) { + long unanimateTimestamp = Long.valueOf(cause.getAdditionalAbility("AnimateSubAbility").getSVar("unanimateTimestamp")); + c.removeChangedCardKeywords(unanimateTimestamp, 0); + c.removeChangedCardTypes(unanimateTimestamp, 0); + c.removeChangedName(unanimateTimestamp, 0); + c.removeNewPT(unanimateTimestamp, 0); + if (c.removeChangedCardTraits(unanimateTimestamp, 0)) { + c.updateStateForView(); + } + } + } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/AnimateAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/AnimateAllEffect.java index 0276fb32bc1..357331bd1d0 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/AnimateAllEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/AnimateAllEffect.java @@ -136,10 +136,10 @@ public class AnimateAllEffect extends AnimateEffectBase { CardCollectionView list; - if (!sa.usesTargeting() && !sa.hasParam("Defined")) { - list = game.getCardsIn(ZoneType.Battlefield); - } else { + if (sa.usesTargeting() || sa.hasParam("Defined")) { list = getTargetPlayers(sa).getCardsIn(ZoneType.Battlefield); + } else { + list = game.getCardsIn(ZoneType.Battlefield); } list = CardLists.getValidCards(list, valid, sa.getActivatingPlayer(), host, sa); @@ -155,18 +155,18 @@ public class AnimateAllEffect extends AnimateEffectBase { game.fireEvent(new GameEventCardStatsChanged(c)); - final GameCommand unanimate = new GameCommand() { - private static final long serialVersionUID = -5861759814760561373L; - - @Override - public void run() { - doUnanimate(c, timestamp); - - game.fireEvent(new GameEventCardStatsChanged(c)); - } - }; - if (!permanent) { + final GameCommand unanimate = new GameCommand() { + private static final long serialVersionUID = -5861759814760561373L; + + @Override + public void run() { + doUnanimate(c, timestamp); + + game.fireEvent(new GameEventCardStatsChanged(c)); + } + }; + addUntilCommand(sa, unanimate); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java index 528ac2bbbc2..4436dfafd7f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java @@ -171,9 +171,11 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect { // need LKI before Animate does apply moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(c)); + final SpellAbility animate = sa.getAdditionalAbility("AnimateSubAbility"); source.addRemembered(c); - AbilityUtils.resolve(sa.getAdditionalAbility("AnimateSubAbility")); + AbilityUtils.resolve(animate); source.removeRemembered(c); + animate.setSVar("unanimateTimestamp", String.valueOf(game.getTimestamp())); } if (sa.hasParam("Tapped")) { c.setTapped(true); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java index c1bcc34c7c7..89e84303122 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java @@ -575,6 +575,17 @@ public class ChangeZoneEffect extends SpellAbilityEffect { movedCard = game.getAction().moveToLibrary(gameCard, libraryPosition, sa); } else { if (destination.equals(ZoneType.Battlefield)) { + Map moveParams = AbilityKey.newMap(); + moveParams.put(AbilityKey.LastStateBattlefield, lastStateBattlefield); + moveParams.put(AbilityKey.LastStateGraveyard, lastStateGraveyard); + if (sa.isReplacementAbility()) { + ReplacementEffect re = sa.getReplacementEffect(); + moveParams.put(AbilityKey.ReplacementEffect, re); + if (ReplacementType.Moved.equals(re.getMode()) && sa.getReplacingObject(AbilityKey.CardLKI) != null) { + moveParams.put(AbilityKey.CardLKI, sa.getReplacingObject(AbilityKey.CardLKI)); + } + } + if (sa.hasParam("Tapped") || sa.isNinjutsu()) { gameCard.setTapped(true); } @@ -583,6 +594,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } if (sa.hasParam("Transformed")) { if (gameCard.isDoubleFaced()) { + // need LKI before Animate does apply + if (!moveParams.containsKey(AbilityKey.CardLKI)) { + moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(gameCard)); + } gameCard.changeCardState("Transform", null, sa); } else { // If it can't Transform, don't change zones. @@ -650,26 +665,17 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } } - Map moveParams = AbilityKey.newMap(); - moveParams.put(AbilityKey.LastStateBattlefield, lastStateBattlefield); - moveParams.put(AbilityKey.LastStateGraveyard, lastStateGraveyard); - if (sa.isReplacementAbility()) { - ReplacementEffect re = sa.getReplacementEffect(); - moveParams.put(AbilityKey.ReplacementEffect, re); - if (ReplacementType.Moved.equals(re.getMode()) && sa.getReplacingObject(AbilityKey.CardLKI) != null) { - moveParams.put(AbilityKey.CardLKI, sa.getReplacingObject(AbilityKey.CardLKI)); - } - } - if (sa.hasAdditionalAbility("AnimateSubAbility")) { // need LKI before Animate does apply if (!moveParams.containsKey(AbilityKey.CardLKI)) { moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(gameCard)); } + final SpellAbility animate = sa.getAdditionalAbility("AnimateSubAbility"); hostCard.addRemembered(gameCard); - AbilityUtils.resolve(sa.getAdditionalAbility("AnimateSubAbility")); + AbilityUtils.resolve(animate); hostCard.removeRemembered(gameCard); + animate.setSVar("unanimateTimestamp", String.valueOf(game.getTimestamp())); } // need to be facedown before it hits the battlefield in case of Replacement Effects or Trigger @@ -1310,9 +1316,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect { // need LKI before Animate does apply moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(c)); + final SpellAbility animate = sa.getAdditionalAbility("AnimateSubAbility"); source.addRemembered(c); - AbilityUtils.resolve(sa.getAdditionalAbility("AnimateSubAbility")); + AbilityUtils.resolve(animate); source.removeRemembered(c); + animate.setSVar("unanimateTimestamp", String.valueOf(game.getTimestamp())); } if (sa.hasParam("GainControl")) { final String g = sa.getParam("GainControl"); @@ -1331,6 +1339,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect { } if (sa.hasParam("Transformed")) { if (c.isDoubleFaced()) { + // need LKI before Animate does apply + if (!moveParams.containsKey(AbilityKey.CardLKI)) { + moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(c)); + } c.changeCardState("Transform", null, sa); } else { // If it can't Transform, don't change zones. @@ -1390,7 +1402,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { movedCard.setTimestamp(ts); - if (sa.hasParam("AttachAfter") && movedCard.isAttachment()) { + if (sa.hasParam("AttachAfter") && movedCard.isAttachment() && movedCard.isInPlay()) { CardCollection list = AbilityUtils.getDefinedCards(source, sa.getParam("AttachAfter"), sa); if (list.isEmpty()) { list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AttachAfter"), c.getController(), c, sa); diff --git a/forge-game/src/main/java/forge/game/ability/effects/DigEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DigEffect.java index 415ea98a006..e6a92d4cb74 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DigEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DigEffect.java @@ -417,13 +417,13 @@ public class DigEffect extends SpellAbilityEffect { } if (sa.hasAdditionalAbility("AnimateSubAbility")) { // need LKI before Animate does apply - if (!moveParams.containsKey(AbilityKey.CardLKI)) { - moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(c)); - } + moveParams.put(AbilityKey.CardLKI, CardUtil.getLKICopy(c)); + final SpellAbility animate = sa.getAdditionalAbility("AnimateSubAbility"); host.addRemembered(c); - AbilityUtils.resolve(sa.getAdditionalAbility("AnimateSubAbility")); + AbilityUtils.resolve(animate); host.removeRemembered(c); + animate.setSVar("unanimateTimestamp", String.valueOf(game.getTimestamp())); } c = game.getAction().moveTo(zone, c, sa, moveParams); if (destZone1.equals(ZoneType.Battlefield)) { diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementLayer.java b/forge-game/src/main/java/forge/game/replacement/ReplacementLayer.java index ca71573f4b8..38ab284bcd9 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementLayer.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementLayer.java @@ -6,6 +6,7 @@ package forge.game.replacement; * */ public enum ReplacementLayer { + CantHappen, // 614.17 Control, // 616.1b Copy, // 616.1c Transform, // 616.1d diff --git a/forge-gui/res/cardsfolder/g/grafdiggers_cage.txt b/forge-gui/res/cardsfolder/g/grafdiggers_cage.txt index f77d045954e..72f0491a6f8 100644 --- a/forge-gui/res/cardsfolder/g/grafdiggers_cage.txt +++ b/forge-gui/res/cardsfolder/g/grafdiggers_cage.txt @@ -1,7 +1,7 @@ Name:Grafdigger's Cage ManaCost:1 Types:Artifact -R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Graveyard,Library | Destination$ Battlefield | ValidLKI$ Creature.Other | Prevent$ True | Description$ Creature cards in graveyards and libraries can't enter the battlefield. +R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Graveyard,Library | Destination$ Battlefield | ValidLKI$ Creature.Other | Prevent$ True | Layer$ CantHappen | Description$ Creature cards in graveyards and libraries can't enter the battlefield. S:Mode$ CantBeCast | Origin$ Graveyard,Library | Description$ Players can't cast spells from graveyards or libraries. SVar:NonStackingEffect:True AI:RemoveDeck:Random diff --git a/forge-gui/res/cardsfolder/g/greasefang_okiba_boss.txt b/forge-gui/res/cardsfolder/g/greasefang_okiba_boss.txt index a5204791aeb..2a083be643c 100644 --- a/forge-gui/res/cardsfolder/g/greasefang_okiba_boss.txt +++ b/forge-gui/res/cardsfolder/g/greasefang_okiba_boss.txt @@ -3,8 +3,9 @@ ManaCost:1 W B Types:Legendary Creature Rat Pilot PT:4/3 T:Mode$ Phase | Phase$ BeginCombat | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigReturn | TriggerDescription$ At the beginning of combat on your turn, return target Vehicle card from your graveyard to the battlefield. It gains haste. Return it to its owner's hand at the beginning of your next end step. -SVar:TrigReturn:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | TgtPrompt$ Select target Vehicle card in your graveyard | ValidTgts$ Vehicle.YouOwn | AnimateSubAbility$ Animate -SVar:Animate:DB$ Animate | Keywords$ Haste | Defined$ Remembered | Duration$ Permanent | AtEOT$ Hand +SVar:TrigReturn:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | TgtPrompt$ Select target Vehicle card in your graveyard | ValidTgts$ Vehicle.YouOwn | SubAbility$ Animate | RememberChanged$ True | AtEOT$ Hand +SVar:Animate:DB$ Animate | Keywords$ Haste | Defined$ Remembered | Duration$ Permanent | SubAbility$ DBCleanup +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True DeckHas:Ability$Graveyard DeckNeeds:Type$Vehicle Oracle:At the beginning of combat on your turn, return target Vehicle card from your graveyard to the battlefield. It gains haste. Return it to its owner's hand at the beginning of your next end step. diff --git a/forge-gui/res/cardsfolder/k/kunoros_hound_of_athreos.txt b/forge-gui/res/cardsfolder/k/kunoros_hound_of_athreos.txt index d13c61e88b4..4924dd4516d 100644 --- a/forge-gui/res/cardsfolder/k/kunoros_hound_of_athreos.txt +++ b/forge-gui/res/cardsfolder/k/kunoros_hound_of_athreos.txt @@ -5,7 +5,7 @@ PT:3/3 K:Vigilance K:Menace K:Lifelink -R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Graveyard | Destination$ Battlefield | ValidLKI$ Creature.Other | Prevent$ True | Description$ Creature cards in graveyards can't enter the battlefield. +R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Graveyard | Destination$ Battlefield | ValidLKI$ Creature.Other | Prevent$ True | Layer$ CantHappen | Description$ Creature cards in graveyards can't enter the battlefield. S:Mode$ CantBeCast | Origin$ Graveyard | Description$ Players can't cast spells from graveyards. SVar:NonStackingEffect:True Oracle:Vigilance, menace, lifelink\nCreature cards in graveyards can't enter the battlefield.\nPlayers can't cast spells from graveyards. diff --git a/forge-gui/res/cardsfolder/o/olivia_crimson_bride.txt b/forge-gui/res/cardsfolder/o/olivia_crimson_bride.txt index d3d21e4db38..6badaf7a978 100644 --- a/forge-gui/res/cardsfolder/o/olivia_crimson_bride.txt +++ b/forge-gui/res/cardsfolder/o/olivia_crimson_bride.txt @@ -5,8 +5,9 @@ PT:3/4 K:Flying K:Haste T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigChange | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME attacks, return target creature card from your graveyard to the battlefield tapped and attacking. It gains "When you don't control a legendary Vampire, exile this creature." -SVar:TrigChange:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouOwn | Tapped$ True | Attacking$ True | AnimateSubAbility$ DBAnimate -SVar:DBAnimate:DB$ Animate | Defined$ Remembered | Duration$ Permanent | Triggers$ TrigOlivia +SVar:TrigChange:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouOwn | Tapped$ True | Attacking$ True | RememberChanged$ True | SubAbility$ DBAnimate +SVar:DBAnimate:DB$ Animate | Defined$ Remembered | Duration$ Permanent | Triggers$ TrigOlivia | SubAbility$ DBCleanup +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:TrigOlivia:Mode$ Always | TriggerZones$ Battlefield | IsPresent$ Vampire.YouCtrl+Legendary | PresentCompare$ EQ0 | Execute$ TrigExile | TriggerDescription$ When you don't control a legendary Vampire, exile this creature. SVar:TrigExile:DB$ ChangeZone | Defined$ Self | Origin$ Battlefield | Destination$ Exile SVar:HasAttackEffect:TRUE diff --git a/forge-gui/res/cardsfolder/s/swords_to_plowshares.txt b/forge-gui/res/cardsfolder/s/swords_to_plowshares.txt index e75a18cde30..7ffd4a071eb 100644 --- a/forge-gui/res/cardsfolder/s/swords_to_plowshares.txt +++ b/forge-gui/res/cardsfolder/s/swords_to_plowshares.txt @@ -2,6 +2,7 @@ Name:Swords to Plowshares ManaCost:W Types:Instant A:SP$ ChangeZone | ValidTgts$ Creature | Origin$ Battlefield | Destination$ Exile | RememberLKI$ True | SubAbility$ DBGainLife | SpellDescription$ Exile target creature. -SVar:DBGainLife:DB$ GainLife | Defined$ RememberedController | LifeAmount$ X | SpellDescription$ Its controller gains life equal to its power. +SVar:DBGainLife:DB$ GainLife | Defined$ RememberedController | LifeAmount$ X | SubAbility$ DBCleanup | SpellDescription$ Its controller gains life equal to its power. +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:X:RememberedLKI$CardPower Oracle:Exile target creature. Its controller gains life equal to its power. diff --git a/forge-gui/res/cardsfolder/w/weathered_runestone.txt b/forge-gui/res/cardsfolder/w/weathered_runestone.txt index 824a1b4f92e..68f39c73052 100644 --- a/forge-gui/res/cardsfolder/w/weathered_runestone.txt +++ b/forge-gui/res/cardsfolder/w/weathered_runestone.txt @@ -1,7 +1,7 @@ Name:Weathered Runestone ManaCost:2 Types:Artifact -R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Graveyard,Library | Destination$ Battlefield | ValidLKI$ Permanent.nonland | Prevent$ True | Description$ Nonland permanent cards in graveyards and libraries can't enter the battlefield. +R:Event$ Moved | ActiveZones$ Battlefield | Origin$ Graveyard,Library | Destination$ Battlefield | ValidLKI$ Permanent.nonland | Prevent$ True | Layer$ CantHappen | Description$ Nonland permanent cards in graveyards and libraries can't enter the battlefield. S:Mode$ CantBeCast | Origin$ Graveyard,Library | Description$ Players can't cast spells from graveyards or libraries. SVar:NonStackingEffect:True AI:RemoveDeck:Random diff --git a/forge-gui/res/cardsfolder/w/worms_of_the_earth.txt b/forge-gui/res/cardsfolder/w/worms_of_the_earth.txt index b758fbfa3d0..b1432871eaa 100644 --- a/forge-gui/res/cardsfolder/w/worms_of_the_earth.txt +++ b/forge-gui/res/cardsfolder/w/worms_of_the_earth.txt @@ -2,7 +2,7 @@ Name:Worms of the Earth ManaCost:2 B B B Types:Enchantment S:Mode$ CantPlayLand | Description$ Players can't play lands. -R:Event$ Moved | ActiveZones$ Battlefield | Destination$ Battlefield | ValidCard$ Land | Prevent$ True | Description$ Lands can't enter the battlefield. +R:Event$ Moved | ActiveZones$ Battlefield | Destination$ Battlefield | ValidCard$ Land | Prevent$ True | Layer$ CantHappen | Description$ Lands can't enter the battlefield. T:Mode$ Phase | Phase$ Upkeep | TriggerZones$ Battlefield | Execute$ RepeatAbility | TriggerDescription$ At the beginning of each upkeep, any player may sacrifice two lands or have CARDNAME deal 5 damage to that player. If a player does either, destroy CARDNAME. SVar:RepeatAbility:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ DBChoose SVar:DBChoose:DB$ GenericChoice | Defined$ Player.IsRemembered | Choices$ SacTwoLands,DealDmg | AILogic$ PayUnlessCost diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index 9b5cc2c5dc0..07cd9c0b126 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -114,9 +114,11 @@ public class HumanPlay { final HumanPlaySpellAbility req = new HumanPlaySpellAbility(controller, sa); if (!req.playAbility(true, false, false)) { - if (flippedToCast && !castFaceDown) { + Card rollback = p.getGame().getCardState(sa.getHostCard()); + if (castFaceDown) { + rollback.setFaceDown(false); + } else if (flippedToCast) { // need to get the changed card if able - Card rollback = p.getGame().getCardState(sa.getHostCard()); rollback.turnFaceDown(true); //need to set correct imagekey when forcing facedown rollback.setImageKey(ImageKeys.getTokenKey(isforetold ? ImageKeys.FORETELL_IMAGE : ImageKeys.HIDDEN_CARD));