Cycling: add YouCycledThisTurn, OnlyFirst for Trigger and ValidCause for ReplaceDraw

This commit is contained in:
Hans Mackowiak
2020-04-19 03:24:10 +00:00
committed by Michael Kamensky
parent 6b38bc4b7f
commit ef95f02fa2
13 changed files with 145 additions and 32 deletions

View File

@@ -1461,12 +1461,18 @@ public class GameAction {
revealTo(card, Collections.singleton(to)); revealTo(card, Collections.singleton(to));
} }
public void revealTo(final CardCollectionView cards, final Player 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<Player> to) { public void revealTo(final Card card, final Iterable<Player> to) {
revealTo(new CardCollection(card), to); revealTo(new CardCollection(card), to);
} }
public void revealTo(final CardCollectionView cards, final Iterable<Player> to) { public void revealTo(final CardCollectionView cards, final Iterable<Player> to) {
revealTo(cards, to, null);
}
public void revealTo(final CardCollectionView cards, final Iterable<Player> to, String messagePrefix) {
if (cards.isEmpty()) { if (cards.isEmpty()) {
return; return;
} }
@@ -1474,7 +1480,7 @@ public class GameAction {
final ZoneType zone = cards.getFirst().getZone().getZoneType(); final ZoneType zone = cards.getFirst().getZone().getZoneType();
final Player owner = cards.getFirst().getOwner(); final Player owner = cards.getFirst().getOwner();
for (final Player p : to) { for (final Player p : to) {
p.getController().reveal(cards, zone, owner); p.getController().reveal(cards, zone, owner, messagePrefix);
} }
} }

View File

@@ -58,7 +58,7 @@ public class DrawEffect extends SpellAbilityEffect {
actualNum = p.getController().chooseNumber(sa, "lblHowMayCardDoYouWantDraw", 0, numCards); 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")) { if (sa.hasParam("Reveal")) {
p.getGame().getAction().reveal(drawn, p); p.getGame().getAction().reveal(drawn, p);
} }

View File

@@ -909,6 +909,9 @@ public class CardFactoryUtil {
return doXMath(c.getXManaCostPaidCount(colors.toString()), m, c); return doXMath(c.getXManaCostPaidCount(colors.toString()), m, c);
} }
if (sq[0].equals("YouCycledThisTurn")) {
return doXMath(cc.getCycledThisTurn(), m, c);
}
if (sq[0].equals("YouDrewThisTurn")) { if (sq[0].equals("YouDrewThisTurn")) {
return doXMath(cc.getNumDrawnThisTurn(), m, c); return doXMath(cc.getNumDrawnThisTurn(), m, c);

View File

@@ -877,6 +877,16 @@ public class CardProperty {
} }
} }
return false; 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")) { } else if (property.equals("sharesPermanentTypeWith")) {

View File

