UNF: Added the 4 eternal-format-legal dice modification/reroll cards (#7489)

This commit is contained in:
autumnmyst
2025-05-06 10:13:52 -07:00
committed by GitHub
parent 8678b5ec5b
commit 0c6f1ff58f
15 changed files with 508 additions and 78 deletions

View File

@@ -15,6 +15,7 @@ import forge.game.*;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect; import forge.game.ability.effects.CharmEffect;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*; import forge.game.card.*;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
@@ -745,6 +746,30 @@ public class PlayerControllerAi extends PlayerController {
return Aggregates.random(rolls); return Aggregates.random(rolls);
} }
@Override
public List<Integer> chooseDiceToReroll(List<Integer> rolls) {
//TODO create AI logic for this
return new ArrayList<>();
}
@Override
public Integer chooseRollToModify(List<Integer> rolls) {
//TODO create AI logic for this
return Aggregates.random(rolls);
}
@Override
public RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls) {
//TODO create AI logic for this
return Aggregates.random(rolls);
}
@Override
public String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness) {
//TODO create AI logic for this
return Aggregates.random(swapChoices);
}
@Override @Override
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) { public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
return !ComputerUtil.wantMulligan(player, cardsToReturn); return !ComputerUtil.wantMulligan(player, cardsToReturn);
@@ -1207,6 +1232,11 @@ public class PlayerControllerAi extends PlayerController {
return false; return false;
} }
public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView<Player> allPayers) {
// TODO logic for AI to pay rerolls and modification costs
return false;
}
@Override @Override
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) { public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) { for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) {

View File

@@ -61,6 +61,7 @@ public enum AbilityKey {
DefendingPlayer("DefendingPlayer"), DefendingPlayer("DefendingPlayer"),
Destination("Destination"), Destination("Destination"),
Devoured("Devoured"), Devoured("Devoured"),
DicePTExchanges("DicePTExchanges"),
Discard("Discard"), Discard("Discard"),
DiscardedBefore("DiscardedBefore"), DiscardedBefore("DiscardedBefore"),
DividedShieldAmount("DividedShieldAmount"), DividedShieldAmount("DividedShieldAmount"),
@@ -94,6 +95,7 @@ public enum AbilityKey {
Mode("Mode"), Mode("Mode"),
Modifier("Modifier"), Modifier("Modifier"),
MonstrosityAmount("MonstrosityAmount"), MonstrosityAmount("MonstrosityAmount"),
NaturalResult("NaturalResult"),
NewCard("NewCard"), NewCard("NewCard"),
NewCounterAmount("NewCounterAmount"), NewCounterAmount("NewCounterAmount"),
NoPreventDamage("NoPreventDamage"), NoPreventDamage("NoPreventDamage"),

View File

@@ -2,6 +2,7 @@ package forge.game.ability.effects;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.PlanarDice; import forge.game.PlanarDice;
@@ -48,6 +49,12 @@ public class ReplaceEffect extends SpellAbilityEffect {
for (Player key : AbilityUtils.getDefinedPlayers(card, sa.getParam("VarKey"), sa)) { for (Player key : AbilityUtils.getDefinedPlayers(card, sa.getParam("VarKey"), sa)) {
m.put(key, m.getOrDefault(key, 0) + AbilityUtils.calculateAmount(card, varValue, sa)); m.put(key, m.getOrDefault(key, 0) + AbilityUtils.calculateAmount(card, varValue, sa));
} }
} else if ("CardSet".equals(type)) {
Set<Card> cards = (Set<Card>) params.get(varName);
List<Card> list = AbilityUtils.getDefinedCards(card, varValue, sa);
if (!list.isEmpty()) {
cards.add(list.get(0));
}
} else if (varName != null) { } else if (varName != null) {
params.put(varName, AbilityUtils.calculateAmount(card, varValue, sa)); params.put(varName, AbilityUtils.calculateAmount(card, varValue, sa));
} }

View File

@@ -5,13 +5,16 @@ import com.google.common.collect.Maps;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.event.GameEventRollDie; import forge.game.event.GameEventRollDie;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.player.PlayerController;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Lang; import forge.util.Lang;
import forge.util.Localizer; import forge.util.Localizer;
import forge.util.MyRandom; import forge.util.MyRandom;
@@ -38,6 +41,59 @@ public class RollDiceEffect extends SpellAbilityEffect {
return sb.toString(); return sb.toString();
} }
public static class DieRollResult {
private int naturalValue;
private int modifiedValue;
public DieRollResult(int naturalValue, int modifiedValue) {
this.naturalValue = naturalValue;
this.modifiedValue = modifiedValue;
}
// Getters
public int getNaturalValue() {
return naturalValue;
}
public int getModifiedValue() {
return modifiedValue;
}
// Setters
public void setNaturalValue(int naturalValue) {
this.naturalValue = naturalValue;
}
public void setModifiedValue(int modifiedValue) {
this.modifiedValue = modifiedValue;
}
@Override
public String toString() {
return String.valueOf(modifiedValue);
}
}
public static List<DieRollResult> getResultsList(List<Integer> naturalResults) {
List<DieRollResult> results = new ArrayList<>();
for (int r : naturalResults) {
results.add(new DieRollResult(r, r));
}
return results;
}
public static List<Integer> getNaturalResults(List<DieRollResult> results) {
List<Integer> naturalResults = new ArrayList<>();
for (DieRollResult r : results) {
naturalResults.add(r.getNaturalValue());
}
return naturalResults;
}
public static List<Integer> getFinalResults(List<DieRollResult> results) {
List<Integer> naturalResults = new ArrayList<>();
for (DieRollResult r : results) {
naturalResults.add(r.getModifiedValue());
}
return naturalResults;
}
/* (non-Javadoc) /* (non-Javadoc)
* @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility) * @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility)
*/ */
@@ -80,12 +136,277 @@ public class RollDiceEffect extends SpellAbilityEffect {
} }
Map<Player, Integer> ignoreChosenMap = Maps.newHashMap(); Map<Player, Integer> ignoreChosenMap = Maps.newHashMap();
Set<Card> dicePTExchanges = new HashSet<>();
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(player); final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(player);
List<Integer> ignored = new ArrayList<>();
List<Integer> naturalRolls = rollAction(amount, sides, ignore, rollsResult, ignored, ignoreChosenMap, dicePTExchanges, player, repParams);
if (sa != null && sa.hasParam("UseHighestRoll")) {
naturalRolls.subList(0, naturalRolls.size() - 1).clear();
}
// Reroll Phase:
String monitorKeyword = "Once each turn, you may pay {1} to reroll one or more dice you rolled.";
CardCollection canRerollDice = getRerollCards(player, monitorKeyword);
while (!canRerollDice.isEmpty()) {
List<Integer> diceToReroll = player.getController().chooseDiceToReroll(naturalRolls);
if (diceToReroll.isEmpty()) {break;}
String message = Localizer.getInstance().getMessage("lblChooseRerollCard");
Card c = player.getController().chooseSingleEntityForEffect(canRerollDice, sa, message, null);
String[] parts = c.getSVar("ModsThisTurn").split("\\$");
int activationsThisTurn = Integer.parseInt(parts[1]);
SpellAbility modifierSA = c.getFirstSpellAbility();
Cost cost = new Cost(c.getSVar("RollRerollCost"), false);
boolean paid = player.getController().payCostDuringRoll(cost, modifierSA, null);
if (paid) {
for (Integer roll : diceToReroll) {
naturalRolls.remove(roll);
}
int amountToReroll = diceToReroll.size();
List<Integer> rerolls = rollAction(amountToReroll, sides, 0, null, ignored, Maps.newHashMap(), dicePTExchanges, player, repParams);
naturalRolls.addAll(rerolls);
activationsThisTurn += 1;
c.setSVar("ModsThisTurn", "Number$" + activationsThisTurn);
canRerollDice.remove(c);
}
}
// Modification Phase:
List<DieRollResult> resultsList = new ArrayList<>();
Integer rollToModify;
String xenoKeyword = "After you roll a die, you may remove a +1/+1 counter from Xenosquirrels. If you do, increase or decrease the result by 1.";
String nightShiftKeyword = "After you roll a die, you may pay 1 life. If you do, increase or decrease the result by 1. Do this only once each turn.";
List<Card> canIncrementDice = getIncrementCards(player, xenoKeyword, nightShiftKeyword);
boolean hasBeenModified = false;
if (!canIncrementDice.isEmpty()) {
do {
rollToModify = player.getController().chooseRollToModify(naturalRolls);
if (rollToModify == null) {break;}
boolean modified = false;
DieRollResult dieResult = new DieRollResult(rollToModify, rollToModify);
// canIncrementThisRoll won't be empty the first iteration because canIncrementDice wasn't empty
CardCollection canIncrementThisRoll = new CardCollection(canIncrementDice);
Card c;
do {
String message = Localizer.getInstance().getMessage("lblChooseRollIncrementCard", rollToModify);
c = player.getController().chooseSingleEntityForEffect(canIncrementThisRoll, sa, message, null);
String[] parts = c.getSVar("ModsThisTurn").split("\\$");
int activationsThisTurn = Integer.parseInt(parts[1]);
SpellAbility modifierSA = c.getFirstSpellAbility();
String costString = c.getSVar("RollModifyCost");
Cost cost = new Cost(costString, false);
boolean paid = player.getController().payCostDuringRoll(cost, modifierSA, null);
if (paid) {
message = Localizer.getInstance().getMessage("lblChooseRollIncrement", rollToModify);
boolean isPositive = player.getController().chooseBinary(sa, message, PlayerController.BinaryChoiceType.IncreaseOrDecrease);
int increment = isPositive ? 1 : -1;
if (!modified) {naturalRolls.remove(rollToModify); modified = true;}
rollToModify += increment;
activationsThisTurn += 1;
c.setSVar("ModsThisTurn", "Number$" + activationsThisTurn);
canIncrementThisRoll.remove(c);
}
} while (!canIncrementThisRoll.isEmpty());
if (modified) {
dieResult.setModifiedValue(rollToModify);
resultsList.add(dieResult);
hasBeenModified = true;
}
canIncrementDice = getIncrementCards(player, xenoKeyword, nightShiftKeyword);
} while (!naturalRolls.isEmpty() && !canIncrementDice.isEmpty());
}
// finish roll list
for (Integer unmodified : naturalRolls) {
// Add all the unmodified rolls into the results
resultsList.add(new DieRollResult(unmodified, unmodified));
}
// Vedalken Exchange
CardCollection vedalkenSwaps = new CardCollection(dicePTExchanges);
if (!vedalkenSwaps.isEmpty()) {
DieRollResult rollToSwap;
do {
rollToSwap = player.getController().chooseRollToSwap(resultsList);
if (rollToSwap == null) {break;}
String message = Localizer.getInstance().getMessage("lblChooseCardToDiceSwap", rollToSwap.getModifiedValue());
Card c = player.getController().chooseSingleEntityForEffect(vedalkenSwaps, sa, message, null);
int cPower = c.getCurrentPower();
int cToughness = c.getCurrentToughness();
String labelPower = Localizer.getInstance().getMessage("lblPower");
String labelToughness = Localizer.getInstance().getMessage("lblToughness");
List<String> choices = Arrays.asList(labelPower, labelToughness);
String powerOrToughness = player.getController().chooseRollSwapValue(choices, rollToSwap.getModifiedValue(), cPower, cToughness);
if (powerOrToughness != null) {
int tempRollValue = rollToSwap.getModifiedValue();
if (powerOrToughness.equals(labelPower)) {
rollToSwap.setModifiedValue(cPower);
c.addNewPT(tempRollValue, cToughness, player.getGame().getNextTimestamp(), 0);
} else if (powerOrToughness.equals(labelToughness)) {
rollToSwap.setModifiedValue(cToughness);
c.addNewPT(cPower, tempRollValue, player.getGame().getNextTimestamp(), 0);
} else {
throw new IllegalStateException("Unexpected value: " + powerOrToughness);
}
vedalkenSwaps.remove(c);
}
} while (!vedalkenSwaps.isEmpty());
}
//Notify of results
if (amount > 0) {
StringBuilder sb = new StringBuilder();
String rollResults = StringUtils.join(getFinalResults(resultsList), ", ");
String resultMessage = toVisitAttractions ? "lblAttractionRollResult" : "lblPlayerRolledResult";
sb.append(Localizer.getInstance().getMessage(resultMessage, player, rollResults));
if (!ignored.isEmpty()) {
sb.append("\r\n").append(Localizer.getInstance().getMessage("lblIgnoredRolls",
StringUtils.join(ignored, ", ")));
}
if (hasBeenModified) {
sb.append("\r\n").append(Localizer.getInstance().getMessage("lblNaturalRolls",
StringUtils.join(getNaturalResults(resultsList), ", ")));
}
player.getGame().getAction().notifyOfValue(sa, player, sb.toString(), null);
player.addDieRollThisTurn(getFinalResults(resultsList));
}
List<Integer> rolls = Lists.newArrayList();
int oddResults = 0;
int evenResults = 0;
int differentResults = 0;
int countMaxRolls = 0;
for (DieRollResult i : resultsList) {
int naturalRoll = i.getNaturalValue();
final int modifiedRoll = i.getModifiedValue() + modifier;
i.setModifiedValue(modifiedRoll);
if (!rolls.contains(modifiedRoll)) {
differentResults++;
}
rolls.add(modifiedRoll);
if (modifiedRoll % 2 == 0) {
evenResults++;
} else {
oddResults++;
}
if (naturalRoll == sides) {
countMaxRolls++;
}
}
if (sa != null) {
if (sa.hasParam("EvenOddResults")) {
sa.setSVar("EvenResults", Integer.toString(evenResults));
sa.setSVar("OddResults", Integer.toString(oddResults));
}
if (sa.hasParam("DifferentResults")) {
sa.setSVar("DifferentResults", Integer.toString(differentResults));
}
if (sa.hasParam("MaxRollsResults")) {
sa.setSVar("MaxRolls", Integer.toString(countMaxRolls));
}
}
int rollNum = 1;
for (DieRollResult roll : resultsList) {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(player);
runParams.put(AbilityKey.Sides, sides);
runParams.put(AbilityKey.Modifier, modifier);
runParams.put(AbilityKey.Result, roll.getModifiedValue());
runParams.put(AbilityKey.NaturalResult, roll.getNaturalValue());
runParams.put(AbilityKey.RolledToVisitAttractions, toVisitAttractions);
runParams.put(AbilityKey.Number, player.getNumRollsThisTurn() - amount + rollNum);
player.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDie, runParams, false);
rollNum++;
}
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(player);
runParams.put(AbilityKey.Sides, sides);
runParams.put(AbilityKey.Result, getFinalResults(resultsList));
runParams.put(AbilityKey.RolledToVisitAttractions, toVisitAttractions);
player.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDieOnce, runParams, false);
return getFinalResults(resultsList).stream().reduce(0, Integer::sum);
}
/**
* Gets a list of cards that can reroll dice roll results for a given player.
* This is currently only Monitor Monitor
*
* @param player The player whose battlefield is being checked for cards that can modify dice rolls
* @param monitorKeyword The keyword text identifying Monitor Monitor cards
* @return A list of cards that are currently able to reroll dice
*/
public static CardCollection getRerollCards(Player player, String monitorKeyword) {
CardCollection monitors = CardLists.getKeyword(player.getCardsIn(ZoneType.Battlefield), monitorKeyword);
return monitors.filter(card -> {
String activationLimit = card.getSVar("RollModificationsLimit");
String[] parts = card.getSVar("ModsThisTurn").split("\\$");
int activationsThisTurn = Integer.parseInt(parts[1]);
return (activationLimit.equals("None") || activationsThisTurn < Integer.parseInt(activationLimit));
});
}
/**
* Gets a list of cards that can modify dice roll results for a given player.
* This includes both Xenosquirrels (which can remove +1/+1 counters to modify rolls)
* and Night Shift cards (which can pay life to modify rolls once per turn).
*
* @param player The player whose battlefield is being checked for cards that can modify dice rolls
* @param xenoKeyword The keyword text identifying Xenosquirrel cards
* @param nightShiftKeyword The keyword text identifying Night Shift cards
* @return A list of cards that are currently able to modify dice roll results
*/
public static List<Card> getIncrementCards(Player player, String xenoKeyword, String nightShiftKeyword) {
CardCollection xenosquirrels = CardLists.getKeyword(player.getCardsIn(ZoneType.Battlefield), xenoKeyword);
CardCollection nightShifts = CardLists.getKeyword(player.getCardsIn(ZoneType.Battlefield), nightShiftKeyword);
List<Card> canIncrementDice = new ArrayList<>();
for (Card c : xenosquirrels) {
// Xenosquirrels must have a P1P1 counter on it to remove in order to modify
Integer P1P1Counters = c.getCounters().get(CounterType.get(CounterEnumType.P1P1));
if (P1P1Counters != null && P1P1Counters > 0 && c.canRemoveCounters(CounterType.get(CounterEnumType.P1P1))) {
canIncrementDice.add(c);
}
}
for (Card c : nightShifts) {
// Night Shift of the Living Dead has a limit of once per turn, player must be able to pay the 1 life cost
String activationLimit = c.getSVar("RollModificationsLimit");
String[] parts = c.getSVar("ModsThisTurn").split("\\$");
int activationsThisTurn = Integer.parseInt(parts[1]);
if ((activationLimit.equals("None") || activationsThisTurn < Integer.parseInt(activationLimit)) && player.canPayLife(1, true, c.getFirstSpellAbility())) {
canIncrementDice.add(c);
}
}
return canIncrementDice;
}
/**
* Performs the dice rolling action with support for replacements, ignoring rolls, and tracking results.
*
* @param amount number of dice to roll
* @param sides number of sides on each die
* @param ignore number of lowest rolls to automatically ignore
* @param rollsResult optional list to store roll results, if null a new list will be created
* @param ignored list to store ignored roll results
* @param ignoreChosenMap mapping of players to number of rolls they can choose to ignore
* @param player the player performing the roll
* @param repParams replacement effect parameters
* @return list of final roll results after applying ignores and replacements, sorted in ascending order
*/
private static List<Integer> rollAction(int amount, int sides, int ignore, List<Integer> rollsResult, List<Integer> ignored, Map<Player, Integer> ignoreChosenMap, Set<Card> dicePTExchanges, Player player, Map<AbilityKey, Object> repParams) {
repParams.put(AbilityKey.Sides, sides);
repParams.put(AbilityKey.Number, amount); repParams.put(AbilityKey.Number, amount);
repParams.put(AbilityKey.Ignore, ignore); repParams.put(AbilityKey.Ignore, ignore);
repParams.put(AbilityKey.DicePTExchanges, dicePTExchanges);
repParams.put(AbilityKey.IgnoreChosen, ignoreChosenMap); repParams.put(AbilityKey.IgnoreChosen, ignoreChosenMap);
switch (player.getGame().getReplacementHandler().run(ReplacementType.RollDice, repParams)) { switch (player.getGame().getReplacementHandler().run(ReplacementType.RollDice, repParams)) {
case NotReplaced: case NotReplaced:
break; break;
@@ -110,7 +431,6 @@ public class RollDiceEffect extends SpellAbilityEffect {
naturalRolls.sort(null); naturalRolls.sort(null);
List<Integer> ignored = new ArrayList<>();
// Ignore lowest rolls // Ignore lowest rolls
if (ignore > 0) { if (ignore > 0) {
for (int i = ignore - 1; i >= 0; --i) { for (int i = ignore - 1; i >= 0; --i) {
@@ -127,75 +447,7 @@ public class RollDiceEffect extends SpellAbilityEffect {
} }
} }
if (sa != null && sa.hasParam("UseHighestRoll")) { return naturalRolls;
naturalRolls.subList(0, naturalRolls.size() - 1).clear();
}
//Notify of results
if (amount > 0) {
StringBuilder sb = new StringBuilder();
String rollResults = StringUtils.join(naturalRolls, ", ");
String resultMessage = toVisitAttractions ? "lblAttractionRollResult" : "lblPlayerRolledResult";
sb.append(Localizer.getInstance().getMessage(resultMessage, player, rollResults));
if (!ignored.isEmpty()) {
sb.append("\r\n").append(Localizer.getInstance().getMessage("lblIgnoredRolls",
StringUtils.join(ignored, ", ")));
}
player.getGame().getAction().notifyOfValue(sa, player, sb.toString(), null);
player.addDieRollThisTurn(naturalRolls);
}
List<Integer> rolls = Lists.newArrayList();
int oddResults = 0;
int evenResults = 0;
int differentResults = 0;
int countMaxRolls = 0;
for (Integer i : naturalRolls) {
final int modifiedRoll = i + modifier;
if (!rolls.contains(modifiedRoll)) {
differentResults++;
}
rolls.add(modifiedRoll);
if (modifiedRoll % 2 == 0) {
evenResults++;
} else {
oddResults++;
}
if (i == sides) {
countMaxRolls++;
}
}
if (sa != null) {
if (sa.hasParam("EvenOddResults")) {
sa.setSVar("EvenResults", Integer.toString(evenResults));
sa.setSVar("OddResults", Integer.toString(oddResults));
}
if (sa.hasParam("DifferentResults")) {
sa.setSVar("DifferentResults", Integer.toString(differentResults));
}
if (sa.hasParam("MaxRollsResults")) {
sa.setSVar("MaxRolls", Integer.toString(countMaxRolls));
}
}
int rollNum = 1;
for (Integer roll : rolls) {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(player);
runParams.put(AbilityKey.Sides, sides);
runParams.put(AbilityKey.Modifier, modifier);
runParams.put(AbilityKey.Result, roll);
runParams.put(AbilityKey.RolledToVisitAttractions, toVisitAttractions);
runParams.put(AbilityKey.Number, player.getNumRollsThisTurn() - amount + rollNum);
player.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDie, runParams, false);
rollNum++;
}
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(player);
runParams.put(AbilityKey.Sides, sides);
runParams.put(AbilityKey.Result, rolls);
runParams.put(AbilityKey.RolledToVisitAttractions, toVisitAttractions);
player.getGame().getTriggerHandler().runTrigger(TriggerType.RolledDieOnce, runParams, false);
return rolls.stream().reduce(0, Integer::sum);
} }
private static void resolveSub(SpellAbility sa, int num) { private static void resolveSub(SpellAbility sa, int num) {

View File

@@ -12,6 +12,7 @@ import forge.deck.Deck;
import forge.deck.DeckSection; import forge.deck.DeckSection;
import forge.game.*; import forge.game.*;
import forge.game.GameOutcome.AnteResult; import forge.game.GameOutcome.AnteResult;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*; import forge.game.card.*;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
@@ -55,7 +56,8 @@ public abstract class PlayerController {
OddsOrEvens, OddsOrEvens,
UntapOrLeaveTapped, UntapOrLeaveTapped,
LeftOrRight, LeftOrRight,
AddOrRemove AddOrRemove,
IncreaseOrDecrease
} }
public enum FullControlFlag { public enum FullControlFlag {
@@ -229,6 +231,10 @@ public abstract class PlayerController {
public abstract PlanarDice choosePDRollToIgnore(List<PlanarDice> rolls); public abstract PlanarDice choosePDRollToIgnore(List<PlanarDice> rolls);
public abstract Integer chooseRollToIgnore(List<Integer> rolls); public abstract Integer chooseRollToIgnore(List<Integer> rolls);
public abstract List<Integer> chooseDiceToReroll(List<Integer> rolls);
public abstract Integer chooseRollToModify(List<Integer> rolls);
public abstract RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls);
public abstract String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness);
public abstract Object vote(SpellAbility sa, String prompt, List<Object> options, ListMultimap<Object, Player> votes, Player forPlayer, boolean optional); public abstract Object vote(SpellAbility sa, String prompt, List<Object> options, ListMultimap<Object, Player> votes, Player forPlayer, boolean optional);
@@ -292,6 +298,7 @@ public abstract class PlayerController {
public abstract List<CostPart> orderCosts(List<CostPart> costs); public abstract List<CostPart> orderCosts(List<CostPart> costs);
public abstract boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers); public abstract boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers);
public abstract boolean payCostDuringRoll(Cost cost, SpellAbility sa, FCollectionView<Player> allPayers);
public abstract boolean payCombatCost(Card card, Cost cost, SpellAbility sa, String prompt); public abstract boolean payCombatCost(Card card, Cost cost, SpellAbility sa, String prompt);

View File

@@ -26,6 +26,11 @@ public class ReplaceRollDice extends ReplacementEffect {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) { if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) {
return false; return false;
} }
if (hasParam("ValidSides")) {
if (((Integer) runParams.get(AbilityKey.Sides)) != Integer.parseInt(getParam("ValidSides"))) {
return false;
}
}
return true; return true;
} }
@@ -37,5 +42,6 @@ public class ReplaceRollDice extends ReplacementEffect {
sa.setReplacingObject(AbilityKey.Number, runParams.get(AbilityKey.Number)); sa.setReplacingObject(AbilityKey.Number, runParams.get(AbilityKey.Number));
sa.setReplacingObject(AbilityKey.Ignore, runParams.get(AbilityKey.Ignore)); sa.setReplacingObject(AbilityKey.Ignore, runParams.get(AbilityKey.Ignore));
sa.setReplacingObject(AbilityKey.IgnoreChosen, runParams.get(AbilityKey.IgnoreChosen)); sa.setReplacingObject(AbilityKey.IgnoreChosen, runParams.get(AbilityKey.IgnoreChosen));
sa.setReplacingObject(AbilityKey.DicePTExchanges, runParams.get(AbilityKey.DicePTExchanges));
} }
} }

