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 d3a30d3a3398..74299385b5af 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -18,6 +18,7 @@ import forge.game.cost.CostAdjustment; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; +import forge.game.keyword.KeywordWithCost; import forge.game.keyword.KeywordWithCostAndType; import forge.game.mana.Mana; import forge.game.mana.ManaConversionMatrix; @@ -3151,6 +3152,65 @@ public static SpellAbility addSpliceEffects(final SpellAbility sa) { return newSA; } + /** + * Adds the effects of one reprised card from the caster's graveyard to an + * instant or sorcery spell. Reprise is deliberately limited to one card, + * unlike splice. + */ + public static SpellAbility addRepriseEffect(final SpellAbility sa) { + final Card source = sa.getHostCard(); + final Player player = sa.getActivatingPlayer(); + + if (!sa.isSpell() || source.isCopiedSpell() + || (!source.isInstant() && !source.isSorcery())) { + return sa; + } + + final CardCollection reprises = CardLists.filter(player.getCardsIn(ZoneType.Graveyard), + card -> card.hasKeyword(Keyword.REPRISE)); + if (reprises.isEmpty()) { + return sa; + } + + final List chosen = player.getController().chooseCardForReprise(sa, reprises); + if (chosen.isEmpty()) { + return sa; + } + + final Card card = chosen.get(0); + final SpellAbility newSA = sa.copy(); + addRepriseEffect(newSA, card); + return newSA; + } + + public static void addRepriseEffect(final SpellAbility sa, final Card card) { + Cost repriseCost = null; + for (final KeywordInterface inst : card.getKeywords(Keyword.REPRISE)) { + if (inst instanceof KeywordWithCost reprise) { + repriseCost = reprise.getCost(); + break; + } + } + if (repriseCost == null) { + return; + } + + final SpellAbility firstSpell = card.getFirstSpellAbility(); + final Map params = Maps.newHashMap(firstSpell.getMapParams()); + final ApiType api = AbilityRecordType.getRecordType(params).getApiTypeOf(params); + final AbilitySub subAbility = (AbilitySub) AbilityFactory.getAbility(AbilityRecordType.SubAbility, api, params, + null, card.getCurrentState(), card.getCurrentState()); + subAbility.setActivatingPlayer(sa.getActivatingPlayer()); + subAbility.setHostCard(sa.getHostCard()); + sa.appendSubAbility(subAbility); + + sa.setBasicSpell(false); + sa.getPayCosts().add(new Cost("ExileFromGrave<1/Card.CardUID_" + card.getId() + ">", false)); + sa.getPayCosts().add(repriseCost); + sa.setDescription(sa.getDescription() + " (Reprising " + card + ")"); + sa.addReprisedCard(card); + } + public static void addSpliceEffect(final SpellAbility sa, final Card c) { Cost spliceCost = null; // This Function thinks that Splice exist only once on the card 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 8233e4971282..ca607ab4b282 100644 --- a/forge-game/src/main/java/forge/game/card/CardUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardUtil.java @@ -55,7 +55,7 @@ private CardUtil() { } public static final ImmutableList modifiableKeywords = ImmutableList.builder().add( "Enchant", "Protection", "Cumulative upkeep", "Equip", "Buyback", "Cycling", "Echo", "Kicker", "Flashback", "Madness", "Morph", - "Affinity", "Entwine", "Splice", "Ninjutsu", + "Affinity", "Entwine", "Splice", "Reprise", "Ninjutsu", "Transmute", "Replicate", "Recover", "Squad", "Suspend", "Aura swap", "Fortify", "Transfigure", "Champion", "Evoke", "Prowl", "Freerunning", "Reinforce", "Unearth", "Level up", "Miracle", "Overload", "Cleave", 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 69bf670eb521..97788a01cfb3 100644 --- a/forge-game/src/main/java/forge/game/keyword/Keyword.java +++ b/forge-game/src/main/java/forge/game/keyword/Keyword.java @@ -160,6 +160,7 @@ public enum Keyword { REINFORCE("Reinforce", KeywordWithCostAndAmount.class, false, "%s, Discard this card: Put {%d:+1/+1 counter} on target creature."), RENOWN("Renown", KeywordWithAmount.class, false, "When this creature deals combat damage to a player, if it isn't renowned, put {%d:+1/+1 counter} on it and it becomes renowned."), REPLICATE("Replicate", KeywordWithCost.class, false, "As an additional cost to cast this spell, you may pay %s any number of times. If you do, copy it that many times. You may choose new targets for the copies."), + REPRISE("Reprise", KeywordWithCost.class, false, "As you cast an instant or sorcery spell, you may exile one card with reprise from your graveyard and pay its reprise cost. If you do, add its effects to that spell."), RETRACE("Retrace", SimpleKeyword.class, false, "You may cast this card from your graveyard by discarding a land card in addition to paying its other costs."), RIOT("Riot", SimpleKeyword.class, false, "This creature enters with your choice of a +1/+1 counter or haste."), RIPPLE("Ripple", KeywordWithAmount.class, false, "When you cast this spell, you may reveal the top {%d:card} of your library. You may cast any of those cards with the same name as this spell without paying their mana costs. Put the rest on the bottom of your library in any order."), diff --git a/forge-game/src/main/java/forge/game/player/PlaySpellAbility.java b/forge-game/src/main/java/forge/game/player/PlaySpellAbility.java index f43938410eae..71c3998d63de 100644 --- a/forge-game/src/main/java/forge/game/player/PlaySpellAbility.java +++ b/forge-game/src/main/java/forge/game/player/PlaySpellAbility.java @@ -610,6 +610,7 @@ public final boolean playAbility(final boolean mayChooseTargets, final boolean i } ability = AbilityUtils.addSpliceEffects(ability); + ability = AbilityUtils.addRepriseEffect(ability); } // used to rollback 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 192ef8737ecd..bd4ce3268a7f 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -238,6 +238,10 @@ public CardCollectionView chooseCardsToDiscardFrom(Player playerDiscard, SpellAb public abstract CardCollectionView chooseCardsToDelve(int genericAmount, CardCollection grave); public abstract Map chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean artifacts, boolean creatures, Integer maxReduction); public abstract List chooseCardsForSplice(SpellAbility sa, List cards); + public List chooseCardForReprise(SpellAbility sa, List cards) { + final List chosen = chooseCardsForSplice(sa, cards); + return chosen.isEmpty() ? chosen : List.of(chosen.get(0)); + } public abstract CardCollectionView chooseCardsToRevealFromHand(int min, int max, CardCollectionView valid); public abstract List chooseSaToActivateFromOpeningHand(List usableFromOpeningHand); 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 bb394675633c..5ccc40c3b9e4 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -104,6 +104,7 @@ public static class EmptySa extends SpellAbility { private StaticAbility grantorStatic; private CardCollection splicedCards = null; + private CardCollection reprisedCards = null; private boolean basicSpell = true; private Trigger triggerObj; @@ -1638,6 +1639,21 @@ public void addSplicedCards(Card splicedCard) { splicedCards.add(splicedCard); } + public CardCollection getReprisedCards() { + return reprisedCards; + } + + public boolean isReprising() { + return reprisedCards != null && !reprisedCards.isEmpty(); + } + + public void addReprisedCard(Card reprisedCard) { + if (reprisedCards == null) { + reprisedCards = new CardCollection(); + } + reprisedCards.add(reprisedCard); + } + public CardCollection knownDetermineDefined(final String defined) { final CardCollection ret = new CardCollection(); final CardCollection list = AbilityUtils.getDefinedCards(getHostCard(), defined, this); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityProperty.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityProperty.java index 189f2ad7ea0e..670f543ebb8d 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityProperty.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityProperty.java @@ -65,6 +65,8 @@ public static boolean hasProperty(SpellAbility sa, Player sourceController, Card return sa.isEternalize(); } else if (property.equals("Flashback")) { return sa.isFlashback(); + } else if (property.equals("Reprise") || property.equals("Reprising")) { + return sa.isReprising(); } else if (property.equals("Harmonize")) { return sa.isHarmonize(); } else if (property.equals("Jumpstart")) { diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 6a15dad2fe73..c5cd1fe128d6 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -3763,6 +3763,23 @@ public List chooseCardsForSplice(SpellAbility sa, List cards) { return chosenCards; } + @Override + public List chooseCardForReprise(SpellAbility sa, List cards) { + GameEntityViewMap gameCacheReprise = GameEntityView.getMap(cards); + List chosen = getGui().many( + "Choose a card to reprise", + "Chosen card", + 0, + 1, + gameCacheReprise.getTrackableKeys(), + sa.getHostCard().getView() + ); + + List chosenCards = new CardCollection(); + gameCacheReprise.addToList(chosen, chosenCards); + return chosenCards; + } + /* * (non-Javadoc) *