Fix crash after removing target when copying divided spell (#8714)

Co-authored-by: tool4EvEr <tool4EvEr@>
This commit is contained in:
tool4ever
2025-09-15 14:27:21 +02:00
committed by GitHub
parent 10d359e7d7
commit 79fd4a3f8d
10 changed files with 27 additions and 35 deletions

View File

@@ -298,13 +298,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean activateForCost = ComputerUtil.activateForCost(sa, ai); boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) { if (sa.hasParam("Origin")) {
try { origin = ZoneType.listValueOf(sa.getParam("Origin"));
origin = ZoneType.listValueOf(sa.getParam("Origin"));
} catch (IllegalArgumentException ex) {
// This happens when Origin is something like
// "Graveyard,Library" (Doomsday)
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
final String destination = sa.getParam("Destination"); final String destination = sa.getParam("Destination");

View File

@@ -20,14 +20,14 @@ public class AirbendEffect extends SpellAbilityEffect {
@Override @Override
protected String getStackDescription(SpellAbility sa) { protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Airbend "); final StringBuilder sb = new StringBuilder("Airbend ");
Iterable<Card> tgts; Iterable<Card> tgts;
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
tgts = getCardsfromTargets(sa); tgts = getCardsfromTargets(sa);
} else { // otherwise add self to list and go from there } else { // otherwise add self to list and go from there
tgts = sa.knownDetermineDefined(sa.getParam("Defined")); tgts = sa.knownDetermineDefined(sa.getParam("Defined"));
} }
sb.append(sa.getParamOrDefault("DefinedDesc", Lang.joinHomogenous(tgts))); sb.append(sa.getParamOrDefault("DefinedDesc", Lang.joinHomogenous(tgts)));
sb.append("."); sb.append(".");
if (Iterables.size(tgts) > 1) { if (Iterables.size(tgts) > 1) {
@@ -46,7 +46,7 @@ public class AirbendEffect extends SpellAbilityEffect {
final Player pl = sa.getActivatingPlayer(); final Player pl = sa.getActivatingPlayer();
final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa); final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa);
for (Card c : getTargetCards(sa)) { for (Card c : getTargetCards(sa)) {
final Card gameCard = game.getCardState(c, null); final Card gameCard = game.getCardState(c, null);
// gameCard is LKI in that case, the card is not in game anymore // gameCard is LKI in that case, the card is not in game anymore
@@ -55,7 +55,7 @@ public class AirbendEffect extends SpellAbilityEffect {
if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) { if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) {
continue; continue;
} }
if (!gameCard.canExiledBy(sa, true)) { if (!gameCard.canExiledBy(sa, true)) {
continue; continue;
} }
@@ -86,7 +86,7 @@ public class AirbendEffect extends SpellAbilityEffect {
} }
triggerList.triggerChangesZoneAll(game, sa); triggerList.triggerChangesZoneAll(game, sa);
handleExiledWith(triggerList.allCards(), sa); handleExiledWith(triggerList.allCards(), sa);
pl.triggerElementalBend(TriggerType.Airbend); pl.triggerElementalBend(TriggerType.Airbend);
} }

View File

@@ -928,7 +928,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
List<ZoneType> origin = Lists.newArrayList(); List<ZoneType> origin = Lists.newArrayList();
if (sa.hasParam("Origin")) { if (sa.hasParam("Origin")) {
origin = ZoneType.listValueOf(sa.getParam("Origin")); origin.addAll(ZoneType.listValueOf(sa.getParam("Origin")));
} }
ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));

View File

