From 0c6f1ff58f852f81a91e16509e42ac056845eb19 Mon Sep 17 00:00:00 2001 From: autumnmyst Date: Tue, 6 May 2025 10:13:52 -0700 Subject: [PATCH] UNF: Added the 4 eternal-format-legal dice modification/reroll cards (#7489) --- .../java/forge/ai/PlayerControllerAi.java | 30 ++ .../java/forge/game/ability/AbilityKey.java | 2 + .../game/ability/effects/ReplaceEffect.java | 7 + .../game/ability/effects/RollDiceEffect.java | 396 ++++++++++++++---- .../forge/game/player/PlayerController.java | 9 +- .../game/replacement/ReplaceRollDice.java | 6 + .../forge/game/trigger/TriggerRolledDie.java | 2 +- .../util/PlayerControllerForTests.java | 32 +- .../res/cardsfolder/m/monitor_monitor.txt | 15 + .../n/night_shift_of_the_living_dead.txt | 14 + .../v/vedalken_squirrel_whacker.txt | 12 + forge-gui/res/cardsfolder/x/xenosquirrels.txt | 11 + forge-gui/res/languages/en-US.properties | 13 + .../tokenscripts/b_2_2_zombie_employee.txt | 6 + .../forge/player/PlayerControllerHuman.java | 31 ++ 15 files changed, 508 insertions(+), 78 deletions(-) create mode 100644 forge-gui/res/cardsfolder/m/monitor_monitor.txt create mode 100644 forge-gui/res/cardsfolder/n/night_shift_of_the_living_dead.txt create mode 100644 forge-gui/res/cardsfolder/v/vedalken_squirrel_whacker.txt create mode 100644 forge-gui/res/cardsfolder/x/xenosquirrels.txt create mode 100644 forge-gui/res/tokenscripts/b_2_2_zombie_employee.txt diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 81668086ab9..6cd6815c86f 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -15,6 +15,7 @@ import forge.game.*; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.ability.effects.CharmEffect; +import forge.game.ability.effects.RollDiceEffect; import forge.game.card.*; import forge.game.combat.Combat; import forge.game.cost.Cost; @@ -745,6 +746,30 @@ public class PlayerControllerAi extends PlayerController { return Aggregates.random(rolls); } + @Override + public List chooseDiceToReroll(List rolls) { + //TODO create AI logic for this + return new ArrayList<>(); + } + + @Override + public Integer chooseRollToModify(List rolls) { + //TODO create AI logic for this + return Aggregates.random(rolls); + } + + @Override + public RollDiceEffect.DieRollResult chooseRollToSwap(List rolls) { + //TODO create AI logic for this + return Aggregates.random(rolls); + } + + @Override + public String chooseRollSwapValue(List swapChoices, Integer currentResult, int power, int toughness) { + //TODO create AI logic for this + return Aggregates.random(swapChoices); + } + @Override public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) { return !ComputerUtil.wantMulligan(player, cardsToReturn); @@ -1207,6 +1232,11 @@ public class PlayerControllerAi extends PlayerController { return false; } + public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView allPayers) { + // TODO logic for AI to pay rerolls and modification costs + return false; + } + @Override public void orderAndPlaySimultaneousSa(List activePlayerSAs) { for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) { diff --git a/forge-game/src/main/java/forge/game/ability/AbilityKey.java b/forge-game/src/main/java/forge/game/ability/AbilityKey.java index f70f13574cb..3771456ae85 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityKey.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityKey.java @@ -61,6 +61,7 @@ public enum AbilityKey { DefendingPlayer("DefendingPlayer"), Destination("Destination"), Devoured("Devoured"), + DicePTExchanges("DicePTExchanges"), Discard("Discard"), DiscardedBefore("DiscardedBefore"), DividedShieldAmount("DividedShieldAmount"), @@ -94,6 +95,7 @@ public enum AbilityKey { Mode("Mode"), Modifier("Modifier"), MonstrosityAmount("MonstrosityAmount"), + NaturalResult("NaturalResult"), NewCard("NewCard"), NewCounterAmount("NewCounterAmount"), NoPreventDamage("NoPreventDamage"), diff --git a/forge-game/src/main/java/forge/game/ability/effects/ReplaceEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ReplaceEffect.java index 84c937f9a80..3a9bbdb8578 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ReplaceEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ReplaceEffect.java @@ -2,6 +2,7 @@ package forge.game.ability.effects; import java.util.List; import java.util.Map; +import java.util.Set; import forge.game.GameObject; import forge.game.PlanarDice; @@ -48,6 +49,12 @@ public class ReplaceEffect extends SpellAbilityEffect { for (Player key : AbilityUtils.getDefinedPlayers(card, sa.getParam("VarKey"), sa)) { m.put(key, m.getOrDefault(key, 0) + AbilityUtils.calculateAmount(card, varValue, sa)); } + } else if ("CardSet".equals(type)) { + Set cards = (Set) params.get(varName); + List list = AbilityUtils.getDefinedCards(card, varValue, sa); + if (!list.isEmpty()) { + cards.add(list.get(0)); + } } else if (varName != null) { params.put(varName, AbilityUtils.calculateAmount(card, varValue, sa)); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java b/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java index 3c9004d55c7..18766519796 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/RollDiceEffect.java @@ -5,13 +5,16 @@ import com.google.common.collect.Maps; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; 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.player.Player; import forge.game.player.PlayerCollection; +import forge.game.player.PlayerController; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; import forge.game.trigger.TriggerType; +import forge.game.zone.ZoneType; import forge.util.Lang; import forge.util.Localizer; import forge.util.MyRandom; @@ -38,6 +41,59 @@ public class RollDiceEffect extends SpellAbilityEffect { 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 getResultsList(List naturalResults) { + List results = new ArrayList<>(); + for (int r : naturalResults) { + results.add(new DieRollResult(r, r)); + } + return results; + } + + public static List getNaturalResults(List results) { + List naturalResults = new ArrayList<>(); + for (DieRollResult r : results) { + naturalResults.add(r.getNaturalValue()); + } + return naturalResults; + } + + public static List getFinalResults(List results) { + List naturalResults = new ArrayList<>(); + for (DieRollResult r : results) { + naturalResults.add(r.getModifiedValue()); + } + return naturalResults; + } + /* (non-Javadoc) * @see forge.card.abilityfactory.SpellEffect#getStackDescription(java.util.Map, forge.card.spellability.SpellAbility) */ @@ -80,12 +136,277 @@ public class RollDiceEffect extends SpellAbilityEffect { } Map ignoreChosenMap = Maps.newHashMap(); + Set dicePTExchanges = new HashSet<>(); final Map repParams = AbilityKey.mapFromAffected(player); + List ignored = new ArrayList<>(); + List 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 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 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 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 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 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 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 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 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 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 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 rollAction(int amount, int sides, int ignore, List rollsResult, List ignored, Map ignoreChosenMap, Set dicePTExchanges, Player player, Map repParams) { + + repParams.put(AbilityKey.Sides, sides); repParams.put(AbilityKey.Number, amount); repParams.put(AbilityKey.Ignore, ignore); + repParams.put(AbilityKey.DicePTExchanges, dicePTExchanges); repParams.put(AbilityKey.IgnoreChosen, ignoreChosenMap); - switch (player.getGame().getReplacementHandler().run(ReplacementType.RollDice, repParams)) { case NotReplaced: break; @@ -110,7 +431,6 @@ public class RollDiceEffect extends SpellAbilityEffect { naturalRolls.sort(null); - List ignored = new ArrayList<>(); // Ignore lowest rolls if (ignore > 0) { for (int i = ignore - 1; i >= 0; --i) { @@ -127,75 +447,7 @@ public class RollDiceEffect extends SpellAbilityEffect { } } - if (sa != null && sa.hasParam("UseHighestRoll")) { - 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 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 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 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); + return naturalRolls; } private static void resolveSub(SpellAbility sa, int num) { diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index 1c14e10bae6..c44f84e8470 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -12,6 +12,7 @@ import forge.deck.Deck; import forge.deck.DeckSection; import forge.game.*; import forge.game.GameOutcome.AnteResult; +import forge.game.ability.effects.RollDiceEffect; import forge.game.card.*; import forge.game.combat.Combat; import forge.game.cost.Cost; @@ -55,7 +56,8 @@ public abstract class PlayerController { OddsOrEvens, UntapOrLeaveTapped, LeftOrRight, - AddOrRemove + AddOrRemove, + IncreaseOrDecrease } public enum FullControlFlag { @@ -229,6 +231,10 @@ public abstract class PlayerController { public abstract PlanarDice choosePDRollToIgnore(List rolls); public abstract Integer chooseRollToIgnore(List rolls); + public abstract List chooseDiceToReroll(List rolls); + public abstract Integer chooseRollToModify(List rolls); + public abstract RollDiceEffect.DieRollResult chooseRollToSwap(List rolls); + public abstract String chooseRollSwapValue(List swapChoices, Integer currentResult, int power, int toughness); public abstract Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer, boolean optional); @@ -292,6 +298,7 @@ public abstract class PlayerController { public abstract List orderCosts(List costs); public abstract boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView allPayers); + public abstract boolean payCostDuringRoll(Cost cost, SpellAbility sa, FCollectionView allPayers); public abstract boolean payCombatCost(Card card, Cost cost, SpellAbility sa, String prompt); diff --git a/forge-game/src/main/java/forge/game/replacement/ReplaceRollDice.java b/forge-game/src/main/java/forge/game/replacement/ReplaceRollDice.java index 4897ca15fb8..9c51baaca95 100644 --- a/forge-game/src/main/java/forge/game/replacement/ReplaceRollDice.java +++ b/forge-game/src/main/java/forge/game/replacement/ReplaceRollDice.java @@ -26,6 +26,11 @@ public class ReplaceRollDice extends ReplacementEffect { if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Affected))) { return false; } + if (hasParam("ValidSides")) { + if (((Integer) runParams.get(AbilityKey.Sides)) != Integer.parseInt(getParam("ValidSides"))) { + return false; + } + } return true; } @@ -37,5 +42,6 @@ public class ReplaceRollDice extends ReplacementEffect { sa.setReplacingObject(AbilityKey.Number, runParams.get(AbilityKey.Number)); sa.setReplacingObject(AbilityKey.Ignore, runParams.get(AbilityKey.Ignore)); sa.setReplacingObject(AbilityKey.IgnoreChosen, runParams.get(AbilityKey.IgnoreChosen)); + sa.setReplacingObject(AbilityKey.DicePTExchanges, runParams.get(AbilityKey.DicePTExchanges)); } } diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerRolledDie.java b/forge-game/src/main/java/forge/game/trigger/TriggerRolledDie.java index e173ad43dad..a01e8d0c67d 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerRolledDie.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerRolledDie.java @@ -32,7 +32,7 @@ public class TriggerRolledDie extends Trigger { String[] params = getParam("ValidResult").split(","); int result = (int) runParams.get(AbilityKey.Result); if (hasParam("Natural")) { - result -= (int) runParams.get(AbilityKey.Modifier); + result = (int) runParams.get(AbilityKey.NaturalResult); } for (String param : params) { if (StringUtils.isNumeric(param)) { diff --git a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java index 382d0dc8251..f4869226e1b 100644 --- a/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java +++ b/forge-gui-desktop/src/test/java/forge/gamesimulationtests/util/PlayerControllerForTests.java @@ -22,6 +22,7 @@ import forge.deck.Deck; import forge.deck.DeckSection; import forge.game.*; import forge.game.ability.AbilityUtils; +import forge.game.ability.effects.RollDiceEffect; import forge.game.card.*; import forge.game.combat.Combat; 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.Pair; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Predicate; /** @@ -526,6 +524,26 @@ public class PlayerControllerForTests extends PlayerController { return Aggregates.random(rolls); } + @Override + public List chooseDiceToReroll(List rolls) { + return new ArrayList<>(); + } + + @Override + public Integer chooseRollToModify(List rolls) { + return Aggregates.random(rolls); + } + + @Override + public RollDiceEffect.DieRollResult chooseRollToSwap(List rolls) { + return Aggregates.random(rolls); + } + + @Override + public String chooseRollSwapValue(List swapChoices, Integer currentResult, int power, int toughness) { + return Aggregates.random(swapChoices); + } + @Override public Object vote(SpellAbility sa, String prompt, List options, ListMultimap votes, Player forPlayer, boolean optional) { return chooseItem(options); @@ -577,6 +595,12 @@ public class PlayerControllerForTests extends PlayerController { return false; } + @Override + public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView allPayers) { + // TODO Auto-generated method stub + return false; + } + @Override public void orderAndPlaySimultaneousSa(List activePlayerSAs) { for (final SpellAbility sa : activePlayerSAs) { diff --git a/forge-gui/res/cardsfolder/m/monitor_monitor.txt b/forge-gui/res/cardsfolder/m/monitor_monitor.txt new file mode 100644 index 00000000000..54c6666e2eb --- /dev/null +++ b/forge-gui/res/cardsfolder/m/monitor_monitor.txt @@ -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. diff --git a/forge-gui/res/cardsfolder/n/night_shift_of_the_living_dead.txt b/forge-gui/res/cardsfolder/n/night_shift_of_the_living_dead.txt new file mode 100644 index 00000000000..6f588f25227 --- /dev/null +++ b/forge-gui/res/cardsfolder/n/night_shift_of_the_living_dead.txt @@ -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. diff --git a/forge-gui/res/cardsfolder/v/vedalken_squirrel_whacker.txt b/forge-gui/res/cardsfolder/v/vedalken_squirrel_whacker.txt new file mode 100644 index 00000000000..5270a2ade23 --- /dev/null +++ b/forge-gui/res/cardsfolder/v/vedalken_squirrel_whacker.txt @@ -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 creature’s 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 creature’s base power or base toughness. diff --git a/forge-gui/res/cardsfolder/x/xenosquirrels.txt b/forge-gui/res/cardsfolder/x/xenosquirrels.txt new file mode 100644 index 00000000000..c2852a598d9 --- /dev/null +++ b/forge-gui/res/cardsfolder/x/xenosquirrels.txt @@ -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. diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 9d21937f332..4196e6d54d3 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -1418,6 +1418,18 @@ lblAssignSprocketNextTurn=(Cranked next turn) lblChooseCrank=Choose contraptions to crank lblChooseSectorEffect=Choose a sector 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 lblTheRingTempts=The Ring tempts {0} lblTop=Top @@ -2149,6 +2161,7 @@ lblDoYouWantRevealYourHand=Do you want to reveal your hand? #RollDiceEffect.java lblPlayerRolledResult={0} rolled {1} lblIgnoredRolls=Ignored rolls: {0} +lblNaturalRolls=Natural rolls: {0} lblRerollResult=Reroll {0}? lblAttractionRollResult={0} rolled to visit their Attractions. Result: {1}. #RollPlanarDiceEffect.java diff --git a/forge-gui/res/tokenscripts/b_2_2_zombie_employee.txt b/forge-gui/res/tokenscripts/b_2_2_zombie_employee.txt new file mode 100644 index 00000000000..c195e1fb5bc --- /dev/null +++ b/forge-gui/res/tokenscripts/b_2_2_zombie_employee.txt @@ -0,0 +1,6 @@ +Name:Zombie Employee Token +ManaCost:no cost +Colors:black +Types:Creature Zombie Employee +PT:2/2 +Oracle: diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index c289cab268c..46ea67c7522 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -16,6 +16,7 @@ import forge.game.*; import forge.game.ability.AbilityKey; import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; +import forge.game.ability.effects.RollDiceEffect; import forge.game.card.*; import forge.game.card.CardView.CardStateView; 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); } + @Override + public List chooseDiceToReroll(List rolls) { + return getGui().many(Localizer.getInstance().getMessage("lblChooseDiceToRerollTitle"), + Localizer.getInstance().getMessage("lblChooseDiceToRerollCaption"),0, rolls.size(), rolls, null); + } + + @Override + public Integer chooseRollToModify(List rolls) { + return getGui().oneOrNone(Localizer.getInstance().getMessage("lblChooseRollToModify"), rolls); + } + + @Override + public RollDiceEffect.DieRollResult chooseRollToSwap(List rolls) { + return getGui().oneOrNone(Localizer.getInstance().getMessage("lblChooseRollToSwap"), rolls); + } + + @Override + public String chooseRollSwapValue(List swapChoices, Integer currentResult, int power, int toughness) { + return getGui().oneOrNone(Localizer.getInstance().getMessage("lblChooseSwapPT", currentResult, power, toughness), swapChoices); + } + @Override public Object vote(final SpellAbility sa, final String prompt, final List options, final ListMultimap votes, Player forPlayer, boolean optional) { @@ -1687,6 +1709,9 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont case AddOrRemove: labels = ImmutableList.of(localizer.getMessage("lblAddCounter"), localizer.getMessage("lblRemoveCounter")); break; + case IncreaseOrDecrease: + labels = ImmutableList.of(localizer.getMessage("lblIncrease"), localizer.getMessage("lblDecrease")); + break; default: 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); } + @Override + public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView 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 private final Map> orderedSALookup = Maps.newHashMap();