From 5222767891b479993f956a4480e34fbaa5138222 Mon Sep 17 00:00:00 2001 From: Klisz Date: Sat, 30 Dec 2023 02:35:10 -0700 Subject: [PATCH 01/15] Update ravens_run.txt (#4446) --- forge-gui/res/cardsfolder/r/ravens_run.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/res/cardsfolder/r/ravens_run.txt b/forge-gui/res/cardsfolder/r/ravens_run.txt index 1d1b83ed5c0..9a569083e30 100644 --- a/forge-gui/res/cardsfolder/r/ravens_run.txt +++ b/forge-gui/res/cardsfolder/r/ravens_run.txt @@ -2,7 +2,7 @@ Name:Raven's Run ManaCost:no cost Types:Plane Shadowmoor S:Mode$ Continuous | EffectZone$ Command | Affected$ Creature | AddKeyword$ Wither | Description$ All Creatures have Wither (They deal damage to creatures in the form of -1/-1 counters.) -T:Mode$ ChaosEnsues | OptionalDecider$ You | TriggerZones$ Command | Execute$ TrigPutCounter | TriggerDescription$ Whenever chaos ensues, put a -1/-1 counter on target creature, two -1/-1 counters on another target creature, and three -1/-1 counters on a third target creature. +T:Mode$ ChaosEnsues | TriggerZones$ Command | Execute$ TrigPutCounter | TriggerDescription$ Whenever chaos ensues, put a -1/-1 counter on target creature, two -1/-1 counters on another target creature, and three -1/-1 counters on a third target creature. SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature | TargetUnique$ True | CounterType$ M1M1 | SubAbility$ DBPutTwo SVar:DBPutTwo:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select another target creature | TargetUnique$ True | CounterType$ M1M1 | CounterNum$ 2 | SubAbility$ DBPutThree SVar:DBPutThree:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select a third target creature | TargetUnique$ True | CounterType$ M1M1 | CounterNum$ 3 From 08309f299d8c911131204cf0b9ba07fda9077a70 Mon Sep 17 00:00:00 2001 From: Klisz Date: Sat, 30 Dec 2023 02:37:25 -0700 Subject: [PATCH 02/15] Update rhuk_hexgold_nabber.txt (#4445) --- forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt b/forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt index e20997d2055..41a71913429 100644 --- a/forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt +++ b/forge-gui/res/cardsfolder/r/rhuk_hexgold_nabber.txt @@ -4,8 +4,8 @@ Types:Legendary Creature Goblin Rebel PT:2/2 K:Trample K:Haste -T:Mode$ Attacks | ValidCard$ Creature.equipped+Other | Execute$ TrigAttach | TriggerZones$ Battlefield | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. -T:Mode$ ChangesZone | ValidCard$ Creature.equipped+Other | Origin$ Battlefield | Destination$ Graveyard | Execute$ TrigAttach | TriggerZones$ Battlefield | OptionalDecider$ You | Secondary$ True | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. +T:Mode$ Attacks | ValidCard$ Creature.equipped+YouCtrl+Other | Execute$ TrigAttach | TriggerZones$ Battlefield | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. +T:Mode$ ChangesZone | ValidCard$ Creature.equipped+YouCtrl+Other | Origin$ Battlefield | Destination$ Graveyard | Execute$ TrigAttach | TriggerZones$ Battlefield | OptionalDecider$ You | Secondary$ True | TriggerDescription$ Whenever an equipped creature you control other than CARDNAME attacks or dies, you may attach all Equipment attached to that creature to NICKNAME. SVar:TrigAttach:DB$ Attach | Object$ AttachedTo TriggeredCard.Equipment | Defined$ Self DeckNeeds:Type$Equipment Oracle:Trample, haste\nWhenever an equipped creature you control other than Rhuk, Hexgold Nabber attacks or dies, you may attach all Equipment attached to that creature to Rhuk. From 78cfe1d83936798ffce127dd39f4e236af33b202 Mon Sep 17 00:00:00 2001 From: tool4ever Date: Sat, 30 Dec 2023 09:47:12 +0000 Subject: [PATCH 03/15] Update veil_of_secrecy.txt --- forge-gui/res/cardsfolder/v/veil_of_secrecy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/res/cardsfolder/v/veil_of_secrecy.txt b/forge-gui/res/cardsfolder/v/veil_of_secrecy.txt index 5b94eb0d24e..3de962e2c61 100644 --- a/forge-gui/res/cardsfolder/v/veil_of_secrecy.txt +++ b/forge-gui/res/cardsfolder/v/veil_of_secrecy.txt @@ -3,7 +3,7 @@ ManaCost:1 U Types:Instant Arcane K:Splice:Arcane:Return<1/Creature.Blue/blue creature> A:SP$ Pump | ValidTgts$ Creature | KW$ Shroud | SubAbility$ DBUnblockable | StackDescription$ REP Target creature_{c:Targeted} | SpellDescription$ Target creature gains shroud until end of turn and can't be blocked this turn. (A creature with shroud can't be the target of spells or abilities.) -SVar:DBUnblockable:DB$ Effect | RememberObjects$ Self | ExileOnMoved$ Battlefield | StaticAbilities$ Unblockable +SVar:DBUnblockable:DB$ Effect | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | StaticAbilities$ Unblockable SVar:Unblockable:Mode$ CantBlockBy | ValidAttacker$ Card.IsRemembered | Description$ This creature can't be blocked this turn. AI:RemoveDeck:All DeckHints:Type$Arcane From c29b6b7fcb3288eac5d17ed0c9b2267cda4533e0 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sat, 30 Dec 2023 16:35:46 +0100 Subject: [PATCH 04/15] GameAction: stop always trigger before etb counter (#4449) * GameAction: stop always trigger before etb counter * ~ always clear trigger and check for static abilities again --- .../src/main/java/forge/game/GameAction.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 5c24ac41b08..8af6addff4b 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -583,14 +583,19 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer } } - // Need to apply any static effects to produce correct triggers - checkStaticAbilities(); - - if (table.replaceCounterEffect(game, null, true, true, params)) { - // update static abilities after etb counters have been placed + if (!table.isEmpty()) { + // we don't want always trigger before counters are placed + game.getTriggerHandler().suppressMode(TriggerType.Always); + // Need to apply any static effects to produce correct triggers checkStaticAbilities(); } + table.replaceCounterEffect(game, null, true, true, params); + + // update static abilities after etb counters have been placed + game.getTriggerHandler().clearSuppression(TriggerType.Always); + checkStaticAbilities(); + // 400.7g try adding keyword back into card if it doesn't already have it if (zoneTo.is(ZoneType.Stack) && cause != null && cause.isSpell() && !cause.isIntrinsic() && c.equals(cause.getHostCard())) { if (cause.getKeyword() != null && !copied.getKeywords().contains(cause.getKeyword())) { From d58f564a8432e534bdc3c5876b2bc32796600ec7 Mon Sep 17 00:00:00 2001 From: Klisz Date: Sat, 30 Dec 2023 23:10:51 -0700 Subject: [PATCH 05/15] WHO 4 cards (#4444) * WHO 3 cards * This Is How It Ends, plus DeckHints/DeckHas --- .../res/cardsfolder/s/sycorax_commander.txt | 16 ++++++++++++++++ .../res/cardsfolder/t/the_dalek_emperor.txt | 13 +++++++++++++ .../cardsfolder/t/the_master_gallifreys_end.txt | 15 +++++++++++++++ .../res/cardsfolder/t/this_is_how_it_ends.txt | 8 ++++++++ 4 files changed, 52 insertions(+) create mode 100644 forge-gui/res/cardsfolder/s/sycorax_commander.txt create mode 100644 forge-gui/res/cardsfolder/t/the_dalek_emperor.txt create mode 100644 forge-gui/res/cardsfolder/t/the_master_gallifreys_end.txt create mode 100644 forge-gui/res/cardsfolder/t/this_is_how_it_ends.txt diff --git a/forge-gui/res/cardsfolder/s/sycorax_commander.txt b/forge-gui/res/cardsfolder/s/sycorax_commander.txt new file mode 100644 index 00000000000..f19e0ff0aec --- /dev/null +++ b/forge-gui/res/cardsfolder/s/sycorax_commander.txt @@ -0,0 +1,16 @@ +Name:Sycorax Commander +ManaCost:2 B R +Types:Creature Alien Soldier +PT:4/2 +K:First strike +K:Haste +T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigChoice | TriggerDescription$ Sanctified Rules of Combat — When CARDNAME enters the battlefield, each opponent faces a villainous choice — That opponent discards all the cards in their hand, then draws that many cards minus one, or CARDNAME deals damage to that player equal to the number of cards in their hand. +SVar:TrigChoice:DB$ VillainousChoice | Defined$ Opponent | Choices$ DBDiscard,DBDamage +SVar:DBDiscard:DB$ Discard | Defined$ Player.IsRemembered | Mode$ Hand | RememberDiscarded$ True | SubAbility$ DBDraw | SpellDescription$ Opponent discards their hand, then draws that many cards minus one. +SVar:DBDraw:DB$ Draw | NumCards$ X | Defined$ Player.IsRemembered | SubAbility$ CleanDrawn +SVar:CleanDrawn:DB$ Cleanup | ClearRemembered$ True +# This calculation isn't considering the remembered player, only the remembered cards +SVar:X:Remembered$Amount/Minus.1 +SVar:DBDamage:DB$ DealDamage | Defined$ Player.IsRemembered | NumDmg$ Y | SpellDescription$ CARDNAME deals damage to opponent equal to the number of cards in their hand. +SVar:Y:Count$ValidHand Card.OwnedBy Player.IsRemembered +Oracle:First strike, haste\nSanctified Rules of Combat — When Sycorax Commander enters the battlefield, each opponent faces a villainous choice — That opponent discards all the cards in their hand, then draws that many cards minus one, or Sycorax Commander deals damage to that player equal to the number of cards in their hand. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/t/the_dalek_emperor.txt b/forge-gui/res/cardsfolder/t/the_dalek_emperor.txt new file mode 100644 index 00000000000..944bc456248 --- /dev/null +++ b/forge-gui/res/cardsfolder/t/the_dalek_emperor.txt @@ -0,0 +1,13 @@ +Name:The Dalek Emperor +ManaCost:5 B R +Types:Legendary Artifact Creature Dalek +PT:6/6 +K:Affinity:Dalek +S:Mode$ Continuous | Affected$ Dalek.Other+YouCtrl | AddKeyword$ Haste | Description$ Other Daleks you control have haste. +T:Mode$ Phase | Phase$ BeginCombat | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigChoice | TriggerDescription$ At the beginning of combat on your turn, each opponent faces a villainous choice — That player sacrifices a creature they control, or you create a 3/3 black Dalek artifact creature token with menace. +SVar:TrigChoice:DB$ VillainousChoice | Defined$ Opponent | Choices$ DBSacrifice,DBToken +SVar:DBSacrifice:DB$ Sacrifice | Defined$ Remembered | SacValid$ Creature | SacMessage$ Creature | SpellDescription$ Opponent sacrifices a creature. +SVar:DBToken:DB$ Token | TokenAmount$ 1 | TokenScript$ b_3_3_a_dalek_menace | TokenOwner$ You | SpellDescription$ You create a 3/3 black Dalek artifact creature token with menace. +DeckHints:Type$Dalek +DeckHas:Ability$Token +Oracle:Affinity for Daleks (This spell costs {1} less to cast for each Dalek you control.)\nOther Daleks you control have haste.\nAt the beginning of combat on your turn, each opponent faces a villainous choice — That player sacrifices a creature they control, or you create a 3/3 black Dalek artifact creature token with menace. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/t/the_master_gallifreys_end.txt b/forge-gui/res/cardsfolder/t/the_master_gallifreys_end.txt new file mode 100644 index 00000000000..9ff3b433323 --- /dev/null +++ b/forge-gui/res/cardsfolder/t/the_master_gallifreys_end.txt @@ -0,0 +1,15 @@ +Name:The Master, Gallifrey's End +ManaCost:2 B R +Types:Legendary Creature Time Lord Rogue +PT:4/3 +T:Mode$ ChangesZone | ValidCard$ Creature.Artifact+nonToken+YouCtrl | Origin$ Battlefield | Destination$ Graveyard | OptionalDecider$ You | Execute$ TrigExile | TriggerZones$ Battlefield | TriggerDescription$ Make Them Pay — Whenever a nontoken artifact creature you control dies, you may exile it. If you do, choose an opponent with the most life among your opponents. That player faces a villainous choice — They lose 4 life, or you create a token that's a copy of that card. +SVar:TrigExile:DB$ ChangeZone | RememberChanged$ True | Origin$ Graveyard | Destination$ Exile | Defined$ TriggeredNewCardLKICopy | SubAbility$ DBChoosePlayer +SVar:DBChoosePlayer:DB$ ChoosePlayer | Defined$ You | Choices$ Player.Opponent+lifeEQX | SubAbility$ DBChoice +SVar:X:Count$OppGreatestLifeTotal +SVar:DBChoice:DB$ VillainousChoice | Defined$ ChosenPlayer | Choices$ DBLoseLife,DBCopy | SubAbility$ DBCleanup +SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 4 | Defined$ Player.IsRemembered | SpellDescription$ Opponent loses 4 life. +SVar:DBCopy:DB$ CopyPermanent | Defined$ Remembered | SubAbility$ DBCleanup | SpellDescription$ You create a token that's a copy of the exiled card. +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True +DeckHints:Type$Artifact +DeckHas:Ability$Token +Oracle:Make Them Pay — Whenever a nontoken artifact creature you control dies, you may exile it. If you do, choose an opponent with the most life among your opponents. That player faces a villainous choice — They lose 4 life, or you create a token that's a copy of that card. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/t/this_is_how_it_ends.txt b/forge-gui/res/cardsfolder/t/this_is_how_it_ends.txt new file mode 100644 index 00000000000..406ed4b2f74 --- /dev/null +++ b/forge-gui/res/cardsfolder/t/this_is_how_it_ends.txt @@ -0,0 +1,8 @@ +Name:This Is How It Ends +ManaCost:3 B +Types:Instant +A:SP$ ChangeZone | ValidTgts$ Creature | TgtPrompt$ Select target creature | Origin$ Battlefield | Destination$ Library | Shuffle$ True | SubAbility$ DBChoice | SpellDescription$ The owner of target creature shuffles it into their library, +SVar:DBChoice:DB$ VillainousChoice | Defined$ TargetedOwner | Choices$ DBLoseLife,DBShuffleAnother | SpellDescription$ then faces a villainous choice — They lose 5 life, or they shuffle another creature they own into their library. +SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 5 | Defined$ Remembered | SpellDescription$ Targeted creature's owner loses 5 life. +SVar:DBShuffleAnother:DB$ ChangeZone | ChangeType$ Creature.OwnedBy Remembered | DefinedPlayer$ Remembered | Chooser$ Remembered | ChangeNum$ 1 | Mandatory$ True | Origin$ Battlefield | Destination$ Library | Shuffle$ True | Hidden$ True | SpellDescription$ Targeted creature's owner shuffles another creature they own into their library. +Oracle:Target creature's owner shuffles it into their library, then faces a villainous choice — They lose 5 life, or they shuffle another creature they own into their library. \ No newline at end of file From c02f7c698dc6bbfb0a841b52439aae80bcb29265 Mon Sep 17 00:00:00 2001 From: Klisz Date: Sat, 30 Dec 2023 23:11:30 -0700 Subject: [PATCH 06/15] Planechase inherent effects (#4408) * Planechase effects draft Draft to close #4262, by moving the planeswalking ability and the static chaos trigger to an effect carried with the player in a planechase game. Currently this effect is visible in the command zone much like the monarch or initiative (but with no image), which is something that needs fixing; I'd have it be a DetachedCardEffect but there's no linked card for it like there are for commander and companion effects (the active plane [or the face-up plane with the earliest timestamp, I suppose] would be a logical choice for the getCardForUi, but that would require updating it as the plane changes, and in any case because there's a separate effect for each player, the fact that the owner of a DetachedCardEffect is set as the owner of the linked card also adds some wrinkles. In any case, I've set the effects to be created in startGame after initPlane is run, rather than in initVariantZones as is the case for the commander and companion DetachedCardEffects, in anticipation of hopefully in the future making the effect be linked to an active plane). * Update Player.java * Move planar dice special action to effect * Update ComputerUtilAbility.java * Move die roll chaos to PlanarDice.java --- .../java/forge/ai/ComputerUtilAbility.java | 23 +++++++++++--- .../forge/ai/ability/RollPlanarDiceAi.java | 11 +++++-- .../src/main/java/forge/game/GameAction.java | 3 ++ .../src/main/java/forge/game/PlanarDice.java | 5 +++ .../java/forge/game/card/CardFactory.java | 31 ------------------- .../main/java/forge/game/player/Player.java | 30 ++++++++++++++++++ 6 files changed, 66 insertions(+), 37 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index d8750ad2e02..13fbee4c46c 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -255,10 +255,25 @@ public int compareEvaluator(final SpellAbility a, final SpellAbility b, boolean } // deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True - if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { - return 1; - } else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { - return -1; + if (ApiType.RollPlanarDice == a.getApi() || ApiType.RollPlanarDice == b.getApi()) { + Card hostCardForGame = a.getHostCard(); + if (hostCardForGame == null) { + if (b.getHostCard() != null) { + hostCardForGame = b.getHostCard(); + } else { + return 0; // fallback if neither SA have a host card somehow + } + } + Game game = hostCardForGame.getGame(); + for (Card c : game.getActivePlanes()) { + if (c.hasSVar("AIRollPlanarDieParams") && c.getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { + if (ApiType.RollPlanarDice == a.getApi()) { + return 1; + } else { + return -1; + } + } + } } // deprioritize pump spells with pure energy cost (can be activated last, diff --git a/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java b/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java index ca686734f15..32b21c964cb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java @@ -19,9 +19,16 @@ public class RollPlanarDiceAi extends SpellAbilityAi { */ @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); - Card plane = sa.getHostCard(); + for (Card c : ai.getGame().getActivePlanes()) { + if (willRollOnPlane(ai, c)) { + return true; + } + } + return false; + } + private boolean willRollOnPlane(Player ai, Card plane) { + AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); boolean decideToRoll = false; boolean rollInMain1 = false; String modeName = "never"; diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 8af6addff4b..0304efda411 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -2090,6 +2090,9 @@ public void startGame(GameOutcome lastGameOutcome, Runnable startGameHook) { // if (game.getRules().hasAppliedVariant(GameType.Planechase)) { first.initPlane(); + for (final Player p1 : game.getPlayers()) { + p1.createPlanechaseEffects(game); + } } first = runOpeningHandActions(first); diff --git a/forge-game/src/main/java/forge/game/PlanarDice.java b/forge-game/src/main/java/forge/game/PlanarDice.java index 7f0b92c7f60..11dcc7d442b 100644 --- a/forge-game/src/main/java/forge/game/PlanarDice.java +++ b/forge-game/src/main/java/forge/game/PlanarDice.java @@ -87,6 +87,11 @@ else if (i == 1) runParams.put(AbilityKey.Result, Arrays.asList(0)); roller.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDieOnce, runParams, false); + if (res == Chaos) { + runParams = AbilityKey.mapFromPlayer(roller); + roller.getGame().getTriggerHandler().runTrigger(TriggerType.ChaosEnsues, runParams, false); + } + return res; } diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 7c3be8266c2..0624524101e 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -318,42 +318,11 @@ private static void buildAbilities(final Card card) { // ****************************************************************** // ************** Link to different CardFactories ******************* - if (card.isPlane()) { - buildPlaneAbilities(card); - } buildBattleAbilities(card); CardFactoryUtil.setupKeywordedAbilities(card); // Should happen AFTER setting left/right split abilities to set Fuse ability to both sides card.updateStateForView(); } - private static void buildPlaneAbilities(Card card) { - String trigger = "Mode$ PlanarDice | Result$ Planeswalk | TriggerZones$ Command | Secondary$ True | " + - "TriggerDescription$ Whenever you roll the Planeswalker symbol on the planar die, planeswalk."; - - String rolledWalk = "DB$ Planeswalk | Cause$ PlanarDie"; - - Trigger planesWalkTrigger = TriggerHandler.parseTrigger(trigger, card, true); - planesWalkTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledWalk, card)); - card.addTrigger(planesWalkTrigger); - - String chaosTrig = "Mode$ PlanarDice | Result$ Chaos | TriggerZones$ Command | Static$ True"; - - String rolledChaos = "DB$ ChaosEnsues"; - - Trigger chaosTrigger = TriggerHandler.parseTrigger(chaosTrig, card, true); - chaosTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledChaos, card)); - card.addTrigger(chaosTrigger); - - String specialA = "ST$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | Activator$ Player | SpecialAction$ True" + - " | ActivationZone$ Command | SpellDescription$ Roll the planar dice. X is equal to the number of " + - "times you have previously taken this action this turn. | CostDesc$ {X}: "; - - SpellAbility planarRoll = AbilityFactory.getAbility(specialA, card); - planarRoll.setSVar("X", "Count$PlanarDiceSpecialActionThisTurn"); - - card.addSpellAbility(planarRoll); - } - private static void buildBattleAbilities(Card card) { if (!card.isBattle()) { return; diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 9b9bc6cb576..230ae9e9e71 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -3176,6 +3176,36 @@ else if (game.getRules().hasAppliedVariant(GameType.Oathbreaker)) { return eff; } + public void createPlanechaseEffects(Game game) { + final PlayerZone com = getZone(ZoneType.Command); + final String name = "Planar Dice"; + final Card eff = new Card(game.nextCardId(), game); + eff.setTimestamp(game.getNextTimestamp()); + eff.setName(name); + eff.setOwner(this); + eff.setImmutable(true); + String image = ImageKeys.getTokenKey("planechase"); + eff.setImageKey(image); + + String trigger = "Mode$ PlanarDice | Result$ Planeswalk | TriggerZones$ Command | ValidPlayer$ You | Secondary$ True | " + + "TriggerDescription$ Whenever you roll the Planeswalker symbol on the planar die, planeswalk."; + String rolledWalk = "DB$ Planeswalk | Cause$ PlanarDie"; + Trigger planesWalkTrigger = TriggerHandler.parseTrigger(trigger, eff, true); + planesWalkTrigger.setOverridingAbility(AbilityFactory.getAbility(rolledWalk, eff)); + eff.addTrigger(planesWalkTrigger); + + String specialA = "ST$ RollPlanarDice | Cost$ X | SorcerySpeed$ True | Activator$ Player | SpecialAction$ True" + + " | ActivationZone$ Command | SpellDescription$ Roll the planar dice. X is equal to the number of " + + "times you have previously taken this action this turn. | CostDesc$ {X}: "; + SpellAbility planarRoll = AbilityFactory.getAbility(specialA, eff); + planarRoll.setSVar("X", "Count$PlanarDiceSpecialActionThisTurn"); + eff.addSpellAbility(planarRoll); + + eff.updateStateForView(); + com.add(eff); + this.updateZoneForView(com); + } + public void createTheRing(Card host) { final PlayerZone com = getZone(ZoneType.Command); if (theRing == null) { From bfe41dcd48149be87ef1fc3a67b57d9eb712a826 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 31 Dec 2023 07:54:03 +0100 Subject: [PATCH 07/15] ~ fix First Strike CounterType --- forge-gui/res/cardsfolder/m/me_the_immortal.txt | 2 +- forge-gui/res/cardsfolder/s/sycorax_commander.txt | 2 +- forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/forge-gui/res/cardsfolder/m/me_the_immortal.txt b/forge-gui/res/cardsfolder/m/me_the_immortal.txt index a554bcae7f9..46bb9a3ec46 100644 --- a/forge-gui/res/cardsfolder/m/me_the_immortal.txt +++ b/forge-gui/res/cardsfolder/m/me_the_immortal.txt @@ -3,7 +3,7 @@ ManaCost:2 G U R Types:Legendary Creature Human Rogue PT:3/3 T:Mode$ Phase | Phase$ BeginCombat | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigCounter | TriggerDescription$ At the beginning of combat on your turn, put your choice of a +1/+1, first strike, vigilance, or menace counter on CARDNAME. -SVar:TrigCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1,First strike,Vigilance,Menace | CounterNum$ 1 +SVar:TrigCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1,First Strike,Vigilance,Menace | CounterNum$ 1 K:Counters remain on CARDNAME as it moves to any zone other than a player's hand or library. SVar:AltCost:Cost$ 2 G U R Discard<2/Card> | ActivationZone$ Graveyard | Description$ You may cast NICKNAME from your graveyard by discarding two cards in addition to paying its other costs. DeckHas:Ability$Counters|Discard diff --git a/forge-gui/res/cardsfolder/s/sycorax_commander.txt b/forge-gui/res/cardsfolder/s/sycorax_commander.txt index f19e0ff0aec..eabf8d7546b 100644 --- a/forge-gui/res/cardsfolder/s/sycorax_commander.txt +++ b/forge-gui/res/cardsfolder/s/sycorax_commander.txt @@ -2,7 +2,7 @@ Name:Sycorax Commander ManaCost:2 B R Types:Creature Alien Soldier PT:4/2 -K:First strike +K:First Strike K:Haste T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigChoice | TriggerDescription$ Sanctified Rules of Combat — When CARDNAME enters the battlefield, each opponent faces a villainous choice — That opponent discards all the cards in their hand, then draws that many cards minus one, or CARDNAME deals damage to that player equal to the number of cards in their hand. SVar:TrigChoice:DB$ VillainousChoice | Defined$ Opponent | Choices$ DBDiscard,DBDamage diff --git a/forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt b/forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt index 5eeb21ee932..248e1a068a4 100644 --- a/forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt +++ b/forge-gui/res/cardsfolder/t/the_night_of_the_doctor.txt @@ -4,6 +4,6 @@ Types:Enchantment Saga K:Chapter:2:DBDestroyAll,DBChangeZone SVar:DBDestroyAll:DB$ DestroyAll | ValidCards$ Creature | SpellDescription$ Destroy all creatures. SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.Legendary+YouCtrl | TgtPrompt$ Choose target legendary creature card in your graveyard | SubAbility$ DBPutCounter | RememberChanged$ True | SpellDescription$ Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it. -SVar:DBPutCounter:DB$ PutCounter | Choices$ Creature.IsRemembered | CounterType$ First strike,Vigilance,Lifelink | CounterNum$ 1| SubAbility$ DBCleanup +SVar:DBPutCounter:DB$ PutCounter | Choices$ Creature.IsRemembered | CounterType$ First Strike,Vigilance,Lifelink | CounterNum$ 1| SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True Oracle:(As this Saga enters and after your draw step, add a lore counter. Sacrifice after II.)\nI — Destroy all creatures.\nII — Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it. From f7d70f1a693acb73ec75cc053f8665a2ff3813a3 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Sun, 31 Dec 2023 11:12:28 +0100 Subject: [PATCH 08/15] Riot: refactor into UnlessCost like Fabricate (#4448) --- .../src/main/java/forge/ai/ComputerUtil.java | 5 +- .../java/forge/ai/ComputerUtilCombat.java | 6 +- .../main/java/forge/ai/ComputerUtilCost.java | 4 +- .../main/java/forge/ai/ComputerUtilMana.java | 4 +- .../java/forge/ai/PlayerControllerAi.java | 3 + .../main/java/forge/ai/SpecialAiLogic.java | 47 ++++++++++++++ .../ai/ability/ChooseGenericEffectAi.java | 54 ---------------- .../src/main/java/forge/game/card/Card.java | 4 ++ .../java/forge/game/card/CardFactoryUtil.java | 18 ++---- .../java/forge/game/cost/CostPayment.java | 4 +- .../java/forge/game/cost/CostPutCounter.java | 64 +++++++++++++++++-- .../game/spellability/AbilityActivated.java | 2 +- .../java/forge/game/spellability/Spell.java | 2 +- .../forge/game/spellability/SpellAbility.java | 3 + 14 files changed, 135 insertions(+), 85 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index e565ef58c76..0655364b8d2 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -30,7 +30,6 @@ import com.google.common.collect.Multimap; import forge.ai.AiCardMemory.MemorySet; -import forge.ai.ability.ChooseGenericEffectAi; import forge.ai.ability.ProtectAi; import forge.ai.ability.TokenAi; import forge.card.CardStateName; @@ -290,7 +289,7 @@ public static final boolean playSpellAbilityWithoutPayingManaCost(final Player a SpellAbility newSA = sa.copyWithNoManaCost(); newSA.setActivatingPlayer(ai, true); - if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) { + if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA, false) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) { return false; } @@ -1176,7 +1175,7 @@ public static boolean castPermanentInMain1(final Player ai, final SpellAbility s return true; } - if (cardState.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) { + if (cardState.hasKeyword(Keyword.RIOT) && SpecialAiLogic.preferHasteForRiot(sa, ai)) { // Planning to choose Haste for Riot, so do this in Main 1 return true; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index 7cbcacda85a..b2a6d08235d 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -1257,7 +1257,7 @@ public static int predictPowerBonusOfAttacker(final Card attacker, final Card bl sa.setActivatingPlayer(source.getController(), true); if (sa.hasParam("Cost")) { - if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) { continue; } } @@ -1454,7 +1454,7 @@ public static int predictToughnessBonusOfAttacker(final Card attacker, final Car continue; } if (sa.hasParam("Cost")) { - if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) { continue; } } @@ -1488,7 +1488,7 @@ public static int predictToughnessBonusOfAttacker(final Card attacker, final Car continue; } if (sa.hasParam("Cost")) { - if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) { + if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) { continue; } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index a6dee98dfc1..d391af96602 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -636,7 +636,7 @@ public boolean apply(Card card) { } return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect) - && CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa); + && CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, effect); } public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView payers) { @@ -760,6 +760,8 @@ else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substri int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList); return evalToken < evalCounter; + } else if ("Riot".equals(aiLogic)) { + return !SpecialAiLogic.preferHasteForRiot(sa, payer); } // Check for shocklands and similar ETB replacement effects diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 45089d914d5..fbeadcbfe89 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -976,7 +976,7 @@ private static boolean canPayShardWithSpellAbility(ManaCostShard toPay, Player a // Check if AI can still play this mana ability ma.setActivatingPlayer(ai, true); // if the AI can't pay the additional costs skip the mana ability - if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) { + if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) { return false; } else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) { return false; @@ -1517,7 +1517,7 @@ public boolean apply(final Card c) { if (cost != null) { // if the AI can't pay the additional costs skip the mana ability m.setActivatingPlayer(ai, true); - if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m)) { + if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m, false)) { continue; } diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 853571bac00..7d0e6ecab39 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1063,6 +1063,9 @@ public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alread final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } }; emptyAbility.setActivatingPlayer(player, true); emptyAbility.setTriggeringObjects(sa.getTriggeringObjects()); + emptyAbility.setReplacingObjects(sa.getReplacingObjects()); + emptyAbility.setTrigger(sa.getTrigger()); + emptyAbility.setReplacementEffect(sa.getReplacementEffect()); emptyAbility.setSVars(sa.getSVars()); emptyAbility.setCardState(sa.getCardState()); emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid()); diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java index 4ea6cb514a7..a4241778f79 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -2,6 +2,8 @@ import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + import forge.ai.ability.TokenAi; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -13,6 +15,7 @@ import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; import forge.util.Aggregates; import forge.util.Expressions; @@ -398,4 +401,48 @@ public static boolean doBranchCounterspellLogic(final Player ai, final SpellAbil } return willPlay; } + + public static boolean preferHasteForRiot(SpellAbility sa, Player player) { + // returning true means preferring Haste, returning false means preferring a +1/+1 counter + final Card host = sa.getHostCard(); + final Game game = host.getGame(); + final Card copy = CardUtil.getLKICopy(host); + copy.setLastKnownZone(player.getZone(ZoneType.Battlefield)); + + // check state it would have on the battlefield + CardCollection preList = new CardCollection(copy); + game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList); + // reset again? + game.getAction().checkStaticAbilities(false); + + // can't gain counters, use Haste + if (!copy.canReceiveCounters(CounterEnumType.P1P1)) { + return true; + } + + // already has Haste, use counter + if (copy.hasKeyword(Keyword.HASTE)) { + return false; + } + + // not AI turn + if (!game.getPhaseHandler().isPlayerTurn(player)) { + return false; + } + + // not before Combat + if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { + return false; + } + + // TODO check other opponents too if able + final Player opp = player.getWeakestOpponent(); + if (opp != null) { + // TODO add predict Combat Damage? + return opp.getLife() < copy.getNetPower(); + } + + // haste might not be good enough? + return false; + } } \ No newline at end of file diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java index 9edb682eabc..b63244ce892 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java @@ -3,7 +3,6 @@ import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; @@ -11,9 +10,7 @@ import forge.card.MagicColor; import forge.game.Game; import forge.game.card.*; -import forge.game.combat.Combat; import forge.game.cost.Cost; -import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -33,8 +30,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) { return true; - } else if ("Riot".equals(aiLogic)) { - return true; } else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) { for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) { if (SpellApiToAi.Converter.get(sb.getApi()).canPlayAIWithSubs(ai, sb)) { @@ -86,9 +81,7 @@ protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells, Map params) { Card host = sa.getHostCard(); - final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final Game game = host.getGame(); - final Combat combat = game.getCombat(); final String logic = sa.getParam("AILogic"); if (logic == null) { return spells.get(0); @@ -287,54 +280,7 @@ public boolean apply(final SpellAbility sp) { if (!filtered.isEmpty()) { return filtered.get(0); } - } else if ("Riot".equals(logic)) { - SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1); - return preferHasteForRiot(sa, player) ? hasteSA : counterSA; } return spells.get(0); // return first choice if no logic found } - - public static boolean preferHasteForRiot(SpellAbility sa, Player player) { - // returning true means preferring Haste, returning false means preferring a +1/+1 counter - final Card host = sa.getHostCard(); - final Game game = host.getGame(); - final Card copy = CardUtil.getLKICopy(host); - copy.setLastKnownZone(player.getZone(ZoneType.Battlefield)); - - // check state it would have on the battlefield - CardCollection preList = new CardCollection(copy); - game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList); - // reset again? - game.getAction().checkStaticAbilities(false); - - // can't gain counters, use Haste - if (!copy.canReceiveCounters(CounterEnumType.P1P1)) { - return true; - } - - // already has Haste, use counter - if (copy.hasKeyword(Keyword.HASTE)) { - return false; - } - - // not AI turn - if (!game.getPhaseHandler().isPlayerTurn(player)) { - return false; - } - - // not before Combat - if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { - return false; - } - - // TODO check other opponents too if able - final Player opp = player.getWeakestOpponent(); - if (opp != null) { - // TODO add predict Combat Damage? - return opp.getLife() < copy.getNetPower(); - } - - // haste might not be good enough? - return false; - } } 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 34e48d2cd28..41c223ea615 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -5532,6 +5532,10 @@ public final int getKeywordMagnitude(final Keyword k, CardState state) { // KeywordCollection#getAmount final String[] parse = kw.contains(":") ? kw.split(":") : kw.split(" "); + if (parse.length < 2) { + count++; + continue; + } final String s = parse[1]; if (StringUtils.isNumeric(s)) { count += Integer.parseInt(s); diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 097220edac6..b097d3a5666 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -2445,21 +2445,11 @@ public static void addReplacementEffect(final KeywordInterface inst, final CardS ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, repeatSA, false, true, intrinsic, "Card.Self", ""); inst.addReplacement(cardre); - } else if (keyword.startsWith("Riot")) { - final String choose = "DB$ GenericChoice | AILogic$ Riot | SpellDescription$ Riot"; + } else if (keyword.equals("Riot")) { + final String hasteStr = "DB$ Animate | Defined$ Self | Keywords$ Haste | Duration$ Permanent | UnlessCost$ AddCounter<1/P1P1> | UnlessPayer$ You | UnlessAI$ Riot | SpellDescription$ Riot"; - final String counter = "DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | ETB$ True | CounterNum$ 1" + - " | SpellDescription$ Put a +1/+1 counter on it."; - final String haste = "DB$ Animate | Defined$ Self | Keywords$ Haste | Duration$ Permanent | SpellDescription$ Haste"; - - SpellAbility saChoose = AbilityFactory.getAbility(choose, card); - - List list = Lists.newArrayList(); - list.add((AbilitySub)AbilityFactory.getAbility(counter, card)); - list.add((AbilitySub)AbilityFactory.getAbility(haste, card)); - saChoose.setAdditionalAbilityList("Choices", list); - - ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, saChoose, false, true, intrinsic, "Card.Self", ""); + final SpellAbility hasteSa = AbilityFactory.getAbility(hasteStr, card); + ReplacementEffect cardre = createETBReplacement(card, ReplacementLayer.Other, hasteSa, false, true, intrinsic, "Card.Self", ""); inst.addReplacement(cardre); } else if (keyword.equals("Sunburst")) { diff --git a/forge-game/src/main/java/forge/game/cost/CostPayment.java b/forge-game/src/main/java/forge/game/cost/CostPayment.java index 0ce3be371f0..e04b0e7870a 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPayment.java +++ b/forge-game/src/main/java/forge/game/cost/CostPayment.java @@ -95,13 +95,13 @@ public CostPayment(final Cost cost, final SpellAbility abil) { * a {@link forge.game.spellability.SpellAbility} object. * @return a boolean. */ - public static boolean canPayAdditionalCosts(Cost cost, final SpellAbility ability) { + public static boolean canPayAdditionalCosts(Cost cost, final SpellAbility ability, final boolean effect) { if (cost == null) { return true; } cost = CostAdjustment.adjust(cost, ability); - return cost.canPay(ability, false); + return cost.canPay(ability, effect); } /** diff --git a/forge-game/src/main/java/forge/game/cost/CostPutCounter.java b/forge-game/src/main/java/forge/game/cost/CostPutCounter.java index 5d2b2f610da..cbadfcda3bc 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPutCounter.java +++ b/forge-game/src/main/java/forge/game/cost/CostPutCounter.java @@ -19,13 +19,21 @@ import java.util.List; +import com.google.common.collect.Sets; + +import forge.game.Game; import forge.game.GameEntityCounterTable; +import forge.game.ability.AbilityKey; import forge.game.card.Card; +import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.card.CardPredicates; +import forge.game.card.CardUtil; import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.player.Player; +import forge.game.replacement.ReplacementEffect; +import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -140,15 +148,36 @@ public final void refund(final Card source) { @Override public final boolean canPay(final SpellAbility ability, final Player payer, final boolean effect) { final Card source = ability.getHostCard(); + final Game game = source.getGame(); if (this.payCostFromSource()) { - return source.isInPlay() && (getAbilityAmount(ability) == 0 || source.canReceiveCounters(this.counter)); + if (isETBReplacement(ability, effect)) { + final Card copy = CardUtil.getLKICopy(source); + copy.setLastKnownZone(payer.getZone(ZoneType.Battlefield)); + + // check state it would have on the battlefield + CardCollection preList = new CardCollection(copy); + game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList); + // reset again? + game.getAction().checkStaticAbilities(false); + if (copy.canReceiveCounters(getCounter())) { + return true; + } + } else { + if (!source.isInPlay()) { + return false; + } + if (source.canReceiveCounters(getCounter())) { + return true; + } + } + return getAbilityAmount(ability) == 0; } // 3 Cards have Put a -1/-1 Counter on a Creature you control. List typeList = CardLists.getValidCards(source.getGame().getCardsIn(ZoneType.Battlefield), this.getType().split(";"), payer, source, ability); - typeList = CardLists.filter(typeList, CardPredicates.canReceiveCounters(this.counter)); + typeList = CardLists.filter(typeList, CardPredicates.canReceiveCounters(getCounter())); return !typeList.isEmpty(); } @@ -172,8 +201,12 @@ public boolean payAsDecided(Player payer, PaymentDecision decision, SpellAbility @Override protected Card doPayment(Player payer, SpellAbility ability, Card targetCard, final boolean effect) { - final int i = this.getAbilityAmount(ability); - targetCard.addCounter(this.getCounter(), i, payer, counterTable); + final int i = getAbilityAmount(ability); + if (isETBReplacement(ability, effect)) { + targetCard.addEtbCounter(getCounter(), i, payer); + } else { + targetCard.addCounter(getCounter(), i, payer, counterTable); + } return targetCard; } @@ -208,4 +241,27 @@ public void resetLists() { counterTable.clear(); } + protected boolean isETBReplacement(final SpellAbility ability, final boolean effect) { + if (!effect) { + return false; + } + // only for itself? + if (!payCostFromSource()) { + return false; + } + if (ability == null) { + return false; + } + if (!ability.isReplacementAbility()) { + return false; + } + ReplacementEffect re = ability.getReplacementEffect(); + if (re.getMode() != ReplacementType.Moved) { + return false; + } + if (!ability.getHostCard().equals(ability.getReplacingObject(AbilityKey.Card))) { + return false; + } + return true; + } } diff --git a/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java b/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java index 5008719eb52..60112d3784c 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilityActivated.java @@ -90,7 +90,7 @@ public boolean canPlay() { return false; } - return CostPayment.canPayAdditionalCosts(this.getPayCosts(), this); + return CostPayment.canPayAdditionalCosts(this.getPayCosts(), this, false); } /** {@inheritDoc} */ diff --git a/forge-game/src/main/java/forge/game/spellability/Spell.java b/forge-game/src/main/java/forge/game/spellability/Spell.java index 17071958e61..27d30f323bd 100644 --- a/forge-game/src/main/java/forge/game/spellability/Spell.java +++ b/forge-game/src/main/java/forge/game/spellability/Spell.java @@ -112,7 +112,7 @@ && isBasicSpell() return false; } - if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this)) { + if (!CostPayment.canPayAdditionalCosts(this.getPayCosts(), this, false)) { return false; } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 797c2d547d0..9363684128f 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -827,6 +827,9 @@ public Object getReplacingObject(final AbilityKey type) { public void setReplacingObject(final AbilityKey type, final Object o) { replacingObjects.put(type, o); } + public void setReplacingObjects(final Map repParams) { + replacingObjects = AbilityKey.newMap(repParams); + } public void setReplacingObjectsFrom(final Map repParams, final AbilityKey... types) { int typesLength = types.length; for (int i = 0; i < typesLength; i += 1) { From 4d02d7be462aa6e0ad20bcb0cf7cd899385bd635 Mon Sep 17 00:00:00 2001 From: tool4ever Date: Sun, 31 Dec 2023 11:04:35 +0000 Subject: [PATCH 09/15] Update and rename witchking_of_angmar.txt to witch_king_of_angmar.txt --- .../w/{witchking_of_angmar.txt => witch_king_of_angmar.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename forge-gui/res/cardsfolder/w/{witchking_of_angmar.txt => witch_king_of_angmar.txt} (100%) diff --git a/forge-gui/res/cardsfolder/w/witchking_of_angmar.txt b/forge-gui/res/cardsfolder/w/witch_king_of_angmar.txt similarity index 100% rename from forge-gui/res/cardsfolder/w/witchking_of_angmar.txt rename to forge-gui/res/cardsfolder/w/witch_king_of_angmar.txt From 76e9b35f72ca72dcab4fa8092c7e862af0a93794 Mon Sep 17 00:00:00 2001 From: tool4ever Date: Sun, 31 Dec 2023 17:43:04 +0100 Subject: [PATCH 10/15] Fix ProtectEffect (#4456) --- .../forge/game/ability/effects/ProtectEffect.java | 13 +++++++++++-- forge-gui/res/cardsfolder/m/magmatic_galleon.txt | 2 +- .../cardsfolder/p/pippin_guard_of_the_citadel.txt | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/forge-game/src/main/java/forge/game/ability/effects/ProtectEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ProtectEffect.java index a4e09a5b5f4..f75f5df741b 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ProtectEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ProtectEffect.java @@ -5,9 +5,12 @@ import java.util.Iterator; import java.util.List; +import org.apache.commons.lang3.StringUtils; + import com.google.common.collect.Lists; import forge.GameCommand; +import forge.card.CardType; import forge.card.MagicColor; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -129,8 +132,12 @@ public void resolve(SpellAbility sa) { } List gainsKWList = Lists.newArrayList(); - for (String color : gains) { - gainsKWList.add(TextUtil.concatWithSpace("Protection from", color)); + for (String type : gains) { + if (isChoice && sa.getParam("Choices").equals("CardType")) { + gainsKWList.add("Protection:" + type); + } else { + gainsKWList.add(TextUtil.concatWithSpace("Protection from", type)); + } } tgtCards.addAll(CardUtil.getRadiance(sa)); @@ -176,6 +183,8 @@ public static List getProtectionList(final SpellAbility sa) { if (choices.contains("AnyColor")) { gains.addAll(MagicColor.Constant.ONLY_COLORS); choices = choices.replaceAll("AnyColor,?", ""); + } else if (choices.contains("CardType")) { + choices = StringUtils.join(CardType.getAllCardTypes(), ","); } // Add any remaining choices if (choices.length() > 0) { diff --git a/forge-gui/res/cardsfolder/m/magmatic_galleon.txt b/forge-gui/res/cardsfolder/m/magmatic_galleon.txt index e6d837f5fc7..e1656fdf595 100644 --- a/forge-gui/res/cardsfolder/m/magmatic_galleon.txt +++ b/forge-gui/res/cardsfolder/m/magmatic_galleon.txt @@ -4,7 +4,7 @@ Types:Artifact Vehicle PT:5/5 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDamage | TriggerDescription$ When CARDNAME enters the battlefield, it deals 5 damage to target creature an opponent controls. SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls | NumDmg$ 5 -T:Mode$ ExcessDamageAll | ValidTarget$ Creature.OppCtrl | CombatDamage$ False | Execute$ TrigTreasure | TriggerDescription$ Whenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token. +T:Mode$ ExcessDamageAll | ValidTarget$ Creature.OppCtrl | CombatDamage$ False | TriggerZones$ Battlefield | Execute$ TrigTreasure | TriggerDescription$ Whenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token. SVar:TrigTreasure:DB$ Token | TokenScript$ c_a_treasure_sac K:Crew:2 Oracle:When Magmatic Galleon enters the battlefield, it deals 5 damage to target creature an opponent controls.\nWhenever one or more creatures your opponents control are dealt excess noncombat damage, create a Treasure token.\nCrew 2 diff --git a/forge-gui/res/cardsfolder/p/pippin_guard_of_the_citadel.txt b/forge-gui/res/cardsfolder/p/pippin_guard_of_the_citadel.txt index 8b83603c367..c9854be4a34 100644 --- a/forge-gui/res/cardsfolder/p/pippin_guard_of_the_citadel.txt +++ b/forge-gui/res/cardsfolder/p/pippin_guard_of_the_citadel.txt @@ -4,5 +4,5 @@ Types:Legendary Creature Halfling Soldier PT:2/2 K:Vigilance K:Ward:1 -A:AB$ Protection | Cost$ T | ValidTgts$ Creature.YouCtrl+Other | TgtPrompt$ Select another target creature you control | Gains$ Choice | Choices$ artifacts,enchantments,creatures,battles,instants,sorceries,planeswalkers,lands | SpellDescription$ Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.) +A:AB$ Protection | Cost$ T | ValidTgts$ Creature.YouCtrl+Other | TgtPrompt$ Select another target creature you control | Gains$ Choice | Choices$ CardType | SpellDescription$ Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.) Oracle:Vigilance, ward {1}\n{T}: Another target creature you control gains protection from the card type of your choice until end of turn. (It can't be blocked, targeted, dealt damage, enchanted, or equipped by anything of that type.) From 36d4332d85a34a8bdc1cabc68ec5238dd38b2cb8 Mon Sep 17 00:00:00 2001 From: Klisz Date: Sun, 31 Dec 2023 16:31:40 -0700 Subject: [PATCH 11/15] Update diseased_vermin.txt Fixes issue where Diseased Vermin's upkeep trigger could target an opponent it hadn't damaged yet, as long as it had already damaged a different player --- forge-gui/res/cardsfolder/d/diseased_vermin.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui/res/cardsfolder/d/diseased_vermin.txt b/forge-gui/res/cardsfolder/d/diseased_vermin.txt index b0e60831e11..f59974187ea 100644 --- a/forge-gui/res/cardsfolder/d/diseased_vermin.txt +++ b/forge-gui/res/cardsfolder/d/diseased_vermin.txt @@ -4,7 +4,7 @@ Types:Creature Rat PT:1/1 T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigPutCounter | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, put an infection counter on it. SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ INFECTION | CounterNum$ 1 | SubAbility$ DBRemember -SVar:DBRemember:DB$ Pump | RememberObjects$ Opponent | StackDescription$ None +SVar:DBRemember:DB$ Pump | RememberObjects$ TriggeredTarget | StackDescription$ None T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ DBDisease | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it. SVar:DBDisease:DB$ DealDamage | ValidTgts$ Opponent.IsRemembered | NumDmg$ X SVar:X:Count$CardCounters.INFECTION From 4161858bcf9d5f281eaf182837120c4e0397e848 Mon Sep 17 00:00:00 2001 From: Jorilx Date: Tue, 2 Jan 2024 15:20:22 +0100 Subject: [PATCH 12/15] Adventure: when loading enemies on a map, if an enemy definition cannot be found choose a random one from the current biome (#4463) --- forge-gui-mobile/src/forge/adventure/stage/MapStage.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java index bd3e696c298..767f5995145 100644 --- a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java +++ b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java @@ -480,8 +480,11 @@ else if (prop.containsKey("portalState")) { if (enemy != null && !enemy.toString().isEmpty()) { EnemyData EN = WorldData.getEnemy(enemy.toString()); if (EN == null) { - System.err.printf("Enemy \"%s\" not found.", enemy); - break; + System.err.printf("Enemy \"%s\" not found, choosing a random one for current biome\n", enemy); + forge.adventure.world.World world = Current.world(); + Vector2 poiPos = AdventureQuestController.instance().mostRecentPOI.getPosition(); + int currentBiome = forge.adventure.world.World.highestBiome(world.getBiome((int) poiPos.x / world.getTileSize(), (int) poiPos.y / world.getTileSize())); + EN = world.getData().GetBiomes().get(currentBiome).getEnemy(1.0f); } EnemySprite mob = new EnemySprite(id, EN); Object dialogObject = prop.get("dialog"); //Check if the enemy has a dialogue attached to it. From aa589134d86920d3e37d34a76499c670a34cc8b5 Mon Sep 17 00:00:00 2001 From: tool4ever Date: Tue, 2 Jan 2024 15:20:35 +0100 Subject: [PATCH 13/15] Support for Genesis of the Daleks (#4462) * Support for Genesis of the Daleks * Fix Rift Elemental * Fix script * Fix logic --------- Co-authored-by: tool4EvEr --- forge-game/src/main/java/forge/game/Game.java | 2 - .../java/forge/game/ability/AbilityUtils.java | 78 ++++++------------- .../ability/effects/ControlGainEffect.java | 2 +- .../src/main/java/forge/game/cost/Cost.java | 4 +- .../forge/game/cost/CostRemoveCounter.java | 55 +++++++------ .../main/java/forge/game/keyword/Keyword.java | 2 +- .../res/cardsfolder/c/churning_reservoir.txt | 2 +- .../res/cardsfolder/d/diseased_vermin.txt | 7 +- .../res/cardsfolder/g/gandalf_the_grey.txt | 2 +- .../res/cardsfolder/r/rift_elemental.txt | 3 +- .../java/forge/player/HumanCostDecision.java | 2 +- 11 files changed, 62 insertions(+), 97 deletions(-) diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 8d131659da9..7574e7716e8 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -253,7 +253,6 @@ public void addLeftGraveyardThisTurn(Card lki) { public List getLeftBattlefieldThisTurn() { return leftBattlefieldThisTurn; } - public List getLeftGraveyardThisTurn() { return leftGraveyardThisTurn; } @@ -261,7 +260,6 @@ public List getLeftGraveyardThisTurn() { public void clearLeftBattlefieldThisTurn() { leftBattlefieldThisTurn.clear(); } - public void clearLeftGraveyardThisTurn() { leftGraveyardThisTurn.clear(); } 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 9beb675215f..b14163f6a40 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -1691,9 +1691,6 @@ public static int xCount(final Card c, final String s, final CardTraitBase ctb) if (root.isTrigger()) { Trigger t = root.getTrigger(); - if (t == null) { - return doXMath(0, expr, c, ctb); - } // ImmediateTrigger should check for the Ability which created the trigger if (t.getSpawningAbility() != null) { @@ -2083,7 +2080,6 @@ public static int xCount(final Card c, final String s, final CardTraitBase ctb) for (Mana m : paidMana) { if (m.toString().equals(type)) { count++; - } } return doXMath(count, expr, c, ctb); @@ -2168,11 +2164,9 @@ public static int xCount(final Card c, final String s, final CardTraitBase ctb) if (sq[0].startsWith("RememberedSize")) { return doXMath(c.getRememberedCount(), expr, c, ctb); } - if (sq[0].startsWith("ChosenSize")) { return doXMath(c.getChosenCards().size(), expr, c, ctb); } - if (sq[0].startsWith("ImprintedSize")) { return doXMath(c.getImprintedCards().size(), expr, c, ctb); } @@ -2714,54 +2708,35 @@ public static int xCount(final Card c, final String s, final CardTraitBase ctb) return MyRandom.getRandom().nextInt(1+max-min) + min; } + String[] paidparts = l[0].split("\\$", 2); + Iterable someCards = null; + // Count$ThisTurnCast // Count$LastTurnCast if (sq[0].startsWith("ThisTurnCast") || sq[0].startsWith("LastTurnCast")) { - String[] paidparts = l[0].split("\\$", 2); final String[] workingCopy = paidparts[0].split("_"); final String validFilter = workingCopy[1]; - List res; if (workingCopy[0].contains("This")) { - res = CardUtil.getThisTurnCast(validFilter, c, ctb, player); - } else { - res = CardUtil.getLastTurnCast(validFilter, c, ctb, player); - } - - int num; - if (paidparts.length > 1) { - num = handlePaid(res, paidparts[1], c, ctb); + someCards = CardUtil.getThisTurnCast(validFilter, c, ctb, player); } else { - num = res.size(); + someCards = CardUtil.getLastTurnCast(validFilter, c, ctb, player); } - - return doXMath(num, expr, c, ctb); } // Count$ThisTurnEntered [from ] - if (sq[0].startsWith("ThisTurnEntered")) { - final String[] workingCopy = l[0].split("_", 5); - - ZoneType destination = ZoneType.smartValueOf(workingCopy[1]); - final boolean hasFrom = workingCopy[2].equals("from"); - ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null; - String validFilter = workingCopy[hasFrom ? 4 : 2]; - - final List res = CardUtil.getThisTurnEntered(destination, origin, validFilter, c, ctb, player); - return doXMath(res.size(), expr, c, ctb); - } - - // Count$LastTurnEntered [from ] - if (sq[0].startsWith("LastTurnEntered")) { - final String[] workingCopy = l[0].split("_", 5); - + if (sq[0].startsWith("ThisTurnEntered") || sq[0].startsWith("LastTurnEntered")) { + final String[] workingCopy = paidparts[0].split("_"); ZoneType destination = ZoneType.smartValueOf(workingCopy[1]); final boolean hasFrom = workingCopy[2].equals("from"); ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null; String validFilter = workingCopy[hasFrom ? 4 : 2]; - final List res = CardUtil.getLastTurnEntered(destination, origin, validFilter, c, ctb, player); - return doXMath(res.size(), expr, c, ctb); + if (workingCopy[0].contains("This")) { + someCards = CardUtil.getThisTurnEntered(destination, origin, validFilter, c, ctb, player); + } else { + someCards = CardUtil.getLastTurnEntered(destination, origin, validFilter, c, ctb, player); + } } if (sq[0].startsWith("CountersAddedThisTurn")) { @@ -2779,7 +2754,6 @@ public static int xCount(final Card c, final String s, final CardTraitBase ctb) // count valid cards in any specified zone/s if (sq[0].startsWith("Valid")) { - String[] paidparts = l[0].split("\\$", 2); String[] lparts = paidparts[0].split(" ", 2); CardCollectionView cardsInZones = null; @@ -2805,13 +2779,7 @@ public static int xCount(final Card c, final String s, final CardTraitBase ctb) } } - int cnt; - if (paidparts.length > 1) { - cnt = handlePaid(CardLists.getValidCards(cardsInZones, lparts[1], player, c, ctb), paidparts[1], c, ctb); - } else { - cnt = CardLists.getValidCardCount(cardsInZones, lparts[1], player, c, ctb); - } - return doXMath(cnt, expr, c, ctb); + someCards = CardLists.getValidCards(cardsInZones, lparts[1], player, c, ctb); } if (sq[0].startsWith("MostCardName")) { @@ -2891,23 +2859,27 @@ public static int xCount(final Card c, final String s, final CardTraitBase ctb) return doXMath(Iterables.size(powers), expr, c, ctb); } if (sq[0].startsWith("DifferentCounterKinds_")) { - final List kinds = Lists.newArrayList(); + final Set kinds = Sets.newHashSet(); final String rest = l[0].substring(22); CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb); for (final Card card : list) { - for (final Map.Entry map : card.getCounters().entrySet()) { - if (!kinds.contains(map.getKey())) { - kinds.add(map.getKey()); - } - } + kinds.addAll(card.getCounters().keySet()); } return doXMath(kinds.size(), expr, c, ctb); } // Complex counting methods - CardCollectionView someCards = getCardListForXCount(c, player, sq, ctb); + Integer num = null; + if (someCards == null) { + someCards = getCardListForXCount(c, player, sq, ctb); + } else if (paidparts.length > 1) { + num = handlePaid(someCards, paidparts[1], c, ctb); + } + if (num == null) { + num = Iterables.size(someCards); + } - return doXMath(someCards.size(), expr, c, ctb); + return doXMath(num, expr, c, ctb); } public static final void applyManaColorConversion(ManaConversionMatrix matrix, String conversion) { diff --git a/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java index b9b14b9b0b1..1f8b462ea58 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ControlGainEffect.java @@ -192,7 +192,7 @@ public void resolve(SpellAbility sa) { tgtC.addChangedCardKeywords(kws, Lists.newArrayList(), false, tStamp, 0); game.fireEvent(new GameEventCardStatsChanged(tgtC)); } - if (hiddenKws.isEmpty()) { + if (!hiddenKws.isEmpty()) { tgtC.addHiddenExtrinsicKeywords(tStamp, 0, hiddenKws); } diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index 9f110f71700..de6cafa4dc8 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -297,7 +297,7 @@ private static CostPart parseCostPart(String parse, boolean tapCost, boolean unt final String[] splitStr = abCostParse(parse, 5); final String type = splitStr.length > 2 ? splitStr[2] : "CARDNAME"; final String description = splitStr.length > 3 ? splitStr[3] : null; - final ZoneType zone = splitStr.length > 4 ? ZoneType.smartValueOf(splitStr[4]) : ZoneType.Battlefield; + final List zone = splitStr.length > 4 ? ZoneType.listValueOf(splitStr[4]) : Lists.newArrayList(ZoneType.Battlefield); boolean oneOrMore = false; if (splitStr[0].equals("X1+")) { oneOrMore = true; @@ -979,7 +979,7 @@ public Cost add(Cost cost1, boolean mergeAdditional, final SpellAbility sa) { if (counters < 0) { costParts.add(new CostPutCounter(String.valueOf(counters *-1), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription())); } else { - costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), ZoneType.Battlefield, false)); + costParts.add(new CostRemoveCounter(String.valueOf(counters), CounterType.get(CounterEnumType.LOYALTY), part.getType(), part.getTypeDescription(), Lists.newArrayList(ZoneType.Battlefield) , false)); } } else { continue; diff --git a/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java b/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java index 4f234f24f86..4c284381471 100644 --- a/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java +++ b/forge-game/src/main/java/forge/game/cost/CostRemoveCounter.java @@ -41,7 +41,7 @@ public class CostRemoveCounter extends CostPart { */ private static final long serialVersionUID = 1L; public final CounterType counter; - public final ZoneType zone; + public final List zone; public final Boolean oneOrMore; /** @@ -57,7 +57,7 @@ public class CostRemoveCounter extends CostPart { * the description * @param zone the zone. */ - public CostRemoveCounter(final String amount, final CounterType counter, final String type, final String description, final ZoneType zone, final boolean oneOrMore) { + public CostRemoveCounter(final String amount, final CounterType counter, final String type, final String description, final List zone, final boolean oneOrMore) { super(amount, type, description); this.counter = counter; @@ -73,23 +73,24 @@ public Integer getMaxAmountX(final SpellAbility ability, final Player payer, fin final CounterType cntrs = this.counter; final Card source = ability.getHostCard(); final String type = this.getType(); + if (this.payCostFromSource()) { return source.getCounters(cntrs); + } + + List typeList; + if (type.equals("OriginalHost")) { + typeList = Lists.newArrayList(ability.getOriginalHost()); } else { - List typeList; - if (type.equals("OriginalHost")) { - typeList = Lists.newArrayList(ability.getOriginalHost()); - } else { - typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability); - } + typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability); + } - // Single Target - int maxcount = 0; - for (Card c : typeList) { - maxcount = Math.max(maxcount, c.getCounters(cntrs)); - } - return maxcount; + // Single Target + int maxcount = 0; + for (Card c : typeList) { + maxcount = Math.max(maxcount, c.getCounters(cntrs)); } + return maxcount; } /* @@ -146,26 +147,24 @@ public final boolean canPay(final SpellAbility ability, final Player payer, fina final int amount; if (getAmount().equals("All")) { amount = source.getCounters(cntrs); - } - else { + } else { amount = getAbilityAmount(ability); } if (this.payCostFromSource()) { return !source.isPhasedOut() && (source.getCounters(cntrs) - amount) >= 0; } - else { - List typeList; - if (type.equals("OriginalHost")) { - typeList = Lists.newArrayList(ability.getOriginalHost()); - } else { - typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability); - } - // (default logic) remove X counters from a single permanent - for (Card c : typeList) { - if (c.getCounters(cntrs) - amount >= 0) { - return true; - } + List typeList; + if (type.equals("OriginalHost")) { + typeList = Lists.newArrayList(ability.getOriginalHost()); + } else { + typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability); + } + + // (default logic) remove X counters from a single permanent + for (Card c : typeList) { + if (c.getCounters(cntrs) - amount >= 0) { + return true; } } diff --git a/forge-game/src/main/java/forge/game/keyword/Keyword.java b/forge-game/src/main/java/forge/game/keyword/Keyword.java index 4a7d46da450..5480961d611 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -175,7 +175,7 @@ public enum Keyword { STRIVE("Strive", KeywordWithCost.class, false, "CARDNAME costs %s more to cast for each target beyond the first."), SUNBURST("Sunburst", SimpleKeyword.class, false, "This enters the battlefield with either a +1/+1 or charge counter on it for each color of mana spent to cast it based on whether it's a creature."), SURGE("Surge", KeywordWithCost.class, false, "You may cast this spell for its surge cost if you or a teammate has cast another spell this turn."), - SUSPEND("Suspend", Suspend.class, false, "Rather than cast this card from your hand, you may pay %s and exile it with {%d:time counter} on it. At the beginning of your upkeep, remove a time counter. When the last is removed, cast it without paying its mana cost."), + SUSPEND("Suspend", Suspend.class, false, "If you could begin to cast this card by putting it onto the stack from your hand, you may pay %s and exile it with {%d:time counter} on it. At the beginning of your upkeep, remove a time counter. When the last is removed, play it without paying its mana cost. If you cast a creature spell this way, it gains haste until you lose control of the spell or the permanent it becomes."), TOTEM_ARMOR("Totem armor", SimpleKeyword.class, true, "If enchanted permanent would be destroyed, instead remove all damage marked on it and destroy this Aura."), TOXIC("Toxic", KeywordWithAmount.class, false, "Players dealt combat damage by this creature also get {%d:poison counter}."), TRAINING("Training", SimpleKeyword.class, false, "Whenever this creature attacks with another creature with greater power, put a +1/+1 counter on this creature."), diff --git a/forge-gui/res/cardsfolder/c/churning_reservoir.txt b/forge-gui/res/cardsfolder/c/churning_reservoir.txt index 73c14d7cd26..a75fcb16827 100644 --- a/forge-gui/res/cardsfolder/c/churning_reservoir.txt +++ b/forge-gui/res/cardsfolder/c/churning_reservoir.txt @@ -5,6 +5,6 @@ T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigPutCounter | Tri SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Artifact.nonToken+YouCtrl+Other,Creature.YouCtrl+Other+nonToken | CounterType$ OIL | CounterNum$ 1 A:AB$ Token | Cost$ 2 T | TokenScript$ r_1_1_phyrexian_goblin | CheckSVar$ CountCountersRemoved | CheckSVarCompare$ GE1 | SpellDescription$ Create a 1/1 red Phyrexian Goblin creature token. Activate only if an oil counter was removed from a permanent you controlled this turn or a permanent with an oil counter on it was put into a graveyard this turn. SVar:CountCountersRemoved:Count$CountersRemovedThisTurn OIL Card.YouCtrl+inRealZoneBattlefield/Plus.X -SVar:X:Count$ThisTurnEntered_Graveyard_from_Battlefield_Permanent.YouCtrl+counters_GE1_OIL +SVar:X:Count$ThisTurnEntered_Graveyard_from_Battlefield_Permanent.counters_GE1_OIL DeckHas:Ability$Token/Counters & Type$Goblin|Phyrexian Oracle:At the beginning of your upkeep, put an oil counter on another target nontoken artifact or creature you control.\n{2}, {T}: Create a 1/1 red Phyrexian Goblin creature token. Activate only if an oil counter was removed from a permanent you controlled this turn or a permanent with an oil counter on it was put into a graveyard this turn. diff --git a/forge-gui/res/cardsfolder/d/diseased_vermin.txt b/forge-gui/res/cardsfolder/d/diseased_vermin.txt index f59974187ea..aca1338b198 100644 --- a/forge-gui/res/cardsfolder/d/diseased_vermin.txt +++ b/forge-gui/res/cardsfolder/d/diseased_vermin.txt @@ -3,11 +3,8 @@ ManaCost:2 B Types:Creature Rat PT:1/1 T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigPutCounter | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, put an infection counter on it. -SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ INFECTION | CounterNum$ 1 | SubAbility$ DBRemember -SVar:DBRemember:DB$ Pump | RememberObjects$ TriggeredTarget | StackDescription$ None +SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ INFECTION | CounterNum$ 1 T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ DBDisease | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it. -SVar:DBDisease:DB$ DealDamage | ValidTgts$ Opponent.IsRemembered | NumDmg$ X +SVar:DBDisease:DB$ DealDamage | ValidTgts$ Opponent.wasDealtDamageThisGameBy Self | NumDmg$ X SVar:X:Count$CardCounters.INFECTION -T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ DBCleanup | Static$ True -SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True Oracle:Whenever Diseased Vermin deals combat damage to a player, put an infection counter on it.\nAt the beginning of your upkeep, Diseased Vermin deals X damage to target opponent previously dealt damage by it, where X is the number of infection counters on it. diff --git a/forge-gui/res/cardsfolder/g/gandalf_the_grey.txt b/forge-gui/res/cardsfolder/g/gandalf_the_grey.txt index b0d5d3497ba..2bbdbe46c60 100644 --- a/forge-gui/res/cardsfolder/g/gandalf_the_grey.txt +++ b/forge-gui/res/cardsfolder/g/gandalf_the_grey.txt @@ -6,7 +6,7 @@ T:Mode$ SpellCast | ValidCard$ Instant,Sorcery | ValidActivatingPlayer$ You | Tr SVar:TrigCharm:DB$ Charm | Choices$ DBTapUntap,DBDamage,DBCopy,DBChangeZone | ChoiceRestriction$ ThisGame | CharmNum$ 1 SVar:DBTapUntap:DB$ TapOrUntap | ValidTgts$ Permanent | TgtPrompt$ Select target permanent to tap or untap | SpellDescription$ You may tap or untap target permanent. SVar:DBDamage:DB$ DealDamage | Defined$ Opponent | NumDmg$ 3 | SpellDescription$ CARDNAME deals 3 damage to each opponent. -SVar:DBCopy:DB$ CopySpellAbility | ValidTgts$ Instant.YouCtrl,Sorcery.YouCtrl | TgtPrompt$ Select target instant or sorcery spell you control | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell you control. You may choose new targets for the copy. +SVar:DBCopy:DB$ CopySpellAbility | ValidTgts$ Instant.YouCtrl,Sorcery.YouCtrl | TargetType$ Spell | TgtPrompt$ Select target instant or sorcery spell you control | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell you control. You may choose new targets for the copy. SVar:DBChangeZone:DB$ ChangeZone | Origin$ Battlefield | Destination$ Library | LibraryPosition$ 0 | SpellDescription$ Put NICKNAME on top of its owner's library. DeckNeeds:Type$Instant|Sorcery Oracle:Whenever you cast an instant or sorcery spell, choose one that hasn't been chosen —\n• You may tap or untap target permanent.\n• Gandalf the Grey deals 3 damage to each opponent.\n• Copy target instant or sorcery spell you control. You may choose new targets for the copy.\n• Put Gandalf on top of its owner's library. diff --git a/forge-gui/res/cardsfolder/r/rift_elemental.txt b/forge-gui/res/cardsfolder/r/rift_elemental.txt index 2d513992aee..8c14619128a 100644 --- a/forge-gui/res/cardsfolder/r/rift_elemental.txt +++ b/forge-gui/res/cardsfolder/r/rift_elemental.txt @@ -2,7 +2,6 @@ Name:Rift Elemental ManaCost:R Types:Creature Elemental PT:1/1 -A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Permanent/permanent you control> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn. -A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Card.suspended/suspended card you own/Exile> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn. +A:AB$ Pump | Cost$ 1 R SubCounter<1/TIME/Permanent.inZoneBattlefield;Card.suspended/a permanent you control or suspended card you own/Battlefield,Exile> | Defined$ Self | NumAtt$ 2 | SpellDescription$ CARDNAME gets +2/+0 until end of turn. AI:RemoveDeck:All Oracle:{1}{R}, Remove a time counter from a permanent you control or suspended card you own: Rift Elemental gets +2/+0 until end of turn. diff --git a/forge-gui/src/main/java/forge/player/HumanCostDecision.java b/forge-gui/src/main/java/forge/player/HumanCostDecision.java index 9d3f15cc8db..3354ba0b7ea 100644 --- a/forge-gui/src/main/java/forge/player/HumanCostDecision.java +++ b/forge-gui/src/main/java/forge/player/HumanCostDecision.java @@ -1036,7 +1036,7 @@ public PaymentDecision visit(final CostRemoveCounter cost) { } final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, validCards, ability); - inp.setMessage(Localizer.getInstance().getMessage("lblRemoveCountersFromAInZoneCard", cost.zone.getTranslatedName())); + inp.setMessage(Localizer.getInstance().getMessage("lblRemoveCountersFromAInZoneCard", Lang.joinHomogenous(cost.zone, ZoneType.Accessors.GET_TRANSLATED_NAME))); inp.setCancelAllowed(true); inp.showAndWait(); From 3cb7fac8f2be0de2c77fce8dd8e3ee3c4582f6c4 Mon Sep 17 00:00:00 2001 From: Jorilx Date: Tue, 2 Jan 2024 15:20:45 +0100 Subject: [PATCH 14/15] Adventure: add config setting 'allowedEditions' (#4431) --- .../adventure/data/AdventureEventData.java | 20 +++++++++++++++---- .../src/forge/adventure/data/ConfigData.java | 1 + .../src/forge/adventure/data/RewardData.java | 10 +++++++--- .../adventure/scene/SpellSmithScene.java | 6 +++++- .../src/forge/adventure/util/CardUtil.java | 9 +++++++-- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java b/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java index 487b5afc4cb..23b9d2c5487 100644 --- a/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java +++ b/forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java @@ -373,8 +373,14 @@ private CardBlock pickWeightedCardBlock() { } } - for (String restricted : Config.instance().getConfigData().restrictedEditions) { - legalBlocks.removeIf(q -> q.getName().equals(restricted)); + ConfigData configData = Config.instance().getConfigData(); + if (configData.allowedEditions != null) { + List allowed = Arrays.asList(configData.allowedEditions); + legalBlocks.removeIf(q -> !allowed.contains(q.getName())); + } else { + for (String restricted : configData.restrictedEditions) { + legalBlocks.removeIf(q -> q.getName().equals(restricted)); + } } return legalBlocks.isEmpty() ? null : Aggregates.random(legalBlocks); } @@ -388,8 +394,14 @@ private CardBlock pickJumpstartCardBlock() { legalBlocks.add(b); } } - for (String restricted : Config.instance().getConfigData().restrictedEditions) { - legalBlocks.removeIf(q -> q.getName().equals(restricted)); + ConfigData configData = Config.instance().getConfigData(); + if (configData.allowedEditions != null) { + List allowed = Arrays.asList(configData.allowedEditions); + legalBlocks.removeIf(q -> !allowed.contains(q.getName())); + } else { + for (String restricted : configData.restrictedEditions) { + legalBlocks.removeIf(q -> q.getName().equals(restricted)); + } } return legalBlocks.isEmpty()?null:Aggregates.random(legalBlocks); } diff --git a/forge-gui-mobile/src/forge/adventure/data/ConfigData.java b/forge-gui-mobile/src/forge/adventure/data/ConfigData.java index bf18d7326c6..453affb632e 100644 --- a/forge-gui-mobile/src/forge/adventure/data/ConfigData.java +++ b/forge-gui-mobile/src/forge/adventure/data/ConfigData.java @@ -22,4 +22,5 @@ public class ConfigData { public RewardData legalCards; public String[] restrictedCards; public String[] restrictedEditions; + public String[] allowedEditions; } diff --git a/forge-gui-mobile/src/forge/adventure/data/RewardData.java b/forge-gui-mobile/src/forge/adventure/data/RewardData.java index a1db60d757e..3b7fcef20dd 100644 --- a/forge-gui-mobile/src/forge/adventure/data/RewardData.java +++ b/forge-gui-mobile/src/forge/adventure/data/RewardData.java @@ -86,7 +86,8 @@ public RewardData(RewardData rewardData) { private static Iterable allEnemyCards; static private void initializeAllCards(){ - RewardData legals = Config.instance().getConfigData().legalCards; + ConfigData configData = Config.instance().getConfigData(); + RewardData legals = configData.legalCards; if(legals==null) allCards = CardUtil.getFullCardPool(false); // we need unique cards only here, so that a unique card can be chosen before a set variant is determined @@ -100,11 +101,14 @@ static private void initializeAllCards(){ return false; if(input.getRules().getAiHints().getRemNonCommanderDecks()) return false; - if(Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(input.getEdition())) + if(configData.allowedEditions != null) { + if (!Arrays.asList(configData.allowedEditions).contains(input.getEdition())) + return false; + } else if(Arrays.asList(configData.restrictedEditions).contains(input.getEdition())) return false; if(input.getRules().isCustom()) return false; - return !Arrays.asList(Config.instance().getConfigData().restrictedCards).contains(input.getName()); + return !Arrays.asList(configData.restrictedCards).contains(input.getName()); }); //Filter AI cards for enemies. allEnemyCards=Iterables.filter(allCards, input -> { diff --git a/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java b/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java index 08b8f87a906..a7e26681b73 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SpellSmithScene.java @@ -11,6 +11,7 @@ import com.github.tommyettinger.textra.TextraLabel; import forge.Forge; import forge.StaticData; +import forge.adventure.data.ConfigData; import forge.adventure.data.RewardData; import forge.adventure.util.*; import forge.card.CardEdition; @@ -151,7 +152,10 @@ public void loadEditions() { .filter(input2 -> input2.getEdition().equals(input.getCode())).collect(Collectors.toList()); if (it.size() == 0) return false; - return (!Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(input.getCode())); + ConfigData configData = Config.instance().getConfigData(); + if (configData.allowedEditions != null) + return Arrays.asList(configData.allowedEditions).contains(input.getCode()); + return (!Arrays.asList(configData.restrictedEditions).contains(input.getCode())); }).sorted(new Comparator() { @Override public int compare(CardEdition e1, CardEdition e2) { diff --git a/forge-gui-mobile/src/forge/adventure/util/CardUtil.java b/forge-gui-mobile/src/forge/adventure/util/CardUtil.java index 67a03becec3..24c994ff56b 100644 --- a/forge-gui-mobile/src/forge/adventure/util/CardUtil.java +++ b/forge-gui-mobile/src/forge/adventure/util/CardUtil.java @@ -6,6 +6,7 @@ import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import forge.StaticData; +import forge.adventure.data.ConfigData; import forge.adventure.data.GeneratedDeckData; import forge.adventure.data.GeneratedDeckTemplateData; import forge.adventure.data.RewardData; @@ -755,8 +756,12 @@ public static Deck generateVintageBoosterAsDeck(){ } public static Deck generateBoosterPackAsDeck(String code){ - - if (Arrays.asList(Config.instance().getConfigData().restrictedEditions).contains(code)){ + ConfigData configData = Config.instance().getConfigData(); + if (configData.allowedEditions != null) { + if (!Arrays.asList(configData.allowedEditions).contains(code)){ + System.err.println("Cannot generate booster pack, '" + code + "' is not an allowed edition"); + } + } else if (Arrays.asList(configData.restrictedEditions).contains(code)){ System.err.println("Cannot generate booster pack, '" + code + "' is a restricted edition"); } From 13cf0810460741f36b9f25f4e31ad8db5f9e259d Mon Sep 17 00:00:00 2001 From: Jorilx Date: Tue, 2 Jan 2024 18:10:15 +0100 Subject: [PATCH 15/15] Adventure: check for decks in user plane directory too (#4401) * Adventure: check for decks in user plane directory too * Adventure: simplified check for decks in user plane directory * Removed unused import --- forge-gui-mobile/src/forge/adventure/util/CardUtil.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/forge-gui-mobile/src/forge/adventure/util/CardUtil.java b/forge-gui-mobile/src/forge/adventure/util/CardUtil.java index 24c994ff56b..a8c6edf766a 100644 --- a/forge-gui-mobile/src/forge/adventure/util/CardUtil.java +++ b/forge-gui-mobile/src/forge/adventure/util/CardUtil.java @@ -24,7 +24,6 @@ import forge.model.FModel; import forge.util.Aggregates; -import java.io.File; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Pattern; @@ -715,7 +714,7 @@ public static Deck getDeck(String path, boolean forAI, boolean isFantasyMode, St public static Deck getDeck(String path, boolean forAI, boolean isFantasyMode, String colors, boolean isTheme, boolean useGeneticAI, CardEdition starterEdition, boolean discourageDuplicates) { if(path.endsWith(".dck")) - return DeckSerializer.fromFile(new File(Config.instance().getCommonFilePath(path))); + return DeckSerializer.fromFile(Config.instance().getFile(path).file()); if(forAI && (isFantasyMode||useGeneticAI)) { Deck deck = DeckgenUtil.getRandomOrPreconOrThemeDeck(colors, forAI, isTheme, useGeneticAI);