@@ -47,7 +47,7 @@ public class EarthbendEffect extends SpellAbilityEffect {
final Game game = source.getGame(); final Game game = source.getGame();
final Player pl = sa.getActivatingPlayer(); final Player pl = sa.getActivatingPlayer();
int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa); int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa);
long ts = game.getNextTimestamp(); long ts = game.getNextTimestamp();
String desc = "When it dies or is exiled, return it to the battlefield tapped."; String desc = "When it dies or is exiled, return it to the battlefield tapped.";
@@ -59,17 +59,17 @@ public class EarthbendEffect extends SpellAbilityEffect {
c.addNewPT(0, 0, ts, 0); c.addNewPT(0, 0, ts, 0);
c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false); c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false);
c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null); c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null);
GameEntityCounterTable table = new GameEntityCounterTable(); GameEntityCounterTable table = new GameEntityCounterTable();
c.addCounter(CounterEnumType.P1P1, num, pl, table); c.addCounter(CounterEnumType.P1P1, num, pl, table);
table.replaceCounterEffect(game, sa, true); table.replaceCounterEffect(game, sa, true);
buildTrigger(sa, c, sbTrigA, "Graveyard"); buildTrigger(sa, c, sbTrigA, "Graveyard");
buildTrigger(sa, c, sbTrigB, "Exile"); buildTrigger(sa, c, sbTrigB, "Exile");
} }
pl.triggerElementalBend(TriggerType.Earthbend); pl.triggerElementalBend(TriggerType.Earthbend);
} }
protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) { protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Game game = source.getGame(); final Game game = source.getGame();

View File

@@ -9,6 +9,7 @@ import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.card.CounterType; import forge.game.card.CounterType;
@@ -20,7 +21,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.CardTranslation; import forge.util.CardTranslation;
import forge.util.Localizer; import forge.util.Localizer;
import forge.util.collect.FCollection;
public class TimeTravelEffect extends SpellAbilityEffect { public class TimeTravelEffect extends SpellAbilityEffect {
@@ -41,10 +41,8 @@ public class TimeTravelEffect extends SpellAbilityEffect {
final CounterType counterType = CounterEnumType.TIME; final CounterType counterType = CounterEnumType.TIME;
for (int i = 0; i < num; i++) { for (int i = 0; i < num; i++) {
FCollection<Card> list = new FCollection<>();
// card you own that is suspended // card you own that is suspended
list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend())); CardCollection list = CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend());
// permanent you control with time counter // permanent you control with time counter
list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounter(counterType))); list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounter(counterType)));

View File

