Fix Crawling Sensation + related tweaks (#4744)

* Fix Foretell rollback

* Fix Crawling Sensation

* Stonebinder's Familiar should trigger when Time Stop exiles spell card

* Support Leave GY LKI

* Tweak logic so it only reuses the table when simultaneous

* Fix NPE

* AI fix

* Fix countered spell not exiled by Dauthi Voidwalker

* Fix Parallax Wave not returning when exiling itself

* Fix LKI update timing

---------

Co-authored-by: TRT <>
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
This commit is contained in:
tool4ever
2024-02-27 15:33:13 +01:00
committed by GitHub
parent 34bd50f222
commit dcbe9c4ba1
22 changed files with 96 additions and 58 deletions

View File

@@ -1252,11 +1252,12 @@ public class GameAction {
boolean orderedDesCreats = false;
boolean orderedNoRegCreats = false;
boolean orderedSacrificeList = false;
CardCollection cardsToUpdateLKI = new CardCollection();
for (int q = 0; q < 9; q++) {
checkStaticAbilities(false, affectedCards, CardCollection.EMPTY);
boolean checkAgain = false;
CardCollection cardsToUpdateLKI = new CardCollection();
checkStaticAbilities(false, affectedCards, CardCollection.EMPTY);
CardZoneTable table = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
Map<AbilityKey, Object> mapParams = AbilityKey.newMap();
@@ -1445,10 +1446,14 @@ public class GameAction {
table.triggerChangesZoneAll(game, null);
if (!checkAgain) {
break; // do not continue the loop
} else {
for (final Card c : cardsToUpdateLKI) {
game.updateLastStateForCard(c);
}
if (checkAgain) {
performedSBA = true;
} else {
break; // do not continue the loop
}
} // for q=0;q<9
@@ -1473,10 +1478,6 @@ public class GameAction {
// this point.
checkStaticAbilities(false, affectedCards, CardCollection.EMPTY);
for (final Card c : cardsToUpdateLKI) {
game.updateLastStateForCard(c);
}
if (!refreeze) {
game.getStack().unfreezeStack();
}

View File

@@ -879,7 +879,7 @@ public final class GameActionUtil {
oldCard.getZone().remove(oldCard);
// in some rare cases the old position no longer exists (Panglacial Wurm + Selvala)
Integer newPosition = zonePosition >= 0 ? Math.min(Integer.valueOf(zonePosition), fromZone.size()) : null;
fromZone.add(oldCard, newPosition);
fromZone.add(oldCard, newPosition, null, true);
ability.setHostCard(oldCard);
ability.setXManaCostPaid(null);
ability.setSpendPhyrexianMana(false);

View File

@@ -1,9 +1,11 @@
package forge.game.ability;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.card.CardZoneTable;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.EnumMap;
import java.util.Map;
@@ -210,9 +212,14 @@ public enum AbilityKey {
map.put(AbilityKey.LastStateGraveyard, table.getLastStateGraveyard());
map.put(AbilityKey.InternalTriggerTable, table);
}
public static CardZoneTable addCardZoneTableParams(Map<AbilityKey, Object> map, forge.game.spellability.SpellAbility sa) {
public static CardZoneTable addCardZoneTableParams(Map<AbilityKey, Object> map, SpellAbility sa) {
CardZoneTable table = new CardZoneTable(sa.getLastStateBattlefield(), sa.getLastStateGraveyard());
addCardZoneTableParams(map, table);
return table;
}
public static CardZoneTable addCardZoneTableParams(Map<AbilityKey, Object> map, Game game) {
CardZoneTable table = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
addCardZoneTableParams(map, table);
return table;
}
}

View File

@@ -1595,9 +1595,6 @@ public class AbilityUtils {
}
host.addRemembered(sb.toString());
}
// make sure that when this is from a trigger LKI is updated
host.getGame().updateLastStateForCard(host);
}
/**

View File

@@ -17,6 +17,7 @@ import forge.game.player.PlayerCollection;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
@@ -945,11 +946,13 @@ public abstract class SpellAbilityEffect {
}
}
public static void handleExiledWith(final Card movedCard, final SpellAbility cause) {
handleExiledWith(movedCard, cause, cause.getHostCard());
}
public static void handleExiledWith(final Card movedCard, final SpellAbility cause, Card exilingSource) {
if (movedCard.isToken()) {
return;
}
Card exilingSource = cause.getHostCard();
// during replacement LKI might be used
if (cause.isReplacementAbility() && exilingSource.isLKI()) {
exilingSource = exilingSource.getGame().getCardState(exilingSource);
@@ -969,7 +972,9 @@ public abstract class SpellAbilityEffect {
}
public CardZoneTable getChangeZoneTable(SpellAbility sa, CardCollectionView lastStateBattlefield, CardCollectionView lastStateGraveyard) {
if (sa.isReplacementAbility() && sa.getReplacingObject(AbilityKey.InternalTriggerTable) != null) {
if (sa.isReplacementAbility() && sa.getReplacementEffect().getMode() == ReplacementType.Moved
&& sa.getReplacingObject(AbilityKey.InternalTriggerTable) != null) {
// if a RE changes the destination zone try to make it simultaneous
return (CardZoneTable) sa.getReplacingObject(AbilityKey.InternalTriggerTable);
}
return new CardZoneTable(lastStateBattlefield, lastStateGraveyard);

View File

@@ -710,6 +710,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
movedCard = game.getAction().moveTo(destination, gameCard, sa, moveParams);
if (destination.equals(ZoneType.Exile) && lastStateBattlefield.contains(gameCard) && hostCard.equals(gameCard)) {
// support Parallax Wave returning itself
handleExiledWith(movedCard, sa, lastStateBattlefield.get(gameCard));
}
if (ZoneType.Hand.equals(destination) && ZoneType.Command.equals(originZone.getZoneType())) {
StringBuilder sb = new StringBuilder();
sb.append(movedCard.getName()).append(" has moved from Command Zone to ").append(player).append("'s hand.");

View File

@@ -68,7 +68,7 @@ public class ConniveEffect extends SpellAbilityEffect {
CardCollection connivers = CardLists.filterControlledBy(toConnive, p);
while (!connivers.isEmpty()) {
GameEntityCounterTable table = new GameEntityCounterTable();
final CardZoneTable triggerList = new CardZoneTable(sa.getLastStateBattlefield(), sa.getLastStateGraveyard());
final CardZoneTable triggerList = new CardZoneTable(game.copyLastStateBattlefield(), game.copyLastStateGraveyard());
Map<Player, CardCollectionView> discardedMap = Maps.newHashMap();
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
AbilityKey.addCardZoneTableParams(moveParams, triggerList);

View File

@@ -53,7 +53,7 @@ public class CounterEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
Map<AbilityKey, Object> params = AbilityKey.newMap();
final CardZoneTable table = AbilityKey.addCardZoneTableParams(params, sa);
final CardZoneTable table = AbilityKey.addCardZoneTableParams(params, game);
for (final SpellAbility tgtSA : getTargetSpells(sa)) {
final Card tgtSACard = tgtSA.getHostCard();

View File

@@ -25,6 +25,9 @@ public class EndCombatPhaseEffect extends SpellAbilityEffect {
return;
}
// CR 721.2a
game.getTriggerHandler().clearWaitingTriggers();
// 1) All spells and abilities on the stack are exiled.
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
CardZoneTable table = new CardZoneTable(sa.getLastStateBattlefield(), sa.getLastStateGraveyard());
@@ -35,7 +38,6 @@ public class EndCombatPhaseEffect extends SpellAbilityEffect {
game.getStack().clear();
game.getStack().clearSimultaneousStack();
game.getTriggerHandler().clearWaitingTriggers();
// 2) State-based actions are checked. No player gets priority, and no
// triggered abilities are put onto the stack.
@@ -43,7 +45,6 @@ public class EndCombatPhaseEffect extends SpellAbilityEffect {
// 3) The current phase and step ends. The game skips straight to the postcombat main phase. As this happens,
// all attacking and blocking creatures are removed from combat and effects that last “until end of combat” expire.
game.getPhaseHandler().endCombat();
game.getPhaseHandler().endCombatPhaseByEffect();
}

View File

@@ -28,7 +28,11 @@ public class EndTurnEffect extends SpellAbilityEffect {
if (sa.hasParam("Optional") && !ender.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantEndTurn"), null)) {
return;
}
Game game = ender.getGame();
// CR 721.1a
game.getTriggerHandler().clearWaitingTriggers();
// Steps taken from gatherer's rulings on Time Stop.
// 1) All spells and abilities on the stack are exiled. This includes
// Time Stop, though it will continue to resolve. It also includes
@@ -42,7 +46,6 @@ public class EndTurnEffect extends SpellAbilityEffect {
game.getStack().clear();
game.getStack().clearSimultaneousStack();
game.getTriggerHandler().clearWaitingTriggers();
// 2) All attacking and blocking creatures are removed from combat.
game.getPhaseHandler().endCombat();

View File

@@ -30,7 +30,6 @@ public class HauntEffect extends SpellAbilityEffect {
final Card copy = game.getAction().exile(card, sa, moveParams);
sa.getTargetCard().addHauntedBy(copy);
table.triggerChangesZoneAll(game, sa);
} else if (!sa.usesTargeting() && card.getHaunting() != null) {
// unhaunt
card.getHaunting().removeHauntedBy(card);

View File

@@ -1899,7 +1899,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
}
public final void cleanupExiledWith() {
if (exiledWith == null) {
if (exiledWith == null || exiledWith.isLKI()) {
return;
}

View File

@@ -13,7 +13,6 @@ import forge.game.CardTraitBase;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.player.PlayerCollection;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
@@ -85,7 +84,7 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
public void triggerChangesZoneAll(final Game game, final SpellAbility cause) {
triggerTokenCreatedOnce(game);
if (cause != null && cause.isReplacementAbility() && cause.getReplacementEffect().getMode() == ReplacementType.Moved) {
if (cause != null && cause.getReplacingObject(AbilityKey.InternalTriggerTable) == this) {
// will be handled by original "cause" instead
return;
}
@@ -113,21 +112,25 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
public CardCollection filterCards(Iterable<ZoneType> origin, ZoneType destination, String valid, Card host, CardTraitBase sa) {
CardCollection allCards = new CardCollection();
if (destination != null) {
if (!containsColumn(destination)) {
return allCards;
}
if (destination != null && !containsColumn(destination)) {
return allCards;
}
if (origin != null) {
for (ZoneType z : origin) {
CardCollectionView lkiLookup = CardCollection.EMPTY;
if (z == ZoneType.Battlefield) {
lkiLookup = lastStateBattlefield;
}
if (containsRow(z)) {
CardCollectionView lkiLookup = CardCollection.EMPTY;
// CR 603.10a
if (z == ZoneType.Battlefield) {
lkiLookup = lastStateBattlefield;
}
if (z == ZoneType.Graveyard && destination == null) {
lkiLookup = lastStateGraveyard;
}
if (destination != null) {
for (Card c : row(z).get(destination)) {
allCards.add(lkiLookup.get(c));
if (row(z).containsKey(destination)) {
for (Card c : row(z).get(destination)) {
allCards.add(lkiLookup.get(c));
}
}
} else {
for (CardCollection cc : row(z).values()) {

View File

@@ -1235,6 +1235,7 @@ public class PhaseHandler implements java.io.Serializable {
}
public final void endCombatPhaseByEffect() {
endCombat();
game.getAction().checkStateEffects(true);
setPhase(PhaseType.COMBAT_END);
advanceToNextPhase();
@@ -1243,6 +1244,7 @@ public class PhaseHandler implements java.io.Serializable {
public final void endTurnByEffect() {
extraPhases.clear();
setPhase(PhaseType.CLEANUP);
game.fireEvent(new GameEventTurnPhase(playerTurn, phase, ""));
onPhaseBegin();
}

View File

@@ -9,6 +9,7 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardUtil;
import forge.game.card.CardZoneTable;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
@@ -31,6 +32,15 @@ public class TriggerChangesZoneAll extends Trigger {
return false;
}
if (hasParam("FirstTime")) {
// currently only for Crawling Sensation
List<Card> entered = CardUtil.getThisTurnEntered(ZoneType.smartValueOf(getParam("Destination")), null, getParam("ValidCards"), getHostCard(), this, getHostCard().getController());
entered.removeAll(filterCards(table));
if (!entered.isEmpty()) {
return false;
}
}
if (!matchesValidParam("ValidCause", runParams.get(AbilityKey.Cause))) {
return false;
}

View File

@@ -85,6 +85,9 @@ public class Zone implements java.io.Serializable, Iterable<Card> {
add(c, index, null);
}
public void add(final Card c, Integer index, final Card latestState) {
add(c, index, latestState, false);
}
public void add(final Card c, Integer index, final Card latestState, final boolean rollback) {
if (index != null && cardList.isEmpty() && index.intValue() > 0) {
// something went wrong, most likely the method fired when the game was in an unexpected state
// (e.g. conceding during the mana payment prompt)
@@ -101,28 +104,30 @@ public class Zone implements java.io.Serializable, Iterable<Card> {
}
}
// Immutable cards are usually emblems and effects
if (!c.isImmutable()) {
final Zone oldZone = game.getZoneOf(c);
final ZoneType zt = oldZone == null ? ZoneType.Stack : oldZone.getZoneType();
if (!rollback) {
// Immutable cards are usually emblems and effects
if (!c.isImmutable()) {
final Zone oldZone = game.getZoneOf(c);
final ZoneType zt = oldZone == null ? ZoneType.Stack : oldZone.getZoneType();
// only if the zoneType differs from this
// don't go in there is its a control change
if (zt != zoneType) {
c.setTurnInController(getPlayer());
c.setTurnInZone(game.getPhaseHandler().getTurn());
if (latestState != null) {
cardsAddedThisTurn.add(zt, latestState);
// only if the zoneType differs from this
// don't go in there is its a control change
if (zt != zoneType) {
c.setTurnInController(getPlayer());
c.setTurnInZone(game.getPhaseHandler().getTurn());
if (latestState != null) {
cardsAddedThisTurn.add(zt, latestState);
}
}
}
}
if (zoneType != ZoneType.Battlefield) {
c.setTapped(false);
}
if (zoneType != ZoneType.Battlefield) {
c.setTapped(false);
}
if (zoneType == (ZoneType.Graveyard) && c.isPermanent() && !c.isToken()) {
c.getOwner().descend();
if (zoneType == ZoneType.Graveyard && c.isPermanent() && !c.isToken()) {
c.getOwner().descend();
}
}
c.setZone(this);