Merge branch 'master' into refactor/multiple-names

This commit is contained in:
theelk801 2024-09-20 12:08:53 -04:00
commit d18bd25d21
190 changed files with 4573 additions and 812 deletions

View file

@ -38,7 +38,7 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
private int maxPawPrints;
private Filter maxModesFilter; // calculates the max number of available modes
private Condition moreCondition; // allows multiple modes choose (example: choose one... if condition, you may choose both)
private int moreLimit = Integer.MAX_VALUE; // if multiple modes are allowed, this limits how many additional modes may be chosen (usually doesn't need to change)
private int moreLimit; // if multiple modes are allowed, this limits how many additional modes may be chosen
private boolean limitUsageByOnce = false; // limit mode selection to once per game
private boolean limitUsageResetOnNewTurn = false; // reset once per game limit on new turn, example: Galadriel, Light of Valinor
@ -245,7 +245,7 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
// use case: make more modes chooseable
if (moreCondition != null && moreCondition.apply(game, source)) {
realMaxModes = this.moreLimit;
realMaxModes = Math.min(this.moreLimit, this.size());
}
// use case: limit max modes by opponents (example: choose one or more... each mode must target a different player)
@ -303,12 +303,9 @@ public class Modes extends LinkedHashMap<UUID, Mode> implements Copyable<Modes>
this.put(mode.getId(), mode);
}
public void setMoreCondition(Condition moreCondition) {
this.moreCondition = moreCondition;
}
public void setMoreLimit(int moreLimit) {
public void setMoreCondition(int moreLimit, Condition moreCondition) {
this.moreLimit = moreLimit;
this.moreCondition = moreCondition;
}
private boolean isAlreadySelectedModesOutdated(Game game, Ability source) {

View file

@ -16,7 +16,7 @@ public class ActivateIfConditionActivatedAbility extends ActivatedAbilityImpl {
public ActivateIfConditionActivatedAbility(Effect effect, Cost cost, Condition condition) {
this(Zone.BATTLEFIELD, effect, cost, condition, TimingRule.INSTANT);
}
public ActivateIfConditionActivatedAbility(Zone zone, Effect effect, Cost cost, Condition condition) {
this(zone, effect, cost, condition, TimingRule.INSTANT);
}
@ -34,6 +34,10 @@ public class ActivateIfConditionActivatedAbility extends ActivatedAbilityImpl {
@Override
public String getRule() {
StringBuilder sb = new StringBuilder(super.getRule());
if (condition.toString().startsWith("You may also")) {
sb.append(' ').append(condition.toString()).append('.');
return sb.toString();
}
if (condition instanceof InvertCondition) {
sb.append(" You can't activate this ability ");
} else {

View file

@ -7,11 +7,14 @@ import mage.constants.TimingRule;
import mage.constants.Zone;
/**
*
* @author weirddan455
*/
public class ActivateOncePerGameActivatedAbility extends ActivatedAbilityImpl {
public ActivateOncePerGameActivatedAbility(Effect effect, Cost cost) {
this(Zone.BATTLEFIELD, effect, cost, TimingRule.INSTANT);
}
public ActivateOncePerGameActivatedAbility(Zone zone, Effect effect, Cost cost, TimingRule timingRule) {
super(zone, effect, cost);
this.timing = timingRule;

View file

@ -1,18 +1,16 @@
package mage.abilities.common;
import java.util.UUID;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.SetTargetPointer;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent.EventType;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.targetpointer.FixedTarget;
import java.util.UUID;
/**
* @author LoneFox
*/
@ -27,7 +25,7 @@ public class DealtDamageAttachedTriggeredAbility extends TriggeredAbilityImpl {
public DealtDamageAttachedTriggeredAbility(Zone zone, Effect effect, boolean optional, SetTargetPointer setTargetPointer) {
super(zone, effect, optional);
this.setTargetPointer = setTargetPointer;
setTriggerPhrase("Whenever enchanted creature is dealt damage, ");
setTriggerPhrase(getWhen() + "enchanted creature is dealt damage, ");
}
protected DealtDamageAttachedTriggeredAbility(final DealtDamageAttachedTriggeredAbility ability) {

View file

@ -19,6 +19,7 @@ public class EntersBattlefieldOrAttacksSourceTriggeredAbility extends TriggeredA
public EntersBattlefieldOrAttacksSourceTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
setTriggerPhrase("Whenever {this} enters or attacks, ");
this.withRuleTextReplacement(true);
}
protected EntersBattlefieldOrAttacksSourceTriggeredAbility(final EntersBattlefieldOrAttacksSourceTriggeredAbility ability) {

View file

@ -14,7 +14,7 @@ public class TransformsOrEntersTriggeredAbility extends TriggeredAbilityImpl {
public TransformsOrEntersTriggeredAbility(Effect effect, boolean optional) {
super(Zone.BATTLEFIELD, effect, optional);
setTriggerPhrase("Whenever this creature enters the battlefield or transforms into {this}, ");
setTriggerPhrase("Whenever this creature enters or transforms into {this}, ");
}
private TransformsOrEntersTriggeredAbility(final TransformsOrEntersTriggeredAbility ability) {

View file

@ -51,7 +51,10 @@ public class AlternativeCostImpl<T extends AlternativeCostImpl<T>> extends Costs
if (onlyCost) {
return getText();
} else {
return (name != null ? name : "") + (isMana ? " " : "&mdash;") + getText() + (isMana ? "" : '.');
String costName = (name != null ? name : "");
String delimiter = (!isMana || (!costName.isEmpty() && costName.substring(costName.length() - 1).matches("\\d")))
? "&mdash;" : " ";
return costName + delimiter + getText() + (isMana ? "" : '.');
}
}

View file

@ -30,11 +30,15 @@ public abstract class AlternativeSourceCostsImpl extends StaticAbility implement
}
protected AlternativeSourceCostsImpl(String name, String reminderText, Cost cost) {
this(name, reminderText, cost, name);
}
protected AlternativeSourceCostsImpl(String name, String reminderText, Cost cost, String activationKey) {
super(Zone.ALL, null);
this.name = name;
this.reminderText = reminderText;
this.alternativeCost = new AlternativeCostImpl<>(name, reminderText, cost);
this.activationKey = getActivationKey(name);
this.activationKey = getActivationKey(activationKey);
}
protected AlternativeSourceCostsImpl(final AlternativeSourceCostsImpl ability) {

View file

@ -1,14 +1,13 @@
package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.costs.SacrificeCost;
import mage.constants.AbilityType;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.ArrayList;
import java.util.List;
@ -46,19 +45,15 @@ public class SacrificeAllCost extends CostImpl implements SacrificeCost {
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
UUID activator = controllerId;
if (ability.getAbilityType().isActivatedAbility() || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) {
if (((ActivatedAbilityImpl) ability).getActivatorId() != null) {
activator = ((ActivatedAbilityImpl) ability).getActivatorId();
} // else, Activator not filled?
Player controller = game.getPlayer(controllerId);
if (controller == null){
return false;
}
for (Permanent permanent : game.getBattlefield().getAllActivePermanents(filter, controllerId, game)) {
if (!game.getPlayer(activator).canPaySacrificeCost(permanent, source, controllerId, game)) {
if (!controller.canPaySacrificeCost(permanent, source, controllerId, game)) {
return false;
}
}
return true;
}

View file

@ -1,15 +1,14 @@
package mage.abilities.costs.common;
import mage.abilities.Ability;
import mage.abilities.ActivatedAbilityImpl;
import mage.abilities.costs.Cost;
import mage.abilities.costs.CostImpl;
import mage.abilities.costs.SacrificeCost;
import mage.constants.AbilityType;
import mage.constants.Outcome;
import mage.filter.FilterPermanent;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetSacrifice;
import mage.util.CardUtil;
@ -58,12 +57,8 @@ public class SacrificeTargetCost extends CostImpl implements SacrificeCost {
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
UUID activator = controllerId;
if (ability.getAbilityType().isActivatedAbility() || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) {
activator = ((ActivatedAbilityImpl) ability).getActivatorId();
}
// can be cancel by user
if (this.getTargets().choose(Outcome.Sacrifice, activator, source.getSourceId(), source, game)) {
// can be cancelled by user
if (this.getTargets().choose(Outcome.Sacrifice, controllerId, source.getSourceId(), source, game)) {
for (UUID targetId : this.getTargets().get(0).getTargets()) {
Permanent permanent = game.getPermanent(targetId);
if (permanent == null) {
@ -88,17 +83,14 @@ public class SacrificeTargetCost extends CostImpl implements SacrificeCost {
@Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
UUID activator = controllerId;
if (ability.getAbilityType().isActivatedAbility() || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) {
if (((ActivatedAbilityImpl) ability).getActivatorId() != null) {
activator = ((ActivatedAbilityImpl) ability).getActivatorId();
} // else, Activator not filled?
Player controller = game.getPlayer(controllerId);
if (controller == null){
return false;
}
int validTargets = 0;
int neededTargets = this.getTargets().get(0).getNumberOfTargets();
for (Permanent permanent : game.getBattlefield().getActivePermanents(((TargetPermanent) this.getTargets().get(0)).getFilter(), controllerId, source, game)) {
if (game.getPlayer(activator).canPaySacrificeCost(permanent, source, controllerId, game)) {
if (controller.canPaySacrificeCost(permanent, source, controllerId, game)) {
validTargets++;
if (validTargets >= neededTargets) {
return true;
@ -106,10 +98,7 @@ public class SacrificeTargetCost extends CostImpl implements SacrificeCost {
}
}
// solves issue #8097, if a sacrifice cost is optional and you don't have valid targets, then the cost can be paid
if (validTargets == 0 && this.getTargets().get(0).getMinNumberOfTargets() == 0) {
return true;
}
return false;
return validTargets == 0 && this.getTargets().get(0).getMinNumberOfTargets() == 0;
}
@Override

View file

@ -6,8 +6,7 @@ import mage.abilities.effects.ReplacementEffectImpl;
import mage.constants.Duration;
import mage.constants.Outcome;
import mage.counters.CounterType;
import mage.filter.common.FilterControlledPermanent;
import mage.filter.predicate.Predicate;
import mage.filter.FilterPermanent;
import mage.filter.predicate.mageobject.AnotherPredicate;
import mage.game.Game;
import mage.game.events.EntersTheBattlefieldEvent;
@ -45,9 +44,9 @@ public class DevourEffect extends ReplacementEffectImpl {
// "creature" is a special case as the rule will not mention it.
//
// 's' will be added to pluralize, so far so good with the current text generation.
private final FilterControlledPermanent filterDevoured;
private final FilterPermanent filterDevoured;
public DevourEffect(int devourFactor, FilterControlledPermanent filterDevoured) {
public DevourEffect(int devourFactor, FilterPermanent filterDevoured) {
super(Duration.EndOfGame, Outcome.Detriment);
this.devourFactor = devourFactor;
this.filterDevoured = filterDevoured;
@ -81,11 +80,8 @@ public class DevourEffect extends ReplacementEffectImpl {
if (creature == null || controller == null) {
return false;
}
FilterControlledPermanent filter = new FilterControlledPermanent(filterDevoured.getMessage() + "s to devour");
for (Predicate predicate : filterDevoured.getPredicates()) {
filter.add(predicate);
}
FilterPermanent filter = filterDevoured.copy();
filter.setMessage(filterDevoured.getMessage() + "s (to devour)");
filter.add(AnotherPredicate.instance);
Target target = new TargetSacrifice(1, Integer.MAX_VALUE, filter);
@ -142,9 +138,9 @@ public class DevourEffect extends ReplacementEffectImpl {
text += devourFactor;
}
text += " <i>(As this enters the battlefield, you may sacrifice any number of "
text += " <i>(As this enters, you may sacrifice any number of "
+ filterMessage + "s. "
+ "This creature enters the battlefield with ";
+ "This creature enters with ";
if (devourFactor == Integer.MAX_VALUE) {
text += "X +1/+1 counters on it for each of those creatures";

View file

@ -35,13 +35,14 @@ public class BecomesCreatureSourceEffect extends ContinuousEffectImpl {
* existing creature types.
*/
protected Token token;
protected CardType retainType; // if null, loses previous types
protected boolean loseAbilities = false;
protected boolean loseEquipmentType = false;
protected DynamicValue power = null;
protected DynamicValue toughness = null;
protected boolean durationRuleAtStart; // put duration rule at the start of the rules text rather than the end
private final Token token;
private final CardType retainType; // if null, loses previous types
private boolean loseAbilities = false;
private boolean loseEquipmentType = false;
private boolean keepCreatureSubtypes;
private DynamicValue power = null;
private DynamicValue toughness = null;
private boolean durationRuleAtStart; // put duration rule at the start of the rules text rather than the end
/**
* @param token Token as blueprint for creature to become
@ -49,20 +50,11 @@ public class BecomesCreatureSourceEffect extends ContinuousEffectImpl {
* @param duration Duration for the effect
*/
public BecomesCreatureSourceEffect(Token token, CardType retainType, Duration duration) {
this(token, retainType, duration, (retainType == CardType.PLANESWALKER || retainType == CardType.CREATURE));
}
/**
* @param token Token as blueprint for creature to become
* @param retainType If null, permanent loses its previous types, otherwise retains types with appropriate text
* @param duration Duration for the effect
* @param durationRuleAtStart for text rule generation
*/
public BecomesCreatureSourceEffect(Token token, CardType retainType, Duration duration, boolean durationRuleAtStart) {
super(duration, Outcome.BecomeCreature);
this.token = token;
this.retainType = retainType;
this.durationRuleAtStart = durationRuleAtStart;
this.keepCreatureSubtypes = (retainType == CardType.ENCHANTMENT); // default usage, override if needed
this.durationRuleAtStart = (retainType == CardType.PLANESWALKER || retainType == CardType.CREATURE);
setText();
this.addDependencyType(DependencyType.BecomeCreature);
}
@ -73,6 +65,7 @@ public class BecomesCreatureSourceEffect extends ContinuousEffectImpl {
this.retainType = effect.retainType;
this.loseAbilities = effect.loseAbilities;
this.loseEquipmentType = effect.loseEquipmentType;
this.keepCreatureSubtypes = effect.keepCreatureSubtypes;
if (effect.power != null) {
this.power = effect.power.copy();
}
@ -124,7 +117,7 @@ public class BecomesCreatureSourceEffect extends ContinuousEffectImpl {
if (loseEquipmentType) {
permanent.removeSubType(game, SubType.EQUIPMENT);
}
if (retainType == CardType.CREATURE || retainType == CardType.ARTIFACT) {
if (!keepCreatureSubtypes) {
permanent.removeAllCreatureTypes(game);
}
permanent.copySubTypesFrom(game, token);
@ -191,6 +184,16 @@ public class BecomesCreatureSourceEffect extends ContinuousEffectImpl {
return this;
}
/**
* Source becomes a creature "in addition to its other types".
* Not needed when retainType is ENCHANTMENT, which sets this true by default.
*/
public BecomesCreatureSourceEffect withKeepCreatureSubtypes(boolean keepCreatureSubtypes) {
this.keepCreatureSubtypes = keepCreatureSubtypes;
setText();
return this;
}
public BecomesCreatureSourceEffect withDurationRuleAtStart(boolean durationRuleAtStart) {
this.durationRuleAtStart = durationRuleAtStart;
setText();
@ -205,7 +208,7 @@ public class BecomesCreatureSourceEffect extends ContinuousEffectImpl {
}
sb.append("{this} becomes a ");
sb.append(token.getDescription());
if (retainType == CardType.ENCHANTMENT) {
if (keepCreatureSubtypes) {
sb.append(" in addition to its other types");
}
if (!duration.toString().isEmpty() && !durationRuleAtStart) {

View file

@ -14,24 +14,28 @@ import mage.players.Player;
* @author notgreat
*/
public class LookAtOpponentFaceDownCreaturesAnyTimeEffect extends ContinuousEffectImpl {
private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("face-down creatures you don't control");
static {
filter.add(FaceDownPredicate.instance);
filter.add(TargetController.NOT_YOU.getControllerPredicate());
}
private final FilterCreaturePermanent filter;
public LookAtOpponentFaceDownCreaturesAnyTimeEffect() {
this(Duration.WhileOnBattlefield);
}
public LookAtOpponentFaceDownCreaturesAnyTimeEffect(Duration duration) {
this(duration, TargetController.NOT_YOU);
}
public LookAtOpponentFaceDownCreaturesAnyTimeEffect(Duration duration, TargetController targetController) {
super(duration, Layer.PlayerEffects, SubLayer.NA, Outcome.Benefit);
staticText = (duration.toString().isEmpty() ? "" : duration.toString() + ", ") + "you may look at face-down creatures you don't control any time";
staticText = makeText(duration, targetController);
filter = new FilterCreaturePermanent();
filter.add(FaceDownPredicate.instance);
filter.add(targetController.getControllerPredicate());
}
protected LookAtOpponentFaceDownCreaturesAnyTimeEffect(final LookAtOpponentFaceDownCreaturesAnyTimeEffect effect) {
super(effect);
this.filter = effect.filter.copy();
}
//Based on LookAtTopCardOfLibraryAnyTimeEffect
@ -56,4 +60,22 @@ public class LookAtOpponentFaceDownCreaturesAnyTimeEffect extends ContinuousEffe
public LookAtOpponentFaceDownCreaturesAnyTimeEffect copy() {
return new LookAtOpponentFaceDownCreaturesAnyTimeEffect(this);
}
private static String makeText(Duration duration, TargetController targetController) {
StringBuilder sb = new StringBuilder();
if (!duration.toString().isEmpty()) {
sb.append(duration);
sb.append(", ");
}
sb.append("you may look at face-down creatures ");
switch (targetController) {
case NOT_YOU:
sb.append("you don't control ");
break;
case OPPONENT:
sb.append("your opponents control ");
}
sb.append("any time");
return sb.toString();
}
}

View file

@ -7,6 +7,7 @@ import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardsImpl;
import mage.constants.Outcome;
import mage.filter.FilterCard;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
@ -21,25 +22,28 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect {
private final boolean upTo;
private final DynamicValue numberToDiscard;
private final FilterCard filter;
public LookTargetHandChooseDiscardEffect() {
this(false, 1);
}
public LookTargetHandChooseDiscardEffect(boolean upTo, int numberToDiscard) {
this(upTo, StaticValue.get(numberToDiscard));
this(upTo, StaticValue.get(numberToDiscard), numberToDiscard == 1 ? StaticFilters.FILTER_CARD : StaticFilters.FILTER_CARD_CARDS);
}
public LookTargetHandChooseDiscardEffect(boolean upTo, DynamicValue numberToDiscard) {
public LookTargetHandChooseDiscardEffect(boolean upTo, DynamicValue numberToDiscard, FilterCard filter) {
super(Outcome.Discard);
this.upTo = upTo;
this.numberToDiscard = numberToDiscard;
this.filter = filter;
}
protected LookTargetHandChooseDiscardEffect(final LookTargetHandChooseDiscardEffect effect) {
super(effect);
this.upTo = effect.upTo;
this.numberToDiscard = effect.numberToDiscard;
this.filter = effect.filter;
}
@Override
@ -56,7 +60,7 @@ public class LookTargetHandChooseDiscardEffect extends OneShotEffect {
}
return true;
}
TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, num > 1 ? StaticFilters.FILTER_CARD_CARDS : StaticFilters.FILTER_CARD);
TargetCard target = new TargetCardInHand(upTo ? 0 : num, num, filter);
if (controller.choose(Outcome.Discard, player.getHand(), target, source, game)) {
player.discard(new CardsImpl(target.getTargets()), false, source, game);
}

View file

@ -0,0 +1,43 @@
package mage.abilities.hint.common;
import mage.abilities.Ability;
import mage.abilities.condition.common.CitysBlessingCondition;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.game.Game;
import mage.game.command.Dungeon;
import mage.players.Player;
/**
* @author JayDi85
*/
public enum CurrentDungeonHint implements Hint {
instance;
private static final ConditionHint hint = new ConditionHint(CitysBlessingCondition.instance, "You have city's blessing");
@Override
public String getText(Game game, Ability ability) {
Player player = game.getPlayer(ability.getControllerId());
if (player == null) {
return "";
}
Dungeon dungeon = game.getPlayerDungeon(ability.getControllerId());
if (dungeon == null) {
return "Current dungeon: not yet entered";
}
String dungeonInfo = "Current dungeon: " + dungeon.getLogName();
if (dungeon.getCurrentRoom() != null) {
dungeonInfo += ", room: " + dungeon.getCurrentRoom().getName();
}
return dungeonInfo;
}
@Override
public Hint copy() {
return instance;
}
}

View file

@ -1,11 +1,10 @@
package mage.abilities.keyword;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.DevourEffect;
import mage.constants.Zone;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.common.FilterControlledPermanent;
/**
* 502.82. Devour
@ -45,12 +44,12 @@ import mage.filter.common.FilterControlledPermanent;
*/
public class DevourAbility extends SimpleStaticAbility {
private static final FilterControlledPermanent filterCreature = new FilterControlledCreaturePermanent("creature");
private static final FilterPermanent filterCreature = new FilterControlledCreaturePermanent("creature");
// Integer.MAX_VALUE is a special value
// for "devour X, where X is the number of devored permanents"
// see DevourEffect for the full details.
public static DevourAbility DevourX() {
public static DevourAbility devourX() {
return new DevourAbility(Integer.MAX_VALUE);
}
@ -58,7 +57,7 @@ public class DevourAbility extends SimpleStaticAbility {
this(devourFactor, filterCreature);
}
public DevourAbility(int devourFactor, FilterControlledPermanent filterDevoured) {
public DevourAbility(int devourFactor, FilterPermanent filterDevoured) {
super(Zone.ALL, new DevourEffect(devourFactor, filterDevoured));
}

View file

@ -1,12 +1,33 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.common.BeginningOfEndStepTriggeredAbility;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.SourceHasCounterCondition;
import mage.abilities.costs.AlternativeSourceCostsImpl;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.decorator.ConditionalOneShotEffect;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.common.AddContinuousEffectToGame;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.effects.common.counter.RemoveCounterSourceEffect;
import mage.constants.*;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.util.CardUtil;
import java.util.stream.Collectors;
/**
* TODO: Implement this
* "Impending N[cost]" is a keyword that represents multiple abilities.
* The official rules are as follows:
* (a) You may choose to pay [cost] rather than pay this spell's mana cost.
* (b) If you chose to pay this spell's impending cost, it enters the battlefield with N time counters on it.
* (c) As long as this permanent has a time counter on it, if it was cast for its impending cost, it's not a creature.
* (d) At the beginning of your end step, if this permanent was cast for its impending cost, remove a time counter from it. Then if it has no time counters on it, it loses impending.
*
* @author TheElk801
*/
@ -16,14 +37,24 @@ public class ImpendingAbility extends AlternativeSourceCostsImpl {
private static final String IMPENDING_REMINDER = "If you cast this spell for its impending cost, " +
"it enters with %s time counters and isn't a creature until the last is removed. " +
"At the beginning of your end step, remove a time counter from it.";
private static final Condition counterCondition = new SourceHasCounterCondition(CounterType.TIME, 0, 0);
public ImpendingAbility(String manaString) {
this(manaString, 4);
}
public ImpendingAbility(String manaString, int amount) {
super(IMPENDING_KEYWORD + ' ' + amount, String.format(IMPENDING_REMINDER, CardUtil.numberToText(amount)), manaString);
public ImpendingAbility(int amount, String manaString) {
super(IMPENDING_KEYWORD + ' ' + amount, String.format(IMPENDING_REMINDER, CardUtil.numberToText(amount)), new ManaCostsImpl<>(manaString), IMPENDING_KEYWORD);
this.setRuleAtTheTop(true);
this.addSubAbility(new EntersBattlefieldAbility(new ConditionalOneShotEffect(
new AddCountersSourceEffect(CounterType.TIME.createInstance(amount)), ImpendingCondition.instance, ""
), "").setRuleVisible(false));
this.addSubAbility(new SimpleStaticAbility(new ImpendingAbilityTypeEffect()).setRuleVisible(false));
Ability ability = new BeginningOfEndStepTriggeredAbility(
new RemoveCounterSourceEffect(CounterType.TIME.createInstance()),
TargetController.YOU, ImpendingCondition.instance, false
);
ability.addEffect(new ConditionalOneShotEffect(
new AddContinuousEffectToGame(new ImpendingAbilityRemoveEffect()),
counterCondition, "Then if it has no time counters on it, it loses impending"
));
this.addSubAbility(ability.setRuleVisible(false));
}
private ImpendingAbility(final ImpendingAbility ability) {
@ -35,12 +66,81 @@ public class ImpendingAbility extends AlternativeSourceCostsImpl {
return new ImpendingAbility(this);
}
@Override
public boolean isAvailable(Ability source, Game game) {
return true;
}
public static String getActivationKey() {
return getActivationKey(IMPENDING_KEYWORD);
}
}
enum ImpendingCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
return CardUtil.checkSourceCostsTagExists(game, source, ImpendingAbility.getActivationKey());
}
}
class ImpendingAbilityTypeEffect extends ContinuousEffectImpl {
ImpendingAbilityTypeEffect() {
super(Duration.WhileOnBattlefield, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.Detriment);
staticText = "As long as this permanent has a time counter on it, if it was cast for its impending cost, it's not a creature.";
}
private ImpendingAbilityTypeEffect(final ImpendingAbilityTypeEffect effect) {
super(effect);
}
@Override
public ImpendingAbilityTypeEffect copy() {
return new ImpendingAbilityTypeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
if (!ImpendingCondition.instance.apply(game, source)) {
return false;
}
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent.getCounters(game).getCount(CounterType.TIME) < 1) {
return false;
}
permanent.removeCardType(game, CardType.CREATURE);
permanent.removeAllCreatureTypes(game);
return true;
}
}
class ImpendingAbilityRemoveEffect extends ContinuousEffectImpl {
ImpendingAbilityRemoveEffect() {
super(Duration.Custom, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.LoseAbility);
}
private ImpendingAbilityRemoveEffect(final ImpendingAbilityRemoveEffect effect) {
super(effect);
}
@Override
public ImpendingAbilityRemoveEffect copy() {
return new ImpendingAbilityRemoveEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
discard();
return false;
}
permanent.removeAbilities(
permanent
.getAbilities(game)
.stream()
.filter(ImpendingAbility.class::isInstance)
.collect(Collectors.toList()),
source.getSourceId(), game
);
return true;
}
}

View file

@ -43,7 +43,8 @@ package mage.constants;
* @author LevelX2
*/
public enum MageObjectType {
ABILITY_STACK("Ability on the Stack", false, false, false),
ABILITY_STACK_FROM_CARD("Ability on the Stack", false, false, false),
ABILITY_STACK_FROM_TOKEN("Ability on the Stack", false, false, true),
CARD("Card", false, true, false),
COPY_CARD("Copy of a Card", false, true, false),
TOKEN("Token", true, true, true),

View file

@ -3,6 +3,7 @@ package mage.designations;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.hint.common.CurrentDungeonHint;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Controllable;
@ -113,6 +114,7 @@ class InitiativeVentureTriggeredAbility extends TriggeredAbilityImpl {
InitiativeVentureTriggeredAbility() {
super(Zone.ALL, new InitiativeUndercityEffect());
addHint(CurrentDungeonHint.instance);
}
private InitiativeVentureTriggeredAbility(final InitiativeVentureTriggeredAbility ability) {

View file

@ -73,7 +73,9 @@ public class FilterCard extends FilterObject<Card> {
throw new UnsupportedOperationException("You may not modify a locked filter");
}
// verify check
checkPredicateIsSuitableForCardFilter(predicate);
Predicates.makeSurePredicateCompatibleWithFilter(predicate, Card.class);
extraPredicates.add(predicate);
}

View file

@ -5,6 +5,7 @@ import mage.constants.SubType;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.Predicates;
import mage.game.Game;
import mage.game.permanent.Permanent;
@ -12,7 +13,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author North
@ -64,6 +64,10 @@ public class FilterPermanent extends FilterObject<Permanent> implements FilterIn
if (isLockedFilter()) {
throw new UnsupportedOperationException("You may not modify a locked filter");
}
// verify check
Predicates.makeSurePredicateCompatibleWithFilter(predicate, Permanent.class);
extraPredicates.add(predicate);
}

View file

@ -4,6 +4,7 @@ import mage.abilities.Ability;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.Predicates;
import mage.game.Game;
import mage.players.Player;
@ -36,6 +37,10 @@ public class FilterPlayer extends FilterImpl<Player> {
if (isLockedFilter()) {
throw new UnsupportedOperationException("You may not modify a locked filter");
}
// verify check
Predicates.makeSurePredicateCompatibleWithFilter(predicate, Player.class);
extraPredicates.add(predicate);
return this;
}

View file

@ -1,10 +1,13 @@
package mage.filter;
import mage.abilities.Ability;
import mage.cards.Card;
import mage.filter.predicate.ObjectSourcePlayer;
import mage.filter.predicate.ObjectSourcePlayerPredicate;
import mage.filter.predicate.Predicate;
import mage.filter.predicate.Predicates;
import mage.game.Game;
import mage.game.stack.Spell;
import mage.game.stack.StackObject;
import java.util.ArrayList;
@ -43,6 +46,11 @@ public class FilterStackObject extends FilterObject<StackObject> {
if (isLockedFilter()) {
throw new UnsupportedOperationException("You may not modify a locked filter");
}
// verify check
// Spell implements Card interface, so it can use some default predicates like owner
Predicates.makeSurePredicateCompatibleWithFilter(predicate, StackObject.class, Spell.class, Card.class);
extraPredicates.add(predicate);
}

View file

@ -1,4 +1,3 @@
package mage.filter.predicate;
import java.io.Serializable;

View file

@ -2,6 +2,8 @@ package mage.filter.predicate;
import mage.game.Game;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -11,7 +13,7 @@ import java.util.List;
*
* <p>All methods returns serializable predicates as long as they're given serializable parameters.</p>
*
* @author North
* @author North, JayDi85
*/
public final class Predicates {
@ -246,4 +248,48 @@ public final class Predicates {
extraPredicates.forEach(p -> collectAllComponents(p, res));
}
}
/**
* Verify check: try to find filters usage
* Example use case: Player predicate was used for Permanent filter
* Example error: java.lang.ClassCastException: mage.game.permanent.PermanentToken cannot be cast to mage.players.Player
*/
public static void makeSurePredicateCompatibleWithFilter(Predicate predicate, Class... compatibleClasses) {
List<Predicate> list = new ArrayList<>();
Predicates.collectAllComponents(predicate, list);
list.forEach(p -> {
Class predicateGenericParamClass = findGenericParam(predicate);
if (predicateGenericParamClass == null) {
throw new IllegalArgumentException("Somthing wrong. Can't find predicate's generic param for " + predicate.getClass());
}
if (Arrays.stream(compatibleClasses).anyMatch(f -> predicateGenericParamClass.isAssignableFrom(f))) {
// predicate is fine
} else {
// How-to fix: use correct predicates (same type, e.g. getControllerPredicate() instead getPlayerPredicate())
throw new IllegalArgumentException(String.format(
"Wrong code usage: predicate [%s] with generic param [%s] can't be added to filter, allow only %s",
predicate.getClass(),
predicateGenericParamClass,
Arrays.toString(compatibleClasses)
));
}
});
}
private static Class findGenericParam(Predicate predicate) {
Type[] interfaces = predicate.getClass().getGenericInterfaces();
for (Type type : interfaces) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
if (actualTypeArguments.length > 0) {
Type actualType = actualTypeArguments[0];
if (actualType instanceof Class) {
return (Class) actualType;
}
}
}
}
return null;
}
}

View file

@ -0,0 +1,63 @@
package mage.game;
import mage.constants.MultiplayerAttackOption;
import mage.constants.RangeOfInfluence;
import mage.game.match.MatchType;
import mage.game.mulligan.MulliganType;
/**
* Fake game for tests and data check, do nothing.
*
* @author JayDi85
*/
public class FakeGame extends GameImpl {
private int numPlayers;
public FakeGame() {
super(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 60, 20, 7);
}
public FakeGame(final FakeGame game) {
super(game);
this.numPlayers = game.numPlayers;
}
@Override
public MatchType getGameType() {
return new FakeGameType();
}
@Override
public int getNumPlayers() {
return numPlayers;
}
@Override
public FakeGame copy() {
return new FakeGame(this);
}
}
class FakeGameType extends MatchType {
public FakeGameType() {
this.name = "Test Game Type";
this.maxPlayers = 10;
this.minPlayers = 3;
this.numTeams = 0;
this.useAttackOption = true;
this.useRange = true;
this.sideboardingAllowed = true;
}
protected FakeGameType(final FakeGameType matchType) {
super(matchType);
}
@Override
public FakeGameType copy() {
return new FakeGameType(this);
}
}

View file

@ -0,0 +1,21 @@
package mage.game;
import mage.game.match.MatchImpl;
import mage.game.match.MatchOptions;
/**
* Fake match for tests and data check, do nothing.
*
* @author JayDi85
*/
public class FakeMatch extends MatchImpl {
public FakeMatch() {
super(new MatchOptions("fake match", "fake game type", true, 2));
}
@Override
public void startGame() throws GameException {
throw new IllegalStateException("Can't start fake match");
}
}

View file

@ -88,7 +88,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
Dungeon getDungeon(UUID objectId);
Dungeon getPlayerDungeon(UUID objectId);
Dungeon getPlayerDungeon(UUID playerId);
UUID getControllerId(UUID objectId);
@ -456,7 +456,12 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
Dungeon addDungeon(Dungeon dungeon, UUID playerId);
void ventureIntoDungeon(UUID playerId, boolean undercity);
/**
* Enter to dungeon or go to next room
*
* @param isEnterToUndercity - enter to Undercity instead choose a new dungeon
*/
void ventureIntoDungeon(UUID playerId, boolean isEnterToUndercity);
void temptWithTheRing(UUID playerId);

View file

@ -560,14 +560,14 @@ public abstract class GameImpl implements Game {
}
@Override
public void ventureIntoDungeon(UUID playerId, boolean undercity) {
public void ventureIntoDungeon(UUID playerId, boolean isEnterToUndercity) {
if (playerId == null) {
return;
}
if (replaceEvent(GameEvent.getEvent(GameEvent.EventType.VENTURE, playerId, null, playerId))) {
return;
}
this.getOrCreateDungeon(playerId, undercity).moveToNextRoom(playerId, this);
this.getOrCreateDungeon(playerId, isEnterToUndercity).moveToNextRoom(playerId, this);
fireEvent(GameEvent.getEvent(GameEvent.EventType.VENTURED, playerId, null, playerId));
}
@ -584,6 +584,9 @@ public abstract class GameImpl implements Game {
return emblem;
}
TheRingEmblem newEmblem = new TheRingEmblem(playerId);
// TODO: add image info
state.addCommandObject(newEmblem);
return newEmblem;
}
@ -1971,7 +1974,9 @@ public abstract class GameImpl implements Game {
ability.setSourceId(newEmblem.getId());
}
state.addCommandObject(newEmblem); // TODO: generate image for emblem here?
// image info setup in setSourceObject
state.addCommandObject(newEmblem);
}
/**
@ -1999,6 +2004,9 @@ public abstract class GameImpl implements Game {
for (Ability ability : newPlane.getAbilities()) {
ability.setSourceId(newPlane.getId());
}
// image info setup in setSourceObject
state.addCommandObject(newPlane);
informPlayers("You have planeswalked to " + newPlane.getLogName());
@ -2020,6 +2028,7 @@ public abstract class GameImpl implements Game {
@Override
public Dungeon addDungeon(Dungeon dungeon, UUID playerId) {
dungeon.setControllerId(playerId);
dungeon.setSourceObject();
state.addCommandObject(dungeon);
return dungeon;
}

View file

@ -13,6 +13,8 @@ import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.Effect;
import mage.abilities.hint.HintUtils;
import mage.cards.FrameStyle;
import mage.cards.repository.TokenInfo;
import mage.cards.repository.TokenRepository;
import mage.choices.Choice;
import mage.choices.ChoiceHintType;
import mage.choices.ChoiceImpl;
@ -87,6 +89,11 @@ public class Dungeon extends CommandObjectImpl {
}
public void moveToNextRoom(UUID playerId, Game game) {
Dungeon dungeon = game.getPlayerDungeon(playerId);
if (dungeon == null) {
return;
}
if (currentRoom == null) {
currentRoom = dungeonRooms.get(0);
} else {
@ -94,7 +101,7 @@ public class Dungeon extends CommandObjectImpl {
}
Player player = game.getPlayer(getControllerId());
if (player != null) {
game.informPlayers(player.getLogName() + " has entered " + currentRoom.getName());
game.informPlayers(player.getLogName() + " has entered " + currentRoom.getName() + " (dungeon: " + dungeon.getLogName() + ")");
}
game.fireEvent(GameEvent.getEvent(
GameEvent.EventType.ROOM_ENTERED, currentRoom.getId(), null, playerId
@ -139,14 +146,14 @@ public class Dungeon extends CommandObjectImpl {
choice.setChoices(dungeonNames);
player.choose(Outcome.Neutral, choice, game);
if (choice.getChoice() != null) {
return createDungeon(choice.getChoice());
return createDungeon(choice.getChoice(), true);
} else {
// on disconnect
return createDungeon("Tomb of Annihilation");
return createDungeon("Tomb of Annihilation", true);
}
}
public static Dungeon createDungeon(String name) {
public static Dungeon createDungeon(String name, boolean isNameMustExists) {
switch (name) {
case "Tomb of Annihilation":
return new TombOfAnnihilationDungeon();
@ -155,7 +162,26 @@ public class Dungeon extends CommandObjectImpl {
case "Dungeon of the Mad Mage":
return new DungeonOfTheMadMageDungeon();
default:
throw new UnsupportedOperationException("A dungeon should have been chosen");
if (isNameMustExists) {
throw new UnsupportedOperationException("A dungeon should have been chosen");
} else {
return null;
}
}
}
public void setSourceObject() {
// choose set code due source
TokenInfo foundInfo = TokenRepository.instance.findPreferredTokenInfoForClass(this.getClass().getName(), null);
if (foundInfo != null) {
this.setExpansionSetCode(foundInfo.getSetCode());
this.setUsesVariousArt(false);
this.setCardNumber("");
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber());
} else {
// how-to fix: add dungeon to the tokens-database
throw new IllegalArgumentException("Wrong code usage: can't find token info for the dungeon: " + this.getClass().getName());
}
}

View file

@ -77,6 +77,11 @@ public class DungeonRoom {
}
public DungeonRoom chooseNextRoom(UUID playerId, Game game) {
Dungeon dungeon = game.getPlayerDungeon(playerId);
if (dungeon == null) {
return null;
}
switch (nextRooms.size()) {
case 0:
return null;
@ -90,8 +95,8 @@ public class DungeonRoom {
return null;
}
return player.chooseUse(
Outcome.Neutral, "Choose which room to go to",
null, room1.name, room2.name, null, game
Outcome.Neutral, "Choose which room to go to in",
"dungeon: " + dungeon.getLogName(), room1.name, room2.name, null, game
) ? room1 : room2;
default:
throw new UnsupportedOperationException("there shouldn't be more than two rooms to go to");

View file

@ -1,4 +1,3 @@
package mage.target;
import mage.abilities.Ability;
@ -110,7 +109,7 @@ public class TargetSpell extends TargetObject {
private boolean canBeChosen(StackObject stackObject, UUID sourceControllerId, Ability source, Game game) {
return stackObject instanceof Spell
&& game.getState().getPlayersInRange(sourceControllerId, game).contains(stackObject.getControllerId())
&& filter.match(stackObject, sourceControllerId, source, game);
&& canTarget(sourceControllerId, stackObject.getId(), source, game);
}
@Override