diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index c4e1a378be8..0d89701881b 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -1461,12 +1461,18 @@ public class GameAction { revealTo(card, Collections.singleton(to)); } public void revealTo(final CardCollectionView cards, final Player to) { - revealTo(cards, Collections.singleton(to)); + revealTo(cards, to, null); + } + public void revealTo(final CardCollectionView cards, final Player to, String messagePrefix) { + revealTo(cards, Collections.singleton(to), messagePrefix); } public void revealTo(final Card card, final Iterable to) { revealTo(new CardCollection(card), to); } public void revealTo(final CardCollectionView cards, final Iterable to) { + revealTo(cards, to, null); + } + public void revealTo(final CardCollectionView cards, final Iterable to, String messagePrefix) { if (cards.isEmpty()) { return; } @@ -1474,7 +1480,7 @@ public class GameAction { final ZoneType zone = cards.getFirst().getZone().getZoneType(); final Player owner = cards.getFirst().getOwner(); for (final Player p : to) { - p.getController().reveal(cards, zone, owner); + p.getController().reveal(cards, zone, owner, messagePrefix); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/DrawEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DrawEffect.java index c1912191dad..e68dbd5ca75 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DrawEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DrawEffect.java @@ -58,7 +58,7 @@ public class DrawEffect extends SpellAbilityEffect { actualNum = p.getController().chooseNumber(sa, "lblHowMayCardDoYouWantDraw", 0, numCards); } - final CardCollectionView drawn = p.drawCards(actualNum); + final CardCollectionView drawn = p.drawCards(actualNum, sa); if (sa.hasParam("Reveal")) { p.getGame().getAction().reveal(drawn, p); } 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 88307959cdb..5af7a98602f 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -909,6 +909,9 @@ public class CardFactoryUtil { return doXMath(c.getXManaCostPaidCount(colors.toString()), m, c); } + if (sq[0].equals("YouCycledThisTurn")) { + return doXMath(cc.getCycledThisTurn(), m, c); + } if (sq[0].equals("YouDrewThisTurn")) { return doXMath(cc.getNumDrawnThisTurn(), m, c); diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index c14d8610cff..3a102b0bb67 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -877,6 +877,16 @@ public class CardProperty { } } return false; + default: + final CardCollection cards1 = AbilityUtils.getDefinedCards(card, restriction, spellAbility); + if (cards1.isEmpty()) { + return false; + } + for (Card c : cards1) { + if (!card.sharesCardTypeWith(c)) { + return false; + } + } } } } else if (property.equals("sharesPermanentTypeWith")) { diff --git a/forge-game/src/main/java/forge/game/cost/CostDraw.java b/forge-game/src/main/java/forge/game/cost/CostDraw.java index b33630f7808..543215712c4 100644 --- a/forge-game/src/main/java/forge/game/cost/CostDraw.java +++ b/forge-game/src/main/java/forge/game/cost/CostDraw.java @@ -96,7 +96,7 @@ public class CostDraw extends CostPart { @Override public final boolean payAsDecided(final Player ai, final PaymentDecision decision, SpellAbility ability) { for (final Player p : getPotentialPlayers(ai, ability.getHostCard())) { - p.drawCards(decision.c); + p.drawCards(decision.c, ability); } return true; } diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 71e34fc7a8b..2c383929ba9 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -93,6 +93,7 @@ public class Player extends GameEntity implements Comparable { private int landsPlayedLastTurn = 0; private int investigatedThisTurn = 0; private int surveilThisTurn = 0; + private int cycledThisTurn = 0; private int lifeLostThisTurn = 0; private int lifeLostLastTurn = 0; private int lifeGainedThisTurn = 0; @@ -1269,7 +1270,7 @@ public class Player extends GameEntity implements Comparable { } public final CardCollectionView drawCard() { - return drawCards(1); + return drawCards(1, null); } public void surveil(int num, SpellAbility cause) { @@ -1339,8 +1340,11 @@ public class Player extends GameEntity implements Comparable { } public final CardCollectionView drawCards(final int n) { + return drawCards(n, null); + } + public final CardCollectionView drawCards(final int n, SpellAbility cause) { final CardCollection drawn = new CardCollection(); - final CardCollection toReveal = new CardCollection(); + final Map toReveal = Maps.newHashMap(); // Replacement effects final Map repRunParams = AbilityKey.mapFromAffected(this); @@ -1357,11 +1361,14 @@ public class Player extends GameEntity implements Comparable { if (gameStarted && !canDraw()) { return drawn; } - drawn.addAll(doDraw(toReveal)); + drawn.addAll(doDraw(toReveal, cause)); } - if (toReveal.size() > 1) { - // reveal multiple drawn cards when playing with the top of the library revealed - game.getAction().reveal(toReveal, this, true, "Revealing cards drawn from "); + + // reveal multiple drawn cards when playing with the top of the library revealed + for (Map.Entry e : toReveal.entrySet()) { + if (e.getValue().size() > 1) { + game.getAction().revealTo(e.getValue(), e.getKey(), "Revealing cards drawn from "); + } } return drawn; } @@ -1369,31 +1376,36 @@ public class Player extends GameEntity implements Comparable { /** * @return a CardCollectionView of cards actually drawn */ - private CardCollectionView doDraw(CardCollection revealed) { + private CardCollectionView doDraw(Map revealed, SpellAbility cause) { final CardCollection drawn = new CardCollection(); final PlayerZone library = getZone(ZoneType.Library); // Replacement effects - if (game.getReplacementHandler().run(ReplacementType.Draw, AbilityKey.mapFromAffected(this)) != ReplacementResult.NotReplaced) { + Map repParams = AbilityKey.mapFromAffected(this); + repParams.put(AbilityKey.Cause, cause); + if (game.getReplacementHandler().run(ReplacementType.Draw, repParams) != ReplacementResult.NotReplaced) { return drawn; } if (!library.isEmpty()) { Card c = library.get(0); - boolean topCardRevealed = false; - for (Player p : this.getAllOtherPlayers()) { + List pList = Lists.newArrayList(); + + for (Player p : getAllOtherPlayers()) { if (c.mayPlayerLook(p)) { - topCardRevealed = true; - break; + pList.add(p); } } - c = game.getAction().moveToHand(c, null); + c = game.getAction().moveToHand(c, cause); drawn.add(c); - if (topCardRevealed) { - revealed.add(c); + for(Player p : pList) { + if (!revealed.containsKey(p)) { + revealed.put(p, new CardCollection()); + } + revealed.get(p).add(c); } setLastDrawnCard(c); @@ -2454,6 +2466,7 @@ public class Player extends GameEntity implements Comparable { resetLandsPlayedThisTurn(); resetInvestigatedThisTurn(); resetSurveilThisTurn(); + resetCycledThisTurn(); resetSacrificedThisTurn(); resetCounterToPermThisTurn(); clearAssignedDamage(); @@ -3157,4 +3170,22 @@ public class Player extends GameEntity implements Comparable { } return controlVotes.last(); } + + public void addCycled(SpellAbility sp) { + cycledThisTurn++; + + Map cycleParams = AbilityKey.mapFromCard(sp.getHostCard()); + cycleParams.put(AbilityKey.Cause, sp); + cycleParams.put(AbilityKey.Player, this); + cycleParams.put(AbilityKey.NumThisTurn, cycledThisTurn); + game.getTriggerHandler().runTrigger(TriggerType.Cycled, cycleParams, false); + } + + public int getCycledThisTurn() { + return cycledThisTurn; + } + + public void resetCycledThisTurn() { + cycledThisTurn = 0; + } } diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceDraw.java b/forge-game/src/main/java/forge/game/replacement/ReplaceDraw.java index 9dbaf529cad..c930b352334 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplaceDraw.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceDraw.java @@ -6,17 +6,18 @@ * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package forge.game.replacement; +import forge.game.Game; import forge.game.ability.AbilityKey; import forge.game.card.Card; import forge.game.phase.PhaseType; @@ -25,7 +26,7 @@ import forge.game.spellability.SpellAbility; import java.util.Map; -/** +/** * TODO: Write javadoc for this type. * */ @@ -46,16 +47,29 @@ public class ReplaceDraw extends ReplacementEffect { */ @Override public boolean canReplace(Map runParams) { + final Game game = this.getHostCard().getGame(); if (hasParam("ValidPlayer")) { - if (!matchesValid(runParams.get(AbilityKey.Affected), getParam("ValidPlayer").split(","), this.getHostCard())) { + if (!matchesValid(runParams.get(AbilityKey.Affected), getParam("ValidPlayer").split(","), getHostCard())) { return false; } } + + if (hasParam("ValidCause")) { + if (!runParams.containsKey(AbilityKey.Cause)) { + return false; + } + SpellAbility cause = (SpellAbility) runParams.get(AbilityKey.Cause); + if (cause == null) { + return false; + } + if (!matchesValid(cause, getParam("ValidCause").split(","), getHostCard())) { + return false; + } + } + if (hasParam("NotFirstCardInDrawStep")) { final Player p = (Player)runParams.get(AbilityKey.Affected); - if (p.numDrawnThisDrawStep() == 0 - && this.getHostCard().getGame().getPhaseHandler().is(PhaseType.DRAW) - && this.getHostCard().getGame().getPhaseHandler().isPlayerTurn(p)) { + if (p.numDrawnThisDrawStep() == 0 && game.getPhaseHandler().is(PhaseType.DRAW, p)) { return false; } } @@ -71,5 +85,12 @@ public class ReplaceDraw extends ReplacementEffect { @Override public void setReplacingObjects(Map runParams, SpellAbility sa) { sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Affected)); + if (runParams.containsKey(AbilityKey.Cause)) { + SpellAbility cause = (SpellAbility) runParams.get(AbilityKey.Cause); + if (cause != null) { + sa.setReplacingObject(AbilityKey.Cause, cause); + sa.setReplacingObject(AbilityKey.Source, cause.getHostCard()); + } + } } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java b/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java index 88311eadac2..e72b72c6a60 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerCycled.java @@ -19,6 +19,7 @@ package forge.game.trigger; import forge.game.ability.AbilityKey; import forge.game.card.Card; +import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.util.Localizer; @@ -68,8 +69,22 @@ public class TriggerCycled extends Trigger { @Override public final boolean performTest(final Map runParams) { if (hasParam("ValidCard")) { - return matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","), - this.getHostCard()); + if (!matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","), getHostCard())) { + return false; + } + } + + if (hasParam("ValidPlayer")) { + Player p = (Player) runParams.get(AbilityKey.Player); + if (!matchesValid(p, getParam("ValidPlayer").split(","), getHostCard())) { + return false; + } + } + + if (hasParam("OnlyFirst")) { + if ((int) runParams.get(AbilityKey.NumThisTurn) != 1) { + return false; + } } return true; } 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 f3e90a992f8..2bc2c712451 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -314,9 +314,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable cycleParams = AbilityKey.mapFromCard(sp.getHostCard()); - cycleParams.put(AbilityKey.Cause, sp); - game.getTriggerHandler().runTrigger(TriggerType.Cycled, cycleParams, false); + activator.addCycled(sp); } if (sp.hasParam("Crew")) { diff --git a/forge-gui/res/cardsfolder/upcoming/C2020/spellpyre_phoenix.txt b/forge-gui/res/cardsfolder/upcoming/C2020/spellpyre_phoenix.txt new file mode 100644 index 00000000000..fc6d02f6bb3 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/C2020/spellpyre_phoenix.txt @@ -0,0 +1,11 @@ +Name:Spellpyre Phoenix +ManaCost:3 R R +Types:Creature Phoenix +PT:4/2 +K:Flying +T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigChangeZone | TriggerDescription$ When CARDNAME enters the battlefield, you may return target instant or sorcery card with a cycling ability from your graveyard to your hand. +SVar:TrigChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | ValidTgts$ Instant.YouOwn+withCycling,Instant.YouOwn+withTypeCycling,Sorcery.YouOwn+withCycling,Sorcery.YouOwn+withTypeCycling | TgtPrompt$ Select target instant or sorcery card with a cycling ability from your graveyard +T:Mode$ Phase | Phase$ End of Turn | TriggerZones$ Graveyard | CheckSVar$ YouCycled | SVarCompare$ GE2 | Execute$ TrigReturn | TriggerDescription$ At the beginning of each end step, if you cycled two or more cards this turn, return CARDNAME from your graveyard to your hand. +SVar:TrigReturn:DB$ ChangeZone | Defined$ Self | Origin$ Graveyard | Destination$ Hand +SVar:YouCycled:Count$YouCycledThisTurn +Oracle:Flying\nWhen Spellpyre Phoenix enters the battlefield, you may return target instant or sorcery card with a cycling ability from your graveyard to your hand.\nAt the beginning of each end step, if you cycled two or more cards this turn, return Spellpyre Phoenix from your graveyard to your hand. diff --git a/forge-gui/res/cardsfolder/upcoming/unpredictable_cyclone.txt b/forge-gui/res/cardsfolder/upcoming/unpredictable_cyclone.txt new file mode 100644 index 00000000000..171e855afbd --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/unpredictable_cyclone.txt @@ -0,0 +1,10 @@ +Name:Unpredictable Cyclone +ManaCost:3 R R +Types:Enchantment +K:Cycling:2 +R:Event$ Draw | ValidCause$ Activated.Cycling+nonLand | ValidPlayer$ You | ActiveZones$ Battlefield | ReplaceWith$ DBDig | Description$ If a cycling ability of another nonland card would cause you to draw a card, instead exile cards from the top of your library until you exile a card that shares a card type with the cycled card. You may cast that card without paying its mana cost. Then put the exiled cards that weren't cast this way on the bottom of your library in a random order. +SVar:DBDig:DB$ DigUntil | Defined$ You | Valid$ Card.sharesCardTypeWith ReplacedSource | ValidDescription$ shares a card type with exiled card | FoundDestination$ Exile | RevealedDestination$ Exile | RememberFound$ True | RememberRevealed$ True | SubAbility$ DBPlay +SVar:DBPlay:DB$ Play | Defined$ Remembered.nonLand+sharesCardTypeWith ReplacedSource | WithoutManaCost$ True | Optional$ True | ForgetTargetRemembered$ True | SubAbility$ DBRestRandomOrder +SVar:DBRestRandomOrder:DB$ ChangeZone | Defined$ Remembered | AtRandom$ True | Origin$ Library | Destination$ Library | LibraryPosition$ -1 | SubAbility$ DBCleanup +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True +Oracle:If a cycling ability of another nonland card would cause you to draw a card, instead exile cards from the top of your library until you exile a card that shares a card type with the cycled card. You may cast that card without paying its mana cost. Then put the exiled cards that weren't cast this way on the bottom of your library in a random order.\nCycling {2} ({2}, Discard this card: Draw a card.) diff --git a/forge-gui/res/cardsfolder/upcoming/valiant_rescuer.txt b/forge-gui/res/cardsfolder/upcoming/valiant_rescuer.txt new file mode 100644 index 00000000000..a95fd17db54 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/valiant_rescuer.txt @@ -0,0 +1,8 @@ +Name:Valiant Rescuer +ManaCost:1 W +Types:Creature Human Soldier +PT:3/1 +T:Mode$ Cycled | ValidCard$ Card.Other | ValidPlayer$ You | TriggerZones$ Battlefield | OnlyFirst$ True | Execute$ TrigToken | TriggerDescription$ Whenever you cycle another card for the first time each turn, create a 1/1 white Human Soldier creature token. +SVar:TrigToken:DB$ Token | TokenAmount$ 1 | TokenScript$ w_1_1_human_soldier | TokenOwner$ You | LegacyImage$ w 1 1 human soldier iko +K:Cycling:2 +Oracle:Whenever you cycle another card for the first time each turn, create a 1/1 white Human Soldier creature token.\nCycling {2} ({2}, Discard this card: Draw a card.) diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index b65361d9835..7d364f62b5e 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -345,7 +345,7 @@ public class HumanPlay { } for (Player player : res) { - player.drawCards(amount); + player.drawCards(amount, sourceAbility); } } else if (part instanceof CostGainLife) {