From f45d17e368efdc66332c0d44fed7424638cd130f Mon Sep 17 00:00:00 2001 From: tool4ever Date: Fri, 9 Feb 2024 14:10:41 +0100 Subject: [PATCH] PlayEffect: support AltCosts (#4649) --- .../src/main/java/forge/ai/AiController.java | 3 +- .../java/forge/ai/ComputerUtilAbility.java | 2 +- .../java/forge/ai/ComputerUtilCombat.java | 2 +- .../java/forge/ai/ability/DiscoverAi.java | 3 +- .../main/java/forge/ai/ability/PlayAi.java | 2 +- .../main/java/forge/game/CardTraitBase.java | 4 ++ .../src/main/java/forge/game/GameAction.java | 2 +- .../main/java/forge/game/GameActionUtil.java | 5 ++- .../java/forge/game/ability/AbilityUtils.java | 44 ++++++++++++------- .../ability/effects/ChangeZoneEffect.java | 1 - .../game/ability/effects/DiscoverEffect.java | 2 - .../game/ability/effects/PlayEffect.java | 12 ++--- .../src/main/java/forge/game/card/Card.java | 7 ++- .../java/forge/game/card/CardFactoryUtil.java | 21 --------- .../main/java/forge/game/combat/Combat.java | 29 ++++++------ .../java/forge/game/spellability/Spell.java | 2 +- .../forge/game/spellability/SpellAbility.java | 11 ++++- .../spellability/SpellAbilityRestriction.java | 9 +++- ...nventive_iteration_living_breakthrough.txt | 2 +- .../cardsfolder/k/karn_the_great_creator.txt | 2 +- .../l/linvala_keeper_of_silence.txt | 2 +- .../res/cardsfolder/p/press_the_enemy.txt | 2 +- .../gamemodes/match/input/InputPayMana.java | 2 +- .../java/forge/player/HumanCostDecision.java | 2 +- .../forge/player/PlayerControllerHuman.java | 2 +- 25 files changed, 87 insertions(+), 88 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 75c468fee60..ba0d03ecb88 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1539,8 +1539,7 @@ public class AiController { boolean mustRespond = false; if (top != null) { mustRespond = top.hasParam("AIRespondsToOwnAbility"); // Forced combos (currently defined for Sensei's Divining Top) - mustRespond |= top.isTrigger() && top.getTrigger().getKeyword() != null - && top.getTrigger().getKeyword().getKeyword() == Keyword.EVOKE; // Evoke sacrifice trigger + mustRespond |= top.isTrigger() && top.getTrigger().isKeyword(Keyword.EVOKE); // Evoke sacrifice trigger } if (topOwnedByAI) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index 6242ca8b4cd..371f98f4496 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -106,7 +106,7 @@ public class ComputerUtilAbility { for (SpellAbility sa : originListWithAddCosts) { // determine which alternative costs are cheaper than the original and prioritize them - List saAltCosts = GameActionUtil.getAlternativeCosts(sa, player); + List saAltCosts = GameActionUtil.getAlternativeCosts(sa, player, false); List priorityAltSa = Lists.newArrayList(); List otherAltSa = Lists.newArrayList(); for (SpellAbility altSa : saAltCosts) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index b2a6d08235d..ac30b112a7b 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -1231,7 +1231,7 @@ public class ComputerUtilCombat { } // Extra check for the Exalted trigger in case we're declaring more than one attacker - if (combat != null && trigger.getKeyword() != null && trigger.getKeyword().getKeyword() == Keyword.EXALTED) { + if (combat != null && trigger.isKeyword(Keyword.EXALTED)) { if (!combat.getAttackers().isEmpty() && !combat.getAttackers().contains(attacker)) { continue; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java index a9afdd1340d..9541d8266d5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java @@ -4,7 +4,6 @@ import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.PlayerControllerAi; import forge.ai.SpellAbilityAi; -import forge.card.CardStateName; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.player.Player; @@ -45,7 +44,7 @@ public class DiscoverAi extends SpellAbilityAi { @Override public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { Card c = (Card)params.get("Card"); - for (SpellAbility s : AbilityUtils.getBasicSpellsFromPlayEffect(c, ai, CardStateName.Original)) { // TODO: other states for split cards and MDFC? + for (SpellAbility s : AbilityUtils.getBasicSpellsFromPlayEffect(c, ai)) { if (s instanceof LandAbility) { // return false or we get a ClassCastException later if the AI encounters MDFC with land backside return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java index 43ca4886a2c..570e44e9d0b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -153,7 +153,7 @@ public class PlayAi extends SpellAbilityAi { public boolean apply(final Card c) { // TODO needs to be aligned for MDFC along with getAbilityToPlay so the knowledge // of which spell was the reason for the choice can be used there - for (SpellAbility s : AbilityUtils.getBasicSpellsFromPlayEffect(c, ai, state)) { + for (SpellAbility s : AbilityUtils.getSpellsFromPlayEffect(c, ai, state, false)) { if (!sa.matchesValidParam("ValidSA", s)) { continue; } diff --git a/forge-game/src/main/java/forge/game/CardTraitBase.java b/forge-game/src/main/java/forge/game/CardTraitBase.java index 427b2c1468a..e7a09ee5eb2 100644 --- a/forge-game/src/main/java/forge/game/CardTraitBase.java +++ b/forge-game/src/main/java/forge/game/CardTraitBase.java @@ -20,6 +20,7 @@ import forge.game.card.CardPredicates; import forge.game.card.CardState; import forge.game.card.CardView; import forge.game.card.IHasCardView; +import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -145,6 +146,9 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView, this.hostCard = c; } + public boolean isKeyword(Keyword kw) { + return this.keyword != null && this.keyword.getKeyword() == kw; + } public KeywordInterface getKeyword() { return this.keyword; } diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index a7a578ff23e..2fed780b748 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -116,7 +116,7 @@ public class GameAction { // Rule 111.8: A token that has left the battlefield can't move to another zone if (!c.isSpell() && c.isToken() && !fromBattlefield && zoneFrom != null && !zoneFrom.is(ZoneType.Stack) - && (cause == null || !(cause instanceof SpellPermanent) || !cause.hasSVar("IsCastFromPlayEffect"))) { + && (cause == null || !(cause instanceof SpellPermanent) || !cause.isCastFromPlayEffect())) { return c; } diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java index ae6cf563e02..1f3e35345d8 100644 --- a/forge-game/src/main/java/forge/game/GameActionUtil.java +++ b/forge-game/src/main/java/forge/game/GameActionUtil.java @@ -82,7 +82,7 @@ public final class GameActionUtil { * a possible alternative cost the provided activator can use to pay * the provided {@link SpellAbility}. */ - public static final List getAlternativeCosts(final SpellAbility sa, final Player activator) { + public static final List getAlternativeCosts(final SpellAbility sa, final Player activator, boolean altCostOnly) { final List alternatives = Lists.newArrayList(); Card source = sa.getHostCard(); @@ -134,6 +134,9 @@ public final class GameActionUtil { newSA.setBasicSpell(false); changedManaCost = true; } else { + if (altCostOnly) { + continue; + } newSA = sa.copy(activator); } 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 61d583fb95d..32f6854b74e 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -2898,21 +2898,17 @@ public class AbilityUtils { } public static final List getBasicSpellsFromPlayEffect(final Card tgtCard, final Player controller) { - return getBasicSpellsFromPlayEffect(tgtCard, controller, CardStateName.Original); + return getSpellsFromPlayEffect(tgtCard, controller, CardStateName.Original, false); } - public static final List getBasicSpellsFromPlayEffect(final Card tgtCard, final Player controller, CardStateName state) { + public static final List getSpellsFromPlayEffect(final Card tgtCard, final Player controller, CardStateName state, boolean withAltCost) { List sas = new ArrayList<>(); - List list = Lists.newArrayList(tgtCard.getBasicSpells()); + List list = new ArrayList<>(); + collectSpellsForPlayEffect(list, tgtCard.getState(tgtCard.getCurrentStateName()), controller, withAltCost); CardState original = tgtCard.getState(state); if (tgtCard.isFaceDown()) { - Iterables.addAll(list, tgtCard.getBasicSpells(original)); + collectSpellsForPlayEffect(list, original, controller, withAltCost); } else { - if (tgtCard.isLand()) { - LandAbility la = new LandAbility(tgtCard, controller, null); - la.setCardState(original); - list.add(la); - } if (state == CardStateName.Transformed && tgtCard.isPermanent() && !tgtCard.isAura()) { // casting defeated battle Spell sp = new SpellPermanent(tgtCard, original); @@ -2920,13 +2916,7 @@ public class AbilityUtils { list.add(sp); } if (tgtCard.isModal()) { - CardState modal = tgtCard.getState(CardStateName.Modal); - Iterables.addAll(list, tgtCard.getBasicSpells(modal)); - if (modal.getType().isLand()) { - LandAbility la = new LandAbility(tgtCard, controller, null); - la.setCardState(modal); - list.add(la); - } + collectSpellsForPlayEffect(list, tgtCard.getState(CardStateName.Modal), controller, withAltCost); } } @@ -2949,6 +2939,7 @@ public class AbilityUtils { if (res.checkTimingRestrictions(tgtCard, newSA) // still need to check the other restrictions like Aftermath && res.checkOtherRestrictions(tgtCard, newSA, controller)) { + newSA.setCastFromPlayEffect(true); sas.add(newSA); } } @@ -2956,6 +2947,27 @@ public class AbilityUtils { return sas; } + private static void collectSpellsForPlayEffect(final List result, final CardState state, final Player controller, final boolean withAltCost) { + if (state.getType().isLand()) { + LandAbility la = new LandAbility(state.getCard(), controller, null); + la.setCardState(state); + result.add(la); + } + final Iterable spells = state.getSpellAbilities(); + for (SpellAbility sa : spells) { + if (!sa.isSpell()) { + continue; + } + if (!withAltCost && !sa.isBasicSpell()) { + continue; + } + result.add(sa); + if (withAltCost) { + result.addAll(GameActionUtil.getAlternativeCosts(sa, controller, true)); + } + } + } + public static final String applyAbilityTextChangeEffects(final String def, final CardTraitBase ability) { if (ability == null || !ability.isIntrinsic() || ability.hasParam("LockInText")) { return def; diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java index f4a1996c13b..d4d7f461d69 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java @@ -1058,7 +1058,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect { if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())), null)) { continue; } - tgtSA.setSVar("IsCastFromPlayEffect", "True"); // if played, that card cannot be found if (decider.getController().playSaFromPlayEffect(tgtSA)) { fetchList.remove(tgtCard); diff --git a/forge-game/src/main/java/forge/game/ability/effects/DiscoverEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DiscoverEffect.java index a8e03769fc8..0d9333ce947 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DiscoverEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DiscoverEffect.java @@ -125,8 +125,6 @@ public class DiscoverEffect extends SpellAbilityEffect { tgtSA.getTargetRestrictions().setMandatory(true); } - tgtSA.setSVar("IsCastFromPlayEffect", "True"); - if (p.getController().playSaFromPlayEffect(tgtSA)) { final Card played = tgtSA.getHostCard(); // add remember successfully played here if ever needed diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index 445350973ce..232a11b3cfd 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java @@ -91,6 +91,7 @@ public class PlayEffect extends SpellAbilityEffect { final boolean imprint = sa.hasParam("ImprintPlayed"); final boolean forget = sa.hasParam("ForgetPlayed"); final boolean hasTotalCMCLimit = sa.hasParam("WithTotalCMC"); + final boolean altCost = sa.hasParam("WithoutManaCost") || sa.hasParam("PlayCost"); int totalCMCLimit = Integer.MAX_VALUE; final Player controller; if (sa.hasParam("Controller")) { @@ -298,9 +299,7 @@ public class PlayEffect extends SpellAbilityEffect { state = CardStateName.Transformed; } - // TODO if cost isn't replaced should include alternative ones - // get basic spells (no flashback, etc.) - List sas = AbilityUtils.getBasicSpellsFromPlayEffect(tgtCard, controller, state); + List sas = AbilityUtils.getSpellsFromPlayEffect(tgtCard, controller, state, !altCost); if (sa.hasParam("ValidSA")) { final String valid[] = sa.getParam("ValidSA").split(","); sas.removeIf(sp -> !sp.isValid(valid, controller , source, sa)); @@ -367,8 +366,7 @@ public class PlayEffect extends SpellAbilityEffect { final int tgtCMC = tgtSA.getPayCosts().getTotalMana().getCMC(); // illegal action, cancel early - if ((sa.hasParam("WithoutManaCost") || sa.hasParam("PlayCost")) && tgtSA.costHasManaX() && - tgtSA.getPayCosts().getCostMana().getXMin() > 0) { + if (altCost && tgtSA.costHasManaX() && tgtSA.getPayCosts().getCostMana().getXMin() > 0) { continue; } @@ -384,7 +382,7 @@ public class PlayEffect extends SpellAbilityEffect { } abCost = new Cost(source.getManaCost(), false); } else if (cost.equals("SuspendCost")) { - abCost = Iterables.find(tgtCard.getNonManaAbilities(), s -> s.getKeyword() != null && s.getKeyword().getKeyword() == Keyword.SUSPEND).getPayCosts(); + abCost = Iterables.find(tgtCard.getNonManaAbilities(), s -> s.isKeyword(Keyword.SUSPEND)).getPayCosts(); } else { if (cost.contains("ConvertedManaCost")) { if (unpayableCost) { @@ -453,8 +451,6 @@ public class PlayEffect extends SpellAbilityEffect { addIllusionaryMaskReplace(tgtCard, sa, moveParams); } - tgtSA.setSVar("IsCastFromPlayEffect", "True"); - // Add controlled by player to target SA so when the spell is resolving, the controller would be changed again if (controlledByPlayer != null) { tgtSA.setControlledByPlayer(controlledByTimeStamp, controlledByPlayer); 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 741400dce4e..4f44da6ac5e 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -7095,7 +7095,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public List getAllPossibleAbilities(final Player player, final boolean removeUnplayable) { CardState oState = getState(CardStateName.Original); - // this can only be called by the Human final List abilities = Lists.newArrayList(); for (SpellAbility sa : getSpellAbilities()) { //adventure spell check @@ -7105,12 +7104,12 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } //add alternative costs as additional spell abilities abilities.add(sa); - abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player)); + abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false)); } if (isFaceDown() && isInZone(ZoneType.Exile)) { for (final SpellAbility sa : oState.getSpellAbilities()) { - abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player)); + abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false)); } } // Add Modal Spells @@ -7120,7 +7119,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { // only add Spells there if (sa.isSpell()) { abilities.add(sa); - abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player)); + abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player, false)); } } } 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 064e77d6866..1331c1cd30b 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -274,27 +274,6 @@ public class CardFactoryUtil { return AbilityFactory.getAbility(ab, sourceCard); } - /** - *

- * countOccurrences. - *

- * - * @param arg1 - * a {@link java.lang.String} object. - * @param arg2 - * a {@link java.lang.String} object. - * @return a int. - */ - public static int countOccurrences(final String arg1, final String arg2) { - int count = 0; - int index = 0; - while ((index = arg1.indexOf(arg2, index)) != -1) { - ++index; - ++count; - } - return count; - } - /** *

* parseMath. diff --git a/forge-game/src/main/java/forge/game/combat/Combat.java b/forge-game/src/main/java/forge/game/combat/Combat.java index 6d7cddb4d08..f1dfbaf4e87 100644 --- a/forge-game/src/main/java/forge/game/combat/Combat.java +++ b/forge-game/src/main/java/forge/game/combat/Combat.java @@ -521,7 +521,7 @@ public class Combat { * @param blocker the blocking creature. */ public void addBlockerToDamageAssignmentOrder(Card attacker, Card blocker) { - final CardCollection oldBlockers = blockersOrderedForDamageAssignment.get(attacker); + final CardCollection oldBlockers = blockersOrderedForDamageAssignment.get(attacker); if (oldBlockers == null || oldBlockers.isEmpty()) { blockersOrderedForDamageAssignment.put(attacker, new CardCollection(blocker)); } else { @@ -815,24 +815,21 @@ public class Combat { defender = getDefenderPlayerByAttacker(attacker); } - assignCombatDamageToCreature = !attacker.getGame().getCombat().isBlocked(attacker) && - getDefendersCreatures().size() > 0 && - attacker.hasKeyword("If CARDNAME is unblocked, you may have it assign its combat damage to " + - "a creature defending player controls.") && + assignCombatDamageToCreature = !attacker.getGame().getCombat().isBlocked(attacker) && getDefendersCreatures().size() > 0 && + attacker.hasKeyword("If CARDNAME is unblocked, you may have it assign its combat damage to a creature defending player controls.") && assigningPlayer.getController().confirmStaticApplication(attacker, PlayerActionConfirmMode.AlternativeDamageAssignment, - Localizer.getInstance().getMessage("lblAssignCombatDamageToCreature", - CardTranslation.getTranslatedName(attacker.getName())), null); - if (divideCombatDamageAsChoose) { - if (orderedBlockers == null || orderedBlockers.isEmpty()) { - orderedBlockers = getDefendersCreatures(); - } else { - for (Card c : getDefendersCreatures()) { - if (!orderedBlockers.contains(c)) { - orderedBlockers.add(c); - } - } + Localizer.getInstance().getMessage("lblAssignCombatDamageToCreature", CardTranslation.getTranslatedName(attacker.getName())), null); + if (divideCombatDamageAsChoose) { + if (orderedBlockers == null || orderedBlockers.isEmpty()) { + orderedBlockers = getDefendersCreatures(); + } else { + for (Card c : getDefendersCreatures()) { + if (!orderedBlockers.contains(c)) { + orderedBlockers.add(c); } } + } + } } assignedDamage = true; 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 27d30f323bd..a2b39c5e161 100644 --- a/forge-game/src/main/java/forge/game/spellability/Spell.java +++ b/forge-game/src/main/java/forge/game/spellability/Spell.java @@ -106,7 +106,7 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable // for uncastables like lotus bloom, check if manaCost is blank (except for morph spells) // but ignore if it comes from PlayEffect if (!isCastFaceDown() - && !hasSVar("IsCastFromPlayEffect") + && !isCastFromPlayEffect() && isBasicSpell() && origCost.isNoCost()) { 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 fa0fccfad60..b2bf0fa3453 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -166,6 +166,8 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private boolean isCopied = false; private boolean mayChooseNewTargets = false; + private boolean isCastFromPlayEffect = false; + private EnumSet optionalCosts = EnumSet.noneOf(OptionalCost.class); private TargetRestrictions targetRestrictions; private TargetChoices targetChosen = new TargetChoices(); @@ -1645,6 +1647,13 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit undoable = b; } + public boolean isCastFromPlayEffect() { + return isCastFromPlayEffect; + } + public void setCastFromPlayEffect(boolean b) { + isCastFromPlayEffect = b; + } + public boolean isCopied() { return isCopied; } @@ -2509,7 +2518,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit if (getRestrictions().isInstantSpeed()) { return true; } - if ((isSpell() || this instanceof LandAbility) && (hasSVar("IsCastFromPlayEffect") || host.isInstant() || host.hasKeyword(Keyword.FLASH))) { + if ((isSpell() || this instanceof LandAbility) && (isCastFromPlayEffect() || host.isInstant() || host.hasKeyword(Keyword.FLASH))) { return true; } diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java index 3b0b8a80097..d2fdf2f73bc 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java @@ -35,6 +35,7 @@ import forge.game.card.CardLists; import forge.game.card.CardPlayOption; import forge.game.card.CardUtil; import forge.game.cost.IndividualCostPaymentInstance; +import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.staticability.StaticAbilityCastWithFlash; @@ -249,7 +250,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { } if (sa.isSpell()) { final CardPlayOption o = c.mayPlay(sa.getMayPlay()); - if (o == null) { + if (o == null || sa.isCastFromPlayEffect()) { return this.getZone() == null || (cardZone != null && cardZone.is(this.getZone())); } else if (o.getPlayer() == activator) { Map params = sa.getMayPlay().getMapParams(); @@ -377,6 +378,10 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { return false; } + if (sa.isKeyword(Keyword.FUSE) && !c.isInZone(ZoneType.Hand)) { + return false; + } + if (getCardsInHand() != -1) { int h = activator.getCardsIn(ZoneType.Hand).size(); if (getCardsInHand2() != -1) { @@ -596,7 +601,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables { return false; } - if (!sa.hasSVar("IsCastFromPlayEffect")) { + if (!sa.isCastFromPlayEffect()) { if (!checkTimingRestrictions(c, sa)) { return false; } diff --git a/forge-gui/res/cardsfolder/i/inventive_iteration_living_breakthrough.txt b/forge-gui/res/cardsfolder/i/inventive_iteration_living_breakthrough.txt index bc58140b632..82ab24f8358 100644 --- a/forge-gui/res/cardsfolder/i/inventive_iteration_living_breakthrough.txt +++ b/forge-gui/res/cardsfolder/i/inventive_iteration_living_breakthrough.txt @@ -3,7 +3,7 @@ ManaCost:3 U Types:Enchantment Saga K:Chapter:3:DBReturn1,DBReturn2,DBTransform SVar:DBReturn1:DB$ ChangeZone | ValidTgts$ Creature,Planeswalker | TargetMin$ 0 | TargetMax$ 1 | Origin$ Battlefield | Destination$ Hand | TgtPrompt$ Select target creature or planeswalker | SpellDescription$ Return up to one target creature or planeswalker to its owner's hand. -SVar:DBReturn2:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | ChangeType$ Artifact | ChangeNum$ 1 | Mandatory$ True | RememberChanged$ True | SubAbility$ DBDraw | SpellDescription$ Return an artifact card from your graveyard to your hand. If you can't, draw a card. +SVar:DBReturn2:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | ChangeType$ Artifact.YouOwn | Hidden$ True | ChangeNum$ 1 | Mandatory$ True | RememberChanged$ True | SubAbility$ DBDraw | SpellDescription$ Return an artifact card from your graveyard to your hand. If you can't, draw a card. SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ 1 | ConditionDefined$ Remembered | ConditionPresent$ Card | ConditionCompare$ EQ0 | SubAbility$ DBCleanup SVar:DBTransform:DB$ ChangeZone | Origin$ Battlefield | Destination$ Exile | RememberChanged$ True | SubAbility$ DBReturn | SpellDescription$ Exile this Saga, then return it to the battlefield transformed under your control. SVar:DBReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Battlefield | Transformed$ True | GainControl$ True | SubAbility$ DBCleanup diff --git a/forge-gui/res/cardsfolder/k/karn_the_great_creator.txt b/forge-gui/res/cardsfolder/k/karn_the_great_creator.txt index 723825dbcc4..e3608155a22 100644 --- a/forge-gui/res/cardsfolder/k/karn_the_great_creator.txt +++ b/forge-gui/res/cardsfolder/k/karn_the_great_creator.txt @@ -2,7 +2,7 @@ Name:Karn, the Great Creator ManaCost:4 Types:Legendary Planeswalker Karn Loyalty:5 -S:Mode$ CantBeActivated | Activator$ Opponent | AffectedZone$ Battlefield | ValidCard$ Artifact | ValidSA$ Activated | Description$ Activated abilities of artifacts your opponents control can't be activated. +S:Mode$ CantBeActivated | AffectedZone$ Battlefield | ValidCard$ Artifact.OppCtrl | ValidSA$ Activated | Description$ Activated abilities of artifacts your opponents control can't be activated. SVar:NonStackingEffect:True A:AB$ Animate | Cost$ AddCounter<1/LOYALTY> | TargetMin$ 0 | TargetMax$ 1 | Planeswalker$ True | ValidTgts$ Artifact.nonCreature | TgtPrompt$ Select target noncreature artifact | Power$ X | Toughness$ X | Types$ Artifact,Creature | Duration$ UntilYourNextTurn | AILogic$ PTByCMC | SpellDescription$ Until your next turn, up to one target noncreature artifact becomes an artifact creature with power and toughness each equal to its mana value. SVar:X:Targeted$CardManaCost diff --git a/forge-gui/res/cardsfolder/l/linvala_keeper_of_silence.txt b/forge-gui/res/cardsfolder/l/linvala_keeper_of_silence.txt index 3a294c9c005..7cbdcc0355d 100644 --- a/forge-gui/res/cardsfolder/l/linvala_keeper_of_silence.txt +++ b/forge-gui/res/cardsfolder/l/linvala_keeper_of_silence.txt @@ -3,6 +3,6 @@ ManaCost:2 W W Types:Legendary Creature Angel PT:3/4 K:Flying -S:Mode$ CantBeActivated | Activator$ Opponent | AffectedZone$ Battlefield | ValidCard$ Creature | ValidSA$ Activated | Description$ Activated abilities of creatures your opponents control can't be activated. +S:Mode$ CantBeActivated | AffectedZone$ Battlefield | ValidCard$ Creature.OppCtrl | ValidSA$ Activated | Description$ Activated abilities of creatures your opponents control can't be activated. SVar:PlayMain1:TRUE Oracle:Flying\nActivated abilities of creatures your opponents control can't be activated. diff --git a/forge-gui/res/cardsfolder/p/press_the_enemy.txt b/forge-gui/res/cardsfolder/p/press_the_enemy.txt index b5dfa4ab248..8ef503a2bfe 100644 --- a/forge-gui/res/cardsfolder/p/press_the_enemy.txt +++ b/forge-gui/res/cardsfolder/p/press_the_enemy.txt @@ -2,7 +2,7 @@ Name:Press the Enemy ManaCost:2 U U Types:Instant A:SP$ ChangeZone | ValidTgts$ Permanent.nonLand+OppCtrl,Card.inZoneStack+OppCtrl | TgtZone$ Stack,Battlefield | Origin$ Battlefield,Stack | Fizzle$ True | Destination$ Hand | SubAbility$ DBMayPlay | SpellDescription$ Return target spell or nonland permanent an opponent controls to its owner's hand. -SVar:DBMayPlay:DB$ Play | Valid$ Card.YouOwn | ValidZone$ Hand| ValidSA$ Instant.cmcLEZ,Sorcery.cmcLEZ | WithoutManaCost$ True | Optional$ True | SubAbility$ DBCleanup | SpellDescription$ You may cast an instant or sorcery spell with equal or lesser mana value from your hand without paying its mana cost. +SVar:DBMayPlay:DB$ Play | Valid$ Card.YouOwn | ValidZone$ Hand | ValidSA$ Instant.cmcLEZ,Sorcery.cmcLEZ | WithoutManaCost$ True | Optional$ True | SubAbility$ DBCleanup | SpellDescription$ You may cast an instant or sorcery spell with equal or lesser mana value from your hand without paying its mana cost. SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True DeckHints:Type$Sorcery|Instant SVar:X:SpellTargeted$CardManaCostLKI diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java index c68784b7da9..60570a15aaa 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java @@ -116,7 +116,7 @@ public abstract class InputPayMana extends InputSyncronizedBase { List result = Lists.newArrayList(); for (SpellAbility sa : card.getManaAbilities()) { result.add(sa); - result.addAll(GameActionUtil.getAlternativeCosts(sa, player)); + result.addAll(GameActionUtil.getAlternativeCosts(sa, player, false)); } final Collection toRemove = Lists.newArrayListWithCapacity(result.size()); for (final SpellAbility sa : result) { diff --git a/forge-gui/src/main/java/forge/player/HumanCostDecision.java b/forge-gui/src/main/java/forge/player/HumanCostDecision.java index bd47e11c89e..790e48d036a 100644 --- a/forge-gui/src/main/java/forge/player/HumanCostDecision.java +++ b/forge-gui/src/main/java/forge/player/HumanCostDecision.java @@ -851,7 +851,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { return PaymentDecision.number(0); } // player might not want to pay if from a trigger - if (!ability.hasSVar("IsCastFromPlayEffect") && hand.size() == num) { + if (!ability.isCastFromPlayEffect() && hand.size() == num) { return PaymentDecision.card(hand); } diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 6beb3086538..673a45da725 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -2910,7 +2910,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont // Human player is choosing targets for an ability // controlled by chosen player. sa.setActivatingPlayer(p); - sa.setSVar("IsCastFromPlayEffect", "True"); + sa.setCastFromPlayEffect(true); HumanPlay.playSaWithoutPayingManaCost(PlayerControllerHuman.this, getGame(), sa, true); } // playSa could fire some triggers