Merge branch 'triggerDiscardedAll' into 'master'

TriggerDiscardAll: new trigger for Rielle, the Everwise

See merge request core-developers/forge!2776
This commit is contained in:
Michael Kamensky
2020-04-30 09:50:55 +00:00
10 changed files with 199 additions and 69 deletions

View File

@@ -2,6 +2,7 @@ package forge.game.ability.effects;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.*;
@@ -10,6 +11,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Lang;
@@ -22,8 +24,8 @@ import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class DiscardEffect extends SpellAbilityEffect {
@@ -109,7 +111,7 @@ public class DiscardEffect extends SpellAbilityEffect {
final Game game = source.getGame();
//final boolean anyNumber = sa.hasParam("AnyNumber");
final List<Card> discarded = new ArrayList<>();
final List<Card> discarded = Lists.newArrayList();
final List<Player> targets = getTargetPlayers(sa),
discarders;
Player firstTarget = null;
@@ -127,6 +129,8 @@ public class DiscardEffect extends SpellAbilityEffect {
final CardZoneTable table = new CardZoneTable();
for (final Player p : discarders) {
boolean firstDiscard = p.getNumDiscardedThisTurn() == 0;
final CardCollection discardedByPlayer = new CardCollection();
if ((mode.equals("RevealTgtChoose") && firstTarget != null) || !sa.usesTargeting() || p.canBeTargetedBy(sa)) {
if (sa.hasParam("RememberDiscarder") && p.canDiscardBy(sa)) {
source.addRemembered(p);
@@ -149,35 +153,28 @@ public class DiscardEffect extends SpellAbilityEffect {
for (final Card c : toDiscard) {
if (p.discard(c, sa, table) != null) {
discarded.add(c);
}
}
if (sa.hasParam("RememberDiscarded")) {
for (final Card c : discarded) {
source.addRemembered(c);
discardedByPlayer.add(c);
}
}
}
continue;
}
if (mode.equals("Hand")) {
if (!p.canDiscardBy(sa)) {
continue;
}
boolean shouldRemember = sa.hasParam("RememberDiscarded");
CardCollectionView toDiscard = new CardCollection(Lists.newArrayList(p.getCardsIn(ZoneType.Hand)));
CardCollectionView toDiscard = p.getCardsIn(ZoneType.Hand);
if (toDiscard.size() > 1) {
toDiscard = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard);
}
for(Card c : toDiscard) { // without copying will get concurrent modification exception
boolean hasDiscarded = p.discard(c, sa, table) != null;
if( hasDiscarded && shouldRemember )
source.addRemembered(c);
for(Card c : Lists.newArrayList(toDiscard)) { // without copying will get concurrent modification exception
if (p.discard(c, sa, table) != null) {
discarded.add(c);
discardedByPlayer.add(c);
}
}
continue;
}
if (mode.equals("NotRemembered")) {
@@ -192,6 +189,7 @@ public class DiscardEffect extends SpellAbilityEffect {
for (final Card c : dPHand) {
if (p.discard(c, sa, table) != null) {
discarded.add(c);
discardedByPlayer.add(c);
}
}
}
@@ -231,6 +229,7 @@ public class DiscardEffect extends SpellAbilityEffect {
for (Card c : toDiscardView) {
if (p.discard(c, sa, table) != null) {
discarded.add(c);
discardedByPlayer.add(c);
}
}
}
@@ -249,7 +248,10 @@ public class DiscardEffect extends SpellAbilityEffect {
}
for (Card c : toDiscard) {
c.getController().discard(c, sa, table);
if (c.getController().discard(c, sa, table) != null) {
discarded.add(c);
discardedByPlayer.add(c);
}
}
}
}
@@ -282,6 +284,7 @@ public class DiscardEffect extends SpellAbilityEffect {
for (final Card c : dPChHand) {
if (p.discard(c, sa, table) != null) {
discarded.add(c);
discardedByPlayer.add(c);
}
}
} else if (mode.equals("RevealYouChoose") || mode.equals("RevealTgtChoose") || mode.equals("TgtChoose")) {
@@ -332,11 +335,21 @@ public class DiscardEffect extends SpellAbilityEffect {
if (card == null) { continue; }
if (p.discard(card, sa, table) != null) {
discarded.add(card);
discardedByPlayer.add(card);
}
}
}
}
}
if (!discardedByPlayer.isEmpty()) {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, p);
runParams.put(AbilityKey.Cards, discardedByPlayer);
runParams.put(AbilityKey.Cause, sa);
runParams.put(AbilityKey.FirstTime, firstDiscard);
game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false);
}
}
if (sa.hasParam("RememberDiscarded")) {

View File

@@ -42,7 +42,7 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
old.add(value);
} else {
old = new CardCollection(value);
dataMap.put(rowKey, columnKey, old);
delegate().put(rowKey, columnKey, old);
}
return old;
}

