From b638d7f35d361bf3b85e3a56a3a44c34233b934f Mon Sep 17 00:00:00 2001 From: tool4ever Date: Sat, 28 Oct 2023 17:54:12 +0200 Subject: [PATCH] Doomsday Confluence and support (#4012) * Doomsday Confluence and support * Clean up * Fix NPE * +AIhint --------- Co-authored-by: tool4EvEr --- .../game/ability/effects/CharmEffect.java | 23 ++++++++++++------- .../game/ability/effects/CleanUpEffect.java | 4 +++- .../game/ability/effects/EffectEffect.java | 2 +- .../src/main/java/forge/game/card/Card.java | 4 ++-- .../main/java/forge/game/card/CardUtil.java | 2 +- .../b/braidss_frightful_return.txt | 5 ++-- .../res/cardsfolder/i/ironclaw_curse.txt | 2 +- .../upcoming/doomsday_confluence.txt | 11 +++++++++ .../upcoming/the_huntsmans_redemption.txt | 6 ++--- .../upcoming/yenna_redtooth_regent.txt | 2 +- .../src/main/java/forge/player/HumanPlay.java | 18 --------------- .../forge/player/HumanPlaySpellAbility.java | 23 ++++++++++++++++--- 12 files changed, 59 insertions(+), 43 deletions(-) create mode 100644 forge-gui/res/cardsfolder/upcoming/doomsday_confluence.txt diff --git a/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java index 2f8e40ee645..026bb4aa038 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CharmEffect.java @@ -59,19 +59,21 @@ public class CharmEffect extends SpellAbilityEffect { Card source = sa.getHostCard(); List list = CharmEffect.makePossibleOptions(sa); - final int num; + String numParam = sa.getParamOrDefault("CharmNum", "1"); + boolean isX = numParam.equals("X"); + int num = 0; boolean additionalDesc = sa.hasParam("AdditionalDescription"); boolean optional = sa.hasParam("Optional"); // hotfix for complex cards when using getCardForUi if (source.getController() == null && additionalDesc && !optional) { // using getCardForUi game is not set, so can't guess max charm num = Integer.MAX_VALUE; - } else { + } else if (!isX) { // fallback needed while ability building if (sa.getActivatingPlayer() == null) { sa.setActivatingPlayer(source.getController(), true); } - num = Math.min(AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa), list.size()); + num = Math.min(AbilityUtils.calculateAmount(source, numParam, sa), list.size()); } final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num; @@ -85,7 +87,9 @@ public class CharmEffect extends SpellAbilityEffect { sb.append(sa.getCostDescription()); sb.append(oppChooses ? "An opponent chooses " : "Choose "); - if (num == min || num == Integer.MAX_VALUE) { + if (isX) { + sb.append("X"); + } else if (num == min || num == Integer.MAX_VALUE) { sb.append(num == 0 ? "up to that many" : Lang.getNumeral(min)); } else if (min == 0 && num == sa.getParam("Choices").split(",").length) { sb.append("any number "); @@ -188,14 +192,17 @@ public class CharmEffect extends SpellAbilityEffect { final Card source = sa.getHostCard(); final Player activator = sa.getActivatingPlayer(); + boolean canRepeat = sa.hasParam("CanRepeatModes"); int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa); final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num; // if the amount of choices is smaller than min then they can't be chosen - if (min > choices.size()) { - return false; + if (!canRepeat) { + if (min > choices.size()) { + return false; + } + num = Math.min(num, choices.size()); } - num = Math.min(num, choices.size()); boolean isOptional = sa.hasParam("Optional"); if (isOptional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeCharm", CardTranslation.getTranslatedName(source.getName())), null)) { @@ -219,7 +226,7 @@ public class CharmEffect extends SpellAbilityEffect { source.setChosenPlayer(chooser); } - List chosen = chooser.getController().chooseModeForAbility(sa, choices, min, num, sa.hasParam("CanRepeatModes")); + List chosen = chooser.getController().chooseModeForAbility(sa, choices, min, num, canRepeat); chainAbilities(sa, chosen); // trigger without chosen modes are removed from stack diff --git a/forge-game/src/main/java/forge/game/ability/effects/CleanUpEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CleanUpEffect.java index 3e4a6581661..d565991e8c7 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CleanUpEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CleanUpEffect.java @@ -1,6 +1,8 @@ package forge.game.ability.effects; +import com.google.common.collect.Lists; + import forge.game.Game; import forge.game.GameEntity; import forge.game.ability.AbilityUtils; @@ -65,7 +67,7 @@ public class CleanUpEffect extends SpellAbilityEffect { source.setChosenColors(null); } if (sa.hasParam("ClearNamedCard")) { - source.setNamedCards(null); + source.setNamedCards(Lists.newArrayList()); } if (sa.hasParam("Log")) { source.getController().getGame().fireEvent(new GameEventRandomLog(logMessage)); diff --git a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java index c5b73aead2b..78cf8199fac 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/EffectEffect.java @@ -293,7 +293,7 @@ public class EffectEffect extends SpellAbilityEffect { // Set Chosen name if (!hostCard.getNamedCard().isEmpty()) { - eff.setNamedCards(hostCard.getNamedCards()); + eff.setNamedCards(Lists.newArrayList(hostCard.getNamedCards())); } // chosen number 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 71a44aaa661..7ef936599e2 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -1987,7 +1987,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { return hasNamedCard() ? Iterables.getLast(chosenName) : ""; } public final List getNamedCards() { - return chosenName == null ? Lists.newArrayList() : chosenName; + return chosenName; } public final void setNamedCards(final List s) { chosenName = s; @@ -2000,7 +2000,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } public boolean hasNamedCard() { - return chosenName != null && !chosenName.isEmpty(); + return !chosenName.isEmpty(); } public boolean hasChosenEvenOdd() { diff --git a/forge-game/src/main/java/forge/game/card/CardUtil.java b/forge-game/src/main/java/forge/game/card/CardUtil.java index 2e17f7c89eb..4ae66e48c92 100644 --- a/forge-game/src/main/java/forge/game/card/CardUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardUtil.java @@ -296,7 +296,7 @@ public final class CardUtil { newCopy.setChosenType(in.getChosenType()); newCopy.setChosenType2(in.getChosenType2()); - newCopy.setNamedCards(in.getNamedCards()); + newCopy.setNamedCards(Lists.newArrayList(in.getNamedCards())); newCopy.setChosenColors(Lists.newArrayList(in.getChosenColors())); if (in.hasChosenNumber()) { newCopy.setChosenNumber(in.getChosenNumber()); diff --git a/forge-gui/res/cardsfolder/b/braidss_frightful_return.txt b/forge-gui/res/cardsfolder/b/braidss_frightful_return.txt index 4b455c80099..ceb4441e6ee 100644 --- a/forge-gui/res/cardsfolder/b/braidss_frightful_return.txt +++ b/forge-gui/res/cardsfolder/b/braidss_frightful_return.txt @@ -1,9 +1,8 @@ Name:Braids's Frightful Return ManaCost:2 B Types:Enchantment Saga -K:Read ahead:3:DBSacrifice,DBChangeZone,DBSacrificeOpp -SVar:DBSacrifice:DB$ Sacrifice | Optional$ True | Defined$ You | RememberSacrificed$ True | SacValid$ Creature | Amount$ 1 | SubAbility$ DBDiscard | SpellDescription$ You may sacrifice a creature. If you do, each opponent discards a card. -SVar:DBDiscard:DB$ Discard | Mode$ TgtChoose | Defined$ Player.Opponent | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ GE1 | NumCards$ 1 | SubAbility$ DBCleanup +K:Read ahead:3:ABDiscard,DBChangeZone,DBSacrificeOpp +SVar:ABDiscard:AB$ Discard | Cost$ Sac<1/Creature> | CostDesc$ You may sacrifice a creature. | Mode$ TgtChoose | Defined$ Opponent | SpellDescription$ If you do, each opponent discards a card. SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | ValidTgts$ Creature.YouOwn | SpellDescription$ Return target creature card from your graveyard to your hand. SVar:DBSacrificeOpp:DB$ Sacrifice | ValidTgts$ Opponent | Optional$ True | SacValid$ Permanent.nonLand+nonToken | RememberSacrificed$ True | SubAbility$ DBDraw | SpellDescription$ Target opponent may sacrifice a nonland, nontoken permanent. If they don't, they lose 2 life and you draw a card. SVar:DBDraw:DB$ Draw | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0 | SubAbility$ DBLoseLife diff --git a/forge-gui/res/cardsfolder/i/ironclaw_curse.txt b/forge-gui/res/cardsfolder/i/ironclaw_curse.txt index 48b068bc681..7a644b5981a 100644 --- a/forge-gui/res/cardsfolder/i/ironclaw_curse.txt +++ b/forge-gui/res/cardsfolder/i/ironclaw_curse.txt @@ -4,6 +4,6 @@ Types:Enchantment Aura K:Enchant creature A:SP$ Attach | Cost$ R | ValidTgts$ Creature | AILogic$ Curse S:Mode$ Continuous | Affected$ Creature.EnchantedBy | AddToughness$ -1 | Description$ Enchanted creature gets -0/-1 and can't block creatures with power equal to or greater than the enchanted creature's toughness. -S:Mode$ CantBlockBy | ValidAttackerRelative$ Creature.powerGEIronclawX | ValidBlocker$ Creature.EnchantedBy | Description$ Enchanted creature can't block creatures with power equal to or greater than the enchanted creature's toughness. +S:Mode$ CantBlockBy | ValidAttackerRelative$ Creature.powerGEIronclawX | ValidBlocker$ Creature.EnchantedBy | Secondary$ True | Description$ Enchanted creature can't block creatures with power equal to or greater than the enchanted creature's toughness. SVar:IronclawX:Count$CardToughness Oracle:Enchant creature\nEnchanted creature gets -0/-1.\nEnchanted creature can't block creatures with power equal to or greater than the enchanted creature's toughness. diff --git a/forge-gui/res/cardsfolder/upcoming/doomsday_confluence.txt b/forge-gui/res/cardsfolder/upcoming/doomsday_confluence.txt new file mode 100644 index 00000000000..f4afdd46882 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/doomsday_confluence.txt @@ -0,0 +1,11 @@ +Name:Doomsday Confluence +ManaCost:X X B +Types:Sorcery +A:SP$ Charm | CharmNum$ X | Choices$ DBSac,DBToken,DBDiscard | CanRepeatModes$ True +SVar:DBSac:DB$ Sacrifice | Defined$ Player | SacValid$ Creature.nonArtifact | SpellDescription$ Each player sacrifices a nonartifact creature. +SVar:DBToken:DB$ Token | TokenScript$ b_3_3_a_dalek_menace | SpellDescription$ Create a 3/3 black Dalek artifact creature token with menace. +SVar:DBDiscard:DB$ Discard | Defined$ Opponent | Mode$ TgtChoose | SpellDescription$ Each opponent discards a card. +SVar:X:Count$xPaid +DeckHas:Ability$Sacrifice|Token|Discard & Type$Artifact|Dalek +AI:RemoveDeck:All +Oracle:Choose X. You may choose the same mode more than once.\n• Each player sacrifices a nonartifact creature.\n• Create a 3/3 black Dalek artifact creature token with menace.\n• Each opponent discards a card. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/upcoming/the_huntsmans_redemption.txt b/forge-gui/res/cardsfolder/upcoming/the_huntsmans_redemption.txt index 54bd40ad7ab..946fc3f34b6 100644 --- a/forge-gui/res/cardsfolder/upcoming/the_huntsmans_redemption.txt +++ b/forge-gui/res/cardsfolder/upcoming/the_huntsmans_redemption.txt @@ -1,11 +1,9 @@ Name:The Huntsman's Redemption ManaCost:2 G Types:Enchantment Saga -K:Saga:3:DBToken,DBSearch,DBPump +K:Saga:3:DBToken,ABSearch,DBPump SVar:DBToken:DB$ Token | TokenAmount$ 1 | TokenScript$ g_3_3_beast | TokenOwner$ You | SpellDescription$ Create a 3/3 green Beast creature token. -SVar:DBSearch:DB$ Sacrifice | SacValid$ Creature | SacMessage$ creature | Optional$ True | RememberSacrificed$ True | SubAbility$ DBChangeZone | SpellDescription$ You may sacrifice a creature. If you do, search your library for a creature or basic land card, reveal it, put it into your hand, then shuffle. -SVar:DBChangeZone:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ConditionDefined$ Remembered | ConditionPresent$ Card | ChangeType$ Land.Basic,Creature | SubAbility$ DBCleanup | ChangeNum$ 1 -SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True +SVar:ABSearch:AB$ ChangeZone | Cost$ Sac<1/Creature> | CostDesc$ You may sacrifice a creature. | Origin$ Library | Destination$ Hand | ChangeType$ Land.Basic,Creature | ChangeNum$ 1 | SpellDescription$ If you do, search your library for a creature or basic land card, reveal it, put it into your hand, then shuffle. SVar:DBPump:DB$ Pump | ValidTgts$ Creature | NumAtt$ +2 | NumDef$ +2 | TgtPrompt$ Select up to two target creatures | TargetMin$ 0 | TargetMax$ 2 | KW$ Trample | SpellDescription$ Up to two target creatures each get +2/+2 and gain trample until end of turn DeckHas:Ability$Token|Sacrifice & Type$Beast Oracle:I — Create a 3/3 green Beast creature token.\nII — You may sacrifice a creature. If you do, search your library for a creature or basic land card, reveal it, put it into your hand, then shuffle.\nIII — Up to two target creatures each get +2/+2 and gain trample until end of turn \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/upcoming/yenna_redtooth_regent.txt b/forge-gui/res/cardsfolder/upcoming/yenna_redtooth_regent.txt index 15bf811c06c..9ca9552b162 100644 --- a/forge-gui/res/cardsfolder/upcoming/yenna_redtooth_regent.txt +++ b/forge-gui/res/cardsfolder/upcoming/yenna_redtooth_regent.txt @@ -2,7 +2,7 @@ Name:Yenna, Redtooth Regent ManaCost:2 G W Types:Legendary Creature Elf Noble PT:4/4 -A:AB$ CopyPermanent | Cost$ 2 T | Defined$ Targeted | RememberTokens$ True | NonLegendary$ True | ValidTgts$ Enchantment.YouCtrl+doesNotShareNameWith OtherYourBattlefield | TgtPrompt$ Choose target enchantment you control that doesn't have the same name as another permanent you control. | SorcerySpeed$ True | SubAbility$ DBUntap | SpellDescription$ Choose target enchantment you control that doesn't have the same name as another permanent you control. Create a token that's a copy of it, except it isn't legendary. If the token is an Aura, untap Yenna, Redtooth Regent, then scry 2. Activate only as a sorcery. +A:AB$ CopyPermanent | Cost$ 2 T | Defined$ Targeted | RememberTokens$ True | NonLegendary$ True | ValidTgts$ Enchantment.YouCtrl+doesNotShareNameWith OtherYourBattlefield | TgtPrompt$ Choose target enchantment you control that doesn't have the same name as another permanent you control. | SorcerySpeed$ True | SubAbility$ DBUntap | SpellDescription$ Choose target enchantment you control that doesn't have the same name as another permanent you control. Create a token that's a copy of it, except it isn't legendary. If the token is an Aura, untap CARDNAME, then scry 2. Activate only as a sorcery. SVar:DBUntap:DB$ Untap | Defined$ Self | ConditionDefined$ Remembered | ConditionPresent$ Card.Aura | SubAbility$ DBScry SVar:DBScry:DB$ Scry | ScryNum$ 2 | ConditionDefined$ Remembered | ConditionPresent$ Card.Aura | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index 085f91a630b..b4e51556c4b 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -15,8 +15,6 @@ import forge.game.GameActionUtil; import forge.game.GameEntityView; import forge.game.GameEntityViewMap; import forge.game.ability.AbilityUtils; -import forge.game.ability.ApiType; -import forge.game.ability.effects.CharmEffect; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; @@ -101,13 +99,6 @@ public class HumanPlay { source.forceTurnFaceUp(); } - if (sa.getApi() == ApiType.Charm && !CharmEffect.makeChoices(sa)) { - // 603.3c If no mode is chosen, the ability is removed from the stack. - return false; - } - - sa = AbilityUtils.addSpliceEffects(sa); - final HumanPlaySpellAbility req = new HumanPlaySpellAbility(controller, sa); if (!req.playAbility(true, false, false)) { Card rollback = p.getGame().getCardState(source); @@ -165,15 +156,6 @@ public class HumanPlay { source.setSplitStateToPlayAbility(sa); - if (sa.getApi() == ApiType.Charm && !CharmEffect.makeChoices(sa)) { - // 603.3c If no mode is chosen, the ability is removed from the stack. - return; - } - - if (!sa.isCopied()) { - sa = AbilityUtils.addSpliceEffects(sa); - } - final HumanPlaySpellAbility req = new HumanPlaySpellAbility(controller, sa); req.playAbility(mayChooseNewTargets, true, false); } diff --git a/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java b/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java index 7860bb60433..83d9918c5d4 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java +++ b/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java @@ -30,6 +30,8 @@ import forge.game.GameActionUtil; import forge.game.GameObject; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; +import forge.game.ability.ApiType; +import forge.game.ability.effects.CharmEffect; import forge.game.card.Card; import forge.game.card.CardPlayOption; import forge.game.cost.Cost; @@ -53,6 +55,7 @@ import forge.util.Localizer; public class HumanPlaySpellAbility { private final PlayerControllerHuman controller; private SpellAbility ability; + private boolean needX = true; public HumanPlaySpellAbility(final PlayerControllerHuman controller0, final SpellAbility ability0) { controller = controller0; @@ -63,9 +66,25 @@ public class HumanPlaySpellAbility { final Player human = ability.getActivatingPlayer(); final Game game = human.getGame(); - // CR 401.5: freeze top library cards until cast/activated so player can't cheat and see the next if (!skipStack) { + // CR 401.5: freeze top library cards until cast/activated so player can't cheat and see the next game.setTopLibsCast(); + + if (ability.getApi() == ApiType.Charm) { + if ("X".equals(ability.getParam("CharmNum"))) { + // CR 601.4 + if (!announceValuesLikeX()) { + return false; + } + needX = false; + } + if (!CharmEffect.makeChoices(ability)) { + // 603.3c If no mode is chosen, the ability is removed from the stack. + return false; + } + } + + ability = AbilityUtils.addSpliceEffects(ability); } // used to rollback @@ -191,9 +210,7 @@ public class HumanPlaySpellAbility { private boolean announceValuesLikeX() { if (ability.isCopied() || ability.isWrapper()) { return true; } //don't re-announce for spell copies - boolean needX = true; final Cost cost = ability.getPayCosts(); - final PlayerController controller = ability.getActivatingPlayer().getController(); final Card card = ability.getHostCard(); // Announcing Requirements like Choosing X or Multikicker