View File

@@ -32,7 +32,7 @@ public class TriggerRolledDie extends Trigger {
String[] params = getParam("ValidResult").split(","); String[] params = getParam("ValidResult").split(",");
int result = (int) runParams.get(AbilityKey.Result); int result = (int) runParams.get(AbilityKey.Result);
if (hasParam("Natural")) { if (hasParam("Natural")) {
result -= (int) runParams.get(AbilityKey.Modifier); result = (int) runParams.get(AbilityKey.NaturalResult);
} }
for (String param : params) { for (String param : params) {
if (StringUtils.isNumeric(param)) { if (StringUtils.isNumeric(param)) {

View File

@@ -22,6 +22,7 @@ import forge.deck.Deck;
import forge.deck.DeckSection; import forge.deck.DeckSection;
import forge.game.*; import forge.game.*;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*; import forge.game.card.*;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
@@ -53,10 +54,7 @@ import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import java.util.Collection; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
/** /**
@@ -526,6 +524,26 @@ public class PlayerControllerForTests extends PlayerController {
return Aggregates.random(rolls); return Aggregates.random(rolls);
} }
@Override
public List<Integer> chooseDiceToReroll(List<Integer> rolls) {
return new ArrayList<>();
}
@Override
public Integer chooseRollToModify(List<Integer> rolls) {
return Aggregates.random(rolls);
}
@Override
public RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls) {
return Aggregates.random(rolls);
}
@Override
public String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness) {
return Aggregates.random(swapChoices);
}
@Override @Override
public Object vote(SpellAbility sa, String prompt, List<Object> options, ListMultimap<Object, Player> votes, Player forPlayer, boolean optional) { public Object vote(SpellAbility sa, String prompt, List<Object> options, ListMultimap<Object, Player> votes, Player forPlayer, boolean optional) {
return chooseItem(options); return chooseItem(options);
@@ -577,6 +595,12 @@ public class PlayerControllerForTests extends PlayerController {
return false; return false;
} }
@Override
public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView<Player> allPayers) {
// TODO Auto-generated method stub
return false;
}
@Override @Override
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) { public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
for (final SpellAbility sa : activePlayerSAs) { for (final SpellAbility sa : activePlayerSAs) {

View File

@@ -0,0 +1,15 @@
Name:Monitor Monitor
ManaCost:2 U U
Types:Creature Human Employee
PT:2/5
# The reroll effect does not work with planar dice currently
T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigOpenAttraction | TriggerDescription$ When CARDNAME enters the battlefield, open an Attraction.
SVar:TrigOpenAttraction:DB$ OpenAttraction
T:Mode$ TurnBegin | Execute$ ResetTurnCount | Static$ True
SVar:ResetTurnCount:DB$ StoreSVar | SVar$ ModsThisTurn | Type$ Number | Expression$ 0
K:Once each turn, you may pay {1} to reroll one or more dice you rolled.
SVar:RollModificationsLimit:1
SVar:ModsThisTurn:Number$0
SVar:RollRerollCost:1
AI:RemoveDeck:All
Oracle:When this creature enters, open an Attraction. (Put the top card of your Attraction deck onto the battlefield.)\nOnce each turn, you may pay {1} to reroll one or more dice you rolled.

View File

@@ -0,0 +1,14 @@
Name:Night Shift of the Living Dead
ManaCost:3 B
Types:Enchantment
K:After you roll a die, you may pay 1 life. If you do, increase or decrease the result by 1. Do this only once each turn.
T:Mode$ TurnBegin | Execute$ ResetTurnCount | Static$ True
SVar:ResetTurnCount:DB$ StoreSVar | SVar$ ModsThisTurn | Type$ Number | Expression$ 0
T:Mode$ RolledDie | TriggerZones$ Battlefield | Execute$ TrigToken | ValidPlayer$ You | ValidResult$ EQ6 | TriggerDescription$ Whenever you roll a 6, create a 2/2 black Zombie Employee creature token.
SVar:TrigToken:DB$ Token | TokenScript$ b_2_2_zombie_employee
SVar:RollModificationsLimit:1
SVar:ModsThisTurn:Number$0
SVar:RollModifyCost:PayLife<1>
DeckHas:Ability$Token
AI:RemoveDeck:All
Oracle:After you roll a die, you may pay 1 life. If you do, increase or decrease the result by 1. Do this only once each turn.\nWhenever you roll a 6, create a 2/2 black Zombie Employee creature token.

View File

@@ -0,0 +1,12 @@
Name:Vedalken Squirrel-Whacker
ManaCost:3 U
Types:Creature Vedalken Guest
PT:*/*
K:ETBReplacement:Other:TrigRoll
SVar:TrigRoll:DB$ RollDice | ResultSVar$ diePwr | SubAbility$ RollTough | SpellDescription$ As CARDNAME enters, roll a six-sided die twice. Its base power becomes the first result and its base toughness becomes the second result.
SVar:RollTough:DB$ RollDice | ResultSVar$ dieTgn | SubAbility$ DBAnimate
SVar:DBAnimate:DB$ Animate | Defined$ Self | Power$ diePwr | Toughness$ dieTgn | Duration$ Permanent
R:Event$ RollDice | ActiveZones$ Battlefield | ValidPlayer$ You | ValidSides$ 6 | ReplaceWith$ SwapRoll | Description$ If you would roll one or more six-sided dice, instead roll them and you may exchange one result with this creatures base power or base toughness.
SVar:SwapRoll:DB$ ReplaceEffect | VarName$ DicePTExchanges | VarType$ CardSet | VarValue$ Self
AI:RemoveDeck:All
Oracle:As this creature enters, roll a six-sided die twice. Its base power becomes the first result and its base toughness becomes the second result.\nIf you would roll one or more six-sided dice, instead roll them and you may exchange one result with this creatures base power or base toughness.

View File

@@ -0,0 +1,11 @@
Name:Xenosquirrels
ManaCost:1 B
Types:Creature Alien Squirrel
PT:0/0
K:etbCounter:P1P1:2
K:After you roll a die, you may remove a +1/+1 counter from Xenosquirrels. If you do, increase or decrease the result by 1.
SVar:RollModifyCost:SubCounter<1/P1P1>
SVar:RollModificationsLimit:None
SVar:ModsThisTurn:Number$0
AI:RemoveDeck:All
Oracle:This creature enters with two +1/+1 counters on it.\nAfter you roll a die, you may remove a +1/+1 counter from this creature. If you do, increase or decrease the result by 1.

View File

@@ -1418,6 +1418,18 @@ lblAssignSprocketNextTurn=(Cranked next turn)
lblChooseCrank=Choose contraptions to crank lblChooseCrank=Choose contraptions to crank
lblChooseSectorEffect=Choose a sector lblChooseSectorEffect=Choose a sector
lblChooseRollIgnore=Choose a roll to ignore lblChooseRollIgnore=Choose a roll to ignore
#RollDiceEffect.java
lblChooseDiceToRerollTitle=Choose rolls to reroll?
lblChooseRerollCard=Choose a card to reroll dice
lblChooseDiceToRerollCaption=Dice to reroll
lblChooseRollToModify=Choose a roll to modify?
lblChooseRollToSwap=Choose a roll to swap?
lblChooseCardToDiceSwap=Choose a card to swap its Power or Toughness with die result {0}
lblChooseSwapPT=Swap result {0} with {1}/{2} creature''s Power or Toughness?
lblChooseRollIncrementCard=Choose a card to modify die result {0}
lblChooseRollIncrement=Increase or decrease result {0} by 1
lblIncrease=Increase
lblDecrease=Decrease
lblChooseRingBearer=Choose your Ring-bearer lblChooseRingBearer=Choose your Ring-bearer
lblTheRingTempts=The Ring tempts {0} lblTheRingTempts=The Ring tempts {0}
lblTop=Top lblTop=Top
@@ -2149,6 +2161,7 @@ lblDoYouWantRevealYourHand=Do you want to reveal your hand?
#RollDiceEffect.java #RollDiceEffect.java
lblPlayerRolledResult={0} rolled {1} lblPlayerRolledResult={0} rolled {1}
lblIgnoredRolls=Ignored rolls: {0} lblIgnoredRolls=Ignored rolls: {0}
lblNaturalRolls=Natural rolls: {0}
lblRerollResult=Reroll {0}? lblRerollResult=Reroll {0}?
lblAttractionRollResult={0} rolled to visit their Attractions. Result: {1}. lblAttractionRollResult={0} rolled to visit their Attractions. Result: {1}.
#RollPlanarDiceEffect.java #RollPlanarDiceEffect.java

View File

@@ -0,0 +1,6 @@
Name:Zombie Employee Token
ManaCost:no cost
Colors:black
Types:Creature Zombie Employee
PT:2/2
Oracle:

View File

@@ -16,6 +16,7 @@ import forge.game.*;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*; import forge.game.card.*;
import forge.game.card.CardView.CardStateView; import forge.game.card.CardView.CardStateView;
import forge.game.card.token.TokenInfo; import forge.game.card.token.TokenInfo;
@@ -1438,6 +1439,27 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
return getGui().one(Localizer.getInstance().getMessage("lblChooseRollIgnore"), rolls); return getGui().one(Localizer.getInstance().getMessage("lblChooseRollIgnore"), rolls);
} }
@Override
public List<Integer> chooseDiceToReroll(List<Integer> rolls) {
return getGui().many(Localizer.getInstance().getMessage("lblChooseDiceToRerollTitle"),
Localizer.getInstance().getMessage("lblChooseDiceToRerollCaption"),0, rolls.size(), rolls, null);
}
@Override
public Integer chooseRollToModify(List<Integer> rolls) {
return getGui().oneOrNone(Localizer.getInstance().getMessage("lblChooseRollToModify"), rolls);
}
@Override
public RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls) {
return getGui().oneOrNone(Localizer.getInstance().getMessage("lblChooseRollToSwap"), rolls);
}
@Override
public String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness) {
return getGui().oneOrNone(Localizer.getInstance().getMessage("lblChooseSwapPT", currentResult, power, toughness), swapChoices);
}
@Override @Override
public Object vote(final SpellAbility sa, final String prompt, final List<Object> options, public Object vote(final SpellAbility sa, final String prompt, final List<Object> options,
final ListMultimap<Object, Player> votes, Player forPlayer, boolean optional) { final ListMultimap<Object, Player> votes, Player forPlayer, boolean optional) {
@@ -1687,6 +1709,9 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
case AddOrRemove: case AddOrRemove:
labels = ImmutableList.of(localizer.getMessage("lblAddCounter"), localizer.getMessage("lblRemoveCounter")); labels = ImmutableList.of(localizer.getMessage("lblAddCounter"), localizer.getMessage("lblRemoveCounter"));
break; break;
case IncreaseOrDecrease:
labels = ImmutableList.of(localizer.getMessage("lblIncrease"), localizer.getMessage("lblDecrease"));
break;
default: default:
labels = ImmutableList.copyOf(kindOfChoice.toString().split("Or")); labels = ImmutableList.copyOf(kindOfChoice.toString().split("Or"));
} }
@@ -1982,6 +2007,12 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
return HumanPlay.payCostDuringAbilityResolve(this, player, sa.getHostCard(), cost, sa, prompt); return HumanPlay.payCostDuringAbilityResolve(this, player, sa.getHostCard(), cost, sa, prompt);
} }
@Override
public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView<Player> allPayers) {
// if it's paid by the AI already the human can pay, but it won't change anything
return HumanPlay.payCostDuringAbilityResolve(this, player, sa.getHostCard(), cost, sa, null);
}
// stores saved order for different sets of SpellAbilities // stores saved order for different sets of SpellAbilities
private final Map<String, List<Integer>> orderedSALookup = Maps.newHashMap(); private final Map<String, List<Integer>> orderedSALookup = Maps.newHashMap();