forked from External/mage
Merge pull request 'master' (#17) from External/mage:master into master
All checks were successful
/ example-docker-compose (push) Successful in 15m36s
All checks were successful
/ example-docker-compose (push) Successful in 15m36s
Reviewed-on: #17
This commit is contained in:
commit
5347eea94b
433 changed files with 8704 additions and 1435 deletions
|
|
@ -104,4 +104,6 @@ public interface TriggeredAbility extends Ability {
|
|||
GameEvent getTriggerEvent();
|
||||
|
||||
TriggeredAbility setTriggerPhrase(String triggerPhrase);
|
||||
|
||||
String getTriggerPhrase();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,11 @@ public abstract class TriggeredAbilityImpl extends AbilityImpl implements Trigge
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTriggerPhrase() {
|
||||
return this.triggerPhrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTriggerEvent(GameEvent triggerEvent) {
|
||||
this.triggerEvent = triggerEvent;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import mage.constants.Zone;
|
|||
|
||||
public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl {
|
||||
|
||||
private boolean showActivateText = true;
|
||||
|
||||
public ActivateAsSorceryActivatedAbility(Effect effect, Cost cost) {
|
||||
this(Zone.BATTLEFIELD, effect, cost);
|
||||
}
|
||||
|
|
@ -20,6 +22,7 @@ public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl {
|
|||
|
||||
protected ActivateAsSorceryActivatedAbility(final ActivateAsSorceryActivatedAbility ability) {
|
||||
super(ability);
|
||||
this.showActivateText = ability.showActivateText;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -27,9 +30,18 @@ public class ActivateAsSorceryActivatedAbility extends ActivatedAbilityImpl {
|
|||
return new ActivateAsSorceryActivatedAbility(this);
|
||||
}
|
||||
|
||||
public ActivateAsSorceryActivatedAbility withShowActivateText(boolean showActivateText) {
|
||||
this.showActivateText = showActivateText;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
String superRule = super.getRule();
|
||||
if (!showActivateText) {
|
||||
return superRule;
|
||||
}
|
||||
|
||||
String newText = (mayActivate == TargetController.OPPONENT
|
||||
? " Only your opponents may activate this ability and only as a sorcery."
|
||||
: " Activate only as a sorcery.");
|
||||
|
|
|
|||
|
|
@ -2,22 +2,33 @@ package mage.abilities.common;
|
|||
|
||||
import mage.abilities.TriggeredAbilityImpl;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.SubType;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class EntersBattlefieldTriggeredAbility extends TriggeredAbilityImpl {
|
||||
|
||||
static public boolean ENABLE_TRIGGER_PHRASE_AUTO_FIX = false;
|
||||
|
||||
public EntersBattlefieldTriggeredAbility(Effect effect) {
|
||||
this(effect, false);
|
||||
}
|
||||
|
||||
public EntersBattlefieldTriggeredAbility(Effect effect, boolean optional) {
|
||||
super(Zone.ALL, effect, optional); // Zone.All because a creature with trigger can be put into play and be sacrificed during the resolution of an effect (discard Obstinate Baloth with Smallpox)
|
||||
this.withRuleTextReplacement(true); // default true to replace "{this}" with "it"
|
||||
this.withRuleTextReplacement(true); // default true to replace "{this}" with "it" or "this creature"
|
||||
|
||||
// warning, it's impossible to add text auto-replacement for creatures here (When this creature enters),
|
||||
// so it was implemented in CardImpl.addAbility instead
|
||||
// see https://github.com/magefree/mage/issues/12791
|
||||
setTriggerPhrase("When {this} enters, ");
|
||||
}
|
||||
|
||||
|
|
@ -43,4 +54,59 @@ public class EntersBattlefieldTriggeredAbility extends TriggeredAbilityImpl {
|
|||
public EntersBattlefieldTriggeredAbility copy() {
|
||||
return new EntersBattlefieldTriggeredAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntersBattlefieldTriggeredAbility setTriggerPhrase(String triggerPhrase) {
|
||||
super.setTriggerPhrase(triggerPhrase);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find description of "{this}" like "this creature"
|
||||
*/
|
||||
static public String getThisObjectDescription(Card card) {
|
||||
// prepare {this} description
|
||||
|
||||
// short names like Aatchik for Aatchik, Emerald Radian
|
||||
// except: Mu Yanling, Wind Rider (maybe related to spaces in name)
|
||||
List<String> parts = Arrays.asList(card.getName().split(","));
|
||||
if (parts.size() > 1 && !parts.get(0).contains(" ")) {
|
||||
return parts.get(0);
|
||||
}
|
||||
|
||||
// some types have priority, e.g. Vehicle instead artifact, example: Boommobile
|
||||
if (card.getSubtype().contains(SubType.VEHICLE)) {
|
||||
return "this Vehicle";
|
||||
}
|
||||
if (card.getSubtype().contains(SubType.AURA)) {
|
||||
return "this Aura";
|
||||
}
|
||||
|
||||
// by priority
|
||||
if (card.isCreature()) {
|
||||
return "this creature";
|
||||
} else if (card.isPlaneswalker()) {
|
||||
return "this planeswalker";
|
||||
} else if (card.isLand()) {
|
||||
return "this land";
|
||||
} else if (card.isEnchantment()) {
|
||||
return "this enchantment";
|
||||
} else if (card.isArtifact()) {
|
||||
return "this artifact";
|
||||
} else {
|
||||
return "this permanent";
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> getPossibleTriggerPhrases() {
|
||||
// for verify tests - must be same list as above (only {this} relates phrases)
|
||||
return Arrays.asList(
|
||||
"when this creature enters",
|
||||
"when this planeswalker enters",
|
||||
"when this land enters",
|
||||
"when this enchantment enters",
|
||||
"when this artifact enters",
|
||||
"when this permanent enters"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ import mage.util.CardUtil;
|
|||
*/
|
||||
public class LimitedTimesPerTurnActivatedAbility extends ActivatedAbilityImpl {
|
||||
|
||||
public LimitedTimesPerTurnActivatedAbility(Effect effect, Cost cost) {
|
||||
this(Zone.BATTLEFIELD, effect, cost);
|
||||
}
|
||||
|
||||
public LimitedTimesPerTurnActivatedAbility(Zone zone, Effect effect, Cost cost) {
|
||||
this(zone, effect, cost, 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import mage.cards.Card;
|
|||
import mage.constants.*;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
|
|
@ -38,9 +39,21 @@ class MaxSpeedAbilityEffect extends ContinuousEffectImpl {
|
|||
|
||||
private final Ability ability;
|
||||
|
||||
private static Duration getDuration(Ability ability) {
|
||||
switch (ability.getZone()) {
|
||||
case BATTLEFIELD:
|
||||
return Duration.WhileOnBattlefield;
|
||||
case GRAVEYARD:
|
||||
return Duration.WhileInGraveyard;
|
||||
default:
|
||||
return Duration.Custom;
|
||||
}
|
||||
}
|
||||
|
||||
MaxSpeedAbilityEffect(Ability ability) {
|
||||
super(Duration.Custom, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
|
||||
super(getDuration(ability), Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
|
||||
this.ability = ability;
|
||||
this.ability.setRuleVisible(false);
|
||||
}
|
||||
|
||||
private MaxSpeedAbilityEffect(final MaxSpeedAbilityEffect effect) {
|
||||
|
|
@ -73,6 +86,6 @@ class MaxSpeedAbilityEffect extends ContinuousEffectImpl {
|
|||
|
||||
@Override
|
||||
public String getText(Mode mode) {
|
||||
return "Max speed — " + ability.getRule();
|
||||
return "Max speed — " + CardUtil.getTextWithFirstCharUpperCase(ability.getRule());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class MayCastFromGraveyardEffect extends AsThoughEffectImpl {
|
|||
|
||||
MayCastFromGraveyardEffect() {
|
||||
super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.PutCreatureInPlay);
|
||||
staticText = "you may cast {this} from your graveyard";
|
||||
staticText = "you may cast this card from your graveyard";
|
||||
}
|
||||
|
||||
private MayCastFromGraveyardEffect(final MayCastFromGraveyardEffect effect) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import mage.util.CardUtil;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author nantuko
|
||||
|
|
@ -27,6 +26,7 @@ public class ExileFromGraveCost extends CostImpl {
|
|||
|
||||
private final List<Card> exiledCards = new ArrayList<>();
|
||||
private boolean setTargetPointer = false;
|
||||
private boolean useSourceExileZone = true;
|
||||
|
||||
public ExileFromGraveCost(TargetCardInYourGraveyard target) {
|
||||
target.withNotTarget(true);
|
||||
|
|
@ -73,6 +73,7 @@ public class ExileFromGraveCost extends CostImpl {
|
|||
super(cost);
|
||||
this.exiledCards.addAll(cost.getExiledCards());
|
||||
this.setTargetPointer = cost.setTargetPointer;
|
||||
this.useSourceExileZone = cost.useSourceExileZone;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -90,11 +91,23 @@ public class ExileFromGraveCost extends CostImpl {
|
|||
}
|
||||
Cards cardsToExile = new CardsImpl();
|
||||
cardsToExile.addAllCards(exiledCards);
|
||||
|
||||
|
||||
UUID exileZoneId = null;
|
||||
String exileZoneName = "";
|
||||
if (useSourceExileZone) {
|
||||
exileZoneId = CardUtil.getExileZoneId(game, source);
|
||||
exileZoneName = CardUtil.getSourceName(game, source);
|
||||
}
|
||||
controller.moveCardsToExile(
|
||||
cardsToExile.getCards(game), source, game, true,
|
||||
CardUtil.getExileZoneId(game, source),
|
||||
CardUtil.getSourceName(game, source)
|
||||
cardsToExile.getCards(game),
|
||||
source,
|
||||
game,
|
||||
true,
|
||||
exileZoneId,
|
||||
exileZoneName
|
||||
);
|
||||
|
||||
if (setTargetPointer) {
|
||||
source.getEffects().setTargetPointer(new FixedTargets(cardsToExile.getCards(game), game));
|
||||
}
|
||||
|
|
@ -118,4 +131,12 @@ public class ExileFromGraveCost extends CostImpl {
|
|||
public List<Card> getExiledCards() {
|
||||
return exiledCards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put exiled cards to source zone, so next linked ability can find it
|
||||
*/
|
||||
public ExileFromGraveCost withSourceExileZone(boolean useSourceExileZone) {
|
||||
this.useSourceExileZone = useSourceExileZone;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import java.util.UUID;
|
|||
public class ExileSourceFromGraveCost extends CostImpl {
|
||||
|
||||
public ExileSourceFromGraveCost() {
|
||||
this.text = "exile {this} from your graveyard";
|
||||
this.text = "exile this card from your graveyard";
|
||||
}
|
||||
|
||||
private ExileSourceFromGraveCost(final ExileSourceFromGraveCost cost) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package mage.abilities.costs.common;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.costs.CostImpl;
|
||||
import mage.abilities.effects.common.continuous.GainSuspendEffect;
|
||||
import mage.abilities.keyword.SuspendAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.MageObjectReference;
|
||||
import mage.players.Player;
|
||||
|
||||
|
||||
/**
|
||||
* @author padfoot
|
||||
*/
|
||||
public class ExileSourceWithTimeCountersCost extends CostImpl {
|
||||
|
||||
private final int counters;
|
||||
private final boolean checksSuspend;
|
||||
private final boolean givesSuspend;
|
||||
private final Zone fromZone;
|
||||
|
||||
public ExileSourceWithTimeCountersCost(int counters) {
|
||||
this (counters, true, false, null);
|
||||
}
|
||||
|
||||
public ExileSourceWithTimeCountersCost(int counters, boolean givesSuspend, boolean checksSuspend, Zone fromZone) {
|
||||
this.counters = counters;
|
||||
this.givesSuspend = givesSuspend;
|
||||
this.checksSuspend = checksSuspend;
|
||||
this.fromZone = fromZone;
|
||||
this.text = "exile {this} " +
|
||||
((fromZone != null) ? " from your " + fromZone.toString().toLowerCase(Locale.ENGLISH) : "") +
|
||||
" and put " + counters + " time counters on it" +
|
||||
(givesSuspend ? ". It gains suspend" : "") +
|
||||
(checksSuspend ? ". If it doesn't have suspend, it gains suspend" : "");
|
||||
}
|
||||
|
||||
private ExileSourceWithTimeCountersCost(final ExileSourceWithTimeCountersCost cost) {
|
||||
super(cost);
|
||||
this.counters = cost.counters;
|
||||
this.givesSuspend = cost.givesSuspend;
|
||||
this.checksSuspend = cost.checksSuspend;
|
||||
this.fromZone = cost.fromZone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
|
||||
Player controller = game.getPlayer(controllerId);
|
||||
if (controller == null) {
|
||||
return paid;
|
||||
}
|
||||
Card card = game.getCard(source.getSourceId());
|
||||
boolean hasSuspend = card.getAbilities(game).containsClass(SuspendAbility.class);
|
||||
if (card != null && (fromZone == null || fromZone == game.getState().getZone(source.getSourceId()))) {
|
||||
UUID exileId = SuspendAbility.getSuspendExileId(controller.getId(), game);
|
||||
if (controller.moveCardsToExile(card, source, game, true, exileId, "Suspended cards of " + controller.getName())) {
|
||||
card.addCounters(CounterType.TIME.createInstance(counters), controller.getId(), source, game);
|
||||
game.informPlayers(controller.getLogName() + " exiles " + card.getLogName() + ((fromZone != null) ? " from their " + fromZone.toString().toLowerCase(Locale.ENGLISH) : "") + " with " + counters + " time counters on it.");
|
||||
if (givesSuspend || (checksSuspend && !hasSuspend)) {
|
||||
game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source);
|
||||
}
|
||||
}
|
||||
// 117.11. The actions performed when paying a cost may be modified by effects.
|
||||
// Even if they are, meaning the actions that are performed don't match the actions
|
||||
// that are called for, the cost has still been paid.
|
||||
// so return state here is not important because the user indended to exile the target anyway
|
||||
paid = true;
|
||||
}
|
||||
return paid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) {
|
||||
return (game.getCard(source.getSourceId()) != null && (fromZone == null || fromZone == game.getState().getZone(source.getSourceId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExileSourceWithTimeCountersCost copy() {
|
||||
return new ExileSourceWithTimeCountersCost(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,16 @@ package mage.abilities.dynamicvalue.common;
|
|||
import mage.abilities.Ability;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.abilities.hint.Hint;
|
||||
import mage.abilities.hint.ValueHint;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.CardType;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.PermanentToken;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
|
|
@ -20,16 +22,18 @@ public enum CardTypesInGraveyardCount implements DynamicValue {
|
|||
YOU("your graveyard"),
|
||||
ALL("all graveyards"),
|
||||
OPPONENTS("your opponents' graveyards");
|
||||
|
||||
private final String message;
|
||||
private final CardTypesInGraveyardHint hint;
|
||||
|
||||
CardTypesInGraveyardCount(String message) {
|
||||
this.message = "the number of card types among cards in " + message;
|
||||
this.hint = new CardTypesInGraveyardHint(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculate(Game game, Ability sourceAbility, Effect effect) {
|
||||
return getStream(game, sourceAbility)
|
||||
.filter(card -> !card.isCopy() && !(card instanceof PermanentToken))
|
||||
return getGraveyardCards(game, sourceAbility)
|
||||
.map(card -> card.getCardType(game))
|
||||
.flatMap(Collection::stream)
|
||||
.distinct()
|
||||
|
|
@ -52,16 +56,16 @@ public enum CardTypesInGraveyardCount implements DynamicValue {
|
|||
return message;
|
||||
}
|
||||
|
||||
private final Stream<Card> getStream(Game game, Ability ability) {
|
||||
public Hint getHint() {
|
||||
return hint;
|
||||
}
|
||||
|
||||
public Stream<Card> getGraveyardCards(Game game, Ability ability) {
|
||||
Collection<UUID> playerIds;
|
||||
switch (this) {
|
||||
case YOU:
|
||||
Player player = game.getPlayer(ability.getControllerId());
|
||||
return player == null
|
||||
? null : player
|
||||
.getGraveyard()
|
||||
.getCards(game)
|
||||
.stream();
|
||||
playerIds = Collections.singletonList(ability.getControllerId());
|
||||
break;
|
||||
case OPPONENTS:
|
||||
playerIds = game.getOpponents(ability.getControllerId());
|
||||
break;
|
||||
|
|
@ -69,13 +73,47 @@ public enum CardTypesInGraveyardCount implements DynamicValue {
|
|||
playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
throw new IllegalArgumentException("Wrong code usage: miss implementation for " + this);
|
||||
}
|
||||
return playerIds.stream()
|
||||
.map(game::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.map(Player::getGraveyard)
|
||||
.map(graveyard -> graveyard.getCards(game))
|
||||
.flatMap(Collection::stream);
|
||||
.flatMap(Collection::stream)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(card -> !card.isCopy() && !(card instanceof PermanentToken));
|
||||
}
|
||||
}
|
||||
|
||||
class CardTypesInGraveyardHint implements Hint {
|
||||
|
||||
CardTypesInGraveyardCount value;
|
||||
|
||||
CardTypesInGraveyardHint(CardTypesInGraveyardCount value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private CardTypesInGraveyardHint(final CardTypesInGraveyardHint hint) {
|
||||
this.value = hint.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(Game game, Ability ability) {
|
||||
Stream<Card> stream = this.value.getGraveyardCards(game, ability);
|
||||
List<String> types = stream
|
||||
.map(card -> card.getCardType(game))
|
||||
.flatMap(Collection::stream)
|
||||
.distinct()
|
||||
.map(CardType::toString)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
return "Card types in " + this.value.getMessage() + ": " + types.size()
|
||||
+ (types.size() > 0 ? " (" + String.join(", ", types) + ')' : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CardTypesInGraveyardHint copy() {
|
||||
return new CardTypesInGraveyardHint(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,9 @@ import mage.abilities.Ability;
|
|||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
|
|
@ -13,8 +16,10 @@ public enum ControllerSpeedCount implements DynamicValue {
|
|||
|
||||
@Override
|
||||
public int calculate(Game game, Ability sourceAbility, Effect effect) {
|
||||
// TODO: Implement this
|
||||
return 0;
|
||||
return Optional
|
||||
.ofNullable(game.getPlayer(sourceAbility.getControllerId()))
|
||||
.map(Player::getSpeed)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package mage.abilities.dynamicvalue.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.game.Game;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public enum SavedDiscardValue implements DynamicValue {
|
||||
MANY("many"),
|
||||
MUCH("much");
|
||||
|
||||
private final String message;
|
||||
|
||||
SavedDiscardValue(String message) {
|
||||
this.message = "that " + message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int calculate(Game game, Ability sourceAbility, Effect effect) {
|
||||
return (Integer) effect.getValue("discarded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedDiscardValue copy() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
@ -1487,8 +1487,12 @@ public class ContinuousEffects implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
public int getTotalEffectsCount() {
|
||||
return allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Effects: " + allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum();
|
||||
return "Effects: " + getTotalEffectsCount();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import mage.constants.Outcome;
|
|||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* @author LevelX2
|
||||
|
|
@ -49,7 +50,7 @@ public class ChooseModeEffect extends OneShotEffect {
|
|||
}
|
||||
if (controller != null && sourcePermanent != null) {
|
||||
Choice choice = new ChoiceImpl(true);
|
||||
choice.setMessage(choiceMessage);
|
||||
choice.setMessage(choiceMessage + CardUtil.getSourceLogName(game, source));
|
||||
choice.getChoices().addAll(modes);
|
||||
if (controller.choose(Outcome.Neutral, choice, game)) {
|
||||
if (!game.isSimulation()) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package mage.abilities.effects.common;
|
||||
|
||||
import mage.abilities.TriggeredAbilityImpl;
|
||||
import mage.abilities.effects.Effect;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class DiscardOneOrMoreCardsTriggeredAbility extends TriggeredAbilityImpl {
|
||||
|
||||
public DiscardOneOrMoreCardsTriggeredAbility(Effect effect) {
|
||||
this(effect, false);
|
||||
}
|
||||
|
||||
public DiscardOneOrMoreCardsTriggeredAbility(Effect effect, boolean optional) {
|
||||
this(Zone.BATTLEFIELD, effect, optional);
|
||||
}
|
||||
|
||||
public DiscardOneOrMoreCardsTriggeredAbility(Zone zone, Effect effect, boolean optional) {
|
||||
super(zone, effect, optional);
|
||||
setTriggerPhrase("Whenever you discard one or more cards, ");
|
||||
}
|
||||
|
||||
private DiscardOneOrMoreCardsTriggeredAbility(final DiscardOneOrMoreCardsTriggeredAbility ability) {
|
||||
super(ability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscardOneOrMoreCardsTriggeredAbility copy() {
|
||||
return new DiscardOneOrMoreCardsTriggeredAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.DISCARDED_CARDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkTrigger(GameEvent event, Game game) {
|
||||
if (!isControlledBy(event.getPlayerId())) {
|
||||
return false;
|
||||
}
|
||||
this.getEffects().setValue("discarded", event.getAmount());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ public class LoseLifeOpponentsYouGainLifeLostEffect extends OneShotEffect {
|
|||
return true;
|
||||
}
|
||||
int totalLifeLost = game
|
||||
.getOpponents(source.getControllerId())
|
||||
.getOpponents(source.getControllerId(), true)
|
||||
.stream()
|
||||
.map(game::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ public class GainAbilitySourceEffect extends ContinuousEffectImpl {
|
|||
super(duration, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility);
|
||||
this.ability = ability;
|
||||
this.onCard = onCard;
|
||||
this.staticText = "{this} gains " + CardUtil.stripReminderText(ability.getRule())
|
||||
+ (duration.toString().isEmpty() ? "" : ' ' + duration.toString());
|
||||
this.staticText = "{this} " + (duration == Duration.WhileOnBattlefield ? "has" : "gains") +
|
||||
' ' + CardUtil.stripReminderText(ability.getRule()) +
|
||||
(duration.toString().isEmpty() ? "" : ' ' + duration.toString());
|
||||
this.generateGainAbilityDependencies(ability, null);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ import java.util.Set;
|
|||
* entering the battlefield, that card isn’t manifested. Its characteristics remain unmodified and it remains in
|
||||
* its previous zone. If it was face up, it remains face up.
|
||||
* <p>
|
||||
* 701.34g TODO: need support it
|
||||
* 701.34g
|
||||
* If a manifested permanent that’s represented by an instant or sorcery card would turn face up, its controller
|
||||
* reveals it and leaves it face down. Abilities that trigger whenever a permanent is turned face up won’t trigger.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public class ConditionTrueHint implements Hint {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Hint copy() {
|
||||
public ConditionTrueHint copy() {
|
||||
return new ConditionTrueHint(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ public class StaticHint implements Hint {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Hint copy() {
|
||||
public StaticHint copy() {
|
||||
return new StaticHint(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
package mage.abilities.hint.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.hint.Hint;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.CardType;
|
||||
import mage.game.Game;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* @author JayDi85
|
||||
*/
|
||||
public enum CardTypesInGraveyardHint implements Hint {
|
||||
|
||||
YOU("your graveyard"),
|
||||
ALL("all graveyards"),
|
||||
OPPONENTS("your opponents' graveyards");
|
||||
private final String message;
|
||||
|
||||
CardTypesInGraveyardHint(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(Game game, Ability ability) {
|
||||
Stream<Card> stream = getStream(game, ability);
|
||||
if (stream == null) {
|
||||
return null;
|
||||
}
|
||||
List<String> types = stream
|
||||
.map(card -> card.getCardType(game))
|
||||
.flatMap(Collection::stream)
|
||||
.distinct()
|
||||
.map(CardType::toString)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
return "Card types in " + this.message + ": " + types.size()
|
||||
+ (types.size() > 0 ? " (" + String.join(", ", types) + ')' : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Hint copy() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private final Stream<Card> getStream(Game game, Ability ability) {
|
||||
Collection<UUID> playerIds;
|
||||
switch (this) {
|
||||
case YOU:
|
||||
Player player = game.getPlayer(ability.getControllerId());
|
||||
return player == null
|
||||
? null : player
|
||||
.getGraveyard()
|
||||
.getCards(game)
|
||||
.stream();
|
||||
case OPPONENTS:
|
||||
playerIds = game.getOpponents(ability.getControllerId());
|
||||
break;
|
||||
case ALL:
|
||||
playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return playerIds.stream()
|
||||
.map(game::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.map(Player::getGraveyard)
|
||||
.map(graveyard -> graveyard.getCards(game))
|
||||
.flatMap(Collection::stream);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
package mage.abilities.hint.common;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.common.CountersOnPermanentsCondition;
|
||||
import mage.abilities.hint.Hint;
|
||||
import mage.counters.Counter;
|
||||
import mage.counters.CounterType;
|
||||
import mage.filter.FilterPermanent;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.abilities.condition.common.CountersOnPermanentsCondition;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
/**
|
||||
* A hint which keeps track of how many counters of a specific type there are
|
||||
* among some type of permanents
|
||||
*
|
||||
*
|
||||
* @author alexander-novo
|
||||
*/
|
||||
public class CountersOnPermanentsHint implements Hint {
|
||||
|
|
@ -23,6 +23,10 @@ public class CountersOnPermanentsHint implements Hint {
|
|||
// Which counter type to count
|
||||
public final CounterType counterType;
|
||||
|
||||
public CountersOnPermanentsHint(CountersOnPermanentsCondition condition) {
|
||||
this(condition.filter, condition.counterType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param filter Which permanents to consider counters on
|
||||
* @param counterType Which counter type to count
|
||||
|
|
@ -32,12 +36,9 @@ public class CountersOnPermanentsHint implements Hint {
|
|||
this.counterType = counterType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy parameters from a {@link CountersOnPermanentsCondition}
|
||||
*/
|
||||
public CountersOnPermanentsHint(CountersOnPermanentsCondition condition) {
|
||||
this.filter = condition.filter;
|
||||
this.counterType = condition.counterType;
|
||||
public CountersOnPermanentsHint(final CountersOnPermanentsHint hint) {
|
||||
this.filter = hint.filter.copy();
|
||||
this.counterType = hint.counterType;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -56,7 +57,7 @@ public class CountersOnPermanentsHint implements Hint {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Hint copy() {
|
||||
return this;
|
||||
public CountersOnPermanentsHint copy() {
|
||||
return new CountersOnPermanentsHint(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import java.util.UUID;
|
|||
public class ChampionAbility extends StaticAbility {
|
||||
|
||||
protected final String objectDescription;
|
||||
protected final String etbObjectDescription;
|
||||
|
||||
/**
|
||||
* Champion one or more creature types or if the subtype array is empty
|
||||
|
|
@ -59,6 +60,8 @@ public class ChampionAbility extends StaticAbility {
|
|||
public ChampionAbility(Card card, SubType... subtypes) {
|
||||
super(Zone.BATTLEFIELD, null);
|
||||
|
||||
this.etbObjectDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(card);
|
||||
|
||||
List<SubType> subTypes = Arrays.asList(subtypes);
|
||||
FilterControlledPermanent filter;
|
||||
switch (subTypes.size()) {
|
||||
|
|
@ -105,6 +108,7 @@ public class ChampionAbility extends StaticAbility {
|
|||
protected ChampionAbility(final ChampionAbility ability) {
|
||||
super(ability);
|
||||
this.objectDescription = ability.objectDescription;
|
||||
this.etbObjectDescription = ability.etbObjectDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -115,7 +119,7 @@ public class ChampionAbility extends StaticAbility {
|
|||
@Override
|
||||
public String getRule() {
|
||||
return "Champion " + CardUtil.addArticle(objectDescription)
|
||||
+ " <i>(When this enters the battlefield, sacrifice it unless you exile another " + objectDescription
|
||||
+ " <i>(When " + etbObjectDescription + " enters, sacrifice it unless you exile another " + objectDescription
|
||||
+ " you control. When this leaves the battlefield, that card returns to the battlefield.)</i>";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public class DecayedAbility extends StaticAbility {
|
|||
super(Zone.BATTLEFIELD, new CantBlockSourceEffect(Duration.WhileOnBattlefield));
|
||||
this.addSubAbility(new AttacksTriggeredAbility(new CreateDelayedTriggeredAbilityEffect(
|
||||
new AtTheEndOfCombatDelayedTriggeredAbility(new SacrificeSourceEffect())
|
||||
).setText("sacrifice it at end of combat")).setTriggerPhrase("When {this} attacks, "));
|
||||
).setText("sacrifice it at end of combat")).setTriggerPhrase("When {this} attacks, ").setRuleVisible(false));
|
||||
}
|
||||
|
||||
private DecayedAbility(final DecayedAbility ability) {
|
||||
|
|
@ -32,6 +32,6 @@ public class DecayedAbility extends StaticAbility {
|
|||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return "decayed";
|
||||
return "decayed <i>(This creature can't block. When it attacks, sacrifice it at end of combat.)</i>";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,10 +63,16 @@ public class DelveAbility extends SimpleStaticAbility implements AlternateManaPa
|
|||
|
||||
private static final DynamicValue cardsInGraveyard = new CardsInControllerGraveyardCount();
|
||||
|
||||
public DelveAbility() {
|
||||
private boolean useSourceExileZone;
|
||||
|
||||
/**
|
||||
* @param useSourceExileZone - keep exiled cards in linked source zone, so next ability can find it
|
||||
*/
|
||||
public DelveAbility(boolean useSourceExileZone) {
|
||||
super(Zone.ALL, null);
|
||||
this.setRuleAtTheTop(true);
|
||||
this.addHint(new ValueHint("Cards in your graveyard", cardsInGraveyard));
|
||||
this.useSourceExileZone = useSourceExileZone;
|
||||
}
|
||||
|
||||
protected DelveAbility(final DelveAbility ability) {
|
||||
|
|
@ -101,8 +107,11 @@ public class DelveAbility extends SimpleStaticAbility implements AlternateManaPa
|
|||
unpaidAmount = 1;
|
||||
}
|
||||
specialAction.addCost(new ExileFromGraveCost(new TargetCardInYourGraveyard(
|
||||
0, Math.min(controller.getGraveyard().size(), unpaidAmount),
|
||||
new FilterCard("cards from your graveyard"), true)));
|
||||
0,
|
||||
Math.min(controller.getGraveyard().size(), unpaidAmount),
|
||||
new FilterCard("cards from your graveyard"),
|
||||
true
|
||||
)).withSourceExileZone(this.useSourceExileZone));
|
||||
if (specialAction.canActivate(source.getControllerId(), game).canActivate()) {
|
||||
game.getState().getSpecialActions().add(specialAction);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import mage.constants.Outcome;
|
|||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.events.GameEvent.EventType;
|
||||
import mage.players.Player;
|
||||
import mage.util.CardUtil;
|
||||
|
||||
|
|
@ -63,13 +62,16 @@ class DredgeEffect extends ReplacementEffectImpl {
|
|||
if (sourceCard == null) {
|
||||
return false;
|
||||
}
|
||||
Player owner = game.getPlayer(game.getCard(source.getSourceId()).getOwnerId());
|
||||
if (owner != null
|
||||
&& owner.getLibrary().size() >= amount
|
||||
&& owner.chooseUse(outcome, new StringBuilder("Dredge ").append(sourceCard.getLogName()).
|
||||
append("? (").append(amount).append(" cards are milled)").toString(), source, game)) {
|
||||
Player owner = game.getPlayer(sourceCard.getOwnerId());
|
||||
if (owner == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String message = "Dredge " + sourceCard.getLogName() + "? (" + amount + " cards are milled)";
|
||||
|
||||
if (owner.getLibrary().size() >= amount && owner.chooseUse(outcome, message, source, game)) {
|
||||
if (!game.isSimulation()) {
|
||||
game.informPlayers(new StringBuilder(owner.getLogName()).append(" dredges ").append(sourceCard.getLogName()).toString());
|
||||
game.informPlayers(owner.getLogName() + " dredges " + sourceCard.getLogName() + CardUtil.getSourceLogName(game, source));
|
||||
}
|
||||
owner.millCards(amount, source, game);
|
||||
owner.moveCards(sourceCard, Zone.HAND, source, game);
|
||||
|
|
@ -85,9 +87,14 @@ class DredgeEffect extends ReplacementEffectImpl {
|
|||
|
||||
@Override
|
||||
public boolean applies(GameEvent event, Ability source, Game game) {
|
||||
Player owner = game.getPlayer(game.getCard(source.getSourceId()).getOwnerId());
|
||||
return (owner != null
|
||||
&& event.getPlayerId().equals(owner.getId())
|
||||
&& owner.getLibrary().size() >= amount);
|
||||
Card card = game.getCard(source.getSourceId());
|
||||
if (card == null) {
|
||||
return false;
|
||||
}
|
||||
Player owner = game.getPlayer(card.getOwnerId());
|
||||
if (owner == null) {
|
||||
return false;
|
||||
}
|
||||
return event.getPlayerId().equals(owner.getId()) && owner.getLibrary().size() >= amount;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,21 +36,25 @@ import java.util.*;
|
|||
public class HideawayAbility extends EntersBattlefieldTriggeredAbility {
|
||||
|
||||
private final int amount;
|
||||
private final String etbObjectDescription;
|
||||
|
||||
public HideawayAbility(int amount) {
|
||||
public HideawayAbility(Card card, int amount) {
|
||||
super(new HideawayExileEffect(amount));
|
||||
this.amount = amount;
|
||||
this.addWatcher(new HideawayWatcher());
|
||||
|
||||
this.etbObjectDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(card);
|
||||
}
|
||||
|
||||
private HideawayAbility(final HideawayAbility ability) {
|
||||
super(ability);
|
||||
this.amount = ability.amount;
|
||||
this.etbObjectDescription = ability.etbObjectDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return "Hideaway " + this.amount + " <i>(When this permanent enters the battlefield, look at the top "
|
||||
return "Hideaway " + this.amount + " <i>(When " + this.etbObjectDescription + " enters, look at the top "
|
||||
+ CardUtil.numberToText(this.amount) + " cards of your library, exile one face down, " +
|
||||
"then put the rest on the bottom of your library in a random order.)</i>";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
package mage.abilities.keyword;
|
||||
|
||||
import mage.abilities.StaticAbility;
|
||||
import mage.abilities.dynamicvalue.common.ControllerSpeedCount;
|
||||
import mage.abilities.hint.Hint;
|
||||
import mage.abilities.hint.ValueHint;
|
||||
import mage.constants.Zone;
|
||||
|
||||
/**
|
||||
* TODO: Implement this
|
||||
*
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class StartYourEnginesAbility extends StaticAbility {
|
||||
|
||||
private static final Hint hint = new ValueHint("Your current speed", ControllerSpeedCount.instance);
|
||||
|
||||
public StartYourEnginesAbility() {
|
||||
super(Zone.BATTLEFIELD, null);
|
||||
this.addHint(hint);
|
||||
}
|
||||
|
||||
private StartYourEnginesAbility(final StartYourEnginesAbility ability) {
|
||||
|
|
@ -25,6 +29,6 @@ public class StartYourEnginesAbility extends StaticAbility {
|
|||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return "Start your engines!";
|
||||
return "start your engines!";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package mage.abilities.mana.builder.common;
|
||||
|
||||
import mage.ConditionalMana;
|
||||
import mage.Mana;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.condition.Condition;
|
||||
import mage.abilities.costs.Cost;
|
||||
import mage.abilities.mana.builder.ConditionalManaBuilder;
|
||||
import mage.abilities.mana.conditional.ManaCondition;
|
||||
import mage.game.Game;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class ActivatedAbilityManaBuilder extends ConditionalManaBuilder {
|
||||
|
||||
@Override
|
||||
public ConditionalMana build(Object... options) {
|
||||
return new ActivatedAbilityConditionalMana(this.mana);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return "Spend this mana only to activate abilities";
|
||||
}
|
||||
}
|
||||
|
||||
class ActivatedAbilityConditionalMana extends ConditionalMana {
|
||||
|
||||
public ActivatedAbilityConditionalMana(Mana mana) {
|
||||
super(mana);
|
||||
staticText = "Spend this mana only to activate abilities";
|
||||
addCondition(new ActivatedAbilityManaCondition());
|
||||
}
|
||||
}
|
||||
|
||||
class ActivatedAbilityManaCondition extends ManaCondition implements Condition {
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
return source != null
|
||||
&& !source.isActivated()
|
||||
&& source.isActivatedAbility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source, UUID originalId, Cost costsToPay) {
|
||||
return apply(game, source);
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ public class OrTriggeredAbility extends TriggeredAbilityImpl {
|
|||
//Remove useless data
|
||||
ability.getEffects().clear();
|
||||
|
||||
for(Watcher watcher : ability.getWatchers()) {
|
||||
for (Watcher watcher : ability.getWatchers()) {
|
||||
super.addWatcher(watcher);
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +51,21 @@ public class OrTriggeredAbility extends TriggeredAbilityImpl {
|
|||
}
|
||||
}
|
||||
setTriggerPhrase(generateTriggerPhrase());
|
||||
|
||||
// runtime check: enters and sacrifice must use Zone.ALL, see https://github.com/magefree/mage/issues/12826
|
||||
boolean haveEnters = false;
|
||||
boolean haveSacrifice = false;
|
||||
for (Ability ability : abilities) {
|
||||
if (ability.getRule().toLowerCase(Locale.ENGLISH).contains("enters")) {
|
||||
haveEnters = true;
|
||||
}
|
||||
if (ability.getRule().toLowerCase(Locale.ENGLISH).contains("sacrifice")) {
|
||||
haveSacrifice = true;
|
||||
}
|
||||
}
|
||||
if (zone != Zone.ALL && haveEnters && haveSacrifice) {
|
||||
throw new IllegalArgumentException("Wrong code usage: on enters and sacrifice OrTriggeredAbility must use Zone.ALL");
|
||||
}
|
||||
}
|
||||
|
||||
public OrTriggeredAbility(OrTriggeredAbility ability) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import mage.MageObject;
|
|||
import mage.MageObjectImpl;
|
||||
import mage.Mana;
|
||||
import mage.abilities.*;
|
||||
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.effects.common.continuous.HasSubtypesSourceEffect;
|
||||
import mage.abilities.keyword.ChangelingAbility;
|
||||
|
|
@ -343,6 +344,19 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rules fix: workaround to fix "When {this} enters" into "When this xxx enters"
|
||||
if (EntersBattlefieldTriggeredAbility.ENABLE_TRIGGER_PHRASE_AUTO_FIX) {
|
||||
if (ability instanceof TriggeredAbility) {
|
||||
TriggeredAbility triggeredAbility = ((TriggeredAbility) ability);
|
||||
if (triggeredAbility.getTriggerPhrase() != null && triggeredAbility.getTriggerPhrase().startsWith("When {this} enters")) {
|
||||
// there are old sets with old oracle, but it's ok for newer sets, so keep that rules fix
|
||||
// see https://github.com/magefree/mage/issues/12791
|
||||
String etbDescription = EntersBattlefieldTriggeredAbility.getThisObjectDescription(this);
|
||||
triggeredAbility.setTriggerPhrase(triggeredAbility.getTriggerPhrase().replace("{this}", etbDescription));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void addAbility(Ability ability, Watcher watcher) {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,9 @@ public enum CardRepository {
|
|||
if (card.getMeldsToCardName() != null && !card.getMeldsToCardName().isEmpty()) {
|
||||
namesList.add(card.getMeldsToCardName());
|
||||
}
|
||||
if (card.getAdventureSpellName() != null && !card.getAdventureSpellName().isEmpty()) {
|
||||
namesList.add(card.getAdventureSpellName());
|
||||
}
|
||||
}
|
||||
|
||||
public static Boolean haveSnowLands(String setCode) {
|
||||
|
|
@ -157,7 +160,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
addNewNames(card, names);
|
||||
|
|
@ -173,7 +176,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%'));
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
|
|
@ -190,7 +193,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
Where<CardInfo, Object> where = qb.where();
|
||||
where.and(
|
||||
where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'),
|
||||
|
|
@ -211,7 +214,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%'));
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
|
|
@ -228,7 +231,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%'));
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
|
|
@ -245,7 +248,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%'));
|
||||
List<CardInfo> results = cardsDao.query(qb.prepare());
|
||||
for (CardInfo card : results) {
|
||||
|
|
@ -262,7 +265,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
Where<CardInfo, Object> where = qb.where();
|
||||
where.and(
|
||||
where.not().like("types", '%' + CardType.CREATURE.name() + '%'),
|
||||
|
|
@ -283,7 +286,7 @@ public enum CardRepository {
|
|||
Set<String> names = new TreeSet<>();
|
||||
try {
|
||||
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
|
||||
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName", "adventureSpellName");
|
||||
Where<CardInfo, Object> where = qb.where();
|
||||
where.and(
|
||||
where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public enum TokenRepository {
|
|||
public static final String XMAGE_IMAGE_NAME_RADIATION = "Radiation";
|
||||
public static final String XMAGE_IMAGE_NAME_THE_RING = "The Ring";
|
||||
public static final String XMAGE_IMAGE_NAME_HELPER_EMBLEM = "Helper Emblem";
|
||||
public static final String XMAGE_IMAGE_NAME_SPEED = "Speed";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(TokenRepository.class);
|
||||
|
||||
|
|
@ -310,6 +311,9 @@ public enum TokenRepository {
|
|||
// The Ring
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_THE_RING, 1, "https://api.scryfall.com/cards/tltr/H13/en?format=image"));
|
||||
|
||||
// Speed
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_SPEED, 1, "https://api.scryfall.com/cards/tdft/14/en?format=image&&face=back"));
|
||||
|
||||
// Helper emblem (for global card hints)
|
||||
// use backface for it
|
||||
res.add(createXmageToken(XMAGE_IMAGE_NAME_HELPER_EMBLEM, 1, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"));
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import java.util.UUID;
|
|||
public enum TargetController {
|
||||
|
||||
ACTIVE,
|
||||
INACTIVE,
|
||||
ANY,
|
||||
YOU,
|
||||
NOT_YOU,
|
||||
|
|
@ -85,6 +86,8 @@ public enum TargetController {
|
|||
return card.isOwnedBy(input.getSource().getFirstTarget());
|
||||
case ACTIVE:
|
||||
return card.isOwnedBy(game.getActivePlayerId());
|
||||
case INACTIVE:
|
||||
return !card.isOwnedBy(game.getActivePlayerId());
|
||||
case MONARCH:
|
||||
return card.isOwnedBy(game.getMonarchId());
|
||||
case ANY:
|
||||
|
|
@ -130,6 +133,8 @@ public enum TargetController {
|
|||
return player.getId().equals(input.getSource().getFirstTarget());
|
||||
case ACTIVE:
|
||||
return game.isActivePlayer(player.getId());
|
||||
case INACTIVE:
|
||||
return !game.isActivePlayer(player.getId());
|
||||
case MONARCH:
|
||||
return player.getId().equals(game.getMonarchId());
|
||||
default:
|
||||
|
|
@ -168,6 +173,8 @@ public enum TargetController {
|
|||
return !object.isControlledBy(playerId);
|
||||
case ACTIVE:
|
||||
return object.isControlledBy(game.getActivePlayerId());
|
||||
case INACTIVE:
|
||||
return !object.isControlledBy(game.getActivePlayerId());
|
||||
case ENCHANTED:
|
||||
Permanent permanent = input.getSource().getSourcePermanentIfItStillExists(game);
|
||||
return permanent != null && input.getObject().isControlledBy(permanent.getAttachedTo());
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ package mage.designations;
|
|||
public enum DesignationType {
|
||||
THE_MONARCH("The Monarch"), // global
|
||||
CITYS_BLESSING("City's Blessing"), // per player
|
||||
THE_INITIATIVE("The Initiative"); // global
|
||||
|
||||
THE_INITIATIVE("The Initiative"), // global
|
||||
SPEED("Speed"); // per player
|
||||
private final String text;
|
||||
|
||||
DesignationType(String text) {
|
||||
|
|
@ -18,5 +18,4 @@ public enum DesignationType {
|
|||
public String toString() {
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
122
Mage/src/main/java/mage/designations/Speed.java
Normal file
122
Mage/src/main/java/mage/designations/Speed.java
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package mage.designations;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.TriggeredAbilityImpl;
|
||||
import mage.abilities.effects.OneShotEffect;
|
||||
import mage.cards.repository.TokenInfo;
|
||||
import mage.cards.repository.TokenRepository;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.Zone;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public class Speed extends Designation {
|
||||
|
||||
public Speed() {
|
||||
super(DesignationType.SPEED);
|
||||
addAbility(new SpeedTriggeredAbility());
|
||||
|
||||
TokenInfo foundInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_SPEED, null);
|
||||
if (foundInfo != null) {
|
||||
this.setExpansionSetCode(foundInfo.getSetCode());
|
||||
this.setUsesVariousArt(true);
|
||||
this.setCardNumber("");
|
||||
this.setImageFileName(""); // use default
|
||||
this.setImageNumber(foundInfo.getImageNumber());
|
||||
} else {
|
||||
// how-to fix: add image to the tokens-database TokenRepository->loadXmageTokens
|
||||
throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_SPEED);
|
||||
}
|
||||
}
|
||||
|
||||
private Speed(final Speed card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Speed copy() {
|
||||
return new Speed(this);
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedTriggeredAbility extends TriggeredAbilityImpl {
|
||||
|
||||
SpeedTriggeredAbility() {
|
||||
super(Zone.ALL, new SpeedEffect());
|
||||
setTriggersLimitEachTurn(1);
|
||||
}
|
||||
|
||||
private SpeedTriggeredAbility(final SpeedTriggeredAbility ability) {
|
||||
super(ability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpeedTriggeredAbility copy() {
|
||||
return new SpeedTriggeredAbility(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkTrigger(GameEvent event, Game game) {
|
||||
return game.isActivePlayer(getControllerId())
|
||||
&& game
|
||||
.getOpponents(getControllerId())
|
||||
.contains(event.getTargetId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkInterveningIfClause(Game game) {
|
||||
return Optional
|
||||
.ofNullable(getControllerId())
|
||||
.map(game::getPlayer)
|
||||
.map(Player::getSpeed)
|
||||
.map(x -> x < 4)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRule() {
|
||||
return "Whenever one or more opponents lose life during your turn, if your speed is less than 4, " +
|
||||
"increase your speed by 1. This ability triggers only once each turn.";
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedEffect extends OneShotEffect {
|
||||
|
||||
SpeedEffect() {
|
||||
super(Outcome.Benefit);
|
||||
}
|
||||
|
||||
private SpeedEffect(final SpeedEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpeedEffect copy() {
|
||||
return new SpeedEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(Game game, Ability source) {
|
||||
Optional.ofNullable(source.getControllerId())
|
||||
.map(game::getPlayer)
|
||||
.ifPresent(player -> player.increaseSpeed(game));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -754,6 +754,28 @@ public final class StaticFilters {
|
|||
FILTER_PERMANENT_CREATURE_OR_LAND.setLockedFilter(true);
|
||||
}
|
||||
|
||||
public static final FilterPermanent FILTER_PERMANENT_CREATURE_OR_VEHICLE = new FilterPermanent("creature or Vehicle");
|
||||
|
||||
static {
|
||||
FILTER_PERMANENT_CREATURE_OR_VEHICLE.add(
|
||||
Predicates.or(
|
||||
CardType.CREATURE.getPredicate(),
|
||||
SubType.VEHICLE.getPredicate()
|
||||
));
|
||||
FILTER_PERMANENT_CREATURE_OR_VEHICLE.setLockedFilter(true);
|
||||
}
|
||||
|
||||
public static final FilterControlledPermanent FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE = new FilterControlledPermanent("creature or Vehicle you control");
|
||||
|
||||
static {
|
||||
FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE.add(
|
||||
Predicates.or(
|
||||
CardType.CREATURE.getPredicate(),
|
||||
SubType.VEHICLE.getPredicate()
|
||||
));
|
||||
FILTER_CONTROLLED_PERMANENT_CREATURE_OR_VEHICLE.setLockedFilter(true);
|
||||
}
|
||||
|
||||
public static final FilterCreaturePermanent FILTER_PERMANENT_A_CREATURE = new FilterCreaturePermanent("a creature");
|
||||
|
||||
static {
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
|||
*
|
||||
* Warning, it will return leaved players until end of turn. For dialogs and one shot effects use excludeLeavedPlayers
|
||||
*/
|
||||
// TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers
|
||||
// TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers, see #13289
|
||||
default Set<UUID> getOpponents(UUID playerId) {
|
||||
return getOpponents(playerId, false);
|
||||
}
|
||||
|
|
@ -314,7 +314,9 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
|||
|
||||
Player getLosingPlayer();
|
||||
|
||||
int getTotalErrorsCount();
|
||||
int getTotalErrorsCount(); // debug only
|
||||
|
||||
int getTotalEffectsCount(); // debug only
|
||||
|
||||
//client event methods
|
||||
void addTableEventListener(Listener<TableEvent> listener);
|
||||
|
|
|
|||
|
|
@ -2862,6 +2862,13 @@ public abstract class GameImpl implements Game {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start Your Engines // Max Speed
|
||||
if (perm.getAbilities(this).containsClass(StartYourEnginesAbility.class)) {
|
||||
Optional.ofNullable(perm.getControllerId())
|
||||
.map(this::getPlayer)
|
||||
.ifPresent(player -> player.initSpeed(this));
|
||||
}
|
||||
}
|
||||
//201300713 - 704.5k
|
||||
// If a player controls two or more legendary permanents with the same name, that player
|
||||
|
|
@ -3169,7 +3176,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.INFO, message, this);
|
||||
}
|
||||
|
||||
|
|
@ -3178,7 +3185,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.STATUS, message, withTime, withTurnInfo, this);
|
||||
}
|
||||
|
||||
|
|
@ -3187,7 +3194,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.UPDATE, null, this);
|
||||
getState().clearLookedAt();
|
||||
getState().clearRevealed();
|
||||
|
|
@ -3198,23 +3205,23 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.END_GAME_INFO, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireErrorEvent(String message, Exception ex) {
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.ERROR, message, ex, this);
|
||||
}
|
||||
|
||||
private void makeSureCalledOutsideLayersEffects() {
|
||||
private void makeSureCalledOutsideLayerEffects() {
|
||||
// very slow, enable/comment it for debug or load/stability tests only
|
||||
// TODO: enable check and remove/rework all wrong usages
|
||||
if (true) return;
|
||||
Arrays.stream(Thread.currentThread().getStackTrace()).forEach(e -> {
|
||||
if (e.toString().contains("GameState.applyEffects")) {
|
||||
throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?");
|
||||
throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?)");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -3542,11 +3549,6 @@ public abstract class GameImpl implements Game {
|
|||
|
||||
}
|
||||
|
||||
protected void removeCreaturesFromCombat() {
|
||||
//20091005 - 511.3
|
||||
getCombat().endCombat(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContinuousEffects getContinuousEffects() {
|
||||
return state.getContinuousEffects();
|
||||
|
|
@ -3689,6 +3691,11 @@ public abstract class GameImpl implements Game {
|
|||
return this.totalErrorsCount.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalEffectsCount() {
|
||||
return this.getContinuousEffects().getTotalEffectsCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cheat(UUID ownerId, Map<Zone, String> commands) {
|
||||
if (commands != null) {
|
||||
|
|
@ -3885,7 +3892,7 @@ public abstract class GameImpl implements Game {
|
|||
@Override
|
||||
public void initTimer(UUID playerId) {
|
||||
if (priorityTime > 0) {
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.INIT_TIMER, playerId, null, this);
|
||||
}
|
||||
}
|
||||
|
|
@ -3893,7 +3900,7 @@ public abstract class GameImpl implements Game {
|
|||
@Override
|
||||
public void resumeTimer(UUID playerId) {
|
||||
if (priorityTime > 0) {
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.RESUME_TIMER, playerId, null, this);
|
||||
}
|
||||
}
|
||||
|
|
@ -3901,7 +3908,7 @@ public abstract class GameImpl implements Game {
|
|||
@Override
|
||||
public void pauseTimer(UUID playerId) {
|
||||
if (priorityTime > 0) {
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.PAUSE_TIMER, playerId, null, this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,7 +312,23 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
Player player = game.getPlayer(attackingPlayerId);
|
||||
if (player != null) {
|
||||
if (groups.size() > 0) {
|
||||
game.informPlayers(player.getLogName() + " attacks with " + groups.size() + (groups.size() == 1 ? " creature" : " creatures"));
|
||||
String defendersInfo = groups.stream()
|
||||
.map(g -> g.defenderId)
|
||||
.distinct()
|
||||
.map(id -> {
|
||||
Player defPlayer = game.getPlayer(id);
|
||||
if (defPlayer != null) {
|
||||
return defPlayer.getLogName();
|
||||
}
|
||||
Permanent defPermanent = game.getPermanentOrLKIBattlefield(id);
|
||||
if (defPermanent != null) {
|
||||
return defPermanent.getLogName();
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.joining(", "));
|
||||
game.informPlayers(player.getLogName() + " attacks " + defendersInfo + " with " + groups.size() + (groups.size() == 1 ? " creature" : " creatures"));
|
||||
} else {
|
||||
game.informPlayers(player.getLogName() + " skip attack");
|
||||
}
|
||||
|
|
@ -670,9 +686,25 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
}
|
||||
|
||||
// choosing until good block configuration
|
||||
int aiTries = 0;
|
||||
while (true) {
|
||||
aiTries++;
|
||||
|
||||
if (controller.isComputer() && aiTries > 20) {
|
||||
// TODO: AI must use real attacker/blocker configuration with all possible combination
|
||||
// (current human like logic will fail sometime, e.g. with menace and big/low creatures)
|
||||
// real game: send warning
|
||||
// test: fast fail
|
||||
game.informPlayers(controller.getLogName() + ": WARNING - AI can't find good blocker combination and will skip it - report your battlefield to github - " + game.getCombat());
|
||||
if (controller.isTestsMode()) {
|
||||
// how-to fix: AI code must support failed abilities or use cases
|
||||
throw new IllegalArgumentException("AI can't find good blocker combination");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// declare normal blockers
|
||||
// TODO: need reseach - is it possible to concede on bad blocker configuration (e.g. user can't continue)
|
||||
// TODO: need research - is it possible to concede on bad blocker configuration (e.g. user can't continue)
|
||||
controller.selectBlockers(source, game, defenderId);
|
||||
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
|
||||
return;
|
||||
|
|
@ -776,18 +808,16 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
/**
|
||||
* Check the block restrictions
|
||||
*
|
||||
* @param player
|
||||
* @param game
|
||||
* @return false - if block restrictions were not complied
|
||||
*/
|
||||
public boolean checkBlockRestrictions(Player player, Game game) {
|
||||
public boolean checkBlockRestrictions(Player defender, Game game) {
|
||||
int count = 0;
|
||||
boolean blockWasLegal = true;
|
||||
for (CombatGroup group : groups) {
|
||||
count += group.getBlockers().size();
|
||||
}
|
||||
for (CombatGroup group : groups) {
|
||||
blockWasLegal &= group.checkBlockRestrictions(game, count);
|
||||
blockWasLegal &= group.checkBlockRestrictions(game, defender, count);
|
||||
}
|
||||
return blockWasLegal;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
* @param first true for first strike damage step, false for normal damage step
|
||||
* @return true if permanent should deal damage this step
|
||||
*/
|
||||
private boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) {
|
||||
public static boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) {
|
||||
if (perm == null) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -773,11 +773,27 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean checkBlockRestrictions(Game game, int blockersCount) {
|
||||
public boolean checkBlockRestrictions(Game game, Player defender, int blockersCount) {
|
||||
boolean blockWasLegal = true;
|
||||
if (attackers.isEmpty()) {
|
||||
return blockWasLegal;
|
||||
}
|
||||
|
||||
// collect possible blockers
|
||||
Map<UUID, Set<UUID>> possibleBlockers = new HashMap<>();
|
||||
for (UUID attackerId : attackers) {
|
||||
Permanent attacker = game.getPermanent(attackerId);
|
||||
Set<UUID> goodBlockers = new HashSet<>();
|
||||
for (Permanent blocker : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, defender.getId(), game)) {
|
||||
if (blocker.canBlock(attackerId, game)) {
|
||||
goodBlockers.add(blocker.getId());
|
||||
}
|
||||
}
|
||||
possibleBlockers.put(attacker.getId(), goodBlockers);
|
||||
}
|
||||
|
||||
// effects: can't block alone
|
||||
// too much blockers
|
||||
if (blockersCount == 1) {
|
||||
List<UUID> toBeRemoved = new ArrayList<>();
|
||||
for (UUID blockerId : getBlockers()) {
|
||||
|
|
@ -802,7 +818,8 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
for (UUID uuid : attackers) {
|
||||
Permanent attacker = game.getPermanent(uuid);
|
||||
if (attacker != null && this.blocked) {
|
||||
// Check if there are enough blockers to have a legal block
|
||||
// effects: can't be blocked except by xxx or more creatures
|
||||
// too few blockers
|
||||
if (attacker.getMinBlockedBy() > 1 && !blockers.isEmpty() && blockers.size() < attacker.getMinBlockedBy()) {
|
||||
for (UUID blockerId : new ArrayList<>(blockers)) {
|
||||
game.getCombat().removeBlocker(blockerId, game);
|
||||
|
|
@ -812,9 +829,16 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
if (!game.isSimulation()) {
|
||||
game.informPlayers(attacker.getLogName() + " can't be blocked except by " + attacker.getMinBlockedBy() + " or more creatures. Blockers discarded.");
|
||||
}
|
||||
blockWasLegal = false;
|
||||
|
||||
// if there aren't any possible blocker configuration then it's legal due mtg rules
|
||||
// warning, it's affect AI related logic like other block auto-fixes does, see https://github.com/magefree/mage/pull/13182
|
||||
if (attacker.getMinBlockedBy() <= possibleBlockers.getOrDefault(attacker.getId(), Collections.emptySet()).size()) {
|
||||
blockWasLegal = false;
|
||||
}
|
||||
}
|
||||
// Check if there are too many blockers (maxBlockedBy = 0 means no restrictions)
|
||||
|
||||
// effects: can't be blocked by more than xxx creature
|
||||
// too much blockers
|
||||
if (attacker.getMaxBlockedBy() > 0 && attacker.getMaxBlockedBy() < blockers.size()) {
|
||||
for (UUID blockerId : new ArrayList<>(blockers)) {
|
||||
game.getCombat().removeBlocker(blockerId, game);
|
||||
|
|
@ -827,6 +851,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
.append(attacker.getMaxBlockedBy() == 1 ? " creature." : " creatures.")
|
||||
.append(" Blockers discarded.").toString());
|
||||
}
|
||||
|
||||
blockWasLegal = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package mage.game.command.emblems;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.EntersBattlefieldAllTriggeredAbility;
|
||||
import mage.abilities.effects.common.DamageTargetEffect;
|
||||
import mage.constants.Zone;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.command.Emblem;
|
||||
import mage.target.common.TargetAnyTarget;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public final class ChandraSparkHunterEmblem extends Emblem {
|
||||
|
||||
/**
|
||||
* Emblem with "Whenever an artifact you control enters, this emblem deals 3 damage to any target."
|
||||
*/
|
||||
|
||||
public ChandraSparkHunterEmblem() {
|
||||
super("Emblem Chandra");
|
||||
Ability ability = new EntersBattlefieldAllTriggeredAbility(
|
||||
Zone.COMMAND, new DamageTargetEffect(3, "this emblem"),
|
||||
StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT, false
|
||||
);
|
||||
ability.addTarget(new TargetAnyTarget());
|
||||
this.getAbilities().add(ability);
|
||||
}
|
||||
|
||||
private ChandraSparkHunterEmblem(final ChandraSparkHunterEmblem card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChandraSparkHunterEmblem copy() {
|
||||
return new ChandraSparkHunterEmblem(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,11 @@ import mage.abilities.costs.mana.ManaCost;
|
|||
import mage.abilities.costs.mana.ManaCosts;
|
||||
import mage.abilities.keyword.NightboundAbility;
|
||||
import mage.abilities.keyword.TransformAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.LevelerCard;
|
||||
import mage.cards.ModalDoubleFacedCard;
|
||||
import mage.cards.SplitCard;
|
||||
import mage.cards.*;
|
||||
import mage.constants.SpellAbilityType;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ZoneChangeEvent;
|
||||
import mage.players.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -45,7 +43,7 @@ public class PermanentCard extends PermanentImpl {
|
|||
}
|
||||
|
||||
// usage check: you must put to play only real card's part
|
||||
// if you use it in test code then call CardUtil.getDefaultCardSideForBattlefield for default side
|
||||
// if you use it in test code or for permanent's copy effects then call CardUtil.getDefaultCardSideForBattlefield for default side
|
||||
// it's a basic check and still allows to create permanent from instant or sorcery
|
||||
boolean goodForBattlefield = true;
|
||||
if (card instanceof ModalDoubleFacedCard) {
|
||||
|
|
@ -185,7 +183,10 @@ public class PermanentCard extends PermanentImpl {
|
|||
// 701.34g. If a manifested permanent that's represented by an instant or sorcery card would turn face up,
|
||||
// its controller reveals it and leaves it face down. Abilities that trigger whenever a permanent
|
||||
// is turned face up won't trigger.
|
||||
// TODO: add reveal effect
|
||||
Player player = game.getPlayer(source.getControllerId());
|
||||
if (player != null) {
|
||||
player.revealCards(source, new CardsImpl(this), game);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (super.turnFaceUp(source, game, playerId)) {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class AshiokNightmareMuseTokenEffect extends OneShotEffect {
|
|||
return false;
|
||||
}
|
||||
Set<Card> cards = game
|
||||
.getOpponents(source.getControllerId())
|
||||
.getOpponents(source.getControllerId(), true)
|
||||
.stream()
|
||||
.map(game::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ import mage.constants.Zone;
|
|||
*/
|
||||
public final class ConsumingBlobOozeToken extends TokenImpl {
|
||||
|
||||
private static final DynamicValue powerValue = CardTypesInGraveyardCount.YOU;
|
||||
|
||||
public ConsumingBlobOozeToken() {
|
||||
super("Ooze Token", "green Ooze creature token with \"This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1.\"");
|
||||
cardType.add(CardType.CREATURE);
|
||||
|
|
@ -26,7 +24,9 @@ public final class ConsumingBlobOozeToken extends TokenImpl {
|
|||
toughness = new MageInt(1);
|
||||
|
||||
// This creature's power is equal to the number of card types among cards in your graveyard and its toughness is equal to that number plus 1.
|
||||
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(powerValue)));
|
||||
this.addAbility(new SimpleStaticAbility(Zone.ALL,
|
||||
new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.YOU)
|
||||
).addHint(CardTypesInGraveyardCount.YOU.getHint()));
|
||||
}
|
||||
|
||||
private ConsumingBlobOozeToken(final ConsumingBlobOozeToken token) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import mage.constants.SubType;
|
|||
public final class PilotSaddleCrewToken extends TokenImpl {
|
||||
|
||||
public PilotSaddleCrewToken() {
|
||||
super("Pilot Token", "1/1 colorless Pilot creature token with \"This creature saddles Mounts and crews Vehicles as though its power were 2 greater.\"");
|
||||
super("Pilot Token", "1/1 colorless Pilot creature token with \"This token saddles Mounts and crews Vehicles as though its power were 2 greater.\"");
|
||||
cardType.add(CardType.CREATURE);
|
||||
subtype.add(SubType.PILOT);
|
||||
power = new MageInt(1);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ public final class TarmogoyfToken extends TokenImpl {
|
|||
toughness = new MageInt(1);
|
||||
|
||||
// Tarmogoyf's power is equal to the number of card types among cards in all graveyards and its toughness is equal to that number plus 1.
|
||||
this.addAbility(new SimpleStaticAbility(Zone.ALL, new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL)));
|
||||
this.addAbility(new SimpleStaticAbility(Zone.ALL,
|
||||
new SetBasePowerToughnessPlusOneSourceEffect(CardTypesInGraveyardCount.ALL)
|
||||
).addHint(CardTypesInGraveyardCount.ALL.getHint()));
|
||||
}
|
||||
|
||||
private TarmogoyfToken(final TarmogoyfToken token) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package mage.game.permanent.token;
|
||||
|
||||
import mage.MageInt;
|
||||
import mage.abilities.keyword.CrewAbility;
|
||||
import mage.constants.CardType;
|
||||
import mage.constants.SubType;
|
||||
|
||||
/**
|
||||
* @author TheElk801
|
||||
*/
|
||||
public final class VehicleToken extends TokenImpl {
|
||||
|
||||
public VehicleToken() {
|
||||
super("Vehicle Token", "3/2 colorless Vehicle artifact token with crew 1");
|
||||
cardType.add(CardType.ARTIFACT);
|
||||
subtype.add(SubType.VEHICLE);
|
||||
power = new MageInt(3);
|
||||
toughness = new MageInt(2);
|
||||
|
||||
this.addAbility(new CrewAbility(1));
|
||||
}
|
||||
|
||||
private VehicleToken(final VehicleToken token) {
|
||||
super(token);
|
||||
}
|
||||
|
||||
public VehicleToken copy() {
|
||||
return new VehicleToken(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -216,6 +216,14 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
|
||||
boolean isDrawsOnOpponentsTurn();
|
||||
|
||||
int getSpeed();
|
||||
|
||||
void initSpeed(Game game);
|
||||
|
||||
void increaseSpeed(Game game);
|
||||
|
||||
void decreaseSpeed(Game game);
|
||||
|
||||
/**
|
||||
* Returns alternative casting costs a player can cast spells for
|
||||
*
|
||||
|
|
@ -620,6 +628,7 @@ public interface Player extends MageItem, Copyable<Player> {
|
|||
* <p>
|
||||
* Warning, if you use it from continuous effect, then check with extra call
|
||||
* isCanLookAtNextTopLibraryCard
|
||||
* If you use revealCards with face-down permanents, they will be revealed face up.
|
||||
*
|
||||
* @param source
|
||||
* @param name
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import mage.counters.CounterType;
|
|||
import mage.counters.Counters;
|
||||
import mage.designations.Designation;
|
||||
import mage.designations.DesignationType;
|
||||
import mage.designations.Speed;
|
||||
import mage.filter.FilterCard;
|
||||
import mage.filter.FilterMana;
|
||||
import mage.filter.FilterPermanent;
|
||||
|
|
@ -153,6 +154,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
protected boolean canPlotFromTopOfLibrary = false;
|
||||
protected boolean drawsFromBottom = false;
|
||||
protected boolean drawsOnOpponentsTurn = false;
|
||||
protected int speed = 0;
|
||||
|
||||
protected FilterPermanent sacrificeCostFilter;
|
||||
protected List<AlternativeSourceCosts> alternativeSourceCosts = new ArrayList<>();
|
||||
|
|
@ -252,6 +254,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary;
|
||||
this.drawsFromBottom = player.drawsFromBottom;
|
||||
this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn;
|
||||
this.speed = player.speed;
|
||||
|
||||
this.attachments.addAll(player.attachments);
|
||||
|
||||
|
|
@ -367,6 +370,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.drawsFromBottom = player.isDrawsFromBottom();
|
||||
this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn();
|
||||
this.alternativeSourceCosts = CardUtil.deepCopyObject(((PlayerImpl) player).alternativeSourceCosts);
|
||||
this.speed = player.getSpeed();
|
||||
|
||||
this.topCardRevealed = player.isTopCardRevealed();
|
||||
|
||||
|
|
@ -480,6 +484,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
this.canPlotFromTopOfLibrary = false;
|
||||
this.drawsFromBottom = false;
|
||||
this.drawsOnOpponentsTurn = false;
|
||||
this.speed = 0;
|
||||
|
||||
this.sacrificeCostFilter = null;
|
||||
this.alternativeSourceCosts.clear();
|
||||
|
|
@ -1905,7 +1910,11 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
int last = cards.size();
|
||||
for (Card card : cards.getCards(game)) {
|
||||
current++;
|
||||
sb.append(GameLog.getColoredObjectName(card)); // TODO: see same usage in OfferingAbility for hide card's id (is it needs for reveal too?!)
|
||||
if (card instanceof PermanentCard && card.isFaceDown(game)) {
|
||||
sb.append(GameLog.getColoredObjectName(card.getMainCard()));
|
||||
} else {
|
||||
sb.append(GameLog.getColoredObjectName(card)); // TODO: see same usage in OfferingAbility for hide card's id (is it needs for reveal too?!)
|
||||
}
|
||||
if (current < last) {
|
||||
sb.append(", ");
|
||||
}
|
||||
|
|
@ -4452,11 +4461,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Only used for AIs
|
||||
*
|
||||
* @param ability
|
||||
* @param game
|
||||
* @return
|
||||
* AI related code
|
||||
*/
|
||||
@Override
|
||||
public List<Ability> getPlayableOptions(Ability ability, Game game) {
|
||||
|
|
@ -4477,6 +4482,9 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI related code
|
||||
*/
|
||||
private void addModeOptions(List<Ability> options, Ability option, Game game) {
|
||||
// TODO: support modal spells with more than one selectable mode (also must use max modes filter)
|
||||
for (Mode mode : option.getModes().values()) {
|
||||
|
|
@ -4499,11 +4507,18 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI related code
|
||||
*/
|
||||
protected void addVariableXOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
||||
addTargetOptions(options, option, targetNum, game);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI related code
|
||||
*/
|
||||
protected void addTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
||||
// TODO: target options calculated for triggered ability too, but do not used in real game
|
||||
for (Target target : option.getTargets().getUnchosen(game).get(targetNum).getTargetOptions(option, game)) {
|
||||
Ability newOption = option.copy();
|
||||
if (target instanceof TargetAmount) {
|
||||
|
|
@ -4516,7 +4531,7 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
newOption.getTargets().get(targetNum).addTarget(targetId, newOption, game, true);
|
||||
}
|
||||
}
|
||||
if (targetNum < option.getTargets().size() - 2) {
|
||||
if (targetNum < option.getTargets().size() - 2) { // wtf
|
||||
addTargetOptions(options, newOption, targetNum + 1, game);
|
||||
} else if (!option.getCosts().getTargets().isEmpty()) {
|
||||
addCostTargetOptions(options, newOption, 0, game);
|
||||
|
|
@ -4526,6 +4541,9 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI related code
|
||||
*/
|
||||
private void addCostTargetOptions(List<Ability> options, Ability option, int targetNum, Game game) {
|
||||
for (UUID targetId : option.getCosts().getTargets().get(targetNum).possibleTargets(playerId, option, game)) {
|
||||
Ability newOption = option.copy();
|
||||
|
|
@ -4674,6 +4692,37 @@ public abstract class PlayerImpl implements Player, Serializable {
|
|||
return drawsOnOpponentsTurn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initSpeed(Game game) {
|
||||
if (speed > 0) {
|
||||
return;
|
||||
}
|
||||
speed = 1;
|
||||
game.getState().addDesignation(new Speed(), game, getId());
|
||||
game.informPlayers(this.getLogName() + "'s speed is now 1.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void increaseSpeed(Game game) {
|
||||
if (speed < 4) {
|
||||
speed++;
|
||||
game.informPlayers(this.getLogName() + "'s speed has increased to " + speed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decreaseSpeed(Game game) {
|
||||
if (speed > 1) {
|
||||
speed--;
|
||||
game.informPlayers(this.getLogName() + "'s speed has decreased to " + speed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean autoLoseGame() {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -79,6 +79,9 @@ public interface Target extends Serializable {
|
|||
|
||||
boolean isLegal(Ability source, Game game);
|
||||
|
||||
/**
|
||||
* AI related code. Returns all possible different target combinations
|
||||
*/
|
||||
List<? extends Target> getTargetOptions(Ability source, Game game);
|
||||
|
||||
boolean canChoose(UUID sourceControllerId, Game game);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
package mage.target;
|
||||
|
||||
import mage.MageObject;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.dynamicvalue.common.StaticValue;
|
||||
import mage.cards.Card;
|
||||
import mage.constants.Outcome;
|
||||
import mage.game.Game;
|
||||
import mage.game.permanent.Permanent;
|
||||
import mage.players.Player;
|
||||
import mage.util.DebugUtil;
|
||||
import mage.util.RandomUtil;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -118,45 +123,229 @@ public abstract class TargetAmount extends TargetImpl {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<? extends TargetAmount> getTargetOptions(Ability source, Game game) {
|
||||
final public List<? extends TargetAmount> getTargetOptions(Ability source, Game game) {
|
||||
if (!amountWasSet) {
|
||||
setAmount(source, game);
|
||||
}
|
||||
|
||||
List<TargetAmount> options = new ArrayList<>();
|
||||
Set<UUID> possibleTargets = possibleTargets(source.getControllerId(), source, game);
|
||||
|
||||
addTargets(this, possibleTargets, options, source, game);
|
||||
// optimizations for less memory/cpu consumptions
|
||||
printTargetsTableAndVariations("before optimize", game, possibleTargets, options, false);
|
||||
optimizePossibleTargets(source, game, possibleTargets);
|
||||
printTargetsTableAndVariations("after optimize", game, possibleTargets, options, false);
|
||||
|
||||
// debug target variations
|
||||
//printTargetsVariations(possibleTargets, options);
|
||||
// calc possible amount variations
|
||||
addTargets(this, possibleTargets, options, source, game);
|
||||
printTargetsTableAndVariations("after calc", game, possibleTargets, options, true);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private void printTargetsVariations(Set<UUID> possibleTargets, List<TargetAmount> options) {
|
||||
// debug target variations
|
||||
// permanent index + amount
|
||||
// example: 7 -> 2; 8 -> 3; 9 -> 1
|
||||
/**
|
||||
* AI related, trying to reduce targets for simulations
|
||||
*/
|
||||
private void optimizePossibleTargets(Ability source, Game game, Set<UUID> possibleTargets) {
|
||||
// remove duplicated/same creatures (example: distribute 3 damage between 10+ same tokens)
|
||||
|
||||
// it must have additional threshold to keep more variations for analyse
|
||||
//
|
||||
// bad example:
|
||||
// - Blessings of Nature
|
||||
// - Distribute four +1/+1 counters among any number of target creatures.
|
||||
// on low targets threshold AI can put 1/1 to opponent's creature instead own, see TargetAmountAITest.test_AI_SimulateTargets
|
||||
|
||||
int maxPossibleTargetsToSimulate = this.remainingAmount * 2;
|
||||
if (possibleTargets.size() < maxPossibleTargetsToSimulate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// split targets by groups
|
||||
Map<UUID, String> targetGroups = new HashMap<>();
|
||||
possibleTargets.forEach(id -> {
|
||||
String groupKey = "";
|
||||
|
||||
// player
|
||||
Player player = game.getPlayer(id);
|
||||
if (player != null) {
|
||||
groupKey = getTargetGroupKeyAsPlayer(player);
|
||||
}
|
||||
|
||||
// game object
|
||||
MageObject object = game.getObject(id);
|
||||
if (object != null) {
|
||||
groupKey = object.getName();
|
||||
if (object instanceof Permanent) {
|
||||
groupKey += getTargetGroupKeyAsPermanent(game, (Permanent) object);
|
||||
} else if (object instanceof Card) {
|
||||
groupKey += getTargetGroupKeyAsCard(game, (Card) object);
|
||||
} else {
|
||||
groupKey += getTargetGroupKeyAsOther(game, object);
|
||||
}
|
||||
}
|
||||
|
||||
// unknown - use all
|
||||
if (groupKey.isEmpty()) {
|
||||
groupKey = id.toString();
|
||||
}
|
||||
|
||||
targetGroups.put(id, groupKey);
|
||||
});
|
||||
|
||||
Map<String, List<UUID>> groups = new HashMap<>();
|
||||
targetGroups.forEach((id, groupKey) -> {
|
||||
groups.computeIfAbsent(groupKey, k -> new ArrayList<>());
|
||||
groups.get(groupKey).add(id);
|
||||
});
|
||||
|
||||
// optimize logic:
|
||||
// - use one target from each target group all the time
|
||||
// - add random target from random group until fill all remainingAmount condition
|
||||
|
||||
// use one target per group
|
||||
Set<UUID> newPossibleTargets = new HashSet<>();
|
||||
groups.forEach((groupKey, groupTargets) -> {
|
||||
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
||||
if (targetId != null) {
|
||||
newPossibleTargets.add(targetId);
|
||||
groupTargets.remove(targetId);
|
||||
}
|
||||
});
|
||||
|
||||
// use random target until fill condition
|
||||
while (newPossibleTargets.size() < maxPossibleTargetsToSimulate) {
|
||||
String groupKey = RandomUtil.randomFromCollection(groups.keySet());
|
||||
if (groupKey == null) {
|
||||
break;
|
||||
}
|
||||
List<UUID> groupTargets = groups.getOrDefault(groupKey, null);
|
||||
if (groupTargets == null || groupTargets.isEmpty()) {
|
||||
groups.remove(groupKey);
|
||||
continue;
|
||||
}
|
||||
UUID targetId = RandomUtil.randomFromCollection(groupTargets);
|
||||
if (targetId != null) {
|
||||
newPossibleTargets.add(targetId);
|
||||
groupTargets.remove(targetId);
|
||||
}
|
||||
}
|
||||
|
||||
// keep final result
|
||||
possibleTargets.clear();
|
||||
possibleTargets.addAll(newPossibleTargets);
|
||||
}
|
||||
|
||||
private String getTargetGroupKeyAsPlayer(Player player) {
|
||||
// use all
|
||||
return String.join(";", Arrays.asList(
|
||||
player.getName(),
|
||||
String.valueOf(player.getId().hashCode())
|
||||
));
|
||||
}
|
||||
|
||||
private String getTargetGroupKeyAsPermanent(Game game, Permanent permanent) {
|
||||
// split by name and stats
|
||||
// TODO: rework and combine with PermanentEvaluator (to use battlefield score)
|
||||
|
||||
// try to use short text/hash for lesser data on debug
|
||||
return String.join(";", Arrays.asList(
|
||||
permanent.getName(),
|
||||
String.valueOf(permanent.getControllerId().hashCode()),
|
||||
String.valueOf(permanent.getOwnerId().hashCode()),
|
||||
String.valueOf(permanent.isTapped()),
|
||||
String.valueOf(permanent.getPower().getValue()),
|
||||
String.valueOf(permanent.getToughness().getValue()),
|
||||
String.valueOf(permanent.getDamage()),
|
||||
String.valueOf(permanent.getCardType(game).toString().hashCode()),
|
||||
String.valueOf(permanent.getSubtype(game).toString().hashCode()),
|
||||
String.valueOf(permanent.getCounters(game).getTotalCount()),
|
||||
String.valueOf(permanent.getAbilities(game).size()),
|
||||
String.valueOf(permanent.getRules(game).toString().hashCode())
|
||||
));
|
||||
}
|
||||
|
||||
private String getTargetGroupKeyAsCard(Game game, Card card) {
|
||||
// split by name and stats
|
||||
return String.join(";", Arrays.asList(
|
||||
card.getName(),
|
||||
String.valueOf(card.getOwnerId().hashCode()),
|
||||
String.valueOf(card.getCardType(game).toString().hashCode()),
|
||||
String.valueOf(card.getSubtype(game).toString().hashCode()),
|
||||
String.valueOf(card.getCounters(game).getTotalCount()),
|
||||
String.valueOf(card.getAbilities(game).size()),
|
||||
String.valueOf(card.getRules(game).toString().hashCode())
|
||||
));
|
||||
}
|
||||
|
||||
private String getTargetGroupKeyAsOther(Game game, MageObject item) {
|
||||
// use all
|
||||
return String.join(";", Arrays.asList(
|
||||
item.getName(),
|
||||
String.valueOf(item.getId().hashCode())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug only. Print targets table and variations.
|
||||
*/
|
||||
private void printTargetsTableAndVariations(String info, Game game, Set<UUID> possibleTargets, List<TargetAmount> options, boolean isPrintOptions) {
|
||||
if (!DebugUtil.AI_SHOW_TARGET_OPTIMIZATION_LOGS) return;
|
||||
|
||||
// output example:
|
||||
//
|
||||
// Targets (after optimize): 5
|
||||
// 0. Balduvian Bears [ac8], C, BalduvianBears, DKM:22::0, 2/2
|
||||
// 1. PlayerA (SimulatedPlayer2)
|
||||
//
|
||||
// Target variations (info): 126
|
||||
// 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 1; 4 -> 1
|
||||
// 0 -> 1; 1 -> 1; 2 -> 1; 3 -> 2
|
||||
// 0 -> 1; 1 -> 1; 2 -> 1; 4 -> 2
|
||||
|
||||
// print table
|
||||
List<UUID> list = new ArrayList<>(possibleTargets);
|
||||
Collections.sort(list);
|
||||
HashMap<UUID, Integer> targetNumbers = new HashMap<>();
|
||||
System.out.println();
|
||||
System.out.println(String.format("Targets (%s): %d", info, list.size()));
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
targetNumbers.put(list.get(i), i);
|
||||
String targetName;
|
||||
Player player = game.getPlayer(list.get(i));
|
||||
if (player != null) {
|
||||
targetName = player.toString();
|
||||
} else {
|
||||
MageObject object = game.getObject(list.get(i));
|
||||
if (object != null) {
|
||||
targetName = object.toString();
|
||||
} else {
|
||||
targetName = "unknown";
|
||||
}
|
||||
}
|
||||
System.out.println(String.format("%d. %s", i, targetName));
|
||||
}
|
||||
System.out.println();
|
||||
|
||||
if (!isPrintOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// print amount variations
|
||||
List<String> res = options
|
||||
.stream()
|
||||
.map(t -> t.getTargets()
|
||||
.stream()
|
||||
.map(id -> targetNumbers.get(id) + " -> " + t.getTargetAmount(id))
|
||||
.sorted()
|
||||
.collect(Collectors.joining("; ")))
|
||||
.collect(Collectors.toList());
|
||||
Collections.sort(res);
|
||||
.collect(Collectors.joining("; "))).sorted().collect(Collectors.toList());
|
||||
System.out.println();
|
||||
System.out.println(res.stream().collect(Collectors.joining("\n")));
|
||||
System.out.println(String.format("Target variations (info): %d", options.size()));
|
||||
System.out.println(String.join("\n", res));
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
protected void addTargets(TargetAmount target, Set<UUID> possibleTargets, List<TargetAmount> options, Ability source, Game game) {
|
||||
if (!amountWasSet) {
|
||||
setAmount(source, game);
|
||||
}
|
||||
final protected void addTargets(TargetAmount target, Set<UUID> possibleTargets, List<TargetAmount> options, Ability source, Game game) {
|
||||
Set<UUID> usedTargets = new HashSet<>();
|
||||
for (UUID targetId : possibleTargets) {
|
||||
usedTargets.add(targetId);
|
||||
|
|
|
|||
|
|
@ -446,13 +446,6 @@ public abstract class TargetImpl implements Target {
|
|||
return !targets.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all possible different target combinations
|
||||
*
|
||||
* @param source
|
||||
* @param game
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<? extends TargetImpl> getTargetOptions(Ability source, Game game) {
|
||||
List<TargetImpl> options = new ArrayList<>();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import mage.abilities.costs.VariableCost;
|
|||
import mage.abilities.costs.mana.*;
|
||||
import mage.abilities.dynamicvalue.DynamicValue;
|
||||
import mage.abilities.dynamicvalue.common.SavedDamageValue;
|
||||
import mage.abilities.dynamicvalue.common.SavedDiscardValue;
|
||||
import mage.abilities.dynamicvalue.common.SavedGainedLifeValue;
|
||||
import mage.abilities.dynamicvalue.common.StaticValue;
|
||||
import mage.abilities.effects.ContinuousEffect;
|
||||
|
|
@ -967,7 +968,9 @@ public final class CardUtil {
|
|||
boolean xValue = amount.toString().equals("X");
|
||||
if (xValue) {
|
||||
sb.append("X ").append(counter.getName()).append(" counters");
|
||||
} else if (amount == SavedDamageValue.MANY || amount == SavedGainedLifeValue.MANY) {
|
||||
} else if (amount == SavedDamageValue.MANY
|
||||
|| amount == SavedGainedLifeValue.MANY
|
||||
|| amount == SavedDiscardValue.MANY) {
|
||||
sb.append("that many ").append(counter.getName()).append(" counters");
|
||||
} else {
|
||||
sb.append(counter.getDescription());
|
||||
|
|
@ -1176,7 +1179,7 @@ public final class CardUtil {
|
|||
.sum();
|
||||
int remainingValue = maxValue - selectedValue;
|
||||
Set<UUID> validTargets = new HashSet<>();
|
||||
for (UUID id: possibleTargets) {
|
||||
for (UUID id : possibleTargets) {
|
||||
MageObject mageObject = game.getObject(id);
|
||||
if (mageObject != null && valueMapper.applyAsInt(mageObject) <= remainingValue) {
|
||||
validTargets.add(id);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ public class DebugUtil {
|
|||
|
||||
public static boolean NETWORK_SHOW_CLIENT_CALLBACK_MESSAGES_LOG = false; // show all callback messages (server commands)
|
||||
|
||||
// AI
|
||||
// game simulations runs in multiple threads, if you stop code to debug then it will be terminated by timeout
|
||||
// so AI debug mode will make single simulation thread without any timeouts
|
||||
public static boolean AI_ENABLE_DEBUG_MODE = false;
|
||||
public static boolean AI_SHOW_TARGET_OPTIMIZATION_LOGS = false; // works with target amount
|
||||
|
||||
// cards basic (card panels)
|
||||
public static boolean GUI_CARD_DRAW_OUTER_BORDER = false;
|
||||
public static boolean GUI_CARD_DRAW_INNER_BORDER = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue