Fix Stonebinder's Familiar not triggering from Nivmagus Elemental (#4424)

* Fix Stonebinder's Familiar not triggering from Nivmagus Elemental

* Fix so it only triggers for cards

* Surveil triggers on empty library

* Fix missing trigger

* Fix trigger

* fix script

* Fix corner case freeze by trigger for loser (Kharn the Betrayer)

* Fix loser making trigger decision e.g. Shakedown Heavy

* Obeka fix when resolving after AP lost

* fix script

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
This commit is contained in:
tool4ever
2023-12-26 04:55:53 +01:00
committed by GitHub
parent e6a9ea20c7
commit a24af8d50e
17 changed files with 71 additions and 60 deletions

View File

@@ -19,6 +19,7 @@ package forge.game;
import com.google.common.base.Predicate;
import com.google.common.collect.*;
import forge.GameCommand;
import forge.StaticData;
import forge.card.CardStateName;
@@ -54,6 +55,7 @@ import forge.item.PaperCard;
import forge.util.*;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.*;
@@ -902,14 +904,10 @@ public class GameAction {
CardZoneTable table = new CardZoneTable(getLastState(AbilityKey.LastStateBattlefield, cause, params), getLastState(AbilityKey.LastStateGraveyard, cause, params));
CardCollection result = new CardCollection();
for (Card card : cards) {
if (cause != null) {
table.put(card.getZone().getZoneType(), ZoneType.Exile, card);
}
result.add(exile(card, cause, params));
}
if (cause != null) {
table.triggerChangesZoneAll(game, cause);
}
return result;
}
public final Card exile(final Card c, SpellAbility cause, Map<AbilityKey, Object> params) {

View File

@@ -1537,6 +1537,10 @@ public class AbilityUtils {
boolean alreadyPaid = false;
for (Player payer : allPayers) {
if (!payer.isInGame()) {
// CR 800.4f
continue;
}
if (unlessCost.equals("LifeTotalHalfUp")) {
String halfup = Integer.toString(Math.max(0,(int) Math.ceil(payer.getLife() / 2.0)));
cost = new Cost("PayLife<" + halfup + ">", true);

View File

@@ -876,7 +876,7 @@ public abstract class SpellAbilityEffect {
}
}
public Player getNewChooser(final SpellAbility sa, final Player activator, final Player loser) {
public static Player getNewChooser(final SpellAbility sa, final Player activator, final Player loser) {
// CR 800.4g
final PlayerCollection options;
if (loser.isOpponentOf(activator)) {

View File

@@ -7,10 +7,10 @@ import forge.game.GameLogEntryType;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.CardTranslation;
import forge.util.Localizer;
@@ -57,7 +57,7 @@ public class EncodeEffect extends SpellAbilityEffect {
moveParams.put(AbilityKey.LastStateGraveyard, sa.getLastStateGraveyard());
// move host card to exile
Card movedCard = game.getAction().moveTo(ZoneType.Exile, host, sa, moveParams);
Card movedCard = game.getAction().exile(new CardCollection(host), sa, moveParams).get(0);
// choose a creature
Card choice = player.getController().chooseSingleEntityForEffect(choices, sa, Localizer.getInstance().getMessage("lblChooseACreatureYouControlToEncode") + " ", false, null);

View File

@@ -17,7 +17,11 @@ public class EndTurnEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final List<Player> enders = getDefinedPlayersOrTargeted(sa, "Defined");
final Player ender = enders.isEmpty() ? sa.getActivatingPlayer() : enders.get(0);
Player ender = enders.isEmpty() ? sa.getActivatingPlayer() : enders.get(0);
if (!ender.isInGame()) {
ender = getNewChooser(sa, sa.getActivatingPlayer(), ender);
}
if (sa.hasParam("Optional") && !ender.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantEndTurn"), null)) {
return;
}

View File

@@ -9,7 +9,6 @@ import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardZoneTable;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
@@ -30,12 +29,9 @@ public class PermanentEffect extends SpellAbilityEffect {
CardZoneTable table = new CardZoneTable();
ZoneType previousZone = host.getZone().getZoneType();
CardCollectionView lastStateBattlefield = game.copyLastStateBattlefield();
CardCollectionView lastStateGraveyard = game.copyLastStateGraveyard();
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, lastStateBattlefield);
moveParams.put(AbilityKey.LastStateGraveyard, lastStateGraveyard);
moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield());
moveParams.put(AbilityKey.LastStateGraveyard, game.copyLastStateGraveyard());
final Card c = game.getAction().moveToPlay(host, host.getController(), sa, moveParams);
sa.setHostCard(c);

View File

@@ -31,6 +31,7 @@ import forge.game.cost.CostPart;
import forge.game.event.GameEventCardForetold;
import forge.game.trigger.TriggerType;
import forge.util.Localizer;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
@@ -78,6 +79,7 @@ import forge.game.trigger.TriggerHandler;
import forge.game.zone.ZoneType;
import forge.util.Lang;
import forge.util.TextUtil;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
@@ -109,12 +111,9 @@ public class CardFactoryUtil {
}
final Game game = hostCard.getGame();
CardCollectionView lastStateBattlefield = game.copyLastStateBattlefield();
CardCollectionView lastStateGraveyard = game.copyLastStateGraveyard();
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, lastStateBattlefield);
moveParams.put(AbilityKey.LastStateGraveyard, lastStateGraveyard);
moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield());
moveParams.put(AbilityKey.LastStateGraveyard, game.copyLastStateGraveyard());
hostCard.getGame().getAction().moveToPlay(hostCard, this, moveParams);
}

View File

@@ -19,6 +19,7 @@ package forge.game.cost;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
@@ -110,7 +111,7 @@ public class CostExileFromStack extends CostPart {
if (si != null) {
game.getStack().remove(si);
}
game.getAction().exile(sa.getHostCard(), null, null);
game.getAction().exile(new CardCollection(sa.getHostCard()), null, null);
}
return true;
}

View File

@@ -1122,10 +1122,7 @@ public class Player extends GameEntity implements Comparable<Player> {
final CardCollection topN = getTopXCardsFromLibrary(num);
if (topN.isEmpty()) {
return;
}
if (!topN.isEmpty()) {
final ImmutablePair<CardCollection, CardCollection> lists = getController().arrangeForSurveil(topN);
final CardCollection toTop = lists.getLeft();
final CardCollection toGrave = lists.getRight();
@@ -1154,6 +1151,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave));
}
surveilThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
@@ -2276,7 +2274,7 @@ public class Player extends GameEntity implements Comparable<Player> {
public final void addInvestigatedThisTurn() {
investigatedThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.Num, investigatedThisTurn);
runParams.put(AbilityKey.FirstTime, investigatedThisTurn == 1);
game.getTriggerHandler().runTrigger(TriggerType.Investigated, runParams, false);
}
public final void resetInvestigatedThisTurn() {

View File

@@ -531,6 +531,10 @@ public class TriggerHandler {
host.addRemembered(triggeredCard);
}
if (!sa.getActivatingPlayer().isInGame()) {
return;
}
sa.setStackDescription(sa.toString());
Player decider = null;

View File

@@ -71,11 +71,12 @@ public class TriggerInvestigated extends Trigger {
return false;
}
if (hasParam("OnlyFirst")) {
if ((int) runParams.get(AbilityKey.Num) != 1) {
if (hasParam("FirstTime")) {
if (!(boolean) runParams.get(AbilityKey.FirstTime)) {
return false;
}
}
return true;
}

View File

@@ -14,6 +14,7 @@ import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityKey;
import forge.game.ability.ApiType;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardDamageMap;
@@ -74,7 +75,7 @@ public class WrappedAbility extends Ability {
);
private final SpellAbility sa;
private final Player decider;
private Player decider;
boolean mandatory = false;
@@ -465,7 +466,7 @@ public class WrappedAbility extends Ability {
// //////////////////////////////////////
@Override
public void resolve() {
final Game game = sa.getActivatingPlayer().getGame();
final Game game = getActivatingPlayer().getGame();
final Trigger regtrig = getTrigger();
if (!(TriggerType.Always.equals(regtrig.getMode())) && !regtrig.hasParam("NoResolvingCheck")) {
@@ -490,9 +491,14 @@ public class WrappedAbility extends Ability {
}
}
if (decider != null && !decider.getController().confirmTrigger(this)) {
if (decider != null) {
if (!decider.isInGame()) {
decider = SpellAbilityEffect.getNewChooser(sa, getActivatingPlayer(), decider);
}
if (!decider.getController().confirmTrigger(this)) {
return;
}
}
if (!regtrig.hasParam("NoTimestampCheck")) {
timestampCheck();

View File

@@ -8,7 +8,7 @@ T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.S
SVar:DBBoon:DB$ Effect | Boon$ True | Duration$ Permanent | Triggers$ SpellCast
SVar:SpellCast:Mode$ SpellCast | ValidCard$ Creature | ValidActivatingPlayer$ You | TriggerZones$ Command | Execute$ ReplEffAddCounter | TriggerDescription$ When you cast your next creature spell, that creature enters the battlefield with an additional +1/+1 counter, flying counter, and lifelink counter on it.
SVar:ReplEffAddCounter:DB$ Effect | ReplacementEffects$ ETBAddCounter | RememberObjects$ TriggeredCard
SVar:ETBAddCounter:Event$ Moved | Origin$ Stack | Destination$ Battlefield | ValidCard$ Card.IsRemembered | ReplaceWith$ ETBAddExtraCounter
SVar:ETBAddCounter:Event$ Moved | Origin$ Stack | Destination$ Battlefield | ValidCard$ Card.IsRemembered | ReplacementResult$ Updated | ReplaceWith$ ETBAddExtraCounter
SVar:ETBAddExtraCounter:DB$ PutCounter | ETB$ True | Defined$ ReplacedCard | CounterType$ P1P1,Flying,Lifelink | CounterNum$ 1 | SubAbility$ DBRemoveSelf
SVar:DBRemoveSelf:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile
DeckHas:Ability$LifeGain|Counters & Keyword$Lifelink|Flying

View File

@@ -3,7 +3,7 @@ ManaCost:4 R
Types:Sorcery
A:SP$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ DBDraw | SpellDescription$ Each player may discard their hand and draw cards equal to the greatest mana value of a commander they own on the battlefield or in the command zone.
SVar:DBDraw:DB$ Draw | UnlessCost$ Discard<1/Hand> | UnlessPayer$ Remembered | UnlessSwitched$ True | Defined$ Remembered | NumCards$ X
SVar:X:Count$ValidBattlefield,Command Card.IsCommander+YouOwn$GreatestCMC
SVar:X:Count$ValidBattlefield,Command Card.IsCommander+RememberedPlayerOwn$GreatestCMC
DeckHas:Ability$Discard
AI:RemoveDeck:NonCommander
Oracle:Each player may discard their hand and draw cards equal to the greatest mana value of a commander they own on the battlefield or in the command zone.

View File

@@ -1,7 +1,7 @@
Name:Siren's Call
ManaCost:U
Types:Instant
A:SP$ Effect | StaticAbilities$ MustAttack | ActivationPhases$ Upkeep->BeginCombat | OpponentTurn$ True | SpellDescription$ Cast this spell only during an opponent's turn, before attackers are declared. Creatures the active player controls attack this turn if able. At the beginning of the next end step, destroy all non-Wall creatures that player controls that didn't attack this turn. Ignore this effect for each creature the player didn't control continuously since the beginning of the turn. | SubAbility$ DestroyPacifist
A:SP$ Effect | StaticAbilities$ MustAttack | ActivationPhases$ Upkeep->BeginCombat | ActivationFirstCombat$ True | OpponentTurn$ True | SpellDescription$ Cast this spell only during an opponent's turn, before attackers are declared. Creatures the active player controls attack this turn if able. At the beginning of the next end step, destroy all non-Wall creatures that player controls that didn't attack this turn. Ignore this effect for each creature the player didn't control continuously since the beginning of the turn. | SubAbility$ DestroyPacifist
SVar:MustAttack:Mode$ MustAttack | EffectZone$ Command | AffectedZone$ Battlefield | ValidCreature$ Creature.ActivePlayerCtrl | Description$ Creatures the active player controls attack this turn if able.
SVar:DestroyPacifist:DB$ DelayedTrigger | Mode$ Phase | Phase$ End of Turn | Execute$ TrigDestroy | TriggerDescription$ At the beginning of the next end step, destroy all non-Wall creatures that player controls that didn't attack this turn.
SVar:TrigDestroy:DB$ DestroyAll | ValidCards$ Creature.ActivePlayerCtrl+notAttackedThisTurn+nonWall+notFirstTurnControlled | SubAbility$ DBCleanup

View File

@@ -2,7 +2,7 @@ Name:Stonebinder's Familiar
ManaCost:W
Types:Creature Spirit Dog
PT:1/1
T:Mode$ ChangesZoneAll | Destination$ Exile | TriggerZones$ Battlefield | Execute$ TrigPutcounter | PlayerTurn$ True | ActivationLimit$ 1 | TriggerDescription$ Whenever one or more cards are put into exile during your turn, put a +1/+1 counter on CARDNAME. This ability triggers only once each turn.
T:Mode$ ChangesZoneAll | ValidCard$ Card.nonToken+nonCopiedSpell | Destination$ Exile | TriggerZones$ Battlefield | Execute$ TrigPutcounter | PlayerTurn$ True | ActivationLimit$ 1 | TriggerDescription$ Whenever one or more cards are put into exile during your turn, put a +1/+1 counter on CARDNAME. This ability triggers only once each turn.
SVar:TrigPutcounter:DB$ PutCounter | CounterType$ P1P1 | Defined$ Self | CounterNum$ 1
DeckHas:Ability$Counters
Oracle:Whenever one or more cards are put into exile during your turn, put a +1/+1 counter on Stonebinder's Familiar. This ability triggers only once each turn.

View File

@@ -3,7 +3,7 @@ ManaCost:2 R W
Types:Legendary Creature Time Lord Doctor
PT:3/5
T:Mode$ PhaseOutAll | ValidCards$ Permanent.phasedOutOther | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Whenever one or more other permanents phase out and whenever one or more other cards are put into exile from anywhere, put a time counter on CARDNAME.
T:Mode$ ChangesZoneAll | ValidCards$ Card.Other | Origin$ Any | Destination$ Exile | TriggerZones$ Battlefield | Execute$ TrigPutCounter | Secondary$ True | TriggerDescription$ Whenever one or more other permanents phase out and whenever one or more other cards are put into exile from anywhere, put a time counter on CARDNAME.
T:Mode$ ChangesZoneAll | ValidCards$ Card.Other+nonToken+nonCopiedSpell | Origin$ Any | Destination$ Exile | TriggerZones$ Battlefield | Execute$ TrigPutCounter | Secondary$ True | TriggerDescription$ Whenever one or more other permanents phase out and whenever one or more other cards are put into exile from anywhere, put a time counter on CARDNAME.
SVar:TrigPutCounter:DB$ PutCounter | CounterType$ TIME
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigDamage | TriggerDescription$ Whenever CARDNAME attacks, it deals damage equal to the number of time counters on it to any target. If a creature dealt damage this way would die this turn, exile it instead.
SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Any | DamageSource$ TriggeredAttackerLKICopy | NumDmg$ Count$CardCounters.TIME | ReplaceDyingDefined$ Targeted.Creature