diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 035a4cecb4c..7ce9a83c61c 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -2103,9 +2103,9 @@ public class AiController { return filterList(list, CardTraitPredicates.hasParam("AiLogic", logic)); } - public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { + public List chooseModeForAbility(SpellAbility sa, List possible, int min, int num, boolean allowRepeat) { if (simPicker != null) { - return simPicker.chooseModeForAbility(sa, min, num, allowRepeat); + return simPicker.chooseModeForAbility(sa, possible, min, num, allowRepeat); } return null; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index bc1cc527832..60e0f02c05d 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -101,7 +101,9 @@ public class ComputerUtil { sa = GameActionUtil.addExtraKeywordCost(sa); if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { - CharmEffect.makeChoices(sa); + if (!CharmEffect.makeChoices(sa)) { + return false; + } } if (chooseTargets != null) { chooseTargets.run(); @@ -261,7 +263,9 @@ public class ComputerUtil { newSA.setHostCard(game.getAction().moveToStack(source, sa)); if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) { - CharmEffect.makeChoices(newSA); + if (!CharmEffect.makeChoices(sa)) { + return false; + } } } diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 8737419409d..68dcead0c09 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -800,8 +800,8 @@ public class PlayerControllerAi extends PlayerController { } @Override - public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { - List result = brains.chooseModeForAbility(sa, min, num, allowRepeat); + public List chooseModeForAbility(SpellAbility sa, List possible, int min, int num, boolean allowRepeat) { + List result = brains.chooseModeForAbility(sa, possible, min, num, allowRepeat); if (result != null) { return result; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java index 296a695b871..8835fc47e33 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java @@ -2,7 +2,9 @@ package forge.ai.ability; import com.google.common.collect.Lists; import forge.ai.*; +import forge.game.ability.AbilityUtils; import forge.game.ability.effects.CharmEffect; +import forge.game.card.Card; import forge.game.player.Player; import forge.game.spellability.AbilitySub; import forge.game.spellability.SpellAbility; @@ -19,10 +21,13 @@ public class CharmAi extends SpellAbilityAi { // sa is Entwined, no need for extra logic if (sa.isEntwine()) { return true; - } + } + + final Card source = sa.getHostCard(); + + final int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa); + final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num; - final int num = Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1"); - final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num; boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now? // Reset the chosen list otherwise it will be locked in forever by earlier calls diff --git a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java index e93a4b5ebfc..b63d0314eba 100644 --- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java +++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java @@ -9,7 +9,6 @@ import forge.ai.ability.ExploreAi; import forge.ai.simulation.GameStateEvaluator.Score; import forge.game.Game; import forge.game.ability.ApiType; -import forge.game.ability.effects.CharmEffect; import forge.game.card.*; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -371,14 +370,12 @@ public class SpellAbilityPicker { return bestScore; } - public List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { + public List chooseModeForAbility(SpellAbility sa, List choices, int min, int num, boolean allowRepeat) { if (interceptor != null) { - List choices = CharmEffect.makePossibleOptions(sa); return interceptor.chooseModesForAbility(choices, min, num, allowRepeat); } if (plan != null && plan.getSelectedDecision() != null && plan.getSelectedDecision().modes != null) { Plan.Decision decision = plan.getSelectedDecision(); - List choices = CharmEffect.makePossibleOptions(sa); // TODO: Validate that there's no discrepancies between choices and modes? List plannedModes = SpellAbilityChoicesIterator.getModeCombination(choices, decision.modes); if (plan.getSelectedDecision().targets != null) { diff --git a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java index e5bfcd5100f..f388eb60c1c 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityFactory.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityFactory.java @@ -286,7 +286,7 @@ public final class AbilityFactory { spellAbility.setDescription(sb.toString()); } else if (api == ApiType.Charm) { - spellAbility.setDescription(CharmEffect.makeSpellDescription(spellAbility)); + spellAbility.setDescription(CharmEffect.makeFormatedDescription(spellAbility)); } else { spellAbility.setDescription(""); } 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 3cba6e96b75..c40b4eb64ea 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -374,7 +374,7 @@ public class AbilityUtils { if (StringUtils.isBlank(amount)) { return 0; } if (card == null) { return 0; } final Player player = card.getController(); - final Game game = player.getGame(); + final Game game = player == null ? card.getGame() : player.getGame(); // Strip and save sign for calculations final boolean startsWithPlus = amount.charAt(0) == '+'; 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 faed7543bcc..652d7728a33 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 @@ -20,27 +20,24 @@ public class CharmEffect extends SpellAbilityEffect { public static List makePossibleOptions(final SpellAbility sa) { final Card source = sa.getHostCard(); - Iterable restriction = null; + List restriction = null; if (sa.hasParam("ChoiceRestriction")) { String rest = sa.getParam("ChoiceRestriction"); - if (rest.equals("NotRemembered")) { - restriction = source.getRemembered(); + if (rest.equals("ThisGame")) { + restriction = source.getChosenModesGame(sa); + } else if (rest.equals("ThisTurn")) { + restriction = source.getChosenModesTurn(sa); } } - + int indx = 0; List choices = Lists.newArrayList(sa.getAdditionalAbilityList("Choices")); if (restriction != null) { List toRemove = Lists.newArrayList(); - for (Object o : restriction) { - if (o instanceof AbilitySub) { - String abText = ((AbilitySub)o).getDescription(); - for (AbilitySub ch : choices) { - if (ch.getDescription().equals(abText)) { - toRemove.add(ch); - } - } + for (AbilitySub ch : choices) { + if (restriction.contains(ch.getDescription())) { + toRemove.add(ch); } } choices.removeAll(toRemove); @@ -54,55 +51,24 @@ public class CharmEffect extends SpellAbilityEffect { return choices; } - public static String makeSpellDescription(SpellAbility sa) { - int num = Integer.parseInt(sa.getParamOrDefault("CharmNum", "1")); - int min = Integer.parseInt(sa.getParamOrDefault("MinCharmNum", String.valueOf(num))); - boolean repeat = sa.hasParam("CanRepeatModes"); - boolean random = sa.hasParam("Random"); - boolean oppChooses = "Opponent".equals(sa.getParam("Chooser")); - - List list = CharmEffect.makePossibleOptions(sa); - - StringBuilder sb = new StringBuilder(); - sb.append(sa.getCostDescription()); - sb.append(oppChooses ? "An opponent chooses " : "Choose "); - - if (num == min) { - sb.append(Lang.getNumeral(num)); - } else if (min == 0) { - sb.append("up to ").append(Lang.getNumeral(num)); - } else { - sb.append(Lang.getNumeral(min)).append(" or ").append(list.size() == 2 ? "both" : "more"); - } - - if (random) { - sb.append("at random."); - } - if (repeat) { - sb.append(". You may choose the same mode more than once."); - } - sb.append(" - "); - int i = 0; - for (AbilitySub sub : list) { - if (i > 0) { - sb.append("; "); - } - sb.append(sub.getParam("SpellDescription")); - ++i; - } - - return sb.toString(); - } - public static String makeFormatedDescription(SpellAbility sa) { - int num = Integer.parseInt(sa.getParamOrDefault("CharmNum", "1")); - int min = Integer.parseInt(sa.getParamOrDefault("MinCharmNum", String.valueOf(num))); + Card source = sa.getHostCard(); + + List list = CharmEffect.makePossibleOptions(sa); + final int num; + // hotfix for Vindictive Lich when using getCardForUi + if (source.getController() == null && sa.getParamOrDefault("CharmNum", "1").contains("MaxUniqueOpponents")) { + // using getCardForUi game is not set, so can't guess max charm + num = Integer.MAX_VALUE; + } else { + num = Math.min(AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa), list.size()); + } + final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num; + boolean repeat = sa.hasParam("CanRepeatModes"); boolean random = sa.hasParam("Random"); boolean oppChooses = "Opponent".equals(sa.getParam("Chooser")); - List list = CharmEffect.makePossibleOptions(sa); - StringBuilder sb = new StringBuilder(); sb.append(sa.getCostDescription()); sb.append(oppChooses ? "An opponent chooses " : "Choose "); @@ -117,8 +83,10 @@ public class CharmEffect extends SpellAbilityEffect { if (sa.hasParam("ChoiceRestriction")) { String rest = sa.getParam("ChoiceRestriction"); - if (rest.equals("NotRemembered")) { + if (rest.equals("ThisGame")) { sb.append(" that hasn't been chosen"); + } else if (rest.equals("ThisTurn")) { + sb.append(" that hasn't been chosen this turn"); } } @@ -163,39 +131,52 @@ public class CharmEffect extends SpellAbilityEffect { //this resets all previous choices sa.setSubAbility(null); + List choices = makePossibleOptions(sa); + // Entwine does use all Choices if (sa.isEntwine()) { - chainAbilities(sa, makePossibleOptions(sa)); - return true; - } - - final int num = sa.hasParam("CharmNumOnResolve") ? - AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CharmNumOnResolve"), sa) - : Integer.parseInt(sa.getParamOrDefault("CharmNum", "1")); - final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num; - - if (sa.hasParam("Random")) { - chainAbilities(sa, Aggregates.random(makePossibleOptions(sa), num)); + chainAbilities(sa, choices); return true; } Card source = sa.getHostCard(); Player activator = sa.getActivatingPlayer(); + + final int num = Math.min(AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa), choices.size()); + final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num; + + // if the amount of choices is smaller than min then they can't be chosen + if (min > choices.size()) { + return false; + } + + if (sa.hasParam("Random")) { + chainAbilities(sa, Aggregates.random(choices, num)); + return true; + } + Player chooser = sa.getActivatingPlayer(); if (sa.hasParam("Chooser")) { // Three modal cards require you to choose a player to make the modal choice' // Two of these also reference the chosen player during the spell effect - - //String choosers = sa.getParam("Chooser"); + + //String choosers = sa.getParam("Chooser"); FCollection opponents = activator.getOpponents(); // all cards have Choser$ Opponent, so it's hardcoded here chooser = activator.getController().chooseSingleEntityForEffect(opponents, sa, "Choose an opponent", null); source.setChosenPlayer(chooser); } - - List chosen = chooser.getController().chooseModeForAbility(sa, min, num, sa.hasParam("CanRepeatModes")); + + List chosen = chooser.getController().chooseModeForAbility(sa, choices, min, num, sa.hasParam("CanRepeatModes")); chainAbilities(sa, chosen); - return chosen != null && !chosen.isEmpty(); + + // trigger without chosen modes are removed from stack + if (sa.isTrigger()) { + return chosen != null && !chosen.isEmpty(); + } + + // for spells and activated abilities it is possible to chose zero if minCharmNum allows it + return true; } private static void chainAbilities(SpellAbility sa, List chosen) { @@ -239,7 +220,7 @@ public class CharmEffect extends SpellAbilityEffect { // add Clone to Tail of sa sa.appendSubAbility(clone); } - + } 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 929bd3c50f5..75bcc36ae3a 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -34,7 +34,6 @@ import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; -import forge.game.ability.effects.CharmEffect; import forge.game.combat.Combat; import forge.game.cost.Cost; import forge.game.cost.CostSacrifice; @@ -280,6 +279,11 @@ public class Card extends GameEntity implements Comparable { private final Table numberTurnActivationsStatic = HashBasedTable.create(); private final Table numberGameActivationsStatic = HashBasedTable.create(); + private final Map> chosenModesTurn = Maps.newHashMap(); + private final Map> chosenModesGame = Maps.newHashMap(); + + private final Table> chosenModesTurnStatic = HashBasedTable.create(); + private final Table> chosenModesGameStatic = HashBasedTable.create(); // Enumeration for CMC request types public enum SplitCMCMode { @@ -2326,14 +2330,7 @@ public class Card extends GameEntity implements Comparable { private String formatSpellAbility(final SpellAbility sa) { final StringBuilder sb = new StringBuilder(); - final String elementText = sa.toString(); - - //Determine if a card has multiple choices, then format it in an easier to read list. - if (ApiType.Charm.equals(sa.getApi())) { - sb.append(CharmEffect.makeFormatedDescription(sa)); - } else { - sb.append(elementText).append("\r\n"); - } + sb.append(sa.toString()).append("\r\n"); return sb.toString(); } @@ -5916,6 +5913,7 @@ public class Card extends GameEntity implements Comparable { clearBlockedThisTurn(); resetMayPlayTurn(); resetExtertedThisTurn(); + resetChosenModeTurn(); } public boolean hasETBTrigger(final boolean drawbackOnly) { @@ -6592,6 +6590,94 @@ public class Card extends GameEntity implements Comparable { numberTurnActivationsStatic.clear(); } + public List getChosenModesTurn(SpellAbility ability) { + SpellAbility original = null; + SpellAbility root = ability.getRootAbility(); + + // because trigger spell abilities are copied, try to get original one + if (root.isTrigger()) { + original = root.getTrigger().getOverridingAbility(); + } else { + original = ability.getOriginalAbility(); + if (original == null) { + original = ability; + } + } + + if (ability.getGrantorStatic() != null) { + return chosenModesTurnStatic.get(original, ability.getGrantorStatic()); + } + return chosenModesTurn.get(original); + } + public List getChosenModesGame(SpellAbility ability) { + SpellAbility original = null; + SpellAbility root = ability.getRootAbility(); + + // because trigger spell abilities are copied, try to get original one + if (root.isTrigger()) { + original = root.getTrigger().getOverridingAbility(); + } else { + original = ability.getOriginalAbility(); + if (original == null) { + original = ability; + } + } + + if (ability.getGrantorStatic() != null) { + return chosenModesGameStatic.get(original, ability.getGrantorStatic()); + } + return chosenModesGame.get(original); + } + + public void addChosenModes(SpellAbility ability, String mode) { + SpellAbility original = null; + SpellAbility root = ability.getRootAbility(); + + // because trigger spell abilities are copied, try to get original one + if (root.isTrigger()) { + original = root.getTrigger().getOverridingAbility(); + } else { + original = ability.getOriginalAbility(); + if (original == null) { + original = ability; + } + } + + if (ability.getGrantorStatic() != null) { + List result = chosenModesTurnStatic.get(original, ability.getGrantorStatic()); + if (result == null) { + result = Lists.newArrayList(); + chosenModesTurnStatic.put(original, ability.getGrantorStatic(), result); + } + result.add(mode); + result = chosenModesGameStatic.get(original, ability.getGrantorStatic()); + if (result == null) { + result = Lists.newArrayList(); + chosenModesGameStatic.put(original, ability.getGrantorStatic(), result); + } + result.add(mode); + } else { + List result = chosenModesTurn.get(original); + if (result == null) { + result = Lists.newArrayList(); + chosenModesTurn.put(original, result); + } + result.add(mode); + + result = chosenModesGame.get(original); + if (result == null) { + result = Lists.newArrayList(); + chosenModesGame.put(original, result); + } + result.add(mode); + } + } + + public void resetChosenModeTurn() { + chosenModesTurn.clear(); + chosenModesTurnStatic.clear(); + } + public int getPlaneswalkerAbilityActivated() { return planeswalkerAbilityActivated; } diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index 6c3e345a52e..2e5592c8a89 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -213,7 +213,7 @@ public abstract class PlayerController { public abstract boolean chooseFlipResult(SpellAbility sa, Player flipper, boolean[] results, boolean call); public abstract Card chooseProtectionShield(GameEntity entityBeingDamaged, List options, Map choiceMap); - public abstract List chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat); + public abstract List chooseModeForAbility(SpellAbility sa, List possible, int min, int num, boolean allowRepeat); public abstract byte chooseColor(String message, SpellAbility sa, ColorSet colors); public abstract byte chooseColorAllowColorless(String message, Card c, ColorSet colors); diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java index b8a03d637a2..06d31a5e0d9 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerHandler.java @@ -575,6 +575,8 @@ public class TriggerHandler { sa.setStackDescription(sa.toString()); if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { + // need to be set for demonic pact to look for chosen modes + sa.setTrigger(regtrig); if (!CharmEffect.makeChoices(sa)) { // 603.3c If no mode is chosen, the ability is removed from the stack. return; diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index 17933b95a88..d0b4873a19e 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -246,9 +246,9 @@ public class MagicStack /* extends MyObservable */ implements Iterable chooseModeForAbility(SpellAbility sa, int min, int num, boolean allowRepeat) { + public List chooseModeForAbility(SpellAbility sa, List possible, int min, int num, boolean allowRepeat) { throw new IllegalStateException("Erring on the side of caution here..."); } diff --git a/forge-gui/res/cardsfolder/c/captive_audience.txt b/forge-gui/res/cardsfolder/c/captive_audience.txt index 3890127f79a..c13efac2b57 100644 --- a/forge-gui/res/cardsfolder/c/captive_audience.txt +++ b/forge-gui/res/cardsfolder/c/captive_audience.txt @@ -5,14 +5,11 @@ R:Event$ Moved | ValidCard$ Card.Self | Destination$ Battlefield | ReplaceWith$ SVar:DBChooseOpp:DB$ ChoosePlayer | Defined$ You | Choices$ Player.Opponent | ChoiceTitle$ Choose an opponent to give control to: | AILogic$ Curse | SubAbility$ MoveToPlay SVar:MoveToPlay:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Battlefield | Defined$ ReplacedCard | GainControl$ True | NewController$ ChosenPlayer | SubAbility$ ClearRemembered T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY -SVar:TrigCharm:DB$ Charm | Choices$ LifePact,DiscardPact,ZombiesPact | ChoiceRestriction$ NotRemembered | RememberChoice$ True | CharmNum$ 1 -SVar:LifePact:DB$ SetLife | Defined$ You | LifeAmount$ 4 | ChoiceName$ LifePact | SpellDescription$ Your life total becomes 4. -SVar:DiscardPact:DB$ Discard | Defined$ You | Mode$ Hand | ChoiceName$ DiscardPact | SpellDescription$ Discard your hand. -SVar:ZombiesPact:DB$ RepeatEach | RepeatPlayers$ Player.Opponent | RepeatSubAbility$ MakeZombies | ChoiceName$ ZombiesPact | ChangeZoneTable$ True | SpellDescription$ Each opponent creates five 2/2 black Zombie creature tokens. +SVar:TrigCharm:DB$ Charm | Choices$ LifePact,DiscardPact,ZombiesPact | ChoiceRestriction$ ThisGame | CharmNum$ 1 +SVar:LifePact:DB$ SetLife | Defined$ You | LifeAmount$ 4 | SpellDescription$ Your life total becomes 4. +SVar:DiscardPact:DB$ Discard | Defined$ You | Mode$ Hand | SpellDescription$ Discard your hand. +SVar:ZombiesPact:DB$ RepeatEach | RepeatPlayers$ Player.Opponent | RepeatSubAbility$ MakeZombies | ChangeZoneTable$ True | SpellDescription$ Each opponent creates five 2/2 black Zombie creature tokens. SVar:MakeZombies:DB$ Token | LegacyImage$ b 2 2 zombie rna | TokenAmount$ 5 | TokenScript$ b_2_2_zombie | TokenOwner$ Remembered | SpellDescription$ Each opponent creates five 2/2 black Zombie creature tokens. -# Clear RememberChoice just in case it's not getting cleared by Zone changes -T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ ClearRemembered | Static$ True -SVar:ClearRemembered:DB$ Cleanup | ClearRemembered$ True | ClearChosenPlayer$ True AI:RemoveDeck:Random DeckHas:Ability$Token Oracle:Captive Audience enters the battlefield under the control of an opponent of your choice.\nAt the beginning of your upkeep, choose one that hasn't been chosen —\n• Your life total becomes 4.\n• Discard your hand.\n• Each opponent creates five 2/2 black Zombie creature tokens. diff --git a/forge-gui/res/cardsfolder/d/demonic_pact.txt b/forge-gui/res/cardsfolder/d/demonic_pact.txt index b240b246ee4..aff9a33cf7a 100644 --- a/forge-gui/res/cardsfolder/d/demonic_pact.txt +++ b/forge-gui/res/cardsfolder/d/demonic_pact.txt @@ -2,15 +2,11 @@ Name:Demonic Pact ManaCost:2 B B Types:Enchantment T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY -SVar:TrigCharm:DB$ Charm | Choices$ DrainPact,DiscardPact,DrawPact,DeathPact | ChoiceRestriction$ NotRemembered | RememberChoice$ True | CharmNum$ 1 -SVar:DrainPact:DB$ DealDamage | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | NumDmg$ 4 | SubAbility$ DBGainLife | ChoiceName$ DrainPact | SpellDescription$ CARDNAME deals 4 damage to any target and you gain 4 life. +SVar:TrigCharm:DB$ Charm | Choices$ DrainPact,DiscardPact,DrawPact,DeathPact | ChoiceRestriction$ ThisGame | CharmNum$ 1 +SVar:DrainPact:DB$ DealDamage | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | NumDmg$ 4 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 4 damage to any target and you gain 4 life. SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 4 -SVar:DiscardPact:DB$ Discard | ValidTgts$ Player | NumCards$ 2 | Mode$ TgtChoose | ChoiceName$ DiscardPact | SpellDescription$ Target player discards two cards. -SVar:DrawPact:DB$ Draw | NumCards$ 2 | ChoiceName$ DrawPact | SpellDescription$ Draw two cards. -SVar:DeathPact:DB$ LosesGame | Defined$ You | ChoiceName$ DeathPact | SpellDescription$ You lose the game. -# Clear RememberChoice just in case it's not getting cleared by Zone changes -T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ ClearRemembered | Static$ True -SVar:ClearRemembered:DB$ Cleanup | ClearRemembered$ True +SVar:DiscardPact:DB$ Discard | ValidTgts$ Player | NumCards$ 2 | Mode$ TgtChoose | SpellDescription$ Target player discards two cards. +SVar:DrawPact:DB$ Draw | NumCards$ 2 | SpellDescription$ Draw two cards. +SVar:DeathPact:DB$ LosesGame | Defined$ You | SpellDescription$ You lose the game. AI:RemoveDeck:All -SVar:Picture:http://www.wizards.com/global/images/magic/general/demonic_pact.jpg Oracle:At the beginning of your upkeep, choose one that hasn't been chosen —\n• Demonic Pact deals 4 damage to any target and you gain 4 life.\n• Target opponent discards two cards.\n• Draw two cards.\n• You lose the game. diff --git a/forge-gui/res/cardsfolder/i/inscription_of_abundance.txt b/forge-gui/res/cardsfolder/i/inscription_of_abundance.txt new file mode 100644 index 00000000000..e022e047e90 --- /dev/null +++ b/forge-gui/res/cardsfolder/i/inscription_of_abundance.txt @@ -0,0 +1,14 @@ +Name:Inscription of Abundance +ManaCost:1 G +Types:Instant +K:Kicker:2 G +A:SP$ Charm | Cost$ 1 G | MinCharmNum$ X | CharmNum$ Y | References$ X,Y | Choices$ DBPutCounter,DBGainLife,DBPump | AdditionalDescription$ If this spell was kicked, choose any number instead. +SVar:DBPutCounter:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select target creature | CounterType$ P1P1 | CounterNum$ 2 | SpellDescription$ Put two +1/+1 counters on target creature. +SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | TgtPrompt$ Select target player | LifeAmount$ Z | References$ Z | SpellDescription$ Target player gains X life, where X is the greatest power among creatures they control. +SVar:DBPump:DB$ Pump | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | AILogic$ Fight | SubAbility$ DBFight | SpellDescription$ Target creature you control fights target creature you don't control. +SVar:DBFight:DB$ Fight | Defined$ ParentTarget | ValidTgts$ Creature.YouDontCtrl | TgtPrompt$ Select target creature you don't control +SVar:X:Count$Kicked.0.1 +SVar:Y:Count$Kicked.3.1 +SVar:Z:Count$GreatestPower_Creature.TargetedPlayerCtrl +Oracle:Kicker {2}{G}\nChoose one. If this spell was kicked, choose any number instead.\n• Put two +1/+1 counters on target creature.\n• Target player gains X life, where X is the greatest power among creatures they control.\n• Target creature you control fights target creature you don't control. + diff --git a/forge-gui/res/cardsfolder/i/inscription_of_insight.txt b/forge-gui/res/cardsfolder/i/inscription_of_insight.txt new file mode 100644 index 00000000000..4752572a7eb --- /dev/null +++ b/forge-gui/res/cardsfolder/i/inscription_of_insight.txt @@ -0,0 +1,14 @@ +Name:Inscription of Insight +ManaCost:3 U +Types:Sorcery +K:Kicker:2 U U +A:SP$ Charm | Cost$ 3 U | MinCharmNum$ X | CharmNum$ Y | References$ X,Y | Choices$ DBReturn,DBScry,DBToken | AdditionalDescription$ If this spell was kicked, choose any number instead. +SVar:DBReturn:DB$ ChangeZone | TargetMin$ 0 | TargetMax$ 2 | ValidTgts$ Creature | TgtPrompt$ Select up to two target creatures | Origin$ Battlefield | Destination$ Hand | SpellDescription$ Return up to two target creatures to their owners' hands. +SVar:DBScry:DB$ Scry | ScryNum$ 2 | SubAbility$ DBDraw | SpellDescription$ Scry 2, then draw two cards. +SVar:DBDraw:DB$ Draw | NumCards$ 2 +SVar:DBToken:DB$ Token | ValidTgts$ Player | TgtPrompt$ Select target player | TokenAmount$ 1 | TokenScript$ u_x_x_illusion | TokenOwner$ TargetedPlayer | TokenPower$ Z | TokenToughness$ Z | References$ Z | SpellDescription$ Target player creates an X/X blue Illusion creature token, where X is the number of cards in their hand. +SVar:X:Count$Kicked.0.1 +SVar:Y:Count$Kicked.3.1 +SVar:Z:TargetedPlayer$CardsInHand +Oracle:Kicker {2}{U}{U}\nChoose one. If this spell was kicked, choose any number instead.\n• Return up to two target creatures to their owners’ hands.\n• Scry 2, then draw two cards.\n• Target player creates an X/X blue Illusion creature token, where X is the number of cards in their hand. + diff --git a/forge-gui/res/cardsfolder/i/inscription_of_ruin.txt b/forge-gui/res/cardsfolder/i/inscription_of_ruin.txt new file mode 100644 index 00000000000..8674495dcaf --- /dev/null +++ b/forge-gui/res/cardsfolder/i/inscription_of_ruin.txt @@ -0,0 +1,12 @@ +Name:Inscription of Ruin +ManaCost:2 B +Types:Sorcery +K:Kicker:2 B B +A:SP$ Charm | Cost$ 2 B | MinCharmNum$ X | CharmNum$ Y | References$ X,Y | Choices$ DBDiscard,DBReturn,DBDestroy | AdditionalDescription$ If this spell was kicked, choose any number instead. +SVar:DBDiscard:DB$ Discard | ValidTgts$ Opponent | NumCards$ 2 | Mode$ TgtChoose | SpellDescription$ Target opponent discards two cards. +SVar:DBReturn:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Card.Creature+cmcLE2+YouOwn | TgtPrompt$ Select target creature card with converted mana cost 2 or less | SpellDescription$ Return target creature card with converted mana cost 2 or less from your graveyard to the battlefield. +SVar:DBDestroy:DB$ Destroy | ValidTgts$ Creature.cmcLE3 | TgtPrompt$ Select target creature with converted mana cost 3 or less | SpellDescription$ Destroy target creature with converted mana cost 3 or less. +SVar:X:Count$Kicked.0.1 +SVar:Y:Count$Kicked.3.1 +Oracle:Kicker {2}{B}{B}\nChoose one. If this spell was kicked, choose any number instead.\n• Target opponent discards two cards.\n• Return target creature card with converted mana cost 2 or less from your graveyard to the battlefield.\n• Destroy target creature with converted mana cost 3 or less. + diff --git a/forge-gui/res/cardsfolder/k/kargan_intimidator.txt b/forge-gui/res/cardsfolder/k/kargan_intimidator.txt index f26e3246a31..44838f413c7 100755 --- a/forge-gui/res/cardsfolder/k/kargan_intimidator.txt +++ b/forge-gui/res/cardsfolder/k/kargan_intimidator.txt @@ -4,9 +4,7 @@ Types:Creature Human Warrior PT:3/1 S:Mode$ Continuous | Affected$ Creature.Coward | AddHiddenKeyword$ CantBlock Creature.Warrior:Warriors | Description$ Cowards can't block Warriors. SVar:PlayMain1:TRUE -T:Mode$ TurnBegin | ValidPlayer$ Player | Static$ True | TriggerZones$ Battlefield | Execute$ CharmReset -SVar:CharmReset:DB$ Cleanup | ClearRemembered$ True -A:AB$ Charm | Cost$ 1 | Choices$ Pump,Coward,Trample | ChoiceRestriction$ NotRemembered | RememberChoice$ True | CharmNum$ 1 +A:AB$ Charm | Cost$ 1 | Choices$ Pump,Coward,Trample | ChoiceRestriction$ ThisTurn | CharmNum$ 1 SVar:Pump:DB$ Pump | Defined$ Self | NumAtt$ 1 | NumDef$ 1 | SpellDescription$ CARDNAME gets +1/+1 until end of turn. SVar:Coward:DB$ Animate | ValidTgts$ Creature | TgtPrompt$ Select target creature | Types$ Coward | RemoveCreatureTypes$ True | SpellDescription$ Target creature becomes a Coward until end of turn. SVar:Trample:DB$ Pump | ValidTgts$ Warrior | TgtPrompt$ Select target Warrior | KW$ Trample | SpellDescription$ Target Warrior gains trample until end of turn. diff --git a/forge-gui/res/cardsfolder/v/vindictive_lich.txt b/forge-gui/res/cardsfolder/v/vindictive_lich.txt index 8c94811a0f7..e8c91bbf45c 100644 --- a/forge-gui/res/cardsfolder/v/vindictive_lich.txt +++ b/forge-gui/res/cardsfolder/v/vindictive_lich.txt @@ -3,11 +3,9 @@ ManaCost:3 B Types:Creature Zombie Wizard PT:4/1 T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigCharm | TriggerController$ TriggeredCardController | TriggerDescription$ When CARDNAME dies, ABILITY -SVar:TrigCharm:DB$ Charm | MinCharmNum$ 1 | CharmNum$ 3 | CharmNumOnResolve$ MaxUniqueOpponents | Choices$ SacCreature,DiscardCards,LoseLife | References$ MaxUniqueOpponents | AdditionalDescription$ Each mode must target a different player. +SVar:TrigCharm:DB$ Charm | MinCharmNum$ 1 | CharmNum$ MaxUniqueOpponents | Choices$ SacCreature,DiscardCards,LoseLife | References$ MaxUniqueOpponents | AdditionalDescription$ Each mode must target a different player. SVar:SacCreature:DB$ Sacrifice | ValidTgts$ Opponent | TargetUnique$ True | SacValid$ Creature | SacMessage$ Creature | SpellDescription$ Target opponent sacrifices a creature. SVar:DiscardCards:DB$ Discard | ValidTgts$ Opponent | TargetUnique$ True | NumCards$ 2 | Mode$ TgtChoose | SpellDescription$ Target opponent discards two cards. SVar:LoseLife:DB$ LoseLife | ValidTgts$ Opponent | TargetUnique$ True | LifeAmount$ 5 | SpellDescription$ Target opponent loses 5 life. SVar:MaxUniqueOpponents:PlayerCountOpponents$Amount -#TODO: The AI is able to target the same player with multiple modes, usually all three. This should not happen. -AI:RemoveDeck:All Oracle:When Vindictive Lich dies, choose one or more. Each mode must target a different player.\n• Target opponent sacrifices a creature.\n• Target opponent discards two cards.\n• Target opponent loses 5 life. diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index 33afd029f80..c30fc7064fd 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -85,7 +85,9 @@ public class HumanPlay { } if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { - CharmEffect.makeChoices(sa); + if (!CharmEffect.makeChoices(sa)) { + return false; + } } sa = AbilityUtils.addSpliceEffects(sa); @@ -150,7 +152,9 @@ public class HumanPlay { if (!sa.isCopied()) { if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { - CharmEffect.makeChoices(sa); + if (!CharmEffect.makeChoices(sa)) { + return; + } } sa = AbilityUtils.addSpliceEffects(sa); } diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 2ae685e6b9b..7b4c4c86d2e 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -1,20 +1,8 @@ package forge.player; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileWriter; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import java.io.*; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; import org.apache.commons.lang3.Range; import org.apache.commons.lang3.StringUtils; @@ -62,7 +50,6 @@ import forge.game.PlanarDice; import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityKey; 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; @@ -1593,14 +1580,13 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont * spellability.SpellAbility, java.util.List, int, int) */ @Override - public List chooseModeForAbility(final SpellAbility sa, final int min, final int num, + public List chooseModeForAbility(final SpellAbility sa, List possible, final int min, final int num, boolean allowRepeat) { boolean trackerFrozen = game.getTracker().isFrozen(); if (trackerFrozen) { // The view tracker needs to be unfrozen to update the SpellAbilityViews at this point, or it may crash game.getTracker().unfreeze(); } - final List possible = CharmEffect.makePossibleOptions(sa); Map spellViewCache = SpellAbilityView.getMap(possible); if (trackerFrozen) { game.getTracker().freeze(); // refreeze if the tracker was frozen prior to this update