mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 20:58:03 +00:00
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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user