@@ -96,7 +96,7 @@ public class CostDraw extends CostPart {
@Override @Override
public final boolean payAsDecided(final Player ai, final PaymentDecision decision, SpellAbility ability) { public final boolean payAsDecided(final Player ai, final PaymentDecision decision, SpellAbility ability) {
for (final Player p : getPotentialPlayers(ai, ability.getHostCard())) { for (final Player p : getPotentialPlayers(ai, ability.getHostCard())) {
p.drawCards(decision.c); p.drawCards(decision.c, ability);
} }
return true; return true;
} }

View File

@@ -93,6 +93,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private int landsPlayedLastTurn = 0; private int landsPlayedLastTurn = 0;
private int investigatedThisTurn = 0; private int investigatedThisTurn = 0;
private int surveilThisTurn = 0; private int surveilThisTurn = 0;
private int cycledThisTurn = 0;
private int lifeLostThisTurn = 0; private int lifeLostThisTurn = 0;
private int lifeLostLastTurn = 0; private int lifeLostLastTurn = 0;
private int lifeGainedThisTurn = 0; private int lifeGainedThisTurn = 0;
@@ -1269,7 +1270,7 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
public final CardCollectionView drawCard() { public final CardCollectionView drawCard() {
return drawCards(1); return drawCards(1, null);
} }
public void surveil(int num, SpellAbility cause) { public void surveil(int num, SpellAbility cause) {
@@ -1339,8 +1340,11 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
public final CardCollectionView drawCards(final int n) { 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 drawn = new CardCollection();
final CardCollection toReveal = new CardCollection(); final Map<Player, CardCollection> toReveal = Maps.newHashMap();
// Replacement effects // Replacement effects
final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(this); final Map<AbilityKey, Object> repRunParams = AbilityKey.mapFromAffected(this);
@@ -1357,11 +1361,14 @@ public class Player extends GameEntity implements Comparable<Player> {
if (gameStarted && !canDraw()) { if (gameStarted && !canDraw()) {
return drawn; 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 // reveal multiple drawn cards when playing with the top of the library revealed
game.getAction().reveal(toReveal, this, true, "Revealing cards drawn from "); for (Map.Entry<Player, CardCollection> e : toReveal.entrySet()) {
if (e.getValue().size() > 1) {
game.getAction().revealTo(e.getValue(), e.getKey(), "Revealing cards drawn from ");
}
} }
return drawn; return drawn;
} }
@@ -1369,31 +1376,36 @@ public class Player extends GameEntity implements Comparable<Player> {
/** /**
* @return a CardCollectionView of cards actually drawn * @return a CardCollectionView of cards actually drawn
*/ */
private CardCollectionView doDraw(CardCollection revealed) { private CardCollectionView doDraw(Map<Player, CardCollection> revealed, SpellAbility cause) {
final CardCollection drawn = new CardCollection(); final CardCollection drawn = new CardCollection();
final PlayerZone library = getZone(ZoneType.Library); final PlayerZone library = getZone(ZoneType.Library);
// Replacement effects // Replacement effects
if (game.getReplacementHandler().run(ReplacementType.Draw, AbilityKey.mapFromAffected(this)) != ReplacementResult.NotReplaced) { Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(this);
repParams.put(AbilityKey.Cause, cause);
if (game.getReplacementHandler().run(ReplacementType.Draw, repParams) != ReplacementResult.NotReplaced) {
return drawn; return drawn;
} }
if (!library.isEmpty()) { if (!library.isEmpty()) {
Card c = library.get(0); Card c = library.get(0);
boolean topCardRevealed = false;
for (Player p : this.getAllOtherPlayers()) { List<Player> pList = Lists.newArrayList();
for (Player p : getAllOtherPlayers()) {
if (c.mayPlayerLook(p)) { if (c.mayPlayerLook(p)) {
topCardRevealed = true; pList.add(p);
break;
} }
} }
c = game.getAction().moveToHand(c, null); c = game.getAction().moveToHand(c, cause);
drawn.add(c); drawn.add(c);
if (topCardRevealed) { for(Player p : pList) {
revealed.add(c); if (!revealed.containsKey(p)) {
revealed.put(p, new CardCollection());
}
revealed.get(p).add(c);
} }
setLastDrawnCard(c); setLastDrawnCard(c);
@@ -2454,6 +2466,7 @@ public class Player extends GameEntity implements Comparable<Player> {
resetLandsPlayedThisTurn(); resetLandsPlayedThisTurn();
resetInvestigatedThisTurn(); resetInvestigatedThisTurn();
resetSurveilThisTurn(); resetSurveilThisTurn();
resetCycledThisTurn();
resetSacrificedThisTurn(); resetSacrificedThisTurn();
resetCounterToPermThisTurn(); resetCounterToPermThisTurn();
clearAssignedDamage(); clearAssignedDamage();
@@ -3157,4 +3170,22 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
return controlVotes.last(); return controlVotes.last();
} }
public void addCycled(SpellAbility sp) {
cycledThisTurn++;
Map<AbilityKey, Object> 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;
}
} }

View File

@@ -6,17 +6,18 @@
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package forge.game.replacement; package forge.game.replacement;
import forge.game.Game;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -25,7 +26,7 @@ import forge.game.spellability.SpellAbility;
import java.util.Map; import java.util.Map;
/** /**
* TODO: Write javadoc for this type. * TODO: Write javadoc for this type.
* *
*/ */
@@ -46,16 +47,29 @@ public class ReplaceDraw extends ReplacementEffect {
*/ */
@Override @Override
public boolean canReplace(Map<AbilityKey, Object> runParams) { public boolean canReplace(Map<AbilityKey, Object> runParams) {
final Game game = this.getHostCard().getGame();
if (hasParam("ValidPlayer")) { 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; 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")) { if (hasParam("NotFirstCardInDrawStep")) {
final Player p = (Player)runParams.get(AbilityKey.Affected); final Player p = (Player)runParams.get(AbilityKey.Affected);
if (p.numDrawnThisDrawStep() == 0 if (p.numDrawnThisDrawStep() == 0 && game.getPhaseHandler().is(PhaseType.DRAW, p)) {
&& this.getHostCard().getGame().getPhaseHandler().is(PhaseType.DRAW)
&& this.getHostCard().getGame().getPhaseHandler().isPlayerTurn(p)) {
return false; return false;
} }
} }
@@ -71,5 +85,12 @@ public class ReplaceDraw extends ReplacementEffect {
@Override @Override
public void setReplacingObjects(Map<AbilityKey, Object> runParams, SpellAbility sa) { public void setReplacingObjects(Map<AbilityKey, Object> runParams, SpellAbility sa) {
sa.setReplacingObject(AbilityKey.Player, runParams.get(AbilityKey.Affected)); 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());
}
}
} }
} }

View File

@@ -19,6 +19,7 @@ package forge.game.trigger;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.util.Localizer; import forge.util.Localizer;
@@ -68,8 +69,22 @@ public class TriggerCycled extends Trigger {
@Override @Override
public final boolean performTest(final Map<AbilityKey, Object> runParams) { public final boolean performTest(final Map<AbilityKey, Object> runParams) {
if (hasParam("ValidCard")) { if (hasParam("ValidCard")) {
return matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","), if (!matchesValid(runParams.get(AbilityKey.Card), getParam("ValidCard").split(","), getHostCard())) {
this.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; return true;
} }

View File

@@ -314,9 +314,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// Run Cycled triggers // Run Cycled triggers
if (sp.isCycling()) { if (sp.isCycling()) {
Map<AbilityKey, Object> cycleParams = AbilityKey.mapFromCard(sp.getHostCard()); activator.addCycled(sp);
cycleParams.put(AbilityKey.Cause, sp);
game.getTriggerHandler().runTrigger(TriggerType.Cycled, cycleParams, false);
} }
if (sp.hasParam("Crew")) { if (sp.hasParam("Crew")) {

View File

@@ -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.

View File

@@ -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.)

View File

@@ -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.)

View File

@@ -345,7 +345,7 @@ public class HumanPlay {
} }
for (Player player : res) { for (Player player : res) {
player.drawCards(amount); player.drawCards(amount, sourceAbility);
} }
} }
else if (part instanceof CostGainLife) { else if (part instanceof CostGainLife) {