mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 04:38:00 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -531,6 +531,10 @@ public class TriggerHandler {
|
||||
host.addRemembered(triggeredCard);
|
||||
}
|
||||
|
||||
if (!sa.getActivatingPlayer().isInGame()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sa.setStackDescription(sa.toString());
|
||||
|
||||
Player decider = null;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user