diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index fe2ab6e2814..8b5d5abb4d4 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -265,7 +265,7 @@ public class AiController { } } - if (!re.requirementsCheck(game, AbilityKey.newMap())) { + if (!re.requirementsCheck(game)) { continue; } SpellAbility exSA = re.getOverridingAbility(); diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java index 9d5decc6d22..671cef7a787 100644 --- a/forge-game/src/main/java/forge/game/GameEntity.java +++ b/forge-game/src/main/java/forge/game/GameEntity.java @@ -96,7 +96,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { if (!re.zonesCheck(getGame().getZoneOf(ca))) { continue; } - if (!re.requirementsCheck(getGame(), AbilityKey.newMap())) { + if (!re.requirementsCheck(getGame())) { continue; } // Immortal Coil prevents the damage but has a similar negative effect diff --git a/forge-game/src/main/java/forge/game/ability/AbilityKey.java b/forge-game/src/main/java/forge/game/ability/AbilityKey.java index dcd4af71b26..03426afa49c 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityKey.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityKey.java @@ -140,10 +140,6 @@ public enum AbilityKey { Valiant("Valiant"), Won("Won"), - // for AI prediction - Phase("Phase"), - PlayerTurn("PlayerTurn"), - // below used across different Replacements, don't reuse InternalTriggerTable("InternalTriggerTable"), SimultaneousETB("SimultaneousETB"); // for CR 614.13c diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index b520f79b68b..5a12d59f878 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -1856,6 +1856,10 @@ public class AbilityUtils { return doXMath(list.size(), expr, c, ctb); } + if (sq[0].equals("ActivatedThisGame")) { + return doXMath(sa.getActivationsThisGame(), expr, c, ctb); + } + if (sq[0].equals("ResolvedThisTurn")) { return doXMath(sa.getResolvedThisTurn(), expr, c, ctb); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersPutOrRemoveEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersPutOrRemoveEffect.java index 90c56c66c70..fa3027bc7ba 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersPutOrRemoveEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersPutOrRemoveEffect.java @@ -65,8 +65,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect { ctype = CounterType.getType(sa.getParam("CounterType")); } - final Player pl = !sa.hasParam("DefinedPlayer") ? sa.getActivatingPlayer() : - AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst(); + final Player pl = AbilityUtils.getDefinedPlayers(source, sa.getParam("DefinedPlayer"), sa).getFirst(); final boolean eachExisting = sa.hasParam("EachExistingCounter"); GameEntityCounterTable table = new GameEntityCounterTable(); @@ -79,7 +78,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect { if (gameCard == null || !tgtCard.equalsWithGameTimestamp(gameCard)) { continue; } - if (!eachExisting && sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null, + if (sa.hasParam("Optional") && !pl.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikePutRemoveCounters", ctype.getName(), CardTranslation.getTranslatedName(gameCard.getName())), null)) { continue; @@ -114,8 +113,6 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect { String prompt = Localizer.getInstance().getMessage("lblSelectCounterTypeToAddOrRemove"); CounterType chosenType = pc.chooseCounterType(list, sa, prompt, params); - params.put("CounterType", chosenType); - prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " "; boolean putCounter; if (sa.hasParam("RemoveConditionSVar")) { final Card host = sa.getHostCard(); @@ -137,6 +134,8 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect { } else if (!canReceive && canRemove) { putCounter = false; } else { + params.put("CounterType", chosenType); + prompt = Localizer.getInstance().getMessage("lblWhatToDoWithTargetCounter", chosenType.getName(), CardTranslation.getTranslatedName(tgtCard.getName())) + " "; putCounter = pc.chooseBinary(sa, prompt, BinaryChoiceType.AddOrRemove, params); } } diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index e654eddb868..f2ed62a23ab 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -65,6 +65,7 @@ import org.apache.commons.lang3.tuple.Triple; import java.util.*; import java.util.Map.Entry; +import java.util.concurrent.FutureTask; import static java.lang.Math.max; @@ -4912,22 +4913,23 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr return true; } - public final boolean canUntap(Player phase, boolean predict) { - if (!tapped) { return false; } + public final boolean canUntap(Player phase, Boolean predict) { + if (predict != null && predict) { + FutureTask proc = new FutureTask<>(() -> { + return canUntap(phase, null); + }); + return getGame().getPhaseHandler().withContext(proc, phase, PhaseType.UNTAP); + } + if (predict != null && !tapped) { return false; } if (phase != null && isExertedBy(phase)) { return false; } if (phase != null && (hasKeyword("CARDNAME doesn't untap during your untap step.") - || hasKeyword("This card doesn't untap during your next untap step.") - || hasKeyword("This card doesn't untap during your next two untap steps."))) { + || hasKeyword("This card doesn't untap during your next untap step."))) { return false; } Map runParams = AbilityKey.mapFromAffected(this); - if (predict) { - runParams.put(AbilityKey.PlayerTurn, phase); - runParams.put(AbilityKey.Phase, PhaseType.UNTAP); - } return !getGame().getReplacementHandler().cantHappenCheck(ReplacementType.Untap, runParams); } diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index af6a6c76be8..eabe90873f7 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -49,6 +49,8 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.time.StopWatch; import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; /** @@ -137,6 +139,23 @@ public class PhaseHandler implements java.io.Serializable { setPriority(playerTurn); } + public T withContext(FutureTask original, Player active, PhaseType pt) { + Player oldTurn = playerTurn; + PhaseType oldPhase = phase; + playerTurn = active; + phase = pt; + original.run(); + try { + return original.get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } finally { + playerTurn = oldTurn; + phase = oldPhase; + } + return null; + } + public final boolean inCombat() { return combat != null; } public final Combat getCombat() { return combat; } @@ -185,9 +204,7 @@ public class PhaseHandler implements java.io.Serializable { playerTurn.setNumPowerSurgeLands(lands); } - // Replacement effects final Map repRunParams = AbilityKey.mapFromAffected(playerTurn); - repRunParams.put(AbilityKey.Phase, phase); ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.BeginPhase, repRunParams); if (repres != ReplacementResult.NotReplaced) { // Currently there is no effect to skip entire beginning phase diff --git a/forge-game/src/main/java/forge/game/phase/Untap.java b/forge-game/src/main/java/forge/game/phase/Untap.java index 2af3403935d..a57dd5ac8c2 100644 --- a/forge-game/src/main/java/forge/game/phase/Untap.java +++ b/forge-game/src/main/java/forge/game/phase/Untap.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import com.google.common.collect.Lists; import com.google.common.collect.Maps; import forge.game.Game; @@ -164,10 +163,6 @@ public class Untap extends Phase { // TODO Replace with Static Abilities for (final Card c : active.getCardsIn(ZoneType.Battlefield)) { c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next untap step."); - if (c.hasKeyword("This card doesn't untap during your next two untap steps.")) { - c.removeHiddenExtrinsicKeyword("This card doesn't untap during your next two untap steps."); - c.addHiddenExtrinsicKeywords(game.getNextTimestamp(), 0, Lists.newArrayList("This card doesn't untap during your next untap step.")); - } } // remove exerted flags from all things in play diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java index 2d56219b44a..e56e8387831 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementEffect.java @@ -154,28 +154,26 @@ public abstract class ReplacementEffect extends TriggerReplacementBase { * * @return a boolean. */ - public boolean requirementsCheck(Game game, Map runParams) { + public boolean requirementsCheck(Game game) { if (this.isSuppressed()) { return false; // Effect removed by effect } if (hasParam("PlayerTurn")) { - Player active = (Player) runParams.getOrDefault(AbilityKey.PlayerTurn, game.getPhaseHandler().getPlayerTurn()); if (getParam("PlayerTurn").equals("True")) { - if (!active.equals(getHostCard().getController())) { + if (!game.getPhaseHandler().isPlayerTurn(getHostCard().getController())) { return false; } } else { List players = AbilityUtils.getDefinedPlayers(getHostCard(), getParam("PlayerTurn"), this); - if (!players.contains(active)) { + if (!players.contains(game.getPhaseHandler().getPlayerTurn())) { return false; } } } if (hasParam("ActivePhases")) { - PhaseType phase = (PhaseType) runParams.getOrDefault(AbilityKey.Phase, game.getPhaseHandler().getPhase()); - if (!PhaseType.parseRange(getParam("ActivePhases")).contains(phase)) { + if (!PhaseType.parseRange(getParam("ActivePhases")).contains(game.getPhaseHandler().getPhase())) { return false; } } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java index 3a2c6ea679f..4a170c45587 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplacementHandler.java @@ -18,6 +18,7 @@ package forge.game.replacement; import java.util.*; +import java.util.concurrent.FutureTask; import com.google.common.base.MoreObjects; import forge.game.card.*; @@ -127,7 +128,7 @@ public class ReplacementHandler { && replacementEffect.modeCheck(event, runParams) && !possibleReplacers.contains(replacementEffect) && replacementEffect.zonesCheck(cardZone) - && replacementEffect.requirementsCheck(game, runParams) + && replacementEffect.requirementsCheck(game) && replacementEffect.canReplace(runParams)) { possibleReplacers.add(replacementEffect); } @@ -855,15 +856,17 @@ public class ReplacementHandler { * Helper function to check if a phase would be skipped for AI. */ public boolean wouldPhaseBeSkipped(final Player player, final PhaseType phase) { - final Map repParams = AbilityKey.newMap(); - repParams.put(AbilityKey.PlayerTurn, player); - repParams.put(AbilityKey.Phase, phase); - List list = getReplacementList(ReplacementType.BeginPhase, repParams, ReplacementLayer.Control); - if (list.isEmpty()) { - return false; - } - return true; + FutureTask proc = new FutureTask<>(() -> { + final Map repParams = AbilityKey.newMap(); + List list = getReplacementList(ReplacementType.BeginPhase, repParams, ReplacementLayer.Control); + if (list.isEmpty()) { + return false; + } + return true; + }); + return player.getGame().getPhaseHandler().withContext(proc, player, phase); } + /** * Helper function to check if an extra turn would be skipped for AI. */ diff --git a/forge-gui/res/cardsfolder/f/fatespinner.txt b/forge-gui/res/cardsfolder/f/fatespinner.txt index 6a5f58ab8e1..626e1554381 100644 --- a/forge-gui/res/cardsfolder/f/fatespinner.txt +++ b/forge-gui/res/cardsfolder/f/fatespinner.txt @@ -3,7 +3,7 @@ ManaCost:1 U U Types:Creature Human Wizard PT:1/2 T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Opponent | Execute$ TrigSkipPhase | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of each opponent's upkeep, that player chooses draw step, main phase, or combat phase. The player skips each instance of the chosen step or phase this turn. -SVar:TrigSkipPhase:DB$ GenericChoice | Defined$ TriggeredPlayer | Choices$ FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat | ShowChoice$ ExceptSelf | AILogic$ Random +SVar:TrigSkipPhase:DB$ GenericChoice | Defined$ TriggeredPlayer | Choices$ FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat | ShowChoice$ ExceptSelf | AILogic$ Fatespinner SVar:FatespinnerSkipDraw:DB$ SkipPhase | Defined$ TriggeredPlayer | Step$ Draw | Duration$ EndOfTurn | SpellDescription$ Draw step SVar:FatespinnerSkipMain:DB$ SkipPhase | Defined$ TriggeredPlayer | Phase$ Main | Duration$ EndOfTurn | SpellDescription$ Main phase SVar:FatespinnerSkipCombat:DB$ SkipPhase | Defined$ TriggeredPlayer | Phase$ BeginCombat | Duration$ EndOfTurn | SpellDescription$ Combat phase diff --git a/forge-gui/res/cardsfolder/t/telekinesis.txt b/forge-gui/res/cardsfolder/t/telekinesis.txt index 62e9e094600..8559cd9a992 100644 --- a/forge-gui/res/cardsfolder/t/telekinesis.txt +++ b/forge-gui/res/cardsfolder/t/telekinesis.txt @@ -3,6 +3,9 @@ ManaCost:U U Types:Instant A:SP$ Tap | ValidTgts$ Creature | SubAbility$ DBEffect | SpellDescription$ Tap target creature. SVar:DBEffect:DB$ Effect | ReplacementEffects$ RPrevent | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | SubAbility$ DBPump | SpellDescription$ Prevent all combat damage that would be dealt by that creature this turn. -SVar:DBPump:DB$ Pump | Defined$ Targeted | KW$ HIDDEN This card doesn't untap during your next two untap steps. | Duration$ Permanent | SpellDescription$ It doesn't untap during its controller's next two untap steps. | StackDescription$ SpellDescription +SVar:DBPump:DB$ Effect | ReplacementEffects$ RUntap | Triggers$ ExileEff | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | Duration$ Permanent | StackDescription$ SpellDescription SVar:RPrevent:Event$ DamageDone | Prevent$ True | IsCombat$ True | ValidSource$ Card.IsRemembered | Description$ Prevent all combat damage that would be dealt by that creature this turn. +SVar:RUntap:Event$ Untap | ValidCard$ Creature.IsRemembered+ActivePlayerCtrl | Layer$ CantHappen | ActivePhases$ Untap | Description$ It doesn't untap during its controller's next two untap steps. +SVar:ExileEff:Mode$ Phase | Phase$ Untap | ValidPlayer$ Player.controlsCard.IsRemembered | Execute$ Exile | Static$ True +SVar:Exile:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile | ConditionCheckSVar$ Count$ActivatedThisGame Oracle:Tap target creature. Prevent all combat damage that would be dealt by that creature this turn. It doesn't untap during its controller's next two untap steps. diff --git a/forge-gui/res/cardsfolder/t/the_horus_heresy.txt b/forge-gui/res/cardsfolder/t/the_horus_heresy.txt index 8cde2e47372..9cd22cf582e 100644 --- a/forge-gui/res/cardsfolder/t/the_horus_heresy.txt +++ b/forge-gui/res/cardsfolder/t/the_horus_heresy.txt @@ -6,7 +6,7 @@ SVar:GainControl:DB$ GainControl | ValidTgts$ Creature.nonLegendary+OppCtrl | Lo SVar:OneEach:PlayerCountOpponents$Amount SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ X | SpellDescription$ Draw a card for each creature you control but don't own. SVar:X:Count$Valid Creature.YouDontOwn+YouCtrl -SVar:DBChoose:DB$ ChooseCard | Defined$ Player | StartingWith$ You | Choices$ Creature | ChoiceTitle$ Choose a creature | Mandatory$ True | SpellDescription$ Starting with you, each player chooses a creature. Destroy each creature chosen this way. +SVar:DBChoose:DB$ ChooseCard | Defined$ Player | StartingWith$ You | Choices$ Creature | ChoiceTitle$ Choose a creature | Mandatory$ True | SubAbility$ DBDestroy | SpellDescription$ Starting with you, each player chooses a creature. Destroy each creature chosen this way. SVar:DBDestroy:DB$ Destroy | Defined$ ChosenCard | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearChosenCard$ True Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)\nI — For each opponent, gain control of up to one target nonlegendary creature that player controls for as long as The Horus Heresy remains on the battlefield.\nII — Draw a card for each creature you control but don't own.\nIII — Starting with you, each player chooses a creature. Destroy each creature chosen this way.