@@ -1120,13 +1120,14 @@ public class Player extends GameEntity implements Comparable<Player> {
getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave)); getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave));
} }
surveilThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.FirstTime, surveilThisTurn == 1); runParams.put(AbilityKey.FirstTime, surveilThisTurn == 0);
if (params != null) { if (params != null) {
runParams.putAll(params); runParams.putAll(params);
} }
getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false); getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false);
surveilThisTurn++;
} }
public int getSurveilThisTurn() { public int getSurveilThisTurn() {

View File

@@ -1,7 +1,6 @@
Name:Footsteps of the Goryo Name:Footsteps of the Goryo
ManaCost:2 B ManaCost:2 B
Types:Sorcery Arcane Types:Sorcery Arcane
A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature in your graveyard | GainControl$ True | SubAbility$ DBPump | AILogic$ BeforeCombat | SpellDescription$ Return target creature card from your graveyard to the battlefield. Sacrifice that creature at the beginning of the next end step. A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature in your graveyard | GainControl$ True | AtEOT$ Sacrifice | AILogic$ BeforeCombat | SpellDescription$ Return target creature card from your graveyard to the battlefield. Sacrifice that creature at the beginning of the next end step.
SVar:DBPump:DB$ Pump | Defined$ Targeted | AtEOT$ Sacrifice
AI:RemoveDeck:Random AI:RemoveDeck:Random
Oracle:Return target creature card from your graveyard to the battlefield. Sacrifice that creature at the beginning of the next end step. Oracle:Return target creature card from your graveyard to the battlefield. Sacrifice that creature at the beginning of the next end step.

View File

@@ -1,11 +1,9 @@
Name:Push the Limit Name:Push the Limit
ManaCost:5 R R ManaCost:5 R R
Types:Sorcery Types:Sorcery
A:SP$ ChangeZoneAll | Origin$ Graveyard | Destination$ Battlefield | ChangeType$ Mount.YouOwn,Vehicle.YouOwn | RememberChanged$ True | SubAbility$ DBPump | SpellDescription$ Return all Mount and Vehicle cards from your graveyard to the battlefield. Sacrifice them at the beginning of the next end step. Vehicles you control become artifact creatures until end of turn. Creatures you control gain haste until end of turn. A:SP$ ChangeZoneAll | Origin$ Graveyard | Destination$ Battlefield | ChangeType$ Mount.YouOwn,Vehicle.YouOwn | AtEOT$ Sacrifice | SubAbility$ DBAnimateAll | SpellDescription$ Return all Mount and Vehicle cards from your graveyard to the battlefield. Sacrifice them at the beginning of the next end step. Vehicles you control become artifact creatures until end of turn. Creatures you control gain haste until end of turn.
SVar:DBPump:DB$ Pump | Defined$ Remembered | AtEOT$ Sacrifice | SubAbility$ DBAnimateAll
SVar:DBAnimateAll:DB$ AnimateAll | Types$ Artifact,Creature | ValidCards$ Vehicle.YouCtrl | SubAbility$ DBPumpAll SVar:DBAnimateAll:DB$ AnimateAll | Types$ Artifact,Creature | ValidCards$ Vehicle.YouCtrl | SubAbility$ DBPumpAll
SVar:DBPumpAll:DB$ PumpAll | ValidCards$ Creature.YouCtrl | KW$ Haste | SubAbility$ DBCleanup SVar:DBPumpAll:DB$ PumpAll | ValidCards$ Creature.YouCtrl | KW$ Haste
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:PlayMain1:TRUE SVar:PlayMain1:TRUE
DeckHas:Ability$Graveyard DeckHas:Ability$Graveyard
DeckHints:Ability$Discard|Sacrifice DeckHints:Ability$Discard|Sacrifice

View File

@@ -2,8 +2,7 @@ Name:Wake the Dead
ManaCost:X B B ManaCost:X B B
Types:Instant Types:Instant
Text:Cast this spell only during combat on an opponent's turn. Text:Cast this spell only during combat on an opponent's turn.
A:SP$ ChangeZone | TargetMin$ X | TargetMax$ X | OpponentTurn$ True | ActivationPhases$ BeginCombat->EndCombat | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouOwn | TgtPrompt$ Select X target creatures in your graveyard | GainControl$ True | SubAbility$ DBPump | StackDescription$ Return X target creature cards [{c:Targeted}] from your graveyard to the battlefield. Sacrifice those creatures at the beginning of the next end step. | SpellDescription$ Return X target creature cards from your graveyard to the battlefield. Sacrifice those creatures at the beginning of the next end step. A:SP$ ChangeZone | TargetMin$ X | TargetMax$ X | OpponentTurn$ True | ActivationPhases$ BeginCombat->EndCombat | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouOwn | TgtPrompt$ Select X target creatures in your graveyard | GainControl$ True | AtEOT$ Sacrifice | StackDescription$ Return X target creature cards [{c:Targeted}] from your graveyard to the battlefield. Sacrifice those creatures at the beginning of the next end step. | SpellDescription$ Return X target creature cards from your graveyard to the battlefield. Sacrifice those creatures at the beginning of the next end step.
SVar:DBPump:DB$ Pump | Defined$ Targeted | AtEOT$ Sacrifice
SVar:X:Count$xPaid SVar:X:Count$xPaid
AI:RemoveDeck:All AI:RemoveDeck:All
DeckHas:Ability$Graveyard|Sacrifice DeckHas:Ability$Graveyard|Sacrifice

View File

@@ -408,15 +408,18 @@ public final class InputSelectTargets extends InputSyncronizedBase {
} }
private void removeTarget(final GameEntity ge) { private void removeTarget(final GameEntity ge) {
if (divisionValues != null) {
divisionValues.add(sa.getDividedValue(ge));
}
targets.remove(ge); targets.remove(ge);
sa.getTargets().remove(ge); sa.getTargets().remove(ge);
if (ge instanceof Card) { if (ge instanceof Card c) {
getController().getGui().setUsedToPay(CardView.get((Card) ge), false); getController().getGui().setUsedToPay(CardView.get(c), false);
// try to get last selected card // try to get last selected card
lastTarget = Iterables.getLast(IterableUtil.filter(targets, Card.class), null); lastTarget = Iterables.getLast(IterableUtil.filter(targets, Card.class), null);
} }
else if (ge instanceof Player) { else if (ge instanceof Player p) {
getController().getGui().setHighlighted(PlayerView.get((Player) ge), false); getController().getGui().setHighlighted(PlayerView.get(p), false);
} }
this.showMessage(); this.showMessage();