View File

@@ -17,6 +17,9 @@
*/
package forge.game.cost;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
@@ -24,6 +27,7 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;
@@ -35,6 +39,8 @@ public class CostDiscard extends CostPartWithList {
// Inputs
protected boolean firstTime = false;
/**
* Serializables need a version ID.
*/
@@ -181,4 +187,23 @@ public class CostDiscard extends CostPartWithList {
public <T> T accept(ICostVisitor<T> visitor) {
return visitor.visit(this);
}
protected void handleBeforePayment(Player ai, SpellAbility ability, CardCollectionView targetCards) {
firstTime = ai.getNumDiscardedThisTurn() == 0;
}
@Override
protected void handleChangeZoneTrigger(Player payer, SpellAbility ability, CardCollectionView targetCards) {
super.handleChangeZoneTrigger(payer, ability, targetCards);
if (!targetCards.isEmpty())
{
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, payer);
runParams.put(AbilityKey.Cards, new CardCollection(targetCards));
runParams.put(AbilityKey.Cause, ability);
runParams.put(AbilityKey.FirstTime, firstTime);
payer.getGame().getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false);
}
}
}

View File

@@ -68,6 +68,9 @@ public abstract class CostPartWithList extends CostPart {
* the hash
*/
public final void reportPaidCardsTo(final SpellAbility sa) {
if (sa == null) {
return;
}
final String lkiPaymentMethod = getHashForLKIList();
for (final Card card : lkiList) {
sa.addCostToHashList(card, lkiPaymentMethod);
@@ -121,19 +124,19 @@ public abstract class CostPartWithList extends CostPart {
}
// always returns true, made this to inline with return
public boolean executePayment(SpellAbility ability, CardCollectionView targetCards) {
protected boolean executePayment(Player payer, SpellAbility ability, CardCollectionView targetCards) {
handleBeforePayment(payer, ability, targetCards);
if (canPayListAtOnce()) { // This is used by reveal. Without it when opponent would reveal hand, you'll get N message boxes.
for (Card c: targetCards) {
lkiList.add(CardUtil.getLKICopy(c));
}
cardList.addAll(doListPayment(ability, targetCards));
handleChangeZoneTrigger(ability);
return true;
} else {
for (Card c: targetCards) {
executePayment(ability, c);
}
}
for (Card c: targetCards) {
executePayment(ability, c);
}
handleChangeZoneTrigger(ability);
handleChangeZoneTrigger(payer, ability, targetCards);
return true;
}
@@ -157,12 +160,16 @@ public abstract class CostPartWithList extends CostPart {
@Override
public boolean payAsDecided(Player ai, PaymentDecision decision, SpellAbility ability) {
executePayment(ability, decision.cards);
executePayment(ai, ability, decision.cards);
reportPaidCardsTo(ability);
return true;
}
protected void handleChangeZoneTrigger(SpellAbility ability) {
protected void handleBeforePayment(Player ai, SpellAbility ability, CardCollectionView targetCards) {
}
protected void handleChangeZoneTrigger(Player payer, SpellAbility ability, CardCollectionView targetCards) {
if (table.isEmpty()) {
return;
}
@@ -170,7 +177,7 @@ public abstract class CostPartWithList extends CostPart {
// copy table because the original get cleaned after the cost is done
final CardZoneTable copyTable = new CardZoneTable();
copyTable.putAll(table);
copyTable.triggerChangesZoneAll(ability.getHostCard().getGame());
copyTable.triggerChangesZoneAll(payer.getGame());
}
}

View File

@@ -164,7 +164,7 @@ public class CostPutCounter extends CostPartWithList {
if (this.payCostFromSource()) {
executePayment(ability, ability.getHostCard());
} else {
executePayment(ability, decision.cards);
executePayment(ai, ability, decision.cards);
}
triggerCounterPutAll(ability);
return true;

View File

@@ -369,10 +369,23 @@ public class PhaseHandler implements java.io.Serializable {
if (numDiscard > 0) {
final CardZoneTable table = new CardZoneTable();
final CardCollection discarded = new CardCollection();
boolean firstDiscarded = playerTurn.getNumDiscardedThisTurn() == 0;
for (Card c : playerTurn.getController().chooseCardsToDiscardToMaximumHandSize(numDiscard)){
playerTurn.discard(c, null, table);
if (playerTurn.discard(c, null, table) != null) {
discarded.add(c);
}
}
table.triggerChangesZoneAll(game);
if (!discarded.isEmpty()) {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, playerTurn);
runParams.put(AbilityKey.Cards, discarded);
runParams.put(AbilityKey.Cause, null);
runParams.put(AbilityKey.FirstTime, firstDiscarded);
game.getTriggerHandler().runTrigger(TriggerType.DiscardedAll, runParams, false);
}
}
// Rule 514.2
@@ -387,7 +400,6 @@ public class PhaseHandler implements java.io.Serializable {
game.getEndOfTurn().registerUntilEndCommand(playerTurn);
for (Player player : game.getPlayers()) {
player.onCleanupPhase();
player.getController().autoPassCancel(); // autopass won't wrap to next turn
}
for (Player player : game.getLostPlayers()) {
@@ -488,6 +500,10 @@ public class PhaseHandler implements java.io.Serializable {
case CLEANUP:
bPreventCombatDamageThisTurn = false;
if (!bRepeatCleanup) {
// only call onCleanupPhase when Cleanup is not repeated
for (Player player : game.getPlayers()) {
player.onCleanupPhase();
}
setPlayerTurn(handleNextTurn());
// "Trigger" for begin turn to get around a phase skipping
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();

View File

@@ -0,0 +1,61 @@
package forge.game.trigger;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.spellability.SpellAbility;
import forge.util.Localizer;
public class TriggerDiscardedAll extends Trigger {
public TriggerDiscardedAll(Map<String, String> params, Card host, boolean intrinsic) {
super(params, host, intrinsic);
}
@Override
public boolean performTest(Map<AbilityKey, Object> runParams) {
if (hasParam("ValidPlayer")) {
if (!matchesValid(runParams.get(AbilityKey.Player), getParam("ValidPlayer").split(","),
this.getHostCard())) {
return false;
}
}
if (hasParam("ValidCause")) {
if (runParams.get(AbilityKey.Cause) == null) {
return false;
}
if (!matchesValid(runParams.get(AbilityKey.Cause), getParam("ValidCause").split(","),
this.getHostCard())) {
return false;
}
}
if (hasParam("FirstTime")) {
if (!(boolean) runParams.get(AbilityKey.FirstTime)) {
return false;
}
}
return true;
}
@Override
public void setTriggeringObjects(SpellAbility sa, Map<AbilityKey, Object> runParams) {
final CardCollection cards = (CardCollection) runParams.get(AbilityKey.Cards);
sa.setTriggeringObject(AbilityKey.Cards, cards);
sa.setTriggeringObject(AbilityKey.Amount, cards.size());
sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player, AbilityKey.Cause);
}
@Override
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player)).append(", ");
sb.append(Localizer.getInstance().getMessage("lblAmount")).append(": ").append(sa.getTriggeringObject(AbilityKey.Amount));
return sb.toString();
}
}

View File

@@ -52,6 +52,7 @@ public enum TriggerType {
Destroyed(TriggerDestroyed.class),
Devoured(TriggerDevoured.class),
Discarded(TriggerDiscarded.class),
DiscardedAll(TriggerDiscardedAll.class),
Drawn(TriggerDrawn.class),
Evolved(TriggerEvolved.class),
Exerted(TriggerExerted.class),

View File

@@ -0,0 +1,10 @@
Name:Rielle, the Everwise
ManaCost:1 U R
Types:Legendary Creature Human Wizard
PT:0/3
S:Mode$ Continuous | EffectZone$ Battlefield | AddPower$ X | References$ X | Description$ CARDNAME gets +1/+0 for each instant and sorcery card in your graveyard.
SVar:X:Count$ValidGraveyard Instant.YouOwn,Sorcery.YouOwn
T:Mode$ DiscardedAll | FirstTime$ True | TriggerZones$ Battlefield | Execute$ TrigDraw | TriggerDescription$ Whenever you discard one or more cards for the first time each turn, draw that many cards.
SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ Y | References$ Y
SVar:Y:TriggerCount$Amount
Oracle:Rielle, the Everwise gets +1/+0 for each instant and sorcery card in your graveyard.\nWhenever you discard one or more cards for the first time each turn, draw that many cards.

View File

@@ -380,7 +380,7 @@ public class HumanPlay {
return false;
}
CardCollectionView listmill = p.getCardsIn(ZoneType.Library, amount);
((CostMill) part).executePayment(sourceAbility, listmill);
((CostMill) part).payAsDecided(p, PaymentDecision.card(listmill), sourceAbility);
}
else if (part instanceof CostFlipCoin) {
final int amount = getAmountFromPart(part, source, sourceAbility);
@@ -488,7 +488,7 @@ public class HumanPlay {
return false;
}
costExile.executePayment(sourceAbility, p.getCardsIn(ZoneType.Graveyard));
costExile.payAsDecided(p, PaymentDecision.card(p.getCardsIn(ZoneType.Graveyard)), sourceAbility);
}
else {
from = costExile.getFrom();
@@ -502,7 +502,7 @@ public class HumanPlay {
return false;
}
list = list.subList(0, nNeeded);
costExile.executePayment(sourceAbility, list);
costExile.payAsDecided(p, PaymentDecision.card(list), sourceAbility);
} else {
// replace this with input
CardCollection newList = new CardCollection();
@@ -515,7 +515,7 @@ public class HumanPlay {
list.remove(c);
newList.add(c);
}
costExile.executePayment(sourceAbility, newList);
costExile.payAsDecided(p, PaymentDecision.card(newList), sourceAbility);
}
}
}
@@ -567,7 +567,7 @@ public class HumanPlay {
}
}
else { // Tainted Specter, Gurzigost, etc.
boolean hasPaid = payCostPart(controller, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblPutIntoLibrary") + orString);
boolean hasPaid = payCostPart(controller, p, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblPutIntoLibrary") + orString);
if (!hasPaid) {
return false;
}
@@ -584,13 +584,13 @@ public class HumanPlay {
else if (part instanceof CostGainControl) {
int amount = Integer.parseInt(part.getAmount());
CardCollectionView list = CardLists.getValidCards(p.getGame().getCardsIn(ZoneType.Battlefield), part.getType(), p, source);
boolean hasPaid = payCostPart(controller, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblGainControl") + orString);
boolean hasPaid = payCostPart(controller, p, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblGainControl") + orString);
if (!hasPaid) { return false; }
}
else if (part instanceof CostReturn) {
CardCollectionView list = CardLists.getValidCards(p.getCardsIn(ZoneType.Battlefield), part.getType(), p, source);
int amount = getAmountFromPartX(part, source, sourceAbility);
boolean hasPaid = payCostPart(controller, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblReturnToHand") + orString);
boolean hasPaid = payCostPart(controller, p, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblReturnToHand") + orString);
if (!hasPaid) { return false; }
}
else if (part instanceof CostDiscard) {
@@ -599,11 +599,11 @@ public class HumanPlay {
return false;
}
((CostDiscard)part).executePayment(sourceAbility, p.getCardsIn(ZoneType.Hand));
((CostDiscard)part).payAsDecided(p, PaymentDecision.card(p.getCardsIn(ZoneType.Hand)), sourceAbility);
} else {
CardCollectionView list = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), part.getType(), p, source);
int amount = getAmountFromPartX(part, source, sourceAbility);
boolean hasPaid = payCostPart(controller, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lbldiscard") + orString);
boolean hasPaid = payCostPart(controller, p, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lbldiscard") + orString);
if (!hasPaid) { return false; }
}
}
@@ -611,14 +611,14 @@ public class HumanPlay {
CostReveal costReveal = (CostReveal) part;
CardCollectionView list = CardLists.getValidCards(p.getCardsIn(costReveal.getRevealFrom()), part.getType(), p, source);
int amount = getAmountFromPartX(part, source, sourceAbility);
boolean hasPaid = payCostPart(controller, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblReveal") + orString);
boolean hasPaid = payCostPart(controller, p, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblReveal") + orString);
if (!hasPaid) { return false; }
}
else if (part instanceof CostTapType) {
CardCollectionView list = CardLists.getValidCards(p.getCardsIn(ZoneType.Battlefield), part.getType(), p, source);
list = CardLists.filter(list, Presets.UNTAPPED);
int amount = getAmountFromPartX(part, source, sourceAbility);
boolean hasPaid = payCostPart(controller, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblTap") + orString);
boolean hasPaid = payCostPart(controller, p, sourceAbility, (CostPartWithList)part, amount, list, Localizer.getInstance().getMessage("lblTap") + orString);
if (!hasPaid) { return false; }
}
else if (part instanceof CostPartMana) {
@@ -677,7 +677,7 @@ public class HumanPlay {
return paid;
}
private static boolean payCostPart(final PlayerControllerHuman controller, SpellAbility sourceAbility, CostPartWithList cpl, int amount, CardCollectionView list, String actionName) {
private static boolean payCostPart(final PlayerControllerHuman controller, Player p, SpellAbility sourceAbility, CostPartWithList cpl, int amount, CardCollectionView list, String actionName) {
if (list.size() < amount) { return false; } // unable to pay (not enough cards)
InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, amount, amount, list, sourceAbility);
@@ -689,11 +689,8 @@ public class HumanPlay {
return false;
}
cpl.executePayment(sourceAbility, new CardCollection(inp.getSelected()));
cpl.payAsDecided(p, PaymentDecision.card(inp.getSelected()), sourceAbility);
if (sourceAbility != null) {
cpl.reportPaidCardsTo(sourceAbility);
}
return true;
}