mirror of
https://github.com/magefree/mage.git
synced 2025-12-26 13:32:06 -08:00
Adding dice rolling trigger/replacement (ready for review) (#7989)
* [AFR] Implemented DiceRolledTriggeredAbility * [AFR] Implemented Brazen Dwarf * [AFR] Implemented Feywild Trickster * [AFC] Implemented Reckless Endeavor * [AFR] Implemented Pixie Guide * [AFR] Implemented Critical Hit * [AFR] Implemented Netherese Puzzle Ward * [AFC] Implemented Neverwinter Hydra * [AFR] Implemented Farideh, Devil's Chosen * [AFR] Implemented Barbarian Class * [AFC] Implemented Vrondiss, Rage of Ancients * [AFC] Implemented Arcane Endeavor * Test framework: added planar die rolls support * Test framework: added random results set up support in AI simulated games; * AI: improved roll die results chooses in computer games; * Roll die: improved combo support for planar die and roll die effects; Co-authored-by: Daniel Bomar <dbdaniel42@gmail.com> Co-authored-by: Oleg Agafonov <jaydi85@gmail.com>
This commit is contained in:
parent
12219cff01
commit
f8d030bef4
97 changed files with 2641 additions and 553 deletions
|
|
@ -0,0 +1,63 @@
|
|||
package mage.abilities.common;
|
||||
|
||||
import mage.abilities.TriggeredAbilityImpl;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.DiceRolledEvent;
|
||||
import mage.game.events.GameEvent;
|
||||
|
||||
/**
|
||||
* @author weirddan455
|
||||
*/
|
||||
public class OneOrMoreDiceRolledTriggeredAbility extends TriggeredAbilityImpl {
|
||||
|
||||
public OneOrMoreDiceRolledTriggeredAbility(Effect effect) {
|
||||
this(effect, false);
|
||||
}
|
||||
|
||||
public OneOrMoreDiceRolledTriggeredAbility(Effect effect, boolean optional) {
|
||||
super(Zone.BATTLEFIELD, effect, optional);
|
||||
}
|
||||
|
||||
private OneOrMoreDiceRolledTriggeredAbility(final OneOrMoreDiceRolledTriggeredAbility effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OneOrMoreDiceRolledTriggeredAbility copy() {
|
||||
return new OneOrMoreDiceRolledTriggeredAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.DICE_ROLLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkTrigger(GameEvent event, Game game) {
|
||||
if (!isControlledBy(event.getPlayerId())) {
|
||||
return false;
|
||||
}
|
||||
int maxRoll = ((DiceRolledEvent) event)
|
||||
.getResults()
|
||||
.stream()
|
||||
.filter(Integer.class::isInstance) // only numerical die result can be masured
|
||||
.map(Integer.class::cast)
|
||||
.mapToInt(Integer::intValue)
|
||||
.max()
|
||||
.orElse(0);
|
||||
this.getEffects().setValue("maxDieRoll", maxRoll);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTriggerPhrase() {
|
||||
return "Whenever you roll one or more dice, ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return super.getRule();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
package mage.abilities.effects.common;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.cards.Card;
|
||||
|
|
@ -10,7 +10,6 @@ import mage.game.Game;
|
|||
import mage.players.Player;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class ReturnSourceFromGraveyardToHandEffect extends OneShotEffect {
|
||||
|
|
@ -32,11 +31,9 @@ public class ReturnSourceFromGraveyardToHandEffect extends OneShotEffect {
|
|||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
Card card = controller.getGraveyard().get(source.getSourceId(), game);
|
||||
if (card != null) {
|
||||
return controller.moveCards(card, Zone.HAND, source, game);
|
||||
}
|
||||
return false;
|
||||
MageObject sourceObject = source.getSourceObjectIfItStillExists(game);
|
||||
return controller != null
|
||||
&& sourceObject instanceof Card
|
||||
&& controller.moveCards((Card) sourceObject, Zone.HAND, source, game);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
package mage.abilities.effects.common;
|
||||
|
||||
import mage.MageObject;
|
||||
|
|
@ -12,7 +11,6 @@ import mage.game.Game;
|
|||
import mage.players.Player;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author spjspj
|
||||
*/
|
||||
public class RollDiceEffect extends OneShotEffect {
|
||||
|
|
@ -47,7 +45,7 @@ public class RollDiceEffect extends OneShotEffect {
|
|||
Player controller = game.getPlayer(source.getControllerId());
|
||||
MageObject mageObject = game.getObject(source.getSourceId());
|
||||
if (controller != null && mageObject != null) {
|
||||
controller.rollDice(source, game, numSides);
|
||||
controller.rollDice(outcome, source, game, numSides);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -58,8 +56,7 @@ public class RollDiceEffect extends OneShotEffect {
|
|||
if (!staticText.isEmpty()) {
|
||||
return staticText;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder("Roll a " + numSides + " sided dice");
|
||||
return sb.toString();
|
||||
return "Roll a " + numSides + " sided die";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ public class RollDieWithResultTableEffect extends OneShotEffect {
|
|||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
int result = player.rollDice(source, game, sides) + modifier.calculate(game, source, this);
|
||||
int result = player.rollDice(outcome, source, game, sides) + modifier.calculate(game, source, this);
|
||||
this.applyResult(result, game, source);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import mage.abilities.effects.ContinuousEffect;
|
|||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.PlanarDieRoll;
|
||||
import mage.constants.PlanarDieRollResult;
|
||||
import mage.constants.Planes;
|
||||
import mage.game.Game;
|
||||
import mage.game.command.CommandObject;
|
||||
|
|
@ -61,8 +61,8 @@ public class RollPlanarDieEffect extends OneShotEffect {
|
|||
Player controller = game.getPlayer(source.getControllerId());
|
||||
MageObject mageObject = game.getObject(source.getSourceId());
|
||||
if (controller != null && mageObject != null) {
|
||||
PlanarDieRoll planarRoll = controller.rollPlanarDie(source, game);
|
||||
if (planarRoll == PlanarDieRoll.CHAOS_ROLL && chaosEffects != null && chaosTargets != null) {
|
||||
PlanarDieRollResult planarRoll = controller.rollPlanarDie(outcome, source, game);
|
||||
if (planarRoll == PlanarDieRollResult.CHAOS_ROLL && chaosEffects != null && chaosTargets != null) {
|
||||
for (int i = 0; i < chaosTargets.size(); i++) {
|
||||
Target target = chaosTargets.get(i);
|
||||
if (target != null) {
|
||||
|
|
@ -95,7 +95,7 @@ public class RollPlanarDieEffect extends OneShotEffect {
|
|||
done = true;
|
||||
}
|
||||
}
|
||||
} else if (planarRoll == PlanarDieRoll.PLANAR_ROLL) {
|
||||
} else if (planarRoll == PlanarDieRollResult.PLANAR_ROLL) {
|
||||
// Steps: 1) Remove the last plane and set its effects to discarded
|
||||
for (CommandObject cobject : game.getState().getCommand()) {
|
||||
if (cobject instanceof Plane) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import mage.game.Game;
|
|||
import mage.players.Player;
|
||||
import mage.target.Target;
|
||||
import mage.target.common.TargetCardInHand;
|
||||
import mage.util.CardUtil;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
/**
|
||||
|
|
@ -24,10 +25,14 @@ import org.apache.log4j.Logger;
|
|||
* Allows player to choose to cast as card from hand without paying its mana
|
||||
* cost.
|
||||
* </p>
|
||||
* TODO: this doesn't work correctly with MDFCs or Adventures (see https://github.com/magefree/mage/issues/7742)
|
||||
*/
|
||||
public class CastWithoutPayingManaCostEffect extends OneShotEffect {
|
||||
|
||||
private final DynamicValue manaCost;
|
||||
private final FilterCard filter;
|
||||
private static final FilterCard defaultFilter
|
||||
= new FilterNonlandCard("card with mana value %mv or less from your hand");
|
||||
|
||||
/**
|
||||
* @param maxCost Maximum converted mana cost for this effect to apply to
|
||||
|
|
@ -37,8 +42,13 @@ public class CastWithoutPayingManaCostEffect extends OneShotEffect {
|
|||
}
|
||||
|
||||
public CastWithoutPayingManaCostEffect(DynamicValue maxCost) {
|
||||
this(maxCost, defaultFilter);
|
||||
}
|
||||
|
||||
public CastWithoutPayingManaCostEffect(DynamicValue maxCost, FilterCard filter) {
|
||||
super(Outcome.PlayForFree);
|
||||
this.manaCost = maxCost;
|
||||
this.filter = filter;
|
||||
this.staticText = "you may cast a spell with mana value "
|
||||
+ maxCost + " or less from your hand without paying its mana cost";
|
||||
}
|
||||
|
|
@ -46,50 +56,54 @@ public class CastWithoutPayingManaCostEffect extends OneShotEffect {
|
|||
public CastWithoutPayingManaCostEffect(final CastWithoutPayingManaCostEffect effect) {
|
||||
super(effect);
|
||||
this.manaCost = effect.manaCost;
|
||||
this.filter = effect.filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
|
||||
if (controller == null) {
|
||||
return false;
|
||||
}
|
||||
int cmc = manaCost.calculate(game, source, this);
|
||||
FilterCard filter = new FilterNonlandCard("card with mana value "
|
||||
+ cmc + " or less from your hand");
|
||||
FilterCard filter = this.filter.copy();
|
||||
filter.setMessage(filter.getMessage().replace("%mv", "" + cmc));
|
||||
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, cmc + 1));
|
||||
|
||||
Target target = new TargetCardInHand(filter);
|
||||
if (target.canChoose(source.getSourceId(), controller.getId(), game)
|
||||
&& controller.chooseUse(Outcome.PlayForFree, "Cast a card with mana value " + cmc
|
||||
+ " or less from your hand without paying its mana cost?", source, game)) {
|
||||
Card cardToCast = null;
|
||||
boolean cancel = false;
|
||||
while (controller.canRespond()
|
||||
&& !cancel) {
|
||||
if (controller.chooseTarget(Outcome.PlayForFree, target, source, game)) {
|
||||
cardToCast = game.getCard(target.getFirstTarget());
|
||||
if (cardToCast != null) {
|
||||
if (cardToCast.getSpellAbility() == null) {
|
||||
Logger.getLogger(CastWithoutPayingManaCostEffect.class).fatal("Card: "
|
||||
+ cardToCast.getName() + " is no land and has no spell ability!");
|
||||
cancel = true;
|
||||
}
|
||||
if (cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) {
|
||||
cancel = true;
|
||||
}
|
||||
if (!target.canChoose(
|
||||
source.getSourceId(), controller.getId(), game
|
||||
) || !controller.chooseUse(
|
||||
Outcome.PlayForFree,
|
||||
"Cast " + CardUtil.addArticle(filter.getMessage())
|
||||
+ " without paying its mana cost?", source, game
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
Card cardToCast = null;
|
||||
boolean cancel = false;
|
||||
while (controller.canRespond()
|
||||
&& !cancel) {
|
||||
if (controller.chooseTarget(Outcome.PlayForFree, target, source, game)) {
|
||||
cardToCast = game.getCard(target.getFirstTarget());
|
||||
if (cardToCast != null) {
|
||||
if (cardToCast.getSpellAbility() == null) {
|
||||
Logger.getLogger(CastWithoutPayingManaCostEffect.class).fatal("Card: "
|
||||
+ cardToCast.getName() + " is no land and has no spell ability!");
|
||||
cancel = true;
|
||||
}
|
||||
if (cardToCast.getSpellAbility().canChooseTarget(game, controller.getId())) {
|
||||
cancel = true;
|
||||
}
|
||||
} else {
|
||||
cancel = true;
|
||||
}
|
||||
} else {
|
||||
cancel = true;
|
||||
}
|
||||
if (cardToCast != null) {
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), Boolean.TRUE);
|
||||
controller.cast(controller.chooseAbilityForCast(cardToCast, game, true),
|
||||
game, true, new ApprovingObject(source, game));
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), null);
|
||||
}
|
||||
}
|
||||
if (cardToCast != null) {
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), Boolean.TRUE);
|
||||
controller.cast(controller.chooseAbilityForCast(cardToCast, game, true),
|
||||
game, true, new ApprovingObject(source, game));
|
||||
game.getState().setValue("PlayFromNotOwnHandZone" + cardToCast.getId(), null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,8 +255,10 @@ public class ChoiceImpl implements Choice {
|
|||
for (String needChoice : answers) {
|
||||
for (Map.Entry<String, String> currentChoice : this.getKeyChoices().entrySet()) {
|
||||
if (currentChoice.getKey().equals(needChoice)) {
|
||||
this.setChoiceByKey(needChoice, false);
|
||||
answers.remove(needChoice);
|
||||
if (removeSelectAnswerFromList) {
|
||||
this.setChoiceByKey(needChoice, false);
|
||||
answers.remove(needChoice);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -266,8 +268,10 @@ public class ChoiceImpl implements Choice {
|
|||
for (String needChoice : answers) {
|
||||
for (Map.Entry<String, String> currentChoice : this.getKeyChoices().entrySet()) {
|
||||
if (currentChoice.getValue().startsWith(needChoice)) {
|
||||
this.setChoiceByKey(currentChoice.getKey(), false);
|
||||
answers.remove(needChoice);
|
||||
if (removeSelectAnswerFromList) {
|
||||
this.setChoiceByKey(currentChoice.getKey(), false);
|
||||
answers.remove(needChoice);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -277,8 +281,10 @@ public class ChoiceImpl implements Choice {
|
|||
for (String needChoice : answers) {
|
||||
for (String currentChoice : this.getChoices()) {
|
||||
if (currentChoice.equals(needChoice)) {
|
||||
this.setChoice(needChoice, false);
|
||||
answers.remove(needChoice);
|
||||
if (removeSelectAnswerFromList) {
|
||||
this.setChoice(needChoice, false);
|
||||
answers.remove(needChoice);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
package mage.constants;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author spjspj
|
||||
*/
|
||||
public enum PlanarDieRoll {
|
||||
NIL_ROLL("Blank Roll"),
|
||||
CHAOS_ROLL("Chaos Roll"),
|
||||
PLANAR_ROLL("Planar Roll");
|
||||
|
||||
private final String text;
|
||||
|
||||
PlanarDieRoll(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
||||
29
Mage/src/main/java/mage/constants/PlanarDieRollResult.java
Normal file
29
Mage/src/main/java/mage/constants/PlanarDieRollResult.java
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package mage.constants;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author spjspj
|
||||
*/
|
||||
public enum PlanarDieRollResult {
|
||||
|
||||
BLANK_ROLL("Blank Roll", 0),
|
||||
CHAOS_ROLL("Chaos Roll", 2),
|
||||
PLANAR_ROLL("Planar Roll", 1);
|
||||
|
||||
private final String text;
|
||||
private final int aiPriority; // priority for AI usage (0 - lower, 2 - higher)
|
||||
|
||||
PlanarDieRollResult(String text, int aiPriority) {
|
||||
this.text = text;
|
||||
this.aiPriority = aiPriority;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public int getAIPriority() {
|
||||
return aiPriority;
|
||||
}
|
||||
}
|
||||
11
Mage/src/main/java/mage/constants/RollDieType.java
Normal file
11
Mage/src/main/java/mage/constants/RollDieType.java
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package mage.constants;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public enum RollDieType {
|
||||
|
||||
NUMERICAL,
|
||||
PLANAR
|
||||
|
||||
}
|
||||
|
|
@ -52,10 +52,14 @@ public class GameOptions implements Serializable, Copyable<GameOptions> {
|
|||
*/
|
||||
public Set<String> bannedUsers = Collections.emptySet();
|
||||
|
||||
/**
|
||||
* Use planechase variant
|
||||
*/
|
||||
|
||||
// PLANECHASE game mode
|
||||
public boolean planeChase = false;
|
||||
// xmage uses increased by 1/3 chances (2/2/9) for chaos/planar result, see 1a9f12f5767ce0beeed26a8ff5c8a8f9490c9c47
|
||||
// if you need combo support with 6-sides rolls then it can be reset to original values
|
||||
public static final int PLANECHASE_PLANAR_DIE_CHAOS_SIDES = 2; // original: 1
|
||||
public static final int PLANECHASE_PLANAR_DIE_PLANAR_SIDES = 2; // original: 1
|
||||
public static final int PLANECHASE_PLANAR_DIE_TOTAL_SIDES = 9; // original: 6
|
||||
|
||||
public GameOptions() {
|
||||
super();
|
||||
|
|
|
|||
29
Mage/src/main/java/mage/game/events/DiceRolledEvent.java
Normal file
29
Mage/src/main/java/mage/game/events/DiceRolledEvent.java
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package mage.game.events;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class DiceRolledEvent extends GameEvent {
|
||||
|
||||
private final int sides;
|
||||
private final List<Object> results = new ArrayList<>(); // Integer for numerical and PlanarDieRollResult for planar
|
||||
|
||||
public DiceRolledEvent(int sides, List<Object> results, Ability source) {
|
||||
super(EventType.DICE_ROLLED, source.getControllerId(), source, source.getControllerId());
|
||||
this.sides = sides;
|
||||
this.results.addAll(results);
|
||||
}
|
||||
|
||||
public int getSides() {
|
||||
return sides;
|
||||
}
|
||||
|
||||
public List<Object> getResults() {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
50
Mage/src/main/java/mage/game/events/DieRolledEvent.java
Normal file
50
Mage/src/main/java/mage/game/events/DieRolledEvent.java
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package mage.game.events;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.PlanarDieRollResult;
|
||||
import mage.constants.RollDieType;
|
||||
|
||||
/**
|
||||
* @author TheElk801, JayDi85
|
||||
*/
|
||||
public class DieRolledEvent extends GameEvent {
|
||||
|
||||
// 706.2.
|
||||
// After the roll, the number indicated on the top face of the die before any modifiers is
|
||||
// the natural result. The instruction may include modifiers to the roll which add to or
|
||||
// subtract from the natural result. Modifiers may also come from other sources. After
|
||||
// considering all applicable modifiers, the final number is the result of the die roll.
|
||||
|
||||
private final RollDieType rollDieType;
|
||||
private final int sides;
|
||||
private final int naturalResult; // planar die returns 0 values in result and natural result
|
||||
private final PlanarDieRollResult planarResult;
|
||||
|
||||
public DieRolledEvent(Ability source, RollDieType rollDieType, int sides, int naturalResult, int modifier, PlanarDieRollResult planarResult) {
|
||||
super(EventType.DIE_ROLLED, source.getControllerId(), source, source.getControllerId(), naturalResult + modifier, false);
|
||||
this.rollDieType = rollDieType;
|
||||
this.sides = sides;
|
||||
this.naturalResult = naturalResult;
|
||||
this.planarResult = planarResult;
|
||||
}
|
||||
|
||||
public RollDieType getRollDieType() {
|
||||
return rollDieType;
|
||||
}
|
||||
|
||||
public int getSides() {
|
||||
return sides;
|
||||
}
|
||||
|
||||
public int getResult() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public int getNaturalResult() {
|
||||
return naturalResult;
|
||||
}
|
||||
|
||||
public PlanarDieRollResult getPlanarResult() {
|
||||
return planarResult;
|
||||
}
|
||||
}
|
||||
|
|
@ -296,8 +296,9 @@ public class GameEvent implements Serializable {
|
|||
SURVEIL, SURVEILED,
|
||||
FATESEALED,
|
||||
FLIP_COIN, COIN_FLIPPED,
|
||||
REPLACE_ROLLED_DIE, // for Clam-I-Am workaround only
|
||||
ROLL_DIE, DIE_ROLLED,
|
||||
ROLL_DICE, DICE_ROLLED,
|
||||
ROLL_PLANAR_DIE, PLANAR_DIE_ROLLED,
|
||||
PLANESWALK, PLANESWALKED,
|
||||
PAID_CUMULATIVE_UPKEEP,
|
||||
DIDNT_PAY_CUMULATIVE_UPKEEP,
|
||||
|
|
@ -621,7 +622,7 @@ public class GameEvent implements Serializable {
|
|||
|
||||
/**
|
||||
* used to store which replacement effects were already applied to an event
|
||||
* or or any modified events that may replace it
|
||||
* or any modified events that may replace it
|
||||
* <p>
|
||||
* 614.5. A replacement effect doesn't invoke itself repeatedly; it gets
|
||||
* only one opportunity to affect an event or any modified events that may
|
||||
|
|
|
|||
41
Mage/src/main/java/mage/game/events/RollDiceEvent.java
Normal file
41
Mage/src/main/java/mage/game/events/RollDiceEvent.java
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package mage.game.events;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.RollDieType;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class RollDiceEvent extends GameEvent {
|
||||
|
||||
private final int sides;
|
||||
private int ignoreLowestAmount = 0; // ignore the lowest results
|
||||
private final RollDieType rollDieType;
|
||||
|
||||
public RollDiceEvent(Ability source, RollDieType rollDieType, int sides, int rollsAmount) {
|
||||
super(EventType.ROLL_DICE, source.getControllerId(), source, source.getControllerId(), rollsAmount, false);
|
||||
this.sides = sides;
|
||||
this.rollDieType = rollDieType;
|
||||
}
|
||||
|
||||
public int getSides() {
|
||||
return sides;
|
||||
}
|
||||
|
||||
public RollDieType getRollDieType() {
|
||||
return rollDieType;
|
||||
}
|
||||
|
||||
public void incAmount(int additionalAmount) {
|
||||
this.amount = CardUtil.overflowInc(this.amount, additionalAmount);
|
||||
}
|
||||
|
||||
public void incIgnoreLowestAmount(int additionalCount) {
|
||||
this.ignoreLowestAmount = CardUtil.overflowInc(this.ignoreLowestAmount, additionalCount);
|
||||
}
|
||||
|
||||
public int getIgnoreLowestAmount() {
|
||||
return ignoreLowestAmount;
|
||||
}
|
||||
}
|
||||
56
Mage/src/main/java/mage/game/events/RollDieEvent.java
Normal file
56
Mage/src/main/java/mage/game/events/RollDieEvent.java
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package mage.game.events;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.constants.RollDieType;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class RollDieEvent extends GameEvent {
|
||||
|
||||
private final RollDieType rollDieType;
|
||||
private final int sides;
|
||||
|
||||
private int resultModifier = 0;
|
||||
private int rollsAmount = 1; // rolls X times and choose result from it
|
||||
private int bigIdeaRollsAmount = 0; // rolls 2x and sum result
|
||||
|
||||
public RollDieEvent(Ability source, RollDieType rollDieType, int sides) {
|
||||
super(EventType.ROLL_DIE, source.getControllerId(), source, source.getControllerId());
|
||||
this.rollDieType = rollDieType;
|
||||
this.sides = sides;
|
||||
}
|
||||
|
||||
public int getResultModifier() {
|
||||
return resultModifier;
|
||||
}
|
||||
|
||||
public void incResultModifier(int modifier) {
|
||||
this.resultModifier = CardUtil.overflowInc(this.resultModifier, modifier);
|
||||
}
|
||||
|
||||
public RollDieType getRollDieType() {
|
||||
return rollDieType;
|
||||
}
|
||||
|
||||
public int getSides() {
|
||||
return sides;
|
||||
}
|
||||
|
||||
public int getRollsAmount() {
|
||||
return rollsAmount;
|
||||
}
|
||||
|
||||
public void doubleRollsAmount() {
|
||||
this.rollsAmount = CardUtil.overflowMultiply(this.rollsAmount, 2);
|
||||
}
|
||||
|
||||
public int getBigIdeaRollsAmount() {
|
||||
return bigIdeaRollsAmount;
|
||||
}
|
||||
|
||||
public void incBigIdeaRollsAmount() {
|
||||
this.bigIdeaRollsAmount = CardUtil.overflowInc(this.bigIdeaRollsAmount, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package mage.game.permanent.token;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.keyword.FlyingAbility;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author weirddan455
|
||||
*/
|
||||
public class FaerieDragonToken extends TokenImpl {
|
||||
|
||||
public FaerieDragonToken() {
|
||||
super("Faerie Dragon", "1/1 blue Faerie Dragon creature token with flying");
|
||||
cardType.add(CardType.CREATURE);
|
||||
color.setBlue(true);
|
||||
subtype.add(SubType.FAERIE);
|
||||
subtype.add(SubType.DRAGON);
|
||||
power = new MageInt(1);
|
||||
toughness = new MageInt(1);
|
||||
this.addAbility(FlyingAbility.getInstance());
|
||||
}
|
||||
|
||||
private FaerieDragonToken(final FaerieDragonToken token) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FaerieDragonToken copy() {
|
||||
return new FaerieDragonToken(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package mage.game.permanent.token;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.TriggeredAbilityImpl;
|
||||
import mage.abilities.effects.common.SacrificeSourceEffect;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
|
||||
public final class VrondissRageOfAncientsToken extends TokenImpl {
|
||||
|
||||
public VrondissRageOfAncientsToken() {
|
||||
super("Dragon Spirit", "5/4 red and green Dragon Spirit creature token with \"When this creature deals damage, sacrifice it.\"");
|
||||
cardType.add(CardType.CREATURE);
|
||||
color.setRed(true);
|
||||
color.setGreen(true);
|
||||
subtype.add(SubType.DRAGON);
|
||||
subtype.add(SubType.SPIRIT);
|
||||
power = new MageInt(5);
|
||||
toughness = new MageInt(4);
|
||||
this.addAbility(new VrondissRageOfAncientsTokenTriggeredAbility());
|
||||
}
|
||||
|
||||
public VrondissRageOfAncientsToken(final VrondissRageOfAncientsToken token) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
public VrondissRageOfAncientsToken copy() {
|
||||
return new VrondissRageOfAncientsToken(this);
|
||||
}
|
||||
}
|
||||
|
||||
class VrondissRageOfAncientsTokenTriggeredAbility extends TriggeredAbilityImpl {
|
||||
|
||||
public VrondissRageOfAncientsTokenTriggeredAbility() {
|
||||
super(Zone.BATTLEFIELD, new SacrificeSourceEffect(), false);
|
||||
}
|
||||
|
||||
public VrondissRageOfAncientsTokenTriggeredAbility(final VrondissRageOfAncientsTokenTriggeredAbility ability) {
|
||||
super(ability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VrondissRageOfAncientsTokenTriggeredAbility copy() {
|
||||
return new VrondissRageOfAncientsTokenTriggeredAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.DAMAGED_PLAYER
|
||||
|| event.getType() == GameEvent.EventType.DAMAGED_PERMANENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkTrigger(GameEvent event, Game game) {
|
||||
return event.getSourceId().equals(this.getSourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return "When this creature deals damage, sacrifice it.";
|
||||
}
|
||||
}
|
||||
|
|
@ -24,10 +24,7 @@ import mage.designations.DesignationType;
|
|||
import mage.filter.FilterCard;
|
||||
import mage.filter.FilterMana;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.game.Game;
|
||||
import mage.game.GameState;
|
||||
import mage.game.Graveyard;
|
||||
import mage.game.Table;
|
||||
import mage.game.*;
|
||||
import mage.game.combat.CombatGroup;
|
||||
import mage.game.draft.Draft;
|
||||
import mage.game.events.GameEvent;
|
||||
|
|
@ -487,19 +484,21 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
|
||||
boolean flipCoin(Ability source, Game game, boolean winnable);
|
||||
|
||||
boolean flipCoin(Ability source, Game game, boolean winnable, List<UUID> appliedEffects);
|
||||
|
||||
boolean flipCoinResult(Game game);
|
||||
|
||||
int rollDice(Ability source, Game game, int numSides);
|
||||
default int rollDice(Outcome outcome, Ability source, Game game, int numSides) {
|
||||
return rollDice(outcome, source, game, numSides, 1, 0).stream().findFirst().orElse(0);
|
||||
}
|
||||
|
||||
int rollDice(Ability source, Game game, List<UUID> appliedEffects, int numSides);
|
||||
List<Integer> rollDice(Outcome outcome, Ability source, Game game, int numSides, int numDice, int ignoreLowestAmount);
|
||||
|
||||
PlanarDieRoll rollPlanarDie(Ability source, Game game);
|
||||
int rollDieResult(int sides, Game game);
|
||||
|
||||
PlanarDieRoll rollPlanarDie(Ability source, Game game, List<UUID> appliedEffects);
|
||||
default PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game) {
|
||||
return rollPlanarDie(outcome, source, game, GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES, GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES);
|
||||
}
|
||||
|
||||
PlanarDieRoll rollPlanarDie(Ability source, Game game, List<UUID> appliedEffects, int numberChaosSides, int numberPlanarSides);
|
||||
PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int numberChaosSides, int numberPlanarSides);
|
||||
|
||||
Card discardOne(boolean random, boolean payForCost, Ability source, Game game);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import mage.abilities.mana.ManaOptions;
|
|||
import mage.actions.MageDrawAction;
|
||||
import mage.cards.*;
|
||||
import mage.cards.decks.Deck;
|
||||
import mage.choices.Choice;
|
||||
import mage.choices.ChoiceImpl;
|
||||
import mage.constants.*;
|
||||
import mage.counters.Counter;
|
||||
import mage.counters.CounterType;
|
||||
|
|
@ -2438,7 +2440,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
userData.resetRequestedHandPlayersList(game.getId()); // users can send request again
|
||||
break;
|
||||
}
|
||||
logger.trace("PASS Priority: " + playerAction.toString());
|
||||
logger.trace("PASS Priority: " + playerAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -2786,21 +2788,15 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
return casted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean flipCoin(Ability source, Game game, boolean winnable) {
|
||||
return this.flipCoin(source, game, winnable, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source
|
||||
* @param game
|
||||
* @param winnable
|
||||
* @param appliedEffects
|
||||
* @return if winnable, true if player won the toss, if not winnable, true
|
||||
* for heads and false for tails
|
||||
*/
|
||||
@Override
|
||||
public boolean flipCoin(Ability source, Game game, boolean winnable, List<UUID> appliedEffects) {
|
||||
public boolean flipCoin(Ability source, Game game, boolean winnable) {
|
||||
boolean chosen = false;
|
||||
if (winnable) {
|
||||
chosen = this.chooseUse(Outcome.Benefit, "Heads or tails?", "", "Heads", "Tails", source, game);
|
||||
|
|
@ -2808,7 +2804,6 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
boolean result = this.flipCoinResult(game);
|
||||
FlipCoinEvent event = new FlipCoinEvent(playerId, source, result, chosen, winnable);
|
||||
event.addAppliedEffects(appliedEffects);
|
||||
game.replaceEvent(event);
|
||||
game.informPlayers(getLogName() + " flipped " + CardUtil.booleanToFlipName(event.getResult())
|
||||
+ CardUtil.getSourceLogName(game, source));
|
||||
|
|
@ -2835,7 +2830,6 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
game.informPlayers(getLogName() + " " + (event.getResult() == event.getChosen() ? "won" : "lost") + " the flip"
|
||||
+ CardUtil.getSourceLogName(game, source));
|
||||
}
|
||||
event.setAppliedEffects(appliedEffects);
|
||||
game.fireEvent(event.createFlippedEvent());
|
||||
if (event.isWinnable()) {
|
||||
return event.getResult() == event.getChosen();
|
||||
|
|
@ -2853,85 +2847,334 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
return RandomUtil.nextBoolean();
|
||||
}
|
||||
|
||||
private static final class RollDieResult {
|
||||
|
||||
// 706.2.
|
||||
// After the roll, the number indicated on the top face of the die before any modifiers is
|
||||
// the natural result. The instruction may include modifiers to the roll which add to or
|
||||
// subtract from the natural result. Modifiers may also come from other sources. After
|
||||
// considering all applicable modifiers, the final number is the result of the die roll.
|
||||
private final int naturalResult;
|
||||
private final int modifier;
|
||||
private final PlanarDieRollResult planarResult;
|
||||
|
||||
RollDieResult(int naturalResult, int modifier, PlanarDieRollResult planarResult) {
|
||||
this.naturalResult = naturalResult;
|
||||
this.modifier = modifier;
|
||||
this.planarResult = planarResult;
|
||||
}
|
||||
|
||||
public int getResult() {
|
||||
return this.naturalResult + this.modifier;
|
||||
}
|
||||
|
||||
public PlanarDieRollResult getPlanarResult() {
|
||||
return this.planarResult;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int rollDice(Ability source, Game game, int numSides) {
|
||||
return this.rollDice(source, game, null, numSides);
|
||||
public int rollDieResult(int sides, Game game) {
|
||||
return RandomUtil.nextInt(sides) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll single die. Support both die types: planar and numerical.
|
||||
*
|
||||
* @param outcome
|
||||
* @param game
|
||||
* @param source
|
||||
* @param rollDieType
|
||||
* @param sidesAmount
|
||||
* @param chaosSidesAmount
|
||||
* @param planarSidesAmount
|
||||
* @param rollsAmount
|
||||
* @return
|
||||
*/
|
||||
private Object rollDieInner(Outcome outcome, Game game, Ability source, RollDieType rollDieType,
|
||||
int sidesAmount, int chaosSidesAmount, int planarSidesAmount, int rollsAmount) {
|
||||
if (rollsAmount == 1) {
|
||||
return rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount);
|
||||
}
|
||||
Set<Object> choices = new HashSet<>();
|
||||
for (int j = 0; j < rollsAmount; j++) {
|
||||
choices.add(rollDieInnerWithReplacement(game, source, rollDieType, sidesAmount, chaosSidesAmount, planarSidesAmount));
|
||||
}
|
||||
if (choices.size() == 1) {
|
||||
return choices.stream().findFirst().orElse(0);
|
||||
}
|
||||
|
||||
// AI hint - use max/min values
|
||||
if (this.isComputer()) {
|
||||
if (rollDieType == RollDieType.NUMERICAL) {
|
||||
// numerical
|
||||
if (outcome.isGood()) {
|
||||
return choices.stream()
|
||||
.map(Integer.class::cast)
|
||||
.max(Comparator.naturalOrder())
|
||||
.orElse(null);
|
||||
} else {
|
||||
return choices.stream()
|
||||
.map(Integer.class::cast)
|
||||
.min(Comparator.naturalOrder())
|
||||
.orElse(null);
|
||||
}
|
||||
} else {
|
||||
// planar
|
||||
// priority: chaos -> planar -> blank
|
||||
return choices.stream()
|
||||
.map(PlanarDieRollResult.class::cast)
|
||||
.max(Comparator.comparingInt(PlanarDieRollResult::getAIPriority))
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
Choice choice = new ChoiceImpl(true);
|
||||
choice.setMessage("Choose which die roll result to keep (the rest will be ignored)");
|
||||
choice.setChoices(choices.stream().sorted().map(Object::toString).collect(Collectors.toSet()));
|
||||
|
||||
this.choose(Outcome.Neutral, choice, game);
|
||||
Object defaultChoice = choices.iterator().next();
|
||||
return choices.stream()
|
||||
.filter(o -> o.toString().equals(choice.getChoice()))
|
||||
.findFirst()
|
||||
.orElse(defaultChoice);
|
||||
}
|
||||
|
||||
private Object rollDieInnerWithReplacement(Game game, Ability source, RollDieType rollDieType, int numSides, int numChaosSides, int numPlanarSides) {
|
||||
switch (rollDieType) {
|
||||
|
||||
case NUMERICAL: {
|
||||
int result = rollDieResult(numSides, game);
|
||||
// Clam-I-Am workaround:
|
||||
// If you roll a 3 on a six-sided die, you may reroll that die.
|
||||
if (numSides == 6
|
||||
&& result == 3
|
||||
&& game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.REPLACE_ROLLED_DIE, source.getControllerId(), source, source.getControllerId()))
|
||||
&& chooseUse(Outcome.Neutral, "Re-roll the 3?", source, game)) {
|
||||
result = rollDieResult(numSides, game);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
case PLANAR: {
|
||||
if (numChaosSides + numPlanarSides > numSides) {
|
||||
numChaosSides = GameOptions.PLANECHASE_PLANAR_DIE_CHAOS_SIDES;
|
||||
numPlanarSides = GameOptions.PLANECHASE_PLANAR_DIE_PLANAR_SIDES;
|
||||
}
|
||||
// for 9 sides:
|
||||
// 1..2 - chaos
|
||||
// 3..7 - blank
|
||||
// 8..9 - planar
|
||||
int result = this.rollDieResult(numSides, game);
|
||||
PlanarDieRollResult roll;
|
||||
if (result <= numChaosSides) {
|
||||
roll = PlanarDieRollResult.CHAOS_ROLL;
|
||||
} else if (result > numSides - numPlanarSides) {
|
||||
roll = PlanarDieRollResult.PLANAR_ROLL;
|
||||
} else {
|
||||
roll = PlanarDieRollResult.BLANK_ROLL;
|
||||
}
|
||||
return roll;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new IllegalArgumentException("Unknown roll die type " + rollDieType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param outcome
|
||||
* @param source
|
||||
* @param game
|
||||
* @param sidesAmount number of sides the dice has
|
||||
* @param rollsAmount number of tries to roll the dice
|
||||
* @param ignoreLowestAmount remove the lowest rolls from the results
|
||||
* @return the number that the player rolled
|
||||
*/
|
||||
@Override
|
||||
public List<Integer> rollDice(Outcome outcome, Ability source, Game game, int sidesAmount, int rollsAmount, int ignoreLowestAmount) {
|
||||
return rollDiceInner(outcome, source, game, RollDieType.NUMERICAL, sidesAmount, 0, 0, rollsAmount, ignoreLowestAmount)
|
||||
.stream()
|
||||
.map(Integer.class::cast)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner code to roll a dice. Support normal and planar types.
|
||||
*
|
||||
* @param outcome
|
||||
* @param source
|
||||
* @param game
|
||||
* @param rollDieType die type to roll, e.g. planar or numerical
|
||||
* @param sidesAmount sides per die
|
||||
* @param chaosSidesAmount for planar die: chaos sides
|
||||
* @param planarSidesAmount for planar die: planar sides
|
||||
* @param rollsAmount rolls
|
||||
* @param ignoreLowestAmount for numerical die: ignore multiple rolls with the lowest values
|
||||
* @return
|
||||
*/
|
||||
private List<Object> rollDiceInner(Outcome outcome, Ability source, Game game, RollDieType rollDieType,
|
||||
int sidesAmount, int chaosSidesAmount, int planarSidesAmount,
|
||||
int rollsAmount, int ignoreLowestAmount) {
|
||||
RollDiceEvent rollDiceEvent = new RollDiceEvent(source, rollDieType, sidesAmount, rollsAmount);
|
||||
if (ignoreLowestAmount > 0) {
|
||||
rollDiceEvent.incIgnoreLowestAmount(ignoreLowestAmount);
|
||||
}
|
||||
game.replaceEvent(rollDiceEvent);
|
||||
|
||||
// 706.6.
|
||||
// In a Planechase game, rolling the planar die will cause any ability that triggers whenever a
|
||||
// player rolls one or more dice to trigger. However, any effect that refers to a numerical
|
||||
// result of a die roll, including ones that compare the results of that roll to other rolls
|
||||
// or to a given number, ignores the rolling of the planar die. See rule 901, “Planechase.”
|
||||
|
||||
// ROLL MULTIPLE dies
|
||||
// results amount can be less than a rolls amount (example: The Big Idea allows rolling 2x instead 1x)
|
||||
List<Object> dieResults = new ArrayList<>();
|
||||
List<RollDieResult> dieRolls = new ArrayList<>();
|
||||
for (int i = 0; i < rollDiceEvent.getAmount(); i++) {
|
||||
// ROLL SINGLE die
|
||||
RollDieEvent rollDieEvent = new RollDieEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides());
|
||||
game.replaceEvent(rollDieEvent);
|
||||
|
||||
Object rollResult;
|
||||
// big idea logic for numerical rolls only
|
||||
if (rollDieEvent.getRollDieType() == RollDieType.NUMERICAL && rollDieEvent.getBigIdeaRollsAmount() > 0) {
|
||||
// rolls 2x + sum results
|
||||
// The Big Idea: roll two six-sided dice and use the total of those results
|
||||
int totalSum = 0;
|
||||
for (int j = 0; j < rollDieEvent.getBigIdeaRollsAmount() + 1; j++) {
|
||||
int singleResult = (Integer) rollDieInner(
|
||||
outcome,
|
||||
game,
|
||||
source,
|
||||
rollDieEvent.getRollDieType(),
|
||||
rollDieEvent.getSides(),
|
||||
chaosSidesAmount,
|
||||
planarSidesAmount,
|
||||
rollDieEvent.getRollsAmount());
|
||||
totalSum += singleResult;
|
||||
dieRolls.add(new RollDieResult(singleResult, rollDieEvent.getResultModifier(), null));
|
||||
}
|
||||
rollResult = totalSum;
|
||||
} else {
|
||||
// rolls 1x
|
||||
switch (rollDieEvent.getRollDieType()) {
|
||||
default:
|
||||
case NUMERICAL: {
|
||||
int naturalResult = (Integer) rollDieInner(
|
||||
outcome,
|
||||
game,
|
||||
source,
|
||||
rollDieEvent.getRollDieType(),
|
||||
rollDieEvent.getSides(),
|
||||
chaosSidesAmount,
|
||||
planarSidesAmount,
|
||||
rollDieEvent.getRollsAmount()
|
||||
);
|
||||
dieRolls.add(new RollDieResult(naturalResult, rollDieEvent.getResultModifier(), null));
|
||||
rollResult = naturalResult;
|
||||
break;
|
||||
}
|
||||
|
||||
case PLANAR: {
|
||||
PlanarDieRollResult planarResult = (PlanarDieRollResult) rollDieInner(
|
||||
outcome,
|
||||
game,
|
||||
source,
|
||||
rollDieEvent.getRollDieType(),
|
||||
rollDieEvent.getSides(),
|
||||
chaosSidesAmount,
|
||||
planarSidesAmount,
|
||||
rollDieEvent.getRollsAmount()
|
||||
);
|
||||
dieRolls.add(new RollDieResult(0, 0, planarResult));
|
||||
rollResult = planarResult;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
dieResults.add(rollResult);
|
||||
}
|
||||
|
||||
// ignore the lowest results
|
||||
// planar dies: due to 706.6. planar die results must be fully ignored
|
||||
//
|
||||
// 706.5.
|
||||
// If a player is instructed to roll two or more dice and ignore the lowest roll, the roll
|
||||
// that yielded the lowest result is considered to have never happened. No abilities trigger
|
||||
// because of the ignored roll, and no effects apply to that roll. If multiple results are tied
|
||||
// for the lowest, the player chooses one of those rolls to be ignored.
|
||||
if (rollDiceEvent.getRollDieType() == RollDieType.NUMERICAL && rollDiceEvent.getIgnoreLowestAmount() > 0) {
|
||||
// find ignored values
|
||||
List<Integer> ignoredResults = new ArrayList<>();
|
||||
for (int i = 0; i < rollDiceEvent.getIgnoreLowestAmount(); i++) {
|
||||
int min = dieResults.stream().map(Integer.class::cast).mapToInt(Integer::intValue).min().orElse(0);
|
||||
dieResults.remove(Integer.valueOf(min));
|
||||
ignoredResults.add(min);
|
||||
}
|
||||
// remove ignored rolls (they not exist anymore)
|
||||
List<RollDieResult> newRolls = new ArrayList<>();
|
||||
for (RollDieResult rollDieResult : dieRolls) {
|
||||
if (ignoredResults.contains(rollDieResult.getResult())) {
|
||||
ignoredResults.remove((Integer) rollDieResult.getResult());
|
||||
} else {
|
||||
newRolls.add(rollDieResult);
|
||||
}
|
||||
}
|
||||
dieRolls.clear();
|
||||
dieRolls.addAll(newRolls);
|
||||
}
|
||||
|
||||
// raise affected roll events
|
||||
for (RollDieResult result : dieRolls) {
|
||||
game.fireEvent(new DieRolledEvent(source, rollDiceEvent.getRollDieType(), rollDiceEvent.getSides(), result.naturalResult, result.modifier, result.planarResult));
|
||||
}
|
||||
game.fireEvent(new DiceRolledEvent(rollDiceEvent.getSides(), dieResults, source));
|
||||
|
||||
String message;
|
||||
switch (rollDiceEvent.getRollDieType()) {
|
||||
default:
|
||||
case NUMERICAL:
|
||||
// [Roll a die] user rolled 2x d6 and got [1, 4] (source: xxx)
|
||||
message = String.format("[Roll a die] %s rolled %s %s and got [%s]%s",
|
||||
getLogName(),
|
||||
(dieResults.size() > 1 ? dieResults.size() + "x" : "a"),
|
||||
"d" + rollDiceEvent.getSides(),
|
||||
dieResults.stream().map(Object::toString).collect(Collectors.joining(", ")),
|
||||
CardUtil.getSourceLogName(game, source));
|
||||
break;
|
||||
case PLANAR:
|
||||
// [Roll a planar die] user rolled CHAOS (source: xxx)
|
||||
message = String.format("[Roll a planar die] %s rolled [%s]%s",
|
||||
getLogName(),
|
||||
dieResults.stream().map(Object::toString).collect(Collectors.joining(", ")),
|
||||
CardUtil.getSourceLogName(game, source));
|
||||
break;
|
||||
}
|
||||
game.informPlayers(message);
|
||||
return dieResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source
|
||||
* @param game
|
||||
* @param appliedEffects
|
||||
* @param numSides Number of sides the dice has
|
||||
* @return the number that the player rolled
|
||||
*/
|
||||
@Override
|
||||
public int rollDice(Ability source, Game game, List<UUID> appliedEffects, int numSides) {
|
||||
int result = RandomUtil.nextInt(numSides) + 1;
|
||||
if (!game.isSimulation()) {
|
||||
game.informPlayers("[Roll a die] " + getLogName() + " rolled a "
|
||||
+ result + " on a " + numSides + " sided die" + CardUtil.getSourceLogName(game, source));
|
||||
}
|
||||
GameEvent event = new GameEvent(GameEvent.EventType.ROLL_DICE, playerId, source, playerId, result, true);
|
||||
event.setAppliedEffects(appliedEffects);
|
||||
event.setAmount(result);
|
||||
event.setData(numSides + "");
|
||||
if (!game.replaceEvent(event)) {
|
||||
GameEvent ge = new GameEvent(GameEvent.EventType.DICE_ROLLED, playerId, source, playerId, event.getAmount(), event.getFlag());
|
||||
ge.setData(numSides + "");
|
||||
game.fireEvent(ge);
|
||||
}
|
||||
return event.getAmount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlanarDieRoll rollPlanarDie(Ability source, Game game) {
|
||||
return this.rollPlanarDie(source, game, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlanarDieRoll rollPlanarDie(Ability source, Game game, List<UUID> appliedEffects) {
|
||||
return rollPlanarDie(source, game, appliedEffects, 2, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param game
|
||||
* @param appliedEffects
|
||||
* @param numberChaosSides The number of chaos sides the planar die
|
||||
* @param chaosSidesAmount The number of chaos sides the planar die
|
||||
* currently has (normally 1 but can be 5)
|
||||
* @param numberPlanarSides The number of chaos sides the planar die
|
||||
* @param planarSidesAmount The number of chaos sides the planar die
|
||||
* currently has (normally 1)
|
||||
* @return the outcome that the player rolled. Either ChaosRoll, PlanarRoll
|
||||
* or NilRoll
|
||||
* or BlankRoll
|
||||
*/
|
||||
@Override
|
||||
public PlanarDieRoll rollPlanarDie(Ability source, Game game, List<UUID> appliedEffects, int numberChaosSides, int numberPlanarSides) {
|
||||
int result = RandomUtil.nextInt(9) + 1;
|
||||
PlanarDieRoll roll = PlanarDieRoll.NIL_ROLL;
|
||||
if (numberChaosSides + numberPlanarSides > 9) {
|
||||
numberChaosSides = 2;
|
||||
numberPlanarSides = 2;
|
||||
}
|
||||
if (result <= numberChaosSides) {
|
||||
roll = PlanarDieRoll.CHAOS_ROLL;
|
||||
} else if (result > 9 - numberPlanarSides) {
|
||||
roll = PlanarDieRoll.PLANAR_ROLL;
|
||||
}
|
||||
if (!game.isSimulation()) {
|
||||
game.informPlayers("[Roll the planar die] " + getLogName()
|
||||
+ " rolled a " + roll + " on the planar die" + CardUtil.getSourceLogName(game, source));
|
||||
}
|
||||
GameEvent event = new GameEvent(GameEvent.EventType.ROLL_PLANAR_DIE,
|
||||
playerId, source, playerId, result, true);
|
||||
event.setAppliedEffects(appliedEffects);
|
||||
event.setData(roll + "");
|
||||
if (!game.replaceEvent(event)) {
|
||||
GameEvent ge = new GameEvent(GameEvent.EventType.PLANAR_DIE_ROLLED,
|
||||
playerId, source, playerId, event.getAmount(), event.getFlag());
|
||||
ge.setData(roll + "");
|
||||
game.fireEvent(ge);
|
||||
}
|
||||
return roll;
|
||||
public PlanarDieRollResult rollPlanarDie(Outcome outcome, Ability source, Game game, int chaosSidesAmount, int planarSidesAmount) {
|
||||
return rollDiceInner(outcome, source, game, RollDieType.PLANAR, GameOptions.PLANECHASE_PLANAR_DIE_TOTAL_SIDES, chaosSidesAmount, planarSidesAmount, 1, 0)
|
||||
.stream()
|
||||
.map(o -> (PlanarDieRollResult) o)
|
||||
.findFirst()
|
||||
.orElse(PlanarDieRollResult.BLANK_ROLL);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -3539,15 +3782,12 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
|
||||
boolean canActivateAsHandZone = approvingObject != null
|
||||
|| (fromZone == Zone.GRAVEYARD && canPlayCardsFromGraveyard());
|
||||
boolean possibleToPlay = false;
|
||||
boolean possibleToPlay = canActivateAsHandZone
|
||||
&& ability.getZone().match(Zone.HAND)
|
||||
&& (isPlaySpell || isPlayLand);
|
||||
|
||||
// spell/hand abilities (play from all zones)
|
||||
// need permitingObject or canPlayCardsFromGraveyard
|
||||
if (canActivateAsHandZone
|
||||
&& ability.getZone().match(Zone.HAND)
|
||||
&& (isPlaySpell || isPlayLand)) {
|
||||
possibleToPlay = true;
|
||||
}
|
||||
|
||||
// zone's abilities (play from specific zone)
|
||||
// no need in permitingObject
|
||||
|
|
@ -4275,7 +4515,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("to Zone" + toZone.toString() + " not supported yet");
|
||||
throw new UnsupportedOperationException("to Zone" + toZone + " not supported yet");
|
||||
}
|
||||
return !successfulMovedCards.isEmpty();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package mage.watchers.common;
|
||||
|
||||
import mage.constants.RollDieType;
|
||||
import mage.constants.WatcherScope;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.DieRolledEvent;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.watchers.Watcher;
|
||||
|
||||
|
|
@ -25,9 +27,10 @@ public class PlanarRollWatcher extends Watcher {
|
|||
|
||||
@Override
|
||||
public void watch(GameEvent event, Game game) {
|
||||
if (event.getType() == GameEvent.EventType.PLANAR_DIE_ROLLED) {
|
||||
UUID playerId = event.getPlayerId();
|
||||
if (playerId != null) {
|
||||
if (event.getType() == GameEvent.EventType.DIE_ROLLED) {
|
||||
DieRolledEvent drEvent = (DieRolledEvent) event;
|
||||
UUID playerId = drEvent.getPlayerId();
|
||||
if (playerId != null && drEvent.getRollDieType() == RollDieType.PLANAR) {
|
||||
Integer amount = numberTimesPlanarDieRolled.get(playerId);
|
||||
if (amount == null) {
|
||||
amount = 1;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue